CSS Paint API

Chrome 65 で実現する新たな可能性

Chrome 65 以降では、CSS Paint API(「CSS Custom Paint」や「Houdini のペイント ワークレット」とも呼ばれます)がデフォルトで有効になっています。概要: データで何ができるか?その仕組みはどうなっているのでしょうか?まあ、読んでね...

CSS Paint API を使用すると、CSS プロパティが画像を必要とする場合に、プログラムで画像を生成できます。通常、background-imageborder-image などのプロパティは、url() で画像ファイルを読み込むため、または linear-gradient() などの CSS 組み込み関数で使用されます。これらを使用する代わりに、paint(myPainter) を使用してペイント ワークレットを参照できるようになりました。

ペイント ワークレットを作成する

myPainter というペイント ワークレットを定義するには、CSS.paintWorklet.addModule('my-paint-worklet.js') を使用して CSS ペイント ワークレット ファイルを読み込む必要があります。このファイルでは、registerPaint 関数を使用してペイント ワークレット クラスを登録できます。

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> を使用したことのある方は、このコードに見覚えがあるはずです。ライブデモをご覧ください。

チェッカーボード パターンを背景画像として使ったテキスト領域
チェッカーボード パターンが背景画像として表示されているテキスト領域。

一般的な背景画像を使用する場合との違いは、ユーザーがテキスト領域のサイズを変更するたびに、パターンがオンデマンドで再描画されることです。つまり、背景画像は高密度ディスプレイの補正も含め、常に必要とするだけのサイズになります。

これは素晴らしいですが、非常に静的でもあります。同じパターンでサイズが異なる正方形を使用するたびに、新しいワークレットを作成することになりますか?答えはノーです。

ワークレットのパラメータ化

幸いなことに、ペイント ワークレットは他の 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 で適切な外観が見つかるまで値を調整できます。

ペイント ワークレットをサポートしていないブラウザ

この記事の執筆時点では、ペイント ワークレットが実装されているのは Chrome のみです。他のブラウザ ベンダーからも肯定的な兆候がありますが、あまり進歩は見られません。 最新の状態を保つには、定期的に [Is Houdini Ready Yet?] を確認します。それまでの間は、ペイント ワークレットがサポートされていない場合でもコードを実行し続けるために、プログレッシブ拡張を使用してください。想定どおりに動作させるには、CSS と JS の 2 か所でコードを調整する必要があります。

JS におけるペイント ワークレットのサポートは、CSS オブジェクトを確認することで検出できます。 js if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('mystuff.js'); } CSS 側には、2 つのオプションがあります。@supports を使用できます。

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

よりコンパクトな方法として、不明な関数がある場合、CSS はプロパティ宣言を無効にして、その後でプロパティ宣言全体を無視する、というものです。プロパティを 2 回指定すると(最初はペイント ワークレットなし、次にペイント ワークレットあり)、プログレッシブ エンハンスメントが拡張されます。

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

ペイント ワークレットをサポートしているブラウザでは、background-image の 2 番目の宣言によって、最初の宣言が上書きされます。ペイント ワークレットをサポートしていないブラウザでは、2 番目の宣言は無効になり、破棄され、最初の宣言が有効になります。

CSS ペイント ポリフィル

CSS Paint Polyfill もさまざまな用途で使用できます。これにより、CSS カスタム ペイントとペイント ワークレットのサポートが最新のブラウザに追加されます。

ユースケース

ペイント ワークレットには多くのユースケースがあり、その中には特に明白なものもあります。明白な方法の一つは、ペイント ワークレットを使用して DOM のサイズを縮小することです。多くの場合、CSS を使用して装飾を作成するためだけに要素を追加します。たとえば、マテリアル デザイン ライトでは、リップル効果が指定されたボタンには、リップル自体を実装するための 2 つの追加 <span> 要素が含まれています。ボタンが多数ある場合、DOM 要素の数が非常に多くなり、モバイルでのパフォーマンスの低下につながる可能性があります。代わりにペイント ワークレットを使用してリップル効果を実装すると、追加要素が 0 個になり、ペイント ワークレットが 1 つだけになります。さらに、カスタマイズとパラメータ化がはるかに容易なものもあります。

ペイント ワークレットを使用するもう一つのメリットは、ほとんどのシナリオで、ペイント ワークレットを使用するソリューションはバイト数的に小さいことです。もちろんトレードオフはあります。キャンバスのサイズまたはいずれかのパラメータが変更されるたびに、ペイント コードが実行されます。そのため、コードが複雑で時間がかかる場合は、ジャンクが発生する可能性があります。Chrome は、ペイント ワークレットをメインスレッドから移動させ、長時間実行されるペイント ワークレットがメインスレッドの応答性に影響を与えないようにしています。

私にとって最も刺激的な展望は、ペイント ワークレットによって、ブラウザがまだ持っていない CSS 機能を効率的にポリフィルできるという点です。一例として、Chrome にネイティブで到達するまで、円錐形グラデーションをポリフィルする場合が挙げられます。別の例: CSS の会議で、複数の枠線の色を使用可能にすることが決定されました。この会議はまだ続いていましたが、同僚の Ian Kilpatrick がペイント ワークレットを使用してこの新しい CSS 動作のポリフィルを作成しました。

既成概念にとらわれない考え方

多くの人は、ペイント ワークレットについて学ぶと、背景画像と枠線について考え始めます。ペイント ワークレットのあまり直感的でないユースケースの 1 つは、DOM 要素を任意の形状にする mask-image です。たとえば、ひし形の場合は次のようになります。

ひし形の形をした DOM 要素。
ひし形の形をした DOM 要素。

mask-image は、要素のサイズに応じた画像を受け取ります。マスク画像が透明な領域は透明です。マスク画像が不透明である領域、つまり要素が不透明である領域。

Chrome で利用可能

ペイント ワークレットは以前より Chrome Canary で使用されていました。Chrome 65 では、この機能がデフォルトで有効になっています。ペイント ワークレットが開く新しい可能性をぜひお試しください。詳細については、Vincent De Oliveira のコレクションをご覧ください。