CSS 페인트 API

Chrome 65의 새로운 가능성

CSS Paint API('CSS Custom Paint' 또는 'Houdini의 페인트 워크렛'이라고도 함)는 Chrome 65부터 기본적으로 사용 설정됩니다. 기본 설명 이 기능으로 할 수 있는 작업 어떻게 작동하나요? 그럼 계속 읽어 보세요.

CSS Paint API를 사용하면 CSS 속성이 이미지를 예상할 때마다 프로그래매틱 방식으로 이미지를 생성할 수 있습니다. background-image 또는 border-image와 같은 속성은 일반적으로 url()와 함께 이미지 파일을 로드하거나 linear-gradient()와 같은 CSS 내장 함수와 함께 사용됩니다. 이제 이를 사용하는 대신 paint(myPainter)를 사용하여 페인트 워크렛을 참조할 수 있습니다.

페인트 워크렛 작성

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

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

registerPaint('myPainter', MyPainter);

paint() 콜백 내에서 ctx<canvas>에서 CanvasRenderingContext2D를 사용하는 것과 동일한 방식으로 사용할 수 있습니다. <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>를 사용한 적이 있다면 이 코드가 익숙할 것입니다. 여기에서 라이브 데모를 확인하세요.

체커보드 패턴을 배경 이미지로 사용하는 Textarea
체크보드 패턴을 배경 이미지로 사용하는 Textarea

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

멋지긴 하지만 정적입니다. 크기가 다른 정사각형으로 동일한 패턴을 만들고 싶을 때마다 새 워크렛을 작성해야 하나요? 답은 '아니요'입니다.

Worklet의 매개변수화

다행히 페인트 워크렛은 다른 CSS 속성에 액세스할 수 있습니다. 여기에서 추가 매개변수 properties가 사용됩니다. 클래스에 정적 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이 구현되어 있습니다. 다른 모든 브라우저 공급업체에서도 긍정적인 신호이지만 진전이 없습니다. 최신 정보를 확인하려면 Houdini 준비 상태를 정기적으로 확인하세요. 그동안은 페인트 워크렛이 지원되지 않더라도 점진적 개선을 사용하여 코드가 계속 실행되도록 하세요. 예상대로 작동하도록 하려면 CSS와 JS의 두 위치에서 코드를 조정해야 합니다.

JS에서 페인트 워크렛 지원을 감지하려면 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);
}

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

CSS 페인트 폴리필

많은 용도의 경우 최신 브라우저에 CSS 맞춤 페인트 및 페인트 워크렛 지원을 추가하는 CSS 페인트 폴리필을 사용할 수도 있습니다.

사용 사례

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

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

가장 흥미로운 점은 페인트 워크렛을 사용하면 브라우저에 아직 없는 CSS 기능을 효율적으로 폴리필할 수 있다는 것입니다. 예를 들어 원뿔형 그라데이션이 Chrome에 기본적으로 제공될 때까지 이를 폴리필하는 경우가 있습니다. 또 다른 예: CSS 회의에서 이제 여러 개의 테두리 색상을 사용할 수 있다고 결정되었습니다. 회의가 진행되는 동안 동료인 이안 킬패트릭이 페인트 워크렛을 사용하여 이 새로운 CSS 동작에 관한 폴리필을 작성했습니다.

'틀을 벗어나 생각하기'

대부분의 사람들은 페인트 워크렛에 관해 배울 때 배경 이미지와 테두리 이미지를 생각하기 시작합니다. 페인트 Worklet의 덜 직관적인 사용 사례는 DOM 요소가 임의의 도형을 갖도록 하는 mask-image입니다. 예를 들어 다이아몬드:

마름모꼴의 DOM 요소입니다.
마름모꼴 모양의 DOM 요소입니다.

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

Chrome에서 사용 가능

페인트 worklet은 한동안 Chrome Canary에 사용되었습니다. Chrome 65에서는 기본적으로 사용 설정되어 있습니다. 페인트 워크렛이 제공하는 새로운 가능성을 사용해 보고 빌드한 내용을 보여주세요. 더 많은 아이디어를 얻으려면 Vincent De Oliveira의 컬렉션을 살펴보세요.