Skip to main content

atmos/os_lib/
canvas2d.rs

1//! Canvas 2D の変換行列とパス幾何(純粋モジュール)。
2//!
3//! `<canvas>` の 2D コンテキストは、現状 API 表面だけが存在し
4//! 全メソッドが `dom_noop` へ接続されている(=一切描画されない)。
5//! 実描画を入れる前提として、ラスタライズに依存しない部分——
6//! **アフィン変換の合成**と**パスの折れ線化**——をここへ集約する。
7//!
8//! ハードウェアにもグローバル状態にも依存しないので、ホスト側で検証できる。
9//!
10//! # 座標系
11//!
12//! 変換行列は CSS/Canvas と同じ `[a b c d e f]` の並びで、点 (x, y) は
13//!
14//! ```text
15//! x' = a*x + c*y + e
16//! y' = b*x + d*y + f
17//! ```
18//!
19//! へ移る。`transform()` は**現在の行列へ右から掛ける**(後から積んだ変換が
20//! 点に対して先に効く)。この順序を取り違えると `translate` してから
21//! `rotate` した図形が原点回りに回ってしまうので、試験で固定する。
22
23extern crate alloc;
24use alloc::vec::Vec;
25
26/// アフィン変換行列 `[a b c d e f]`。
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Matrix {
29    pub a: f32,
30    pub b: f32,
31    pub c: f32,
32    pub d: f32,
33    pub e: f32,
34    pub f: f32,
35}
36
37impl Default for Matrix {
38    fn default() -> Self {
39        Self::identity()
40    }
41}
42
43impl Matrix {
44    pub fn identity() -> Self {
45        Matrix { a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: 0.0, f: 0.0 }
46    }
47
48    pub fn translate(tx: f32, ty: f32) -> Self {
49        Matrix { a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: tx, f: ty }
50    }
51
52    pub fn scale(sx: f32, sy: f32) -> Self {
53        Matrix { a: sx, b: 0.0, c: 0.0, d: sy, e: 0.0, f: 0.0 }
54    }
55
56    /// 角度はラジアン。正の角度で「x 軸から y 軸へ」回る(Canvas の既定は
57    /// y 軸が下向きなので、画面上は時計回りに見える)。
58    pub fn rotate(rad: f32) -> Self {
59        let (s, c) = (libm::sinf(rad), libm::cosf(rad));
60        Matrix { a: c, b: s, c: -s, d: c, e: 0.0, f: 0.0 }
61    }
62
63    /// `self` に `m` を**右から**掛ける(`self * m`)。
64    ///
65    /// Canvas の `ctx.transform(...)` の意味論。後から積んだ変換ほど
66    /// 点に対して先に効く。
67    pub fn multiply(&self, m: &Matrix) -> Matrix {
68        Matrix {
69            a: self.a * m.a + self.c * m.b,
70            b: self.b * m.a + self.d * m.b,
71            c: self.a * m.c + self.c * m.d,
72            d: self.b * m.c + self.d * m.d,
73            e: self.a * m.e + self.c * m.f + self.e,
74            f: self.b * m.e + self.d * m.f + self.f,
75        }
76    }
77
78    /// 点を変換する。
79    pub fn apply(&self, x: f32, y: f32) -> (f32, f32) {
80        (
81            self.a * x + self.c * y + self.e,
82            self.b * x + self.d * y + self.f,
83        )
84    }
85
86    /// 行列式。0 なら退化(逆行列を持たない)。
87    pub fn determinant(&self) -> f32 {
88        self.a * self.d - self.b * self.c
89    }
90
91    /// 逆行列。退化していれば `None`(呼び出し側は変換を諦める)。
92    ///
93    /// `isPointInPath` のようにデバイス座標をユーザー座標へ戻す用途で要る。
94    pub fn invert(&self) -> Option<Matrix> {
95        let det = self.determinant();
96        if !det.is_finite() || libm::fabsf(det) < 1e-12 {
97            return None;
98        }
99        let inv = 1.0 / det;
100        Some(Matrix {
101            a: self.d * inv,
102            b: -self.b * inv,
103            c: -self.c * inv,
104            d: self.a * inv,
105            e: (self.c * self.f - self.d * self.e) * inv,
106            f: (self.b * self.e - self.a * self.f) * inv,
107        })
108    }
109}
110
111/// パスを構成する副パス(連続した折れ線)。
112#[derive(Debug, Clone, PartialEq)]
113pub struct SubPath {
114    /// 折れ線の頂点(変換適用済みのデバイス座標)。
115    pub points: Vec<(f32, f32)>,
116    /// `closePath()` で閉じられたか。
117    pub closed: bool,
118}
119
120/// パス構築器。曲線は折れ線へ分割して保持する。
121///
122/// ラスタライザは「折れ線の集まり」だけを相手にすればよくなるので、
123/// 曲線の分割規則をここへ閉じ込めて試験できる。
124#[derive(Debug, Clone, Default)]
125pub struct PathBuilder {
126    subpaths: Vec<SubPath>,
127    current: Option<SubPath>,
128    /// 直近の点(**変換前**のユーザー座標)。曲線の始点に使う。
129    last_user: Option<(f32, f32)>,
130}
131
132/// 円弧を折れ線化するときの、90 度あたりの分割数。
133const ARC_SEGMENTS_PER_QUARTER: usize = 8;
134/// ベジェ 1 本あたりの分割数。
135const BEZIER_SEGMENTS: usize = 16;
136
137impl PathBuilder {
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    /// 現在の副パスを確定して次へ移る。
143    ///
144    /// 点が 1 個以下の副パスは描画に寄与しないので捨てる
145    /// (`moveTo` だけして何も引かなかった場合など)。
146    fn flush(&mut self) {
147        if let Some(sp) = self.current.take() {
148            if sp.points.len() >= 2 {
149                self.subpaths.push(sp);
150            }
151        }
152    }
153
154    pub fn begin_path(&mut self) {
155        self.subpaths.clear();
156        self.current = None;
157        self.last_user = None;
158    }
159
160    pub fn move_to(&mut self, m: &Matrix, x: f32, y: f32) {
161        self.flush();
162        self.current = Some(SubPath {
163            points: alloc::vec![m.apply(x, y)],
164            closed: false,
165        });
166        self.last_user = Some((x, y));
167    }
168
169    pub fn line_to(&mut self, m: &Matrix, x: f32, y: f32) {
170        // `moveTo` 無しの `lineTo` は、仕様上その点への `moveTo` と同じ扱い。
171        if self.current.is_none() {
172            self.move_to(m, x, y);
173            return;
174        }
175        if let Some(sp) = self.current.as_mut() {
176            sp.points.push(m.apply(x, y));
177        }
178        self.last_user = Some((x, y));
179    }
180
181    pub fn close_path(&mut self) {
182        if let Some(sp) = self.current.as_mut() {
183            sp.closed = true;
184        }
185        self.flush();
186    }
187
188    /// 3 次ベジェ。始点は直近の点。
189    pub fn bezier_curve_to(
190        &mut self,
191        m: &Matrix,
192        c1x: f32,
193        c1y: f32,
194        c2x: f32,
195        c2y: f32,
196        x: f32,
197        y: f32,
198    ) {
199        let (x0, y0) = match self.last_user {
200            Some(p) => p,
201            None => {
202                self.move_to(m, c1x, c1y);
203                (c1x, c1y)
204            }
205        };
206        for i in 1..=BEZIER_SEGMENTS {
207            let t = i as f32 / BEZIER_SEGMENTS as f32;
208            let mt = 1.0 - t;
209            let px = mt * mt * mt * x0
210                + 3.0 * mt * mt * t * c1x
211                + 3.0 * mt * t * t * c2x
212                + t * t * t * x;
213            let py = mt * mt * mt * y0
214                + 3.0 * mt * mt * t * c1y
215                + 3.0 * mt * t * t * c2y
216                + t * t * t * y;
217            self.line_to(m, px, py);
218        }
219        self.last_user = Some((x, y));
220    }
221
222    /// 2 次ベジェ。3 次へ昇格させて処理する。
223    pub fn quadratic_curve_to(&mut self, m: &Matrix, cx: f32, cy: f32, x: f32, y: f32) {
224        let (x0, y0) = match self.last_user {
225            Some(p) => p,
226            None => {
227                self.move_to(m, cx, cy);
228                (cx, cy)
229            }
230        };
231        // 2 次 → 3 次の標準的な昇格。
232        let c1x = x0 + 2.0 / 3.0 * (cx - x0);
233        let c1y = y0 + 2.0 / 3.0 * (cy - y0);
234        let c2x = x + 2.0 / 3.0 * (cx - x);
235        let c2y = y + 2.0 / 3.0 * (cy - y);
236        self.bezier_curve_to(m, c1x, c1y, c2x, c2y, x, y);
237    }
238
239    /// 円弧。`anticlockwise` が真なら反時計回り。
240    pub fn arc(
241        &mut self,
242        m: &Matrix,
243        cx: f32,
244        cy: f32,
245        r: f32,
246        start: f32,
247        end: f32,
248        anticlockwise: bool,
249    ) {
250        // 負の半径・非有限値は仕様上エラーだが、ここでは黙って無視する
251        // (描画系でパニックさせない方が安全)。
252        if !r.is_finite() || r < 0.0 || !start.is_finite() || !end.is_finite() {
253            return;
254        }
255        let sweep = normalize_sweep(start, end, anticlockwise);
256        let steps = arc_steps(sweep);
257        for i in 0..=steps {
258            let t = i as f32 / steps as f32;
259            let ang = start + sweep * t;
260            let px = cx + r * libm::cosf(ang);
261            let py = cy + r * libm::sinf(ang);
262            if i == 0 && self.current.is_none() {
263                self.move_to(m, px, py);
264            } else {
265                self.line_to(m, px, py);
266            }
267        }
268    }
269
270    /// 矩形。独立した閉じた副パスになる。
271    pub fn rect(&mut self, m: &Matrix, x: f32, y: f32, w: f32, h: f32) {
272        self.flush();
273        self.current = Some(SubPath {
274            points: alloc::vec![
275                m.apply(x, y),
276                m.apply(x + w, y),
277                m.apply(x + w, y + h),
278                m.apply(x, y + h),
279            ],
280            closed: true,
281        });
282        self.flush();
283        self.last_user = Some((x, y));
284    }
285
286    /// 構築済みの副パス一覧を返す(未確定の副パスも含める)。
287    pub fn finish(&self) -> Vec<SubPath> {
288        let mut out = self.subpaths.clone();
289        if let Some(sp) = &self.current {
290            if sp.points.len() >= 2 {
291                out.push(sp.clone());
292            }
293        }
294        out
295    }
296}
297
298/// 円弧の掃引角を求める。
299///
300/// Canvas の仕様では、時計回り指定で `end < start` のときは 2π を足し、
301/// 反時計回り指定で `end > start` のときは 2π を引く。
302/// 差が 2π を超える場合は 1 周に丸める(分割数が発散しないように)。
303pub fn normalize_sweep(start: f32, end: f32, anticlockwise: bool) -> f32 {
304    let tau = core::f32::consts::PI * 2.0;
305    let mut sweep = end - start;
306    if anticlockwise {
307        if sweep > 0.0 {
308            sweep -= tau;
309        }
310        if sweep < -tau {
311            sweep = -tau;
312        }
313    } else {
314        if sweep < 0.0 {
315            sweep += tau;
316        }
317        if sweep > tau {
318            sweep = tau;
319        }
320    }
321    sweep
322}
323
324/// 掃引角に応じた分割数(最低 1)。
325pub fn arc_steps(sweep: f32) -> usize {
326    let quarters = libm::fabsf(sweep) / (core::f32::consts::PI / 2.0);
327    let n = (quarters * ARC_SEGMENTS_PER_QUARTER as f32) as usize;
328    n.max(1)
329}
330
331// ───────────── ラスタライズ(走査線) ─────────────
332
333/// 塗りつぶし規則。
334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub enum FillRule {
336    /// `nonzero`(既定)。巻き数が 0 でなければ内側。
337    NonZero,
338    /// `evenodd`。交差回数が奇数なら内側。
339    EvenOdd,
340}
341
342/// 走査線 `y` とパスの交点を求め、塗るべき x 区間を返す(純粋関数)。
343///
344/// 返り値は `(x_start, x_end)` の並びで、いずれも**昇順・非重複**。
345/// 閉じていない副パスも、塗りのときは仕様上「閉じているものとして」扱う。
346///
347/// # なぜ純粋関数にするか
348///
349/// 巻き数の数え方(`nonzero` と `evenodd` の違い)と、頂点をちょうど
350/// 走査線が通るときの二重計上は、目視では気づけない典型的なバグ源。
351/// ここを試験で固定しておけば、実際のピクセル書き込み側は
352/// 「区間を塗るだけ」の単純な処理に保てる。
353///
354/// 計算量: **O(辺の数 log 辺の数)**(交点の整列ぶん)。
355pub fn scanline_spans(subpaths: &[SubPath], y: f32, rule: FillRule) -> Vec<(f32, f32)> {
356    // (x, 巻き方向) の組を集める。
357    let mut hits: Vec<(f32, i32)> = Vec::new();
358    for sp in subpaths {
359        let n = sp.points.len();
360        if n < 2 {
361            continue;
362        }
363        // 塗りでは常に閉じたものとして扱う(最後の点と最初の点を結ぶ)。
364        for i in 0..n {
365            let (x0, y0) = sp.points[i];
366            let (x1, y1) = sp.points[(i + 1) % n];
367            if !y0.is_finite() || !y1.is_finite() || !x0.is_finite() || !x1.is_finite() {
368                continue;
369            }
370            // 水平な辺は交差を作らない。
371            if (y0 - y1).abs() < f32::EPSILON {
372                continue;
373            }
374            // 半開区間 [min, max) で判定する。
375            //
376            // これをしないと、頂点をちょうど走査線が通るときに
377            // 上下 2 本の辺が同じ交点を二重に数え、`evenodd` で
378            // 内外が反転する(塗り残し・塗り過ぎの典型的な原因)。
379            let (ylo, yhi) = if y0 < y1 { (y0, y1) } else { (y1, y0) };
380            if y < ylo || y >= yhi {
381                continue;
382            }
383            let t = (y - y0) / (y1 - y0);
384            let x = x0 + t * (x1 - x0);
385            // 下向きの辺を +1、上向きを -1 とする。
386            let dir = if y1 > y0 { 1 } else { -1 };
387            hits.push((x, dir));
388        }
389    }
390    if hits.is_empty() {
391        return Vec::new();
392    }
393    // x 昇順に並べる(NaN は上で除外済み)。
394    hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
395
396    let mut spans = Vec::new();
397    match rule {
398        FillRule::EvenOdd => {
399            let mut i = 0;
400            while i + 1 < hits.len() {
401                spans.push((hits[i].0, hits[i + 1].0));
402                i += 2;
403            }
404        }
405        FillRule::NonZero => {
406            let mut winding = 0i32;
407            let mut start = 0.0f32;
408            for &(x, dir) in &hits {
409                let was_inside = winding != 0;
410                winding += dir;
411                let now_inside = winding != 0;
412                if !was_inside && now_inside {
413                    start = x;
414                } else if was_inside && !now_inside {
415                    spans.push((start, x));
416                }
417            }
418        }
419    }
420    spans
421}
422
423
424// ───────────── バッキングストアへの描画 ─────────────
425
426/// Canvas のバッキングストア(ARGB8888 のピクセル配列)。
427///
428/// 画面へ直接描かず、まずここへ描く。`<canvas>` は
429/// 「レイアウトで決まった矩形へ、自分のピクセルを貼る」要素なので、
430/// 画面座標ではなく**キャンバス内座標**で完結させる必要がある。
431#[derive(Debug, Clone)]
432pub struct Surface {
433    pub width: u32,
434    pub height: u32,
435    /// 長さは `width * height`。各要素は 0xAARRGGBB。
436    pub pixels: Vec<u32>,
437    /// 有効なクリップ領域。`None` なら全面に描ける。
438    ///
439    /// 状態側(`DrawState`)が持ち主で、描画の直前に `sync_clip` で写す。
440    /// 塗りの実装すべてが最後は `blend_pixel` を通るので、
441    /// ここで一度だけ見れば経路の取りこぼしが起きない。
442    pub clip: Option<alloc::sync::Arc<ClipMask>>,
443}
444
445impl Surface {
446    /// 透明で初期化した面を作る。
447    ///
448    /// 面積が過大な場合は `None`(ヒープを食い潰さないための上限)。
449    /// 実サイトの canvas は大きくても数千×数千なので、
450    /// 4096×4096(約 64MB)を上限とする。
451    pub fn new(width: u32, height: u32) -> Option<Self> {
452        const MAX_SIDE: u32 = 4096;
453        if width == 0 || height == 0 || width > MAX_SIDE || height > MAX_SIDE {
454            return None;
455        }
456        let n = (width as usize).checked_mul(height as usize)?;
457        Some(Surface {
458            width,
459            height,
460            pixels: alloc::vec![0u32; n],
461            clip: None,
462        })
463    }
464
465    /// 1 ピクセルを source-over で合成する。範囲外は黙って無視する。
466    pub fn blend_pixel(&mut self, x: i32, y: i32, argb: u32) {
467        if x < 0 || y < 0 || x as u32 >= self.width || y as u32 >= self.height {
468            return;
469        }
470        if let Some(c) = &self.clip {
471            if !c.allows(x, y) {
472                return;
473            }
474        }
475        let a = (argb >> 24) & 0xFF;
476        if a == 0 {
477            return;
478        }
479        let idx = y as usize * self.width as usize + x as usize;
480        if a == 255 {
481            self.pixels[idx] = argb;
482            return;
483        }
484        let dst = self.pixels[idx];
485        let inv = 255 - a;
486        // source-over: out = src + dst * (1 - srcA)
487        let da = (dst >> 24) & 0xFF;
488        let out_a = a + da * inv / 255;
489        let mix = |shift: u32| -> u32 {
490            let s = (argb >> shift) & 0xFF;
491            let d = (dst >> shift) & 0xFF;
492            (s * a + d * inv) / 255
493        };
494        self.pixels[idx] =
495            (out_a.min(255) << 24) | (mix(16) << 16) | (mix(8) << 8) | mix(0);
496    }
497
498    /// 矩形を塗りつぶす(`clearRect` は `argb=0` かつ `replace=true` で使う)。
499    ///
500    /// `replace` が真なら合成せず上書きする(`clearRect` の意味論)。
501    pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, argb: u32, replace: bool) {
502        mark_canvas_dirty();
503        if w <= 0 || h <= 0 {
504            return;
505        }
506        let x0 = x.max(0);
507        let y0 = y.max(0);
508        let x1 = (x.saturating_add(w)).min(self.width as i32);
509        let y1 = (y.saturating_add(h)).min(self.height as i32);
510        for py in y0..y1 {
511            for px in x0..x1 {
512                if replace {
513                    // `clearRect` も合成を飛ばすだけで、クリップは効く(仕様)。
514                    // ここは `blend_pixel` を通らないので個別に見る。
515                    if let Some(c) = &self.clip {
516                        if !c.allows(px, py) {
517                            continue;
518                        }
519                    }
520                    let idx = py as usize * self.width as usize + px as usize;
521                    self.pixels[idx] = argb;
522                } else {
523                    self.blend_pixel(px, py, argb);
524                }
525            }
526        }
527    }
528
529    /// パスを塗りつぶす。
530    ///
531    /// 走査線はピクセル中心(`y + 0.5`)で判定する。境界ちょうどの
532    /// 走査線を使うと、隣り合う図形の継ぎ目に隙間や二重塗りが出る。
533    pub fn fill_path(&mut self, subpaths: &[SubPath], rule: FillRule, argb: u32) {
534        mark_canvas_dirty();
535        if subpaths.is_empty() {
536            return;
537        }
538        // 走査する y 範囲をパスの境界から絞る(全面走査を避ける)。
539        let (mut ymin, mut ymax) = (f32::MAX, f32::MIN);
540        for sp in subpaths {
541            for &(_, py) in &sp.points {
542                if py.is_finite() {
543                    ymin = ymin.min(py);
544                    ymax = ymax.max(py);
545                }
546            }
547        }
548        if ymin > ymax {
549            return;
550        }
551        let y_start = (libm::floorf(ymin) as i32).max(0);
552        let y_end = (libm::ceilf(ymax) as i32).min(self.height as i32);
553        for py in y_start..y_end {
554            let spans = scanline_spans(subpaths, py as f32 + 0.5, rule);
555            for (sx, ex) in spans {
556                // 区間の両端はピクセル境界へ丸める。
557                let x0 = (libm::roundf(sx) as i32).max(0);
558                let x1 = (libm::roundf(ex) as i32).min(self.width as i32);
559                for px in x0..x1 {
560                    self.blend_pixel(px, py, argb);
561                }
562            }
563        }
564    }
565
566    /// 折れ線を線幅 1 で描く(`stroke` の最小実装)。
567    ///
568    /// 線幅・線端・線結合は未対応。まず「線が見える」ところまでを担保する。
569    pub fn stroke_path(&mut self, subpaths: &[SubPath], argb: u32) {
570        mark_canvas_dirty();
571        for sp in subpaths {
572            let n = sp.points.len();
573            if n < 2 {
574                continue;
575            }
576            let last = if sp.closed { n } else { n - 1 };
577            for i in 0..last {
578                let (x0, y0) = sp.points[i];
579                let (x1, y1) = sp.points[(i + 1) % n];
580                self.draw_line(x0, y0, x1, y1, argb);
581            }
582        }
583    }
584
585    /// Bresenham で線分を引く。非有限座標は無視する。
586    fn draw_line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, argb: u32) {
587        if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
588            return;
589        }
590        let mut x = libm::roundf(x0) as i32;
591        let mut y = libm::roundf(y0) as i32;
592        let xe = libm::roundf(x1) as i32;
593        let ye = libm::roundf(y1) as i32;
594        let dx = (xe - x).abs();
595        let dy = -(ye - y).abs();
596        let sx = if x < xe { 1 } else { -1 };
597        let sy = if y < ye { 1 } else { -1 };
598        let mut err = dx + dy;
599        // 上限を設けて、万一の座標異常でも無限ループにしない。
600        let mut guard = 0i32;
601        let limit = (self.width as i32 + self.height as i32) * 4 + 16;
602        loop {
603            self.blend_pixel(x, y, argb);
604            if (x == xe && y == ye) || guard > limit {
605                break;
606            }
607            guard += 1;
608            let e2 = err * 2;
609            if e2 >= dy {
610                err += dy;
611                x += sx;
612            }
613            if e2 <= dx {
614                err += dx;
615                y += sy;
616            }
617        }
618    }
619}
620
621
622// ───────────── 2D コンテキストの状態 ─────────────
623
624/// `save()`/`restore()` で退避・復帰する描画状態。
625#[derive(Debug, Clone)]
626pub struct DrawState {
627    pub transform: Matrix,
628    /// 塗り色(ARGB8888)。
629    pub fill: u32,
630    /// 線色(ARGB8888)。
631    pub stroke: u32,
632    pub line_width: f32,
633    /// `lineCap`。
634    pub line_cap: LineCap,
635    /// `lineJoin`。
636    pub line_join: LineJoin,
637    /// `miterLimit`。
638    pub miter_limit: f32,
639    /// `setLineDash` のパターン。空なら実線。
640    pub line_dash: Vec<f32>,
641    /// `lineDashOffset`。
642    pub line_dash_offset: f32,
643    /// クリップ領域。`None` なら全面。
644    pub clip: Option<alloc::sync::Arc<ClipMask>>,
645    /// `globalAlpha`(0.0〜1.0)。
646    pub global_alpha: f32,
647}
648
649impl Default for DrawState {
650    fn default() -> Self {
651        DrawState {
652            transform: Matrix::identity(),
653            // Canvas の既定は不透明な黒。
654            fill: 0xFF00_0000,
655            stroke: 0xFF00_0000,
656            line_width: 1.0,
657            line_cap: LineCap::Butt,
658            line_join: LineJoin::Miter,
659            miter_limit: DEFAULT_MITER_LIMIT,
660            line_dash: Vec::new(),
661            line_dash_offset: 0.0,
662            clip: None,
663            global_alpha: 1.0,
664        }
665    }
666}
667
668/// 1 つの `<canvas>` に対する 2D コンテキストの全状態。
669///
670/// JS 側のオブジェクトは軽いハンドルだけを持ち、実体はこれを指す。
671/// パスと面はどちらもここに持たせて、描画の途中経過が
672/// JS オブジェクトの寿命に左右されないようにする。
673#[derive(Debug)]
674pub struct Canvas2dContext {
675    pub surface: Surface,
676    pub path: PathBuilder,
677    pub state: DrawState,
678    /// `save()` の退避スタック。
679    stack: Vec<DrawState>,
680}
681
682/// `save()` の入れ子上限。
683///
684/// 際限なく積むと、壊れた(`restore` を呼ばない)スクリプトで
685/// ヒープを食い潰す。実用上 32 段もあれば足りる。
686const MAX_SAVE_DEPTH: usize = 32;
687
688impl Canvas2dContext {
689    pub fn new(width: u32, height: u32) -> Option<Self> {
690        Some(Canvas2dContext {
691            surface: Surface::new(width, height)?,
692            path: PathBuilder::new(),
693            state: DrawState::default(),
694            stack: Vec::new(),
695        })
696    }
697
698    /// 現在の状態を退避する。上限を超えたら黙って無視する
699    /// (`restore` との対応が崩れるが、落ちるよりはよい)。
700    pub fn save(&mut self) {
701        if self.stack.len() >= MAX_SAVE_DEPTH {
702            return;
703        }
704        self.stack.push(self.state.clone());
705    }
706
707    /// 直近の退避状態へ戻す。スタックが空なら何もしない
708    /// (仕様どおり。`restore` の呼び過ぎはエラーではない)。
709    pub fn restore(&mut self) {
710        if let Some(s) = self.stack.pop() {
711            self.state = s;
712        }
713    }
714
715    /// `globalAlpha` を適用した色を返す。
716    ///
717    /// アルファは掛け算で合成する。`globalAlpha=0.5` の半透明色は
718    /// さらに薄くなる(仕様どおり)。
719    pub fn apply_alpha(&self, argb: u32) -> u32 {
720        let ga = self.state.global_alpha.clamp(0.0, 1.0);
721        if ga >= 1.0 {
722            return argb;
723        }
724        let a = ((argb >> 24) & 0xFF) as f32 * ga;
725        let a = libm::roundf(a).clamp(0.0, 255.0) as u32;
726        (a << 24) | (argb & 0x00FF_FFFF)
727    }
728
729    /// 現在の変換を適用して矩形を塗る。
730    ///
731    /// 変換が平行移動と拡大だけなら矩形のまま塗れるが、回転が入ると
732    /// 軸平行でなくなる。判定を分けず、常にパス経由で塗ることで
733    /// 回転時も正しくなるようにしている。
734    pub fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
735        self.sync_clip();
736        let mut p = PathBuilder::new();
737        p.rect(&self.state.transform, x, y, w, h);
738        let color = self.apply_alpha(self.state.fill);
739        self.surface.fill_path(&p.finish(), FillRule::NonZero, color);
740    }
741
742    /// 矩形を透明で消す。`globalAlpha` の影響は受けない(仕様どおり)。
743    ///
744    /// 回転が入っていると軸平行に消せないため、パスの外接矩形を消す。
745    /// 厳密ではないが、`clearRect` に回転を掛ける使い方は稀。
746    pub fn clear_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
747        self.sync_clip();
748        let m = &self.state.transform;
749        let pts = [
750            m.apply(x, y),
751            m.apply(x + w, y),
752            m.apply(x + w, y + h),
753            m.apply(x, y + h),
754        ];
755        let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
756        for (px, py) in pts {
757            if !px.is_finite() || !py.is_finite() {
758                return;
759            }
760            x0 = x0.min(px);
761            y0 = y0.min(py);
762            x1 = x1.max(px);
763            y1 = y1.max(py);
764        }
765        self.surface.fill_rect(
766            libm::floorf(x0) as i32,
767            libm::floorf(y0) as i32,
768            libm::ceilf(x1 - x0) as i32,
769            libm::ceilf(y1 - y0) as i32,
770            0,
771            true,
772        );
773    }
774
775    /// 現在のパスを塗る。
776    pub fn fill(&mut self, rule: FillRule) {
777        self.sync_clip();
778        let color = self.apply_alpha(self.state.fill);
779        let sub = self.path.finish();
780        self.surface.fill_path(&sub, rule, color);
781    }
782
783    /// 現在のパスを線描する。
784    pub fn stroke(&mut self) {
785        self.sync_clip();
786        let color = self.apply_alpha(self.state.stroke);
787        let sub = self.path.finish();
788        self.surface.stroke_path(&sub, color);
789    }
790
791    /// 現在の変換へ平行移動を積む。
792    pub fn translate(&mut self, tx: f32, ty: f32) {
793        self.state.transform = self.state.transform.multiply(&Matrix::translate(tx, ty));
794    }
795
796    /// 現在の変換へ拡大縮小を積む。
797    pub fn scale(&mut self, sx: f32, sy: f32) {
798        self.state.transform = self.state.transform.multiply(&Matrix::scale(sx, sy));
799    }
800
801    /// 現在の変換へ回転を積む。
802    pub fn rotate(&mut self, rad: f32) {
803        self.state.transform = self.state.transform.multiply(&Matrix::rotate(rad));
804    }
805
806    /// 変換行列を直接置き換える(`setTransform`)。
807    pub fn set_transform(&mut self, m: Matrix) {
808        self.state.transform = m;
809    }
810
811    /// 変換を単位行列へ戻す(`resetTransform`)。
812    pub fn reset_transform(&mut self) {
813        self.state.transform = Matrix::identity();
814    }
815}
816
817
818// ───────────── コンテキスト登録表 ─────────────
819
820/// `<canvas>` ごとの 2D コンテキストを id で保持する表。
821///
822/// JS 側のオブジェクトは `ObjKind::Host("canvas2d:<id>")` というタグだけを
823/// 持ち、実体はここにある。JS の値へ Rust の可変状態を直接埋めると
824/// 借用と寿命の扱いが複雑になるため、既存の DOM プロキシ(style/classList)
825/// と同じ「タグで引く」流儀に合わせた。
826static CONTEXTS: spin::Mutex<alloc::collections::BTreeMap<u32, Canvas2dContext>> =
827    spin::Mutex::new(alloc::collections::BTreeMap::new());
828
829/// 次に払い出す id。
830static NEXT_ID: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(1);
831
832/// 同時に保持するコンテキスト数の上限。
833///
834/// ページ遷移のたびに増え続けると、面(最大 64MB)が積み上がって
835/// ヒープを食い潰す。上限を超えたら最も古い id から捨てる。
836const MAX_CONTEXTS: usize = 8;
837
838/// 新しいコンテキストを作り、その id を返す。
839///
840/// 面が確保できない(サイズが不正・過大)場合は `None`。
841pub fn create_context(width: u32, height: u32) -> Option<u32> {
842    let ctx = Canvas2dContext::new(width, height)?;
843    let id = NEXT_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
844    let mut map = CONTEXTS.lock();
845    // 上限を超えたら最も古い(id が小さい)ものから捨てる。
846    while map.len() >= MAX_CONTEXTS {
847        let Some(&oldest) = map.keys().next() else {
848            break;
849        };
850        map.remove(&oldest);
851    }
852    map.insert(id, ctx);
853    Some(id)
854}
855
856/// id のコンテキストへ可変アクセスする。
857///
858/// 見つからなければ `f` を呼ばず `None` を返す(ページ遷移で
859/// 破棄された後の呼び出しは黙って無視する)。
860pub fn with_context<R>(id: u32, f: impl FnOnce(&mut Canvas2dContext) -> R) -> Option<R> {
861    let mut map = CONTEXTS.lock();
862    map.get_mut(&id).map(f)
863}
864
865/// 保持している全コンテキストを破棄する(ページ遷移時に呼ぶ)。
866pub fn clear_contexts() {
867    CONTEXTS.lock().clear();
868}
869
870/// `ObjKind::Host` のタグ文字列から id を取り出す。
871///
872/// 形式は `canvas2d:<id>`。合致しなければ `None`。
873pub fn id_from_tag(tag: &str) -> Option<u32> {
874    tag.strip_prefix("canvas2d:")?.parse::<u32>().ok()
875}
876
877/// id からタグ文字列を作る。
878pub fn tag_for_id(id: u32) -> alloc::string::String {
879    alloc::format!("canvas2d:{}", id)
880}
881
882impl PathBuilder {
883    /// 楕円弧(`ellipse`)。
884    ///
885    /// `arc` の一般形で、x/y に別々の半径と、楕円自体の回転を持つ。
886    /// 単位円上の点を「半径で伸ばす → 回転する → 中心へ移す」順で写す。
887    /// この順序を取り違えると、回転した楕円が歪む。
888    #[allow(clippy::too_many_arguments)]
889    pub fn ellipse(
890        &mut self,
891        m: &Matrix,
892        cx: f32,
893        cy: f32,
894        rx: f32,
895        ry: f32,
896        rotation: f32,
897        start: f32,
898        end: f32,
899        anticlockwise: bool,
900    ) {
901        if !rx.is_finite()
902            || !ry.is_finite()
903            || rx < 0.0
904            || ry < 0.0
905            || !start.is_finite()
906            || !end.is_finite()
907            || !rotation.is_finite()
908        {
909            return;
910        }
911        let sweep = normalize_sweep(start, end, anticlockwise);
912        let steps = arc_steps(sweep);
913        let (rs, rc) = (libm::sinf(rotation), libm::cosf(rotation));
914        for i in 0..=steps {
915            let t = i as f32 / steps as f32;
916            let ang = start + sweep * t;
917            // 単位円 → 半径で伸ばす
918            let ux = rx * libm::cosf(ang);
919            let uy = ry * libm::sinf(ang);
920            // → 楕円の回転 → 中心へ移す
921            let px = cx + ux * rc - uy * rs;
922            let py = cy + ux * rs + uy * rc;
923            if i == 0 && self.current.is_none() {
924                self.move_to(m, px, py);
925            } else {
926                self.line_to(m, px, py);
927            }
928        }
929    }
930
931    /// 角丸矩形(`roundRect`)。
932    ///
933    /// 半径は 4 隅ぶん(左上・右上・右下・左下)。
934    /// 半径の合計が辺の長さを超える場合は、仕様どおり**比率を保ったまま
935    /// 縮小**する。しないと角が重なって図形が破綻する。
936    #[allow(clippy::too_many_arguments)]
937    pub fn round_rect(
938        &mut self,
939        m: &Matrix,
940        x: f32,
941        y: f32,
942        w: f32,
943        h: f32,
944        radii: [f32; 4],
945    ) {
946        if !w.is_finite() || !h.is_finite() || w == 0.0 || h == 0.0 {
947            return;
948        }
949        // 負の幅・高さは反対側から描く(仕様)。
950        let (x, w) = if w < 0.0 { (x + w, -w) } else { (x, w) };
951        let (y, h) = if h < 0.0 { (y + h, -h) } else { (y, h) };
952
953        let mut r = [0.0f32; 4];
954        for (i, v) in radii.iter().enumerate() {
955            r[i] = if v.is_finite() && *v > 0.0 { *v } else { 0.0 };
956        }
957        // 辺ごとに「両端の半径の和」が辺長を超えないよう縮小率を求める。
958        let mut scale = 1.0f32;
959        let pairs = [
960            (r[0] + r[1], w), // 上辺: 左上 + 右上
961            (r[2] + r[3], w), // 下辺: 右下 + 左下
962            (r[1] + r[2], h), // 右辺: 右上 + 右下
963            (r[3] + r[0], h), // 左辺: 左下 + 左上
964        ];
965        for (sum, len) in pairs {
966            if sum > 0.0 && sum > len {
967                scale = scale.min(len / sum);
968            }
969        }
970        for v in r.iter_mut() {
971            *v *= scale;
972        }
973
974        let half_pi = core::f32::consts::PI / 2.0;
975        let pi = core::f32::consts::PI;
976        self.flush();
977        // 左上から時計回りに、直線 → 角の 1/4 円 を繰り返す。
978        self.move_to(m, x + r[0], y);
979        self.line_to(m, x + w - r[1], y);
980        if r[1] > 0.0 {
981            self.arc(m, x + w - r[1], y + r[1], r[1], -half_pi, 0.0, false);
982        }
983        self.line_to(m, x + w, y + h - r[2]);
984        if r[2] > 0.0 {
985            self.arc(m, x + w - r[2], y + h - r[2], r[2], 0.0, half_pi, false);
986        }
987        self.line_to(m, x + r[3], y + h);
988        if r[3] > 0.0 {
989            self.arc(m, x + r[3], y + h - r[3], r[3], half_pi, pi, false);
990        }
991        self.line_to(m, x, y + r[0]);
992        if r[0] > 0.0 {
993            self.arc(m, x + r[0], y + r[0], r[0], pi, pi + half_pi, false);
994        }
995        self.close_path();
996    }
997}
998
999impl Surface {
1000    /// 線幅つきで折れ線を描く。
1001    ///
1002    /// 各線分を「線幅ぶんの太さを持つ四角形」に展開して塗る。
1003    /// 線端・線結合(`lineCap`/`lineJoin`)は未対応で、
1004    /// 結合部には円を置いて隙間を埋める(`round` 相当の近似)。
1005    ///
1006    /// # なぜ四角形へ展開するか
1007    ///
1008    /// Bresenham を平行にずらして何本も引く方法は、斜線で
1009    /// 太さが不均一になる(見た目が痩せる)。線分に垂直な方向へ
1010    /// 幅の半分ずつ広げた四角形を塗れば、角度によらず一定の太さになる。
1011    /// 線幅つきで折れ線を描く(線端 `Butt`・結合 `Round`)。
1012    ///
1013    /// 線端・結合を選ぶ場合は `stroke_path_styled` を使う。
1014    pub fn stroke_path_width(&mut self, subpaths: &[SubPath], argb: u32, width: f32) {
1015        self.stroke_path_styled(
1016            subpaths,
1017            argb,
1018            width,
1019            LineCap::Butt,
1020            LineJoin::Round,
1021            DEFAULT_MITER_LIMIT,
1022        );
1023    }
1024
1025    /// 中心 (cx, cy)・半径 r の円を塗る(線の結合部を埋めるのに使う)。
1026    fn fill_disc(&mut self, cx: f32, cy: f32, r: f32, argb: u32) {
1027        if !cx.is_finite() || !cy.is_finite() || !r.is_finite() || r <= 0.0 {
1028            return;
1029        }
1030        let y0 = (libm::floorf(cy - r) as i32).max(0);
1031        let y1 = (libm::ceilf(cy + r) as i32).min(self.height as i32);
1032        let r2 = r * r;
1033        for py in y0..y1 {
1034            let dy = py as f32 + 0.5 - cy;
1035            let d = r2 - dy * dy;
1036            if d < 0.0 {
1037                continue;
1038            }
1039            let dx = libm::sqrtf(d);
1040            let x0 = (libm::roundf(cx - dx) as i32).max(0);
1041            let x1 = (libm::roundf(cx + dx) as i32).min(self.width as i32);
1042            for px in x0..x1 {
1043                self.blend_pixel(px, py, argb);
1044            }
1045        }
1046    }
1047}
1048
1049impl Canvas2dContext {
1050    /// 現在のパスを、`lineWidth` を反映して線描する。
1051    pub fn stroke_with_width(&mut self) {
1052        self.sync_clip();
1053        let color = self.apply_alpha(self.state.stroke);
1054        let w = self.state.line_width;
1055        let (cap, join, limit) = (
1056            self.state.line_cap,
1057            self.state.line_join,
1058            self.state.miter_limit,
1059        );
1060        let sub = self.path.finish();
1061        let sub = self.apply_dash(&sub);
1062        self.surface
1063            .stroke_path_styled(&sub, color, w, cap, join, limit);
1064    }
1065}
1066
1067/// 線端の形状(`lineCap`)。
1068#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1069pub enum LineCap {
1070    /// 端点で切り落とす(既定)。
1071    Butt,
1072    /// 端点に半円を付ける。
1073    Round,
1074    /// 端点から線幅の半分だけ四角く伸ばす。
1075    Square,
1076}
1077
1078/// 線の結合形状(`lineJoin`)。
1079#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1080pub enum LineJoin {
1081    /// 外側の辺を延長して尖らせる(既定)。
1082    Miter,
1083    /// 結合点に円を置く。
1084    Round,
1085    /// 外側を直線で切り落とす。
1086    Bevel,
1087}
1088
1089/// `miterLimit` の既定値。仕様どおり 10。
1090pub const DEFAULT_MITER_LIMIT: f32 = 10.0;
1091
1092pub fn parse_line_cap(s: &str) -> Option<LineCap> {
1093    match s.trim() {
1094        "butt" => Some(LineCap::Butt),
1095        "round" => Some(LineCap::Round),
1096        "square" => Some(LineCap::Square),
1097        _ => None,
1098    }
1099}
1100
1101pub fn parse_line_join(s: &str) -> Option<LineJoin> {
1102    match s.trim() {
1103        "miter" => Some(LineJoin::Miter),
1104        "round" => Some(LineJoin::Round),
1105        "bevel" => Some(LineJoin::Bevel),
1106        _ => None,
1107    }
1108}
1109
1110/// 2 直線の交点。`p1` を通り向き `d1` の直線と、`p2` を通り向き `d2` の直線。
1111///
1112/// 平行(または反転)なら交点は無いので `None`。miter を延ばす先を
1113/// 求めるのに使い、`None` のときは bevel へ落とす。
1114pub fn line_intersection(
1115    p1: (f32, f32),
1116    d1: (f32, f32),
1117    p2: (f32, f32),
1118    d2: (f32, f32),
1119) -> Option<(f32, f32)> {
1120    let den = d1.0 * d2.1 - d1.1 * d2.0;
1121    // ほぼ平行なら交点が無限遠へ飛ぶ。閾値は目視で差が出ない範囲。
1122    if libm::fabsf(den) < 1e-6 {
1123        return None;
1124    }
1125    let t = ((p2.0 - p1.0) * d2.1 - (p2.1 - p1.1) * d2.0) / den;
1126    let x = p1.0 + d1.0 * t;
1127    let y = p1.1 + d1.1 * t;
1128    if !x.is_finite() || !y.is_finite() {
1129        return None;
1130    }
1131    Some((x, y))
1132}
1133
1134impl Surface {
1135    /// 線端・結合を指定して折れ線を描く。
1136    ///
1137    /// `stroke_path_width` は端 `Butt`・結合 `Round` でこれを呼ぶ。
1138    pub fn stroke_path_styled(
1139        &mut self,
1140        subpaths: &[SubPath],
1141        argb: u32,
1142        width: f32,
1143        cap: LineCap,
1144        join: LineJoin,
1145        miter_limit: f32,
1146    ) {
1147        mark_canvas_dirty();
1148        // 1 以下は従来の 1px 線で足りる(見た目も速度も有利)。
1149        if !width.is_finite() || width <= 1.0 {
1150            self.stroke_path(subpaths, argb);
1151            return;
1152        }
1153        let half = width / 2.0;
1154        let limit = if miter_limit.is_finite() && miter_limit > 0.0 {
1155            miter_limit
1156        } else {
1157            DEFAULT_MITER_LIMIT
1158        };
1159        for sp in subpaths {
1160            let n = sp.points.len();
1161            if n < 2 {
1162                continue;
1163            }
1164            let seg_end = if sp.closed { n } else { n - 1 };
1165
1166            // 各線分を、線分に垂直な方向へ half ずつ広げた四角形へ展開。
1167            for i in 0..seg_end {
1168                let a = sp.points[i];
1169                let b = sp.points[(i + 1) % n];
1170                let Some((dx, dy, len)) = unit_delta(a, b) else {
1171                    continue;
1172                };
1173                // 端が Square のときは、線分の外側へ half だけ伸ばす。
1174                let (mut ax, mut ay) = a;
1175                let (mut bx, mut by) = b;
1176                if cap == LineCap::Square && !sp.closed {
1177                    if i == 0 {
1178                        ax -= dx * half;
1179                        ay -= dy * half;
1180                    }
1181                    if i == seg_end - 1 {
1182                        bx += dx * half;
1183                        by += dy * half;
1184                    }
1185                }
1186                let _ = len;
1187                let (nx, ny) = (-dy * half, dx * half);
1188                self.fill_quad(
1189                    (ax + nx, ay + ny),
1190                    (bx + nx, by + ny),
1191                    (bx - nx, by - ny),
1192                    (ax - nx, ay - ny),
1193                    argb,
1194                );
1195            }
1196
1197            // 結合部を埋める。線分の四角形だけでは外側の角が欠ける。
1198            let joint_from = if sp.closed { 0 } else { 1 };
1199            let joint_to = if sp.closed { n } else { n - 1 };
1200            for j in joint_from..joint_to {
1201                let prev = sp.points[(j + n - 1) % n];
1202                let cur = sp.points[j];
1203                let next = sp.points[(j + 1) % n];
1204                self.fill_joint(prev, cur, next, half, argb, join, limit);
1205            }
1206
1207            // 端が Round なら両端に半円(円で近似)を置く。
1208            if cap == LineCap::Round && !sp.closed {
1209                let first = sp.points[0];
1210                let last = sp.points[n - 1];
1211                self.fill_disc(first.0, first.1, half, argb);
1212                self.fill_disc(last.0, last.1, half, argb);
1213            }
1214        }
1215    }
1216
1217    /// 結合部 1 箇所を埋める。
1218    #[allow(clippy::too_many_arguments)]
1219    fn fill_joint(
1220        &mut self,
1221        prev: (f32, f32),
1222        cur: (f32, f32),
1223        next: (f32, f32),
1224        half: f32,
1225        argb: u32,
1226        join: LineJoin,
1227        limit: f32,
1228    ) {
1229        if join == LineJoin::Round {
1230            self.fill_disc(cur.0, cur.1, half, argb);
1231            return;
1232        }
1233        let (Some((d1x, d1y, _)), Some((d2x, d2y, _))) =
1234            (unit_delta(prev, cur), unit_delta(cur, next))
1235        else {
1236            return;
1237        };
1238        // 曲がる向き。外側だけを埋めれば足りる(内側は線分の四角形が覆う)。
1239        let cross = d1x * d2y - d1y * d2x;
1240        if libm::fabsf(cross) < 1e-6 {
1241            // まっすぐ、または折り返し。角は生じないので何もしない。
1242            return;
1243        }
1244        // 左法線を (-dy, dx) とすると、左折(cross>0)では外側は右。
1245        let s = if cross > 0.0 { -1.0f32 } else { 1.0f32 };
1246        let p1 = (cur.0 + s * -d1y * half, cur.1 + s * d1x * half);
1247        let p2 = (cur.0 + s * -d2y * half, cur.1 + s * d2x * half);
1248
1249        if join == LineJoin::Miter {
1250            if let Some(mp) = line_intersection(p1, (d1x, d1y), p2, (d2x, d2y)) {
1251                let (mx, my) = (mp.0 - cur.0, mp.1 - cur.1);
1252                let miter_len = libm::sqrtf(mx * mx + my * my);
1253                // 鋭角では尖りが際限なく伸びる。仕様どおり
1254                // miterLength/halfWidth が上限を超えたら bevel へ落とす。
1255                if miter_len / half <= limit {
1256                    self.fill_quad(cur, p1, mp, p2, argb);
1257                    return;
1258                }
1259            }
1260        }
1261        // Bevel(および miter の打ち切り)。外側を直線で結ぶ三角形。
1262        self.fill_tri(cur, p1, p2, argb);
1263    }
1264
1265    fn fill_quad(
1266        &mut self,
1267        a: (f32, f32),
1268        b: (f32, f32),
1269        c: (f32, f32),
1270        d: (f32, f32),
1271        argb: u32,
1272    ) {
1273        for p in [a, b, c, d] {
1274            if !p.0.is_finite() || !p.1.is_finite() {
1275                return;
1276            }
1277        }
1278        let poly = alloc::vec![SubPath {
1279            points: alloc::vec![a, b, c, d],
1280            closed: true,
1281        }];
1282        self.fill_path(&poly, FillRule::NonZero, argb);
1283    }
1284
1285    fn fill_tri(&mut self, a: (f32, f32), b: (f32, f32), c: (f32, f32), argb: u32) {
1286        for p in [a, b, c] {
1287            if !p.0.is_finite() || !p.1.is_finite() {
1288                return;
1289            }
1290        }
1291        let poly = alloc::vec![SubPath {
1292            points: alloc::vec![a, b, c],
1293            closed: true,
1294        }];
1295        self.fill_path(&poly, FillRule::NonZero, argb);
1296    }
1297}
1298
1299/// `a` から `b` への単位ベクトルと距離。長さが 0 なら `None`。
1300pub fn unit_delta(a: (f32, f32), b: (f32, f32)) -> Option<(f32, f32, f32)> {
1301    let (dx, dy) = (b.0 - a.0, b.1 - a.1);
1302    if !dx.is_finite() || !dy.is_finite() {
1303        return None;
1304    }
1305    let len = libm::sqrtf(dx * dx + dy * dy);
1306    if len < 1e-6 {
1307        return None;
1308    }
1309    Some((dx / len, dy / len, len))
1310}
1311
1312/// JS が canvas へ描いたことを示す旗(エッジトリガ再描画用)。
1313///
1314/// canvas への描画はバッキングストア(`Surface`)を書き換えるだけで、
1315/// 画面には出ない。以前は「他の理由で再描画が起きたとき」にだけ
1316/// ついでに反映されていたため、JS が描いても画面が変わらないことがあった。
1317///
1318/// 立てっぱなしにすると毎フレーム全画面を描き直して画面が波打つので、
1319/// 画像完了(`image_results_ready`)と同じく**変化があった一度だけ**
1320/// 再描画させる。描画側が `take_canvas_dirty` で降ろす。
1321static CANVAS_DIRTY: core::sync::atomic::AtomicBool =
1322    core::sync::atomic::AtomicBool::new(false);
1323
1324/// 画素が変わったことを記録する。`Surface` の塗り口から呼ぶ。
1325pub fn mark_canvas_dirty() {
1326    CANVAS_DIRTY.store(true, core::sync::atomic::Ordering::Relaxed);
1327}
1328
1329/// 変化があったかを見る(降ろさない)。再描画するかの判断に使う。
1330pub fn canvas_dirty() -> bool {
1331    CANVAS_DIRTY.load(core::sync::atomic::Ordering::Relaxed)
1332}
1333
1334/// 変化があったかを見て、同時に降ろす。描画を始める側から呼ぶ。
1335pub fn take_canvas_dirty() -> bool {
1336    CANVAS_DIRTY.swap(false, core::sync::atomic::Ordering::Relaxed)
1337}
1338
1339/// 破線のパターンを正規化する(`setLineDash` の引数検査)。
1340///
1341/// 仕様では、負の値や非有限が混じるパターンは**まるごと捨てる**
1342/// (一部だけ直すのではない)。奇数個なら 2 周ぶんに複製して偶数個にする。
1343/// 全部 0 のパターンは「破線なし」と同じ扱い。
1344pub fn normalize_dash(pattern: &[f32]) -> Option<Vec<f32>> {
1345    if pattern.is_empty() {
1346        return None;
1347    }
1348    for v in pattern {
1349        if !v.is_finite() || *v < 0.0 {
1350            return None;
1351        }
1352    }
1353    if pattern.iter().all(|v| *v == 0.0) {
1354        return None;
1355    }
1356    let mut out: Vec<f32> = pattern.to_vec();
1357    // 奇数個なら繰り返して偶数個にする(仕様)。
1358    if out.len() % 2 == 1 {
1359        let dup = out.clone();
1360        out.extend(dup);
1361    }
1362    Some(out)
1363}
1364
1365/// 折れ線を破線に切り分ける。
1366///
1367/// `pattern` は「描く長さ・空ける長さ」の交互。`offset` はパターンの
1368/// 開始位置をずらす量(`lineDashOffset`)。戻り値は描く区間だけを集めた
1369/// 開いた折れ線の並びで、そのまま `stroke_path_styled` へ渡せる。
1370///
1371/// 線分の途中でパターンが切り替わるので、線分を長さで分割しながら進む。
1372pub fn dash_subpaths(subpaths: &[SubPath], pattern: &[f32], offset: f32) -> Vec<SubPath> {
1373    let Some(pat) = normalize_dash(pattern) else {
1374        return subpaths.to_vec();
1375    };
1376    let total: f32 = pat.iter().sum();
1377    if total <= 0.0 {
1378        return subpaths.to_vec();
1379    }
1380    let mut out: Vec<SubPath> = Vec::new();
1381    for sp in subpaths {
1382        let n = sp.points.len();
1383        if n < 2 {
1384            continue;
1385        }
1386        // パターン内の位置。offset は仕様どおり周期で巻き戻す。
1387        let mut phase = if offset.is_finite() {
1388            let m = libm::fmodf(offset, total);
1389            if m < 0.0 {
1390                m + total
1391            } else {
1392                m
1393            }
1394        } else {
1395            0.0
1396        };
1397        // phase がどの区間にいるか(偶数番が「描く」)。
1398        let mut idx = 0usize;
1399        while phase >= pat[idx] {
1400            phase -= pat[idx];
1401            idx = (idx + 1) % pat.len();
1402        }
1403        let mut drawing = idx % 2 == 0;
1404        let mut cur: Vec<(f32, f32)> = Vec::new();
1405        if drawing {
1406            cur.push(sp.points[0]);
1407        }
1408
1409        let seg_end = if sp.closed { n } else { n - 1 };
1410        for i in 0..seg_end {
1411            let a = sp.points[i];
1412            let b = sp.points[(i + 1) % n];
1413            let Some((dx, dy, len)) = unit_delta(a, b) else {
1414                continue;
1415            };
1416            let mut travelled = 0.0f32;
1417            while travelled < len {
1418                // この区間の残り長。
1419                let remain = pat[idx] - phase;
1420                let step = remain.min(len - travelled);
1421                travelled += step;
1422                phase += step;
1423                let p = (a.0 + dx * travelled, a.1 + dy * travelled);
1424                if phase >= pat[idx] - 1e-6 {
1425                    // 区間の切り替わり。描いていたなら、ここで区切る。
1426                    if drawing {
1427                        cur.push(p);
1428                        if cur.len() >= 2 {
1429                            out.push(SubPath {
1430                                points: core::mem::take(&mut cur),
1431                                closed: false,
1432                            });
1433                        } else {
1434                            cur.clear();
1435                        }
1436                    } else {
1437                        cur.clear();
1438                        cur.push(p);
1439                    }
1440                    phase = 0.0;
1441                    idx = (idx + 1) % pat.len();
1442                    drawing = !drawing;
1443                } else if drawing {
1444                    // 線分の終わりで、まだ同じ区間の途中。
1445                    cur.push(p);
1446                }
1447            }
1448        }
1449        if drawing && cur.len() >= 2 {
1450            out.push(SubPath {
1451                points: cur,
1452                closed: false,
1453            });
1454        }
1455    }
1456    out
1457}
1458
1459impl Canvas2dContext {
1460    /// 現在の破線設定をパスへ適用する。実線ならそのまま返す。
1461    pub fn apply_dash(&self, subpaths: &[SubPath]) -> Vec<SubPath> {
1462        if self.state.line_dash.is_empty() {
1463            return subpaths.to_vec();
1464        }
1465        dash_subpaths(subpaths, &self.state.line_dash, self.state.line_dash_offset)
1466    }
1467}
1468
1469// ───────────── クリップ領域 ─────────────
1470
1471/// クリップ領域(`clip()`)。画素ごとに「描いてよいか」を持つ。
1472///
1473/// 走査線ベースの塗りに合わせて、パスをその場でラスタライズして持つ。
1474/// 領域を式のまま持つと、交差のたびに判定が複雑になっていく。
1475#[derive(Debug, Clone)]
1476pub struct ClipMask {
1477    pub width: u32,
1478    pub height: u32,
1479    /// `true` の画素だけ描ける。長さは `width * height`。
1480    pub bits: Vec<bool>,
1481}
1482
1483impl ClipMask {
1484    /// パスからクリップ領域を作る。塗りと同じ走査線・同じ巻き数規則を使う。
1485    ///
1486    /// 塗りとずれた判定をすると、クリップの縁に 1px の隙間や food が出る。
1487    pub fn from_path(width: u32, height: u32, subpaths: &[SubPath], rule: FillRule) -> ClipMask {
1488        let mut bits = alloc::vec![false; (width as usize) * (height as usize)];
1489        for y in 0..height {
1490            // 塗りと同じくピクセル中心で判定する。
1491            let spans = scanline_spans(subpaths, y as f32 + 0.5, rule);
1492            for (x0, x1) in spans {
1493                let a = (libm::ceilf(x0 - 0.5) as i32).max(0);
1494                let b = (libm::ceilf(x1 - 0.5) as i32).min(width as i32);
1495                for x in a..b {
1496                    bits[y as usize * width as usize + x as usize] = true;
1497                }
1498            }
1499        }
1500        ClipMask {
1501            width,
1502            height,
1503            bits,
1504        }
1505    }
1506
1507    /// 2 つのクリップ領域の交わり。`clip()` は既存の領域を**狭める**だけで、
1508    /// 広げることはできない(仕様)。
1509    pub fn intersect(&self, other: &ClipMask) -> ClipMask {
1510        if self.width != other.width || self.height != other.height {
1511            // 大きさが食い違うことは無い想定。安全側(狭い方)へ倒す。
1512            return self.clone();
1513        }
1514        let bits = self
1515            .bits
1516            .iter()
1517            .zip(other.bits.iter())
1518            .map(|(a, b)| *a && *b)
1519            .collect();
1520        ClipMask {
1521            width: self.width,
1522            height: self.height,
1523            bits,
1524        }
1525    }
1526
1527    /// その画素に描いてよいか。範囲外は描けない。
1528    pub fn allows(&self, x: i32, y: i32) -> bool {
1529        if x < 0 || y < 0 || x as u32 >= self.width || y as u32 >= self.height {
1530            return false;
1531        }
1532        self.bits[y as usize * self.width as usize + x as usize]
1533    }
1534}
1535
1536impl Canvas2dContext {
1537    /// 現在のパスでクリップ領域を狭める(`clip()`)。
1538    ///
1539    /// `save`/`restore` で元に戻る。状態を複製するだけで復帰できるよう
1540    /// `Rc` で共有しており、色と違って**正しく戻せる**。
1541    pub fn clip(&mut self, rule: FillRule) {
1542        let sub = self.path.finish();
1543        if sub.is_empty() {
1544            return;
1545        }
1546        let mask = ClipMask::from_path(self.surface.width, self.surface.height, &sub, rule);
1547        let merged = match &self.state.clip {
1548            Some(prev) => prev.intersect(&mask),
1549            None => mask,
1550        };
1551        self.state.clip = Some(alloc::sync::Arc::new(merged));
1552    }
1553
1554    /// 描画前に、現在のクリップ領域を `Surface` へ渡す。
1555    pub fn sync_clip(&mut self) {
1556        self.surface.clip = self.state.clip.clone();
1557    }
1558}
1559
1560// ───────────── ImageData ─────────────
1561
1562/// `Surface` の一部を `ImageData` のバイト列(RGBA8・行優先)へ取り出す。
1563///
1564/// 面の外にはみ出した部分は**透明**として埋める(仕様)。
1565/// はみ出しを切り詰めると、返る配列の大きさが要求と食い違ってしまう。
1566pub fn get_image_data(s: &Surface, x: i32, y: i32, w: i32, h: i32) -> Vec<u8> {
1567    if w <= 0 || h <= 0 {
1568        return Vec::new();
1569    }
1570    let mut out = alloc::vec![0u8; (w as usize) * (h as usize) * 4];
1571    for row in 0..h {
1572        for col in 0..w {
1573            let (sx, sy) = (x + col, y + row);
1574            if sx < 0 || sy < 0 || sx as u32 >= s.width || sy as u32 >= s.height {
1575                continue;
1576            }
1577            let argb = s.pixels[sy as usize * s.width as usize + sx as usize];
1578            let o = ((row as usize) * (w as usize) + col as usize) * 4;
1579            out[o] = ((argb >> 16) & 0xFF) as u8;
1580            out[o + 1] = ((argb >> 8) & 0xFF) as u8;
1581            out[o + 2] = (argb & 0xFF) as u8;
1582            out[o + 3] = ((argb >> 24) & 0xFF) as u8;
1583        }
1584    }
1585    out
1586}
1587
1588/// `ImageData` のバイト列を `Surface` へ書き戻す。
1589///
1590/// `putImageData` は**合成しない**(上書きする)。クリップも無視する。
1591/// これは仕様で、`fill` 系とは意味論が違う。
1592pub fn put_image_data(s: &mut Surface, data: &[u8], w: i32, h: i32, x: i32, y: i32) {
1593    if w <= 0 || h <= 0 {
1594        return;
1595    }
1596    // 長さが足りない配列は、足りるぶんだけ書く(途中で落とさない)。
1597    mark_canvas_dirty();
1598    for row in 0..h {
1599        for col in 0..w {
1600            let o = ((row as usize) * (w as usize) + col as usize) * 4;
1601            if o + 3 >= data.len() {
1602                return;
1603            }
1604            let (dx, dy) = (x + col, y + row);
1605            if dx < 0 || dy < 0 || dx as u32 >= s.width || dy as u32 >= s.height {
1606                continue;
1607            }
1608            let argb = ((data[o + 3] as u32) << 24)
1609                | ((data[o] as u32) << 16)
1610                | ((data[o + 1] as u32) << 8)
1611                | (data[o + 2] as u32);
1612            s.pixels[dy as usize * s.width as usize + dx as usize] = argb;
1613        }
1614    }
1615}
1616
1617/// canvas の大きさ変更。`<canvas width>`/`height` への代入で呼ぶ。
1618///
1619/// 仕様では、大きさを代入すると**同じ値でも**内容と描画状態が
1620/// まるごと初期化される。これを省くと、大きさを設定し直す定型句
1621/// (`canvas.width = canvas.width` による消去)が効かなくなる。
1622///
1623/// 大きさが上限を超える等で面が作れなければ何もしない(元の面を残す)。
1624pub fn resize_context(id: u32, width: u32, height: u32) -> bool {
1625    let Some(fresh) = Surface::new(width, height) else {
1626        return false;
1627    };
1628    mark_canvas_dirty();
1629    with_context(id, |c| {
1630        c.surface = fresh;
1631        c.state = DrawState::default();
1632        c.path = PathBuilder::new();
1633        c.stack.clear();
1634    })
1635    .is_some()
1636}
1637
1638// ───────────── 画像の転送 ─────────────
1639
1640/// 転送元の画素(RGBA8・行優先)。
1641pub struct ImageSource<'a> {
1642    pub width: u32,
1643    pub height: u32,
1644    pub rgba: &'a [u8],
1645}
1646
1647impl ImageSource<'_> {
1648    /// 最近傍で 1 画素を読む。範囲外は透明。
1649    fn sample(&self, x: i32, y: i32) -> u32 {
1650        if x < 0 || y < 0 || x as u32 >= self.width || y as u32 >= self.height {
1651            return 0;
1652        }
1653        let o = (y as usize * self.width as usize + x as usize) * 4;
1654        if o + 3 >= self.rgba.len() {
1655            return 0;
1656        }
1657        ((self.rgba[o + 3] as u32) << 24)
1658            | ((self.rgba[o] as u32) << 16)
1659            | ((self.rgba[o + 1] as u32) << 8)
1660            | (self.rgba[o + 2] as u32)
1661    }
1662}
1663
1664/// 転送の指定(`drawImage` の 9 引数形に対応)。
1665#[derive(Debug, Clone, Copy)]
1666pub struct BlitSpec {
1667    /// 転送元の矩形。
1668    pub sx: f32,
1669    pub sy: f32,
1670    pub sw: f32,
1671    pub sh: f32,
1672    /// 転送先の矩形(変換前の canvas 座標)。
1673    pub dx: f32,
1674    pub dy: f32,
1675    pub dw: f32,
1676    pub dh: f32,
1677}
1678
1679impl Surface {
1680    /// 画像を現在の変換のもとで転送する(`drawImage`)。
1681    ///
1682    /// # なぜ逆変換で引くか
1683    ///
1684    /// 転送元を 1 画素ずつ変換先へ「押し出す」と、拡大時に隙間が空き
1685    /// 回転時には縞が出る。転送先の画素ごとに**逆変換で元を引く**と、
1686    /// どの倍率・角度でも穴が空かない。
1687    ///
1688    /// 変換が退化している(面積 0 に潰れる)場合は何もしない。
1689    pub fn draw_image(&mut self, src: &ImageSource, spec: &BlitSpec, m: &Matrix, alpha: f32) {
1690        if spec.dw == 0.0 || spec.dh == 0.0 || spec.sw == 0.0 || spec.sh == 0.0 {
1691            return;
1692        }
1693        for v in [
1694            spec.sx, spec.sy, spec.sw, spec.sh, spec.dx, spec.dy, spec.dw, spec.dh,
1695        ] {
1696            if !v.is_finite() {
1697                return;
1698            }
1699        }
1700        let Some(inv) = m.invert() else {
1701            return;
1702        };
1703        mark_canvas_dirty();
1704
1705        // 転送先の 4 隅を変換して、走査すべき範囲を求める。
1706        let corners = [
1707            m.apply(spec.dx, spec.dy),
1708            m.apply(spec.dx + spec.dw, spec.dy),
1709            m.apply(spec.dx + spec.dw, spec.dy + spec.dh),
1710            m.apply(spec.dx, spec.dy + spec.dh),
1711        ];
1712        let mut x0 = corners[0].0;
1713        let mut x1 = corners[0].0;
1714        let mut y0 = corners[0].1;
1715        let mut y1 = corners[0].1;
1716        for (cx, cy) in corners.iter().skip(1) {
1717            x0 = x0.min(*cx);
1718            x1 = x1.max(*cx);
1719            y0 = y0.min(*cy);
1720            y1 = y1.max(*cy);
1721        }
1722        let px0 = (libm::floorf(x0) as i32).max(0);
1723        let px1 = (libm::ceilf(x1) as i32).min(self.width as i32);
1724        let py0 = (libm::floorf(y0) as i32).max(0);
1725        let py1 = (libm::ceilf(y1) as i32).min(self.height as i32);
1726
1727        let a = alpha.clamp(0.0, 1.0);
1728        for py in py0..py1 {
1729            for px in px0..px1 {
1730                // 画素中心を逆変換して、変換前の canvas 座標へ戻す。
1731                let (ux, uy) = inv.apply(px as f32 + 0.5, py as f32 + 0.5);
1732                // 転送先の矩形の中だけを描く。
1733                let tx = (ux - spec.dx) / spec.dw;
1734                let ty = (uy - spec.dy) / spec.dh;
1735                if !(0.0..1.0).contains(&tx) || !(0.0..1.0).contains(&ty) {
1736                    continue;
1737                }
1738                // 転送元の対応位置(最近傍)。
1739                let sxf = spec.sx + tx * spec.sw;
1740                let syf = spec.sy + ty * spec.sh;
1741                let argb = src.sample(libm::floorf(sxf) as i32, libm::floorf(syf) as i32);
1742                if argb >> 24 == 0 {
1743                    continue;
1744                }
1745                let argb = if a >= 1.0 {
1746                    argb
1747                } else {
1748                    let na = (((argb >> 24) & 0xFF) as f32 * a) as u32;
1749                    (na << 24) | (argb & 0x00FF_FFFF)
1750                };
1751                self.blend_pixel(px, py, argb);
1752            }
1753        }
1754    }
1755}
1756
1757// ───────────── テキスト ─────────────
1758
1759/// `textAlign`。
1760#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1761pub enum TextAlign {
1762    Start,
1763    End,
1764    Left,
1765    Right,
1766    Center,
1767}
1768
1769/// `textBaseline`。
1770#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1771pub enum TextBaseline {
1772    Top,
1773    Hanging,
1774    Middle,
1775    Alphabetic,
1776    Ideographic,
1777    Bottom,
1778}
1779
1780pub fn parse_text_align(s: &str) -> Option<TextAlign> {
1781    match s.trim() {
1782        "start" => Some(TextAlign::Start),
1783        "end" => Some(TextAlign::End),
1784        "left" => Some(TextAlign::Left),
1785        "right" => Some(TextAlign::Right),
1786        "center" => Some(TextAlign::Center),
1787        _ => None,
1788    }
1789}
1790
1791pub fn parse_text_baseline(s: &str) -> Option<TextBaseline> {
1792    match s.trim() {
1793        "top" => Some(TextBaseline::Top),
1794        "hanging" => Some(TextBaseline::Hanging),
1795        "middle" => Some(TextBaseline::Middle),
1796        "alphabetic" => Some(TextBaseline::Alphabetic),
1797        "ideographic" => Some(TextBaseline::Ideographic),
1798        "bottom" => Some(TextBaseline::Bottom),
1799        _ => None,
1800    }
1801}
1802
1803/// 指定位置から見た、描き始めの x のずれ。
1804///
1805/// `start`/`end` は書字方向で決まる。この処理系は左から右のみ扱うので
1806/// `start` は `left`、`end` は `right` と同じになる。
1807pub fn align_offset(align: TextAlign, total_advance: f32) -> f32 {
1808    match align {
1809        TextAlign::Start | TextAlign::Left => 0.0,
1810        TextAlign::End | TextAlign::Right => -total_advance,
1811        TextAlign::Center => -total_advance / 2.0,
1812    }
1813}
1814
1815/// 指定位置から見た、ベースラインの y のずれ。
1816///
1817/// 字形はベースライン基準で置かれるので、`top` 指定なら
1818/// ベースラインを下げる(正の向きへずらす)。
1819/// 比率はこの処理系の字形配置(アセンダ 0.85)に合わせた近似で、
1820/// フォントごとの実測値ではない。
1821pub fn baseline_offset(baseline: TextBaseline, size_px: f32) -> f32 {
1822    match baseline {
1823        TextBaseline::Alphabetic => 0.0,
1824        TextBaseline::Top => size_px * 0.85,
1825        TextBaseline::Hanging => size_px * 0.8,
1826        TextBaseline::Middle => size_px * 0.35,
1827        TextBaseline::Ideographic | TextBaseline::Bottom => -size_px * 0.15,
1828    }
1829}
1830
1831/// `font` から文字の大きさ(px)を取り出す。
1832///
1833/// CSS の `font` 一括指定は語順が自由なので、**`px` で終わる数値**を
1834/// 探す。`pt`/`em` 等は換算せず、見つからなければ `None`
1835/// (呼び出し側が既定の 10px を使う)。
1836pub fn parse_font_size(font: &str) -> Option<f32> {
1837    for tok in font.split(|c: char| c.is_whitespace() || c == '/') {
1838        let t = tok.trim();
1839        let Some(num) = t.strip_suffix("px") else {
1840            continue;
1841        };
1842        if let Ok(v) = num.parse::<f32>() {
1843            if v.is_finite() && v > 0.0 {
1844                return Some(v);
1845            }
1846        }
1847    }
1848    None
1849}
1850
1851/// 1 文字ぶんの字形(アルファ値のビットマップ)。
1852///
1853/// フォントの実装(`kernel::vector_font`)に依存しないよう、
1854/// ここでは**値として受け取る**。canvas 側を純粋に保つため。
1855pub struct GlyphBitmap<'a> {
1856    pub width: u32,
1857    pub height: u32,
1858    /// 字形の左上を、ペン位置(ベースライン左端)から見たずれ。
1859    pub x_offset: i32,
1860    pub y_offset: i32,
1861    /// 次の文字までの送り幅。
1862    pub advance: f32,
1863    /// 長さは `width * height`。0 は透明。
1864    pub alpha: &'a [u8],
1865}
1866
1867impl Surface {
1868    /// 字形を 1 つ描く。
1869    ///
1870    /// 字形は軸に沿ったビットマップだが、**現在の変換の下で**置きたい。
1871    /// アルファを色に載せた画像として `draw_image` に通すことで、
1872    /// 回転・拡大も転送と同じ経路で扱える(別実装を増やさない)。
1873    pub fn draw_glyph(&mut self, g: &GlyphBitmap, pen_x: f32, pen_y: f32, argb: u32, m: &Matrix) {
1874        if g.width == 0 || g.height == 0 {
1875            return;
1876        }
1877        let n = (g.width as usize) * (g.height as usize);
1878        if g.alpha.len() < n {
1879            return;
1880        }
1881        let (r, gg, b) = ((argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF);
1882        let base_a = (argb >> 24) & 0xFF;
1883        let mut rgba = alloc::vec![0u8; n * 4];
1884        for i in 0..n {
1885            let a = (g.alpha[i] as u32 * base_a / 255) as u8;
1886            rgba[i * 4] = r as u8;
1887            rgba[i * 4 + 1] = gg as u8;
1888            rgba[i * 4 + 2] = b as u8;
1889            rgba[i * 4 + 3] = a;
1890        }
1891        let src = ImageSource {
1892            width: g.width,
1893            height: g.height,
1894            rgba: &rgba,
1895        };
1896        let spec = BlitSpec {
1897            sx: 0.0,
1898            sy: 0.0,
1899            sw: g.width as f32,
1900            sh: g.height as f32,
1901            dx: pen_x + g.x_offset as f32,
1902            dy: pen_y + g.y_offset as f32,
1903            dw: g.width as f32,
1904            dh: g.height as f32,
1905        };
1906        self.draw_image(&src, &spec, m, 1.0);
1907    }
1908}