Skia Canvas: Node.js向けHTML Canvas描画APIのブラウザーレス実装
(skia-canvas.org)- Skia Canvas は Node.js で HTML Canvas 描画 API をオンスクリーン・オフスクリーンレンダリングに使えるようにし、Google Skia ベースのため Chrome の
<canvas>と非常によく似た出力を実現する - 出力結果は PDF・SVG のようなベクター形式と、JPEG・PNG・WEBP のようなビットマップ形式で書き出せ、ファイル・dataURL・Buffer・Sharp オブジェクトに接続できる
- ブラウザー Canvas の外側の領域も扱い、GUI Window レンダリング、イベントフレームワーク、複数ページ Canvas、パス演算、3D 遠近変換、CSS filter、高度なタイポグラフィ制御を提供する
- 非同期レンダリングとファイル I/O は、ユーザーが設定する ワーカープール のネイティブスレッドで処理されるため、サーバーサイド画像生成や serverless 環境に適している
- ベンチマークは serial と async モードを分けて計測し、async では複数の反復処理を同時に開始して マルチスレッド 効果を比較している
Node.jsで使うCanvas実装
- Skia Canvas は Node.js 向けの HTML Canvas 描画 API 実装で、オンスクリーンとオフスクリーンレンダリングの両方をサポートする
- Google Skia グラフィックスエンジンを使用し、Chrome の
<canvas>要素と非常に近い結果を出力する - ブラウザー Canvas がまだ提供していない機能まで含んでいる
出力形式とエクスポート方法
- 画像出力は ベクター形式 と ビットマップ形式 の両方をサポートする
- ベクター: PDF, SVG
- ビットマップ: JPEG, PNG, WEBP
- Canvas の結果を複数の出力先へ送れる
- インタラクティブな GUI Window に直接描画でき、ブラウザーに近い イベント フレームワークを提供する
非同期レンダリングと複数ページ
- レンダリングとファイル I/O は、ネイティブスレッドを使う ワーカープール で非同期に実行される
- ワーカープールは ユーザー設定 が可能
- 1つの Canvas に 複数ページ を作成できる
- 単一の複数ページ PDF として 出力 できる
- 複数ファイルに分かれた画像シーケンスとして保存することもできる
ブラウザーCanvasを超えるグラフィックス機能
- Bézier パスはブール演算と点単位の補間をサポートする
- 変換機能は基本的な Canvas の拡大縮小・回転・移動に加え、3D 遠近 変換を提供する
- 図形の塗りつぶしにはビットマップベースの Pattern だけでなく、ベクターベースの Texture も使える
- 線描画にはカスタム marker を付けられる
- 画像処理では CSS filter 演算子一式をサポートする
タイポグラフィ制御
- テキストレンダリングは複数行と 自動改行 に対応する
- 行単位の テキストメトリクス を提供する
- small-caps や ligature などの OpenType 機能を標準の font-variant 構文で使える
- 文字間隔、単語間隔、leading を比例的に制御できる
- variable fonts をサポートし、weight 値を透過的にマッピングする
- ローカルファイルから 読み込んだ 非システムフォントも使える
使用例
- 画像ファイル生成の例では、
Canvasを作成しgetContext("2d")で 2D コンテキストを取得した後、円錐形グラデーションを stroke スタイルとして使って四角形を描くsaveAs("rainbox.png", {density:2})で高密度画像を保存するcanvas.pngはcanvas.toBuffer("png")の短縮形として使われるtoDataURL("png")の結果を HTML 文字列の中に埋め込めるsaveAsSync("rainbox.pdf")によりメインスレッドで同期保存も可能
- 複数ページシーケンスの例では、
canvas.newPage()で色の異なるページを作成して保存するall-pages.pdfとして複数ページ PDF を保存するpage-{2}.pngパターンでpage-01.png,page-02.pngのような複数ファイルを生成する
- Window レンダリングの例では、
Window(300, 300)を作成し、drawイベントでフレーム値に応じて線の太さが変わる円を描く - Sharp.js 統合の例では、Canvas の結果を Sharp オブジェクト に変換して後処理したり、Sharp で作成した画像を
loadImageで Canvas に描画する流れを示している
ベンチマーク方法と結果
- ベンチマークは Skia Canvas を 2 つのモードに分けてテストしている
- serial: 各レンダリング処理の完了を待ってから次の反復に進む
- async: すべてのテスト反復を一度に開始し、マルチスレッド対応により並列実行する
- 全体結果は canvas-benchmarks 結果ページ で確認できる
- 起動遅延時間テストでは、100 回反復基準で skia-canvas が 1 回あたり 1ms 未満、合計 33ms を記録した
- canvaskit-wasm: 1 回あたり 25ms、合計 2.47 秒
- canvas: 1 回あたり 88ms、合計 8.77 秒
- @napi-rs/canvas: 1 回あたり 69ms、合計 6.87 秒
- Bézier curves テストでは、20 回反復基準で async モードの並列処理差が表れた
- skia-canvas serial: 1 回あたり 137ms、合計 2.74 秒
- skia-canvas async: 1 回あたり 28ms、合計 558ms
- 比較対象は canvaskit-wasm 790ms、canvas 486ms、@napi-rs/canvas 230ms だった
- SVG to PNG テストでは、100 回反復基準で canvaskit-wasm は未対応で、skia-canvas は serial で 1 回あたり 58ms・async で 1 回あたり 11ms を記録した
- skia-canvas serial: 合計 5.83 秒
- skia-canvas async: 合計 1.08 秒
- 画像の拡大縮小・回転テスト 50 回反復の結果
- skia-canvas serial: 1 回あたり 100ms、合計 5.00 秒
- skia-canvas async: 1 回あたり 19ms、合計 935ms
- 基本テキストテスト 200 回反復の結果
- skia-canvas serial: 1 回あたり 21ms、合計 4.26 秒
- skia-canvas async: 1 回あたり 4ms、合計 819ms
1件のコメント
Hacker Newsのコメント
https://windowjs.org もかなり似たコンセプト Skiaをラップして Canvas API として公開しているが、Nodeの代わりにv8を内蔵し、非常に小さなランタイムで動作する 約3年前に公開した最初のオープンソースプロジェクトで、WebGL・オーディオなども公開してデスクトップ向けJavaScriptゲームプラットフォームにする計画もあったが、別の仕事やプロジェクトのため開発は中断した ブラウザ外でもCanvasを使えるようにするこうしたプロジェクトが出てきてうれしい
興味本位だが、こういうライブラリがどこで使われるのか気になる デスクトップなら図形を描くのにもっと良い ネイティブライブラリ があるのではと思う
SkiaにはNodeをサポートするWASMビルドのCanvasKitが含まれている https://www.npmjs.com/package/canvaskit-wasm が、このモジュールは Rustバインディング のように見える 2つのアプローチの長所と短所が気になる
これはRustクレートを包んだラッパーのようなもの? https://rust-skia.github.io/doc/skia_safe/canvas/struct.Canvas.html
こういうものをずっと待っていた 以前に似たことをやろうとした試みでは Node-Gypのインストール が必要で、本当に悪夢のようだった
単なるレンダリングAPI以上のものだ: 「OSネイティブのグラフィックパイプラインでウィンドウにレンダリングでき、ブラウザのようなUIイベントフレームワークを提供する」とある WebGPU対応のために wgpu を、WebGL対応のためにANGLEを追加することもできそう
Node互換のCanvas実装に興味があるなら、いくつかある Skiaプロジェクトのcanvaskit-wasmはGPUアクセラレーションではないようだ: https://github.com/google/skia/tree/main/modules/canvaskit/npm_build @napi-rs/canvasは最速のバインディング: https://github.com/Brooooooklyn/canvas?tab=readme-ov-file#performance node-canvasはSkiaではなくCairoを使っている: https://github.com/Automattic/node-canvas
Advent of Codeで簡単なグラフィックウィンドウが必要なとき、今はDenoと一緒に使っている window-exampleもある: https://skia-canvas.org/#rendering-to-a-window
deno add npm:skia-canvasで追加し、deno.jsonに"nodeModulesDir": "auto"を入れてからdeno install --allow-scriptsを実行すればよいこれを見ると本当に期待が高まる スタンドアロン実行も可能で、Web互換のグラフィックスをずっと楽に書けるようになりそう 1つ目のスクリプト言語向けライブラリをコンパイルするために、2つ目のスクリプト言語でソースからビルドしてコード生成する必要もなくなる VS Codeのような場所でCanvasコードを リアルタイムレンダリング したり、Web Canvasで動かすための変換レイヤーを持つコードを実行したりするのに、どれほど向いているのかも興味深い