์๋ฌธ: https://www.webtips.dev/how-to-make-dynamic-backgrounds-with-the-css-paint-api
Unsplash์ Paweล Czerwiลski์ ์ฌ์ง.
ํ๋์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง์ ์ด๋ฏธ์ง๊ฐ ์ฌ์ฉ๋๋ฉฐ ๋ค์ด๋ก๋๋๋ ์ฉ๋์ ๋๋ถ๋ถ์ ์ฐจ์งํ๋ค. ์ด๋ฅผ ์ต์ ํํ๋ฉด ์นํ์ด์ง์ ์ฑ๋ฅ์ ํจ๊ณผ์ ์ผ๋ก ํฅ์์ํฌ ์ ์๋ค. ๊ธฐํํ์ ๋ชจ์์ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, CSS Paint API๋ฅผ ์ฌ์ฉํ์ฌ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ์ด๋ฅผ ๋์ฒดํ ๊ฒฝ์ฐ ํจ๊ณผ์ ์ผ๋ก ์นํ์ด์ง์ ์ฑ๋ฅ์ ํฅ์์ํฌ ์ ์๋ค.
์ด ํํ ๋ฆฌ์ผ์์ CSS Paint API์ ๊ธฐ๋ฅ์ ์ดํด๋ณด๊ณ , ํด์๋ ์ํฅ์ ๋ฐ์ง ์๋ ๋์ ๋ฐฐ๊ฒฝ์ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์์๋ณด์. ๋จผ์ ํํ ๋ฆฌ์ผ ๊ฒฐ๊ณผ๋ฅผ ์ดํด๋ณด์.
๋จผ์ ์ index.html
ํ์ผ์ ์์ฑํ๊ณ ๋ค์ ์ฝ๋๋ก ์ฑ์๋ณด์.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>๐จ CSS Paint API</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<textarea class="pattern"></textarea>
<script>
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('pattern.js');
}
</script>
</body>
</html>
๋ค์ ๋ช ๊ฐ์ง ์ฌํญ์ ์ ์ํด์ผ ํ๋ค.
paintWorklet
์ ๋ก๋ํ๋ค. ๊ธ๋ก๋ฒ ๋ธ๋ผ์ฐ์ ์ง์์จ์ ํ์ฌ 63%์ด๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฐ๋๋ ํ๊ฒฝ์์ ๋จผ์ paintWorklet
์ด ์ง์๋๋์ง๋ถํฐ ํ์ธํด์ผ ํ๋ค.textarea
์ ์บ๋ฒ์ค๋ก ์ฌ์ฉํ๊ณ ์์ผ๋ฏ๋ก, textarea
์ ํฌ๊ธฐ๋ฅผ ์กฐ์ ํ์ฌ ํจํด์ด ์ด๋ป๊ฒ ๋ค์ ๊ทธ๋ ค์ง๋์ง ํ์ธํ ์ ์๋ค.pattern.js
์ ๋ช ๊ฐ์ง ์คํ์ผ์ ์ ์ํ ์ ์๋ styles.css
๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค.paintWorklet์ ๊ทธ๋ ค์ผ ํ๋ ๊ฒ์ ์ ์ํ๋ ํด๋์ค๋ก์ ์บ๋ฒ์ค ์์์ ์ ์ฌํ๊ฒ ๋์ํ๋ค. ๋ง์ฝ ์บ๋ฒ์ค ์์์ ๋ํด ์ด๋ฏธ ์๊ณ ์๋ค๋ฉด ์ฝ๋๋ ์ต์ํด ๋ณด์ผ ๊ฒ์ด๋ค. ํ์ง๋ง 100% ๋์ผํ์ง๋ ์๋ค. ์๋ฅผ ๋ค๋ฉด, ํ ์คํธ ๋๋๋ง์ ์์ง paintWorklet์์ ์ง์ํ์ง ์๋๋ค.
์ด์ CSS ์คํ์ผ์ ์ ์ํด๋ณด์. CSS์์ paintWorklet์ ์ฌ์ฉ์ ์ง์ ํ ์ ์๋ค.
.pattern {
width: 250px;
height: 250px;
border: 1px solid #000;
background-image: paint(pattern);
}
ํ
์คํธ ์์ญ์ ๋ ์ ํ์ธํ ์ ์๋๋ก ๊ฒ์์ ํ
๋๋ฆฌ๋ฅผ ์ถ๊ฐํ๋ค.
paintWorklet
์์
๋ฌผ์ ์ฐธ์กฐํ๋ ค๋ฉด paint(paintWorklet ์ด๋ฆ)
๋ฅผ background-image
์์ฑ ๊ฐ์ผ๋ก ์ ๋ฌํด์ผ ํ๋ค.pattern
์ ๋ค์ ๋จ๊ณ์์ ์ดํด๋ณด์. ์์ง ์ ์ํ์ง ์์์ผ๋ ๋ค์ ๋จ๊ณ์์ ์ดํด๋ณด์.
pattern.js
ํ์ผ์ ๋ง๋ค๊ณ ๋ค์ ๋ด์ฉ์ ์ถ๊ฐํ์.
class Pattern {
paint(context, canvas, properties) {
}
}
registerPaint('pattern', Pattern);
registerPaint
๋ฉ์๋๋ก paintWorklet
์ ๋ฑ๋กํ ์ ์๋ค. ์ฒซ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ CSS์์ ์ฐธ์กฐํ๊ธฐ ์ํ ๋ค์ด๋ฐ์ด๊ณ , ๋ ๋ฒ์งธ ๋งค๊ฐ๋ณ์๋ ์บ๋ฒ์ค์ ๊ทธ๋ฆด ๊ฒ์ ์ ์ํ๋ ํด๋์ค์ด๋ค.์ด ํด๋์ค๋ ์ธ ๊ฐ์ง ๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํ๋๋ paint
๋ฉ์๋๋ฅผ ๊ฐ์ง๋ค.
context
- CanvasRenderingContext2D
API์ ํ์ ์งํฉ์ ๊ตฌํํ๋ PaintRenderingContext2D
๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.canvas
- ๋๋น์ ๋์ด์ ๋ ๊ฐ์ง ์์ฑ๋ง ๊ฐ๋ PaintSize
๊ฐ์ฒด๋ค.properties
- CSS ์์ฑ๊ณผ ๊ฐ์ ์ฝ๋ ๋ฐ ์ฌ์ฉ๋๋ StylePropertyMapReadOnly
๊ฐ์ฒด๊ฐ ๋ฐํ๋๋ค.์ด์ , ์ง์ฌ๊ฐํ์ ๊ทธ๋ ค๋ณด์. paint
๋ฉ์๋์ ๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํ๋ค.
paint(context, canvas, properties) {
for (let x = 0; x < canvas.height / 20; x++) {
for (let y = 0; y < canvas.width / 20; y++) {
const bgColor = (x + y) % 2 === 0 ? '#FFF' : '#FFCC00';
context.shadowColor = '#212121';
context.shadowBlur = 10;
context.shadowOffsetX = 10;
context.shadowOffsetY = 1;
context.beginPath();
context.fillStyle = bgColor;
context.rect(x * 20, y * 20, 20, 20);
context.fill();
}
}
}
์ฌ๊ธฐ์๋ ์ค์ฒฉ ๋ฃจํ๋ฅผ ๋ง๋ค์ด ์บ๋ฒ์ค์ ํญ๊ณผ ๋์ด๋ฅผ ์ด์ฉํ ๊ณ์ฐ์ ํ๋ค. ์ง์ฌ๊ฐํ์ ํฌ๊ธฐ๊ฐ 20์ด๋ฏ๋ก ๋์ด์ ํญ์ ๋ชจ๋ 20์ผ๋ก ๋๋๋ค.
๋ค ๋ฒ์งธ ๋ผ์ธ์์ ๋๋จธ์ง ์ฐ์ฐ์๋ฅผ ์ด์ฉํ์ฌ ๋ ์์ ์ฌ์ด๋ฅผ ์ ํํ๋ค. ๊น์ด ํํ์ ์ํด shadow์์ฑ์ ๋ช ๊ฐ ์ถ๊ฐํ๋ค. ๋ง์ง๋ง์ผ๋ก ์ง์ฌ๊ฐํ์ ๊ทธ๋ฆฐ๋ค. ๋ธ๋ผ์ฐ์ ์์ ํ์ผ์ ์ด๋ฉด ์๋์ ๊ฐ์ด ๋ณด์ฌ์ผ ํ๋ค.
textarea
์ ํฌ๊ธฐ๋ฅผ ์กฐ์ ํ ๋ Paint API๊ฐ ๋ค์ ๊ทธ๋ฆฌ๋ ๊ฑธ ์ฟ๋ณผ ์ ์์ง๋ง, ์ํ๊น๊ฒ๋ ๊ทธ๋๋ ์ฌ์ ํ ์ ์ ์ด๋ค.
์ฐ๋ฆฌ๊ฐ ๋ณ๊ฒฝ ๊ฐ๋ฅํ ์ฌ์ฉ์ ์ ์ CSS์์ฑ์ ์ถ๊ฐํ์ฌ ์ข ๋ ๋์ ์ธ ๋ฐฐ๊ฒฝ์ ๋ง๋ค์ด ๋ณด์.
styles.css
ํ์ผ์ ์ด๊ณ ์๋ ์ฝ๋๋ฅผ ์ถ๊ฐํ์.
.pattern {
width: 250px;
height: 250px;
border: 1px solid #000;
background-image: paint(pattern);
+ --pattern-color: #FFCC00;
+ --pattern-size: 23;
+ --pattern-spacing: 0;
+ --pattern-shadow-blur: 10;
+ --pattern-shadow-x: 10;
+ --pattern-shadow-y: 1;
}
์ฌ์ฉ์ ์ ์ CSS ์์ฑ์ --
๋ก ์ ๋์ฌ๋ฅผ ๋ถ์ฌ ์ ์ํ ์ ์๋ค. ๋ณดํต์ var()
ํจ์๋ฅผ ์ด์ฉํ์ฌ ์ฌ์ฉํ ์ ์๋ค. ํ์ง๋ง ์ฐ๋ฆฌ๋ paintWorklet์์ ์ฌ์ฉํ ๊ฒ์ด๋ค.
CSS๋ฅผ ํตํด Paint API๊ฐ ์ง์๋๋์ง ํ์ธํ ์๋ ์๋ค. ์ด๋ฅผ ์ํ ๋ ๊ฐ์ง ์ต์ ์ด ์๋ค.
@supports
๋ฅผ ์ด์ฉํ์ฌ ๊ท์น์ ๋ณดํธ./* ์ฒซ๋ฒ์งธ ์ต์
*/
@supports (background: paint(pattern)) {
/**
* ์ด ๋ถ๋ถ์ด ์คํ๋๋ฉด Paint API๊ฐ ์ง์๋จ์ ์๋ฏธํ๋ค
**/
}
/**
* ๋๋ฒ์งธ ์ต์
* Paint API๊ฐ ์ง์๋๋ ๊ฒฝ์ฐ ํ์์ ๊ท์น์ผ๋ก ์ฌ์ ์ ๋๋ค
* ๊ทธ๋ ์ง ์์ ๊ฒฝ์ฐ url()์ด ์ ์ฉ๋๋ค
**/
.pattern {
background-image: url(pattern.png);
background-image: paint(pattern);
}
pattern.js
๋ด์์ ๋งค๊ฐ ๋ณ์๋ฅผ ์ฝ์ผ๋ ค๋ฉด ์๋์ฒ๋ผ paintWorklet
์ ์ ์ํ๋ ํด๋์ค์ ์ ๋ฉ์๋๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค.
class Pattern {
// paintWorklet์ `inputProperties` ๋ฉ์๋๊ฐ ๋ฐํํ๋ ๋ชจ๋ ํญ๋ชฉ์ ์ ๊ทผํ ์ ์๋ค.
static get inputProperties() {
return [
'--pattern-color',
'--pattern-size',
'--pattern-spacing',
'--pattern-shadow-blur',
'--pattern-shadow-x',
'--pattern-shadow-y'
];
}
}
paint
๋ฉ์๋ ๋ด์์ properties.get
๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑ์ ์ก์ธ์ค ํ ์ ์๋ค.
paint(context, canvas, properties) {
const props = {
color: properties.get('--pattern-color').toString().trim(),
size: parseInt(properties.get('--pattern-size').toString()),
spacing: parseInt(properties.get('--pattern-spacing').toString()),
shadow: {
blur: parseInt(properties.get('--pattern-shadow-blur').toString()),
x: parseInt(properties.get('--pattern-shadow-x').toString()),
y: parseInt(properties.get('--pattern-shadow-y').toString())
}
};
}
properties.get
์ด CSSUnparsedValue
๋ฅผ ๋ฐํํ๊ธฐ ๋๋ฌธ์, ์์์ ๋ฌธ์์ด๋ก ๋ณํํด์ผ ํ๋ฉฐ ๋๋จธ์ง๋ ๋ชจ๋ ์ซ์๋ก ๋ณํํด์ผ ํ๋ค.
์ข ๋ ์ฝํ๊ธฐ ์ฌ์ด ์ฝ๋๋ฅผ ๋ง๋ค๊ธฐ ์ํด ํ์ฑ์ ์ฒ๋ฆฌํ๋ ๋ ๊ฐ์ง ์๋ก์ด ํจ์๋ฅผ ๋ง๋ค์๋ค.
paint(context, canvas, properties) {
const getPropertyAsString = property => properties.get(property).toString().trim();
const getPropertyAsNumber = property => parseInt(properties.get(property).toString());
const props = {
color: getPropertyAsString('--pattern-color'),
size: getPropertyAsNumber('--pattern-size'),
spacing: getPropertyAsNumber('--pattern-spacing'),
shadow: {
blur: getPropertyAsNumber('--pattern-shadow-blur'),
x: getPropertyAsNumber('--pattern-shadow-x'),
y: getPropertyAsNumber('--pattern-shadow-y')
}
};
}
์ด์ for ๋ฃจํ์ ์๋ ๋ชจ๋ ์์ฑ ๊ฐ์ ํด๋นํ๋ prop
๊ฐ์ผ๋ก ๋ฐ๊พธ๊ธฐ๋ง ํ๋ฉด ๋๋ค.
for (let x = 0; x < canvas.height / props.size; x++) {
for (let y = 0; y < canvas.width / props.size; y++) {
const bgColor = (x + y) % 2 === 0 ? '#FFF' : props.color;
context.shadowColor = '#212121';
context.shadowBlur = props.shadow.blur;
context.shadowOffsetX = props.shadow.x;
context.shadowOffsetY = props.shadow.y;
context.beginPath();
context.fillStyle = bgColor;
context.rect(x * (props.size + props.spacing),
y * (props.size + props.spacing), props.size, props.size);
context.fill();
}
}
์ด์ ๋ธ๋ผ์ฐ์ ๋ก ๋์๊ฐ ์์ฑ์ ๋ณ๊ฒฝํด๋ณด์.
๊ฐ๋ฐ์ ๋๊ตฌ๋ฅผ ํตํ ๋ฐฐ๊ฒฝ ํธ์ง
CSS Paint API๊ฐ ์ ์ ์ฉํ ๊น? ์ฌ์ฉ ์ฌ๋ก๊ฐ ์์๊น?
๋ถ๋ช ํ ๊ฒ์ API์ ๋์์ผ๋ก ์๋ต์ ํฌ๊ธฐ๋ฅผ ์ค์ธ๋ค๋ ๊ฒ์ด๋ค. ์ด๋ฏธ์ง ์ฌ์ฉ์ ์ ๊ฑฐํจ์ผ๋ก์จ ํ๋์ ๋คํธ์ํฌ ์์ฒญ๊ณผ ๋ช ํฌ๋ก๋ฐ์ดํธ์ ์ฉ๋์ ์ ์ฝํ์ฌ ์ฑ๋ฅ์ ํฅ์์ํฌ ์ ์๋ค.
๋ค๋ฅธ ์๋ก, DOM ์์๋ฅผ ์ฌ์ฉํ๋ ๋ณต์กํ CSSํจ๊ณผ์ ๊ฒฝ์ฐ ํ์ด์ง์ ๋ ธ๋ ์๋ ์ค์ผ ์ ์๋ค. Paint API๋ฅผ ์ฌ์ฉํ์ฌ ๋ณต์กํ ์ ๋๋ฉ์ด์ ์ ๋ง๋ค ์ ์์ผ๋ฏ๋ก ๋น์ด์๋ ๋ ธ๋๋ฅผ ์ถ๊ฐํ ํ์๊ฐ ์๋ค.
ํ์๊ฐ ์๊ฐํ๋ ๊ฐ์ฅ ํฐ ์ด์ ์ ์ ์ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง๋ณด๋ค ํจ์ฌ ๋ ๋ง์ ์ฌ์ฉ์ ์ ์๊ฐ ๊ฐ๋ฅํ๋ค๋ ๊ฒ์ด๋ค. API๋ ํด์๋์ ์ํฅ์ด ์๋ ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๋ฏ๋ก ํน์ ํ ํ๋ฉด ํฌ๊ธฐ๋ฅผ ๋์น๋ ๊ฒ์ ๋ํด ๊ฑฑ์ ํ ํ์๊ฐ ์๋ค.
CSS Paint API๋ ์์ง ๋ธ๋ผ์ฐ์ ์ง์๋ฅ ์ด ๋์ง ์์ผ๋ฏ๋ก ์ ์ฉ ์ ์ polyfill ์ฌ์ฉ์ ๊ณ ๋ คํด์ผ ํ๋ค. ํํ ๋ฆฌ์ผ์ ์์ฑ๋ ํ๋ก์ ํธ๋ฅผ ๋ณด๋ ค๋ฉด ์ด ๊นํ ์ ์ฅ์๋ฅผ ์ฐธ๊ณ ํ๊ธฐ ๋ฐ๋๋ค.
์ด ๊ธ์ ์ฝ์ด์ค์ ๊ฐ์ฌํ๋ค. ์ฆ๊ฑฐ์ด ์ฝ๋ฉํ๊ธธ ๋ฐ๋๋ค!