Canvas 2D(<canvas> の 2D コンテキスト)

実装: arm/v1/src/os_lib/canvas2d.rs(純粋モジュール) 試験: arm/v1/tests/src/test_canvas2d.rs(40 件)

着手前の状態

getContext("2d") は既に実装されており、fillRect / arc / bezierCurveTo / drawImage / getImageData など 30 以上のメソッドが 登録済みだった。ただしすべて dom_noop(何もしない)へ接続されており、 canvas を使うページでは一切描画されなかった。 状態プロパティ(fillStyle/lineWidth/font 等)も保持されるだけだった。

つまり「API 表面はあるが中身が無い」状態。

本モジュールの範囲

ハードウェア(画面)に依存しない部分をすべて扱う。

  1. アフィン変換(Matrix): 合成・適用・行列式・逆行列
  2. パスの折れ線化(PathBuilder): moveTo/lineTo/closePath/ ベジェ(2 次は 3 次へ昇格)/円弧/rect
  3. 走査線による塗り判定(scanline_spans): nonzero / evenodd
  4. バッキングストアへの描画(Surface): ピクセル合成・矩形塗り・ パス塗り・線描
  5. コンテキストの状態(Canvas2dContext / DrawState): 変換の積み上げ、save/restore、globalAlpha、 fillRect/clearRect/fill/stroke の実行
  6. 登録表(create_context/with_context): JS オブジェクトと Rust 側の状態を id で結び付ける

JS からの接続

JS 側のコンテキストオブジェクトは _c2d_id(registry の id)だけを持ち、 実体は Rust 側の表にある。JS の値へ Rust の可変状態を直接埋めると借用と 寿命の扱いが複雑になるため、既存の DOM プロキシ(style/classList)と同じ 「id で引く」流儀に合わせた。

接続済みのメソッド: fillRect / strokeRect / clearRect / beginPath / closePath / moveTo / lineTo / rect / arc / bezierCurveTo / quadraticCurveTo / ellipse / roundRect / fill / stroke / save / restore / translate / scale / rotate / transform / setTransform / resetTransform。

roundRect の半径は数値 1 個でも配列(1〜4 要素)でも取れる。 補完規則は CSS の border-radius と同じ(1 個なら全隅、2 個なら対角の組、 3 個なら左上・対角・右下)。半径の和が辺長を超える場合は比率を保ったまま 縮小する——しないと角が重なって図形が破綻する。

restore() は変換だけでなく globalAlpha/lineWidth も JS 側へ 書き戻す。これらは JS の素のプロパティなので、Rust 側で戻すだけでは JS から読んだときに古い値が見えてしまう。 色(fillStyle/strokeStyle)は文字列表現を保持していないため 戻せない(既知の割り切り)。

fillStyle 等は JS の素のプロパティで代入を検知できないため、 各描画メソッドの入口で毎回読み直す(canvas_sync_state)。 色は CSS の parse_color をそのまま使う。

同時に保持するコンテキストは 8 個までで、超えたら古い id から捨てる。 ページ遷移のたびに面(最大 64MB)が積み上がるのを防ぐため。

曲線分割規則と巻き数計算という目視では気づけないバグ源を試験で固定できる。

なぜ画面ではなくバッキングストアへ描くか

<canvas> は「レイアウトで決まった矩形へ、自分のピクセルを貼る」要素。 画面座標へ直接描くと、スクロール・クリップ・再描画のたびに canvas の内容を作り直す必要が出る。Surface(ARGB8888 配列)へ キャンバス内座標で完結させておけば、貼り付けだけが画面側の仕事になる。

面のサイズは 4096×4096 を上限とし、超える指定は None を返す (ヒープを食い潰さないため)。

不変条件

塗りつぶし規則

両者の違いは入れ子図形でしか現れないため、試験で明示的に区別している。

追加の不変条件(Surface)

画面への転送

レイアウトは StyledNode しか見ないため、JS のオブジェクトを直接引かせず DOM 属性 _c2d_id を橋渡しに使う。

  1. getContext("2d") が registry へコンテキストを作り、id を DOM 要素の _c2d_id 属性へ書く
  2. レイアウトが <canvas> 要素からその属性を読み、 RenderElement::canvas_ctx_id へ載せる
  3. 描画時に registry から Surface を引き、要素の矩形へ転送する

面のサイズと要素のサイズが違う場合は最近傍で拡縮する。 CSS で width/height を指定した canvas は、属性の解像度と表示サイズが 食い違うのが普通なので、この経路は必須。

線幅

lineWidth は stroke / strokeRect に反映される。各線分を 「線幅ぶんの太さを持つ四角形」へ展開して塗り、結合部には円を置く。 ## 線端・線結合

lineCap は butt(既定)/round/square、lineJoin は miter(既定)/round/bevel に対応する。miterLimit も反映する。

低水準の stroke_path_width は端 butt・結合 round 固定で、 stroke_path_styled へ委譲する。既存の描画の見た目を変えないための 互換窓口であり、新しい呼び出しは stroke_path_styled を使う。

再描画

canvas はバッキングストア(Surface)へ描くだけなので、そのままでは 画面に出ない。画素を書く入口(fill_rect/fill_path/stroke_path/ stroke_path_styled)で旗を立て、ブラウザの再描画判定がそれを見る。 旗は描画の開始時に降ろす(take_canvas_dirty)。

破線・クリップ

setLineDash/lineDashOffset は stroke/strokeRect に反映される。 クリップは DrawState が Arc<ClipMask> として持ち、描画の直前に Surface へ写す。save/restore は状態を複製するだけなので、 クリップは正しく戻る(色と違って文字列表現に依存しない)。

ImageData・大きさの変更

getImageData/putImageData は面の画素を読み書きする (createImageData は透明のまま)。canvas.width/height への代入は 面を作り直して描画状態を初期化する。

画像の転送

drawImage は <img>(src で共有の置き場を引く)と別の canvas (_c2d_id からコンテキストを引く)を転送元にできる。 変換・globalAlpha・クリップに従う。

テキスト

fillText/measureText はベクトルフォントの字形を使う。 textAlign/textBaseline/font の大きさに従う。 strokeText は塗りで代用する(線でなぞる形は未対応)。 ベースラインの比率はこの処理系の字形配置に合わせた近似で、 フォントごとの実測値ではない。

残課題

  1. restore() で色を戻せない(文字列表現を保持していないため)
  2. strokeText の輪郭描き(現状は塗りで代用)
  3. Path2D(clip(path, rule) の形は未対応)
  4. 転送の補間は最近傍のみ(imageSmoothingEnabled は未対応)
  5. 線描(stroke)の線幅・線端・線結合(現状は線幅 1 固定)
  6. クリップ領域(clip())
  7. <canvas width height> の動的変更への追従(現状は getContext 時のみ)
  8. 再描画の契機——JS が描いた後にレイアウト/再描画が走らないと 画面へ反映されない。現状は他の要因で再描画が起きたときに反映される。