Paint는 대상을 그리는 방법을 다루는 단계입니다.
위의 렌더링 파이프라인은 Chrome 기준이며 브라우저에 따라 차이가 있을 수 있습니다.
CSS Painting API는 Worklet의 형태로 개발자가 정의한대로 대상을 그리는 코드를 추가합니다.
위에서 언급했듯이 Worklet은 Worker의 경량 버전이라고도 합니다.
하지만 Worker와는 다르게 한 Thread에 여러개가 생성될 수 있고, 메인 Thread에서 실행될 수 있습니다.
Worklet은 독립적인 GlobalScope와 Event Loop를 가지고 있습니다.
결국, 그려야 할 대상이 많거나, 성능이 필요한 경우에 여러 개의 Thread에서 병렬로 동작할 수 있게 합니다.
그리는 부분을 독립적인 파일로 만들어서 addModule을 하는 형태입니다.
CSS.paintWorklet.addModule("slideWorklet.js")
위의 파일에 JS Class로 작성을 하며, registerPaint
를 사용하여 해당 paint를 등록합니다.
class Slide {
static get inputArguments() { return []; }
static get inputProperties() { return []; }
paint(ctx, geom, props, args){ }
}
registerPaint("slide", Slide);
paint method
에는 기본 4개의 인자가 있습니다.
PaintRenderingContext2D
객체로, 대상이 어떻게 그려질지 표현합니다.props와 args는 각각 inputProperties()
, inputArguments()
를 사용하여 입력받을 값(속성명)을 지정하여야 합니다.
기존의 슬라이드 체크박스와 Paint API를 사용한 슬라이드 체크박스를 만들어 보겠습니다.
index.html
<head>
<link rel="stylesheet" href="index.css">
<script src="index.js"></script>
</head>
<body>
<input type="checkbox" class="switch">
<label class="switch">
<input type="checkbox">
<span class="slider round"></span>
</label>
</body>
index.js
CSS.registerProperty({
name: '--slide-on',
syntax: '<number>',
inherits: true,
initialValue: "0"
});
CSS.paintWorklet.addModule("slideWorklet.js")
index.css
/* CSS Paint API */
.slide-checkbox{
background-image: paint(slide);
display: block;
color: green;
width: 60px;
height: 34px;
--slide-on : 0;
-webkit-appearance: none;
transition: --slide-on 200ms
}
.slide-checkbox:checked{
--slide-on : 1;
background-image: paint(slide);
}
/* Custom Checkbox for CSS */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .2s;
transition: .2s;
}
.slider:before {
position: absolute;
content: "";
height: 28px;
width: 28px;
left: 3px;
bottom: 3px;
background-color: white;
-webkit-transition: .2s;
transition: .2s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
slideWorklet.js
const DEG_360 = Math.PI * 2;
const FG_COLOR = "white";
const BG_COLOR = "#E1E1E1";
const BG_COLOR_ON = "#FFCD00";
const CIRCLE_MARGIN = 3;
class Slide {
static get inputProperties() {
return ['--slide-on'];
}
paint(ctx, geom, props, args){
const {width, height} = geom;
const halfOfCircleSize = height / 2;
const innerWidth = width - height;
const on = parseFloat(props.get('--slide-on')).toString();
const x = halfOfCircleSize + innerWidth * on
ctx.fillStyle = on == 1 ? BG_COLOR_ON : BG_COLOR;
ctx.beginPath();
// 양쪽에 원을 그린다.
ctx.arc(halfOfCircleSize, halfOfCircleSize, halfOfCircleSize, 0, DEG_360);
ctx.arc(width - halfOfCircleSize, halfOfCircleSize, halfOfCircleSize, 0, DEG_360);
// 가운데 사각형을 그려준다.
ctx.rect(halfOfCircleSize, 0, innerWidth, height);
ctx.fill();
// 슬라이드시 움직이는 원을 그린다.
ctx.fillStyle = FG_COLOR;
ctx.beginPath();
ctx.arc(x, halfOfCircleSize, halfOfCircleSize - CIRCLE_MARGIN, 0, DEG_360);
ctx.fill();
}
}
registerPaint("slide", Slide);
렌더링 파이프라인은 이전 단계의 결과물이 다음 단계의 입력으로 사용됩니다.
따라서 엘리먼트가 많아지게 되면 DOM 객체가 많아지고 이후 모든 과정들의 연산량과 메모리 사용량이 증가하게 됩니다.
위의 슬라이드 체크박스를 약 5,000개를 만들었을 경우, 5000 * 3개의 엘리먼트가 5000개의 엘리먼트로 대체됩니다.
CSS Layout API를 사용하면 CSS가 적용된 대상의 자식 엘리먼트들의 배치를 정의할 수 있습니다.
.cloud{
display: layout(cloud);
}
렌더링 파이프라인 중 Layout 단계는 대상을 배치하는 방법을 다룹니다.
CSS Layout API는 Worklet의 형태로 자식 엘리먼트를 개발자가 정의한대로 배치하는 코드를 추가합니다.
기존의 방법은, 렌더링이 끝난 후, JS로 재배치하면 렌더링 파이프라인을 한번 더 수행합니다.
CSS.layoutWorklet.addModule('cloudLayout.js');
class CloudLayout {
static get inputProperties() {return [];}
*intrinsicSizes(children, edges, styleMap) {}
*layout(children, edges, constraints, styleMap){}
}
registerLayout("cloud", CloudLayout);
paint method에는 기본 4가지의 인자가 있습니다.
paint API와 동일하게 대상 엘리먼트에 적용된 스타일 정보를 읽어올 수 있습니다.
마찬가지로 static inputProperties
필드로 읽어오려는 속성의 이름을 미리 선언해야합니다.
layout() 함수는 자식요소들의 비동기 처리를 위해서 제네레이터 함수로 작성해야 합니다.
const childFragments = yield children.map(child => {
return child.layoutNextFragment()
})
각각 자식요소에서 layoutNextFragment() 함수를 호출해서 자식요소의 크기를 알 수 있습니다.
layoutNextFragment 함수는 LayoutFragment 객체를 반환합니다.
LayoutFragment 객체는 4가지의 정보를 가지고 있습니다.
blockSize, inlineSize는 fragment의 크기를 나타냅니다.
blockOffset, inlineOffset은 fragment가 원점으로 부터 떨어진 거리를 나타냅니다.
Parent Div의 중심을 기준으로 단어 랜덤배치를 해보겠습니다.
index.html
<head>
<link rel="stylesheet" href="index.css">
<script src="index.js"></script>
</head>
<body>
<div class="cloud">
<div class="child">Websquare sp2</div>
<div class="child">Websquare sp3</div>
<div class="child">Websquare sp4</div>
<div class="child">Websquare sp5</div>
<div class="child">Inswave</div>
<div class="child">IWD</div>
<div class="child">Websquare</div>
<div class="child">Websquare</div>
<div class="child">Websquare</div>
</div>
</body>
index.js
CSS.layoutWorklet.addModule('cloudLayout.js');
index.css
.cloud{
display: layout(cloud);
--random-seed: 30;
width : 600px;
height : 600px;
border-radius: 25px;
}
.child{
font-size: 24px;
font-weight: blod;
color: #579DDC;
text-shadow: 0px 2px 2px rgba(0,0,0,0.3);
}
cloudLayout.js
class CloudLayout {
static get inputProperties() {
return ["--random-seed"];
}
*intrinsicSizes(children, edges, styleMap) {
}
*layout(children, edges, constraints, styleMap){
const childFragments = yield children.map(child => {
return child.layoutNextFragment()
})
const availableInlineSize = constraints.fixedInlineSize;
const availableBlockSize = constraints.fixedBlockSize;
const randomSeed = parseInt(styleMap.get("--random-seed"));
let seed = randomSeed;
const random = () => {
let x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}
for (const fragment of childFragments) {
fragment.blockOffset = random() * availableBlockSize;
fragment.inlineOffset = random() * availableInlineSize;
}
return{
childFragments
}
}
}
registerLayout("cloud", CloudLayout);
CSS에는 CSSOM이 있습니다.
CSSOM은 JavaScript에서 CSS를 조작할 수 있게 해주는 API입니다. CSSOM은 웹 페이지에서 발견되는 CSS 스타일의 기본 '맵'으로, DOM과 결합된 CSSOM은 브라우저에서 웹 페이지를 표현하는데 사용됩니다.
JavaScript에서 .style을 read 또는 set할 때 항상 아래와 같이 사용해왔습니다.
// 요소의 스타일
el.style.opacity = 0.3;
typeof el.style.opacity === 'string' // true
// 스타일시트 규칙
document.styleSheets[0].cssRules[0].style.opacity = 0.3;
새로 나온 CSS Typed Object Model(Typed OM)은 CSS 값에 타입과 메소드, 적절한 객체모델을 추가함으로써 세계관을 넓혔습니다.
값이 문자열이 아닌 JavaScript 객체로 나타나기 때문에 CSS를 효율적으로(정상적으로) 조작할 수 있습니다.
기존의 사용하던 element.style 대신, .attributeStyleMap
속성을 사용하여 스타일에 접근할 수 있습니다.
스타일시트 규칙에는 .styleMap
속성을 사용합니다.
두 속성 모두 StylePropertyMap 객체를 반환합니다.
// 요소의 스타일
el.attributeStyleMap.set('opacity', 0.3);
typeof el.attributeStyleMap.get('opacity').value === 'number' // true
// 스타일시트 규칙
const stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].styleMap.set('background', 'blue');
StylePropertyMap은 Map과 유사한 객체이기 때문에, 일반적인 함수(get/set/keys/values/entries)를 전부 지원합니다. 따라서 아래와 같이 유연하게 작업할 수 있습니다.
// 아래 3가지가 모두 동일하다.
el.attributeStyleMap.set('opacity', 0.3);
el.attributeStyleMap.set('opacity', '0.3');
el.attributeStyleMap.set('opacity', CSS.number(0.3)); // 'Unit values' 파트 참고
// el.attributeStyleMap.get('opacity').value === 0.3
// StylePropertyMaps은 반복 가능하다.
for (const [prop, val] of el.attributeStyleMap) {
console.log(prop, val.value);
} // → opacity, 0.3
el.attributeStyleMap.has('opacity') // true
el.attributeStyleMap.delete('opacity') // opacity 제거
el.attributeStyleMap.clear(); // 모든 스타일 제거
두 번째 예에서 opacity를 문자열 '0.3'으로 set 했지만 속성을 read 할 때는 숫자로 읽힌다는 것을 명심하세요.
주어진 CSS 속성이 숫자를 지원한다면, Typed OM은 문자열 값을 입력하더라도 항상 숫자값을 반환합니다!
CSS Typed OM이 해결하려는 문제가 무엇일까요? CSS Typed OM이 이전의 Object Model보다 훨씬 장황하다고 주장할 수도 있습니다.
Typed OM을 작성하기 전에 아래의 몇 가지 주요 특징을 고려하세요.
el.style.opacity += 0.1;
el.style.opacity === '0.30.1' // CSSOM은 문자열로 붙는다!
컴퓨터 그래픽에서 '클램핑'이란, 어떤 위치를 범위 안으로 한정시키는 방법입니다. 위치를 제일 가까운 사용 가능한 값으로 옮깁니다.
초기 CSS 벤치마크에서 Typed OM이 기존의 CSSOM을 사용할 때보다 초당 작동 속도가 30%까지 빠르다는 것을 입증됐습니다. 이는
requestionAnimationFrame()
를 사용하여 빠른 CSS 애니메이션을 구현할 때 중요합니다.
Global style