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> の背景画像として使用してみましょう。(ここでは、デフォルトでサイズ変更が可能なため、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 つの場所でコードを調整する必要があります。

JavaScript でペイント ワークレットのサポートを検出するには、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 カスタム ペイントとペイント ワークレットのサポートが追加されます。

ユースケース

ペイント ワークレットには多くのユースケースがあり、そのうちのいくつかは他よりも明白です。わかりやすい方法の 1 つは、ペイント ワークレットを使用して DOM のサイズを小さくすることです。多くの場合、要素は純粋に CSS を使って装飾を追加するために追加されます。たとえば、マテリアル デザイン ライトでは、波紋効果を適用したボタンに、リップル自体を実装するための 2 つの <span> 要素が追加されています。ボタンの数が多い場合、DOM 要素の数が増え、モバイルでのパフォーマンスが低下する可能性があります。代わりにペイント ワークレットを使用してリップル エフェクトを実装すると、追加の要素が 0 になり、ペイント ワークレットが 1 つだけになります。さらに、カスタマイズとパラメータ化が非常に簡単になります。

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

私にとって最も興味を引かれたのは、ペイント ワークレットによって、ブラウザにはまだ存在していない CSS 機能を効率的にポリフィルできることです。たとえば、円錐グラデーションを Chrome でネイティブに取得できるまで、ポリフィルできます。もう一つの例は、CSS の会議で、枠線の色を複数設定できるようになったことです。会議がまだ始まっている間に、同僚の Ian Kilpatrick がペイント ワークレットを使用してこの新しい CSS 動作用のポリフィルを作成しました。

既成概念にとらわれずに考える

背景画像や枠線の画像は、ペイント ワークレットについて学ぶとよくわかります。あまり直感的ではないペイント ワークレットのユースケースの一つに、DOM 要素の形状を任意にする mask-image があります。たとえば、ダイヤモンドです。

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

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

Chrome で利用可能に

ペイント ワークレットは以前から Chrome Canary で使用されていました。Chrome 65 では、この機能がデフォルトで有効になっています。ペイント ワークレットの新たな可能性を試して、作成したものをお見せください。さらにヒントが必要な場合は、Vincent De Oliveira のコレクションをご覧ください。