[chrome] CSS Paint API in 65 #1 / 2

개요top

어느 새 GDG korea의 webtech파트의 운영진이 되었습니다(경축!)

이제부터는 원래 관심 많던 크롬의 기능에 대해 다루는 코너를 신설하여 공유할까 합니다.

18년 5월에 출시된 크롬65에는 3월에 초안이 제출된 CSS Paint API가 탑재되어있습니다.

사실 CSS Paint API는 독립적인 스펙이라기보단 2016년도에 이미 시작된 프로젝트 후디니(Houdini)의 일부입니다.

여기에는 많은 하부 항목이 포함되어 있는데 크롬에서는 이미 Typed OM(66)과 PaintAPI(65)에 반영되었고 LayoutAPI, ValuesAPI, AnimationWorklet등도 카나리에는 반영되어있습니다(다른 제조사들은 먼산..)

후디니에서 CSS기능 중 일부를 자바스크립트에 연결할 수 있으며, 이렇게 연결된 스크립트는 비동기적으로 워커쓰레드에서 작동하도록 구성되어있습니다. 백그라운드 쓰레드에서 CSS를 지원하는 스크립트를 구동 시키는 식입니다.
이렇게 CSS의 특정 기능을 위임하여 워커쓰레드에게 넘긴 스크립트를 worklet이라고 부르며 각 주제별로 worklet을 만들어 사용하는 게 스펙의 주된 내용입니다.

후디니의 하부 스펙 중 가장 먼저 크롬에 반영된 Paint API를 자세히 살펴보겠습니다.

PaintWorklettop

이 스펙은 background등의 프레그먼트 작업을 처리하기 위한 스크립트를 연결합니다. 따라서 직접 색칠할 스크립트를 관리해야 하는데 이 역할을 PaintWorklet이 해줍니다. 간단히 하트를 채우는 예제를 보죠.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>PaintAPI 1</title>
  <style>
    body{margin:0;padding:0}
    .bg{height:300px;background: black}
    .test{width:50%;height:500px;background:paint(heart)}
  </style>
</head>
<body>
  <div class="bg">
    <div class="test"></div>
  </div>
<script>
CSS.paintWorklet.addModule('heart.js');
</script>
</body>
</html>

위 예제에서 test클래스를 부여한 div는 paint(heart)로 채워지는데, 이때 heart는 paintWorklet에 등록된 모듈의 이름입니다(파일이름이 아니라!)
하단 스크립트에서는 paintWorklet에 heart.js를 모듈로 등록하려고 합니다.

//heart.js
const Heart = class{
  static get contextOptions(){ return {alpha: true};}
  constructor(){
    this.path = [
     [75, 37, 70, 25, 50, 25],
     [20, 25, 20, 62.5, 20, 62.5],
     [20, 80, 40, 102, 75, 120],
     [110, 102, 130, 80, 130, 62.5],
     [130, 62.5, 130, 25, 100, 25],
     [85, 25, 75, 37, 75, 40]
    ];
  }
  paint(ctx, geom){
    ctx.fillStyle = 'rgba(255,0,0,0.5)';
    for(let y = 0; y < geom.height; y+=60){
      for(let x = 0; x < geom.width; x+=60){
        ctx.save();
        ctx.translate(x, y);
        ctx.scale(0.5, 0.5)
        ctx.beginPath();
        ctx.moveTo(75, 40);
        this.path.forEach(v=>ctx.bezierCurveTo(...v));
        ctx.fill();
        ctx.restore();
      }
    }
    console.log("painted");
  }
};
registerPaint('heart', Heart);

위 js에서 registerPaint라는 전역함수는 통상적으로는 존재하지 않지만 paintWorklet에 의해 워커쓰레드로 로딩된 경우에만 존재하게 되는 함수입니다.
백그라운드에서 js가 실행될 때는 전역 객체로 PaintWorkletGlobalScope가 설정되는데 여기에 registerPaint가 정의되어있습니다.
이 함수를 이용해 실제 그림 그릴 클래스를 등록합니다. 이 때 등록된 이름이 ‘heart’이므로 css에서 paint(heart)가 작동하게 되는 식입니다.

이렇게 화면에 표시된 결과물은 다음과 같습니다.

테스트하려면 클릭!

이 샘플은 가로의 넓이를 50%로 줬으므로 브라우저의 크기를 변경해보면 그때마다 콘솔에 painted가 찍히는 걸 볼 수 있습니다. 즉 reflow상황마다 재호출되는 것이죠.

Paint 클래스 정의top

여기 등록된 클래스는 paint definition규격을 따르는데 이 규격에 따라 다음의 메소드를 사용할 수 있습니다.

  1. static get contextOptions – 다양한 캔버스 컨텍스트의 설정을 줄 수 있습니다.
  2. static get inputProperties – 연동할 css변수를 기술합니다.
  3. static get inputArguments – 호출 시 추가로 전달할 인자를 기술합니다.
  4. paint(ctx, size, styleMap, arguments) – 실제 그림을 그릴 로직을 기술합니다.

이걸 실제 클래스로 스케치해보면 다음과 같습니다.

const Paint = class{
  static get contextOptions(){ return {alpha: true};}
  static get inputProperties() { return ['--foo']; }
  static get inputArguments() { return ['<color>']; }
  paint(ctx, geom, properties, arguments){}
};

각각을 자세히 파고 들어보죠.

static get contextOptions

canvas 2d context에 대한 기본 설정을 반환할 수 있습니다. 하지만 현재 공식적으로 2d에서 지원되는 옵션은 alpha뿐입니다.
따라서 그림을 칠할 때 알파속성을 주고 싶다면 {alpha:true}를 반환해주면 됩니다.

static get inputProperties

여기서 property는 css의 변수를 의미하는 것입니다. 이미 크롬에는 css4규격의 css변수를 지원하고 있으므로 paint를 호출한 css영역 내에 선언된 변수를 참조할 수 있습니다. 어떤 변수와 연동할 지 기술해두면 paint의 인자에 키, 값 쌍으로 들어오게 됩니다.
예를 들어 heart의 색상은 현재 반투명한 빨강이지만 외부에서 받아들이고 싶다면 다음과 같이 html의 style부분에 변수를 선언해볼 수 있을 것입니다.

<style>
  body{margin:0;padding:0}
  .bg{height:300px;background: black}
  .test{
    --color:blue;
    width:50%;height:500px;
    background:paint(heart)
  }
</style>

이제 heart모듈에서는 –color속성을 인식할 수 있어야 하니 다음과 같이 수정해야 합니다.

const Heart = class{
  static get contextOptions(){ return {alpha: true};}

  //연동하고 싶은 속성 값을 순서와 무관하게 배열에 문자열로 기술한다.
  static get inputProperties(){return ['--color'];}

  constructor(){
    this.path = [...];
  }
  paint(ctx, geom, props){

    //컬러를 props의 get을 이용해 얻어온다.
    ctx.fillStyle = props.get('--color');

    for(let y = 0; y < geom.height; y+=60){
      for(let x = 0; x < geom.width; x+=60){
        ...
      }
    }
  }
};
registerPaint('heart', Heart);

앞의 코드와 비교하여 크게 달라지는 점은 inputProperties를 구상하여 연동할 –color속성을 가져오라고 사전에 설정한 것과, 실제 이 값을 paint에서 사용하여 하트의 색상을 결정하는 점입니다. 이제 하트는 파란색으로 표시됩니다.

테스트하려면 클릭!

이제 paint로직은 고정한 채 css속성으로 통해 값을 보낼 수 있게 되었습니다.

static get inputArguments

이 기능은 보다 찰지게 paint(heart, blue) 처럼 아예 paint를 호출하는 시점에 인자를 넘기고 이를 paint의 마지막 인자에서 받아들일 때 해당 인자가 어떤 타입의 값이 올지 정해주는 역할을 합니다. 이를 실제 코드화해보면 다음과 같습니다.

static get inputArguments() { return ['<color>']; }

paint(ctx, geom, props, arg){

  //컬러를 arg로 얻어온다.
  ctx.fillStyle = arg[0];
}

이에 맞춰 html쪽도 고쳐야합니다. 위의 property보다 훨씬 직관적인 형태로 변경됩니다.

<style>
  .test{
    ...
    background:paint(heart, blue)
  }
</style>

일반 함수 부르는 것처럼 인자로 전달하기 때문에 훨씬 직관적입니다만 아직 이 스펙은 크롬에 구현되지 못했습니다.
이 스펙이 구현되려면 후디니의 일부인 Properties & Values API가 수현되어야합니다. 카나리에서는 구현된 상태니 아마도 올해안에는 사용할 수 있을 거라 생각되지만 지금은 인자를 추가하면 paint작업 자체가 무시됩니다.

결론top

후디니(Houdini)의 일부인 Paint API를 살펴봤습니다. 후디니 자체가 구글이 주도하고 있는 기술이라 다른 브라우저들은 거의 지원되지 않고 모질라에서 천천히 관심을 갖는 정도이긴 하지만 현 시점에 크롬에서 실제 사용할 수 있는 기술이라는 점에서 매우 매력이 있고 DOM의 구조 그대로에서 캔버스의 복잡한 그리기 기능을 자연스럽게 녹여낼 수 있는 점을 십분활용하여 기존에 복잡한 렌더링을 분산시켜 관리할 수 있죠.
다음 글에서는 보다 깊이 파고 들어 아래와 같은 주제를 다루도록 해보죠.

  • reflow응용
  • property의 변화응용
  • 속성을 통한 이미지 사용
  • 원래 value가 image타입인 css속성을 이용한 이미지 사용
  • 아틀라스구성과 애니메이션
  • 필터
  • Blob 인라인화