CSS paint apiを使って複雑な背景描画を実現する
CSS Paint APIとは、CSSの背景や枠線の描画をjavascriptでプログラム描画する機能です。
描画した図形画像をアニメーションさせたり、受け取るパラメータによって変更したりする事もできます。
JSで描画するのでプログラム知識が必要になりますが、高度な演出が可能になります。
CSS Paint APIとは
本記事ではCSSのスタイリングにjavascriptを使う方法をご紹介します。
それがCSS Houdini(フーディーニ)APIという機能です。
CSS HoudiniのCSS Paint API
CSS HoudiniのAPIは、CSSの機能をJavaScirptによる描画処理を利用して拡張した機能です。
より高度で複雑なCSSスタイルをプログラミング感覚で実現する事ができます。
今回はCSS HoudiniのAPIの一部となる、現在Google ChromeやEdgeで実装されている「CSS Paint API」を紹介します。
CSS Paint APIを使用すれば、JavaScript関数を記述する事でCSS要素の背景や境界線、またはコンテンツに直接描画ができるようになります。
CSSに高度で複雑な背景画像・処理が利用できる
分かりやすく言うと、CSSで用いる画像をJavaScriptから動的に生成するためのAPIと言えます。
生成した画像は、background-imageやborder-imageで利用可能です。
様々な演出効果が実現できる
今までcanvas要素で無理やり実現していた複雑な背景なども、CSSの中で実現できるようになる訳です。
画像は単なる静止画だけでなく、アニメーションやパラメータを使ったバリエーションも作成できます。
このAPIはローカル上では動作確認できないため、サーバー上で確認しましょう。
実装と基本的な仕組みの解説
それでは各工程を解説しながら、具体的に実装をしていきます。
HTMLソース
<div class="box"></div>
まず適当にdivにクラス名を付けて、上記の様なHTMLソースを書きましょう。
workletのJSコードを続けて追記
<script> if (CSS.paintWorklet) { CSS.paintWorklet.addModule('js/paint.js'); } </script>
上記ソースに続けて、<script>でcssにpaintworkletを追加し、モジュール読込先のjsファイルを指定します。
Workletとは
CSS Paint APIを使用するには、まずWorkerの一種であるWorkletの作成が必要です。
記述されているPaintWorkletは、Workletの中でもCSS Paint API向けの描画に関係する機能を備えています。
PaintWorkletの描画ファイルを読込
PaintWorkletはメインスレッド(UIスレッド)で動作します。
PaintWorkletのモジュール読込先となるJavaScriptファイル(js/paint.js)を用意します。
その中に任意の処理を書く事で、.boxのCSS描画を拡張していく訳です。
CSS
.box { height: 100px; background: paint(draw); }
設定したCSSクラス名に対し、backgroundでpaint()メソッドを設定します。
ここでは「draw」の名称を付けています。
JS
registerPaint('draw', class { paint(ctx, size) { ctx.fillStyle = 'red'; ctx.fillRect(0, 0, size.width, size.height); } });
JSファイルの中で、まずregisterPaint関数でdrawクラスを登録します。
同時にcssのpaintメソッド(draw)における描画処理を作成します。
コード中のpaint()のところが描画をしている部分で、今回は1色べた塗しています。
この登録したdrawクラス名をCSS側から利用する事になりますね。
事例サンプル:赤いパネル描画
この赤い帯はCSSのbackground-colorで演出したものではなく、Javascriptによる描画になります。
paintメソッド変数の追加
paintメソッドは、propertiesのところでCSSのプロパティに使うパラメータを受け取る事ができます。
CSSプロパティをパラメータとして使うことで、場面ごとの柔軟なバリエーションを作る事ができます。
クラスにstatic get inputPropertiesを実装し、読み取りたいプロパティの名前の配列を返します。
そしてpaintメソッドの第3引数であるpropertiesからgetメソッドを使って値を取得します。
HTML
<div class="box" style="--color:red"></div> <div class="box" style="--color:blue"></div>
上記はCSSの方ではなく、直接スタイルを書き込んだ状態です。
CSS
.box { height: 100px; /* --color: blue; */ background:paint(draw); }
上記はコメントアウトしていますが、本来はこちらに書いても良いと思います。
JS
registerPaint('draw', class { static get inputProperties() {return ['--color']} paint(ctx, size, properties) { const c = properties.get('--color'); ctx.fillStyle = c; ctx.fillRect(0, 0, size.width, size.height); } });
この様にする事で、HTMLタグ或いはCSS上の引数に応じて、その色で敷き詰めた画像を描画する事ができます。
事例サンプル:パラメータに応じた色の変化
HTMLソース上でredとしたものは赤に、blueとしたものは青になっています。
アニメーション例
ポイントになるのはJSコード中でどのような演出効果を作るかになってきます。
本記事ではJS内の複雑な解説はしませんが、以下の様な複雑なアニメーション効果も実現できます。
サンプルを紹介しておきます。
マウスオーバーアニメーション例1:(六角形)
JSファイル
六角形によるマスク効果が入るアニメーション演出です。
registerPaint('fragmentation', class { static get inputProperties() { return [ '--f-r', '--f-o' ] } paint(ctx, size, properties) { const r = parseInt(properties.get('--f-r')); const o = properties.get('--f-o'); const w = size.width; const h = size.height; const a = 2 * Math.PI / 6; const l = 7; /* seeded random */ const mask = 0xffffffff; const seed = 30; let m_w = (123456789 + seed) & mask; let m_z = (987654321 - seed) & mask; let random = function() { m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask; m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask; var result = ((m_z << 16) + (m_w & 65535)) >>> 0; result /= 4294967296; return result; } /**/ for (let y = 0; y + r * Math.sin(a) < (h + 3*r); y += r * Math.sin(a)) { for (let x = 0, j = 0; x + r * (1 + Math.cos(a)) < (w + 3*r); x += r * (1 + Math.cos(a)), y += (-1) ** j++ * r * Math.sin(a)) { ctx.beginPath(); for (let i = 0; i < 6; i++) { ctx.lineTo(x + r * Math.cos(a * i), y + r * Math.sin(a * i)); } ctx.closePath(); var alpha = (random()*(l-1) + 1) - (1-o)*l; ctx.fillStyle = 'rgba(0,0,0,'+alpha+')'; ctx.strokeStyle = 'rgba(0,0,0,'+alpha+')'; ctx.stroke(); ctx.fill(); } } } })
マウスオーバーアニメーション例2:(四角形)
JSファイル
上と同じで今度は四角形によるマスク効果が入るアニメーション演出です。
registerPaint('fragmentation', class { static get inputProperties() { return [ '--f-n', '--f-m', '--f-o' ] } paint(ctx, size, properties) { const n = properties.get('--f-n'); const m = properties.get('--f-m'); const o = properties.get('--f-o'); const w = size.width/n; const h = size.height/m; const l = 10; const mask = 0xffffffff; const seed = 30; let m_w = (123456789 + seed) & mask; let m_z = (987654321 - seed) & mask; let random = function() { m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask; m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask; var result = ((m_z << 16) + (m_w & 65535)) >>> 0; result /= 4294967296; return result; } for(var i=0;i<n;i++) { for(var j=0;j<m;j++) { ctx.fillStyle = 'rgba(0,0,0,'+((random()*(l-1) + 1) - (1-o)*l)+')'; ctx.fillRect(i*w, j*h, w, h); } } } })