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/// パスを (dx, dy) だけ平行移動した新しい副パス列を生成する(Canvas 2D Shadow用)。
121pub(crate) fn offset_subpaths(paths: &[SubPath], dx: f32, dy: f32) -> Vec<SubPath> {
122    paths
123        .iter()
124        .map(|sp| SubPath {
125            points: sp.points.iter().map(|&(x, y)| (x + dx, y + dy)).collect(),
126            closed: sp.closed,
127        })
128        .collect()
129}
130
131/// パス構築器。曲線は折れ線へ分割して保持する。
132///
133/// ラスタライザは「折れ線の集まり」だけを相手にすればよくなるので、
134/// 曲線の分割規則をここへ閉じ込めて試験できる。
135#[derive(Debug, Clone, Default)]
136pub struct PathBuilder {
137    subpaths: Vec<SubPath>,
138    current: Option<SubPath>,
139    /// 直近の点(**変換前**のユーザー座標)。曲線の始点に使う。
140    last_user: Option<(f32, f32)>,
141}
142
143/// 円弧を折れ線化するときの、90 度あたりの分割数。
144const ARC_SEGMENTS_PER_QUARTER: usize = 8;
145/// ベジェ 1 本あたりの分割数。
146const BEZIER_SEGMENTS: usize = 16;
147
148impl PathBuilder {
149    pub fn new() -> Self {
150        Self::default()
151    }
152
153    /// 現在の副パスを確定して次へ移る。
154    ///
155    /// 点が 1 個以下の副パスは描画に寄与しないので捨てる
156    /// (`moveTo` だけして何も引かなかった場合など)。
157    fn flush(&mut self) {
158        if let Some(sp) = self.current.take() {
159            if sp.points.len() >= 2 {
160                self.subpaths.push(sp);
161            }
162        }
163    }
164
165    pub fn begin_path(&mut self) {
166        self.subpaths.clear();
167        self.current = None;
168        self.last_user = None;
169    }
170
171    pub fn move_to(&mut self, m: &Matrix, x: f32, y: f32) {
172        self.flush();
173        self.current = Some(SubPath {
174            points: alloc::vec![m.apply(x, y)],
175            closed: false,
176        });
177        self.last_user = Some((x, y));
178    }
179
180    pub fn line_to(&mut self, m: &Matrix, x: f32, y: f32) {
181        // `moveTo` 無しの `lineTo` は、仕様上その点への `moveTo` と同じ扱い。
182        if self.current.is_none() {
183            self.move_to(m, x, y);
184            return;
185        }
186        if let Some(sp) = self.current.as_mut() {
187            sp.points.push(m.apply(x, y));
188        }
189        self.last_user = Some((x, y));
190    }
191
192    pub fn close_path(&mut self) {
193        if let Some(sp) = self.current.as_mut() {
194            sp.closed = true;
195        }
196        self.flush();
197    }
198
199    pub fn add_path(&mut self, other: &PathBuilder, transform: Option<&Matrix>) {
200        self.flush();
201        let id_mat = Matrix::identity();
202        let m = transform.unwrap_or(&id_mat);
203        for sp in &other.subpaths {
204            let pts = sp.points.iter().map(|p| m.apply(p.0, p.1)).collect();
205            self.subpaths.push(SubPath {
206                points: pts,
207                closed: sp.closed,
208            });
209        }
210        if let Some(ref sp) = other.current {
211            if sp.points.len() >= 2 {
212                let pts = sp.points.iter().map(|p| m.apply(p.0, p.1)).collect();
213                self.subpaths.push(SubPath {
214                    points: pts,
215                    closed: sp.closed,
216                });
217            }
218        }
219        if let Some((lx, ly)) = other.last_user {
220            let lp = m.apply(lx, ly);
221            self.last_user = Some((lp.0, lp.1));
222        }
223    }
224
225    /// 3 次ベジェ。始点は直近の点。
226    pub fn bezier_curve_to(
227        &mut self,
228        m: &Matrix,
229        c1x: f32,
230        c1y: f32,
231        c2x: f32,
232        c2y: f32,
233        x: f32,
234        y: f32,
235    ) {
236        let (x0, y0) = match self.last_user {
237            Some(p) => p,
238            None => {
239                self.move_to(m, c1x, c1y);
240                (c1x, c1y)
241            }
242        };
243        for i in 1..=BEZIER_SEGMENTS {
244            let t = i as f32 / BEZIER_SEGMENTS as f32;
245            let mt = 1.0 - t;
246            let px = mt * mt * mt * x0
247                + 3.0 * mt * mt * t * c1x
248                + 3.0 * mt * t * t * c2x
249                + t * t * t * x;
250            let py = mt * mt * mt * y0
251                + 3.0 * mt * mt * t * c1y
252                + 3.0 * mt * t * t * c2y
253                + t * t * t * y;
254            self.line_to(m, px, py);
255        }
256        self.last_user = Some((x, y));
257    }
258
259    /// 2 次ベジェ。3 次へ昇格させて処理する。
260    pub fn quadratic_curve_to(&mut self, m: &Matrix, cx: f32, cy: f32, x: f32, y: f32) {
261        let (x0, y0) = match self.last_user {
262            Some(p) => p,
263            None => {
264                self.move_to(m, cx, cy);
265                (cx, cy)
266            }
267        };
268        // 2 次 → 3 次の標準的な昇格。
269        let c1x = x0 + 2.0 / 3.0 * (cx - x0);
270        let c1y = y0 + 2.0 / 3.0 * (cy - y0);
271        let c2x = x + 2.0 / 3.0 * (cx - x);
272        let c2y = y + 2.0 / 3.0 * (cy - y);
273        self.bezier_curve_to(m, c1x, c1y, c2x, c2y, x, y);
274    }
275
276    /// 円弧。`anticlockwise` が真なら反時計回り。
277    pub fn arc(
278        &mut self,
279        m: &Matrix,
280        cx: f32,
281        cy: f32,
282        r: f32,
283        start: f32,
284        end: f32,
285        anticlockwise: bool,
286    ) {
287        // 負の半径・非有限値は仕様上エラーだが、ここでは黙って無視する
288        // (描画系でパニックさせない方が安全)。
289        if !r.is_finite() || r < 0.0 || !start.is_finite() || !end.is_finite() {
290            return;
291        }
292        let sweep = normalize_sweep(start, end, anticlockwise);
293        let steps = arc_steps(sweep);
294        for i in 0..=steps {
295            let t = i as f32 / steps as f32;
296            let ang = start + sweep * t;
297            let px = cx + r * libm::cosf(ang);
298            let py = cy + r * libm::sinf(ang);
299            if i == 0 && self.current.is_none() {
300                self.move_to(m, px, py);
301            } else {
302                self.line_to(m, px, py);
303            }
304        }
305    }
306
307    /// 2本の接線((x0,y0)-(x1,y1) と (x1,y1)-(x2,y2))に接する半径 `radius` の円弧を追加する。
308    pub fn arc_to(&mut self, m: &Matrix, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32) {
309        if !x1.is_finite() || !y1.is_finite() || !x2.is_finite() || !y2.is_finite() || !radius.is_finite() {
310            return;
311        }
312        if radius < 0.0 {
313            return;
314        }
315        let (x0, y0) = match self.last_user {
316            Some(p) => p,
317            None => {
318                self.move_to(m, x1, y1);
319                (x1, y1)
320            }
321        };
322
323        // 半径0、またはP0とP1が一致する場合、P1への直線
324        if radius == 0.0 || (x0 == x1 && y0 == y1) {
325            self.line_to(m, x1, y1);
326            return;
327        }
328
329        let v1x = x0 - x1;
330        let v1y = y0 - y1;
331        let v2x = x2 - x1;
332        let v2y = y2 - y1;
333
334        let len1 = libm::hypotf(v1x, v1y);
335        let len2 = libm::hypotf(v2x, v2y);
336
337        if len1 == 0.0 || len2 == 0.0 {
338            self.line_to(m, x1, y1);
339            return;
340        }
341
342        let u1x = v1x / len1;
343        let u1y = v1y / len1;
344        let u2x = v2x / len2;
345        let u2y = v2y / len2;
346
347        let cross = u1x * u2y - u1y * u2x;
348        let dot = u1x * u2x + u1y * u2y;
349
350        // 同一直線上(crossが極小)なら P1 への直線
351        if libm::fabsf(cross) < 1e-6 {
352            self.line_to(m, x1, y1);
353            return;
354        }
355
356        // 接点までの距離: dist = radius * (1.0 + dot) / |cross|
357        let dist = radius * (1.0 + dot) / libm::fabsf(cross);
358
359        let t1x = x1 + dist * u1x;
360        let t1y = y1 + dist * u1y;
361        let t2x = x1 + dist * u2x;
362        let t2y = y1 + dist * u2y;
363
364        // 円の中心 C: T1 から u1 に直交するベクトルへ radius 進む
365        let (cx, cy) = if cross > 0.0 {
366            (t1x - radius * u1y, t1y + radius * u1x)
367        } else {
368            (t1x + radius * u1y, t1y - radius * u1x)
369        };
370
371        let start_ang = libm::atan2f(t1y - cy, t1x - cx);
372        let end_ang = libm::atan2f(t2y - cy, t2x - cx);
373        let anticlockwise = cross > 0.0;
374
375        self.line_to(m, t1x, t1y);
376        self.arc(m, cx, cy, radius, start_ang, end_ang, anticlockwise);
377        self.last_user = Some((t2x, t2y));
378    }
379
380    /// 矩形。独立した閉じた副パスになる。
381    pub fn rect(&mut self, m: &Matrix, x: f32, y: f32, w: f32, h: f32) {
382        self.flush();
383        self.current = Some(SubPath {
384            points: alloc::vec![
385                m.apply(x, y),
386                m.apply(x + w, y),
387                m.apply(x + w, y + h),
388                m.apply(x, y + h),
389            ],
390            closed: true,
391        });
392        self.flush();
393        self.last_user = Some((x, y));
394    }
395
396    /// 構築済みの副パス一覧を返す(未確定の副パスも含める)。
397    pub fn finish(&self) -> Vec<SubPath> {
398        let mut out = self.subpaths.clone();
399        if let Some(sp) = &self.current {
400            if sp.points.len() >= 2 {
401                out.push(sp.clone());
402            }
403        }
404        out
405    }
406}
407
408/// 円弧の掃引角を求める。
409///
410/// Canvas の仕様では、時計回り指定で `end < start` のときは 2π を足し、
411/// 反時計回り指定で `end > start` のときは 2π を引く。
412/// 差が 2π を超える場合は 1 周に丸める(分割数が発散しないように)。
413pub fn normalize_sweep(start: f32, end: f32, anticlockwise: bool) -> f32 {
414    let tau = core::f32::consts::PI * 2.0;
415    let mut sweep = end - start;
416    if anticlockwise {
417        if sweep > 0.0 {
418            sweep -= tau;
419        }
420        if sweep < -tau {
421            sweep = -tau;
422        }
423    } else {
424        if sweep < 0.0 {
425            sweep += tau;
426        }
427        if sweep > tau {
428            sweep = tau;
429        }
430    }
431    sweep
432}
433
434/// 掃引角に応じた分割数(最低 1)。
435pub fn arc_steps(sweep: f32) -> usize {
436    let quarters = libm::fabsf(sweep) / (core::f32::consts::PI / 2.0);
437    let n = (quarters * ARC_SEGMENTS_PER_QUARTER as f32) as usize;
438    n.max(1)
439}
440
441// ───────────── ラスタライズ(走査線) ─────────────
442
443/// 塗りつぶし規則。
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub enum FillRule {
446    /// `nonzero`(既定)。巻き数が 0 でなければ内側。
447    NonZero,
448    /// `evenodd`。交差回数が奇数なら内側。
449    EvenOdd,
450}
451
452/// 走査線 `y` とパスの交点を求め、塗るべき x 区間を返す(純粋関数)。
453///
454/// 返り値は `(x_start, x_end)` の並びで、いずれも**昇順・非重複**。
455/// 閉じていない副パスも、塗りのときは仕様上「閉じているものとして」扱う。
456///
457/// # なぜ純粋関数にするか
458///
459/// 巻き数の数え方(`nonzero` と `evenodd` の違い)と、頂点をちょうど
460/// 走査線が通るときの二重計上は、目視では気づけない典型的なバグ源。
461/// ここを試験で固定しておけば、実際のピクセル書き込み側は
462/// 「区間を塗るだけ」の単純な処理に保てる。
463///
464/// 計算量: **O(辺の数 log 辺の数)**(交点の整列ぶん)。
465pub fn scanline_spans(subpaths: &[SubPath], y: f32, rule: FillRule) -> Vec<(f32, f32)> {
466    // (x, 巻き方向) の組を集める。
467    let mut hits: Vec<(f32, i32)> = Vec::new();
468    for sp in subpaths {
469        let n = sp.points.len();
470        if n < 2 {
471            continue;
472        }
473        // 塗りでは常に閉じたものとして扱う(最後の点と最初の点を結ぶ)。
474        for i in 0..n {
475            let (x0, y0) = sp.points[i];
476            let (x1, y1) = sp.points[(i + 1) % n];
477            if !y0.is_finite() || !y1.is_finite() || !x0.is_finite() || !x1.is_finite() {
478                continue;
479            }
480            // 水平な辺は交差を作らない。
481            if (y0 - y1).abs() < f32::EPSILON {
482                continue;
483            }
484            // 半開区間 [min, max) で判定する。
485            //
486            // これをしないと、頂点をちょうど走査線が通るときに
487            // 上下 2 本の辺が同じ交点を二重に数え、`evenodd` で
488            // 内外が反転する(塗り残し・塗り過ぎの典型的な原因)。
489            let (ylo, yhi) = if y0 < y1 { (y0, y1) } else { (y1, y0) };
490            if y < ylo || y >= yhi {
491                continue;
492            }
493            let t = (y - y0) / (y1 - y0);
494            let x = x0 + t * (x1 - x0);
495            // 下向きの辺を +1、上向きを -1 とする。
496            let dir = if y1 > y0 { 1 } else { -1 };
497            hits.push((x, dir));
498        }
499    }
500    if hits.is_empty() {
501        return Vec::new();
502    }
503    // x 昇順に並べる(NaN は上で除外済み)。
504    hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
505
506    let mut spans = Vec::new();
507    match rule {
508        FillRule::EvenOdd => {
509            let mut i = 0;
510            while i + 1 < hits.len() {
511                spans.push((hits[i].0, hits[i + 1].0));
512                i += 2;
513            }
514        }
515        FillRule::NonZero => {
516            let mut winding = 0i32;
517            let mut start = 0.0f32;
518            for &(x, dir) in &hits {
519                let was_inside = winding != 0;
520                winding += dir;
521                let now_inside = winding != 0;
522                if !was_inside && now_inside {
523                    start = x;
524                } else if was_inside && !now_inside {
525                    spans.push((start, x));
526                }
527            }
528        }
529    }
530    spans
531}
532
533/// 指定した座標 (x, y) がパス(subpaths)の内部に含まれるかを判定する(W3C isPointInPath 準拠)。
534pub fn is_point_in_path(subpaths: &[SubPath], x: f32, y: f32, rule: FillRule) -> bool {
535    if !x.is_finite() || !y.is_finite() || subpaths.is_empty() {
536        return false;
537    }
538    let spans = scanline_spans(subpaths, y, rule);
539    for (sx, ex) in spans {
540        if x >= sx - 1e-4 && x <= ex + 1e-4 {
541            return true;
542        }
543    }
544    false
545}
546
547/// 指定した座標 (x, y) がストローク線(subpaths, line_width)の範囲内にあるかを判定する(W3C isPointInStroke 準拠)。
548pub fn is_point_in_stroke(subpaths: &[SubPath], x: f32, y: f32, line_width: f32) -> bool {
549    if !x.is_finite() || !y.is_finite() || subpaths.is_empty() || line_width <= 0.0 {
550        return false;
551    }
552    let max_dist = line_width * 0.5;
553    let max_dist_sq = max_dist * max_dist;
554
555    for sp in subpaths {
556        let n = sp.points.len();
557        if n < 2 {
558            continue;
559        }
560        for i in 0..n - 1 {
561            let (x0, y0) = sp.points[i];
562            let (x1, y1) = sp.points[i + 1];
563            if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
564                continue;
565            }
566            let dx = x1 - x0;
567            let dy = y1 - y0;
568            let len_sq = dx * dx + dy * dy;
569            let dist_sq = if len_sq < 1e-6 {
570                let px = x - x0;
571                let py = y - y0;
572                px * px + py * py
573            } else {
574                let t = (((x - x0) * dx + (y - y0) * dy) / len_sq).clamp(0.0, 1.0);
575                let qx = x0 + t * dx;
576                let qy = y0 + t * dy;
577                let px = x - qx;
578                let py = y - qy;
579                px * px + py * py
580            };
581            if dist_sq <= max_dist_sq {
582                return true;
583            }
584        }
585        if sp.closed && n >= 3 {
586            let (x0, y0) = sp.points[n - 1];
587            let (x1, y1) = sp.points[0];
588            let dx = x1 - x0;
589            let dy = y1 - y0;
590            let len_sq = dx * dx + dy * dy;
591            let dist_sq = if len_sq < 1e-6 {
592                let px = x - x0;
593                let py = y - y0;
594                px * px + py * py
595            } else {
596                let t = (((x - x0) * dx + (y - y0) * dy) / len_sq).clamp(0.0, 1.0);
597                let qx = x0 + t * dx;
598                let qy = y0 + t * dy;
599                let px = x - qx;
600                let py = y - qy;
601                px * px + py * py
602            };
603            if dist_sq <= max_dist_sq {
604                return true;
605            }
606        }
607    }
608    false
609}
610
611/// 指定した座標 (x, y) が、線幅・端点・結合・破線パターンを反映したストローク範囲内にあるかを判定する(W3C isPointInStroke 準拠)。
612pub fn is_point_in_stroke_styled(
613    subpaths: &[SubPath],
614    x: f32,
615    y: f32,
616    line_width: f32,
617    cap: LineCap,
618    join: LineJoin,
619    miter_limit: f32,
620    dash: &[f32],
621    dash_offset: f32,
622) -> bool {
623    if !x.is_finite() || !y.is_finite() || subpaths.is_empty() || line_width <= 0.0 {
624        return false;
625    }
626    let dashed = if dash.is_empty() {
627        subpaths.to_vec()
628    } else {
629        dash_subpaths(subpaths, dash, dash_offset)
630    };
631    let outline = stroke_to_outline(&dashed, line_width, cap, join, miter_limit);
632    if outline.is_empty() {
633        return false;
634    }
635    is_point_in_path(&outline, x, y, FillRule::NonZero)
636}
637
638
639// ───────────── グラデーション ─────────────
640
641/// グラデーションのカラーストップ。
642#[derive(Debug, Clone, Copy, PartialEq)]
643pub struct ColorStop {
644    pub offset: f32, // 0.0 ..= 1.0
645    pub color: u32,  // ARGB8888
646}
647
648/// グラデーションの種別と幾何パラメータ。
649#[derive(Debug, Clone, PartialEq)]
650pub enum GradientKind {
651    Linear {
652        x0: f32,
653        y0: f32,
654        x1: f32,
655        y1: f32,
656    },
657    Radial {
658        x0: f32,
659        y0: f32,
660        r0: f32,
661        x1: f32,
662        y1: f32,
663        r1: f32,
664    },
665    Conic {
666        start_angle: f32,
667        cx: f32,
668        cy: f32,
669    },
670}
671
672/// カラーストップ配列から位置 `t`(0.0..=1.0)に対応する ARGB8888 色を線形補間する。
673pub fn sample_gradient_stops(stops: &[ColorStop], t: f32) -> u32 {
674    if stops.is_empty() {
675        return 0; // 透明
676    }
677    let t = t.clamp(0.0, 1.0);
678    if t <= stops[0].offset {
679        return stops[0].color;
680    }
681    let last = &stops[stops.len() - 1];
682    if t >= last.offset {
683        return last.color;
684    }
685    for i in 0..stops.len() - 1 {
686        let s0 = &stops[i];
687        let s1 = &stops[i + 1];
688        if t >= s0.offset && t <= s1.offset {
689            let span = s1.offset - s0.offset;
690            let ratio = if span > 1e-6 { (t - s0.offset) / span } else { 0.0 };
691            let a0 = ((s0.color >> 24) & 0xFF) as f32;
692            let r0 = ((s0.color >> 16) & 0xFF) as f32;
693            let g0 = ((s0.color >> 8) & 0xFF) as f32;
694            let b0 = (s0.color & 0xFF) as f32;
695
696            let a1 = ((s1.color >> 24) & 0xFF) as f32;
697            let r1 = ((s1.color >> 16) & 0xFF) as f32;
698            let g1 = ((s1.color >> 8) & 0xFF) as f32;
699            let b1 = (s1.color & 0xFF) as f32;
700
701            let a = ((1.0 - ratio) * a0 + ratio * a1 + 0.5) as u32;
702            let r = ((1.0 - ratio) * r0 + ratio * r1 + 0.5) as u32;
703            let g = ((1.0 - ratio) * g0 + ratio * g1 + 0.5) as u32;
704            let b = ((1.0 - ratio) * b0 + ratio * b1 + 0.5) as u32;
705
706            return (a.min(255) << 24) | (r.min(255) << 16) | (g.min(255) << 8) | b.min(255);
707        }
708    }
709    last.color
710}
711
712/// Canvas 2D のグラデーションオブジェクト。
713#[derive(Debug, Clone, PartialEq)]
714pub struct CanvasGradient {
715    pub kind: GradientKind,
716    pub stops: Vec<ColorStop>,
717}
718
719impl CanvasGradient {
720    pub fn new_linear(x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
721        CanvasGradient {
722            kind: GradientKind::Linear { x0, y0, x1, y1 },
723            stops: Vec::new(),
724        }
725    }
726
727    pub fn new_radial(x0: f32, y0: f32, r0: f32, x1: f32, y1: f32, r1: f32) -> Self {
728        CanvasGradient {
729            kind: GradientKind::Radial { x0, y0, r0, x1, y1, r1 },
730            stops: Vec::new(),
731        }
732    }
733
734    pub fn new_conic(start_angle: f32, cx: f32, cy: f32) -> Self {
735        CanvasGradient {
736            kind: GradientKind::Conic { start_angle, cx, cy },
737            stops: Vec::new(),
738        }
739    }
740
741    pub fn add_color_stop(&mut self, offset: f32, color: u32) {
742        let offset = offset.clamp(0.0, 1.0);
743        let stop = ColorStop { offset, color };
744        let idx = self.stops.partition_point(|s| s.offset <= offset);
745        self.stops.insert(idx, stop);
746    }
747
748    /// 256段の高速ルックアップテーブル(LUT)を生成する。
749    pub fn build_lut(&self) -> [u32; 256] {
750        let mut lut = [0u32; 256];
751        if self.stops.is_empty() {
752            return lut;
753        }
754        for i in 0..256 {
755            let t = i as f32 / 255.0;
756            lut[i] = sample_gradient_stops(&self.stops, t);
757        }
758        lut
759    }
760
761    /// 座標 (x, y) における射影パラメータ t (0.0..=1.0) を計算する。
762    pub fn calculate_t(&self, x: f32, y: f32) -> f32 {
763        match self.kind {
764            GradientKind::Linear { x0, y0, x1, y1 } => {
765                let dx = x1 - x0;
766                let dy = y1 - y0;
767                let len_sq = dx * dx + dy * dy;
768                if len_sq < 1e-6 {
769                    return 0.0;
770                }
771                let proj = (x - x0) * dx + (y - y0) * dy;
772                (proj / len_sq).clamp(0.0, 1.0)
773            }
774            GradientKind::Radial { x0: _, y0: _, r0, x1, y1, r1 } => {
775                let dx = x - x1;
776                let dy = y - y1;
777                let dist = libm::sqrtf(dx * dx + dy * dy);
778                let r_span = r1 - r0;
779                if libm::fabsf(r_span) < 1e-6 {
780                    return 0.0;
781                }
782                ((dist - r0) / r_span).clamp(0.0, 1.0)
783            }
784            GradientKind::Conic { start_angle, cx, cy } => {
785                let dx = x - cx;
786                let dy = y - cy;
787                if dx == 0.0 && dy == 0.0 {
788                    return 0.0;
789                }
790                let angle = libm::atan2f(dy, dx);
791                let mut diff = angle - start_angle;
792                let two_pi = core::f32::consts::PI * 2.0;
793                while diff < 0.0 {
794                    diff += two_pi;
795                }
796                while diff >= two_pi {
797                    diff -= two_pi;
798                }
799                (diff / two_pi).clamp(0.0, 1.0)
800            }
801        }
802    }
803}
804
805
806// ───────────── パターン ─────────────
807
808/// `createPattern` のリピート方式。
809#[derive(Debug, Clone, Copy, PartialEq, Eq)]
810pub enum Repetition {
811    Repeat,
812    RepeatX,
813    RepeatY,
814    NoRepeat,
815}
816
817/// 文字列から `Repetition` を判定する(仕様上、大文字小文字無視、空文字や未知の値は Repeat)。
818pub fn parse_repetition(s: &str) -> Repetition {
819    let s = s.trim();
820    if s.is_empty() {
821        return Repetition::Repeat;
822    }
823    if s.eq_ignore_ascii_case("repeat") {
824        Repetition::Repeat
825    } else if s.eq_ignore_ascii_case("repeat-x") {
826        Repetition::RepeatX
827    } else if s.eq_ignore_ascii_case("repeat-y") {
828        Repetition::RepeatY
829    } else if s.eq_ignore_ascii_case("no-repeat") {
830        Repetition::NoRepeat
831    } else {
832        Repetition::Repeat
833    }
834}
835
836/// Canvas 2D のパターンオブジェクト。
837#[derive(Debug, Clone, PartialEq)]
838pub struct CanvasPattern {
839    pub width: u32,
840    pub height: u32,
841    pub pixels: Vec<u32>,
842    pub repetition: Repetition,
843    pub transform: Matrix,
844}
845
846impl CanvasPattern {
847    pub fn new(width: u32, height: u32, pixels: Vec<u32>, repetition: Repetition) -> Self {
848        CanvasPattern {
849            width,
850            height,
851            pixels,
852            repetition,
853            transform: Matrix::identity(),
854        }
855    }
856
857    pub fn set_transform(&mut self, m: Matrix) {
858        self.transform = m;
859    }
860
861    /// 座標 (x, y) におけるピクセル色 (ARGB8888) をサンプリングする。
862    /// 範囲外(no-repeat等)は 0(透明)を返す。
863    pub fn sample(&self, x: f32, y: f32) -> u32 {
864        if self.width == 0 || self.height == 0 || self.pixels.is_empty() {
865            return 0;
866        }
867        let (u, v) = if let Some(inv) = self.transform.invert() {
868            inv.apply(x, y)
869        } else {
870            (x, y)
871        };
872        let w = self.width as i32;
873        let h = self.height as i32;
874
875        let (ix, iy) = match self.repetition {
876            Repetition::Repeat => (
877                (libm::floorf(u) as i32).rem_euclid(w),
878                (libm::floorf(v) as i32).rem_euclid(h),
879            ),
880            Repetition::RepeatX => {
881                let y_int = libm::floorf(v) as i32;
882                if y_int < 0 || y_int >= h {
883                    return 0;
884                }
885                ((libm::floorf(u) as i32).rem_euclid(w), y_int)
886            }
887            Repetition::RepeatY => {
888                let x_int = libm::floorf(u) as i32;
889                if x_int < 0 || x_int >= w {
890                    return 0;
891                }
892                (x_int, (libm::floorf(v) as i32).rem_euclid(h))
893            }
894            Repetition::NoRepeat => {
895                let x_int = libm::floorf(u) as i32;
896                let y_int = libm::floorf(v) as i32;
897                if x_int < 0 || x_int >= w || y_int < 0 || y_int >= h {
898                    return 0;
899                }
900                (x_int, y_int)
901            }
902        };
903
904        let idx = (iy as usize) * (self.width as usize) + (ix as usize);
905        self.pixels.get(idx).copied().unwrap_or(0)
906    }
907}
908
909
910// ───────────── バッキングストアへの描画 ─────────────
911
912/// Canvas のバッキングストア(ARGB8888 のピクセル配列)。
913///
914/// 画面へ直接描かず、まずここへ描く。`<canvas>` は
915/// 「レイアウトで決まった矩形へ、自分のピクセルを貼る」要素なので、
916/// 画面座標ではなく**キャンバス内座標**で完結させる必要がある。
917#[derive(Debug, Clone)]
918pub struct Surface {
919    pub width: u32,
920    pub height: u32,
921    /// 長さは `width * height`。各要素は 0xAARRGGBB。
922    pub pixels: Vec<u32>,
923    /// 有効なクリップ領域。`None` なら全面に描ける。
924    ///
925    /// 状態側(`DrawState`)が持ち主で、描画の直前に `sync_clip` で写す。
926    /// 塗りの実装すべてが最後は `blend_pixel` を通るので、
927    /// ここで一度だけ見れば経路の取りこぼしが起きない。
928    pub clip: Option<alloc::sync::Arc<ClipMask>>,
929    /// 合成モード(既定 `SourceOver`)。
930    pub composite_op: CompositeOperation,
931}
932
933impl Surface {
934    /// 透明で初期化した面を作る。
935    ///
936    /// 面積が過大な場合は `None`(ヒープを食い潰さないための上限)。
937    /// 実サイトの canvas は大きくても数千×数千なので、
938    /// 4096×4096(約 64MB)を上限とする。
939    pub fn new(width: u32, height: u32) -> Option<Self> {
940        const MAX_SIDE: u32 = 4096;
941        if width == 0 || height == 0 || width > MAX_SIDE || height > MAX_SIDE {
942            return None;
943        }
944        let n = (width as usize).checked_mul(height as usize)?;
945        Some(Surface {
946            width,
947            height,
948            pixels: alloc::vec![0u32; n],
949            clip: None,
950            composite_op: CompositeOperation::SourceOver,
951        })
952    }
953
954    /// 指定座標のピクセル(0xAARRGGBB)を返す。範囲外は 0(透明)。
955    pub fn get_pixel(&self, x: i32, y: i32) -> u32 {
956        if x < 0 || y < 0 || x as u32 >= self.width || y as u32 >= self.height {
957            return 0;
958        }
959        let idx = y as usize * self.width as usize + x as usize;
960        self.pixels[idx]
961    }
962
963    /// 指定座標にピクセル(0xAARRGGBB)を直接書き込む。範囲外は無視する。
964    pub fn set_pixel(&mut self, x: i32, y: i32, argb: u32) {
965        if x < 0 || y < 0 || x as u32 >= self.width || y as u32 >= self.height {
966            return;
967        }
968        let idx = y as usize * self.width as usize + x as usize;
969        self.pixels[idx] = argb;
970    }
971
972    /// 1 ピクセルを現在の合成モード(`self.composite_op`)で描画する。範囲外は黙って無視する。
973    pub fn blend_pixel(&mut self, x: i32, y: i32, argb: u32) {
974        self.blend_pixel_with_op(x, y, argb, self.composite_op);
975    }
976
977    /// 1 ピクセルを指定した合成モード(Porter-Duff)で描画する。
978    pub fn blend_pixel_with_op(&mut self, x: i32, y: i32, argb: u32, op: CompositeOperation) {
979        if x < 0 || y < 0 || x as u32 >= self.width || y as u32 >= self.height {
980            return;
981        }
982        if let Some(c) = &self.clip {
983            if !c.allows(x, y) {
984                return;
985            }
986        }
987        let idx = y as usize * self.width as usize + x as usize;
988        let sa = (argb >> 24) & 0xFF;
989        let sr = (argb >> 16) & 0xFF;
990        let sg = (argb >> 8) & 0xFF;
991        let sb = argb & 0xFF;
992
993        let dst = self.pixels[idx];
994        let da = (dst >> 24) & 0xFF;
995        let dr = (dst >> 16) & 0xFF;
996        let dg = (dst >> 8) & 0xFF;
997        let db = dst & 0xFF;
998
999        let (out_a, out_r, out_g, out_b) = match op {
1000            CompositeOperation::Copy => (sa, sr, sg, sb),
1001            CompositeOperation::SourceOver => {
1002                if sa == 0 {
1003                    return;
1004                }
1005                if sa == 255 {
1006                    self.pixels[idx] = argb;
1007                    return;
1008                }
1009                let inv = 255 - sa;
1010                let out_a = sa + da * inv / 255;
1011                if out_a == 0 {
1012                    (0, 0, 0, 0)
1013                } else {
1014                    let r = (sr * sa + dr * da * inv / 255) / out_a;
1015                    let g = (sg * sa + dg * da * inv / 255) / out_a;
1016                    let b = (sb * sa + db * da * inv / 255) / out_a;
1017                    (out_a, r.min(255), g.min(255), b.min(255))
1018                }
1019            }
1020            CompositeOperation::DestinationOver => {
1021                if da == 255 {
1022                    return;
1023                }
1024                let inv = 255 - da;
1025                let out_a = da + sa * inv / 255;
1026                if out_a == 0 {
1027                    (0, 0, 0, 0)
1028                } else {
1029                    let r = (dr * da + sr * sa * inv / 255) / out_a;
1030                    let g = (dg * da + sg * sa * inv / 255) / out_a;
1031                    let b = (db * da + sb * sa * inv / 255) / out_a;
1032                    (out_a, r.min(255), g.min(255), b.min(255))
1033                }
1034            }
1035            CompositeOperation::SourceIn => {
1036                let out_a = (sa * da) / 255;
1037                (out_a, sr, sg, sb)
1038            }
1039            CompositeOperation::SourceOut => {
1040                let out_a = (sa * (255 - da)) / 255;
1041                (out_a, sr, sg, sb)
1042            }
1043            CompositeOperation::SourceAtop => {
1044                let out_a = da;
1045                let r = (sr * sa + dr * (255 - sa)) / 255;
1046                let g = (sg * sa + dg * (255 - sa)) / 255;
1047                let b = (sb * sa + db * (255 - sa)) / 255;
1048                (out_a, r.min(255), g.min(255), b.min(255))
1049            }
1050            CompositeOperation::DestinationIn => {
1051                let out_a = (da * sa) / 255;
1052                (out_a, dr, dg, db)
1053            }
1054            CompositeOperation::DestinationOut => {
1055                let out_a = (da * (255 - sa)) / 255;
1056                (out_a, dr, dg, db)
1057            }
1058            CompositeOperation::DestinationAtop => {
1059                let out_a = sa;
1060                let r = (dr * da + sr * (255 - da)) / 255;
1061                let g = (dg * da + sg * (255 - da)) / 255;
1062                let b = (db * da + sb * (255 - da)) / 255;
1063                (out_a, r.min(255), g.min(255), b.min(255))
1064            }
1065            CompositeOperation::Lighter => {
1066                let out_a = (sa + da).min(255);
1067                let r = (sr + dr).min(255);
1068                let g = (sg + dg).min(255);
1069                let b = (sb + db).min(255);
1070                (out_a, r, g, b)
1071            }
1072            CompositeOperation::Xor => {
1073                let inv_d = 255 - da;
1074                let inv_s = 255 - sa;
1075                let out_a = (sa * inv_d + da * inv_s) / 255;
1076                if out_a == 0 {
1077                    (0, 0, 0, 0)
1078                } else {
1079                    let r = (sr * sa * inv_d + dr * da * inv_s) / (out_a * 255);
1080                    let g = (sg * sa * inv_d + dg * da * inv_s) / (out_a * 255);
1081                    let b = (sb * sa * inv_d + db * da * inv_s) / (out_a * 255);
1082                    (out_a.min(255), r.min(255), g.min(255), b.min(255))
1083                }
1084            }
1085        };
1086
1087        self.pixels[idx] = (out_a.min(255) << 24) | (out_r.min(255) << 16) | (out_g.min(255) << 8) | out_b.min(255);
1088    }
1089
1090    /// 矩形を塗りつぶす(`clearRect` は `argb=0` かつ `replace=true` で使う)。
1091    ///
1092    /// `replace` が真なら合成せず上書きする(`clearRect` の意味論)。
1093    pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, argb: u32, replace: bool) {
1094        mark_canvas_dirty();
1095        if w <= 0 || h <= 0 {
1096            return;
1097        }
1098        let x0 = x.max(0);
1099        let y0 = y.max(0);
1100        let x1 = (x.saturating_add(w)).min(self.width as i32);
1101        let y1 = (y.saturating_add(h)).min(self.height as i32);
1102        for py in y0..y1 {
1103            for px in x0..x1 {
1104                if replace {
1105                    // `clearRect` も合成を飛ばすだけで、クリップは効く(仕様)。
1106                    // ここは `blend_pixel` を通らないので個別に見る。
1107                    if let Some(c) = &self.clip {
1108                        if !c.allows(px, py) {
1109                            continue;
1110                        }
1111                    }
1112                    let idx = py as usize * self.width as usize + px as usize;
1113                    self.pixels[idx] = argb;
1114                } else {
1115                    self.blend_pixel(px, py, argb);
1116                }
1117            }
1118        }
1119    }
1120
1121    /// パスを塗りつぶす。
1122    ///
1123    /// 走査線はピクセル中心(`y + 0.5`)で判定する。境界ちょうどの
1124    /// 走査線を使うと、隣り合う図形の継ぎ目に隙間や二重塗りが出る。
1125    pub fn fill_path(&mut self, subpaths: &[SubPath], rule: FillRule, argb: u32) {
1126        mark_canvas_dirty();
1127        if subpaths.is_empty() {
1128            return;
1129        }
1130        // 走査する y 範囲をパスの境界から絞る(全面走査を避ける)。
1131        let (mut ymin, mut ymax) = (f32::MAX, f32::MIN);
1132        for sp in subpaths {
1133            for &(_, py) in &sp.points {
1134                if py.is_finite() {
1135                    ymin = ymin.min(py);
1136                    ymax = ymax.max(py);
1137                }
1138            }
1139        }
1140        if ymin > ymax {
1141            return;
1142        }
1143        let y_start = (libm::floorf(ymin) as i32).max(0);
1144        let y_end = (libm::ceilf(ymax) as i32).min(self.height as i32);
1145        for py in y_start..y_end {
1146            let spans = scanline_spans(subpaths, py as f32 + 0.5, rule);
1147            for (sx, ex) in spans {
1148                // 区間の両端はピクセル境界へ丸める。
1149                let x0 = (libm::roundf(sx) as i32).max(0);
1150                let x1 = (libm::roundf(ex) as i32).min(self.width as i32);
1151                for px in x0..x1 {
1152                    self.blend_pixel(px, py, argb);
1153                }
1154            }
1155        }
1156    }
1157
1158    /// パスをグラデーション(線形または円形)で塗りつぶす。
1159    pub fn fill_path_gradient(
1160        &mut self,
1161        subpaths: &[SubPath],
1162        rule: FillRule,
1163        gradient: &CanvasGradient,
1164        alpha_multiplier: f32,
1165    ) {
1166        mark_canvas_dirty();
1167        if subpaths.is_empty() || gradient.stops.is_empty() {
1168            return;
1169        }
1170        let lut = gradient.build_lut();
1171        let (mut ymin, mut ymax) = (f32::MAX, f32::MIN);
1172        for sp in subpaths {
1173            for &(_, py) in &sp.points {
1174                if py.is_finite() {
1175                    ymin = ymin.min(py);
1176                    ymax = ymax.max(py);
1177                }
1178            }
1179        }
1180        if ymin > ymax {
1181            return;
1182        }
1183        let y_start = (libm::floorf(ymin) as i32).max(0);
1184        let y_end = (libm::ceilf(ymax) as i32).min(self.height as i32);
1185
1186        match gradient.kind {
1187            GradientKind::Linear { x0, y0, x1, y1 } => {
1188                let dx = x1 - x0;
1189                let dy = y1 - y0;
1190                let len_sq = dx * dx + dy * dy;
1191                let (cx, cy, c_base) = if len_sq >= 1e-6 {
1192                    let inv = 1.0 / len_sq;
1193                    (dx * inv, dy * inv, -(x0 * dx + y0 * dy) * inv)
1194                } else {
1195                    (0.0, 0.0, 0.0)
1196                };
1197
1198                for py in y_start..y_end {
1199                    let y_mid = py as f32 + 0.5;
1200                    let t_row_base = c_base + cy * y_mid;
1201                    let spans = scanline_spans(subpaths, y_mid, rule);
1202                    for (sx, ex) in spans {
1203                        let px0 = (libm::roundf(sx) as i32).max(0);
1204                        let px1 = (libm::roundf(ex) as i32).min(self.width as i32);
1205                        for px in px0..px1 {
1206                            let x_mid = px as f32 + 0.5;
1207                            let t = (t_row_base + cx * x_mid).clamp(0.0, 1.0);
1208                            let lut_idx = (t * 255.0 + 0.5) as usize;
1209                            let mut color = lut[lut_idx.min(255)];
1210                            if alpha_multiplier < 1.0 {
1211                                let a = (((color >> 24) & 0xFF) as f32 * alpha_multiplier + 0.5) as u32;
1212                                color = (a.min(255) << 24) | (color & 0x00FF_FFFF);
1213                            }
1214                            self.blend_pixel(px, py, color);
1215                        }
1216                    }
1217                }
1218            }
1219            GradientKind::Radial { .. } | GradientKind::Conic { .. } => {
1220                for py in y_start..y_end {
1221                    let y_mid = py as f32 + 0.5;
1222                    let spans = scanline_spans(subpaths, y_mid, rule);
1223                    for (sx, ex) in spans {
1224                        let px0 = (libm::roundf(sx) as i32).max(0);
1225                        let px1 = (libm::roundf(ex) as i32).min(self.width as i32);
1226                        for px in px0..px1 {
1227                            let x_mid = px as f32 + 0.5;
1228                            let t = gradient.calculate_t(x_mid, y_mid);
1229                            let lut_idx = (t * 255.0 + 0.5) as usize;
1230                            let mut color = lut[lut_idx.min(255)];
1231                            if alpha_multiplier < 1.0 {
1232                                let a = (((color >> 24) & 0xFF) as f32 * alpha_multiplier + 0.5) as u32;
1233                                color = (a.min(255) << 24) | (color & 0x00FF_FFFF);
1234                            }
1235                            self.blend_pixel(px, py, color);
1236                        }
1237                    }
1238                }
1239            }
1240        }
1241    }
1242
1243    /// パスをパターン(タイリング画像)で塗りつぶす。
1244    pub fn fill_path_pattern(
1245        &mut self,
1246        subpaths: &[SubPath],
1247        rule: FillRule,
1248        pattern: &CanvasPattern,
1249        alpha_multiplier: f32,
1250    ) {
1251        mark_canvas_dirty();
1252        if subpaths.is_empty() || pattern.width == 0 || pattern.height == 0 || pattern.pixels.is_empty() {
1253            return;
1254        }
1255        let (mut ymin, mut ymax) = (f32::MAX, f32::MIN);
1256        for sp in subpaths {
1257            for &(_, py) in &sp.points {
1258                if py.is_finite() {
1259                    ymin = ymin.min(py);
1260                    ymax = ymax.max(py);
1261                }
1262            }
1263        }
1264        if ymin > ymax {
1265            return;
1266        }
1267        let y_start = (libm::floorf(ymin) as i32).max(0);
1268        let y_end = (libm::ceilf(ymax) as i32).min(self.height as i32);
1269
1270        for py in y_start..y_end {
1271            let y_mid = py as f32 + 0.5;
1272            let spans = scanline_spans(subpaths, y_mid, rule);
1273            for (sx, ex) in spans {
1274                let px0 = (libm::roundf(sx) as i32).max(0);
1275                let px1 = (libm::roundf(ex) as i32).min(self.width as i32);
1276                for px in px0..px1 {
1277                    let x_mid = px as f32 + 0.5;
1278                    let color = pattern.sample(x_mid, y_mid);
1279                    let a = (color >> 24) & 0xFF;
1280                    if a == 0 {
1281                        continue;
1282                    }
1283                    let final_color = if alpha_multiplier < 1.0 {
1284                        let final_a = libm::roundf(a as f32 * alpha_multiplier).clamp(0.0, 255.0) as u32;
1285                        (final_a << 24) | (color & 0x00FF_FFFF)
1286                    } else {
1287                        color
1288                    };
1289                    self.blend_pixel(px, py, final_color);
1290                }
1291            }
1292        }
1293    }
1294
1295    /// 折れ線を線幅 1 で描く(`stroke` の最小実装)。
1296    ///
1297    /// 線幅・線端・線結合は未対応。まず「線が見える」ところまでを担保する。
1298    pub fn stroke_path(&mut self, subpaths: &[SubPath], argb: u32) {
1299        mark_canvas_dirty();
1300        for sp in subpaths {
1301            let n = sp.points.len();
1302            if n < 2 {
1303                continue;
1304            }
1305            let last = if sp.closed { n } else { n - 1 };
1306            for i in 0..last {
1307                let (x0, y0) = sp.points[i];
1308                let (x1, y1) = sp.points[(i + 1) % n];
1309                self.draw_line(x0, y0, x1, y1, argb);
1310            }
1311        }
1312    }
1313
1314    /// Bresenham で線分を引く。非有限座標は無視する。
1315    fn draw_line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, argb: u32) {
1316        if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
1317            return;
1318        }
1319        let mut x = libm::roundf(x0) as i32;
1320        let mut y = libm::roundf(y0) as i32;
1321        let xe = libm::roundf(x1) as i32;
1322        let ye = libm::roundf(y1) as i32;
1323        let dx = (xe - x).abs();
1324        let dy = -(ye - y).abs();
1325        let sx = if x < xe { 1 } else { -1 };
1326        let sy = if y < ye { 1 } else { -1 };
1327        let mut err = dx + dy;
1328        // 上限を設けて、万一の座標異常でも無限ループにしない。
1329        let mut guard = 0i32;
1330        let limit = (self.width as i32 + self.height as i32) * 4 + 16;
1331        loop {
1332            self.blend_pixel(x, y, argb);
1333            if (x == xe && y == ye) || guard > limit {
1334                break;
1335            }
1336            guard += 1;
1337            let e2 = err * 2;
1338            if e2 >= dy {
1339                err += dy;
1340                x += sx;
1341            }
1342            if e2 <= dx {
1343                err += dx;
1344                y += sy;
1345            }
1346        }
1347    }
1348}
1349
1350
1351// ───────────── 2D コンテキストの状態 ─────────────
1352
1353/// `save()`/`restore()` で退避・復帰する描画状態。
1354#[derive(Debug, Clone)]
1355pub struct DrawState {
1356    pub transform: Matrix,
1357    /// 塗り色(ARGB8888)。
1358    pub fill: u32,
1359    /// 塗り色の文字列表現(例: "#000000", "red")。
1360    pub fill_str: alloc::string::String,
1361    /// 線色(ARGB8888)。
1362    pub stroke: u32,
1363    /// 線色の文字列表現。
1364    pub stroke_str: alloc::string::String,
1365    pub line_width: f32,
1366    /// `lineCap`。
1367    pub line_cap: LineCap,
1368    /// `lineJoin`。
1369    pub line_join: LineJoin,
1370    /// `miterLimit`。
1371    pub miter_limit: f32,
1372    /// `setLineDash` のパターン。空なら実線。
1373    pub line_dash: Vec<f32>,
1374    /// `lineDashOffset`。
1375    pub line_dash_offset: f32,
1376    /// クリップ領域。`None` なら全面。
1377    pub clip: Option<alloc::sync::Arc<ClipMask>>,
1378    /// `globalAlpha`(0.0〜1.0)。
1379    pub global_alpha: f32,
1380    /// フォント指定文字列(例: "10px sans-serif")。
1381    pub font_str: alloc::string::String,
1382    /// 水平揃え文字列(例: "start")。
1383    pub text_align_str: alloc::string::String,
1384    /// 垂直ベースライン文字列(例: "alphabetic")。
1385    pub text_baseline_str: alloc::string::String,
1386    /// 画像平滑化有効フラグ(imageSmoothingEnabled)。
1387    pub image_smoothing_enabled: bool,
1388    /// 塗りグラデーション(指定時は単色 fill より優先)。
1389    pub fill_gradient: Option<CanvasGradient>,
1390    /// 線グラデーション。
1391    pub stroke_gradient: Option<CanvasGradient>,
1392    /// 塗りパターン(指定時は単色 fill より優先)。
1393    pub fill_pattern: Option<CanvasPattern>,
1394    /// 線パターン。
1395    pub stroke_pattern: Option<CanvasPattern>,
1396    /// shadowBlur(非負の float。既定 0.0)。
1397    pub shadow_blur: f32,
1398    /// shadowColor(ARGB8888。既定 0x0000_0000 完全透明)。
1399    pub shadow_color: u32,
1400    /// shadowColor の文字列表現(既定 "rgba(0, 0, 0, 0)")。
1401    pub shadow_color_str: alloc::string::String,
1402    /// shadowOffsetX(既定 0.0)。
1403    pub shadow_offset_x: f32,
1404    /// shadowOffsetY(既定 0.0)。
1405    pub shadow_offset_y: f32,
1406    /// CSS filter 文字列(既定 "none")。
1407    pub filter: alloc::string::String,
1408    /// 合成モード(既定 `SourceOver`)。
1409    pub global_composite_operation: CompositeOperation,
1410    /// 合成モードの文字列表現(既定 `"source-over"`)。
1411    pub global_composite_operation_str: alloc::string::String,
1412    /// 書字方向(既定 `Inherit`)。
1413    pub direction: CanvasDirection,
1414    /// 書字方向の文字列表現(既定 `"inherit"`)。
1415    pub direction_str: alloc::string::String,
1416}
1417
1418impl DrawState {
1419    /// 影描画が有効か(アルファ > 0 かつ オフセットまたはぼかしが存在する)。
1420    pub fn has_shadow(&self) -> bool {
1421        let a = (self.shadow_color >> 24) & 0xFF;
1422        a > 0 && (self.shadow_offset_x != 0.0 || self.shadow_offset_y != 0.0 || self.shadow_blur > 0.0)
1423    }
1424
1425    /// 実効的な書字方向を返す(`Inherit` は `Ltr` にフォールバック)。
1426    pub fn effective_direction(&self) -> CanvasDirection {
1427        match self.direction {
1428            CanvasDirection::Rtl => CanvasDirection::Rtl,
1429            _ => CanvasDirection::Ltr,
1430        }
1431    }
1432}
1433
1434impl Default for DrawState {
1435    fn default() -> Self {
1436        DrawState {
1437            transform: Matrix::identity(),
1438            // Canvas の既定は不透明な黒。
1439            fill: 0xFF00_0000,
1440            fill_str: alloc::string::String::from("#000000"),
1441            stroke: 0xFF00_0000,
1442            stroke_str: alloc::string::String::from("#000000"),
1443            line_width: 1.0,
1444            line_cap: LineCap::Butt,
1445            line_join: LineJoin::Miter,
1446            miter_limit: DEFAULT_MITER_LIMIT,
1447            line_dash: Vec::new(),
1448            line_dash_offset: 0.0,
1449            clip: None,
1450            global_alpha: 1.0,
1451            font_str: alloc::string::String::from("10px sans-serif"),
1452            text_align_str: alloc::string::String::from("start"),
1453            text_baseline_str: alloc::string::String::from("alphabetic"),
1454            image_smoothing_enabled: true,
1455            fill_gradient: None,
1456            stroke_gradient: None,
1457            fill_pattern: None,
1458            stroke_pattern: None,
1459            shadow_blur: 0.0,
1460            shadow_color: 0x0000_0000,
1461            shadow_color_str: alloc::string::String::from("rgba(0, 0, 0, 0)"),
1462            shadow_offset_x: 0.0,
1463            shadow_offset_y: 0.0,
1464            filter: alloc::string::String::from("none"),
1465            global_composite_operation: CompositeOperation::SourceOver,
1466            global_composite_operation_str: alloc::string::String::from("source-over"),
1467            direction: CanvasDirection::Inherit,
1468            direction_str: alloc::string::String::from("inherit"),
1469        }
1470    }
1471}
1472
1473/// 1 つの `<canvas>` に対する 2D コンテキストの全状態。
1474///
1475/// JS 側のオブジェクトは軽いハンドルだけを持ち、実体はこれを指す。
1476/// パスと面はどちらもここに持たせて、描画の途中経過が
1477/// JS オブジェクトの寿命に左右されないようにする。
1478#[derive(Debug)]
1479pub struct Canvas2dContext {
1480    pub surface: Surface,
1481    pub path: PathBuilder,
1482    pub state: DrawState,
1483    /// `save()` の退避スタック。
1484    stack: Vec<DrawState>,
1485}
1486
1487/// `save()` の入れ子上限。
1488///
1489/// 際限なく積むと、壊れた(`restore` を呼ばない)スクリプトで
1490/// ヒープを食い潰す。実用上 32 段もあれば足りる。
1491const MAX_SAVE_DEPTH: usize = 32;
1492
1493impl Canvas2dContext {
1494    pub fn new(width: u32, height: u32) -> Option<Self> {
1495        Some(Canvas2dContext {
1496            surface: Surface::new(width, height)?,
1497            path: PathBuilder::new(),
1498            state: DrawState::default(),
1499            stack: Vec::new(),
1500        })
1501    }
1502
1503    /// 現在の状態を退避する。上限を超えたら黙って無視する
1504    /// (`restore` との対応が崩れるが、落ちるよりはよい)。
1505    pub fn save(&mut self) {
1506        if self.stack.len() >= MAX_SAVE_DEPTH {
1507            return;
1508        }
1509        self.stack.push(self.state.clone());
1510    }
1511
1512    /// 直近の退避状態へ戻す。スタックが空なら何もしない
1513    /// (仕様どおり。`restore` の呼び過ぎはエラーではない)。
1514    pub fn restore(&mut self) {
1515        if let Some(s) = self.stack.pop() {
1516            self.state = s;
1517        }
1518    }
1519
1520    /// `globalAlpha` を適用した色を返す。
1521    ///
1522    /// アルファは掛け算で合成する。`globalAlpha=0.5` の半透明色は
1523    /// さらに薄くなる(仕様どおり)。
1524    pub fn apply_alpha(&self, argb: u32) -> u32 {
1525        let ga = self.state.global_alpha.clamp(0.0, 1.0);
1526        if ga >= 1.0 {
1527            return argb;
1528        }
1529        let a = ((argb >> 24) & 0xFF) as f32 * ga;
1530        let a = libm::roundf(a).clamp(0.0, 255.0) as u32;
1531        (a << 24) | (argb & 0x00FF_FFFF)
1532    }
1533
1534    /// `globalAlpha` を適用した影の色(ARGB8888)を返す。
1535    pub fn shadow_color_argb(&self) -> u32 {
1536        self.apply_alpha(self.state.shadow_color)
1537    }
1538
1539    /// 現在の変換を適用して矩形を塗る。
1540    ///
1541    /// 変換が平行移動と拡大だけなら矩形のまま塗れるが、回転が入ると
1542    /// 軸平行でなくなる。判定を分けず、常にパス経由で塗ることで
1543    /// 回転時も正しくなるようにしている。
1544    pub fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
1545        self.sync_clip();
1546        let mut p = PathBuilder::new();
1547        p.rect(&self.state.transform, x, y, w, h);
1548        let sub = p.finish();
1549        if self.state.has_shadow() {
1550            let shadow_sub = offset_subpaths(&sub, self.state.shadow_offset_x, self.state.shadow_offset_y);
1551            let s_color = self.shadow_color_argb();
1552            self.surface.fill_path(&shadow_sub, FillRule::NonZero, s_color);
1553        }
1554        if let Some(ref grad) = self.state.fill_gradient {
1555            self.surface.fill_path_gradient(&sub, FillRule::NonZero, grad, self.state.global_alpha.clamp(0.0, 1.0));
1556        } else if let Some(ref pat) = self.state.fill_pattern {
1557            self.surface.fill_path_pattern(&sub, FillRule::NonZero, pat, self.state.global_alpha.clamp(0.0, 1.0));
1558        } else {
1559            let color = self.apply_alpha(self.state.fill);
1560            self.surface.fill_path(&sub, FillRule::NonZero, color);
1561        }
1562    }
1563
1564    /// 矩形を透明で消す。`globalAlpha` の影響は受けない(仕様どおり)。
1565    ///
1566    /// 回転が入っていると軸平行に消せないため、パスの外接矩形を消す。
1567    /// 厳密ではないが、`clearRect` に回転を掛ける使い方は稀。
1568    pub fn clear_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
1569        self.sync_clip();
1570        let m = &self.state.transform;
1571        let pts = [
1572            m.apply(x, y),
1573            m.apply(x + w, y),
1574            m.apply(x + w, y + h),
1575            m.apply(x, y + h),
1576        ];
1577        let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
1578        for (px, py) in pts {
1579            if !px.is_finite() || !py.is_finite() {
1580                return;
1581            }
1582            x0 = x0.min(px);
1583            y0 = y0.min(py);
1584            x1 = x1.max(px);
1585            y1 = y1.max(py);
1586        }
1587        self.surface.fill_rect(
1588            libm::floorf(x0) as i32,
1589            libm::floorf(y0) as i32,
1590            libm::ceilf(x1 - x0) as i32,
1591            libm::ceilf(y1 - y0) as i32,
1592            0,
1593            true,
1594        );
1595    }
1596
1597    /// 現在のパスを塗る。
1598    pub fn fill(&mut self, rule: FillRule) {
1599        self.sync_clip();
1600        let sub = self.path.finish();
1601        if self.state.has_shadow() {
1602            let shadow_sub = offset_subpaths(&sub, self.state.shadow_offset_x, self.state.shadow_offset_y);
1603            let s_color = self.shadow_color_argb();
1604            self.surface.fill_path(&shadow_sub, rule, s_color);
1605        }
1606        if let Some(ref grad) = self.state.fill_gradient {
1607            self.surface.fill_path_gradient(&sub, rule, grad, self.state.global_alpha.clamp(0.0, 1.0));
1608        } else if let Some(ref pat) = self.state.fill_pattern {
1609            self.surface.fill_path_pattern(&sub, rule, pat, self.state.global_alpha.clamp(0.0, 1.0));
1610        } else {
1611            let color = self.apply_alpha(self.state.fill);
1612            self.surface.fill_path(&sub, rule, color);
1613        }
1614    }
1615
1616    /// サブパス群を指定された描画状態(線幅、キャップ、ジョイント、ダッシュ、影、グラデーション、パターン)に従って線描する内部ヘルパー。
1617    fn stroke_subpaths_internal(&mut self, subpaths: &[SubPath]) {
1618        self.sync_clip();
1619        let dashed = self.apply_dash(subpaths);
1620        let lw = self.state.line_width;
1621        if !lw.is_finite() || lw <= 0.0 {
1622            return;
1623        }
1624        let (cap, join, limit) = (self.state.line_cap, self.state.line_join, self.state.miter_limit);
1625        let has_shadow = self.state.has_shadow();
1626        let shadow_offset_x = self.state.shadow_offset_x;
1627        let shadow_offset_y = self.state.shadow_offset_y;
1628        let s_color = if has_shadow { self.shadow_color_argb() } else { 0 };
1629        let alpha = self.state.global_alpha.clamp(0.0, 1.0);
1630        let stroke_color = self.apply_alpha(self.state.stroke);
1631        let stroke_grad = self.state.stroke_gradient.clone();
1632        let stroke_pat = self.state.stroke_pattern.clone();
1633
1634        if has_shadow {
1635            let shadow_sub = offset_subpaths(&dashed, shadow_offset_x, shadow_offset_y);
1636            self.surface.stroke_path_styled(&shadow_sub, s_color, lw, cap, join, limit);
1637        }
1638        if let Some(ref grad) = stroke_grad {
1639            let outline = stroke_to_outline(&dashed, lw, cap, join, limit);
1640            self.surface.fill_path_gradient(&outline, FillRule::NonZero, grad, alpha);
1641        } else if let Some(ref pat) = stroke_pat {
1642            let outline = stroke_to_outline(&dashed, lw, cap, join, limit);
1643            self.surface.fill_path_pattern(&outline, FillRule::NonZero, pat, alpha);
1644        } else {
1645            self.surface.stroke_path_styled(&dashed, stroke_color, lw, cap, join, limit);
1646        }
1647    }
1648
1649    /// 現在のパスを線描する。
1650    pub fn stroke(&mut self) {
1651        let sub = self.path.finish();
1652        self.stroke_subpaths_internal(&sub);
1653    }
1654
1655    /// 矩形を線描する(現在のパスは変更しない)。
1656    pub fn stroke_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
1657        let mut p = PathBuilder::new();
1658        p.rect(&self.state.transform, x, y, w, h);
1659        let sub = p.finish();
1660        self.stroke_subpaths_internal(&sub);
1661    }
1662
1663    /// 現在の変換へ平行移動を積む。
1664    pub fn translate(&mut self, tx: f32, ty: f32) {
1665        self.state.transform = self.state.transform.multiply(&Matrix::translate(tx, ty));
1666    }
1667
1668    /// 現在の変換へ拡大縮小を積む。
1669    pub fn scale(&mut self, sx: f32, sy: f32) {
1670        self.state.transform = self.state.transform.multiply(&Matrix::scale(sx, sy));
1671    }
1672
1673    /// 現在の変換へ回転を積む。
1674    pub fn rotate(&mut self, rad: f32) {
1675        self.state.transform = self.state.transform.multiply(&Matrix::rotate(rad));
1676    }
1677
1678    /// 変換行列を直接置き換える(`setTransform`)。
1679    pub fn set_transform(&mut self, m: Matrix) {
1680        self.state.transform = m;
1681    }
1682
1683    /// 変換を単位行列へ戻す(`resetTransform`)。
1684    pub fn reset_transform(&mut self) {
1685        self.state.transform = Matrix::identity();
1686    }
1687
1688    pub fn begin_path(&mut self) {
1689        self.path.begin_path();
1690    }
1691
1692    pub fn close_path(&mut self) {
1693        self.path.close_path();
1694    }
1695
1696    pub fn move_to(&mut self, x: f32, y: f32) {
1697        let m = self.state.transform;
1698        self.path.move_to(&m, x, y);
1699    }
1700
1701    pub fn line_to(&mut self, x: f32, y: f32) {
1702        let m = self.state.transform;
1703        self.path.line_to(&m, x, y);
1704    }
1705
1706    pub fn arc_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32) {
1707        let m = self.state.transform;
1708        self.path.arc_to(&m, x1, y1, x2, y2, radius);
1709    }
1710}
1711
1712
1713// ───────────── コンテキスト登録表 ─────────────
1714
1715/// `<canvas>` ごとの 2D コンテキストを id で保持する表。
1716///
1717/// JS 側のオブジェクトは `ObjKind::Host("canvas2d:<id>")` というタグだけを
1718/// 持ち、実体はここにある。JS の値へ Rust の可変状態を直接埋めると
1719/// 借用と寿命の扱いが複雑になるため、既存の DOM プロキシ(style/classList)
1720/// と同じ「タグで引く」流儀に合わせた。
1721static CONTEXTS: spin::Mutex<alloc::collections::BTreeMap<u32, Canvas2dContext>> =
1722    spin::Mutex::new(alloc::collections::BTreeMap::new());
1723
1724/// 次に払い出す id。
1725static NEXT_ID: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(1);
1726
1727/// 同時に保持するコンテキスト数の上限。
1728///
1729/// ページ遷移のたびに増え続けると、面(最大 64MB)が積み上がって
1730/// ヒープを食い潰す。上限を超えたら最も古い id から捨てる。
1731const MAX_CONTEXTS: usize = 8;
1732
1733/// 新しいコンテキストを作り、その id を返す。
1734///
1735/// 面が確保できない(サイズが不正・過大)場合は `None`。
1736pub fn create_context(width: u32, height: u32) -> Option<u32> {
1737    let ctx = Canvas2dContext::new(width, height)?;
1738    let id = NEXT_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
1739    let mut map = CONTEXTS.lock();
1740    // 上限を超えたら最も古い(id が小さい)ものから捨てる。
1741    while map.len() >= MAX_CONTEXTS {
1742        let Some(&oldest) = map.keys().next() else {
1743            break;
1744        };
1745        map.remove(&oldest);
1746    }
1747    map.insert(id, ctx);
1748    Some(id)
1749}
1750
1751/// id のコンテキストへ可変アクセスする。
1752///
1753/// 見つからなければ `f` を呼ばず `None` を返す(ページ遷移で
1754/// 破棄された後の呼び出しは黙って無視する)。
1755pub fn with_context<R>(id: u32, f: impl FnOnce(&mut Canvas2dContext) -> R) -> Option<R> {
1756    let mut map = CONTEXTS.lock();
1757    map.get_mut(&id).map(f)
1758}
1759
1760/// 保持している全コンテキストを破棄する(ページ遷移時に呼ぶ)。
1761pub fn clear_contexts() {
1762    CONTEXTS.lock().clear();
1763}
1764
1765/// `ObjKind::Host` のタグ文字列から id を取り出す。
1766///
1767/// 形式は `canvas2d:<id>`。合致しなければ `None`。
1768pub fn id_from_tag(tag: &str) -> Option<u32> {
1769    tag.strip_prefix("canvas2d:")?.parse::<u32>().ok()
1770}
1771
1772/// id からタグ文字列を作る。
1773pub fn tag_for_id(id: u32) -> alloc::string::String {
1774    alloc::format!("canvas2d:{}", id)
1775}
1776
1777impl PathBuilder {
1778    /// 楕円弧(`ellipse`)。
1779    ///
1780    /// `arc` の一般形で、x/y に別々の半径と、楕円自体の回転を持つ。
1781    /// 単位円上の点を「半径で伸ばす → 回転する → 中心へ移す」順で写す。
1782    /// この順序を取り違えると、回転した楕円が歪む。
1783    #[allow(clippy::too_many_arguments)]
1784    pub fn ellipse(
1785        &mut self,
1786        m: &Matrix,
1787        cx: f32,
1788        cy: f32,
1789        rx: f32,
1790        ry: f32,
1791        rotation: f32,
1792        start: f32,
1793        end: f32,
1794        anticlockwise: bool,
1795    ) {
1796        if !rx.is_finite()
1797            || !ry.is_finite()
1798            || rx < 0.0
1799            || ry < 0.0
1800            || !start.is_finite()
1801            || !end.is_finite()
1802            || !rotation.is_finite()
1803        {
1804            return;
1805        }
1806        let sweep = normalize_sweep(start, end, anticlockwise);
1807        let steps = arc_steps(sweep);
1808        let (rs, rc) = (libm::sinf(rotation), libm::cosf(rotation));
1809        for i in 0..=steps {
1810            let t = i as f32 / steps as f32;
1811            let ang = start + sweep * t;
1812            // 単位円 → 半径で伸ばす
1813            let ux = rx * libm::cosf(ang);
1814            let uy = ry * libm::sinf(ang);
1815            // → 楕円の回転 → 中心へ移す
1816            let px = cx + ux * rc - uy * rs;
1817            let py = cy + ux * rs + uy * rc;
1818            if i == 0 && self.current.is_none() {
1819                self.move_to(m, px, py);
1820            } else {
1821                self.line_to(m, px, py);
1822            }
1823        }
1824    }
1825
1826    /// 角丸矩形(`roundRect`)。
1827    ///
1828    /// 半径は 4 隅ぶん(左上・右上・右下・左下)。
1829    /// 半径の合計が辺の長さを超える場合は、仕様どおり**比率を保ったまま
1830    /// 縮小**する。しないと角が重なって図形が破綻する。
1831    #[allow(clippy::too_many_arguments)]
1832    pub fn round_rect(
1833        &mut self,
1834        m: &Matrix,
1835        x: f32,
1836        y: f32,
1837        w: f32,
1838        h: f32,
1839        radii: [f32; 4],
1840    ) {
1841        if !w.is_finite() || !h.is_finite() || w == 0.0 || h == 0.0 {
1842            return;
1843        }
1844        // 負の幅・高さは反対側から描く(仕様)。
1845        let (x, w) = if w < 0.0 { (x + w, -w) } else { (x, w) };
1846        let (y, h) = if h < 0.0 { (y + h, -h) } else { (y, h) };
1847
1848        let mut r = [0.0f32; 4];
1849        for (i, v) in radii.iter().enumerate() {
1850            r[i] = if v.is_finite() && *v > 0.0 { *v } else { 0.0 };
1851        }
1852        // 辺ごとに「両端の半径の和」が辺長を超えないよう縮小率を求める。
1853        let mut scale = 1.0f32;
1854        let pairs = [
1855            (r[0] + r[1], w), // 上辺: 左上 + 右上
1856            (r[2] + r[3], w), // 下辺: 右下 + 左下
1857            (r[1] + r[2], h), // 右辺: 右上 + 右下
1858            (r[3] + r[0], h), // 左辺: 左下 + 左上
1859        ];
1860        for (sum, len) in pairs {
1861            if sum > 0.0 && sum > len {
1862                scale = scale.min(len / sum);
1863            }
1864        }
1865        for v in r.iter_mut() {
1866            *v *= scale;
1867        }
1868
1869        let half_pi = core::f32::consts::PI / 2.0;
1870        let pi = core::f32::consts::PI;
1871        self.flush();
1872        // 左上から時計回りに、直線 → 角の 1/4 円 を繰り返す。
1873        self.move_to(m, x + r[0], y);
1874        self.line_to(m, x + w - r[1], y);
1875        if r[1] > 0.0 {
1876            self.arc(m, x + w - r[1], y + r[1], r[1], -half_pi, 0.0, false);
1877        }
1878        self.line_to(m, x + w, y + h - r[2]);
1879        if r[2] > 0.0 {
1880            self.arc(m, x + w - r[2], y + h - r[2], r[2], 0.0, half_pi, false);
1881        }
1882        self.line_to(m, x + r[3], y + h);
1883        if r[3] > 0.0 {
1884            self.arc(m, x + r[3], y + h - r[3], r[3], half_pi, pi, false);
1885        }
1886        self.line_to(m, x, y + r[0]);
1887        if r[0] > 0.0 {
1888            self.arc(m, x + r[0], y + r[0], r[0], pi, pi + half_pi, false);
1889        }
1890        self.close_path();
1891    }
1892}
1893
1894impl Surface {
1895    /// 線幅つきで折れ線を描く。
1896    ///
1897    /// 各線分を「線幅ぶんの太さを持つ四角形」に展開して塗る。
1898    /// 線端・線結合(`lineCap`/`lineJoin`)は未対応で、
1899    /// 結合部には円を置いて隙間を埋める(`round` 相当の近似)。
1900    ///
1901    /// # なぜ四角形へ展開するか
1902    ///
1903    /// Bresenham を平行にずらして何本も引く方法は、斜線で
1904    /// 太さが不均一になる(見た目が痩せる)。線分に垂直な方向へ
1905    /// 幅の半分ずつ広げた四角形を塗れば、角度によらず一定の太さになる。
1906    /// 線幅つきで折れ線を描く(線端 `Butt`・結合 `Round`)。
1907    ///
1908    /// 線端・結合を選ぶ場合は `stroke_path_styled` を使う。
1909    pub fn stroke_path_width(&mut self, subpaths: &[SubPath], argb: u32, width: f32) {
1910        self.stroke_path_styled(
1911            subpaths,
1912            argb,
1913            width,
1914            LineCap::Butt,
1915            LineJoin::Round,
1916            DEFAULT_MITER_LIMIT,
1917        );
1918    }
1919
1920    /// 中心 (cx, cy)・半径 r の円を塗る(線の結合部を埋めるのに使う)。
1921    fn fill_disc(&mut self, cx: f32, cy: f32, r: f32, argb: u32) {
1922        if !cx.is_finite() || !cy.is_finite() || !r.is_finite() || r <= 0.0 {
1923            return;
1924        }
1925        let y0 = (libm::floorf(cy - r) as i32).max(0);
1926        let y1 = (libm::ceilf(cy + r) as i32).min(self.height as i32);
1927        let r2 = r * r;
1928        for py in y0..y1 {
1929            let dy = py as f32 + 0.5 - cy;
1930            let d = r2 - dy * dy;
1931            if d < 0.0 {
1932                continue;
1933            }
1934            let dx = libm::sqrtf(d);
1935            let x0 = (libm::roundf(cx - dx) as i32).max(0);
1936            let x1 = (libm::roundf(cx + dx) as i32).min(self.width as i32);
1937            for px in x0..x1 {
1938                self.blend_pixel(px, py, argb);
1939            }
1940        }
1941    }
1942}
1943
1944impl Canvas2dContext {
1945    /// 現在のパスを、`lineWidth` を反映して線描する。
1946    pub fn stroke_with_width(&mut self) {
1947        self.stroke();
1948    }
1949}
1950
1951/// 線端の形状(`lineCap`)。
1952#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1953pub enum LineCap {
1954    /// 端点で切り落とす(既定)。
1955    Butt,
1956    /// 端点に半円を付ける。
1957    Round,
1958    /// 端点から線幅の半分だけ四角く伸ばす。
1959    Square,
1960}
1961
1962/// 線の結合形状(`lineJoin`)。
1963#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1964pub enum LineJoin {
1965    /// 外側の辺を延長して尖らせる(既定)。
1966    Miter,
1967    /// 結合点に円を置く。
1968    Round,
1969    /// 外側を直線で切り落とす。
1970    Bevel,
1971}
1972
1973/// `miterLimit` の既定値。仕様どおり 10。
1974pub const DEFAULT_MITER_LIMIT: f32 = 10.0;
1975
1976pub fn parse_line_cap(s: &str) -> Option<LineCap> {
1977    match s.trim() {
1978        "butt" => Some(LineCap::Butt),
1979        "round" => Some(LineCap::Round),
1980        "square" => Some(LineCap::Square),
1981        _ => None,
1982    }
1983}
1984
1985pub fn parse_line_join(s: &str) -> Option<LineJoin> {
1986    match s.trim() {
1987        "miter" => Some(LineJoin::Miter),
1988        "round" => Some(LineJoin::Round),
1989        "bevel" => Some(LineJoin::Bevel),
1990        _ => None,
1991    }
1992}
1993
1994/// 合成モード(`globalCompositeOperation` / Porter-Duff)。
1995#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1996pub enum CompositeOperation {
1997    SourceOver,
1998    SourceIn,
1999    SourceOut,
2000    SourceAtop,
2001    DestinationOver,
2002    DestinationIn,
2003    DestinationOut,
2004    DestinationAtop,
2005    Lighter,
2006    Copy,
2007    Xor,
2008}
2009
2010pub fn parse_composite_operation(s: &str) -> Option<CompositeOperation> {
2011    let t = s.trim();
2012    if t.eq_ignore_ascii_case("source-over") {
2013        Some(CompositeOperation::SourceOver)
2014    } else if t.eq_ignore_ascii_case("source-in") {
2015        Some(CompositeOperation::SourceIn)
2016    } else if t.eq_ignore_ascii_case("source-out") {
2017        Some(CompositeOperation::SourceOut)
2018    } else if t.eq_ignore_ascii_case("source-atop") {
2019        Some(CompositeOperation::SourceAtop)
2020    } else if t.eq_ignore_ascii_case("destination-over") {
2021        Some(CompositeOperation::DestinationOver)
2022    } else if t.eq_ignore_ascii_case("destination-in") {
2023        Some(CompositeOperation::DestinationIn)
2024    } else if t.eq_ignore_ascii_case("destination-out") {
2025        Some(CompositeOperation::DestinationOut)
2026    } else if t.eq_ignore_ascii_case("destination-atop") {
2027        Some(CompositeOperation::DestinationAtop)
2028    } else if t.eq_ignore_ascii_case("lighter") || t.eq_ignore_ascii_case("plus-lighter") {
2029        Some(CompositeOperation::Lighter)
2030    } else if t.eq_ignore_ascii_case("copy") {
2031        Some(CompositeOperation::Copy)
2032    } else if t.eq_ignore_ascii_case("xor") {
2033        Some(CompositeOperation::Xor)
2034    } else {
2035        None
2036    }
2037}
2038
2039/// 2 直線の交点。`p1` を通り向き `d1` の直線と、`p2` を通り向き `d2` の直線。
2040///
2041/// 平行(または反転)なら交点は無いので `None`。miter を延ばす先を
2042/// 求めるのに使い、`None` のときは bevel へ落とす。
2043pub fn line_intersection(
2044    p1: (f32, f32),
2045    d1: (f32, f32),
2046    p2: (f32, f32),
2047    d2: (f32, f32),
2048) -> Option<(f32, f32)> {
2049    let den = d1.0 * d2.1 - d1.1 * d2.0;
2050    // ほぼ平行なら交点が無限遠へ飛ぶ。閾値は目視で差が出ない範囲。
2051    if libm::fabsf(den) < 1e-6 {
2052        return None;
2053    }
2054    let t = ((p2.0 - p1.0) * d2.1 - (p2.1 - p1.1) * d2.0) / den;
2055    let x = p1.0 + d1.0 * t;
2056    let y = p1.1 + d1.1 * t;
2057    if !x.is_finite() || !y.is_finite() {
2058        return None;
2059    }
2060    Some((x, y))
2061}
2062
2063impl Surface {
2064    /// 線端・結合を指定して折れ線を描く。
2065    ///
2066    /// `stroke_path_width` は端 `Butt`・結合 `Round` でこれを呼ぶ。
2067    pub fn stroke_path_styled(
2068        &mut self,
2069        subpaths: &[SubPath],
2070        argb: u32,
2071        width: f32,
2072        cap: LineCap,
2073        join: LineJoin,
2074        miter_limit: f32,
2075    ) {
2076        mark_canvas_dirty();
2077        // 1 以下は従来の 1px 線で足りる(見た目も速度も有利)。
2078        if !width.is_finite() || width <= 1.0 {
2079            self.stroke_path(subpaths, argb);
2080            return;
2081        }
2082        let half = width / 2.0;
2083        let limit = if miter_limit.is_finite() && miter_limit > 0.0 {
2084            miter_limit
2085        } else {
2086            DEFAULT_MITER_LIMIT
2087        };
2088        for sp in subpaths {
2089            let n = sp.points.len();
2090            if n < 2 {
2091                continue;
2092            }
2093            let seg_end = if sp.closed { n } else { n - 1 };
2094
2095            // 各線分を、線分に垂直な方向へ half ずつ広げた四角形へ展開。
2096            for i in 0..seg_end {
2097                let a = sp.points[i];
2098                let b = sp.points[(i + 1) % n];
2099                let Some((dx, dy, len)) = unit_delta(a, b) else {
2100                    continue;
2101                };
2102                // 端が Square のときは、線分の外側へ half だけ伸ばす。
2103                let (mut ax, mut ay) = a;
2104                let (mut bx, mut by) = b;
2105                if cap == LineCap::Square && !sp.closed {
2106                    if i == 0 {
2107                        ax -= dx * half;
2108                        ay -= dy * half;
2109                    }
2110                    if i == seg_end - 1 {
2111                        bx += dx * half;
2112                        by += dy * half;
2113                    }
2114                }
2115                let _ = len;
2116                let (nx, ny) = (-dy * half, dx * half);
2117                self.fill_quad(
2118                    (ax + nx, ay + ny),
2119                    (bx + nx, by + ny),
2120                    (bx - nx, by - ny),
2121                    (ax - nx, ay - ny),
2122                    argb,
2123                );
2124            }
2125
2126            // 結合部を埋める。線分の四角形だけでは外側の角が欠ける。
2127            let joint_from = if sp.closed { 0 } else { 1 };
2128            let joint_to = if sp.closed { n } else { n - 1 };
2129            for j in joint_from..joint_to {
2130                let prev = sp.points[(j + n - 1) % n];
2131                let cur = sp.points[j];
2132                let next = sp.points[(j + 1) % n];
2133                self.fill_joint(prev, cur, next, half, argb, join, limit);
2134            }
2135
2136            // 端が Round なら両端に半円(円で近似)を置く。
2137            if cap == LineCap::Round && !sp.closed {
2138                let first = sp.points[0];
2139                let last = sp.points[n - 1];
2140                self.fill_disc(first.0, first.1, half, argb);
2141                self.fill_disc(last.0, last.1, half, argb);
2142            }
2143        }
2144    }
2145
2146    /// 結合部 1 箇所を埋める。
2147    #[allow(clippy::too_many_arguments)]
2148    fn fill_joint(
2149        &mut self,
2150        prev: (f32, f32),
2151        cur: (f32, f32),
2152        next: (f32, f32),
2153        half: f32,
2154        argb: u32,
2155        join: LineJoin,
2156        limit: f32,
2157    ) {
2158        if join == LineJoin::Round {
2159            self.fill_disc(cur.0, cur.1, half, argb);
2160            return;
2161        }
2162        let (Some((d1x, d1y, _)), Some((d2x, d2y, _))) =
2163            (unit_delta(prev, cur), unit_delta(cur, next))
2164        else {
2165            return;
2166        };
2167        // 曲がる向き。外側だけを埋めれば足りる(内側は線分の四角形が覆う)。
2168        let cross = d1x * d2y - d1y * d2x;
2169        if libm::fabsf(cross) < 1e-6 {
2170            // まっすぐ、または折り返し。角は生じないので何もしない。
2171            return;
2172        }
2173        // 左法線を (-dy, dx) とすると、左折(cross>0)では外側は右。
2174        let s = if cross > 0.0 { -1.0f32 } else { 1.0f32 };
2175        let p1 = (cur.0 + s * -d1y * half, cur.1 + s * d1x * half);
2176        let p2 = (cur.0 + s * -d2y * half, cur.1 + s * d2x * half);
2177
2178        if join == LineJoin::Miter {
2179            if let Some(mp) = line_intersection(p1, (d1x, d1y), p2, (d2x, d2y)) {
2180                let (mx, my) = (mp.0 - cur.0, mp.1 - cur.1);
2181                let miter_len = libm::sqrtf(mx * mx + my * my);
2182                // 鋭角では尖りが際限なく伸びる。仕様どおり
2183                // miterLength/halfWidth が上限を超えたら bevel へ落とす。
2184                if miter_len / half <= limit {
2185                    self.fill_quad(cur, p1, mp, p2, argb);
2186                    return;
2187                }
2188            }
2189        }
2190        // Bevel(および miter の打ち切り)。外側を直線で結ぶ三角形。
2191        self.fill_tri(cur, p1, p2, argb);
2192    }
2193
2194    fn fill_quad(
2195        &mut self,
2196        a: (f32, f32),
2197        b: (f32, f32),
2198        c: (f32, f32),
2199        d: (f32, f32),
2200        argb: u32,
2201    ) {
2202        for p in [a, b, c, d] {
2203            if !p.0.is_finite() || !p.1.is_finite() {
2204                return;
2205            }
2206        }
2207        let poly = alloc::vec![SubPath {
2208            points: alloc::vec![a, b, c, d],
2209            closed: true,
2210        }];
2211        self.fill_path(&poly, FillRule::NonZero, argb);
2212    }
2213
2214    fn fill_tri(&mut self, a: (f32, f32), b: (f32, f32), c: (f32, f32), argb: u32) {
2215        for p in [a, b, c] {
2216            if !p.0.is_finite() || !p.1.is_finite() {
2217                return;
2218            }
2219        }
2220        let poly = alloc::vec![SubPath {
2221            points: alloc::vec![a, b, c],
2222            closed: true,
2223        }];
2224        self.fill_path(&poly, FillRule::NonZero, argb);
2225    }
2226}
2227
2228/// 多角形の符号付き面積を計算する(画面座標系: Y下向き)。
2229pub fn polygon_signed_area(pts: &[(f32, f32)]) -> f32 {
2230    let n = pts.len();
2231    if n < 3 {
2232        return 0.0;
2233    }
2234    let mut area = 0.0f32;
2235    for i in 0..n {
2236        let (x0, y0) = pts[i];
2237        let (x1, y1) = pts[(i + 1) % n];
2238        area += x0 * y1 - x1 * y0;
2239    }
2240    area * 0.5
2241}
2242
2243fn normalize_convex_poly(mut pts: Vec<(f32, f32)>) -> Option<SubPath> {
2244    for p in &pts {
2245        if !p.0.is_finite() || !p.1.is_finite() {
2246            return None;
2247        }
2248    }
2249    if pts.len() < 3 {
2250        return None;
2251    }
2252    let area = polygon_signed_area(&pts);
2253    if libm::fabsf(area) < 1e-5 {
2254        return None;
2255    }
2256    if area < 0.0 {
2257        pts.reverse();
2258    }
2259    Some(SubPath {
2260        points: pts,
2261        closed: true,
2262    })
2263}
2264
2265fn circle_poly(cx: f32, cy: f32, r: f32) -> Option<SubPath> {
2266    if !cx.is_finite() || !cy.is_finite() || !r.is_finite() || r <= 0.0 {
2267        return None;
2268    }
2269    const SEGMENTS: usize = 16;
2270    let mut pts = Vec::with_capacity(SEGMENTS);
2271    for k in 0..SEGMENTS {
2272        let theta = (k as f32) * (2.0 * core::f32::consts::PI / (SEGMENTS as f32));
2273        let px = cx + r * libm::cosf(theta);
2274        let py = cy + r * libm::sinf(theta);
2275        pts.push((px, py));
2276    }
2277    normalize_convex_poly(pts)
2278}
2279
2280/// パス(折れ線群)を、指定された線幅・端点形状・結合形状に従って
2281/// 閉じたポリゴン(アウトライン)のサブパス群に変換する純粋幾何関数。
2282///
2283/// 全ての出力ポリゴンは同一の巻き方向(正の符号付き面積)に揃えられ、
2284/// `FillRule::NonZero` によるラスタライズ(グラデーション/パターン)で
2285/// 穴や打ち消し合いが生じないように正規化される。
2286pub fn stroke_to_outline(
2287    subpaths: &[SubPath],
2288    width: f32,
2289    cap: LineCap,
2290    join: LineJoin,
2291    miter_limit: f32,
2292) -> Vec<SubPath> {
2293    if !width.is_finite() || width <= 0.0 || subpaths.is_empty() {
2294        return Vec::new();
2295    }
2296    let half = width / 2.0;
2297    let limit = if miter_limit.is_finite() && miter_limit > 0.0 {
2298        miter_limit
2299    } else {
2300        DEFAULT_MITER_LIMIT
2301    };
2302
2303    let mut result = Vec::new();
2304
2305    for sp in subpaths {
2306        let n = sp.points.len();
2307        if n < 2 {
2308            continue;
2309        }
2310        let seg_end = if sp.closed { n } else { n - 1 };
2311
2312        // 各線分の Quad
2313        for i in 0..seg_end {
2314            let a = sp.points[i];
2315            let b = sp.points[(i + 1) % n];
2316            let Some((dx, dy, _)) = unit_delta(a, b) else {
2317                continue;
2318            };
2319            let (mut ax, mut ay) = a;
2320            let (mut bx, mut by) = b;
2321            if cap == LineCap::Square && !sp.closed {
2322                if i == 0 {
2323                    ax -= dx * half;
2324                    ay -= dy * half;
2325                }
2326                if i == seg_end - 1 {
2327                    bx += dx * half;
2328                    by += dy * half;
2329                }
2330            }
2331            let (nx, ny) = (-dy * half, dx * half);
2332            let p0 = (ax + nx, ay + ny);
2333            let p1 = (bx + nx, by + ny);
2334            let p2 = (bx - nx, by - ny);
2335            let p3 = (ax - nx, ay - ny);
2336            if let Some(poly) = normalize_convex_poly(alloc::vec![p0, p1, p2, p3]) {
2337                result.push(poly);
2338            }
2339        }
2340
2341        // 結合部
2342        let joint_from = if sp.closed { 0 } else { 1 };
2343        let joint_to = if sp.closed { n } else { n - 1 };
2344        for j in joint_from..joint_to {
2345            let prev = sp.points[(j + n - 1) % n];
2346            let cur = sp.points[j];
2347            let next = sp.points[(j + 1) % n];
2348
2349            if join == LineJoin::Round {
2350                if let Some(circle) = circle_poly(cur.0, cur.1, half) {
2351                    result.push(circle);
2352                }
2353                continue;
2354            }
2355
2356            let (Some((d1x, d1y, _)), Some((d2x, d2y, _))) =
2357                (unit_delta(prev, cur), unit_delta(cur, next))
2358            else {
2359                continue;
2360            };
2361
2362            let cross = d1x * d2y - d1y * d2x;
2363            if libm::fabsf(cross) < 1e-6 {
2364                continue;
2365            }
2366            let s = if cross > 0.0 { -1.0f32 } else { 1.0f32 };
2367            let p1 = (cur.0 + s * -d1y * half, cur.1 + s * d1x * half);
2368            let p2 = (cur.0 + s * -d2y * half, cur.1 + s * d2x * half);
2369
2370            if join == LineJoin::Miter {
2371                if let Some(mp) = line_intersection(p1, (d1x, d1y), p2, (d2x, d2y)) {
2372                    let (mx, my) = (mp.0 - cur.0, mp.1 - cur.1);
2373                    let miter_len = libm::sqrtf(mx * mx + my * my);
2374                    if miter_len / half <= limit {
2375                        if let Some(poly) = normalize_convex_poly(alloc::vec![cur, p1, mp, p2]) {
2376                            result.push(poly);
2377                            continue;
2378                        }
2379                    }
2380                }
2381            }
2382            // Bevel(または miter limit 超過時)
2383            if let Some(poly) = normalize_convex_poly(alloc::vec![cur, p1, p2]) {
2384                result.push(poly);
2385            }
2386        }
2387
2388        // Round 端点
2389        if cap == LineCap::Round && !sp.closed {
2390            let first = sp.points[0];
2391            let last = sp.points[n - 1];
2392            if let Some(circle) = circle_poly(first.0, first.1, half) {
2393                result.push(circle);
2394            }
2395            if let Some(circle) = circle_poly(last.0, last.1, half) {
2396                result.push(circle);
2397            }
2398        }
2399    }
2400
2401    result
2402}
2403
2404/// `a` から `b` への単位ベクトルと距離。長さが 0 なら `None`。
2405pub fn unit_delta(a: (f32, f32), b: (f32, f32)) -> Option<(f32, f32, f32)> {
2406    let (dx, dy) = (b.0 - a.0, b.1 - a.1);
2407    if !dx.is_finite() || !dy.is_finite() {
2408        return None;
2409    }
2410    let len = libm::sqrtf(dx * dx + dy * dy);
2411    if len < 1e-6 {
2412        return None;
2413    }
2414    Some((dx / len, dy / len, len))
2415}
2416
2417/// JS が canvas へ描いたことを示す旗(エッジトリガ再描画用)。
2418///
2419/// canvas への描画はバッキングストア(`Surface`)を書き換えるだけで、
2420/// 画面には出ない。以前は「他の理由で再描画が起きたとき」にだけ
2421/// ついでに反映されていたため、JS が描いても画面が変わらないことがあった。
2422///
2423/// 立てっぱなしにすると毎フレーム全画面を描き直して画面が波打つので、
2424/// 画像完了(`image_results_ready`)と同じく**変化があった一度だけ**
2425/// 再描画させる。描画側が `take_canvas_dirty` で降ろす。
2426static CANVAS_DIRTY: core::sync::atomic::AtomicBool =
2427    core::sync::atomic::AtomicBool::new(false);
2428
2429/// 画素が変わったことを記録する。`Surface` の塗り口から呼ぶ。
2430pub fn mark_canvas_dirty() {
2431    CANVAS_DIRTY.store(true, core::sync::atomic::Ordering::Relaxed);
2432}
2433
2434/// 変化があったかを見る(降ろさない)。再描画するかの判断に使う。
2435pub fn canvas_dirty() -> bool {
2436    CANVAS_DIRTY.load(core::sync::atomic::Ordering::Relaxed)
2437}
2438
2439/// 変化があったかを見て、同時に降ろす。描画を始める側から呼ぶ。
2440pub fn take_canvas_dirty() -> bool {
2441    CANVAS_DIRTY.swap(false, core::sync::atomic::Ordering::Relaxed)
2442}
2443
2444/// 破線のパターンを正規化する(`setLineDash` の引数検査)。
2445///
2446/// 仕様では、負の値や非有限が混じるパターンは**まるごと捨てる**
2447/// (一部だけ直すのではない)。奇数個なら 2 周ぶんに複製して偶数個にする。
2448/// 全部 0 のパターンは「破線なし」と同じ扱い。
2449pub fn normalize_dash(pattern: &[f32]) -> Option<Vec<f32>> {
2450    if pattern.is_empty() {
2451        return None;
2452    }
2453    for v in pattern {
2454        if !v.is_finite() || *v < 0.0 {
2455            return None;
2456        }
2457    }
2458    if pattern.iter().all(|v| *v == 0.0) {
2459        return None;
2460    }
2461    let mut out: Vec<f32> = pattern.to_vec();
2462    // 奇数個なら繰り返して偶数個にする(仕様)。
2463    if out.len() % 2 == 1 {
2464        let dup = out.clone();
2465        out.extend(dup);
2466    }
2467    Some(out)
2468}
2469
2470/// 折れ線を破線に切り分ける。
2471///
2472/// `pattern` は「描く長さ・空ける長さ」の交互。`offset` はパターンの
2473/// 開始位置をずらす量(`lineDashOffset`)。戻り値は描く区間だけを集めた
2474/// 開いた折れ線の並びで、そのまま `stroke_path_styled` へ渡せる。
2475///
2476/// 線分の途中でパターンが切り替わるので、線分を長さで分割しながら進む。
2477pub fn dash_subpaths(subpaths: &[SubPath], pattern: &[f32], offset: f32) -> Vec<SubPath> {
2478    let Some(pat) = normalize_dash(pattern) else {
2479        return subpaths.to_vec();
2480    };
2481    let total: f32 = pat.iter().sum();
2482    if total <= 0.0 {
2483        return subpaths.to_vec();
2484    }
2485    let mut out: Vec<SubPath> = Vec::new();
2486    for sp in subpaths {
2487        let n = sp.points.len();
2488        if n < 2 {
2489            continue;
2490        }
2491        // パターン内の位置。offset は仕様どおり周期で巻き戻す。
2492        let mut phase = if offset.is_finite() {
2493            let m = libm::fmodf(offset, total);
2494            if m < 0.0 {
2495                m + total
2496            } else {
2497                m
2498            }
2499        } else {
2500            0.0
2501        };
2502        // phase がどの区間にいるか(偶数番が「描く」)。
2503        let mut idx = 0usize;
2504        while phase >= pat[idx] {
2505            phase -= pat[idx];
2506            idx = (idx + 1) % pat.len();
2507        }
2508        let mut drawing = idx % 2 == 0;
2509        let mut cur: Vec<(f32, f32)> = Vec::new();
2510        if drawing {
2511            cur.push(sp.points[0]);
2512        }
2513
2514        let seg_end = if sp.closed { n } else { n - 1 };
2515        for i in 0..seg_end {
2516            let a = sp.points[i];
2517            let b = sp.points[(i + 1) % n];
2518            let Some((dx, dy, len)) = unit_delta(a, b) else {
2519                continue;
2520            };
2521            let mut travelled = 0.0f32;
2522            while travelled < len {
2523                // この区間の残り長。
2524                let remain = pat[idx] - phase;
2525                let step = remain.min(len - travelled);
2526                travelled += step;
2527                phase += step;
2528                let p = (a.0 + dx * travelled, a.1 + dy * travelled);
2529                if phase >= pat[idx] - 1e-6 {
2530                    // 区間の切り替わり。描いていたなら、ここで区切る。
2531                    if drawing {
2532                        cur.push(p);
2533                        if cur.len() >= 2 {
2534                            out.push(SubPath {
2535                                points: core::mem::take(&mut cur),
2536                                closed: false,
2537                            });
2538                        } else {
2539                            cur.clear();
2540                        }
2541                    } else {
2542                        cur.clear();
2543                        cur.push(p);
2544                    }
2545                    phase = 0.0;
2546                    idx = (idx + 1) % pat.len();
2547                    drawing = !drawing;
2548                } else if drawing {
2549                    // 線分の終わりで、まだ同じ区間の途中。
2550                    cur.push(p);
2551                }
2552            }
2553        }
2554        if drawing && cur.len() >= 2 {
2555            out.push(SubPath {
2556                points: cur,
2557                closed: false,
2558            });
2559        }
2560    }
2561    out
2562}
2563
2564impl Canvas2dContext {
2565    /// 現在の破線設定をパスへ適用する。実線ならそのまま返す。
2566    pub fn apply_dash(&self, subpaths: &[SubPath]) -> Vec<SubPath> {
2567        if self.state.line_dash.is_empty() {
2568            return subpaths.to_vec();
2569        }
2570        dash_subpaths(subpaths, &self.state.line_dash, self.state.line_dash_offset)
2571    }
2572
2573    /// 現在のコンテキストの線描状態(lineWidth, lineCap, lineJoin, miterLimit, lineDash)を
2574    /// 反映して、指定された座標 (x, y) がストローク領域内にあるかを判定する。
2575    pub fn is_point_in_stroke(&self, subpaths: &[SubPath], x: f32, y: f32) -> bool {
2576        let dashed = self.apply_dash(subpaths);
2577        let outline = stroke_to_outline(
2578            &dashed,
2579            self.state.line_width,
2580            self.state.line_cap,
2581            self.state.line_join,
2582            self.state.miter_limit,
2583        );
2584        is_point_in_path(&outline, x, y, FillRule::NonZero)
2585    }
2586}
2587
2588// ───────────── クリップ領域 ─────────────
2589
2590/// クリップ領域(`clip()`)。画素ごとに「描いてよいか」を持つ。
2591///
2592/// 走査線ベースの塗りに合わせて、パスをその場でラスタライズして持つ。
2593/// 領域を式のまま持つと、交差のたびに判定が複雑になっていく。
2594#[derive(Debug, Clone)]
2595pub struct ClipMask {
2596    pub width: u32,
2597    pub height: u32,
2598    /// `true` の画素だけ描ける。長さは `width * height`。
2599    pub bits: Vec<bool>,
2600}
2601
2602impl ClipMask {
2603    /// パスからクリップ領域を作る。塗りと同じ走査線・同じ巻き数規則を使う。
2604    ///
2605    /// 塗りとずれた判定をすると、クリップの縁に 1px の隙間や food が出る。
2606    pub fn from_path(width: u32, height: u32, subpaths: &[SubPath], rule: FillRule) -> ClipMask {
2607        let mut bits = alloc::vec![false; (width as usize) * (height as usize)];
2608        for y in 0..height {
2609            // 塗りと同じくピクセル中心で判定する。
2610            let spans = scanline_spans(subpaths, y as f32 + 0.5, rule);
2611            for (x0, x1) in spans {
2612                let a = (libm::ceilf(x0 - 0.5) as i32).max(0);
2613                let b = (libm::ceilf(x1 - 0.5) as i32).min(width as i32);
2614                for x in a..b {
2615                    bits[y as usize * width as usize + x as usize] = true;
2616                }
2617            }
2618        }
2619        ClipMask {
2620            width,
2621            height,
2622            bits,
2623        }
2624    }
2625
2626    /// 2 つのクリップ領域の交わり。`clip()` は既存の領域を**狭める**だけで、
2627    /// 広げることはできない(仕様)。
2628    pub fn intersect(&self, other: &ClipMask) -> ClipMask {
2629        if self.width != other.width || self.height != other.height {
2630            // 大きさが食い違うことは無い想定。安全側(狭い方)へ倒す。
2631            return self.clone();
2632        }
2633        let bits = self
2634            .bits
2635            .iter()
2636            .zip(other.bits.iter())
2637            .map(|(a, b)| *a && *b)
2638            .collect();
2639        ClipMask {
2640            width: self.width,
2641            height: self.height,
2642            bits,
2643        }
2644    }
2645
2646    /// その画素に描いてよいか。範囲外は描けない。
2647    pub fn allows(&self, x: i32, y: i32) -> bool {
2648        if x < 0 || y < 0 || x as u32 >= self.width || y as u32 >= self.height {
2649            return false;
2650        }
2651        self.bits[y as usize * self.width as usize + x as usize]
2652    }
2653}
2654
2655impl Canvas2dContext {
2656    /// 現在のパスでクリップ領域を狭める(`clip()`)。
2657    ///
2658    /// `save`/`restore` で元に戻る。状態を複製するだけで復帰できるよう
2659    /// `Rc` で共有しており、色と違って**正しく戻せる**。
2660    pub fn clip(&mut self, rule: FillRule) {
2661        let sub = self.path.finish();
2662        if sub.is_empty() {
2663            return;
2664        }
2665        let mask = ClipMask::from_path(self.surface.width, self.surface.height, &sub, rule);
2666        let merged = match &self.state.clip {
2667            Some(prev) => prev.intersect(&mask),
2668            None => mask,
2669        };
2670        self.state.clip = Some(alloc::sync::Arc::new(merged));
2671    }
2672
2673    /// 描画前に、現在のクリップ領域および合成モードを `Surface` へ渡す。
2674    pub fn sync_clip(&mut self) {
2675        self.surface.clip = self.state.clip.clone();
2676        self.surface.composite_op = self.state.global_composite_operation;
2677    }
2678}
2679
2680// ───────────── ImageData ─────────────
2681
2682/// `Surface` の一部を `ImageData` のバイト列(RGBA8・行優先)へ取り出す。
2683///
2684/// 面の外にはみ出した部分は**透明**として埋める(仕様)。
2685/// はみ出しを切り詰めると、返る配列の大きさが要求と食い違ってしまう。
2686pub fn get_image_data(s: &Surface, x: i32, y: i32, w: i32, h: i32) -> Vec<u8> {
2687    if w <= 0 || h <= 0 {
2688        return Vec::new();
2689    }
2690    let mut out = alloc::vec![0u8; (w as usize) * (h as usize) * 4];
2691    for row in 0..h {
2692        for col in 0..w {
2693            let (sx, sy) = (x + col, y + row);
2694            if sx < 0 || sy < 0 || sx as u32 >= s.width || sy as u32 >= s.height {
2695                continue;
2696            }
2697            let argb = s.pixels[sy as usize * s.width as usize + sx as usize];
2698            let o = ((row as usize) * (w as usize) + col as usize) * 4;
2699            out[o] = ((argb >> 16) & 0xFF) as u8;
2700            out[o + 1] = ((argb >> 8) & 0xFF) as u8;
2701            out[o + 2] = (argb & 0xFF) as u8;
2702            out[o + 3] = ((argb >> 24) & 0xFF) as u8;
2703        }
2704    }
2705    out
2706}
2707
2708/// `ImageData` のバイト列を `Surface` へ書き戻す。
2709///
2710/// `putImageData` は**合成しない**(上書きする)。クリップも無視する。
2711/// これは仕様で、`fill` 系とは意味論が違う。
2712pub fn put_image_data(s: &mut Surface, data: &[u8], w: i32, h: i32, x: i32, y: i32) {
2713    if w <= 0 || h <= 0 {
2714        return;
2715    }
2716    // 長さが足りない配列は、足りるぶんだけ書く(途中で落とさない)。
2717    mark_canvas_dirty();
2718    for row in 0..h {
2719        for col in 0..w {
2720            let o = ((row as usize) * (w as usize) + col as usize) * 4;
2721            if o + 3 >= data.len() {
2722                return;
2723            }
2724            let (dx, dy) = (x + col, y + row);
2725            if dx < 0 || dy < 0 || dx as u32 >= s.width || dy as u32 >= s.height {
2726                continue;
2727            }
2728            let argb = ((data[o + 3] as u32) << 24)
2729                | ((data[o] as u32) << 16)
2730                | ((data[o + 1] as u32) << 8)
2731                | (data[o + 2] as u32);
2732            s.pixels[dy as usize * s.width as usize + dx as usize] = argb;
2733        }
2734    }
2735}
2736
2737/// canvas の大きさ変更。`<canvas width>`/`height` への代入で呼ぶ。
2738///
2739/// 仕様では、大きさを代入すると**同じ値でも**内容と描画状態が
2740/// まるごと初期化される。これを省くと、大きさを設定し直す定型句
2741/// (`canvas.width = canvas.width` による消去)が効かなくなる。
2742///
2743/// 大きさが上限を超える等で面が作れなければ何もしない(元の面を残す)。
2744pub fn resize_context(id: u32, width: u32, height: u32) -> bool {
2745    let Some(fresh) = Surface::new(width, height) else {
2746        return false;
2747    };
2748    mark_canvas_dirty();
2749    with_context(id, |c| {
2750        c.surface = fresh;
2751        c.state = DrawState::default();
2752        c.path = PathBuilder::new();
2753        c.stack.clear();
2754    })
2755    .is_some()
2756}
2757
2758// ───────────── 画像の転送 ─────────────
2759
2760/// 転送元の画素(RGBA8・行優先)。
2761pub struct ImageSource<'a> {
2762    pub width: u32,
2763    pub height: u32,
2764    pub rgba: &'a [u8],
2765}
2766
2767impl ImageSource<'_> {
2768    /// 最近傍で 1 画素を読む。範囲外は透明。
2769    pub fn sample(&self, x: i32, y: i32) -> u32 {
2770        if x < 0 || y < 0 || x as u32 >= self.width || y as u32 >= self.height {
2771            return 0;
2772        }
2773        let o = (y as usize * self.width as usize + x as usize) * 4;
2774        if o + 3 >= self.rgba.len() {
2775            return 0;
2776        }
2777        ((self.rgba[o + 3] as u32) << 24)
2778            | ((self.rgba[o] as u32) << 16)
2779            | ((self.rgba[o + 1] as u32) << 8)
2780            | (self.rgba[o + 2] as u32)
2781    }
2782
2783    /// バイリニア補間で 1 画素を読む(imageSmoothingEnabled 用)。
2784    pub fn sample_bilinear(&self, sxf: f32, syf: f32) -> u32 {
2785        let u = sxf - 0.5;
2786        let v = syf - 0.5;
2787        let x0 = libm::floorf(u) as i32;
2788        let y0 = libm::floorf(v) as i32;
2789        let x1 = x0 + 1;
2790        let y1 = y0 + 1;
2791
2792        let fx = ((u - x0 as f32) * 256.0).clamp(0.0, 256.0) as u32;
2793        let fy = ((v - y0 as f32) * 256.0).clamp(0.0, 256.0) as u32;
2794
2795        let c00 = self.sample(x0, y0);
2796        let c10 = self.sample(x1, y0);
2797        let c01 = self.sample(x0, y1);
2798        let c11 = self.sample(x1, y1);
2799
2800        if c00 == c10 && c10 == c01 && c01 == c11 {
2801            return c00;
2802        }
2803
2804        let w00 = (256 - fx) * (256 - fy);
2805        let w10 = fx * (256 - fy);
2806        let w01 = (256 - fx) * fy;
2807        let w11 = fx * fy;
2808
2809        let a = (((c00 >> 24) & 0xFF) * w00
2810            + ((c10 >> 24) & 0xFF) * w10
2811            + ((c01 >> 24) & 0xFF) * w01
2812            + ((c11 >> 24) & 0xFF) * w11 + 32768) >> 16;
2813
2814        let r = (((c00 >> 16) & 0xFF) * w00
2815            + ((c10 >> 16) & 0xFF) * w10
2816            + ((c01 >> 16) & 0xFF) * w01
2817            + ((c11 >> 16) & 0xFF) * w11 + 32768) >> 16;
2818
2819        let g = (((c00 >> 8) & 0xFF) * w00
2820            + ((c10 >> 8) & 0xFF) * w10
2821            + ((c01 >> 8) & 0xFF) * w01
2822            + ((c11 >> 8) & 0xFF) * w11 + 32768) >> 16;
2823
2824        let b = ((c00 & 0xFF) * w00
2825            + (c10 & 0xFF) * w10
2826            + (c01 & 0xFF) * w01
2827            + (c11 & 0xFF) * w11 + 32768) >> 16;
2828
2829        (a.min(255) << 24) | (r.min(255) << 16) | (g.min(255) << 8) | b.min(255)
2830    }
2831}
2832
2833/// 転送の指定(`drawImage` の 9 引数形に対応)。
2834#[derive(Debug, Clone, Copy)]
2835pub struct BlitSpec {
2836    /// 転送元の矩形。
2837    pub sx: f32,
2838    pub sy: f32,
2839    pub sw: f32,
2840    pub sh: f32,
2841    /// 転送先の矩形(変換前の canvas 座標)。
2842    pub dx: f32,
2843    pub dy: f32,
2844    pub dw: f32,
2845    pub dh: f32,
2846}
2847
2848impl Surface {
2849    /// 画像を現在の変換のもとで転送する(`drawImage`)。
2850    ///
2851    /// # なぜ逆変換で引くか
2852    ///
2853    /// 転送元を 1 画素ずつ変換先へ「押し出す」と、拡大時に隙間が空き
2854    /// 回転時には縞が出る。転送先の画素ごとに**逆変換で元を引く**と、
2855    /// どの倍率・角度でも穴が空かない。
2856    ///
2857    /// 変換が退化している(面積 0 に潰れる)場合は何もしない。
2858    pub fn draw_image(&mut self, src: &ImageSource, spec: &BlitSpec, m: &Matrix, alpha: f32, smooth: bool) {
2859        self.draw_image_with_filter(src, spec, m, alpha, smooth, "none");
2860    }
2861
2862    /// 画像を描画する(CSS filter 対応)。
2863    pub fn draw_image_with_filter(
2864        &mut self,
2865        src: &ImageSource,
2866        spec: &BlitSpec,
2867        m: &Matrix,
2868        alpha: f32,
2869        smooth: bool,
2870        filter: &str,
2871    ) {
2872        if spec.dw == 0.0 || spec.dh == 0.0 || spec.sw == 0.0 || spec.sh == 0.0 {
2873            return;
2874        }
2875        for v in [
2876            spec.sx, spec.sy, spec.sw, spec.sh, spec.dx, spec.dy, spec.dw, spec.dh,
2877        ] {
2878            if !v.is_finite() {
2879                return;
2880            }
2881        }
2882        let Some(inv) = m.invert() else {
2883            return;
2884        };
2885        mark_canvas_dirty();
2886
2887        // 転送先の 4 隅を変換して、走査すべき範囲を求める。
2888        let corners = [
2889            m.apply(spec.dx, spec.dy),
2890            m.apply(spec.dx + spec.dw, spec.dy),
2891            m.apply(spec.dx + spec.dw, spec.dy + spec.dh),
2892            m.apply(spec.dx, spec.dy + spec.dh),
2893        ];
2894        let mut x0 = corners[0].0;
2895        let mut x1 = corners[0].0;
2896        let mut y0 = corners[0].1;
2897        let mut y1 = corners[0].1;
2898        for (cx, cy) in corners.iter().skip(1) {
2899            x0 = x0.min(*cx);
2900            x1 = x1.max(*cx);
2901            y0 = y0.min(*cy);
2902            y1 = y1.max(*cy);
2903        }
2904        let px0 = (libm::floorf(x0) as i32).max(0);
2905        let px1 = (libm::ceilf(x1) as i32).min(self.width as i32);
2906        let py0 = (libm::floorf(y0) as i32).max(0);
2907        let py1 = (libm::ceilf(y1) as i32).min(self.height as i32);
2908
2909        let a = alpha.clamp(0.0, 1.0);
2910        let has_filter = filter != "none" && !filter.is_empty();
2911        for py in py0..py1 {
2912            for px in px0..px1 {
2913                // 画素中心を逆変換して、変換前の canvas 座標へ戻す。
2914                let (ux, uy) = inv.apply(px as f32 + 0.5, py as f32 + 0.5);
2915                // 転送先の矩形の中だけを描く。
2916                let tx = (ux - spec.dx) / spec.dw;
2917                let ty = (uy - spec.dy) / spec.dh;
2918                if !(0.0..1.0).contains(&tx) || !(0.0..1.0).contains(&ty) {
2919                    continue;
2920                }
2921                // 転送元の対応位置(smooth に応じてバイリニア / 最近傍)。
2922                let sxf = spec.sx + tx * spec.sw;
2923                let syf = spec.sy + ty * spec.sh;
2924                let mut argb = if smooth {
2925                    src.sample_bilinear(sxf, syf)
2926                } else {
2927                    src.sample(libm::floorf(sxf) as i32, libm::floorf(syf) as i32)
2928                };
2929                if argb >> 24 == 0 {
2930                    continue;
2931                }
2932                if has_filter {
2933                    argb = crate::os_lib::web_engine::css_filters::apply_css_filters_argb(filter, argb);
2934                }
2935                let argb = if a >= 1.0 {
2936                    argb
2937                } else {
2938                    let na = (((argb >> 24) & 0xFF) as f32 * a) as u32;
2939                    (na << 24) | (argb & 0x00FF_FFFF)
2940                };
2941                self.blend_pixel(px, py, argb);
2942            }
2943        }
2944    }
2945}
2946
2947// ───────────── テキスト ─────────────
2948
2949/// `textAlign`。
2950#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2951pub enum TextAlign {
2952    Start,
2953    End,
2954    Left,
2955    Right,
2956    Center,
2957}
2958
2959/// `textBaseline`。
2960#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2961pub enum TextBaseline {
2962    Top,
2963    Hanging,
2964    Middle,
2965    Alphabetic,
2966    Ideographic,
2967    Bottom,
2968}
2969
2970pub fn parse_text_align(s: &str) -> Option<TextAlign> {
2971    match s.trim() {
2972        "start" => Some(TextAlign::Start),
2973        "end" => Some(TextAlign::End),
2974        "left" => Some(TextAlign::Left),
2975        "right" => Some(TextAlign::Right),
2976        "center" => Some(TextAlign::Center),
2977        _ => None,
2978    }
2979}
2980
2981pub fn parse_text_baseline(s: &str) -> Option<TextBaseline> {
2982    match s.trim() {
2983        "top" => Some(TextBaseline::Top),
2984        "hanging" => Some(TextBaseline::Hanging),
2985        "middle" => Some(TextBaseline::Middle),
2986        "alphabetic" => Some(TextBaseline::Alphabetic),
2987        "ideographic" => Some(TextBaseline::Ideographic),
2988        "bottom" => Some(TextBaseline::Bottom),
2989        _ => None,
2990    }
2991}
2992
2993/// Canvas 2D の書字方向(`ctx.direction`)。
2994#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2995pub enum CanvasDirection {
2996    Ltr,
2997    Rtl,
2998    Inherit,
2999}
3000
3001impl Default for CanvasDirection {
3002    fn default() -> Self {
3003        CanvasDirection::Inherit
3004    }
3005}
3006
3007/// 文字列から `CanvasDirection` を判定する(大文字小文字無視)。
3008pub fn parse_canvas_direction(s: &str) -> Option<CanvasDirection> {
3009    let s = s.trim();
3010    if s.eq_ignore_ascii_case("ltr") {
3011        Some(CanvasDirection::Ltr)
3012    } else if s.eq_ignore_ascii_case("rtl") {
3013        Some(CanvasDirection::Rtl)
3014    } else if s.eq_ignore_ascii_case("inherit") {
3015        Some(CanvasDirection::Inherit)
3016    } else {
3017        None
3018    }
3019}
3020
3021/// 指定位置から見た、描き始めの x のずれ(書字方向対応)。
3022///
3023/// `start`/`end` は書字方向(`direction`)で決まる:
3024/// - LTR: `start` = 0.0 (左揃え), `end` = -total_advance (右揃え)
3025/// - RTL: `start` = -total_advance (右揃え), `end` = 0.0 (左揃え)
3026/// - `left` / `right` / `center` は書字方向に左右されず物理配置を維持。
3027pub fn align_offset_with_direction(
3028    align: TextAlign,
3029    total_advance: f32,
3030    direction: CanvasDirection,
3031) -> f32 {
3032    let is_rtl = matches!(direction, CanvasDirection::Rtl);
3033    match align {
3034        TextAlign::Start => {
3035            if is_rtl {
3036                -total_advance
3037            } else {
3038                0.0
3039            }
3040        }
3041        TextAlign::End => {
3042            if is_rtl {
3043                0.0
3044            } else {
3045                -total_advance
3046            }
3047        }
3048        TextAlign::Left => 0.0,
3049        TextAlign::Right => -total_advance,
3050        TextAlign::Center => -total_advance / 2.0,
3051    }
3052}
3053
3054/// 指定位置から見た、描き始めの x のずれ(後方互換: 既定 LTR)。
3055pub fn align_offset(align: TextAlign, total_advance: f32) -> f32 {
3056    align_offset_with_direction(align, total_advance, CanvasDirection::Ltr)
3057}
3058
3059/// 指定位置から見た、ベースラインの y のずれ。
3060///
3061/// 字形はベースライン基準で置かれるので、`top` 指定なら
3062/// ベースラインを下げる(正の向きへずらす)。
3063/// 比率はこの処理系の字形配置(アセンダ 0.85)に合わせた近似で、
3064/// フォントごとの実測値ではない。
3065pub fn baseline_offset(baseline: TextBaseline, size_px: f32) -> f32 {
3066    match baseline {
3067        TextBaseline::Alphabetic => 0.0,
3068        TextBaseline::Top => size_px * 0.85,
3069        TextBaseline::Hanging => size_px * 0.8,
3070        TextBaseline::Middle => size_px * 0.35,
3071        TextBaseline::Ideographic | TextBaseline::Bottom => -size_px * 0.15,
3072    }
3073}
3074
3075/// `font` から文字の大きさ(px)を取り出す。
3076///
3077/// CSS の `font` 一括指定は語順が自由なので、**`px` で終わる数値**を
3078/// 探す。`pt`/`em` 等は換算せず、見つからなければ `None`
3079/// (呼び出し側が既定の 10px を使う)。
3080pub fn parse_font_size(font: &str) -> Option<f32> {
3081    for tok in font.split(|c: char| c.is_whitespace() || c == '/') {
3082        let t = tok.trim();
3083        let Some(num) = t.strip_suffix("px") else {
3084            continue;
3085        };
3086        if let Ok(v) = num.parse::<f32>() {
3087            if v.is_finite() && v > 0.0 {
3088                return Some(v);
3089            }
3090        }
3091    }
3092    None
3093}
3094
3095/// 1 文字ぶんの字形(アルファ値のビットマップ)。
3096///
3097/// フォントの実装(`kernel::vector_font`)に依存しないよう、
3098/// ここでは**値として受け取る**。canvas 側を純粋に保つため。
3099pub struct GlyphBitmap<'a> {
3100    pub width: u32,
3101    pub height: u32,
3102    /// 字形の左上を、ペン位置(ベースライン左端)から見たずれ。
3103    pub x_offset: i32,
3104    pub y_offset: i32,
3105    /// 次の文字までの送り幅。
3106    pub advance: f32,
3107    /// 長さは `width * height`。0 は透明。
3108    pub alpha: &'a [u8],
3109}
3110
3111impl Surface {
3112    /// 字形を 1 つ描く。
3113    ///
3114    /// 字形は軸に沿ったビットマップだが、**現在の変換の下で**置きたい。
3115    /// アルファを色に載せた画像として `draw_image` に通すことで、
3116    /// 回転・拡大も転送と同じ経路で扱える(別実装を増やさない)。
3117    pub fn draw_glyph(&mut self, g: &GlyphBitmap, pen_x: f32, pen_y: f32, argb: u32, m: &Matrix) {
3118        if g.width == 0 || g.height == 0 {
3119            return;
3120        }
3121        let n = (g.width as usize) * (g.height as usize);
3122        if g.alpha.len() < n {
3123            return;
3124        }
3125        let (r, gg, b) = ((argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF);
3126        let base_a = (argb >> 24) & 0xFF;
3127        let mut rgba = alloc::vec![0u8; n * 4];
3128        for i in 0..n {
3129            let a = (g.alpha[i] as u32 * base_a / 255) as u8;
3130            rgba[i * 4] = r as u8;
3131            rgba[i * 4 + 1] = gg as u8;
3132            rgba[i * 4 + 2] = b as u8;
3133            rgba[i * 4 + 3] = a;
3134        }
3135        let src = ImageSource {
3136            width: g.width,
3137            height: g.height,
3138            rgba: &rgba,
3139        };
3140        let spec = BlitSpec {
3141            sx: 0.0,
3142            sy: 0.0,
3143            sw: g.width as f32,
3144            sh: g.height as f32,
3145            dx: pen_x + g.x_offset as f32,
3146            dy: pen_y + g.y_offset as f32,
3147            dw: g.width as f32,
3148            dh: g.height as f32,
3149        };
3150        self.draw_image(&src, &spec, m, 1.0, false);
3151    }
3152
3153    /// 字形の輪郭(ストローク)を描く。
3154    ///
3155    /// 線幅 `line_width` に応じて近傍へ輪郭マスクを膨張(dilation)させて描画する。
3156    pub fn draw_glyph_stroke(
3157        &mut self,
3158        g: &GlyphBitmap,
3159        pen_x: f32,
3160        pen_y: f32,
3161        stroke_argb: u32,
3162        line_width: f32,
3163        m: &Matrix,
3164    ) {
3165        if g.width == 0 || g.height == 0 || line_width <= 0.0 {
3166            return;
3167        }
3168        let n = (g.width as usize) * (g.height as usize);
3169        if g.alpha.len() < n {
3170            return;
3171        }
3172        let r = (line_width / 2.0).clamp(0.5, 4.0);
3173        let ir = libm::ceilf(r) as i32;
3174        let w = g.width as i32;
3175        let h = g.height as i32;
3176
3177        let mut stroke_alpha = alloc::vec![0u8; n];
3178        for y in 0..h {
3179            for x in 0..w {
3180                let idx = (y * w + x) as usize;
3181                let a = g.alpha[idx];
3182                if a > 0 {
3183                    for dy in -ir..=ir {
3184                        for dx in -ir..=ir {
3185                            let dist = libm::sqrtf((dx * dx + dy * dy) as f32);
3186                            if dist <= r {
3187                                let nx = x + dx;
3188                                let ny = y + dy;
3189                                if nx >= 0 && nx < w && ny >= 0 && ny < h {
3190                                    let nidx = (ny * w + nx) as usize;
3191                                    let falloff = if r > 0.0 {
3192                                        (1.0 - (dist / (r + 0.5))).clamp(0.0, 1.0)
3193                                    } else {
3194                                        1.0
3195                                    };
3196                                    let strk_val = (a as f32 * falloff) as u8;
3197                                    if strk_val > stroke_alpha[nidx] {
3198                                        stroke_alpha[nidx] = strk_val;
3199                                    }
3200                                }
3201                            }
3202                        }
3203                    }
3204                }
3205            }
3206        }
3207        let (r_col, g_col, b_col) = (
3208            (stroke_argb >> 16) & 0xFF,
3209            (stroke_argb >> 8) & 0xFF,
3210            stroke_argb & 0xFF,
3211        );
3212        let base_a = (stroke_argb >> 24) & 0xFF;
3213        let mut rgba = alloc::vec![0u8; n * 4];
3214        for i in 0..n {
3215            let a = (stroke_alpha[i] as u32 * base_a / 255) as u8;
3216            rgba[i * 4] = r_col as u8;
3217            rgba[i * 4 + 1] = g_col as u8;
3218            rgba[i * 4 + 2] = b_col as u8;
3219            rgba[i * 4 + 3] = a;
3220        }
3221        let src = ImageSource {
3222            width: g.width,
3223            height: g.height,
3224            rgba: &rgba,
3225        };
3226        let spec = BlitSpec {
3227            sx: 0.0,
3228            sy: 0.0,
3229            sw: g.width as f32,
3230            sh: g.height as f32,
3231            dx: pen_x + g.x_offset as f32,
3232            dy: pen_y + g.y_offset as f32,
3233            dw: g.width as f32,
3234            dh: g.height as f32,
3235        };
3236        self.draw_image(&src, &spec, m, 1.0, false);
3237    }
3238}
3239
3240
3241// ───────────── SVG パス文字列のパース ─────────────
3242
3243/// SVG パス文字列から浮動小数点数を順次読み取るトークナイザ。
3244struct SvgPathTokenizer<'a> {
3245    data: &'a [u8],
3246    pos: usize,
3247}
3248
3249impl<'a> SvgPathTokenizer<'a> {
3250    fn new(data: &'a str) -> Self {
3251        Self {
3252            data: data.as_bytes(),
3253            pos: 0,
3254        }
3255    }
3256
3257    fn skip_whitespace_and_comma(&mut self) {
3258        while self.pos < self.data.len() {
3259            let c = self.data[self.pos];
3260            if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' || c == b',' {
3261                self.pos += 1;
3262            } else {
3263                break;
3264            }
3265        }
3266    }
3267
3268    fn next_cmd(&mut self) -> Option<char> {
3269        self.skip_whitespace_and_comma();
3270        if self.pos >= self.data.len() {
3271            return None;
3272        }
3273        let c = self.data[self.pos] as char;
3274        if c.is_ascii_alphabetic() {
3275            self.pos += 1;
3276            Some(c)
3277        } else {
3278            None
3279        }
3280    }
3281
3282    fn next_f32(&mut self) -> Option<f32> {
3283        self.skip_whitespace_and_comma();
3284        if self.pos >= self.data.len() {
3285            return None;
3286        }
3287        let start = self.pos;
3288        if self.pos < self.data.len() && (self.data[self.pos] == b'-' || self.data[self.pos] == b'+') {
3289            self.pos += 1;
3290        }
3291        let mut has_digit = false;
3292        while self.pos < self.data.len() && self.data[self.pos].is_ascii_digit() {
3293            self.pos += 1;
3294            has_digit = true;
3295        }
3296        if self.pos < self.data.len() && self.data[self.pos] == b'.' {
3297            self.pos += 1;
3298            while self.pos < self.data.len() && self.data[self.pos].is_ascii_digit() {
3299                self.pos += 1;
3300                has_digit = true;
3301            }
3302        }
3303        if has_digit && self.pos < self.data.len() && (self.data[self.pos] == b'e' || self.data[self.pos] == b'E') {
3304            self.pos += 1;
3305            if self.pos < self.data.len() && (self.data[self.pos] == b'-' || self.data[self.pos] == b'+') {
3306                self.pos += 1;
3307            }
3308            while self.pos < self.data.len() && self.data[self.pos].is_ascii_digit() {
3309                self.pos += 1;
3310            }
3311        }
3312        if !has_digit {
3313            self.pos = start;
3314            return None;
3315        }
3316        let s = core::str::from_utf8(&self.data[start..self.pos]).ok()?;
3317        s.parse::<f32>().ok()
3318    }
3319
3320    fn next_flag(&mut self) -> Option<bool> {
3321        self.skip_whitespace_and_comma();
3322        if self.pos >= self.data.len() {
3323            return None;
3324        }
3325        let b = self.data[self.pos];
3326        if b == b'0' {
3327            self.pos += 1;
3328            Some(false)
3329        } else if b == b'1' {
3330            self.pos += 1;
3331            Some(true)
3332        } else {
3333            None
3334        }
3335    }
3336}
3337
3338/// W3C SVG 1.1 Appendix F.6.5: 端点パラメータ (x1, y1, rx, ry, phi, large_arc, sweep, x2, y2)
3339/// を中心パラメータ (cx, cy, rx, ry, phi, theta1, theta2, anticlockwise) に変換する。
3340#[allow(clippy::too_many_arguments)]
3341fn svg_arc_to_center(
3342    x1: f32,
3343    y1: f32,
3344    mut rx: f32,
3345    mut ry: f32,
3346    angle_deg: f32,
3347    large_arc: bool,
3348    sweep: bool,
3349    x2: f32,
3350    y2: f32,
3351) -> Option<(f32, f32, f32, f32, f32, f32, f32, bool)> {
3352    if libm::fabsf(x1 - x2) < 1e-6 && libm::fabsf(y1 - y2) < 1e-6 {
3353        return None;
3354    }
3355    rx = libm::fabsf(rx);
3356    ry = libm::fabsf(ry);
3357    if rx < 1e-6 || ry < 1e-6 {
3358        return None;
3359    }
3360
3361    let phi = angle_deg * (core::f32::consts::PI / 180.0);
3362    let cos_phi = libm::cosf(phi);
3363    let sin_phi = libm::sinf(phi);
3364
3365    // Step 1: (x1', y1') の計算
3366    let dx = (x1 - x2) / 2.0;
3367    let dy = (y1 - y2) / 2.0;
3368    let x1_prime = cos_phi * dx + sin_phi * dy;
3369    let y1_prime = -sin_phi * dx + cos_phi * dy;
3370
3371    // Step 2: 半径の拡大調整
3372    let lambda = (x1_prime * x1_prime) / (rx * rx) + (y1_prime * y1_prime) / (ry * ry);
3373    if lambda > 1.0 {
3374        let sqrt_lambda = libm::sqrtf(lambda);
3375        rx *= sqrt_lambda;
3376        ry *= sqrt_lambda;
3377    }
3378
3379    // Step 3: (cx', cy') の計算
3380    let rx_sq = rx * rx;
3381    let ry_sq = ry * ry;
3382    let x1_p_sq = x1_prime * x1_prime;
3383    let y1_p_sq = y1_prime * y1_prime;
3384
3385    let num = rx_sq * ry_sq - rx_sq * y1_p_sq - ry_sq * x1_p_sq;
3386    let den = rx_sq * y1_p_sq + ry_sq * x1_p_sq;
3387    let sq = if den > 0.0 { libm::fmaxf(0.0, num / den) } else { 0.0 };
3388    let sign = if large_arc == sweep { -1.0 } else { 1.0 };
3389    let coef = sign * libm::sqrtf(sq);
3390
3391    let cx_prime = coef * ((rx * y1_prime) / ry);
3392    let cy_prime = coef * -((ry * x1_prime) / rx);
3393
3394    // Step 4: (cx, cy) の復元
3395    let cx = cos_phi * cx_prime - sin_phi * cy_prime + (x1 + x2) / 2.0;
3396    let cy = sin_phi * cx_prime + cos_phi * cy_prime + (y1 + y2) / 2.0;
3397
3398    // Step 5: 開始角と掃引角の計算
3399    let ux = (x1_prime - cx_prime) / rx;
3400    let uy = (y1_prime - cy_prime) / ry;
3401    let vx = (-x1_prime - cx_prime) / rx;
3402    let vy = (-y1_prime - cy_prime) / ry;
3403
3404    let u_len = libm::sqrtf(ux * ux + uy * uy);
3405    let cos_theta1 = if u_len > 0.0 { (ux / u_len).clamp(-1.0, 1.0) } else { 1.0 };
3406    let mut theta1 = libm::acosf(cos_theta1);
3407    if uy < 0.0 {
3408        theta1 = -theta1;
3409    }
3410
3411    let dot = ux * vx + uy * vy;
3412    let v_len = libm::sqrtf(vx * vx + vy * vy);
3413    let uv_len = u_len * v_len;
3414    let cos_dtheta = if uv_len > 0.0 { (dot / uv_len).clamp(-1.0, 1.0) } else { 1.0 };
3415    let mut dtheta = libm::acosf(cos_dtheta);
3416    if (ux * vy - uy * vx) < 0.0 {
3417        dtheta = -dtheta;
3418    }
3419
3420    let tau = core::f32::consts::PI * 2.0;
3421    if !sweep && dtheta > 0.0 {
3422        dtheta -= tau;
3423    } else if sweep && dtheta < 0.0 {
3424        dtheta += tau;
3425    }
3426
3427    let anticlockwise = !sweep;
3428    let theta2 = theta1 + dtheta;
3429    Some((cx, cy, rx, ry, phi, theta1, theta2, anticlockwise))
3430}
3431
3432/// SVG パス文字列 `d` を解析し、`PathBuilder` へ追加する。
3433pub fn parse_svg_path_into(d: &str, p: &mut PathBuilder, m: &Matrix) {
3434    let mut tok = SvgPathTokenizer::new(d);
3435    let mut curr_x = 0.0f32;
3436    let mut curr_y = 0.0f32;
3437    let mut start_x = 0.0f32;
3438    let mut start_y = 0.0f32;
3439    let mut last_cmd = ' ';
3440    let mut last_cubic_ctrl: Option<(f32, f32)> = None;
3441    let mut last_quad_ctrl: Option<(f32, f32)> = None;
3442
3443    while tok.pos < tok.data.len() {
3444        tok.skip_whitespace_and_comma();
3445        if tok.pos >= tok.data.len() {
3446            break;
3447        }
3448        let cmd = if let Some(c) = tok.next_cmd() {
3449            last_cmd = c;
3450            c
3451        } else if last_cmd != ' ' {
3452            if last_cmd == 'M' {
3453                'L'
3454            } else if last_cmd == 'm' {
3455                'l'
3456            } else {
3457                last_cmd
3458            }
3459        } else {
3460            break;
3461        };
3462
3463        match cmd {
3464            'M' => {
3465                if let (Some(x), Some(y)) = (tok.next_f32(), tok.next_f32()) {
3466                    curr_x = x;
3467                    curr_y = y;
3468                    start_x = x;
3469                    start_y = y;
3470                    p.move_to(m, x, y);
3471                    last_cubic_ctrl = None;
3472                    last_quad_ctrl = None;
3473                }
3474            }
3475            'm' => {
3476                if let (Some(dx), Some(dy)) = (tok.next_f32(), tok.next_f32()) {
3477                    curr_x += dx;
3478                    curr_y += dy;
3479                    start_x = curr_x;
3480                    start_y = curr_y;
3481                    p.move_to(m, curr_x, curr_y);
3482                    last_cubic_ctrl = None;
3483                    last_quad_ctrl = None;
3484                }
3485            }
3486            'L' => {
3487                if let (Some(x), Some(y)) = (tok.next_f32(), tok.next_f32()) {
3488                    curr_x = x;
3489                    curr_y = y;
3490                    p.line_to(m, x, y);
3491                    last_cubic_ctrl = None;
3492                    last_quad_ctrl = None;
3493                }
3494            }
3495            'l' => {
3496                if let (Some(dx), Some(dy)) = (tok.next_f32(), tok.next_f32()) {
3497                    curr_x += dx;
3498                    curr_y += dy;
3499                    p.line_to(m, curr_x, curr_y);
3500                    last_cubic_ctrl = None;
3501                    last_quad_ctrl = None;
3502                }
3503            }
3504            'H' => {
3505                if let Some(x) = tok.next_f32() {
3506                    curr_x = x;
3507                    p.line_to(m, curr_x, curr_y);
3508                    last_cubic_ctrl = None;
3509                    last_quad_ctrl = None;
3510                }
3511            }
3512            'h' => {
3513                if let Some(dx) = tok.next_f32() {
3514                    curr_x += dx;
3515                    p.line_to(m, curr_x, curr_y);
3516                    last_cubic_ctrl = None;
3517                    last_quad_ctrl = None;
3518                }
3519            }
3520            'V' => {
3521                if let Some(y) = tok.next_f32() {
3522                    curr_y = y;
3523                    p.line_to(m, curr_x, curr_y);
3524                    last_cubic_ctrl = None;
3525                    last_quad_ctrl = None;
3526                }
3527            }
3528            'v' => {
3529                if let Some(dy) = tok.next_f32() {
3530                    curr_y += dy;
3531                    p.line_to(m, curr_x, curr_y);
3532                    last_cubic_ctrl = None;
3533                    last_quad_ctrl = None;
3534                }
3535            }
3536            'C' => {
3537                if let (Some(x1), Some(y1), Some(x2), Some(y2), Some(x), Some(y)) = (
3538                    tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32()
3539                ) {
3540                    p.bezier_curve_to(m, x1, y1, x2, y2, x, y);
3541                    last_cubic_ctrl = Some((x2, y2));
3542                    last_quad_ctrl = None;
3543                    curr_x = x;
3544                    curr_y = y;
3545                }
3546            }
3547            'c' => {
3548                if let (Some(dx1), Some(dy1), Some(dx2), Some(dy2), Some(dx), Some(dy)) = (
3549                    tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32()
3550                ) {
3551                    let (x1, y1) = (curr_x + dx1, curr_y + dy1);
3552                    let (x2, y2) = (curr_x + dx2, curr_y + dy2);
3553                    let (x, y) = (curr_x + dx, curr_y + dy);
3554                    p.bezier_curve_to(m, x1, y1, x2, y2, x, y);
3555                    last_cubic_ctrl = Some((x2, y2));
3556                    last_quad_ctrl = None;
3557                    curr_x = x;
3558                    curr_y = y;
3559                }
3560            }
3561            'S' => {
3562                if let (Some(x2), Some(y2), Some(x), Some(y)) = (
3563                    tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32()
3564                ) {
3565                    let (x1, y1) = if let Some((prev_x2, prev_y2)) = last_cubic_ctrl {
3566                        (2.0 * curr_x - prev_x2, 2.0 * curr_y - prev_y2)
3567                    } else {
3568                        (curr_x, curr_y)
3569                    };
3570                    p.bezier_curve_to(m, x1, y1, x2, y2, x, y);
3571                    last_cubic_ctrl = Some((x2, y2));
3572                    last_quad_ctrl = None;
3573                    curr_x = x;
3574                    curr_y = y;
3575                }
3576            }
3577            's' => {
3578                if let (Some(dx2), Some(dy2), Some(dx), Some(dy)) = (
3579                    tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32()
3580                ) {
3581                    let (x1, y1) = if let Some((prev_x2, prev_y2)) = last_cubic_ctrl {
3582                        (2.0 * curr_x - prev_x2, 2.0 * curr_y - prev_y2)
3583                    } else {
3584                        (curr_x, curr_y)
3585                    };
3586                    let (x2, y2) = (curr_x + dx2, curr_y + dy2);
3587                    let (x, y) = (curr_x + dx, curr_y + dy);
3588                    p.bezier_curve_to(m, x1, y1, x2, y2, x, y);
3589                    last_cubic_ctrl = Some((x2, y2));
3590                    last_quad_ctrl = None;
3591                    curr_x = x;
3592                    curr_y = y;
3593                }
3594            }
3595            'Q' => {
3596                if let (Some(x1), Some(y1), Some(x), Some(y)) = (
3597                    tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32()
3598                ) {
3599                    p.quadratic_curve_to(m, x1, y1, x, y);
3600                    last_quad_ctrl = Some((x1, y1));
3601                    last_cubic_ctrl = None;
3602                    curr_x = x;
3603                    curr_y = y;
3604                }
3605            }
3606            'q' => {
3607                if let (Some(dx1), Some(dy1), Some(dx), Some(dy)) = (
3608                    tok.next_f32(), tok.next_f32(), tok.next_f32(), tok.next_f32()
3609                ) {
3610                    let (x1, y1) = (curr_x + dx1, curr_y + dy1);
3611                    let (x, y) = (curr_x + dx, curr_y + dy);
3612                    p.quadratic_curve_to(m, x1, y1, x, y);
3613                    last_quad_ctrl = Some((x1, y1));
3614                    last_cubic_ctrl = None;
3615                    curr_x = x;
3616                    curr_y = y;
3617                }
3618            }
3619            'T' => {
3620                if let (Some(x), Some(y)) = (tok.next_f32(), tok.next_f32()) {
3621                    let (cx, cy) = if let Some((prev_cx, prev_cy)) = last_quad_ctrl {
3622                        (2.0 * curr_x - prev_cx, 2.0 * curr_y - prev_cy)
3623                    } else {
3624                        (curr_x, curr_y)
3625                    };
3626                    p.quadratic_curve_to(m, cx, cy, x, y);
3627                    last_quad_ctrl = Some((cx, cy));
3628                    last_cubic_ctrl = None;
3629                    curr_x = x;
3630                    curr_y = y;
3631                }
3632            }
3633            't' => {
3634                if let (Some(dx), Some(dy)) = (tok.next_f32(), tok.next_f32()) {
3635                    let (cx, cy) = if let Some((prev_cx, prev_cy)) = last_quad_ctrl {
3636                        (2.0 * curr_x - prev_cx, 2.0 * curr_y - prev_cy)
3637                    } else {
3638                        (curr_x, curr_y)
3639                    };
3640                    let (x, y) = (curr_x + dx, curr_y + dy);
3641                    p.quadratic_curve_to(m, cx, cy, x, y);
3642                    last_quad_ctrl = Some((cx, cy));
3643                    last_cubic_ctrl = None;
3644                    curr_x = x;
3645                    curr_y = y;
3646                }
3647            }
3648            'A' | 'a' => {
3649                let rx_opt = tok.next_f32();
3650                let ry_opt = tok.next_f32();
3651                let angle_opt = tok.next_f32();
3652                let large_arc_opt = tok.next_flag();
3653                let sweep_opt = tok.next_flag();
3654                let x_opt = tok.next_f32();
3655                let y_opt = tok.next_f32();
3656
3657                if let (Some(rx), Some(ry), Some(angle), Some(large_arc), Some(sweep), Some(x), Some(y)) =
3658                    (rx_opt, ry_opt, angle_opt, large_arc_opt, sweep_opt, x_opt, y_opt)
3659                {
3660                    let (target_x, target_y) = if cmd == 'a' {
3661                        (curr_x + x, curr_y + y)
3662                    } else {
3663                        (x, y)
3664                    };
3665
3666                    if let Some((cx, cy, r_x, r_y, phi, theta1, theta2, anticlockwise)) =
3667                        svg_arc_to_center(curr_x, curr_y, rx, ry, angle, large_arc, sweep, target_x, target_y)
3668                    {
3669                        p.ellipse(m, cx, cy, r_x, r_y, phi, theta1, theta2, anticlockwise);
3670                    } else {
3671                        p.line_to(m, target_x, target_y);
3672                    }
3673                    curr_x = target_x;
3674                    curr_y = target_y;
3675                    last_cubic_ctrl = None;
3676                    last_quad_ctrl = None;
3677                }
3678            }
3679            'Z' | 'z' => {
3680                p.close_path();
3681                curr_x = start_x;
3682                curr_y = start_y;
3683                last_cubic_ctrl = None;
3684                last_quad_ctrl = None;
3685            }
3686            _ => {
3687                tok.pos += 1;
3688            }
3689        }
3690    }
3691}
3692
3693// ───────────── Path2D 登録表 ─────────────
3694
3695static PATH2D_MAP: spin::Mutex<alloc::collections::BTreeMap<u32, PathBuilder>> =
3696    spin::Mutex::new(alloc::collections::BTreeMap::new());
3697static NEXT_PATH2D_ID: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(1);
3698const MAX_PATH2D: usize = 64;
3699
3700pub fn create_path2d(init_svg: Option<&str>) -> u32 {
3701    let id = NEXT_PATH2D_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3702    let mut builder = PathBuilder::new();
3703    if let Some(svg) = init_svg {
3704        parse_svg_path_into(svg, &mut builder, &Matrix::identity());
3705    }
3706    let mut map = PATH2D_MAP.lock();
3707    while map.len() >= MAX_PATH2D {
3708        if let Some(&oldest) = map.keys().next() {
3709            map.remove(&oldest);
3710        } else {
3711            break;
3712        }
3713    }
3714    map.insert(id, builder);
3715    id
3716}
3717
3718pub fn clone_path2d(src_id: u32) -> u32 {
3719    let new_id = NEXT_PATH2D_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3720    let mut map = PATH2D_MAP.lock();
3721    while map.len() >= MAX_PATH2D {
3722        if let Some(&oldest) = map.keys().next() {
3723            map.remove(&oldest);
3724        } else {
3725            break;
3726        }
3727    }
3728    let cloned = map.get(&src_id).cloned().unwrap_or_else(PathBuilder::new);
3729    map.insert(new_id, cloned);
3730    new_id
3731}
3732
3733pub fn with_path2d<R>(id: u32, f: impl FnOnce(&mut PathBuilder) -> R) -> Option<R> {
3734    let mut map = PATH2D_MAP.lock();
3735    map.get_mut(&id).map(f)
3736}
3737
3738pub fn get_path2d_subpaths(id: u32) -> Option<Vec<SubPath>> {
3739    let map = PATH2D_MAP.lock();
3740    map.get(&id).map(|p| p.finish())
3741}
3742
3743pub fn add_path_to_path2d(dst_id: u32, src_id: u32, transform: Option<&Matrix>) {
3744    let mut map = PATH2D_MAP.lock();
3745    if let Some(src) = map.get(&src_id).cloned() {
3746        if let Some(dst) = map.get_mut(&dst_id) {
3747            dst.add_path(&src, transform);
3748        }
3749    }
3750}
3751
3752impl Canvas2dContext {
3753    /// 外部のパス(Path2D 等)を指定して塗る。
3754    pub fn fill_subpaths(&mut self, subpaths: &[SubPath], rule: FillRule) {
3755        self.sync_clip();
3756        if let Some(ref grad) = self.state.fill_gradient {
3757            self.surface.fill_path_gradient(subpaths, rule, grad, self.state.global_alpha.clamp(0.0, 1.0));
3758        } else if let Some(ref pat) = self.state.fill_pattern {
3759            self.surface.fill_path_pattern(subpaths, rule, pat, self.state.global_alpha.clamp(0.0, 1.0));
3760        } else {
3761            let color = self.apply_alpha(self.state.fill);
3762            self.surface.fill_path(subpaths, rule, color);
3763        }
3764    }
3765
3766    /// 外部のパス(Path2D 等)を指定して線を描く。
3767    pub fn stroke_subpaths(&mut self, subpaths: &[SubPath]) {
3768        self.stroke_subpaths_internal(subpaths);
3769    }
3770
3771    /// 外部のパス(Path2D 等)を指定してクリップする。
3772    pub fn clip_subpaths(&mut self, subpaths: &[SubPath], rule: FillRule) {
3773        if subpaths.is_empty() {
3774            return;
3775        }
3776        let mask = ClipMask::from_path(self.surface.width, self.surface.height, subpaths, rule);
3777        let merged = match &self.state.clip {
3778            Some(prev) => prev.intersect(&mask),
3779            None => mask,
3780        };
3781        self.state.clip = Some(alloc::sync::Arc::new(merged));
3782    }
3783}
3784
3785
3786// ───────────── CanvasGradient 登録表 ─────────────
3787
3788static GRADIENT_MAP: spin::Mutex<alloc::collections::BTreeMap<u32, CanvasGradient>> =
3789    spin::Mutex::new(alloc::collections::BTreeMap::new());
3790static NEXT_GRADIENT_ID: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(1);
3791const MAX_GRADIENTS: usize = 64;
3792
3793pub fn create_linear_gradient(x0: f32, y0: f32, x1: f32, y1: f32) -> u32 {
3794    let id = NEXT_GRADIENT_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3795    let grad = CanvasGradient::new_linear(x0, y0, x1, y1);
3796    let mut map = GRADIENT_MAP.lock();
3797    while map.len() >= MAX_GRADIENTS {
3798        if let Some(&oldest) = map.keys().next() {
3799            map.remove(&oldest);
3800        } else {
3801            break;
3802        }
3803    }
3804    map.insert(id, grad);
3805    id
3806}
3807
3808pub fn create_radial_gradient(x0: f32, y0: f32, r0: f32, x1: f32, y1: f32, r1: f32) -> u32 {
3809    let id = NEXT_GRADIENT_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3810    let grad = CanvasGradient::new_radial(x0, y0, r0, x1, y1, r1);
3811    let mut map = GRADIENT_MAP.lock();
3812    while map.len() >= MAX_GRADIENTS {
3813        if let Some(&oldest) = map.keys().next() {
3814            map.remove(&oldest);
3815        } else {
3816            break;
3817        }
3818    }
3819    map.insert(id, grad);
3820    id
3821}
3822
3823pub fn create_conic_gradient(start_angle: f32, cx: f32, cy: f32) -> u32 {
3824    let id = NEXT_GRADIENT_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3825    let grad = CanvasGradient::new_conic(start_angle, cx, cy);
3826    let mut map = GRADIENT_MAP.lock();
3827    while map.len() >= MAX_GRADIENTS {
3828        if let Some(&oldest) = map.keys().next() {
3829            map.remove(&oldest);
3830        } else {
3831            break;
3832        }
3833    }
3834    map.insert(id, grad);
3835    id
3836}
3837
3838pub fn add_color_stop_to_gradient(id: u32, offset: f32, color: u32) {
3839    let mut map = GRADIENT_MAP.lock();
3840    if let Some(grad) = map.get_mut(&id) {
3841        grad.add_color_stop(offset, color);
3842    }
3843}
3844
3845pub fn get_gradient(id: u32) -> Option<CanvasGradient> {
3846    let map = GRADIENT_MAP.lock();
3847    map.get(&id).cloned()
3848}
3849
3850
3851// ───────────── CanvasPattern 登録表 ─────────────
3852
3853static PATTERN_MAP: spin::Mutex<alloc::collections::BTreeMap<u32, CanvasPattern>> =
3854    spin::Mutex::new(alloc::collections::BTreeMap::new());
3855static NEXT_PATTERN_ID: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(1);
3856const MAX_PATTERNS: usize = 64;
3857
3858pub fn create_pattern(width: u32, height: u32, pixels: Vec<u32>, repetition: Repetition) -> u32 {
3859    let id = NEXT_PATTERN_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3860    let pat = CanvasPattern::new(width, height, pixels, repetition);
3861    let mut map = PATTERN_MAP.lock();
3862    while map.len() >= MAX_PATTERNS {
3863        if let Some(&oldest) = map.keys().next() {
3864            map.remove(&oldest);
3865        } else {
3866            break;
3867        }
3868    }
3869    map.insert(id, pat);
3870    id
3871}
3872
3873pub fn get_pattern(id: u32) -> Option<CanvasPattern> {
3874    let map = PATTERN_MAP.lock();
3875    map.get(&id).cloned()
3876}
3877
3878pub fn set_pattern_transform(id: u32, transform: Matrix) {
3879    let mut map = PATTERN_MAP.lock();
3880    if let Some(pat) = map.get_mut(&id) {
3881        pat.set_transform(transform);
3882    }
3883}