CSS 페인트 API

Chrome 65의 새로운 가능성

CSS Paint API('CSS 맞춤 페인트' 또는 'Houdini의 페인트 워크렛'이라고도 함)는 Chrome 65부터 기본적으로 사용 설정됩니다. 기본 설명 이걸로 무엇을 할 수 있을까요? 어떤 방식으로 작동할까요? 그럼 계속 읽어 보세요.

CSS Paint API를 사용하면 CSS 속성에서 이미지가 필요할 때마다 프로그래매틱 방식으로 이미지를 생성할 수 있습니다. background-image 또는 border-image 같은 속성은 일반적으로 이미지 파일을 로드할 때 url() 또는 linear-gradient()와 같은 CSS 기본 제공 함수와 함께 사용됩니다. 이를 사용하는 대신 이제 paint(myPainter)를 사용하여 페인트 Worklet을 참조할 수 있습니다.

페인트 Worklet 작성

myPainter라는 페인트 Worklet을 정의하려면 CSS.paintWorklet.addModule('my-paint-worklet.js')를 사용하여 CSS 페인트 Worklet 파일을 로드해야 합니다. 이 파일에서 registerPaint 함수를 사용하여 페인트 Worklet 클래스를 등록할 수 있습니다.

class MyPainter {
  paint(ctx, geometry, properties) {
    // ...
  }
}

registerPaint('myPainter', MyPainter);

paint() 콜백 내에서 <canvas>에서 알고 있는 CanvasRenderingContext2D와 동일한 방식으로 ctx를 사용할 수 있습니다. <canvas>에 그리는 방법을 알고 있다면 페인트 워크렛으로 그릴 수 있습니다. geometry는 원하는 캔버스의 너비와 높이를 알려줍니다. properties 이 문서의 뒷부분에서 설명하겠습니다.

입문용 예로 격자무늬 페인트 워크릿을 작성하여 <textarea>의 배경 이미지로 사용해 보겠습니다. 기본적으로 텍스트 영역의 크기를 조절할 수 있으므로 텍스트 영역을 사용하고 있습니다.

<!-- index.html -->
<!doctype html>
<style>
  textarea {
    background-image: paint(checkerboard);
  }
</style>
<textarea></textarea>
<script>
  CSS.paintWorklet.addModule('checkerboard.js');
</script>
// checkerboard.js
class CheckerboardPainter {
  paint(ctx, geom, properties) {
    // Use `ctx` as if it was a normal canvas
    const colors = ['red', 'green', 'blue'];
    const size = 32;
    for(let y = 0; y < geom.height/size; y++) {
      for(let x = 0; x < geom.width/size; x++) {
        const color = colors[(x + y) % colors.length];
        ctx.beginPath();
        ctx.fillStyle = color;
        ctx.rect(x * size, y * size, size, size);
        ctx.fill();
      }
    }
  }
}

// Register our class under a specific name
registerPaint('checkerboard', CheckerboardPainter);

이전에 <canvas>를 사용한 적이 있다면 다음 코드가 익숙할 것입니다. 여기에서 실시간 데모를 확인하세요.

배경 이미지에 격자무늬 패턴이 있는 텍스트 영역
배경 이미지로 격자무늬 패턴을 사용하는 텍스트 영역

여기서 일반적인 배경 이미지를 사용하는 것과 다른 점은 사용자가 텍스트 영역의 크기를 조절할 때마다 요청 시 패턴이 다시 그려진다는 것입니다. 즉, 고밀도 디스플레이 보정을 포함하여 배경 이미지는 항상 필요한 만큼 정확히 커집니다.

꽤 멋지지만 상당히 정적입니다. 동일한 패턴이지만 다른 크기의 정사각형을 사용할 때마다 새 Worklet을 작성해야 할까요? 답은 '아니요'입니다.

Worklet 매개변수화

다행히 페인트 Worklet은 추가 매개변수 properties가 적용되는 다른 CSS 속성에 액세스할 수 있습니다. 클래스에 정적 inputProperties 속성을 부여하면 맞춤 속성을 비롯한 모든 CSS 속성의 변경사항을 구독할 수 있습니다. 이 값은 properties 매개변수를 통해 제공됩니다.

<!-- index.html -->
<!doctype html>
<style>
  textarea {
    /* The paint worklet subscribes to changes of these custom properties. */
    --checkerboard-spacing: 10;
    --checkerboard-size: 32;
    background-image: paint(checkerboard);
  }
</style>
<textarea></textarea>
<script>
  CSS.paintWorklet.addModule('checkerboard.js');
</script>
// checkerboard.js
class CheckerboardPainter {
  // inputProperties returns a list of CSS properties that this paint function gets access to
  static get inputProperties() { return ['--checkerboard-spacing', '--checkerboard-size']; }

  paint(ctx, geom, properties) {
    // Paint worklet uses CSS Typed OM to model the input values.
    // As of now, they are mostly wrappers around strings,
    // but will be augmented to hold more accessible data over time.
    const size = parseInt(properties.get('--checkerboard-size').toString());
    const spacing = parseInt(properties.get('--checkerboard-spacing').toString());
    const colors = ['red', 'green', 'blue'];
    for(let y = 0; y < geom.height/size; y++) {
      for(let x = 0; x < geom.width/size; x++) {
        ctx.fillStyle = colors[(x + y) % colors.length];
        ctx.beginPath();
        ctx.rect(x*(size + spacing), y*(size + spacing), size, size);
        ctx.fill();
      }
    }
  }
}

registerPaint('checkerboard', CheckerboardPainter);

이제 서로 다른 모든 종류의 격자무늬에 동일한 코드를 사용할 수 있습니다. 하지만 더 좋은 것은 이제 DevTools로 이동하여 적절한 스타일을 찾을 때까지 값을 조정할 수 있다는 것입니다.

페인트 Worklet을 지원하지 않는 브라우저

이 글을 작성하는 시점을 기준으로 Chrome만 페인트 Worklet이 구현되어 있습니다. 다른 모든 브라우저 공급업체에서 긍정적인 신호를 보내고 있지만, 별다른 진전이 없습니다. 최신 정보를 확인하려면 Is Houdini Ready Yet?를 정기적으로 확인하세요. 그때까지는 페인트 Worklet을 지원하지 않더라도 점진적인 개선을 통해 코드를 계속 실행하세요. 예상대로 작동하도록 하려면 CSS와 JS라는 두 위치에서 코드를 조정해야 합니다.

JS에서 페인트 Worklet에 대한 지원을 감지하는 것은 CSS 객체를 확인하는 방법으로 수행할 수 있습니다. js if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('mystuff.js'); } CSS의 경우 두 가지 옵션이 있습니다. 다음과 같이 @supports를 사용할 수 있습니다.

@supports (background: paint(id)) {
  /* ... */
}

좀 더 간단한 방법은 CSS가 속성 선언을 무효화하고 알 수 없는 함수가 있는 경우 전체 속성 선언을 무시한다는 사실을 사용하는 것입니다. 먼저 페인트 Worklet을 사용하지 않고 페인트 Worklet을 사용하여 속성을 두 번 지정하면 점진적으로 개선됩니다.

textarea {
  background-image: linear-gradient(0, red, blue);
  background-image: paint(myGradient, red, blue);
}

페인트 Worklet을 지원하는 브라우저에서 background-image의 두 번째 선언이 첫 번째 선언을 덮어씁니다. 페인트 Worklet을 지원하지 않는 브라우저에서는 두 번째 선언이 유효하지 않으며 삭제되고 첫 번째 선언이 적용됩니다.

CSS 페인트 폴리필

다양한 용도에서 CSS 맞춤 페인트 및 페인트 Worklet 지원을 최신 브라우저에 추가하는 CSS 페인트 폴리필을 사용할 수도 있습니다.

사용 사례

페인트 워크릿에는 많은 사용 사례가 있으며, 그중 일부는 다른 워크릿보다 훨씬 명확합니다. 가장 명확한 방법 중 하나는 페인트 Worklet을 사용하여 DOM의 크기를 줄이는 것입니다. CSS를 사용해 장식을 만들기 위해 순전히 요소를 추가하는 경우도 있습니다. 예를 들어 Material Design Lite에서는 물결 효과가 적용된 버튼에 물결 효과 자체를 구현하는 <span> 요소가 2개 더 포함되어 있습니다. 버튼이 많으면 DOM 요소가 상당히 많아질 수 있고 이로 인해 모바일의 성능이 저하될 수 있습니다. 대신 페인트 Worklet을 사용하여 물결 효과를 구현하면 추가 요소 0개와 페인트 Worklet 하나만 갖게 됩니다. 또한 맞춤설정 및 매개변수화가 훨씬 더 쉽습니다.

페인트 Worklet을 사용하는 또 다른 장점은 대부분의 시나리오에서 페인트 Worklet을 사용하는 솔루션이 바이트 측면에서 작다는 것입니다. 물론 장단점이 있습니다. 캔버스의 크기나 매개변수가 변경될 때마다 페인트 코드가 실행됩니다. 따라서 코드가 복잡하고 시간이 오래 걸리면 버벅거림이 발생할 수 있습니다. Chrome은 장기 실행되는 페인트 Worklet도 기본 스레드의 응답에 영향을 미치지 않도록 페인트 워크릿을 기본 스레드 외부로 이동하는 작업을 하고 있습니다.

제 생각에 가장 흥미로운 전망은 페인트 Worklet을 통해 브라우저에는 아직 없는 CSS 기능의 효율적인 폴리필이 가능하다는 것입니다. 한 가지 예는 Chrome에 기본적으로 출시될 때까지 원뿔 경사를 폴리필하는 것입니다. 또 다른 예로, CSS 회의에서 여러 개의 테두리 색상을 사용할 수 있다는 것이 결정되었습니다. 이 회의가 아직 진행 중인 동안 제 동료인 Ian Kilpatrick이 페인트 Worklet을 사용하여 이 새로운 CSS 동작의 폴리필을 작성했습니다.

'틀에 박힌' 사고

대부분의 사람들은 페인트 Worklet에 관해 배울 때 배경 이미지와 테두리 이미지를 떠올리기 시작합니다. 페인트 Worklet의 덜 직관적인 사용 사례는 DOM 요소를 임의의 도형으로 만드는 mask-image입니다. 다이아몬드의 예는 다음과 같습니다.

다이아몬드 모양의 DOM 요소입니다.
다이아몬드 모양의 DOM 요소입니다.

mask-image는 요소의 크기인 이미지를 사용합니다. 마스크 이미지가 투명하고 요소가 투명한 영역입니다. 마스크 이미지가 불투명한 영역, 요소는 불투명한 영역

이제 Chrome에서 지원됩니다

페인트 Worklet은 한동안 Chrome Canary에 있었습니다. Chrome 65에서는 기본적으로 사용 설정되어 있습니다 이제 페인트 Worklet이 펼치는 새로운 가능성을 시험해 보고 여러분이 무엇을 빌드했는지 보여주세요. 더 많은 아이디어를 얻으려면 빈센트 드 올리베이라의 컬렉션을 살펴보세요.