Subterranean Flower

CSS Paint APIでJavaScriptからCSS用のグラフィックを動的に生成する

Author
古都こと
ふよんとえんよぷよぐやま!!!

Houdiniというプロジェクトをご存知ですか?HoudiniはJavaScirptからアクセスできるCSSの機能を広げ、プログラマブルなCSSを実現するためのものです。Houdiniが実現すれば、まるでハリー・フーディーニのように物事を自在に操れるようになること間違いなしです!

そんなHoudiniの中で、CSS Paint API(CSS Painting API)がChrome 65で実装予定です。 CSS Paint APIを使って、ブラウザの上の魔術であるHoudiniを体験してみましょう。

CSS Paint API

CSS Paint APIは、CSSで用いる画像をJavaScriptから動的に生成するためのAPIです。生成した画像は、background-imageやborder-imageで利用可能です。今までcanvas要素で無理やり実現していた複雑な背景なども、CSSの枠組みの中で実現することができるようになります。

画像はプログラマブルに生成できるので、単なる静止画だけでなく、アニメーションさせることもできます。また、パラメータを使ったバリエーションも作成できます。

実装状況

CSS Paint APIの実装状況は現在のところあまり良くはありません。実装状況は以下の通りです:

  • Chrome: 65で実装予定
  • Firefox: 実装中
  • Safari: 未定
  • Edge: 未定

また、Chrome 65でも一部機能はまだ使えません。

ここではChrome 65を前提に話を進めていきます。

ソースコード

この記事で使用するソースコードはGithubで公開しています。以下のURLからアクセスできます。ご自由にお使いください。

基本的な使い方

CSS Paint APIを使用するには、まずWorkletと呼ばれるものを作成する必要があります。WorkletはWorkerの一種で、特定用途向けの環境で実行されます。CSS Paint APIで使用するのはPaintWorkletと呼ばれていて、描画に関係する環境を備えたWorkletとなります。PaintWorkletはメインスレッド(UIスレッド)で動作します。

PaintWorkletの作成は簡単で、まず別ファイルとして(たとえばworklet.jsとしましょう)JavaScriptファイルを作り、その中に任意の処理を書きます。

CSS Paint APIを使用するには、まず適当なクラスを作ります。そしてそのクラスの中にpaintメソッドを実装します。そして最後にregisterPaint関数でクラスを登録します。例えば以下のようにです:

// 自分でクラスを作成する
class CirclePainter {
    // context: コンテキスト(canvasのcontextとほぼ同じ!)
    // geometry: 描画領域の情報(width, heightsのみ)
    // propeties: CSSプロパティ
    paint(context, geometry, properties) {
        context.fillStyle = 'red';

        const centerX = geometry.width / 2;
        const centerY = geometry.height / 2;
        const radius = 50;

        // 普通のcanvasのように使える
        context.arc(centerX, centerY, radius, 0, Math.PI * 2);
        context.fill();
    }
}

// paint(circle)として登録する
registerPaint('circle', CirclePainter);

paintメソッドは引数を3つ取ります。最初の引数contextはCanvasの2DContextとほぼ同じです。contextに対して描画命令を発行することによってグラフィックを生成できます。

次のgeometryは適用した要素の縦横の大きさ(width, height)を含むオブジェクトです。他の情報は含んでいません。

最後のpropertiesはアクセスできるCSSプロパティの一覧です。これについては後述します。

つまり、contextを受け取って、geometryでサイズを測りながら、好きに図形を描いていけば完成するというわけです。この例では、要素の中心に半径50pxの赤い円を描いています。

そして最後にregisterPaint関数で任意の名前で登録します。今回はcircleという名前で登録しました。

次にこれをCSS側から利用します:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>CSS Paint API</title>
    <style>
        .target {
            width: 500px;
            height: 500px;
            /* paint(登録した名前)で画像として使える */
            background-image: paint(circle);
        }
    </style>
</head>
<body>
    <div class="target"></div>

    <script>
        // workletを登録する
        CSS.paintWorklet.addModule('worklet.js');
    </script>
</body>
</html>

CSSから利用するには、まずCSS.paintWorklet.addModuleメソッドを使ってWorkletを登録します。そしてCSS側で、「paint(登録した名前)」とすることで画像として扱うことができます。今回はbackground-imageに使用しています。

そして何も中身のない500×500のdiv要素にこのpaint(circle)を背景として適用しています。さてどうなるでしょう。

これを実行してみます:

赤い丸が表示されました!

注意点

PaintWorkletにはひとつ注意点があります。それはcontextはcanvas要素のcontextとは完全に一緒ではないということです。簡単にいうと、いくつかの機能が欠けています。具体的にいうと、テキスト描画系の命令は存在せず、imageData系の命令も使えません。

あくまでcanvasのサブセットということに注意しつつ使用しましょう。

CSSのプロパティを読み出す

PaintWorkletを使用することでCSS用のグラフィックを描画することができました。しかしこれでは何か物足りません。もう少し凝ったことをしてみましょう。

先ほどの例でも少し見えましたが、paintメソッドはpropertiesという引数を受け取ります。これはCSSのプロパティを読み出すために使う引数です。CSSのプロパティをパラメータとして使うことで、柔軟にバリエーションを作ることができます。

プロパティを読み出すにはクラスにstatic get inputPropertiesを実装し、読み取りたいプロパティの名前の配列を返します。

そしてpaintメソッドの第3引数であるpropertiesからgetメソッドを使って値を取得します。例えば以下のようにします:

// 自分でクラスを作成する
class CirclePainter {
    // static get inputPropertiesゲッターを定義する。
    // ここで配列として返したプロパティを取得できる。
    static get inputProperties() {
        return [
            '--circle-color'
        ];
    }

    // context: コンテキスト(canvasのcontextとほぼ同じ!)
    // geometry: 描画領域の情報(width, heightsのみ)
    // propeties: CSSプロパティ
    paint(context, geometry, properties) {
        // propertiesのgetメソッドを使って値を取得する
        // CSSStyleValueが返ってくるのでtoStringで文字列にしてやる
        const color = properties.get('--circle-color').toString();
        context.fillStyle = color;

        const centerX = geometry.width / 2;
        const centerY = geometry.height / 2;
        const radius = 50;

        // 普通のcanvasのように使える
        context.arc(centerX, centerY, radius, 0, Math.PI * 2);
        context.fill();
    }
}

// paint(circle)として登録する
registerPaint('circle', CirclePainter);

properties.getで返ってくるのはCSSStyleValueなのでtoStringメソッドで文字列に変換します。あとは普通にその値を使えば、プロパティをパラメータとしての描画ができます。

あとはCSS側で変数を定義し、そのままpaintを使います:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>CSS Paint API</title>
    <style>
        .target {
            width: 500px;
            height: 500px;

            /* 変数を定義する */
            --circle-color: blue;

            /* paint(登録した名前)で画像として使える */
            background-image: paint(circle);
        }
    </style>
</head>
<body>
    <div class="target"></div>

    <script>
        // workletを登録する
        CSS.paintWorklet.addModule('worklet.js');
    </script>
</body>
</html>

これを実行してみます:

今度は青い円が描画されました!これで自由自在に描画できます。

paint()の引数

注:この機能はChrome 65では未実装です。

CSSのプロパティを使ってパラメータを実現することができました。しかしプログラミングに少し興味のある人なら「方法がちょっとダサい」と思うはずです。

そこでもう少し賢い方法が用意されています。CSSで使うpaint()に、引数として値を渡す方法です。CSS側では、以下のようにします:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>CSS Paint API</title>
    <style>
        .target {
            width: 500px;
            height: 500px;
            
            /* 引数として値を渡す */
            background-image: paint(circle, blue);
        }
    </style>
</head>
<body>
    <div class="target"></div>

    <script>
        // workletを登録する
        CSS.paintWorklet.addModule('worklet.js');
    </script>
</body>
</html>

この方がプログラミング的にかっこいいですね!

このときWorklet側ではstatic get inputArgumentsを実装します。inputArgumentsは引数の型の配列を返します。例えば引数が色ならば'<color>’という文字列を配列に入れます。複数の引数を定義するときは、その数だけの型文字列を配列に入れてください。

そしてpaintメソッドの引数を4つに増やします。4つめの引数がCSSでの引数になります。

これを実装したWorkletは以下のようになります:

// 自分でクラスを作成する
class CirclePainter {
    // static get inputPropertiesを定義する。
    // ここで配列として返したプロパティを取得できる。
    // 配列には型情報(文字列)を入れる。
    //
    // 型の一覧は以下のURLを参照:
    // https://www.w3.org/TR/css-properties-values-api-1/#supported-syntax-strings
    static get inputArguments() {
        return [
            '<color>'
        ];
    }

    // arguments: CSSから渡された引数
    paint(context, geometry, properties, arguments) {
        // argumentsは引数の配列になっている
        const color = arguments[0].toString();
        context.fillStyle = color;

        const centerX = geometry.width / 2;
        const centerY = geometry.height / 2;
        const radius = 50;

        context.arc(centerX, centerY, radius, 0, Math.PI * 2);
        context.fill();
    }
}

// paint(circle)として登録する
registerPaint('circle', CirclePainter);

このようにしてCSSから引数を受け取ることができるようになります。

ここで少し馴染みの薄いものが出てきます。CSS引数の型です。ここには'<color>’や'<length>’などを指定します。これらの型については詳しくは以下のURLをご覧ください:

アニメーションさせる

静止画がひとつ描画されるだけ、というのもなんだか面白くないです。今度はアニメーションさせてみましょう。

アニメーションには、プロパティの取得かCSSの引数を使います。値をどんどん変化させていくことで、アニメーションが実現できます。実際にやってみましょう。

Wroklet側ではCSSのプロパティを見て描画する図形を変えます。

class CirclePainter {
    static get inputProperties() {
        return ['--circle-radius'];
    }

    paint(context, geometry, properties) {
        context.fillStyle = 'red';

        const centerX = geometry.width / 2;
        const centerY = geometry.height / 2;

        // --cirlce-raidus変数を変化させアニメーションする
        const radius = parseFloat(properties.get('--circle-radius').toString());

        context.arc(centerX, centerY, radius, 0, Math.PI * 2);
        context.fill();
    }
}

// paint(circle)として登録する
registerPaint('circle', CirclePainter);

そしてJavaScriptでプロパティをどんどん変化させていきます。CSS VariablesはTransition/Animationに対応していなため、requestAnimationFrameを使っての手動での変化になります。

requestAnimationFrameを使ったアニメーションに馴染みのない方は、「JavaScriptとcanvasでアニメーションを作る」もあわせてご覧ください。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>CSS Paint API</title>
    <style>
        .target {
            width: 500px;
            height: 500px;

            --circle-radius: 50;

            background-image: paint(circle);
        }
    </style>
</head>
<body>
    <div class="target"></div>

    <script>
        // workletを登録する
        CSS.paintWorklet.addModule('worklet.js');

        // アニメーションさせる。
        // CSS Transition/CSS Animationは
        // CSS Variablesには対応してないので手動で動かす。
        const target = document.body.querySelector('.target');

        let radius = 50; // 円の半径
        let velocity = 1; // 円の膨張速度

        // アニメーションループ用の関数
        function loop(timestamp) {
            radius += velocity;
            if(radius < 50 || radius > 150) {
                velocity = -velocity;
            }

            // --circle-radiusに値をセットする
            target.style.setProperty('--circle-radius', radius);

            // 次のフレームを要求する
            requestAnimationFrame((ts) => loop(ts));
        }

        // アニメーション開始
        requestAnimationFrame((ts) => loop(ts));
    </script>
</body>
</html>

これを実行してみます:

アニメーションさせることができました!

このように、CSS Paint APIを使うことで、CSSで使える画像を様々に生成できます。まだまだ対応ブラウザが少ない状況ですが、実装が増えればいろいろと使いどころのあるAPIとなっています。各ブラウザの足並みが揃ってきたら、ぜひ使ってみましょう。