svg.bend

gralha-svg-render@0.1.0.0 · 5,871 lines · 0 definitions

1# SVG parsing, geometry, painting and rasterization. No foreign code.
2import Base
3import gralha-text-scan@0.1.0.0/scan.bend as U
4import gralha-xml-doc@0.1.0.0/xml.bend as XML
5import gralha-css-style@0.1.0.0/css.bend as CSS
6import gralha-bytes@0.1.0.0/bytes.bend as Bin
7import gralha-image@0.1.0.0/image.bend as Img
8import gralha-png-decode@0.1.0.0/png.bend as PNG
9import gralha-jpeg-decode@0.1.0.0/jpeg.bend as JPEG
10import gralha-coverage@0.1.0.0/cover.bend as Cov
11import gralha-truetype@0.1.0.0/font.bend as Font
12
13def length.go(s: String, acc: U32) -> U32:
14  match s:
15    case SNil{}: acc
16    case SCon{c, t}: length.go(t, U32.inc(acc))
17
18def length(s: String) -> U32:
19  length.go(s, 0)
20
21# Affine transforms, composed in SVG's column - vector order.
22type Matrix is Data:
23  Matrix{a: F32, b: F32, c: F32, d: F32, e: F32, f: F32}
24
25def identity() -> Matrix:
26  Matrix{1.0, 0.0, 0.0, 1.0, 0.0, 0.0}
27
28def matrix.mul(+m: Matrix, +n: Matrix) -> Matrix:
29  Matrix{a, b, c, d, e, f} = m
30  Matrix{g, h, i, j, k, l} = n
31  Matrix{(a * g + c * h : F32), (b * g + d * h : F32), (a * i + c * j : F32),
32    (b * i + d * j : F32), (a * k + c * l + e : F32), (b * k + d * l + f : F32)}
33
34def matrix.inverse.values(+m: Matrix, +det: F32) -> Matrix:
35  Matrix{a, b, c, d, e, f} = m
36  Matrix{(d / det : F32), (0.0 - b / det : F32), (0.0 - c / det : F32),
37    (a / det : F32), ((c * f - d * e) / det : F32), ((b * e - a * f) / det : F32)}
38
39def matrix.inverse(+m: Matrix) -> Matrix:
40  Matrix{a, b, c, d, e, f} = m
41  matrix.inverse.values(m, (a * d - b * c : F32))
42
43type Point is Data:
44  Point{x: F32, y: F32}
45
46def matrix.point(+m: Matrix, +p: Point) -> Point:
47  Matrix{a, b, c, d, e, f} = m
48  Point{x, y} = p
49  Point{(a * x + c * y + e : F32), (b * x + d * y + f : F32)}
50
51def translate(x: F32, y: F32) -> Matrix:
52  Matrix{1.0, 0.0, 0.0, 1.0, x, y}
53
54def scale(x: F32, y: F32) -> Matrix:
55  Matrix{x, 0.0, 0.0, y, 0.0, 0.0}
56
57def rotate(+angle: F32) -> Matrix:
58  +r = (angle * 0.017453292519943295 : F32)
59  Matrix{F32.cos(r), F32.sin(r), F32.neg(F32.sin(r)), F32.cos(r), 0.0, 0.0}
60
61def transform.one(+name: String, +v: List<&2, F32>) -> Matrix:
62  +x = U.nth(v, 0n, 0.0)
63  +y = U.nth(v, 1n, 0.0)
64  +z = U.nth(v, 2n, 0.0)
65  U.branch(Matrix, U.string.equal(name, "translate"), _ => translate(x, y),
66    _ => U.branch(Matrix, U.string.equal(name, "scale"), _ => scale(x, U.nth(v, 1n, x)),
67      _ => U.branch(Matrix, U.string.equal(name, "rotate"),
68        _ => matrix.mul(translate(y, z), matrix.mul(rotate(x), translate(F32.neg(y), F32.neg(z)))),
69        _ => U.branch(Matrix, U.string.equal(name, "matrix"),
70          _ => Matrix{x, y, z, U.nth(v, 3n, 1.0), U.nth(v, 4n, 0.0), U.nth(v, 5n, 0.0)},
71          _ => U.branch(Matrix, U.string.equal(name, "skewX"),
72            _ => Matrix{1.0, 0.0, F32.tan((x * 0.017453292519943295 : F32)), 1.0, 0.0, 0.0},
73            _ => U.branch(Matrix, U.string.equal(name, "skewY"),
74              _ => Matrix{1.0, F32.tan((x * 0.017453292519943295 : F32)), 0.0, 1.0, 0.0, 0.0},
75              _ => identity()))))))
76
77def transform.args(n: String, s: U.Scan, m: Matrix, next: String -> Matrix -> Matrix) -> Matrix:
78  U.Scan{h, t} = s
79  next(t)(matrix.mul(m, transform.one(n, U.numbers(h))))
80
81def transform.name(s: U.Scan, m: Matrix, next: String -> Matrix -> Matrix) -> Matrix:
82  U.Scan{h, t} = s
83  transform.args(h, U.until(t, 41, ""), m, next)
84
85@unsafe
86def transform(+s: String, +m: Matrix) -> Matrix:
87  U.branch(Matrix, U.string.equal(U.trim(s), ""), _ => m,
88    _ => transform.name(U.until(U.separators(s), 40, ""), m, t => n => transform(t, n)))
89
90# Paths are flattened in user coordinates. The fill-only closing edge of an
91# open subpath is kept separate from its stroked edges.
92# An edge is one heap node: its endpoints are stored as four numbers, so
93# building, walking and freeing edge lists touch half the nodes a pair of
94# points would.
95type Edge is Data:
96  Edge{x0: F32, y0: F32, x1: F32, y1: F32, stroke: Bool, closed: Bool}
97
98def edge.of(a: Point, b: Point, stroke: Bool, closed: Bool) -> Edge:
99  Point{x0, y0} = a
100  Point{x1, y1} = b
101  Edge{x0, y0, x1, y1, stroke, closed}
102
103# ---- analytic coverage ----
104# A cover is a pixel-space coverage mask (see cover.bend) together with the raster
105# grid it was built for, so device points map straight to its cells.
106type Cover is Data:
107  Cover{mask: Cov.CovMask, origin: Point, step: F32}
108
109def cover.none() -> Cover:
110  Cover{Cov.mask.none(), Point{0.0, 0.0}, 1.0}
111
112def cover.present(+c: Cover) -> Bool:
113  Cover{mask, origin, step} = c
114  Cov.mask.present(mask)
115
116def cover.at(+c: Cover, +p: Point) -> F32:
117  Cover{mask, Point{ox, oy}, step} = c
118  Point{x, y} = p
119  Cov.mask.at(mask, ((x - ox) / step : F32), ((y - oy) / step : F32))
120
121def point.add(p: Point, q: Point) -> Point:
122  Point{x, y} = p
123  Point{u, v} = q
124  Point{(x + u : F32), (y + v : F32)}
125
126def point.sub(p: Point, q: Point) -> Point:
127  Point{x, y} = p
128  Point{u, v} = q
129  Point{(x - u : F32), (y - v : F32)}
130
131def point.scale(p: Point, +s: F32) -> Point:
132  Point{x, y} = p
133  Point{(x * s : F32), (y * s : F32)}
134
135def point.dot(p: Point, q: Point) -> F32:
136  Point{x, y} = p
137  Point{u, v} = q
138  (x * u + y * v : F32)
139
140def point.mid(p: Point, q: Point) -> Point:
141  point.scale(point.add(p, q), 0.5)
142
143def point.distance(+p: Point, +q: Point) -> F32:
144  +d = point.sub(p, q)
145  F32.sqrt(point.dot(d, d))
146
147# Curves are flattened to a tolerance measured in device pixels (converted to
148# local units through the element's matrix by the caller), so a curve gets as
149# many segments as the output size needs and no more.
150def flat.tolerance() -> F32:
151  0.03
152
153# The largest scale the matrix applies to any direction, bounded by its
154# Frobenius norm; local tolerance = pixel tolerance / scale.
155def matrix.norm(+m: Matrix) -> F32:
156  Matrix{a, b, c, d, e, f} = m
157  U.max(0.000001, F32.sqrt((a * a + b * b + c * c + d * d : F32)))
158
159# scale is the device pixels per unit of the frame m maps into (1 when m
160# maps to the device, the tile's density for pattern content).
161def local.tolerance(+m: Matrix, +scale: F32) -> F32:
162  (flat.tolerance() / (scale * matrix.norm(m)) : F32)
163
164def chord.cross(p: Point, q: Point) -> F32:
165  Point{x, y} = p
166  Point{u, v} = q
167  (x * v - y * u : F32)
168
169# Whether x lies within tol of the chord pq: at most tol from its line and
170# not beyond either end by more than tol.
171def chord.near(+p: Point, +q: Point, +x: Point, +tol: F32) -> Bool:
172  +d = point.sub(q, p)
173  +len = F32.sqrt(point.dot(d, d))
174  +off = point.sub(x, p)
175  U.branch(Bool, F32.is_lt(len, 0.000001), _ => F32.is_le(point.distance(p, x), tol),
176    _ => F32.is_le(F32.abs(chord.cross(d, off)), (tol * len : F32))
177      && F32.is_ge(point.dot(d, off), (F32.neg(tol) * len : F32))
178      && F32.is_le(point.dot(d, off), ((len + tol) * len : F32)))
179
180# A cubic's distance from its chord is 3t(1-t) times a mix of its control
181# points' distances, at most three quarters of the larger, so the curve is
182# within tol when both control points are within 4/3 tol.
183def cubic.flat(+p: Point, +a: Point, +b: Point, +q: Point, +tol: F32) -> Bool:
184  +reach = (tol * 1.3333334 : F32)
185  chord.near(p, q, a, reach) && chord.near(p, q, b, reach)
186
187# Wang's bound: a cubic cut into n equal parameter steps stays within e of
188# its chords when n >= sqrt(3/4 * L / e), L the larger second difference of
189# the control points. The bound assumes the curvature all sits in one place,
190# so the tolerance is doubled to land near the adaptive count; the count is
191# fixed up front and each point costs one evaluation and no flatness test.
192def cubic.count(+p: Point, +a: Point, +b: Point, +q: Point, +tol: F32) -> U32:
193  +l = U.max(point.distance(point.add(p, b), point.scale(a, 2.0)), point.distance(point.add(a, q), point.scale(b, 2.0)))
194  F32.to_u32(U.clamp(F32.ceil(F32.sqrt((0.375 * l / U.max(tol, 0.000001) : F32))), 1.0, 1024.0))
195
196def cubic.at(+p: Point, +a: Point, +b: Point, +q: Point, +t: F32) -> Point:
197  +u = (1.0 - t : F32)
198  point.add(point.add(point.scale(p, (u * u * u : F32)), point.scale(a, (3.0 * u * u * t : F32))),
199    point.add(point.scale(b, (3.0 * u * t * t : F32)), point.scale(q, (t * t * t : F32))))
200
201@unsafe
202def cubic.go(+n: Nat, +i: F32, +step: F32, +p: Point, +a: Point, +b: Point, +q: Point, +prev: Point, +edges: List<&2, Edge>) -> List<&2, Edge>:
203  match n:
204    case 0n: edges
205    case 1n+d:
206      +next = U.choose(Point, U32.is_zero(U32.from_nat(d)), q, cubic.at(p, a, b, q, (i * step : F32)))
207      cubic.go(d, (i + 1.0 : F32), step, p, a, b, q, next, Con{edge.of(prev, next, True{}, False{}), edges})
208
209def cubic(+p: Point, +a: Point, +b: Point, +q: Point, +edges: List<&2, Edge>, +tol: F32) -> List<&2, Edge>:
210  +n = cubic.count(p, a, b, q, tol)
211  cubic.go(U32.to_nat(n), 1.0, (1.0 / U32.to_f32(n) : F32), p, a, b, q, p, edges)
212
213# What follows a command's numbers, scanned one step ahead: `kind` is 0 at
214# the end of the data, 1 at a letter, 2 at a number repeating the pending
215# command, 3 at anything else.
216type PathNext is Data:
217  PathNext{kind: U32, ch: U32, rest: String}
218
219type Params is Data:
220  Params{valid: Bool, values: List<&2, F32>, next: PathNext}
221
222def path.letter(+ch: U32) -> Bool:
223  (U32.is_ge(ch, 65) && U32.is_le(ch, 90)) || (U32.is_ge(ch, 97) && U32.is_le(ch, 122))
224
225def path.next(s: String, +cmd: U32) -> PathNext:
226  match s:
227    case SNil{}: PathNext{0, 0, ""}
228    case SCon{Chr{c}, t}:
229      +cc = c
230      PathNext{U.choose(U32, path.letter(cc), 1, U.choose(U32, U32.is_ne(cmd, 0), 2, 3)), cc, t}
231
232def floats.rev(xs: List<&2, F32>, acc: List<&2, F32>) -> List<&2, F32>:
233  match xs:
234    case Nil{}: acc
235    case Con{h, t}: floats.rev(t, Con{h, acc})
236
237def arc.flag(+s: String) -> U.Number:
238  match s:
239    case SNil{}: U.Number{0.0, "", False{}}
240    case SCon{Chr{c}, t}: U.Number{U.choose(F32, U32.is_eq(c, 49), 1.0, 0.0), t, U32.is_eq(c, 48) || U32.is_eq(c, 49)}
241
242def args.number.pick(flag: Bool, s: String) -> U.Number:
243  match flag:
244    case True{}: arc.flag(U.separators(s))
245    case False{}: U.number(s)
246
247# The arguments are counted down; an arc's flags are the 4th and 3rd from its end.
248def args.number(+n: Nat, s: String, +arc: Bool) -> U.Number:
249  args.number.pick(arc && (U32.is_eq(U32.from_nat(n), 4) || U32.is_eq(U32.from_nat(n), 3)), s)
250
251# The numbers of one command, gathered in a loop; `n` counts those still to
252# read after `num`. An invalid number ends the whole parse.
253@unsafe
254def path.args(num: U.Number, +n: Nat, acc: List<&2, F32>, +arc: Bool, +cmd: U32) -> Params:
255  match num:
256    case U.Number{x, t, valid}:
257      match valid:
258        case False{}: Params{False{}, Nil{}, PathNext{0, 0, t}}
259        case True{}:
260          match n:
261            case 0n: Params{True{}, floats.rev(Con{x, acc}, Nil{}), path.next(U.separators(t), cmd)}
262            case 1n+d: path.args(args.number(1n+d, t, arc), d, Con{x, acc}, arc, cmd)
263
264def path.arguments(+n: Nat, s: String, +arc: Bool, +cmd: U32) -> Params:
265  match n:
266    case 0n: Params{True{}, Nil{}, path.next(U.separators(s), cmd)}
267    case 1n+d: path.args(args.number(1n+d, s, arc), d, Nil{}, arc, cmd)
268
269# Preserve source segment endpoints and analytic tangents only when requested.
270type MarkerSegment is Data:
271  MarkerSegment{p: Point, q: Point, outgoing: Point, incoming: Point, kind: U32}
272
273def marker.segment(trace: Maybe<&2, List<&2, MarkerSegment>>, segment: MarkerSegment) -> Maybe<&2, List<&2, MarkerSegment>>:
274  match trace:
275    case None{}: None{}
276    case Some{segments}: Some{Con{segment, segments}}
277
278def marker.nonzero(+v: Point) -> Bool:
279  F32.is_gt(point.dot(v, v), 0.000000000001)
280
281def marker.vector(+a: Point, b: Point) -> Point:
282  U.choose(Point, marker.nonzero(a), a, b)
283
284def marker.cubic(+p: Point, +a: Point, +b: Point, +q: Point) -> MarkerSegment:
285  MarkerSegment{p, q, marker.vector(point.sub(a, p), marker.vector(point.sub(b, p), point.sub(q, p))),
286    marker.vector(point.sub(q, b), marker.vector(point.sub(q, a), point.sub(q, p))), 0}
287
288def marker.line(+p: Point, +q: Point, kind: U32) -> MarkerSegment:
289  MarkerSegment{p, q, point.sub(q, p), point.sub(q, p), kind}
290
291def marker.arc.tangent(+rx: F32, +ry: F32, +phi: F32, +theta: F32, +delta: F32) -> Point:
292  +direction = U.choose(F32, F32.is_lt(delta, 0.0), F32.neg(1.0), 1.0)
293  Point{((F32.neg(rx) * F32.cos(phi) * F32.sin(theta) - ry * F32.sin(phi) * F32.cos(theta)) * direction : F32),
294    ((F32.neg(rx) * F32.sin(phi) * F32.sin(theta) + ry * F32.cos(phi) * F32.cos(theta)) * direction : F32)}
295
296type Path is Data:
297  Path{current: Point, start: Point, control: Point, command: U32, previous: U32, edges: List<&2, Edge>, trace: Maybe<&2, List<&2, MarkerSegment>>, tolerance: F32}
298
299def upper(+c: U32) -> U32:
300  U.choose(U32, U32.is_ge(c, 97) && U32.is_le(c, 122), (c - 32 : U32), c)
301
302def relative(+cmd: U32, +current: Point, p: Point) -> Point:
303  point.add(U.choose(Point, U32.is_ge(cmd, 97), current, Point{0.0, 0.0}), p)
304
305def path.move(+p: Path, +q: Point, cmd: U32) -> Path:
306  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
307  Path{q, q, q, U.choose(U32, U32.is_ge(cmd, 97), 108, 76), 77,
308    U.choose(List<&2, Edge>, U32.is_eq(prev, 0), es, Con{edge.of(cur, start, False{}, False{}), es}), marker.segment(trace, marker.line(q, q, 1)), tol}
309
310def path.line(+p: Path, +q: Point, +cmd: U32) -> Path:
311  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
312  Path{q, start, q, cmd, upper(cmd), Con{edge.of(cur, q, True{}, False{}), es}, marker.segment(trace, marker.line(cur, q, 0)), tol}
313
314def path.cubic(+p: Path, +a: Point, +b: Point, +q: Point, +cmd: U32) -> Path:
315  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
316  +tt = tol
317  Path{q, start, b, cmd, upper(cmd), cubic(cur, a, b, q, es, tt), marker.segment(trace, marker.cubic(cur, a, b, q)), tt}
318
319def path.quad(+p: Path, +a: Point, +q: Point, +cmd: U32) -> Path:
320  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
321  ca = point.add(cur, point.scale(point.sub(a, cur), 0.6666666666666666))
322  cb = point.add(q, point.scale(point.sub(a, q), 0.6666666666666666))
323  +tt = tol
324  Path{q, start, a, cmd, upper(cmd), cubic(cur, ca, cb, q, es, tt), marker.segment(trace, marker.cubic(cur, a, a, q)), tt}
325
326def path.close(+p: Path) -> Path:
327  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
328  Path{start, start, start, 0, 90, Con{edge.of(cur, start, True{}, True{}), es}, marker.segment(trace, marker.line(cur, start, 2)), tol}
329
330def path.h(+cur: Point, +v: F32, cmd: U32) -> Point:
331  Point{x, y} = cur
332  Point{U.choose(F32, U32.is_ge(cmd, 97), (x + v : F32), v), y}
333
334def path.v(+cur: Point, +v: F32, cmd: U32) -> Point:
335  Point{x, y} = cur
336  Point{x, U.choose(F32, U32.is_ge(cmd, 97), (y + v : F32), v)}
337
338# Endpoint-to-center conversion from SVG's elliptical arc implementation notes.
339def arc.point(+center: Point, +rx: F32, +ry: F32, +phi: F32, +t: F32) -> Point:
340  point.add(center, Point{(rx * F32.cos(phi) * F32.cos(t) - ry * F32.sin(phi) * F32.sin(t) : F32),
341    (rx * F32.sin(phi) * F32.cos(t) + ry * F32.cos(phi) * F32.sin(t) : F32)})
342
343def arc.edges(+n: Nat, +i: F32, +step: F32, +theta: F32, +center: Point,
344  +rx: F32, +ry: F32, +phi: F32, +endpoint: Point, p: Point, edges: List<&2, Edge>) -> List<&2, Edge>:
345  match n:
346    case 0n: edges
347    case 1n+d:
348      +q = U.choose(Point, U32.is_zero(U32.from_nat(d)), endpoint, arc.point(center, rx, ry, phi, (theta + i * step : F32)))
349      arc.edges(d, (i + 1.0 : F32), step, theta, center, rx, ry, phi, endpoint, q, Con{edge.of(p, q, True{}, False{}), edges})
350
351def arc.draw(+p: Path, +q: Point, +cmd: U32, +rx: F32, +ry: F32, +phi: F32,
352  +center: Point, +theta: F32, +delta: F32) -> Path:
353  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
354  # A chord of angle step on the larger radius stays within the tolerance:
355  # step = 2 acos(1 - tol / r), approximated by 2 sqrt(2 tol / r).
356  +tt = tol
357  +step = U.max(0.001, (2.0 * F32.sqrt((2.0 * U.max(tt, 0.000001) / U.max(rx, ry) : F32)) : F32))
358  +count = U.max(1.0, F32.ceil((F32.abs(delta) / step : F32)))
359  Path{q, start, q, cmd, 65, arc.edges(U32.to_nat(F32.to_u32(U.min(4096.0, count))),
360    1.0, (delta / U.min(4096.0, count) : F32), theta, center, rx, ry, phi, q, cur, es), marker.segment(trace, MarkerSegment{cur, q, marker.arc.tangent(rx, ry, phi, theta, delta), marker.arc.tangent(rx, ry, phi, (theta + delta : F32), delta), 0}), tt}
361
362def arc.angles(+p: Path, +q: Point, +cmd: U32, +rx: F32, +ry: F32, +phi: F32,
363  +xp: F32, +yp: F32, +cx: F32, +cy: F32, +center: Point, sweep: F32) -> Path:
364  +ux = ((xp - cx) / rx : F32)
365  +uy = ((yp - cy) / ry : F32)
366  +vx = ((0.0 - xp - cx) / rx : F32)
367  +vy = ((0.0 - yp - cy) / ry : F32)
368  +delta = F32.atan2((ux * vy - uy * vx : F32), (ux * vx + uy * vy : F32))
369  +sw = F32.is_gt(sweep, 0.0)
370  arc.draw(p, q, cmd, rx, ry, phi, center, F32.atan2(uy, ux),
371    U.choose(F32, sw && F32.is_lt(delta, 0.0), (delta + 6.283185307179586 : F32),
372      U.choose(F32, Bool.not(sw) && F32.is_gt(delta, 0.0), (delta - 6.283185307179586 : F32), delta)))
373
374def arc.center(+p: Path, +q: Point, +cmd: U32, +rx: F32, +ry: F32, +phi: F32,
375  +xp: F32, +yp: F32, +coef: F32, sweep: F32) -> Path:
376  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
377  +cx = (coef * rx * yp / ry : F32)
378  +cy = (0.0 - coef * ry * xp / rx : F32)
379  center = point.add(point.mid(cur, q), Point{(F32.cos(phi) * cx - F32.sin(phi) * cy : F32),
380    (F32.sin(phi) * cx + F32.cos(phi) * cy : F32)})
381  arc.angles(p, q, cmd, rx, ry, phi, xp, yp, cx, cy, center, sweep)
382
383def arc.radii(+p: Path, +q: Point, +cmd: U32, +rx: F32, +ry: F32, +phi: F32,
384  +xp: F32, +yp: F32, +large: F32, +sweep: F32) -> Path:
385  +ratio = U.max(1.0, F32.sqrt((xp * xp / (rx * rx) + yp * yp / (ry * ry) : F32)))
386  +r = (rx * ratio : F32)
387  +s = (ry * ratio : F32)
388  +den = (r * r * yp * yp + s * s * xp * xp : F32)
389  +factor = F32.sqrt(U.max(0.0, ((r * r * s * s - den) / den : F32)))
390  arc.center(p, q, cmd, r, s, phi, xp, yp,
391    U.choose(F32, F32.is_eq(large, sweep), F32.neg(factor), factor), sweep)
392
393def arc.prepare(+p: Path, +q: Point, cmd: U32, rx: F32, ry: F32, +phi: F32,
394  large: F32, sweep: F32, delta: Point) -> Path:
395  Point{x, y} = delta
396  +dx = x
397  +dy = y
398  arc.radii(p, q, cmd, rx, ry, phi, (F32.cos(phi) * dx + F32.sin(phi) * dy : F32),
399    (0.0 - F32.sin(phi) * dx + F32.cos(phi) * dy : F32), large, sweep)
400
401def path.arc(+p: Path, +q: Point, +cmd: U32, +rx: F32, +ry: F32, phi: F32,
402  large: F32, sweep: F32) -> Path:
403  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
404  U.branch(Path, F32.is_lt(point.distance(cur, q), 0.000001), _ => Path{q, start, q, cmd, 65, es, trace, tol},
405    _ => U.branch(Path, F32.is_eq(rx, 0.0) || F32.is_eq(ry, 0.0), _ => path.line(p, q, cmd),
406      _ => arc.prepare(p, q, cmd, F32.abs(rx), F32.abs(ry), (phi * 0.017453292519943295 : F32),
407        large, sweep, point.scale(point.sub(cur, q), 0.5))))
408
409# The command's numbers are read by pattern, exactly as many as it takes;
410# the parser has already counted them.
411def path.cursor(p: Path) -> Point:
412  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
413  cur
414
415# The reflected control point of a smooth curve after a curve of the same
416# kind (commands a or b), else the current point.
417def path.smooth(p: Path, +a: U32, +b: U32) -> Point:
418  Path{cur, start, ctrl, old, prev, es, trace, tol} = p
419  +c = cur
420  +pv = prev
421  U.choose(Point, U32.is_eq(pv, a) || U32.is_eq(pv, b), point.sub(point.scale(c, 2.0), ctrl), c)
422
423def path.apply(+p: Path, +cmd: U32, c: U32, vs: List<&2, F32>) -> Path:
424  match c:
425    case 77:
426      match vs:
427        case Con{x, Con{y, r}}:
428          path.move(p, relative(cmd, path.cursor(p), Point{x, y}), cmd)
429        case _: p
430    case 76:
431      match vs:
432        case Con{x, Con{y, r}}:
433          path.line(p, relative(cmd, path.cursor(p), Point{x, y}), cmd)
434        case _: p
435    case 72:
436      match vs:
437        case Con{x, r}:
438          path.line(p, path.h(path.cursor(p), x, cmd), cmd)
439        case _: p
440    case 86:
441      match vs:
442        case Con{y, r}:
443          path.line(p, path.v(path.cursor(p), y, cmd), cmd)
444        case _: p
445    case 67:
446      match vs:
447        case Con{x1, Con{y1, Con{x2, Con{y2, Con{x, Con{y, r}}}}}}:
448          path.cubic(p, relative(cmd, path.cursor(p), Point{x1, y1}), relative(cmd, path.cursor(p), Point{x2, y2}), relative(cmd, path.cursor(p), Point{x, y}), cmd)
449        case _: p
450    case 83:
451      match vs:
452        case Con{x2, Con{y2, Con{x, Con{y, r}}}}:
453          path.cubic(p, path.smooth(p, 67, 83), relative(cmd, path.cursor(p), Point{x2, y2}), relative(cmd, path.cursor(p), Point{x, y}), cmd)
454        case _: p
455    case 81:
456      match vs:
457        case Con{x1, Con{y1, Con{x, Con{y, r}}}}:
458          path.quad(p, relative(cmd, path.cursor(p), Point{x1, y1}), relative(cmd, path.cursor(p), Point{x, y}), cmd)
459        case _: p
460    case 84:
461      match vs:
462        case Con{x, Con{y, r}}:
463          path.quad(p, path.smooth(p, 81, 84), relative(cmd, path.cursor(p), Point{x, y}), cmd)
464        case _: p
465    case 65:
466      match vs:
467        case Con{rx, Con{ry, Con{rot, Con{large, Con{sweep, Con{x, Con{y, r}}}}}}}:
468          path.arc(p, relative(cmd, path.cursor(p), Point{x, y}), cmd, rx, ry, rot, large, sweep)
469        case _: p
470    case _: p
471
472def path.arity(+c: U32) -> Nat:
473  U.choose(Nat, U32.is_eq(c, 90), 0n, U.choose(Nat, U32.is_eq(c, 72) || U32.is_eq(c, 86), 1n,
474    U.choose(Nat, U32.is_eq(c, 67), 6n,
475      U.choose(Nat, U32.is_eq(c, 83) || U32.is_eq(c, 81), 4n,
476        U.choose(Nat, U32.is_eq(c, 65), 7n, 2n)))))
477
478# A command applied to the path: 0 nothing (before the first command),
479# 1 a close, 2 a drawing command with its numbers.
480def path.step(kind: U32, p: Path, +cmd: U32, vs: List<&2, F32>) -> Path:
481  match kind:
482    case 0: p
483    case 1: path.close(p)
484    case _: path.apply(p, cmd, upper(cmd), vs)
485
486# The command a bare number after `cmd` repeats: a move continues as a
487# line, a close as nothing (the parse ends), any other command as itself.
488def path.follow(+cmd: U32) -> U32:
489  U.choose(U32, U32.is_eq(cmd, 77), 76, U.choose(U32, U32.is_eq(cmd, 109), 108, U.choose(U32, U32.is_eq(upper(cmd), 90), 0, cmd)))
490
491def path.step.kind(+cmd: U32) -> U32:
492  U.choose(U32, U32.is_eq(cmd, 0), 0, U.choose(U32, U32.is_eq(upper(cmd), 90), 1, 2))
493
494# The commands in turn: each step applies the pending command with the
495# numbers just read, then reads the next command's numbers. A non-letter
496# repeats the pending command (SVG's implicit repetition); with none pending
497# it ends the parse.
498@unsafe
499def path.run(ps: Params, +cmd: U32, p: Path) -> Path:
500  match ps:
501    case Params{valid, vs, nx}:
502      match valid:
503        case False{}: p
504        case True{}:
505          match nx:
506            case PathNext{kind, ch, rest}:
507              match kind:
508                case 1:
509                  +c2 = ch
510                  path.run(path.arguments(path.arity(upper(c2)), rest, U32.is_eq(upper(c2), 65), path.follow(c2)), c2, path.step(path.step.kind(cmd), p, cmd, vs))
511                case 2:
512                  +pending = path.follow(cmd)
513                  path.run(path.arguments(path.arity(upper(pending)), SCon{Chr{ch}, rest}, U32.is_eq(upper(pending), 65), path.follow(pending)), pending, path.step(path.step.kind(cmd), p, cmd, vs))
514                case _: path.step(path.step.kind(cmd), p, cmd, vs)
515
516def path.loop(s: String, p: Path) -> Path:
517  path.run(path.arguments(0n, s, False{}, 0), 0, p)
518
519def path.finish(+p: Path) -> List<&2, Edge>:
520  Path{cur, start, ctrl, cmd, prev, es, trace, tol} = p
521  U.choose(List<&2, Edge>, U32.is_eq(prev, 0), Nil{}, Con{edge.of(cur, start, False{}, False{}), es})
522
523def path(s: String, +tol: F32) -> List<&2, Edge>:
524  path.finish(path.loop(s, Path{Point{0.0, 0.0}, Point{0.0, 0.0}, Point{0.0, 0.0}, 0, 0, Nil{}, None{}, tol}))
525
526# Geometry queries use half-open crossings, supporting both SVG fill rules.
527def edge.cross(+edge: Edge, +p: Point) -> F32:
528  Edge{x1, y1, x2, y2, stroked, closed} = edge
529  Point{x, y} = p
530  U.branch(F32, (F32.is_le(y1, y) && F32.is_gt(y2, y)) || (F32.is_le(y2, y) && F32.is_gt(y1, y)),
531    _ => U.choose(F32, F32.is_gt((x1 + (y - y1) * (x2 - x1) / (y2 - y1) : F32), x),
532      U.choose(F32, F32.is_gt(y2, y1), 1.0, F32.neg(1.0)), 0.0), _ => 0.0)
533
534def winding(edges: List<&2, Edge>, +p: Point) -> F32:
535  match edges:
536    case Nil{}: 0.0
537    case Con{h, t}: (edge.cross(h, p) + winding(t, p) : F32)
538
539def fill.inside(edges: List<&2, Edge>, p: Point, even: Bool) -> Bool:
540  +w = winding(edges, p)
541  F32.is_gt(F32.abs(U.choose(F32, even, (w % 2.0 : F32), w)), 0.5)
542
543def edge.distance(+e: Edge, +p: Point) -> F32:
544  Edge{ax, ay, bx, by, stroked, closed} = e
545  +a = {Point{ax, ay} : Point}
546  +ab = point.sub(Point{bx, by}, a)
547  +den = point.dot(ab, ab)
548  +t = U.clamp((point.dot(point.sub(p, a), ab) / U.max(0.000000000001, den) : F32), 0.0, 1.0)
549  U.choose(F32, stroked, point.distance(p, point.add(a, point.scale(ab, t))), 100000000000000000000.0)
550
551def stroke.inside(edges: List<&2, Edge>, +p: Point, +radius: F32) -> Bool:
552  match edges:
553    case Nil{}: False{}
554    case Con{h, t}:
555      U.branch(Bool, F32.is_le(edge.distance(h, p), radius), _ => True{},
556        _ => stroke.inside(t, p, radius))
557
558type Bounds is Data:
559  Bounds{x0: F32, y0: F32, x1: F32, y1: F32}
560
561def bounds.point(+b: Bounds, +p: Point) -> Bounds:
562  Bounds{x0, y0, x1, y1} = b
563  Point{x, y} = p
564  Bounds{U.min(x0, x), U.min(y0, y), U.max(x1, x), U.max(y1, y)}
565
566def bounds.at(+b: Bounds, +x: F32, +y: F32) -> Bounds:
567  Bounds{x0, y0, x1, y1} = b
568  Bounds{U.min(x0, x), U.min(y0, y), U.max(x1, x), U.max(y1, y)}
569
570def bounds(edges: List<&2, Edge>, box: Bounds) -> Bounds:
571  match edges:
572    case Nil{}: box
573    case Con{Edge{ax, ay, bx, by, stroked, closed}, t}: bounds(t, bounds.at(bounds.at(box, ax, ay), bx, by))
574
575def bounds.inside(b: Bounds, +p: Point, +margin: F32) -> Bool:
576  Bounds{x0, y0, x1, y1} = b
577  Point{x, y} = p
578  F32.is_ge(x, (x0 - margin : F32)) && F32.is_ge(y, (y0 - margin : F32))
579    && F32.is_le(x, (x1 + margin : F32)) && F32.is_le(y, (y1 + margin : F32))
580
581def inflate(+data: Bin.Bytes, limit: U32) -> Bin.Inflate:
582  Bin.inflate.blocks(Bin.BitStream{data, 0, (Bin.bytes.size(data) * 8 : U32), True{}}, Bin.bytes.empty(), limit)
583
584def color.fade(c: Img.Color, +opacity: F32) -> Img.Color:
585  Img.Color{r, g, b, a} = c
586  Img.Color{(r * opacity : F32), (g * opacity : F32), (b * opacity : F32), (a * opacity : F32)}
587
588def color.over(+front: Img.Color, back: Img.Color) -> Img.Color:
589  Img.Color{r, g, b, a} = front
590  Img.Color{x, y, z, w} = back
591  Img.Color{(r + x * (1.0 - a) : F32), (g + y * (1.0 - a) : F32),
592    (b + z * (1.0 - a) : F32), (a + w * (1.0 - a) : F32)}
593
594def color.add(c: Img.Color, d: Img.Color) -> Img.Color:
595  Img.Color{r, g, b, a} = c
596  Img.Color{x, y, z, w} = d
597  Img.Color{(r + x : F32), (g + y : F32), (b + z : F32), (a + w : F32)}
598
599def byte(+x: U32, n: U32) -> F32:
600  (U32.to_f32(U32.mod(U32.div(x, n), 256)) / 255.0 : F32)
601
602type Raster is Data:
603  Raster{origin: Point, step: F32, width: U32, height: U32, depth: Nat, linear: Bool, pixels: Img.Pixels}
604
605def color.rgb(+x: U32) -> Img.Color:
606  Img.rgba(byte(x, 65536), byte(x, 256), byte(x, 1), 1.0)
607
608def nibble(+x: U32, n: U32) -> F32:
609  (U32.to_f32(U32.mod(U32.div(x, n), 16)) / 15.0 : F32)
610
611def color.hex.short(+x: U32, len: U32) -> Img.Color:
612  U.choose(Img.Color, U32.is_eq(len, 4), Img.rgba(nibble(x, 4096), nibble(x, 256), nibble(x, 16), nibble(x, 1)),
613    Img.rgba(nibble(x, 256), nibble(x, 16), nibble(x, 1), 1.0))
614
615def color.hex(+x: U32, +len: U32) -> Img.Color:
616  U.branch(Img.Color, U32.is_eq(len, 3) || U32.is_eq(len, 4),
617    _ => color.hex.short(x, len),
618    _ => U.choose(Img.Color, U32.is_eq(len, 8), Img.rgba(byte(x, 16777216), byte(x, 65536), byte(x, 256), byte(x, 1)), color.rgb(x)))
619
620def color.named.a(+s: String) -> U32:
621  U.branch(U32, U.string.equal(s, "aliceblue"), _ => 15792383, _ => U.branch(U32, U.string.equal(s, "antiquewhite"), _ => 16444375, _ => U.branch(U32, U.string.equal(s, "aqua"), _ => 65535, _ => U.branch(U32, U.string.equal(s, "aquamarine"), _ => 8388564, _ => U.branch(U32, U.string.equal(s, "azure"), _ => 15794175, _ => 16777216)))))
622
623def color.named.b(+s: String) -> U32:
624  U.branch(U32, U.string.equal(s, "beige"), _ => 16119260, _ => U.branch(U32, U.string.equal(s, "bisque"), _ => 16770244, _ => U.branch(U32, U.string.equal(s, "black"), _ => 0, _ => U.branch(U32, U.string.equal(s, "blanchedalmond"), _ => 16772045, _ => U.branch(U32, U.string.equal(s, "blue"), _ => 255, _ => U.branch(U32, U.string.equal(s, "blueviolet"), _ => 9055202, _ => U.branch(U32, U.string.equal(s, "brown"), _ => 10824234, _ => U.branch(U32, U.string.equal(s, "burlywood"), _ => 14596231, _ => 16777216))))))))
625
626def color.named.c(+s: String) -> U32:
627  U.branch(U32, U.string.equal(s, "cadetblue"), _ => 6266528, _ => U.branch(U32, U.string.equal(s, "chartreuse"), _ => 8388352, _ => U.branch(U32, U.string.equal(s, "chocolate"), _ => 13789470, _ => U.branch(U32, U.string.equal(s, "coral"), _ => 16744272, _ => U.branch(U32, U.string.equal(s, "cornflowerblue"), _ => 6591981, _ => U.branch(U32, U.string.equal(s, "cornsilk"), _ => 16775388, _ => U.branch(U32, U.string.equal(s, "crimson"), _ => 14423100, _ => U.branch(U32, U.string.equal(s, "cyan"), _ => 65535, _ => 16777216))))))))
628
629def color.named.d(+s: String) -> U32:
630  U.branch(U32, U.string.equal(s, "darkblue"), _ => 139, _ => U.branch(U32, U.string.equal(s, "darkcyan"), _ => 35723, _ => U.branch(U32, U.string.equal(s, "darkgoldenrod"), _ => 12092939, _ => U.branch(U32, U.string.equal(s, "darkgray"), _ => 11119017, _ => U.branch(U32, U.string.equal(s, "darkgreen"), _ => 25600, _ => U.branch(U32, U.string.equal(s, "darkgrey"), _ => 11119017, _ => U.branch(U32, U.string.equal(s, "darkkhaki"), _ => 12433259, _ => U.branch(U32, U.string.equal(s, "darkmagenta"), _ => 9109643, _ => U.branch(U32, U.string.equal(s, "darkolivegreen"), _ => 5597999, _ => U.branch(U32, U.string.equal(s, "darkorange"), _ => 16747520, _ => U.branch(U32, U.string.equal(s, "darkorchid"), _ => 10040012, _ => U.branch(U32, U.string.equal(s, "darkred"), _ => 9109504, _ => U.branch(U32, U.string.equal(s, "darksalmon"), _ => 15308410, _ => U.branch(U32, U.string.equal(s, "darkseagreen"), _ => 9419919, _ => U.branch(U32, U.string.equal(s, "darkslateblue"), _ => 4734347, _ => U.branch(U32, U.string.equal(s, "darkslategray"), _ => 3100495, _ => U.branch(U32, U.string.equal(s, "darkslategrey"), _ => 3100495, _ => U.branch(U32, U.string.equal(s, "darkturquoise"), _ => 52945, _ => U.branch(U32, U.string.equal(s, "darkviolet"), _ => 9699539, _ => U.branch(U32, U.string.equal(s, "deeppink"), _ => 16716947, _ => U.branch(U32, U.string.equal(s, "deepskyblue"), _ => 49151, _ => U.branch(U32, U.string.equal(s, "dimgray"), _ => 6908265, _ => U.branch(U32, U.string.equal(s, "dimgrey"), _ => 6908265, _ => U.branch(U32, U.string.equal(s, "dodgerblue"), _ => 2003199, _ => 16777216))))))))))))))))))))))))
631
632def color.named.f(+s: String) -> U32:
633  U.branch(U32, U.string.equal(s, "firebrick"), _ => 11674146, _ => U.branch(U32, U.string.equal(s, "floralwhite"), _ => 16775920, _ => U.branch(U32, U.string.equal(s, "forestgreen"), _ => 2263842, _ => U.branch(U32, U.string.equal(s, "fuchsia"), _ => 16711935, _ => 16777216))))
634
635def color.named.g(+s: String) -> U32:
636  U.branch(U32, U.string.equal(s, "gainsboro"), _ => 14474460, _ => U.branch(U32, U.string.equal(s, "ghostwhite"), _ => 16316671, _ => U.branch(U32, U.string.equal(s, "gold"), _ => 16766720, _ => U.branch(U32, U.string.equal(s, "goldenrod"), _ => 14329120, _ => U.branch(U32, U.string.equal(s, "gray"), _ => 8421504, _ => U.branch(U32, U.string.equal(s, "green"), _ => 32768, _ => U.branch(U32, U.string.equal(s, "greenyellow"), _ => 11403055, _ => U.branch(U32, U.string.equal(s, "grey"), _ => 8421504, _ => 16777216))))))))
637
638def color.named.h(+s: String) -> U32:
639  U.branch(U32, U.string.equal(s, "honeydew"), _ => 15794160, _ => U.branch(U32, U.string.equal(s, "hotpink"), _ => 16738740, _ => 16777216))
640
641def color.named.i(+s: String) -> U32:
642  U.branch(U32, U.string.equal(s, "indianred"), _ => 13458524, _ => U.branch(U32, U.string.equal(s, "indigo"), _ => 4915330, _ => U.branch(U32, U.string.equal(s, "ivory"), _ => 16777200, _ => 16777216)))
643
644def color.named.k(+s: String) -> U32:
645  U.branch(U32, U.string.equal(s, "khaki"), _ => 15787660, _ => 16777216)
646
647def color.named.l(+s: String) -> U32:
648  U.branch(U32, U.string.equal(s, "lavender"), _ => 15132410, _ => U.branch(U32, U.string.equal(s, "lavenderblush"), _ => 16773365, _ => U.branch(U32, U.string.equal(s, "lawngreen"), _ => 8190976, _ => U.branch(U32, U.string.equal(s, "lemonchiffon"), _ => 16775885, _ => U.branch(U32, U.string.equal(s, "lightblue"), _ => 11393254, _ => U.branch(U32, U.string.equal(s, "lightcoral"), _ => 15761536, _ => U.branch(U32, U.string.equal(s, "lightcyan"), _ => 14745599, _ => U.branch(U32, U.string.equal(s, "lightgoldenrodyellow"), _ => 16448210, _ => U.branch(U32, U.string.equal(s, "lightgray"), _ => 13882323, _ => U.branch(U32, U.string.equal(s, "lightgreen"), _ => 9498256, _ => U.branch(U32, U.string.equal(s, "lightgrey"), _ => 13882323, _ => U.branch(U32, U.string.equal(s, "lightpink"), _ => 16758465, _ => U.branch(U32, U.string.equal(s, "lightsalmon"), _ => 16752762, _ => U.branch(U32, U.string.equal(s, "lightseagreen"), _ => 2142890, _ => U.branch(U32, U.string.equal(s, "lightskyblue"), _ => 8900346, _ => U.branch(U32, U.string.equal(s, "lightslategray"), _ => 7833753, _ => U.branch(U32, U.string.equal(s, "lightslategrey"), _ => 7833753, _ => U.branch(U32, U.string.equal(s, "lightsteelblue"), _ => 11584734, _ => U.branch(U32, U.string.equal(s, "lightyellow"), _ => 16777184, _ => U.branch(U32, U.string.equal(s, "lime"), _ => 65280, _ => U.branch(U32, U.string.equal(s, "limegreen"), _ => 3329330, _ => U.branch(U32, U.string.equal(s, "linen"), _ => 16445670, _ => 16777216))))))))))))))))))))))
649
650def color.named.m(+s: String) -> U32:
651  U.branch(U32, U.string.equal(s, "magenta"), _ => 16711935, _ => U.branch(U32, U.string.equal(s, "maroon"), _ => 8388608, _ => U.branch(U32, U.string.equal(s, "mediumaquamarine"), _ => 6737322, _ => U.branch(U32, U.string.equal(s, "mediumblue"), _ => 205, _ => U.branch(U32, U.string.equal(s, "mediumorchid"), _ => 12211667, _ => U.branch(U32, U.string.equal(s, "mediumpurple"), _ => 9662683, _ => U.branch(U32, U.string.equal(s, "mediumseagreen"), _ => 3978097, _ => U.branch(U32, U.string.equal(s, "mediumslateblue"), _ => 8087790, _ => U.branch(U32, U.string.equal(s, "mediumspringgreen"), _ => 64154, _ => U.branch(U32, U.string.equal(s, "mediumturquoise"), _ => 4772300, _ => U.branch(U32, U.string.equal(s, "mediumvioletred"), _ => 13047173, _ => U.branch(U32, U.string.equal(s, "midnightblue"), _ => 1644912, _ => U.branch(U32, U.string.equal(s, "mintcream"), _ => 16121850, _ => U.branch(U32, U.string.equal(s, "mistyrose"), _ => 16770273, _ => U.branch(U32, U.string.equal(s, "moccasin"), _ => 16770229, _ => 16777216)))))))))))))))
652
653def color.named.n(+s: String) -> U32:
654  U.branch(U32, U.string.equal(s, "navajowhite"), _ => 16768685, _ => U.branch(U32, U.string.equal(s, "navy"), _ => 128, _ => 16777216))
655
656def color.named.o(+s: String) -> U32:
657  U.branch(U32, U.string.equal(s, "oldlace"), _ => 16643558, _ => U.branch(U32, U.string.equal(s, "olive"), _ => 8421376, _ => U.branch(U32, U.string.equal(s, "olivedrab"), _ => 7048739, _ => U.branch(U32, U.string.equal(s, "orange"), _ => 16753920, _ => U.branch(U32, U.string.equal(s, "orangered"), _ => 16729344, _ => U.branch(U32, U.string.equal(s, "orchid"), _ => 14315734, _ => 16777216))))))
658
659def color.named.p(+s: String) -> U32:
660  U.branch(U32, U.string.equal(s, "palegoldenrod"), _ => 15657130, _ => U.branch(U32, U.string.equal(s, "palegreen"), _ => 10025880, _ => U.branch(U32, U.string.equal(s, "paleturquoise"), _ => 11529966, _ => U.branch(U32, U.string.equal(s, "palevioletred"), _ => 14381203, _ => U.branch(U32, U.string.equal(s, "papayawhip"), _ => 16773077, _ => U.branch(U32, U.string.equal(s, "peachpuff"), _ => 16767673, _ => U.branch(U32, U.string.equal(s, "peru"), _ => 13468991, _ => U.branch(U32, U.string.equal(s, "pink"), _ => 16761035, _ => U.branch(U32, U.string.equal(s, "plum"), _ => 14524637, _ => U.branch(U32, U.string.equal(s, "powderblue"), _ => 11591910, _ => U.branch(U32, U.string.equal(s, "purple"), _ => 8388736, _ => 16777216)))))))))))
661
662def color.named.r(+s: String) -> U32:
663  U.branch(U32, U.string.equal(s, "rebeccapurple"), _ => 6697881, _ => U.branch(U32, U.string.equal(s, "red"), _ => 16711680, _ => U.branch(U32, U.string.equal(s, "rosybrown"), _ => 12357519, _ => U.branch(U32, U.string.equal(s, "royalblue"), _ => 4286945, _ => 16777216))))
664
665def color.named.s(+s: String) -> U32:
666  U.branch(U32, U.string.equal(s, "saddlebrown"), _ => 9127187, _ => U.branch(U32, U.string.equal(s, "salmon"), _ => 16416882, _ => U.branch(U32, U.string.equal(s, "sandybrown"), _ => 16032864, _ => U.branch(U32, U.string.equal(s, "seagreen"), _ => 3050327, _ => U.branch(U32, U.string.equal(s, "seashell"), _ => 16774638, _ => U.branch(U32, U.string.equal(s, "sienna"), _ => 10506797, _ => U.branch(U32, U.string.equal(s, "silver"), _ => 12632256, _ => U.branch(U32, U.string.equal(s, "skyblue"), _ => 8900331, _ => U.branch(U32, U.string.equal(s, "slateblue"), _ => 6970061, _ => U.branch(U32, U.string.equal(s, "slategray"), _ => 7372944, _ => U.branch(U32, U.string.equal(s, "slategrey"), _ => 7372944, _ => U.branch(U32, U.string.equal(s, "snow"), _ => 16775930, _ => U.branch(U32, U.string.equal(s, "springgreen"), _ => 65407, _ => U.branch(U32, U.string.equal(s, "steelblue"), _ => 4620980, _ => 16777216))))))))))))))
667
668def color.named.t(+s: String) -> U32:
669  U.branch(U32, U.string.equal(s, "tan"), _ => 13808780, _ => U.branch(U32, U.string.equal(s, "teal"), _ => 32896, _ => U.branch(U32, U.string.equal(s, "thistle"), _ => 14204888, _ => U.branch(U32, U.string.equal(s, "tomato"), _ => 16737095, _ => U.branch(U32, U.string.equal(s, "turquoise"), _ => 4251856, _ => 16777216)))))
670
671def color.named.v(+s: String) -> U32:
672  U.branch(U32, U.string.equal(s, "violet"), _ => 15631086, _ => 16777216)
673
674def color.named.w(+s: String) -> U32:
675  U.branch(U32, U.string.equal(s, "wheat"), _ => 16113331, _ => U.branch(U32, U.string.equal(s, "white"), _ => 16777215, _ => U.branch(U32, U.string.equal(s, "whitesmoke"), _ => 16119285, _ => 16777216)))
676
677def color.named.y(+s: String) -> U32:
678  U.branch(U32, U.string.equal(s, "yellow"), _ => 16776960, _ => U.branch(U32, U.string.equal(s, "yellowgreen"), _ => 10145074, _ => 16777216))
679
680def color.named.code(+s: String) -> U32:
681  U.branch(U32, U.prefix(s, "a"), _ => color.named.a(s), _ => U.branch(U32, U.prefix(s, "b"), _ => color.named.b(s), _ => U.branch(U32, U.prefix(s, "c"), _ => color.named.c(s), _ => U.branch(U32, U.prefix(s, "d"), _ => color.named.d(s), _ => U.branch(U32, U.prefix(s, "f"), _ => color.named.f(s), _ => U.branch(U32, U.prefix(s, "g"), _ => color.named.g(s), _ => U.branch(U32, U.prefix(s, "h"), _ => color.named.h(s), _ => U.branch(U32, U.prefix(s, "i"), _ => color.named.i(s), _ => U.branch(U32, U.prefix(s, "k"), _ => color.named.k(s), _ => U.branch(U32, U.prefix(s, "l"), _ => color.named.l(s), _ => U.branch(U32, U.prefix(s, "m"), _ => color.named.m(s), _ => U.branch(U32, U.prefix(s, "n"), _ => color.named.n(s), _ => U.branch(U32, U.prefix(s, "o"), _ => color.named.o(s), _ => U.branch(U32, U.prefix(s, "p"), _ => color.named.p(s), _ => U.branch(U32, U.prefix(s, "r"), _ => color.named.r(s), _ => U.branch(U32, U.prefix(s, "s"), _ => color.named.s(s), _ => U.branch(U32, U.prefix(s, "t"), _ => color.named.t(s), _ => U.branch(U32, U.prefix(s, "v"), _ => color.named.v(s), _ => U.branch(U32, U.prefix(s, "w"), _ => color.named.w(s), _ => U.branch(U32, U.prefix(s, "y"), _ => color.named.y(s), _ => 16777216))))))))))))))))))))
682
683def color.named(s: String) -> Img.Color:
684  +code = color.named.code(s)
685  color.rgb(U.choose(U32, U32.is_lt(code, 16777216), code, 0))
686
687# Img.Color arguments retain separators and units; unlike SVG coordinates, these
688# functions do not permit adjacent numbers or mixed comma/slash syntax.
689type ColorToken is Data:
690  ColorToken{value: F32, kind: U32, separator: U32}
691
692def color.word(+s: String, +acc: String) -> U.Scan:
693  match s:
694    case SNil{}: U.Scan{U.rev(acc, ""), ""}
695    case SCon{Chr{c}, tail}:
696      U.branch(U.Scan, U.space(c) || U32.is_eq(c, 44) || U32.is_eq(c, 47) || U32.is_eq(c, 41),
697        _ => U.Scan{U.rev(acc, ""), s}, _ => color.word(tail, SCon{Chr{c}, acc}))
698
699def color.token.number(num: U.Number, sep: U32) -> ColorToken:
700  U.Number{x, unitName, ok} = num
701  +u = unitName
702  ColorToken{x, U.choose(U32, Bool.not(ok), 7, U.choose(U32, U.string.equal(u, ""), 0, U.choose(U32, U.string.equal(u, "%"), 1,
703    U.choose(U32, U.string.equal(u, "deg"), 2, U.choose(U32, U.string.equal(u, "rad"), 3, U.choose(U32, U.string.equal(u, "grad"), 4,
704      U.choose(U32, U.string.equal(u, "turn"), 5, 7))))))), sep}
705
706def color.token(+word: String, +sep: U32) -> ColorToken:
707  U.branch(ColorToken, U.string.equal(word, "none"), _ => ColorToken{0.0, 6, sep}, _ => color.token.number(U.number(word), sep))
708
709def color.tokens.word(+scan: U.Scan, next: String -> List<&2, ColorToken>) -> List<&2, ColorToken>:
710  U.Scan{word, rest} = scan
711  +s = U.trim(rest)
712  +token = word
713  +comma = U.prefix(s, ",")
714  +slash = U.prefix(s, "/")
715  +end = U.prefix(s, ")") && U.string.equal(U.trim(U.drop(s, ")")), "")
716  +spaced = Bool.not(U.string.equal(rest, s))
717  +sep = U.choose(U32, end, 0, U.choose(U32, comma, 2, U.choose(U32, slash, 3, U.choose(U32, spaced, 1, 9))))
718  +tail = U.choose(String, comma, U.drop(s, ","), U.choose(String, slash, U.drop(s, "/"), s))
719  Con{color.token(token, sep), U.choose(Unit -> List<&2, ColorToken>, end || U32.is_eq(sep, 9), _ => Nil{}, _ => next(tail))(Unit{})}
720
721def color.tokens(fuel: Nat, s: String) -> List<&2, ColorToken>:
722  match fuel:
723    case 0n: Con{ColorToken{0.0, 7, 9}, Nil{}}
724    case 1n+n: color.tokens.word(color.word(U.trim(s), ""), rest => color.tokens(n, rest))
725
726def color.component(+kind: U32) -> Bool:
727  U32.is_eq(kind, 0) || U32.is_eq(kind, 1) || U32.is_eq(kind, 6)
728
729def color.hue(+x: F32, +kind: U32) -> F32:
730  +degrees = U.choose(F32, U32.is_eq(kind, 3), (x * 57.29577951308232 : F32), U.choose(F32, U32.is_eq(kind, 4), (x * 0.9 : F32), U.choose(F32, U32.is_eq(kind, 5), (x * 360.0 : F32), x)))
731  (degrees - 360.0 * F32.floor((degrees / 360.0 : F32)) : F32)
732
733def color.hsl.channel(+h: F32, +s: F32, +l: F32, n: F32) -> F32:
734  +turn = (n + h / 30.0 : F32)
735  +k = (turn - 12.0 * F32.floor((turn / 12.0 : F32)) : F32)
736  (l - s * U.min(l, (1.0 - l : F32)) * U.max(F32.neg(1.0), U.min(U.min((k - 3.0 : F32), (9.0 - k : F32)), 1.0)) : F32)
737
738def color.hsl(+h: F32, +s: F32, +l: F32, a: F32) -> Img.Color:
739  +sat = U.clamp(s, 0.0, 1.0)
740  +light = U.clamp(l, 0.0, 1.0)
741  Img.rgba(color.hsl.channel(h, sat, light, 0.0), color.hsl.channel(h, sat, light, 8.0), color.hsl.channel(h, sat, light, 4.0), a)
742
743def color.hwb.mix(c: Img.Color, +white: F32, +black: F32, alpha: F32) -> Img.Color:
744  Img.Color{r, g, b, a} = c
745  +remaining = (1.0 - white - black : F32)
746  Img.rgba((r * remaining + white : F32), (g * remaining + white : F32), (b * remaining + white : F32), alpha)
747
748def color.hwb(h: F32, +w: F32, +b: F32, +a: F32) -> Img.Color:
749  +white = U.clamp(w, 0.0, 1.0)
750  +black = U.clamp(b, 0.0, 1.0)
751  +sum = (white + black : F32)
752  +gray = (white / U.max(sum, 0.000001) : F32)
753  U.branch(Img.Color, F32.is_ge(sum, 1.0), _ => Img.rgba(gray, gray, gray, a), _ => color.hwb.mix(color.hsl(h, 1.0, 0.5, 1.0), white, black, a))
754
755def color.function.channels(+r: ColorToken, +g: ColorToken, +b: ColorToken, +a: ColorToken, +mode: U32, +alpha: Bool) -> Img.Color:
756  ColorToken{rr, kr, sr} = r
757  ColorToken{gg, kg, sg} = g
758  ColorToken{bb, kb, sb} = b
759  ColorToken{aa, ka, sa} = a
760  +legacy = U32.is_eq(sr, 2) && U32.is_eq(sg, 2) && U32.is_eq(sb, U.choose(U32, alpha, 2, 0)) && U32.is_eq(sa, 0)
761  +modern = U32.is_eq(sr, 1) && U32.is_eq(sg, 1) && U32.is_eq(sb, U.choose(U32, alpha, 3, 0)) && U32.is_eq(sa, 0)
762  +rgb = U32.is_eq(mode, 0)
763  +hue = U32.is_lt(kr, 7) && Bool.not(U32.is_eq(kr, 1))
764  +kinds = color.component(kg) && color.component(kb) && color.component(ka) && U.choose(Bool, rgb, color.component(kr), hue)
765  +oldKinds = U32.is_lt(ka, 6) && U.choose(Bool, rgb, U32.is_lt(kr, 2) && U32.is_eq(kr, kg) && U32.is_eq(kr, kb), U32.is_lt(kr, 6) && U32.is_eq(kg, 1) && U32.is_eq(kb, 1))
766  +valid = kinds && (modern || (legacy && oldKinds && Bool.not(U32.is_eq(mode, 2))))
767  +opacity = U.clamp(U.choose(F32, U32.is_eq(ka, 1), (aa / 100.0 : F32), aa), 0.0, 1.0)
768  U.branch(Img.Color, valid,
769    _ => U.branch(Img.Color, rgb, _ => Img.rgba(U.clamp((rr / U.choose(F32, U32.is_eq(kr, 1), 100.0, 255.0) : F32), 0.0, 1.0),
770      U.clamp((gg / U.choose(F32, U32.is_eq(kg, 1), 100.0, 255.0) : F32), 0.0, 1.0), U.clamp((bb / U.choose(F32, U32.is_eq(kb, 1), 100.0, 255.0) : F32), 0.0, 1.0), opacity),
771      _ => U.branch(Img.Color, U32.is_eq(mode, 1), _ => color.hsl(color.hue(rr, kr), (gg / 100.0 : F32), (bb / 100.0 : F32), opacity),
772        _ => color.hwb(color.hue(rr, kr), (gg / 100.0 : F32), (bb / 100.0 : F32), opacity))), _ => color.rgb(0))
773
774def color.function.args(xs: List<&2, ColorToken>, mode: U32) -> Img.Color:
775  match xs:
776    case Con{r, Con{g, Con{b, Nil{}}}}: color.function.channels(r, g, b, ColorToken{1.0, 0, 0}, mode, False{})
777    case Con{r, Con{g, Con{b, Con{a, Nil{}}}}}: color.function.channels(r, g, b, a, mode, True{})
778    case _: color.rgb(0)
779
780def color.hex.valid(s: String) -> Bool:
781  match s:
782    case SNil{}: True{}
783    case SCon{Chr{c}, rest}:
784      +ch = c
785      (U.digit(ch) || (U32.is_ge(ch, 97) && U32.is_le(ch, 102))) && color.hex.valid(rest)
786
787def color.hex.string(+s: String) -> Img.Color:
788  +n = length(s)
789  U.branch(Img.Color, (U32.is_eq(n, 3) || U32.is_eq(n, 4) || U32.is_eq(n, 6) || U32.is_eq(n, 8)) && color.hex.valid(s),
790    _ => color.hex(U.hex(s, 0), n), _ => color.rgb(0))
791
792def color.parse(+s: String) -> Img.Color:
793  U.branch(Img.Color, U.prefix(s, "#"), _ => color.hex.string(U.drop(s, "#")),
794    _ => U.branch(Img.Color, U.prefix(s, "rgb(") || U.prefix(s, "rgba("), _ => color.function.args(color.tokens(4n, U.drop(s, U.choose(String, U.prefix(s, "rgba("), "rgba(", "rgb("))), 0),
795    _ => U.branch(Img.Color, U.prefix(s, "hsl(") || U.prefix(s, "hsla("), _ => color.function.args(color.tokens(4n, U.drop(s, U.choose(String, U.prefix(s, "hsla("), "hsla(", "hsl("))), 1),
796    _ => U.branch(Img.Color, U.prefix(s, "hwb("), _ => color.function.args(color.tokens(4n, U.drop(s, "hwb(")), 2),
797    _ => U.branch(Img.Color, U.string.equal(s, "none") || U.string.equal(s, "transparent"), _ => Img.clear(), _ => color.named(s))))))
798
799# Hex digits after a '#', read in one pass: the value and count when the
800# rest is only whitespace, else a count of 0 so the general parser decides.
801def color.hex.digit(+ch: U32) -> Bool:
802  U.digit(ch) || (U32.is_ge(U32.or(ch, 32), 97) && U32.is_le(U32.or(ch, 32), 102))
803
804# One character at a time, decided one step ahead: `hexed` says whether `ch`
805# is a hex digit; the rest is scanned only while digits continue.
806@unsafe
807def color.hex.scan(hexed: Bool, +ch: U32, t: String, +acc: U32, +n: U32) -> U32 & U32:
808  match hexed:
809    case True{}:
810      match t:
811        case SNil{}: ((acc * 16 + U.hex.digit(ch) : U32), (n + 1 : U32))
812        case SCon{Chr{c}, t2}:
813          +c2 = c
814          color.hex.scan(color.hex.digit(c2), c2, t2, (acc * 16 + U.hex.digit(ch) : U32), (n + 1 : U32))
815    case False{}: U.choose(U32 & U32, U.space(ch) && U.string.equal(U.trim(t), ""), (acc, n), (0, 0))
816
817def color.hex.start(rest: String) -> U32 & U32:
818  match rest:
819    case SNil{}: (0, 0)
820    case SCon{Chr{c}, t}:
821      +c2 = c
822      color.hex.scan(color.hex.digit(c2), c2, t, 0, 0)
823
824def color.hex.fast(scanned: U32 & U32, s: String) -> Img.Color:
825  (x, n) = scanned
826  +len = n
827  U.branch(Img.Color, U32.is_eq(len, 3) || U32.is_eq(len, 4) || U32.is_eq(len, 6) || U32.is_eq(len, 8),
828    _ => color.hex(x, len), _ => color.parse(CSS.css.lower(U.trim.both(s))))
829
830def color.head(+s: String) -> Img.Color:
831  match s:
832    case SNil{}: color.parse("")
833    case SCon{Chr{c}, t}:
834      +ch = c
835      +rest = t
836      U.branch(Img.Color, U32.is_eq(ch, 35), _ => color.hex.fast(color.hex.start(rest), s),
837        _ => color.parse(CSS.css.lower(U.trim.both(s))))
838
839def color(s: String) -> Img.Color:
840  color.head(U.trim(s))
841
842def color.opacity.number(num: U.Number, fallback: F32) -> F32:
843  U.Number{x, tail, ok} = num
844  +u = U.trim.both(tail)
845  U.clamp(U.choose(F32, ok && (U.string.equal(u, "") || U.string.equal(u, "%")), (x / U.choose(F32, U.string.equal(u, "%"), 100.0, 1.0) : F32), fallback), 0.0, 1.0)
846
847def color.opacity(s: String, fallback: F32) -> F32:
848  color.opacity.number(U.number(s), fallback)
849
850def dimension(attrs: List<&2, XML.Attr>, key: String, ref: F32, fallback: String) -> F32:
851  U.unit(U.number(XML.attr.get(attrs, key, fallback)), ref)
852
853def xy(x: F32, y: F32) -> String:
854  F32.show(x) ++ "," ++ F32.show(y) ++ " "
855
856def shape.rect.path(+x: F32, +y: F32, +w: F32, +h: F32, +rx: F32, +ry: F32) -> String:
857  +a = xy(rx, ry) ++ "0 0 1 "
858  "M" ++ xy((x + rx : F32), y) ++ "H" ++ F32.show((x + w - rx : F32))
859    ++ "A" ++ a ++ xy((x + w : F32), (y + ry : F32)) ++ "V" ++ F32.show((y + h - ry : F32))
860    ++ "A" ++ a ++ xy((x + w - rx : F32), (y + h : F32)) ++ "H" ++ F32.show((x + rx : F32))
861    ++ "A" ++ a ++ xy(x, (y + h - ry : F32)) ++ "V" ++ F32.show((y + ry : F32))
862    ++ "A" ++ a ++ xy((x + rx : F32), y) ++ "Z"
863
864def shape.rect(+as: List<&2, XML.Attr>, +vw: F32, +vh: F32) -> String:
865  +w = dimension(as, "width", vw, "0")
866  +h = dimension(as, "height", vh, "0")
867  +rx = U.min((w / 2.0 : F32), U.max(0.0, dimension(as, "rx", vw, XML.attr.get(as, "ry", "0"))))
868  +ry = U.min((h / 2.0 : F32), U.max(0.0, dimension(as, "ry", vh, XML.attr.get(as, "rx", "0"))))
869  U.branch(String, F32.is_gt(w, 0.0) && F32.is_gt(h, 0.0),
870    _ => shape.rect.path(dimension(as, "x", vw, "0"), dimension(as, "y", vh, "0"), w, h, rx, ry), _ => "")
871
872def shape.ellipse.path(+x: F32, +y: F32, +rx: F32, +ry: F32) -> String:
873  U.branch(String, F32.is_gt(rx, 0.0) && F32.is_gt(ry, 0.0),
874    _ => "M" ++ xy((x + rx : F32), y) ++ "A" ++ xy(rx, ry) ++ "0 1 1 " ++ xy((x - rx : F32), y)
875      ++ "A" ++ xy(rx, ry) ++ "0 1 1 " ++ xy((x + rx : F32), y) ++ "Z", _ => "")
876
877def edges.transform(es: List<&2, Edge>, +m: Matrix) -> List<&2, Edge>:
878  match es:
879    case Nil{}: Nil{}
880    case Con{Edge{ax, ay, bx, by, stroked, closed}, rest}:
881      Con{edge.of(matrix.point(m, Point{ax, ay}), matrix.point(m, Point{bx, by}), stroked, closed), edges.transform(rest, m)}
882
883# The path data of a shape; a path's own `d` arrives separately (taken out
884# of its attributes) so the parser owns that text outright.
885def shape.data(+name: String, +as: List<&2, XML.Attr>, +vw: F32, +vh: F32, data: String) -> String:
886  U.branch(String, U.string.equal(name, "path"), _ => data,
887    _ => U.branch(String, U.string.equal(name, "rect") || U.string.equal(name, "image") || U.string.equal(name, "#svg-image"), _ => shape.rect(as, vw, vh),
888      _ => U.branch(String, U.string.equal(name, "circle"),
889        _ => shape.ellipse.path(dimension(as, "cx", vw, "0"), dimension(as, "cy", vh, "0"),
890          dimension(as, "r", F32.sqrt(((vw * vw + vh * vh) / 2.0 : F32)), "0"),
891          dimension(as, "r", F32.sqrt(((vw * vw + vh * vh) / 2.0 : F32)), "0")),
892        _ => U.branch(String, U.string.equal(name, "ellipse"),
893          _ => shape.ellipse.path(dimension(as, "cx", vw, "0"), dimension(as, "cy", vh, "0"),
894            dimension(as, "rx", vw, "0"), dimension(as, "ry", vh, "0")),
895          _ => U.branch(String, U.string.equal(name, "line"),
896            _ => "M" ++ xy(dimension(as, "x1", vw, "0"), dimension(as, "y1", vh, "0"))
897              ++ "L" ++ xy(dimension(as, "x2", vw, "0"), dimension(as, "y2", vh, "0")),
898            _ => U.branch(String, U.string.equal(name, "polygon") || U.string.equal(name, "polyline"),
899              _ => "M" ++ XML.attr.get(as, "points", "") ++ U.choose(String, U.string.equal(name, "polygon"), "Z", ""),
900              _ => ""))))))
901
902def glyph.path(+d: String, +gm: Matrix, +tol: F32) -> List<&2, Edge>:
903  edges.transform(path(d, (tol / matrix.norm(gm) : F32)), gm)
904
905def shape.of(glyph: Bool, data: String, +name: String, +as: List<&2, XML.Attr>, +vw: F32, +vh: F32, +tol: F32) -> List<&2, Edge>:
906  match glyph:
907    case True{}: glyph.path(data, transform(XML.attr.get(as, "#glyph-transform", ""), identity()), tol)
908    case False{}: path(shape.data(name, as, vw, vh, data), tol)
909
910def shape(+name: String, +as: List<&2, XML.Attr>, +vw: F32, +vh: F32, +tol: F32, data: String) -> List<&2, Edge>:
911  shape.of(U.string.equal(name, "#glyph"), data, name, as, vw, vh, tol)
912
913# Paint adjacent glyphs with one shared style as one geometry. This keeps all
914# fills together and all strokes together, including overlapping glyphs.
915def glyph.edges.append(xs: List<&2, Edge>, ys: List<&2, Edge>) -> List<&2, Edge>:
916  match xs:
917    case Nil{}: ys
918    case Con{h, t}: Con{h, glyph.edges.append(t, ys)}
919
920def glyph.edges(xs: List<&2, XML.Element>, +tol: F32) -> List<&2, Edge>:
921  match xs:
922    case Nil{}: Nil{}
923    case Con{XML.Element{n, as, kids, txt}, rest}:
924      +attrs = as
925      glyph.edges.append(glyph.path(XML.attr.get(attrs, "d", ""), transform(XML.attr.get(attrs, "#glyph-transform", ""), identity()), tol), glyph.edges(rest, tol))
926
927# The element's edges in local units, flattened for the local-to-device matrix m.
928def shape.children(+name: String, as: List<&2, XML.Attr>, kids: List<&2, XML.Element>, vw: F32, vh: F32, +tol: F32, data: String) -> List<&2, Edge>:
929  U.branch(List<&2, Edge>, U.string.equal(name, "#glyph-run"), _ => glyph.edges(kids, tol), _ => shape(name, as, vw, vh, tol, data))
930
931type MarkerSubpath is Data:
932  MarkerSubpath{start: Point, segments: List<&2, MarkerSegment>, closed: Bool}
933
934def marker.segments.reverse(xs: List<&2, MarkerSegment>, acc: List<&2, MarkerSegment>) -> List<&2, MarkerSegment>:
935  match xs:
936    case Nil{}: acc
937    case Con{h, t}: marker.segments.reverse(t, Con{h, acc})
938
939def marker.subpaths.reverse(xs: List<&2, MarkerSubpath>, acc: List<&2, MarkerSubpath>) -> List<&2, MarkerSubpath>:
940  match xs:
941    case Nil{}: acc
942    case Con{h, t}: marker.subpaths.reverse(t, Con{h, acc})
943
944def marker.subpaths(+xs: List<&2, MarkerSegment>, +start: Point, +acc: List<&2, MarkerSegment>, +have: Bool, +done: List<&2, MarkerSubpath>) -> List<&2, MarkerSubpath>:
945  match xs:
946    case Nil{}: marker.subpaths.reverse(U.choose(List<&2, MarkerSubpath>, have, Con{MarkerSubpath{start, marker.segments.reverse(acc, Nil{}), False{}}, done}, done), Nil{})
947    case Con{segment, rest}:
948      MarkerSegment{p, q, outgoing, incoming, kind} = segment
949      +seg = {MarkerSegment{p, q, outgoing, incoming, kind} : MarkerSegment}
950      U.branch(List<&2, MarkerSubpath>, U32.is_eq(kind, 1),
951        _ => marker.subpaths(rest, q, Nil{}, True{}, U.choose(List<&2, MarkerSubpath>, have, Con{MarkerSubpath{start, marker.segments.reverse(acc, Nil{}), False{}}, done}, done)),
952        _ => U.branch(List<&2, MarkerSubpath>, U32.is_eq(kind, 2),
953          _ => marker.subpaths(rest, q, Nil{}, False{}, Con{MarkerSubpath{U.choose(Point, have, start, p), marker.segments.reverse(Con{seg, acc}, Nil{}), True{}}, done}),
954          _ => marker.subpaths(rest, U.choose(Point, have, start, p), Con{seg, acc}, True{}, done)))
955
956def marker.first(xs: List<&2, MarkerSegment>, fallback: Point) -> Point:
957  match xs:
958    case Nil{}: fallback
959    case Con{MarkerSegment{p, q, outgoing, incoming, kind}, rest}:
960      +v = outgoing
961      U.branch(Point, marker.nonzero(v), _ => v, _ => marker.first(rest, fallback))
962
963def marker.last(xs: List<&2, MarkerSegment>, fallback: Point) -> Point:
964  match xs:
965    case Nil{}: fallback
966    case Con{MarkerSegment{p, q, outgoing, incoming, kind}, rest}:
967      marker.last(rest, marker.vector(incoming, fallback))
968
969def marker.angle.vector(v: Point) -> F32:
970  Point{x, y} = v
971  (F32.atan2(y, x) * 57.29577951308232 : F32)
972
973def marker.angle.mean(+a: F32, +b: F32) -> F32:
974  +wrap = F32.is_gt(F32.abs((a - b : F32)), 180.0)
975  ((a + U.choose(F32, wrap && F32.is_lt(a, b), 360.0, 0.0) + b + U.choose(F32, wrap && F32.is_lt(b, a), 360.0, 0.0)) / 2.0 : F32)
976
977def marker.direction(+incoming: Point, +outgoing: Point) -> F32:
978  marker.angle.mean(marker.angle.vector(marker.vector(incoming, outgoing)), marker.angle.vector(marker.vector(outgoing, incoming)))
979
980type MarkerVertex is Data:
981  MarkerVertex{point: Point, angle: F32}
982
983@unsafe
984def marker.vertices.tail(xs: List<&2, MarkerSegment>, previous: Point, +first: Point, +endAngle: F32) -> List<&2, MarkerVertex>:
985  match xs:
986    case Nil{}: Nil{}
987    case Con{MarkerSegment{p, q, outgoing, incoming, kind}, Nil{}}: Con{MarkerVertex{q, endAngle}, Nil{}}
988    case Con{MarkerSegment{p, q, outgoing, incoming, kind}, Con{head, tail}}:
989      +before = marker.vector(incoming, previous)
990      +remaining = {Con{head, tail} : List<&2, MarkerSegment>}
991      Con{MarkerVertex{q, marker.direction(before, marker.first(remaining, first))}, marker.vertices.tail(remaining, before, first, endAngle)}
992
993def marker.vertices.one(+subpath: MarkerSubpath) -> List<&2, MarkerVertex>:
994  MarkerSubpath{start, segments, closed} = subpath
995  +zero = {Point{0.0, 0.0} : Point}
996  +outgoing = marker.first(segments, zero)
997  +incoming = marker.last(segments, zero)
998  +joined = marker.direction(incoming, outgoing)
999  +startAngle = U.choose(F32, closed, joined, marker.angle.vector(outgoing))
1000  +endAngle = U.choose(F32, closed, joined, marker.angle.vector(incoming))
1001  Con{MarkerVertex{start, startAngle}, marker.vertices.tail(segments, U.choose(Point, closed, incoming, zero), U.choose(Point, closed, outgoing, zero), endAngle)}
1002
1003def marker.vertices.append(xs: List<&2, MarkerVertex>, ys: List<&2, MarkerVertex>) -> List<&2, MarkerVertex>:
1004  match xs:
1005    case Nil{}: ys
1006    case Con{h, t}: Con{h, marker.vertices.append(t, ys)}
1007
1008def marker.vertices(xs: List<&2, MarkerSubpath>) -> List<&2, MarkerVertex>:
1009  match xs:
1010    case Nil{}: Nil{}
1011    case Con{h, t}: marker.vertices.append(marker.vertices.one(h), marker.vertices(t))
1012
1013def marker.trace.result(trace: Maybe<&2, List<&2, MarkerSegment>>) -> List<&2, MarkerVertex>:
1014  match trace:
1015    case None{}: Nil{}
1016    case Some{segments}: marker.vertices(marker.subpaths(marker.segments.reverse(segments, Nil{}), Point{0.0, 0.0}, Nil{}, False{}, Nil{}))
1017
1018def marker.trace.path(p: Path) -> List<&2, MarkerVertex>:
1019  Path{cur, start, ctrl, cmd, prev, es, trace, tol} = p
1020  marker.trace.result(trace)
1021
1022def marker.trace(s: String) -> List<&2, MarkerVertex>:
1023  marker.trace.path(path.loop(s, Path{Point{0.0, 0.0}, Point{0.0, 0.0}, Point{0.0, 0.0}, 0, 0, Nil{}, Some{Nil{}}, 1.0}))
1024
1025# Stroke geometry is compiled once. Overlapping parts form a union, so alpha
1026# is applied once even at joins, caps and self intersections.
1027type StrokeSettings is Data:
1028  StrokeSettings{cap: String, join: String, limit: F32, dash: String, offset: String, vector: String}
1029
1030def stroke.settings() -> StrokeSettings:
1031  StrokeSettings{"butt", "miter", 4.0, "none", "0", "none"}
1032
1033# A property from an element's attributes: absent means the inherited value
1034# (or the initial one for non-inherited properties) with no string work at
1035# all; present, the CSS-wide keywords are honoured.
1036# The CSS-wide keywords of a value: 1 inherit, 2 initial, 3 unset, 0 none.
1037# Decided from the first letter on, case-insensitively, with blanks around
1038# allowed, without lowercasing or trimming a copy of every value.
1039def keyword.blank(s: String) -> Bool:
1040  match s:
1041    case SNil{}: True{}
1042    case SCon{Chr{c}, t}: U.space(c) && keyword.blank(t)
1043
1044def keyword.rest(p: String, s: String) -> Bool:
1045  match p:
1046    case SNil{}: keyword.blank(s)
1047    case SCon{Chr{pc}, pt}:
1048      match s:
1049        case SNil{}: False{}
1050        case SCon{Chr{c}, t}: U32.is_eq(U32.or(c, 32), pc) && keyword.rest(pt, t)
1051
1052def keyword.pick(+s: String) -> U32:
1053  U.choose(U32, keyword.rest("inherit", s), 1, U.choose(U32, keyword.rest("initial", s), 2, U.choose(U32, keyword.rest("unset", s), 3, 0)))
1054
1055def keyword.head(s: String) -> U32:
1056  match s:
1057    case SNil{}: 0
1058    case SCon{Chr{c}, t}:
1059      +ch = c
1060      U.choose(U32, U32.is_eq(U32.or(ch, 32), 105) || U32.is_eq(U32.or(ch, 32), 117), keyword.pick(SCon{Chr{ch}, t}), 0)
1061
1062def style.keyword(s: String) -> U32:
1063  keyword.head(U.trim(s))
1064
1065def style.present(found: Maybe<&2, String>, +fallback: String, +initial: String, +inherited: Bool) -> String:
1066  match found:
1067    case None{}: U.choose(String, inherited, fallback, initial)
1068    case Some{v}:
1069      +vv = v
1070      +k = style.keyword(vv)
1071      U.choose(String, U32.is_eq(k, 1) || (inherited && U32.is_eq(k, 3)), fallback,
1072        U.choose(String, U32.is_eq(k, 2) || U32.is_eq(k, 3), initial, vv))
1073
1074def style.value(+as: List<&2, XML.Attr>, key: String, +fallback: String, +initial: String, +inherited: Bool) -> String:
1075  style.present(XML.attr.find(as, key), fallback, initial, inherited)
1076
1077# A numeric inherited property: the parent's number stays a number when the
1078# attribute is absent instead of being printed and parsed again.
1079def style.number.present(found: Maybe<&2, String>, +parent: F32, +initial: F32, parse: String -> F32) -> F32:
1080  match found:
1081    case None{}: parent
1082    case Some{v}:
1083      +vv = v
1084      +k = style.keyword(vv)
1085      U.choose(F32, U32.is_eq(k, 1) || U32.is_eq(k, 3), parent,
1086        U.choose(F32, U32.is_eq(k, 2), initial, parse(vv)))
1087
1088def style.number(+as: List<&2, XML.Attr>, key: String, +parent: F32, +initial: F32, parse: String -> F32) -> F32:
1089  style.number.present(XML.attr.find(as, key), parent, initial, parse)
1090
1091# The style properties an element's own attributes may set, picked up in
1092# one pass over the attribute list (the first occurrence of a key counts,
1093# as a lookup by name would find it), so inheritance reads them without a
1094# walk per property.
1095type Picked is Data:
1096  Picked{fill: Maybe<&2, String>, stroke: Maybe<&2, String>, color: Maybe<&2, String>, fo: Maybe<&2, String>, so: Maybe<&2, String>,
1097    width: Maybe<&2, String>, rule: Maybe<&2, String>, vis: Maybe<&2, String>, render: Maybe<&2, String>, order: Maybe<&2, String>,
1098    cap: Maybe<&2, String>, join: Maybe<&2, String>, limit: Maybe<&2, String>, dash: Maybe<&2, String>, offset: Maybe<&2, String>,
1099    vector: Maybe<&2, String>, ms: Maybe<&2, String>, mm: Maybe<&2, String>, me: Maybe<&2, String>}
1100
1101def pick.none() -> Picked:
1102  Picked{None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}}
1103
1104def pick.first(old: Maybe<&2, String>, v: String) -> Maybe<&2, String>:
1105  match old:
1106    case None{}: Some{v}
1107    case Some{x}: Some{x}
1108
1109# Which property a key names, 0 for none, by its first letter then the name.
1110def pick.slot.f(+k: String) -> U32:
1111  U.choose(U32, U.string.equal(k, "fill"), 1, U.choose(U32, U.string.equal(k, "fill-opacity"), 4, U.choose(U32, U.string.equal(k, "fill-rule"), 7, 0)))
1112
1113def pick.slot.s(+k: String) -> U32:
1114  U.choose(U32, U.string.equal(k, "stroke"), 2, U.choose(U32, U.string.equal(k, "stroke-opacity"), 5, U.choose(U32, U.string.equal(k, "stroke-width"), 6,
1115    U.choose(U32, U.string.equal(k, "stroke-linecap"), 11, U.choose(U32, U.string.equal(k, "stroke-linejoin"), 12, U.choose(U32, U.string.equal(k, "stroke-miterlimit"), 13,
1116      U.choose(U32, U.string.equal(k, "stroke-dasharray"), 14, U.choose(U32, U.string.equal(k, "stroke-dashoffset"), 15, 0))))))))
1117
1118def pick.slot.m(+k: String) -> U32:
1119  U.choose(U32, U.string.equal(k, "marker-start"), 17, U.choose(U32, U.string.equal(k, "marker-mid"), 18, U.choose(U32, U.string.equal(k, "marker-end"), 19, 0)))
1120
1121def pick.slot.other(+k: String, +c: U32) -> U32:
1122  U.choose(U32, U32.is_eq(c, 99) && U.string.equal(k, "color"), 3,
1123    U.choose(U32, U32.is_eq(c, 118) && U.string.equal(k, "visibility"), 8, U.choose(U32, U32.is_eq(c, 118) && U.string.equal(k, "vector-effect"), 16,
1124      U.choose(U32, U32.is_eq(c, 105) && U.string.equal(k, "image-rendering"), 9, U.choose(U32, U32.is_eq(c, 112) && U.string.equal(k, "paint-order"), 10, 0)))))
1125
1126def pick.slot(k: String) -> U32:
1127  match k:
1128    case SNil{}: 0
1129    case SCon{Chr{c}, t}:
1130      +ch = c
1131      +key = {SCon{Chr{ch}, t} : String}
1132      U.choose(U32, U32.is_eq(ch, 102), pick.slot.f(key), U.choose(U32, U32.is_eq(ch, 115), pick.slot.s(key), U.choose(U32, U32.is_eq(ch, 109), pick.slot.m(key), pick.slot.other(key, ch))))
1133
1134def pick.put(p: Picked, +slot: U32, v: String) -> Picked:
1135  Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19} = p
1136  match slot:
1137    case 1: Picked{pick.first(q1, v), q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1138    case 2: Picked{q1, pick.first(q2, v), q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1139    case 3: Picked{q1, q2, pick.first(q3, v), q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1140    case 4: Picked{q1, q2, q3, pick.first(q4, v), q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1141    case 5: Picked{q1, q2, q3, q4, pick.first(q5, v), q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1142    case 6: Picked{q1, q2, q3, q4, q5, pick.first(q6, v), q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1143    case 7: Picked{q1, q2, q3, q4, q5, q6, pick.first(q7, v), q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1144    case 8: Picked{q1, q2, q3, q4, q5, q6, q7, pick.first(q8, v), q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1145    case 9: Picked{q1, q2, q3, q4, q5, q6, q7, q8, pick.first(q9, v), q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1146    case 10: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, pick.first(q10, v), q11, q12, q13, q14, q15, q16, q17, q18, q19}
1147    case 11: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, pick.first(q11, v), q12, q13, q14, q15, q16, q17, q18, q19}
1148    case 12: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, pick.first(q12, v), q13, q14, q15, q16, q17, q18, q19}
1149    case 13: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, pick.first(q13, v), q14, q15, q16, q17, q18, q19}
1150    case 14: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, pick.first(q14, v), q15, q16, q17, q18, q19}
1151    case 15: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, pick.first(q15, v), q16, q17, q18, q19}
1152    case 16: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, pick.first(q16, v), q17, q18, q19}
1153    case 17: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, pick.first(q17, v), q18, q19}
1154    case 18: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, pick.first(q18, v), q19}
1155    case 19: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, pick.first(q19, v)}
1156    case _: Picked{q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13, q14, q15, q16, q17, q18, q19}
1157
1158def style.pick(as: List<&2, XML.Attr>, p: Picked) -> Picked:
1159  match as:
1160    case Nil{}: p
1161    case Con{XML.Attr{k, v}, rest}: style.pick(rest, pick.put(p, pick.slot(k), v))
1162
1163def stroke.inherit(s: StrokeSettings, cap: Maybe<&2, String>, join: Maybe<&2, String>, lim: Maybe<&2, String>, dash: Maybe<&2, String>, offset: Maybe<&2, String>, vector: Maybe<&2, String>) -> StrokeSettings:
1164  StrokeSettings{cap0, join0, limit, dash0, offset0, vector0} = s
1165  +lim0 = limit
1166  StrokeSettings{style.present(cap, cap0, "butt", True{}), style.present(join, join0, "miter", True{}),
1167    U.max(0.0, style.number.present(lim, lim0, 4.0, v => U.scalar(v, lim0))),
1168    style.present(dash, dash0, "none", True{}), style.present(offset, offset0, "0", True{}),
1169    style.present(vector, vector0, "none", False{})}
1170
1171def point.cross(p: Point, q: Point) -> F32:
1172  Point{x, y} = p
1173  Point{u, v} = q
1174  (x * v - y * u : F32)
1175
1176def point.normal(p: Point) -> Point:
1177  Point{x, y} = p
1178  Point{F32.neg(y), x}
1179
1180def point.unit(+p: Point) -> Point:
1181  point.scale(p, (1.0 / U.max(0.0000001, F32.sqrt(point.dot(p, p))) : F32))
1182
1183type Subpath is Data:
1184  Subpath{points: List<&2, Point>, closed: Bool}
1185
1186def points.rev(xs: List<&2, Point>, acc: List<&2, Point>) -> List<&2, Point>:
1187  match xs:
1188    case Nil{}: acc
1189    case Con{h, t}: points.rev(t, Con{h, acc})
1190
1191# Repeated points dropped, each decided a step ahead: `near` says whether
1192# h coincides with the point kept before it.
1193@unsafe
1194def points.clean.go(near: Bool, +h: Point, t: List<&2, Point>, +previous: Point) -> List<&2, Point>:
1195  match near:
1196    case True{}:
1197      match t:
1198        case Nil{}: Nil{}
1199        case Con{h2, t2}:
1200          +hh = h2
1201          points.clean.go(F32.is_lt(point.distance(hh, previous), 0.0000001), hh, t2, previous)
1202    case False{}:
1203      match t:
1204        case Nil{}: Con{h, Nil{}}
1205        case Con{h2, t2}:
1206          +hh = h2
1207          Con{h, points.clean.go(F32.is_lt(point.distance(hh, h), 0.0000001), hh, t2, h)}
1208
1209def points.clean(xs: List<&2, Point>, +previous: Point) -> List<&2, Point>:
1210  match xs:
1211    case Nil{}: Nil{}
1212    case Con{h, t}:
1213      +hh = h
1214      points.clean.go(F32.is_lt(point.distance(hh, previous), 0.0000001), hh, t, previous)
1215
1216def subpath.finish(points: List<&2, Point>, closed: Bool, paths: List<&2, Subpath>) -> List<&2, Subpath>:
1217  match points:
1218    case Nil{}: paths
1219    case Con{h, t}:
1220      +p = h
1221      Con{Subpath{points.rev(Con{p, points.clean(t, p)}, Nil{}), closed}, paths}
1222
1223def edges.rev(es: List<&2, Edge>, acc: List<&2, Edge>) -> List<&2, Edge>:
1224  match es:
1225    case Nil{}: acc
1226    case Con{h, t}: edges.rev(t, Con{h, acc})
1227
1228def points.add(points: List<&2, Point>, a: Point, b: Point) -> List<&2, Point>:
1229  match points:
1230    case Nil{}: Con{b, Con{a, Nil{}}}
1231    case Con{h, t}: Con{b, Con{h, t}}
1232
1233# The polylines under construction: the current one's points (reversed)
1234# and the finished subpaths.
1235type Sub is Data:
1236  Sub{points: List<&2, Point>, paths: List<&2, Subpath>}
1237
1238# 0 a fill-only edge (ends the current subpath, open), 1 a stroked edge
1239# (extends it), 2 a closing edge (ends it, closed).
1240def edge.kind(+stroked: Bool, +closed: Bool) -> U32:
1241  U.choose(U32, stroked, U.choose(U32, closed, 2, 1), 0)
1242
1243def subpaths.add(kind: U32, +ax: F32, +ay: F32, +bx: F32, +by: F32, st: Sub) -> Sub:
1244  match kind:
1245    case 0:
1246      Sub{points, paths} = st
1247      Sub{Nil{}, subpath.finish(points, False{}, paths)}
1248    case 1:
1249      Sub{points, paths} = st
1250      Sub{points.add(points, Point{ax, ay}, Point{bx, by}), paths}
1251    case _:
1252      Sub{points, paths} = st
1253      Sub{Nil{}, subpath.finish(points.add(points, Point{ax, ay}, Point{bx, by}), True{}, paths)}
1254
1255def subpaths.finish(st: Sub) -> List<&2, Subpath>:
1256  Sub{points, paths} = st
1257  subpath.finish(points, False{}, paths)
1258
1259@unsafe
1260def subpaths.go(kind: U32, +ax: F32, +ay: F32, +bx: F32, +by: F32, rest: List<&2, Edge>, st: Sub) -> List<&2, Subpath>:
1261  match rest:
1262    case Nil{}: subpaths.finish(subpaths.add(kind, ax, ay, bx, by, st))
1263    case Con{Edge{x0, y0, x1, y1, s2, c2}, r2}: subpaths.go(edge.kind(s2, c2), x0, y0, x1, y1, r2, subpaths.add(kind, ax, ay, bx, by, st))
1264
1265def subpaths(es: List<&2, Edge>) -> List<&2, Subpath>:
1266  match es:
1267    case Nil{}: Nil{}
1268    case Con{Edge{ax, ay, bx, by, stroked, closed}, rest}: subpaths.go(edge.kind(stroked, closed), ax, ay, bx, by, rest, Sub{Nil{}, Nil{}})
1269
1270type Segment is Data:
1271  Segment{px: F32, py: F32, qx: F32, qy: F32, ux: F32, uy: F32, length: F32, distance: F32}
1272
1273def segment.of(p: Point, q: Point, u: Point, +length: F32, +distance: F32) -> Segment:
1274  Point{px, py} = p
1275  Point{qx, qy} = q
1276  Point{ux, uy} = u
1277  Segment{px, py, qx, qy, ux, uy, length, distance}
1278
1279def segments(+points: List<&2, Point>, +distance: F32) -> List<&2, Segment>:
1280  match points:
1281    case Nil{}: Nil{}
1282    case Con{p, Nil{}}: Nil{}
1283    case Con{p, Con{q, rest}}:
1284      +len = point.distance(p, q)
1285      Con{segment.of(p, q, point.unit(point.sub(q, p)), len, distance), segments(Con{q, rest}, (distance + len : F32))}
1286
1287def segments.length(xs: List<&2, Segment>) -> F32:
1288  match xs:
1289    case Nil{}: 0.0
1290    case Con{Segment{px, py, qx, qy, ux, uy, len, distance}, rest}: (len + segments.length(rest) : F32)
1291
1292# Dash intervals are measured along each flattened subpath. A dash crossing a
1293# vertex gets a join; a dash ending there gets a cap instead.
1294type DashPattern is Data:
1295  DashPattern{values: List<&2, F32>, period: F32, offset: F32}
1296
1297type DashCursor is Data:
1298  DashCursor{remaining: F32, rest: List<&2, F32>, painted: Bool}
1299
1300type DashRun is Data:
1301  DashRun{start: F32, end: F32}
1302
1303def floats.append(xs: List<&2, F32>, ys: List<&2, F32>) -> List<&2, F32>:
1304  match xs:
1305    case Nil{}: ys
1306    case Con{h, t}: Con{h, floats.append(t, ys)}
1307
1308def floats.count(xs: List<&2, F32>) -> U32:
1309  match xs:
1310    case Nil{}: 0
1311    case Con{h, t}: U32.inc(floats.count(t))
1312
1313def floats.sum(xs: List<&2, F32>) -> F32:
1314  match xs:
1315    case Nil{}: 0.0
1316    case Con{h, t}: (h + floats.sum(t) : F32)
1317
1318def floats.negative(xs: List<&2, F32>) -> Bool:
1319  match xs:
1320    case Nil{}: False{}
1321    case Con{h, t}: F32.is_lt(h, 0.0) || floats.negative(t)
1322
1323def dash.skip.units(+s: String) -> String:
1324  match s:
1325    case SNil{}: ""
1326    case SCon{Chr{c}, t}:
1327      U.branch(String, (U32.is_ge(c, 65) && U32.is_le(c, 90)) || (U32.is_ge(c, 97) && U32.is_le(c, 122)) || U32.is_eq(c, 37),
1328        _ => dash.skip.units(t), _ => s)
1329
1330def dash.value(+n: U.Number, ref: F32, next: String -> List<&2, F32>) -> List<&2, F32>:
1331  U.Number{x, t, valid} = n
1332  U.choose(Unit -> List<&2, F32>, valid, _ => Con{U.unit(n, ref), next(dash.skip.units(t))}, _ => Nil{})(Unit{})
1333
1334@unsafe
1335def dash.values(s: String, +ref: F32) -> List<&2, F32>:
1336  dash.value(U.number(s), ref, t => dash.values(t, ref))
1337
1338def dash.pattern.values(+values: List<&2, F32>, offset: F32) -> DashPattern:
1339  +vs = U.choose(List<&2, F32>, U32.is_eq((floats.count(values) % 2 : U32), 1), floats.append(values, values), values)
1340  DashPattern{vs, U.choose(F32, floats.negative(vs), 0.0, floats.sum(vs)), offset}
1341
1342def floats.scale(xs: List<&2, F32>, +factor: F32) -> List<&2, F32>:
1343  match xs:
1344    case Nil{}: Nil{}
1345    case Con{h, t}: Con{(h * factor : F32), floats.scale(t, factor)}
1346
1347def dash.pattern(settings: StrokeSettings, +ref: F32, +factor: F32) -> DashPattern:
1348  StrokeSettings{cap, join, limit, dash, offset, vector} = settings
1349  dash.pattern.values(floats.scale(dash.values(dash, ref), factor), (U.unit(U.number(offset), ref) * factor : F32))
1350
1351# Whether the pattern dashes at all.
1352def dash.some(pattern: DashPattern) -> Bool:
1353  DashPattern{values, period, offset} = pattern
1354  F32.is_gt(period, 0.0000001)
1355
1356def dash.seek(xs: List<&2, F32>, +offset: F32, +painted: Bool) -> DashCursor:
1357  match xs:
1358    case Nil{}: DashCursor{0.0, Nil{}, painted}
1359    case Con{h, t}:
1360      +len = h
1361      +tail = t
1362      U.branch(DashCursor, F32.is_le(offset, len), _ => DashCursor{(len - offset : F32), tail, painted},
1363        _ => dash.seek(tail, (offset - len : F32), Bool.not(painted)))
1364
1365def dash.next(xs: List<&2, F32>, all: List<&2, F32>, painted: Bool) -> DashCursor:
1366  match xs:
1367    case Nil{}: dash.seek(all, 0.0, painted)
1368    case Con{h, t}: DashCursor{h, t, painted}
1369
1370@unsafe
1371def dash.walk(+cursor: DashCursor, +all: List<&2, F32>, +position: F32, +total: F32) -> List<&2, DashRun>:
1372  DashCursor{remaining, rest, painted} = cursor
1373  +end = U.min(total, (position + remaining : F32))
1374  +tail = U.branch(List<&2, DashRun>, F32.is_ge(end, total), _ => Nil{},
1375    _ => dash.walk(dash.next(rest, all, Bool.not(painted)), all, end, total))
1376  U.choose(List<&2, DashRun>, painted, Con{DashRun{position, end}, tail}, tail)
1377
1378def dash.runs(+pattern: DashPattern, +total: F32) -> List<&2, DashRun>:
1379  DashPattern{values, period, offset} = pattern
1380  U.branch(List<&2, DashRun>, F32.is_le(period, 0.0000001), _ => Con{DashRun{0.0, total}, Nil{}},
1381    _ => dash.walk(dash.seek(values, (offset - period * F32.floor((offset / period : F32)) : F32), True{}), values, 0.0, total))
1382
1383def dash.crosses(xs: List<&2, DashRun>, +t: F32) -> Bool:
1384  match xs:
1385    case Nil{}: False{}
1386    case Con{DashRun{a, b}, rest}:
1387      (F32.is_lt(a, t) && F32.is_gt(b, t)) || dash.crosses(rest, t)
1388
1389def dash.start(xs: List<&2, DashRun>) -> Bool:
1390  match xs:
1391    case Nil{}: False{}
1392    case Con{DashRun{a, b}, rest}:
1393      (F32.is_le(a, 0.0) && F32.is_gt(b, 0.0)) || dash.start(rest)
1394
1395def dash.end(xs: List<&2, DashRun>, +total: F32) -> Bool:
1396  match xs:
1397    case Nil{}: False{}
1398    case Con{DashRun{a, b}, rest}:
1399      (F32.is_lt(a, total) && F32.is_ge(b, total)) || dash.end(rest, total)
1400
1401def segment.dir(s: Segment) -> Point:
1402  Segment{px, py, qx, qy, ux, uy, len, distance} = s
1403  Point{ux, uy}
1404
1405def segment.start(s: Segment) -> Point:
1406  Segment{px, py, qx, qy, ux, uy, len, distance} = s
1407  Point{px, py}
1408
1409def segment.end(s: Segment) -> Point:
1410  Segment{px, py, qx, qy, ux, uy, len, distance} = s
1411  Point{qx, qy}
1412
1413# ---- outline stroker ----
1414# A stroke is filled as an outline with nonzero winding: for an open subpath
1415# one polygon (the left offsets forward, the end cap, the right offsets
1416# backward, the start cap), for a closed one two (the left and the right
1417# offsets, each closed, running opposite ways so the band between them has
1418# winding one). Joins are built on the outer side of a vertex only; on the
1419# inner side the two offsets cross, and the small loop that makes still
1420# fills under nonzero winding. Round parts are flattened to the device
1421# tolerance through `scale`, the device pixels per local unit.
1422
1423# The join and cap of a stroke as codes, decided once per stroke instead of
1424# by name at every vertex: cap 0 butt, 1 round, 2 square; join 0 miter,
1425# 1 round, 2 bevel, 3 miter-clip.
1426type Joins is Data:
1427  Joins{cap: U32, join: U32, limit: F32}
1428
1429def cap.code(+s: String) -> U32:
1430  U.choose(U32, U.string.equal(s, "round"), 1, U.choose(U32, U.string.equal(s, "square"), 2, 0))
1431
1432def join.code(+s: String) -> U32:
1433  U.choose(U32, U.string.equal(s, "round"), 1, U.choose(U32, U.string.equal(s, "bevel"), 2, U.choose(U32, U.string.equal(s, "miter-clip"), 3, 0)))
1434
1435def joins.of(settings: StrokeSettings) -> Joins:
1436  StrokeSettings{cap, join, limit, dash, offset, vector} = settings
1437  Joins{cap.code(cap), join.code(join), limit}
1438
1439def outline.tolerance(+scale: F32) -> F32:
1440  (flat.tolerance() / U.max(scale, 0.000001) : F32)
1441
1442# Steps for an arc of `sweep` radians at radius r within the tolerance.
1443def outline.steps(+sweep: F32, +r: F32, +scale: F32) -> U32:
1444  +tol = outline.tolerance(scale)
1445  +step = U.max(0.001, (2.0 * F32.sqrt((2.0 * U.max(tol, 0.000001) / U.max(r, 0.000001) : F32)) : F32))
1446  F32.to_u32(U.min(4096.0, U.max(1.0, F32.ceil((F32.abs(sweep) / step : F32)))))
1447
1448def point.angle(+p: Point) -> F32:
1449  Point{x, y} = p
1450  F32.atan2(y, x)
1451
1452# n points from `angle` on, `step` apart, prepended in order.
1453def outline.arc(n: Nat, +c: Point, +r: F32, +angle: F32, +step: F32, acc: List<&2, Point>) -> List<&2, Point>:
1454  match n:
1455    case 0n: acc
1456    case 1n+d: outline.arc(d, c, r, (angle + step : F32), step, Con{point.add(c, Point{(r * F32.cos(angle) : F32), (r * F32.sin(angle) : F32)}), acc})
1457
1458# The points strictly between the start and end of an arc around c from the
1459# direction `from` sweeping `sweep` radians (signed).
1460def outline.sweep(+c: Point, +r: F32, +from: Point, +sweep: F32, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1461  +n = outline.steps(sweep, r, scale)
1462  +step = (sweep / U32.to_f32(n) : F32)
1463  outline.arc(U32.to_nat((n - 1 : U32)), c, r, (point.angle(from) + step : F32), step, acc)
1464
1465# The signed angle from u to v, both unit vectors.
1466def point.turn(+u: Point, +v: Point) -> F32:
1467  F32.atan2(point.cross(u, v), point.dot(u, v))
1468
1469# The join on the left side at vertex p, from the incoming direction u to the
1470# outgoing v, when that side is the outer one (the path turns right). Both
1471# offset endpoints a = p + r n(u) and b = p + r n(v) are pushed by the
1472# segments; only what lies between them is added here.
1473def outline.miter.pick(fits: Bool, clip: Bool, +m: Point, +a: Point, +b: Point, +da: F32, +db: F32, +distance: F32, +reach: F32, acc: List<&2, Point>) -> List<&2, Point>:
1474  match fits:
1475    case True{}: Con{m, acc}
1476    case False{}:
1477      match clip:
1478        case True{}:
1479          Con{point.add(b, point.scale(point.sub(m, b), U.clamp(((reach - db) / (distance - db) : F32), 0.0, 1.0))),
1480            Con{point.add(a, point.scale(point.sub(m, a), U.clamp(((reach - da) / (distance - da) : F32), 0.0, 1.0))), acc}}
1481        case False{}: acc
1482
1483def outline.miter(+p: Point, +a: Point, +b: Point, +u: Point, +v: Point, +r: F32, +limit: F32, +join: U32, acc: List<&2, Point>) -> List<&2, Point>:
1484  +m = point.add(a, point.scale(u, (point.cross(point.sub(b, a), v) / point.cross(u, v) : F32)))
1485  +distance = point.distance(p, m)
1486  +n = point.unit(point.sub(m, p))
1487  +reach = (limit * r : F32)
1488  outline.miter.pick(F32.is_le(distance, reach), U32.is_eq(join, 3), m, a, b, point.dot(point.sub(a, p), n), point.dot(point.sub(b, p), n), distance, reach, acc)
1489
1490def outline.join.kind(+join: U32, +p: Point, +u: Point, +v: Point, +nu: Point, +r: F32, +limit: F32, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1491  match join:
1492    case 1: outline.sweep(p, r, nu, point.turn(u, v), scale, acc)
1493    case 2: acc
1494    case _: outline.miter(p, point.add(p, point.scale(nu, r)), point.add(p, point.scale(point.normal(v), r)), u, v, r, limit, join, acc)
1495
1496def outline.join.outer(+p: Point, +u: Point, +v: Point, +r: F32, js: Joins, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1497  Joins{cap, join, limit} = js
1498  outline.join.kind(join, p, u, v, point.normal(u), r, limit, scale, acc)
1499
1500# A reversal (v = -u) gets the half circle through p + r u when joins are round.
1501def outline.join.back.kind(join: U32, +p: Point, +u: Point, +r: F32, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1502  match join:
1503    case 1: outline.sweep(p, r, point.normal(u), F32.neg(3.141592653589793), scale, acc)
1504    case _: acc
1505
1506def outline.join.back(+p: Point, +u: Point, +r: F32, js: Joins, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1507  Joins{cap, join, limit} = js
1508  outline.join.back.kind(join, p, u, r, scale, acc)
1509
1510# The previous segment along a side: its direction, the end of its offset
1511# (not yet emitted, since an inner join replaces it) and its length.
1512type Prev is Data:
1513  Prev{dir: Point, end: Point, len: F32}
1514
1515def prev.of(seg: Segment, +r: F32) -> Prev:
1516  Segment{px, py, qx, qy, ux, uy, len, distance} = seg
1517  +u = {Point{ux, uy} : Point}
1518  Prev{u, point.add(Point{qx, qy}, point.scale(point.normal(u), r)), len}
1519
1520# On the inner side the two offset lines cross; when they cross within both
1521# segments that point replaces both offset ends, so the outline has no loop
1522# there. Otherwise (a segment shorter than the offset) the ends stay and the
1523# loop they make still fills under nonzero winding.
1524def outline.inner(+p: Point, +u: Point, +a1: Point, +lenPrev: F32, +v: Point, +b0: Point, +lenNext: F32, +acc: List<&2, Point>) -> List<&2, Point>:
1525  +den = point.cross(u, v)
1526  +t = (point.cross(point.sub(b0, a1), v) / den : F32)
1527  +s = (point.cross(point.sub(b0, a1), u) / den : F32)
1528  U.choose(List<&2, Point>, F32.is_le(t, 0.0) && F32.is_ge(t, F32.neg(lenPrev)) && F32.is_ge(s, 0.0) && F32.is_le(s, lenNext),
1529    Con{point.add(a1, point.scale(u, t)), acc}, Con{b0, Con{a1, acc}})
1530
1531def outline.back.if(reversal: Bool, +p: Point, +u: Point, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1532  match reversal:
1533    case True{}: outline.join.back(p, u, r, js, scale, acc)
1534    case False{}: acc
1535
1536# The points at vertex p between the previous segment (direction u, offset
1537# end a1) and the next (direction v, offset start b0).
1538def outline.vertex.pick(straight: Bool, outer: Bool, +p: Point, +u: Point, +a1: Point, +lenPrev: F32, +v: Point, +b0: Point, +lenNext: F32, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1539  match straight:
1540    case True{}: Con{b0, outline.back.if(F32.is_lt(point.dot(u, v), 0.0), p, u, r, js, scale, Con{a1, acc})}
1541    case False{}:
1542      match outer:
1543        case True{}: Con{b0, outline.join.outer(p, u, v, r, js, scale, Con{a1, acc})}
1544        case False{}: outline.inner(p, u, a1, lenPrev, v, b0, lenNext, acc)
1545
1546def outline.vertex.join(+p: Point, +u: Point, +a1: Point, +lenPrev: F32, +v: Point, +b0: Point, +lenNext: F32, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1547  +turn = point.cross(u, v)
1548  outline.vertex.pick(F32.is_lt(F32.abs(turn), 0.000001), F32.is_lt(turn, 0.0), p, u, a1, lenPrev, v, b0, lenNext, r, js, scale, acc)
1549
1550def outline.vertex(previous: Maybe<&2, Prev>, +p: Point, +v: Point, +lenNext: F32, +b0: Point, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1551  match previous:
1552    case None{}: Con{b0, acc}
1553    case Some{Prev{u, a1, lenPrev}}: outline.vertex.join(p, u, a1, lenPrev, v, b0, lenNext, r, js, scale, acc)
1554
1555# At the end of an open side the last offset end is emitted; a closed loop
1556# emitted it through its first vertex.
1557def outline.side.end(previous: Maybe<&2, Prev>, closed: Bool, acc: List<&2, Point>) -> List<&2, Point>:
1558  match previous:
1559    case None{}: acc
1560    case Some{Prev{u, e, l}}:
1561      match closed:
1562        case True{}: acc
1563        case False{}: Con{e, acc}
1564
1565# The left side of a polyline: each segment's offset endpoints with the
1566# joins between segments. `previous` carries the segment arriving at the
1567# first vertex (the last one, for a closed subpath).
1568def outline.side(xs: List<&2, Segment>, previous: Maybe<&2, Prev>, +closed: Bool, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1569  match xs:
1570    case Nil{}: outline.side.end(previous, closed, acc)
1571    case Con{Segment{px, py, qx, qy, ux, uy, len, distance}, rest}:
1572      +u = {Point{ux, uy} : Point}
1573      +pp = {Point{px, py} : Point}
1574      +ln = len
1575      +n = point.scale(point.normal(u), r)
1576      outline.side(rest, Some{Prev{u, point.add(Point{qx, qy}, n), ln}}, closed, r, js, scale, outline.vertex(previous, pp, u, ln, point.add(pp, n), r, js, scale, acc))
1577
1578# The cap at point p leaving in direction u, between the offsets p + r n and
1579# p - r n (which the sides push): nothing for butt.
1580def outline.cap(cap: U32, +p: Point, +u: Point, +r: F32, +scale: F32, acc: List<&2, Point>) -> List<&2, Point>:
1581  match cap:
1582    case 1: outline.sweep(p, r, point.normal(u), F32.neg(3.141592653589793), scale, acc)
1583    case 2:
1584      +n = point.scale(point.normal(u), r)
1585      +ahead = point.add(p, point.scale(u, r))
1586      Con{point.sub(ahead, n), Con{point.add(ahead, n), acc}}
1587    case _: acc
1588
1589def outline.edges.go(+first: Point, pts: List<&2, Point>, +prev: Point, acc: List<&2, Edge>) -> List<&2, Edge>:
1590  match pts:
1591    case Nil{}: Con{edge.of(prev, first, True{}, True{}), acc}
1592    case Con{p, rest}:
1593      +pp = p
1594      outline.edges.go(first, rest, pp, Con{edge.of(prev, pp, True{}, False{}), acc})
1595
1596# The closed polygon through the points, as edges.
1597def outline.edges(pts: List<&2, Point>, acc: List<&2, Edge>) -> List<&2, Edge>:
1598  match pts:
1599    case Nil{}: acc
1600    case Con{p, rest}:
1601      +pp = p
1602      outline.edges.go(pp, rest, pp, acc)
1603
1604def segments.last(xs: List<&2, Segment>, fallback: Segment) -> Segment:
1605  match xs:
1606    case Nil{}: fallback
1607    case Con{h, t}: segments.last(t, h)
1608
1609# The outline of an open polyline: the left side, the end cap, the right side
1610# (the left side of the reversed points) and the start cap.
1611def joins.cap(+s: Joins) -> U32:
1612  Joins{cap, join, limit} = s
1613  cap
1614
1615def outline.open.go(forward: List<&2, Segment>, back: List<&2, Segment>, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1616  match forward:
1617    case Nil{}: acc
1618    case Con{h, t}:
1619      +first = h
1620      +tt = t
1621      +last = segments.last(tt, first)
1622      +cap = joins.cap(js)
1623      outline.edges(outline.cap(cap, segment.start(first), point.scale(segment.dir(first), F32.neg(1.0)), r, scale,
1624        outline.side(back, None{}, False{}, r, js, scale,
1625          outline.cap(cap, segment.end(last), segment.dir(last), r, scale,
1626            outline.side(Con{first, tt}, None{}, False{}, r, js, scale, Nil{})))), acc)
1627
1628# The segments backwards, each turned around, for the right side.
1629def segments.flip(xs: List<&2, Segment>, acc: List<&2, Segment>) -> List<&2, Segment>:
1630  match xs:
1631    case Nil{}: acc
1632    case Con{Segment{px, py, qx, qy, ux, uy, len, distance}, rest}: segments.flip(rest, Con{Segment{qx, qy, px, py, F32.neg(ux), F32.neg(uy), len, distance}, acc})
1633
1634def outline.open.segs(+forward: List<&2, Segment>, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1635  outline.open.go(forward, segments.flip(forward, Nil{}), r, js, scale, acc)
1636
1637def outline.open(pts: List<&2, Point>, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1638  outline.open.segs(segments(pts, 0.0), r, js, scale, acc)
1639
1640# A closed subpath: one closed loop per side, each joined at the start vertex.
1641def outline.loop(+xs: List<&2, Segment>, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1642  match xs:
1643    case Nil{}: acc
1644    case Con{h, t}:
1645      +first = h
1646      outline.edges(outline.side(Con{h, t}, Some{prev.of(segments.last(t, first), r)}, True{}, r, js, scale, Nil{}), acc)
1647
1648def outline.closed.segs(+forward: List<&2, Segment>, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1649  outline.loop(segments.flip(forward, Nil{}), r, js, scale, outline.loop(forward, r, js, scale, acc))
1650
1651def outline.closed(pts: List<&2, Point>, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1652  outline.closed.segs(segments(pts, 0.0), r, js, scale, acc)
1653
1654# A zero-length piece: a dot for round caps, a square for square caps.
1655def outline.dot.cap(+cap: U32, +p: Point, +u: Point, +r: F32, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1656  match cap:
1657    case 0: acc
1658    case _:
1659      +n = point.scale(point.normal(u), r)
1660      outline.edges(outline.cap(cap, p, point.scale(u, F32.neg(1.0)), r, scale, Con{point.sub(p, n), outline.cap(cap, p, u, r, scale, Con{point.add(p, n), Nil{}})}), acc)
1661
1662def outline.dot(+p: Point, +u: Point, +r: F32, js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1663  outline.dot.cap(joins.cap(js), p, u, r, scale, acc)
1664
1665# ---- dashes as polylines ----
1666# The point at distance d along the segments, and the direction there.
1667def segment.at(s: Segment, +d: F32) -> Point:
1668  Segment{px, py, qx, qy, ux, uy, len, distance} = s
1669  point.add(Point{px, py}, point.scale(Point{ux, uy}, U.clamp((d - distance : F32), 0.0, len)))
1670
1671def segment.reaches(s: Segment, +d: F32) -> Bool:
1672  Segment{px, py, qx, qy, ux, uy, len, distance} = s
1673  F32.is_le(d, (distance + len : F32))
1674
1675@unsafe
1676def segments.at(xs: List<&2, Segment>, +d: F32, +fallback: Point) -> Point:
1677  match xs:
1678    case Nil{}: fallback
1679    case Con{h, Nil{}}: segment.at(h, d)
1680    case Con{h, Con{g, t}}:
1681      +hh = h
1682      U.branch(Point, segment.reaches(hh, d), _ => segment.at(hh, d), _ => segments.at(Con{g, t}, d, fallback))
1683
1684@unsafe
1685def segments.dir.at(xs: List<&2, Segment>, +d: F32, +fallback: Point) -> Point:
1686  match xs:
1687    case Nil{}: fallback
1688    case Con{h, Nil{}}: segment.dir(h)
1689    case Con{h, Con{g, t}}:
1690      +hh = h
1691      U.branch(Point, segment.reaches(hh, d), _ => segment.dir(hh), _ => segments.dir.at(Con{g, t}, d, fallback))
1692
1693def points.push.if(hit: Bool, +p: Point, tail: List<&2, Point>) -> List<&2, Point>:
1694  match hit:
1695    case True{}: Con{p, tail}
1696    case False{}: tail
1697
1698# The vertices strictly inside (s, e), in order, prepended before `acc`.
1699def segments.between(xs: List<&2, Segment>, +s: F32, +e: F32, acc: List<&2, Point>) -> List<&2, Point>:
1700  match xs:
1701    case Nil{}: acc
1702    case Con{Segment{px, py, qx, qy, ux, uy, len, distance}, rest}:
1703      +at = (distance + len : F32)
1704      points.push.if(F32.is_gt(at, s) && F32.is_lt(at, e), Point{qx, qy}, segments.between(rest, s, e, acc))
1705
1706# The polyline of the run [s, e] along the segments.
1707def dash.points(+xs: List<&2, Segment>, +s: F32, +e: F32) -> List<&2, Point>:
1708  Con{segments.at(xs, s, Point{0.0, 0.0}), segments.between(xs, s, e, Con{segments.at(xs, e, Point{0.0, 0.0}), Nil{}})}
1709
1710def outline.run(+xs: List<&2, Segment>, +s: F32, +e: F32, +r: F32, +js: Joins, +scale: F32, +acc: List<&2, Edge>) -> List<&2, Edge>:
1711  U.branch(List<&2, Edge>, F32.is_le((e - s : F32), 0.000001),
1712    _ => outline.dot(segments.at(xs, s, Point{0.0, 0.0}), segments.dir.at(xs, s, Point{1.0, 0.0}), r, js, scale, acc),
1713    _ => outline.open(dash.points(xs, s, e), r, js, scale, acc))
1714
1715# The runs of an open subpath, or of a closed one whose dashes do not meet at
1716# the seam. A closed subpath painted along its whole length is a closed loop.
1717def outline.runs(runs: List<&2, DashRun>, +xs: List<&2, Segment>, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1718  match runs:
1719    case Nil{}: acc
1720    case Con{DashRun{s, e}, rest}: outline.runs(rest, xs, r, js, scale, outline.run(xs, s, e, r, js, scale, acc))
1721
1722def dash.first.end(xs: List<&2, DashRun>) -> F32:
1723  match xs:
1724    case Nil{}: 0.0
1725    case Con{DashRun{s, e}, rest}: e
1726
1727def dash.last.start(xs: List<&2, DashRun>, +fallback: F32) -> F32:
1728  match xs:
1729    case Nil{}: fallback
1730    case Con{DashRun{s, e}, rest}: dash.last.start(rest, s)
1731
1732def dash.drop.first(xs: List<&2, DashRun>) -> List<&2, DashRun>:
1733  match xs:
1734    case Nil{}: Nil{}
1735    case Con{h, t}: t
1736
1737def dash.drop.last(xs: List<&2, DashRun>) -> List<&2, DashRun>:
1738  match xs:
1739    case Nil{}: Nil{}
1740    case Con{h, Nil{}}: Nil{}
1741    case Con{h, Con{g, t}}: Con{h, dash.drop.last(Con{g, t})}
1742
1743def dash.whole(xs: List<&2, DashRun>, +total: F32) -> Bool:
1744  match xs:
1745    case Nil{}: False{}
1746    case Con{DashRun{s, e}, Nil{}}: F32.is_le(s, 0.0) && F32.is_ge(e, total)
1747    case Con{DashRun{s, e}, Con{g, t}}: False{}
1748
1749def dash.points.tail(+xs: List<&2, Segment>, +s: F32, +e: F32) -> List<&2, Point>:
1750  segments.between(xs, s, e, Con{segments.at(xs, e, Point{0.0, 0.0}), Nil{}})
1751
1752def points.append(xs: List<&2, Point>, ys: List<&2, Point>) -> List<&2, Point>:
1753  match xs:
1754    case Nil{}: ys
1755    case Con{h, t}: Con{h, points.append(t, ys)}
1756
1757# The dashes of a closed subpath crossing the seam merge into one polyline:
1758# the last run's points followed by the first run's, minus the shared point.
1759def outline.seam(+xs: List<&2, Segment>, +runs: List<&2, DashRun>, +total: F32, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1760  +s = dash.last.start(runs, 0.0)
1761  +e = dash.first.end(runs)
1762  outline.runs(dash.drop.last(dash.drop.first(runs)), xs, r, js, scale,
1763    outline.open(points.append(dash.points(xs, s, total), dash.points.tail(xs, 0.0, e)), r, js, scale, acc))
1764
1765def outline.dashed.runs(+pts: List<&2, Point>, +closed: Bool, +r: F32, +js: Joins, +pattern: DashPattern, +scale: F32, +acc: List<&2, Edge>) -> List<&2, Edge>:
1766  +xs = segments(pts, 0.0)
1767  +total = segments.length(xs)
1768  +runs = dash.runs(pattern, total)
1769  U.branch(List<&2, Edge>, closed && dash.whole(runs, total), _ => outline.closed(pts, r, js, scale, acc),
1770    _ => U.branch(List<&2, Edge>, closed && dash.start(runs) && dash.end(runs, total) && Bool.not(dash.whole(runs, total)),
1771      _ => outline.seam(xs, runs, total, r, js, scale, acc),
1772      _ => outline.runs(runs, xs, r, js, scale, acc)))
1773
1774# An undashed subpath is outlined whole; only a dash pattern measures it.
1775def outline.plain(closed: Bool, pts: List<&2, Point>, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1776  match closed:
1777    case True{}: outline.closed(pts, r, js, scale, acc)
1778    case False{}: outline.open(pts, r, js, scale, acc)
1779
1780def outline.dashed(dashed: Bool, pts: List<&2, Point>, +closed: Bool, +r: F32, +js: Joins, pattern: DashPattern, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1781  match dashed:
1782    case True{}: outline.dashed.runs(pts, closed, r, js, pattern, scale, acc)
1783    case False{}: outline.plain(closed, pts, r, js, scale, acc)
1784
1785def dash.any(xs: List<&2, DashRun>) -> Bool:
1786  match xs:
1787    case Nil{}: False{}
1788    case Con{h, t}: True{}
1789
1790def outline.dot.if(paint: Bool, +p: Point, +r: F32, +js: Joins, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1791  match paint:
1792    case True{}: outline.dot(p, Point{1.0, 0.0}, r, js, scale, acc)
1793    case False{}: acc
1794
1795# A subpath of one point: caps only, as a dot in the x direction, when the
1796# dash pattern paints at its start.
1797def outline.subpath(+pts: List<&2, Point>, +closed: Bool, +r: F32, +js: Joins, +pattern: DashPattern, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1798  match pts:
1799    case Nil{}: acc
1800    case Con{p, Nil{}}: outline.dot.if(dash.any(dash.runs(pattern, 0.0)), p, r, js, scale, acc)
1801    case Con{p, Con{q, rest}}: outline.dashed(dash.some(pattern), Con{p, Con{q, rest}}, closed, r, js, pattern, scale, acc)
1802
1803def outline.subpaths(paths: List<&2, Subpath>, +r: F32, +js: Joins, +pattern: DashPattern, +scale: F32, acc: List<&2, Edge>) -> List<&2, Edge>:
1804  match paths:
1805    case Nil{}: acc
1806    case Con{Subpath{points, closed}, rest}: outline.subpaths(rest, r, js, pattern, scale, outline.subpath(points, closed, r, js, pattern, scale, acc))
1807
1808def subpaths.length(paths: List<&2, Subpath>) -> F32:
1809  match paths:
1810    case Nil{}: 0.0
1811    case Con{Subpath{points, closed}, rest}: (segments.length(segments(points, 0.0)) + subpaths.length(rest) : F32)
1812
1813def stroke.compile.pick(measured: Bool, paths: List<&2, Subpath>, r: F32, +settings: StrokeSettings, ref: F32, +declared: F32, +scale: F32) -> List<&2, Edge>:
1814  match measured:
1815    case True{}:
1816      +ps = paths
1817      outline.subpaths(ps, r, joins.of(settings), dash.pattern(settings, ref, (subpaths.length(ps) / declared : F32)), scale, Nil{})
1818    case False{}: outline.subpaths(paths, r, joins.of(settings), dash.pattern(settings, ref, 1.0), scale, Nil{})
1819
1820def stroke.compile.paths(paths: List<&2, Subpath>, r: F32, +settings: StrokeSettings, ref: F32, +declared: F32, +scale: F32) -> List<&2, Edge>:
1821  stroke.compile.pick(F32.is_gt(declared, 0.0), paths, r, settings, ref, declared, scale)
1822
1823# The stroke outline edges of a shape's edges, in the same units.
1824def stroke.compile(es: List<&2, Edge>, r: F32, settings: StrokeSettings, ref: F32, declared: F32, +scale: F32) -> List<&2, Edge>:
1825  stroke.compile.paths(subpaths(edges.rev(es, Nil{})), r, settings, ref, declared, scale)
1826
1827def bounds.empty() -> Bounds:
1828  Bounds{1000000000.0, 1000000000.0, F32.neg(1000000000.0), F32.neg(1000000000.0)}
1829
1830def bounds.merge(a: Bounds, b: Bounds) -> Bounds:
1831  Bounds{x0, y0, x1, y1} = a
1832  Bounds{u0, v0, u1, v1} = b
1833  Bounds{U.min(x0, u0), U.min(y0, v0), U.max(x1, u1), U.max(y1, v1)}
1834
1835# A shape's edges with their bounds. Point queries walk the list; rasters
1836# read the list back directly, so no per-edge tree is ever built.
1837type EdgeTree is Data:
1838  EdgeFlat{box: Bounds, edges: List<&2, Edge>}
1839
1840def edge.tree.bounds(t: EdgeTree) -> Bounds:
1841  match t:
1842    case EdgeFlat{box, edges}: box
1843
1844def edge.tree(+es: List<&2, Edge>) -> EdgeTree:
1845  EdgeFlat{bounds(es, bounds.empty()), es}
1846
1847def edge.tree.empty() -> EdgeTree:
1848  EdgeFlat{bounds.empty(), Nil{}}
1849
1850def bounds.ray(b: Bounds, p: Point) -> Bool:
1851  Bounds{x0, y0, x1, y1} = b
1852  Point{x, y} = p
1853  +py = y
1854  F32.is_ge(py, y0) && F32.is_lt(py, y1) && F32.is_le(x, x1)
1855
1856def edges.winding(es: List<&2, Edge>, +p: Point, +acc: F32) -> F32:
1857  match es:
1858    case Nil{}: acc
1859    case Con{e, rest}: edges.winding(rest, p, (acc + edge.cross(e, p) : F32))
1860
1861def edge.tree.winding(t: EdgeTree, +p: Point) -> F32:
1862  match t:
1863    case EdgeFlat{box, edges}: U.branch(F32, bounds.ray(box, p), _ => edges.winding(edges, p, 0.0), _ => 0.0)
1864
1865def edge.tree.inside(t: EdgeTree, p: Point, even: Bool) -> Bool:
1866  +w = edge.tree.winding(t, p)
1867  F32.is_gt(F32.abs(U.choose(F32, even, (w % 2.0 : F32), w)), 0.5)
1868
1869# A fill keeps its bounds for the scene box and, once built for a raster,
1870# the cover that gives each pixel its exact coverage. The edges stay with
1871# the scene alone, so building the cover consumes them as their owner (the
1872# covered scene keeps none); a point query without a cover, picking on the
1873# compiled scene, walks them from there.
1874type Fill is Data:
1875  Fill{box: Bounds, cover: Cover}
1876
1877def fill.new(es: List<&2, Edge>) -> Fill:
1878  Fill{bounds(es, bounds.empty()), cover.none()}
1879
1880def fill.bounds(f: Fill) -> Bounds:
1881  Fill{box, cover} = f
1882  box
1883
1884def fill.coverage(f: Fill, es: List<&2, Edge>, +q: Point, +p: Point, +even: Bool) -> F32:
1885  Fill{box, cover} = f
1886  +cov = cover
1887  U.branch(F32, cover.present(cov), _ => cover.at(cov, p), _ => U.choose(F32, fill.inside(es, q, even), 1.0, 0.0))
1888
1889def bounds.disk(p: Point, +r: F32) -> Bounds:
1890  Point{x, y} = p
1891  +px = x
1892  +py = y
1893  Bounds{(px - r : F32), (py - r : F32), (px + r : F32), (py + r : F32)}
1894
1895# The stroke outline as an edge tree in local units, with its inverse matrix
1896# (device to local) and, after the cover pass, its coverage.
1897type StrokeMesh is Data:
1898  StrokeMesh{tree: EdgeTree, inverse: Matrix, cover: Cover}
1899
1900def stroke.mesh(+edges: List<&2, Edge>, inverse: Matrix) -> StrokeMesh:
1901  StrokeMesh{edge.tree(edges), inverse, cover.none()}
1902
1903def stroke.mesh.inside(mesh: StrokeMesh, p: Point) -> Bool:
1904  StrokeMesh{tree, inv, cover} = mesh
1905  edge.tree.inside(tree, matrix.point(inv, p), False{})
1906
1907def stroke.coverage(mesh: StrokeMesh, +p: Point) -> F32:
1908  StrokeMesh{stree, inv, cover} = mesh
1909  +cov = cover
1910  U.branch(F32, cover.present(cov), _ => cover.at(cov, p), _ => U.choose(F32, edge.tree.inside(stree, matrix.point(inv, p), False{}), 1.0, 0.0))
1911
1912type MarkerStyle is Data:
1913  MarkerStyle{start: String, middle: String, end: String}
1914
1915def marker.style(ms: MarkerStyle, s: Maybe<&2, String>, m: Maybe<&2, String>, e: Maybe<&2, String>) -> MarkerStyle:
1916  MarkerStyle{start, middle, end} = ms
1917  MarkerStyle{style.present(s, start, "none", True{}), style.present(m, middle, "none", True{}), style.present(e, end, "none", True{})}
1918
1919# Normalize the small CSS paint-order vocabulary; omitted operations follow in
1920# default fill/stroke/markers order. Invalid values retain the inherited order.
1921def paint.order.normalize(s: String, +out: String, +gap: Bool) -> String:
1922  match s:
1923    case SNil{}: out
1924    case SCon{Chr{c}, rest}:
1925      +ch = c
1926      +tail = rest
1927      U.branch(String, U.space(ch), _ => paint.order.normalize(tail, out, Bool.not(U.string.equal(out, ""))),
1928        _ => paint.order.normalize(tail, out ++ U.choose(String, gap, " ", "") ++ SCon{Chr{ch}, SNil{}}, False{}))
1929
1930def paint.order.name(+order: U32) -> String:
1931  U.choose(String, U32.is_eq(order, 1), "fill markers stroke", U.choose(String, U32.is_eq(order, 2), "stroke fill markers",
1932    U.choose(String, U32.is_eq(order, 3), "stroke markers fill", U.choose(String, U32.is_eq(order, 4), "markers fill stroke",
1933      U.choose(String, U32.is_eq(order, 5), "markers stroke fill", "normal")))))
1934
1935def paint.order.value(+s: String, fallback: U32) -> U32:
1936  +v = paint.order.normalize(CSS.css.lower(U.trim.both(s)), "", False{})
1937  U.choose(U32, U.string.equal(v, "normal") || U.string.equal(v, "fill") || U.string.equal(v, "fill stroke") || U.string.equal(v, "fill stroke markers"), 0,
1938    U.choose(U32, U.string.equal(v, "fill markers") || U.string.equal(v, "fill markers stroke"), 1,
1939      U.choose(U32, U.string.equal(v, "stroke") || U.string.equal(v, "stroke fill") || U.string.equal(v, "stroke fill markers"), 2,
1940        U.choose(U32, U.string.equal(v, "stroke markers") || U.string.equal(v, "stroke markers fill"), 3,
1941          U.choose(U32, U.string.equal(v, "markers") || U.string.equal(v, "markers fill") || U.string.equal(v, "markers fill stroke"), 4,
1942            U.choose(U32, U.string.equal(v, "markers stroke") || U.string.equal(v, "markers stroke fill"), 5, fallback))))))
1943
1944# The order from an element's own attribute, else the inherited one as it is.
1945def paint.order.pick(found: Maybe<&2, String>, +inherited: U32) -> U32:
1946  match found:
1947    case None{}: inherited
1948    case Some{v}:
1949      +vv = v
1950      +k = style.keyword(vv)
1951      U.choose(U32, U32.is_eq(k, 1) || U32.is_eq(k, 3), inherited, U.choose(U32, U32.is_eq(k, 2), 0, paint.order.value(vv, inherited)))
1952
1953# Compilation resolves geometry and inherited presentation once per edit.
1954type Style is Data:
1955  Style{fill: String, stroke: String, color: String, fillOpacity: F32,
1956    strokeOpacity: F32, width: F32, even: Bool, visible: Bool, strokeSettings: StrokeSettings, markers: MarkerStyle, imageRendering: String, paintOrder: U32}
1957
1958def style.default() -> Style:
1959  Style{"black", "none", "black", 1.0, 1.0, 1.0, False{}, True{}, stroke.settings(), MarkerStyle{"none", "none", "none"}, "auto", 0}
1960
1961def style.inherit.picked(+s: Style, p: Picked, +ref: F32) -> Style:
1962  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
1963  Picked{pfill, pstroke, pcolor, pfo, pso, pwidth, prule, pvis, prender, porder, pcap, pjoin, plimit, pdash, poffset, pvector, pms, pmm, pme} = p
1964  +po = order
1965  Style{style.present(pfill, fill, "black", True{}), style.present(pstroke, stroke, "none", True{}), style.present(pcolor, col, "black", True{}),
1966    style.number.present(pfo, fo, 1.0, v => color.opacity(v, fo)),
1967    style.number.present(pso, so, 1.0, v => color.opacity(v, so)),
1968    U.max(0.0, style.number.present(pwidth, width, 1.0, v => U.unit(U.number(v), ref))),
1969    U.string.equal(style.present(prule, U.choose(String, even, "evenodd", "nonzero"), "nonzero", True{}), "evenodd"),
1970    U.string.equal(style.present(pvis, U.choose(String, visible, "visible", "hidden"), "visible", True{}), "visible"), stroke.inherit(ss, pcap, pjoin, plimit, pdash, poffset, pvector), marker.style(ms, pms, pmm, pme), style.present(prender, rendering, "auto", True{}), paint.order.pick(porder, po)}
1971
1972def style.inherit(+s: Style, as: List<&2, XML.Attr>, +ref: F32) -> Style:
1973  style.inherit.picked(s, style.pick(as, pick.none()), ref)
1974
1975# The properties a resource may inherit, in the order every resolved list
1976# carries them; a resolved parent list starts with the same keys in the same
1977# order, so a parent's value is read positionally and only the element's own
1978# attributes are searched by name.
1979type Prop is Data:
1980  Prop{key: String, initial: String, inherited: Bool}
1981
1982def props.spec() -> List<&2, Prop>:
1983  Con{Prop{"color", "black", True{}}, Con{Prop{"color-interpolation", "sRGB", True{}}, Con{Prop{"stop-color", "black", False{}}, Con{Prop{"stop-opacity", "1", False{}},
1984    Con{Prop{"fill", "black", True{}}, Con{Prop{"stroke", "none", True{}}, Con{Prop{"stroke-width", "1", True{}}, Con{Prop{"fill-opacity", "1", True{}},
1985    Con{Prop{"stroke-opacity", "1", True{}}, Con{Prop{"fill-rule", "nonzero", True{}}, Con{Prop{"clip-rule", "nonzero", True{}}, Con{Prop{"stroke-linecap", "butt", True{}},
1986    Con{Prop{"stroke-linejoin", "miter", True{}}, Con{Prop{"stroke-miterlimit", "4", True{}}, Con{Prop{"stroke-dasharray", "none", True{}}, Con{Prop{"stroke-dashoffset", "0", True{}},
1987    Con{Prop{"visibility", "visible", True{}}, Con{Prop{"opacity", "1", False{}}, Con{Prop{"image-rendering", "auto", True{}}, Con{Prop{"color-interpolation-filters", "linearRGB", True{}},
1988    Con{Prop{"flood-color", "black", False{}}, Con{Prop{"flood-opacity", "1", False{}}, Con{Prop{"marker-start", "none", True{}}, Con{Prop{"marker-mid", "none", True{}},
1989    Con{Prop{"marker-end", "none", True{}}, Nil{}}}}}}}}}}}}}}}}}}}}}}}}}}
1990
1991def props.head.pick(hit: Bool, +v: String, whole: List<&2, XML.Attr>, +key: String, +initial: String) -> String:
1992  match hit:
1993    case True{}: v
1994    case False{}: XML.attr.get(whole, key, initial)
1995
1996# The parent's value for key: the head when the parent list is resolved, else by name.
1997def props.head.value(parent: List<&2, XML.Attr>, +key: String, +initial: String) -> String:
1998  match parent:
1999    case Nil{}: initial
2000    case Con{XML.Attr{k, v}, t}:
2001      +kk = k
2002      +vv = v
2003      props.head.pick(U.string.equal(kk, key), vv, Con{XML.Attr{kk, vv}, t}, key, initial)
2004
2005def props.advance.pick(hit: Bool, whole: List<&2, XML.Attr>, rest: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
2006  match hit:
2007    case True{}: rest
2008    case False{}: whole
2009
2010def props.advance(parent: List<&2, XML.Attr>, +key: String) -> List<&2, XML.Attr>:
2011  match parent:
2012    case Nil{}: Nil{}
2013    case Con{XML.Attr{k, v}, t}:
2014      +kk = k
2015      +tt = t
2016      props.advance.pick(U.string.equal(kk, key), Con{XML.Attr{kk, v}, tt}, tt)
2017
2018def props.fold(spec: List<&2, Prop>, +parent: List<&2, XML.Attr>, +as: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
2019  match spec:
2020    case Nil{}: as
2021    case Con{Prop{key, initial, inherited}, rest}:
2022      +k = key
2023      +ini = initial
2024      Con{XML.Attr{k, style.value(as, k, props.head.value(parent, k, ini), ini, inherited)}, props.fold(rest, props.advance(parent, k), as)}
2025
2026# Referenced elements (gradients, patterns, markers, clip paths, masks,
2027# filters) get their inherited properties on demand: the raw tree is kept
2028# under a root element whose attributes are the root svg's, and a lookup
2029# by id resolves the properties down the ancestor chain of the element it
2030# finds. Nothing is resolved for the elements a document never references.
2031def resources(xs: List<&2, XML.Element>, +root: List<&2, XML.Attr>) -> List<&2, XML.Element>:
2032  Con{XML.Element{"#root", root, xs, ""}, Nil{}}
2033
2034# The subtree of a found element gets its properties resolved too, since its
2035# content (stops, primitives, pattern or marker content) inherits from it.
2036def resources.tree(xs: List<&2, XML.Element>, +parent: List<&2, XML.Attr>) -> List<&2, XML.Element>:
2037  match xs:
2038    case Nil{}: Nil{}
2039    case Con{XML.Element{n, attrs, kids, txt}, rest}:
2040      +props = props.fold(props.spec(), parent, CSS.properties(attrs))
2041      Con{XML.Element{n, props, resources.tree(kids, props), txt}, resources.tree(rest, parent)}
2042
2043# The properties of an element below a chain of raw ancestor attribute
2044# lists, outermost last.
2045def resource.resolve(chain: List<&2, List<&2, XML.Attr>>, own: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
2046  match chain:
2047    case Nil{}: own
2048    case Con{as, rest}: props.fold(props.spec(), resource.resolve(rest, as), own)
2049
2050def resource.found(+n: String, +props: List<&2, XML.Attr>, kids: List<&2, XML.Element>, +txt: String) -> Maybe<&2, XML.Element>:
2051  Some{XML.Element{n, props, resources.tree(kids, props), txt}}
2052
2053# A lookup runs rarely, so it branches through lambdas over copies.
2054def resource.search(xs: List<&2, XML.Element>, +id: String, +chain: List<&2, List<&2, XML.Attr>>) -> Maybe<&2, XML.Element>:
2055  match xs:
2056    case Nil{}: None{}
2057    case Con{XML.Element{n, as, kids, txt}, rest}:
2058      +nn = n
2059      +attrs = as
2060      +kk = kids
2061      +tt = txt
2062      U.branch(Maybe<&2, XML.Element>, U.string.equal(XML.attr.get(attrs, "id", ""), id),
2063        _ => resource.found(nn, resource.resolve(chain, CSS.properties(attrs)), kk, tt),
2064        _ => XML.element.prefer(U.branch(Maybe<&2, XML.Element>, U.string.equal(nn, "#svg-image"), _ => None{}, _ => resource.search(kk, id, Con{attrs, chain})),
2065          resource.search(rest, id, chain)))
2066
2067def resource.find(+all: List<&2, XML.Element>, +id: String) -> Maybe<&2, XML.Element>:
2068  resource.search(all, id, Nil{})
2069
2070# Gradient resources are compiled into paint functions before rasterization.
2071type Stop is Data:
2072  Stop{offset: F32, color: Img.Color}
2073
2074type Clip is Data:
2075  ClipAll{}
2076  ClipShape{edges: EdgeTree, inverse: Matrix, even: Bool, cover: Cover}
2077  ClipUnion{left: Clip, right: Clip}
2078  ClipIntersect{left: Clip, right: Clip}
2079
2080type Paint<-B: Data> is Data:
2081  Paint{fill: B, stroke: B}
2082
2083type FilterEffect is Data:
2084  NoFilter{}
2085  FilterSpec{region: Bounds, units: Matrix, viewport: Point, attrs: List<&2, XML.Attr>, primitives: List<&2, XML.Element>}
2086  FilterImage{raster: Raster}
2087
2088type Scene<-B: Data> is Data:
2089  Scene{edges: List<&2, Edge>, bounds: Bounds, inverse: Matrix, style: Style,
2090    opacity: F32, children: List<&2, Scene<B>>, id: String, paint: Paint<B>, clip: Clip, outline: StrokeMesh, fill: Fill, maskNodes: List<&2, Scene<B>>, maskMode: U32, markerNodes: List<&2, Scene<B>>, coverage: Bounds, effect: FilterEffect}
2091
2092type Brush is Data:
2093  Bitmap{raster: Raster, inverse: Matrix, nearest: Bool}
2094  Solid{value: Img.Color, source: Img.Color}
2095  PendingContext{stroke: Bool, opacity: F32}
2096  ContextPaint{source: Brush, matrix: Matrix, opacity: F32}
2097  Pattern{inverse: Matrix, width: F32, height: F32, opacity: F32, nodes: List<&2, Scene<Brush>>}
2098  Tile{raster: Raster, inverse: Matrix, width: F32, height: F32, opacity: F32, u: Point, v: Point, grid: U32}
2099  Gradient{inverse: Matrix, radial: Bool, start: Point, end: Point, radius: F32,
2100    focalRadius: F32, stops: List<&2, Stop>, spread: U32, opacity: F32, linear: Bool}
2101
2102def gradient.has.stops(xs: List<&2, XML.Element>) -> Bool:
2103  match xs:
2104    case Nil{}: False{}
2105    case Con{XML.Element{n, as, kids, txt}, rest}: U.string.equal(n, "stop") || gradient.has.stops(rest)
2106
2107def gradient.merge(local: XML.Element, template: XML.Element) -> XML.Element:
2108  XML.Element{n, as, kids, txt} = local
2109  XML.Element{tn, tas, tkids, ttxt} = template
2110  +children = kids
2111  XML.Element{n, XML.attrs.append(as, tas), U.choose(List<&2, XML.Element>, gradient.has.stops(children), children, tkids), txt}
2112
2113def gradient.reference(+node: XML.Element, result: Maybe<&2, XML.Element>, next: XML.Element -> XML.Element) -> XML.Element:
2114  match result:
2115    case None{}: node
2116    case Some{e}: gradient.merge(node, next(e))
2117
2118def gradient.resolve(depth: Nat, +node: XML.Element, +all: List<&2, XML.Element>) -> XML.Element:
2119  match depth:
2120    case 0n: node
2121    case 1n+d:
2122      XML.Element{n, as, kids, txt} = node
2123      +href = XML.attr.get(as, "href", XML.attr.get(as, "xlink:href", ""))
2124      U.branch(XML.Element, U.prefix(href, "#"),
2125        _ => gradient.reference(node, resource.find(all, U.drop(href, "#")), e => gradient.resolve(d, e, all)), _ => node)
2126
2127def srgb.linear(+v: F32) -> F32:
2128  U.branch(F32, F32.is_le(v, 0.04045), _ => (v / 12.92 : F32), _ => F32.pow(((v + 0.055) / 1.055 : F32), 2.4))
2129
2130def linear.srgb(+v: F32) -> F32:
2131  U.branch(F32, F32.is_le(v, 0.0031308), _ => (v * 12.92 : F32),
2132    _ => (1.055 * F32.pow(v, 0.4166666666666667) - 0.055 : F32))
2133
2134def stop.color(c: Img.Color, opacity: F32, +linear: Bool) -> Img.Color:
2135  Img.Color{r, g, b, a} = c
2136  +alpha = a
2137  +rr = (r / U.max(0.0000001, alpha) : F32)
2138  +gg = (g / U.max(0.0000001, alpha) : F32)
2139  +bb = (b / U.max(0.0000001, alpha) : F32)
2140  Img.Color{U.choose(F32, linear, srgb.linear(rr), rr), U.choose(F32, linear, srgb.linear(gg), gg),
2141    U.choose(F32, linear, srgb.linear(bb), bb), (alpha * opacity : F32)}
2142
2143def property.value(+as: List<&2, XML.Attr>, +parent: List<&2, XML.Attr>, +key: String, +initial: String, inherited: Bool) -> String:
2144  style.value(as, key, XML.attr.get(parent, key, initial), initial, inherited)
2145
2146def gradient.stops(xs: List<&2, XML.Element>, +as: List<&2, XML.Attr>, +previous: F32, +linear: Bool) -> List<&2, Stop>:
2147  match xs:
2148    case Nil{}: Nil{}
2149    case Con{XML.Element{n, attrs, kids, txt}, rest}:
2150      +props = CSS.properties(attrs)
2151      +offset = U.max(previous, U.clamp(U.unit(U.number(XML.attr.get(props, "offset", "0")), 1.0), 0.0, 1.0))
2152      +col = XML.attr.get(props, "stop-color", "black")
2153      +tail = rest
2154      U.branch(List<&2, Stop>, U.string.equal(n, "stop"),
2155        _ => Con{Stop{offset, stop.color(color(U.choose(String, U.string.equal(CSS.css.lower(U.trim.both(col)), "currentcolor"),
2156          XML.attr.get(props, "color", XML.attr.get(as, "color", "black")), col)), U.clamp(U.unit(U.number(XML.attr.get(props, "stop-opacity", "1")), 1.0), 0.0, 1.0), linear)},
2157          gradient.stops(tail, as, offset, linear)}, _ => gradient.stops(tail, as, previous, linear))
2158
2159def gradient.mix(+a: Img.Color, +b: Img.Color, +t: F32) -> Img.Color:
2160  color.add(color.fade(a, (1.0 - t : F32)), color.fade(b, t))
2161
2162def gradient.interval(xs: List<&2, Stop>, +previous: Stop, +t: F32) -> Img.Color:
2163  match xs:
2164    case Nil{}:
2165      Stop{o, c} = previous
2166      c
2167    case Con{Stop{o, c}, rest}:
2168      Stop{po, pc} = previous
2169      +offset = o
2170      +ink = c
2171      U.branch(Img.Color, F32.is_lt(t, offset),
2172        _ => gradient.mix(pc, ink, U.clamp(((t - po) / U.max(0.0000001, (offset - po : F32)) : F32), 0.0, 1.0)),
2173        _ => gradient.interval(rest, Stop{offset, ink}, t))
2174
2175def gradient.color(xs: List<&2, Stop>, +t: F32) -> Img.Color:
2176  match xs:
2177    case Nil{}: Img.clear()
2178    case Con{Stop{o, c}, rest}:
2179      +ink = c
2180      +offset = o
2181      U.branch(Img.Color, F32.is_lt(t, offset), _ => ink,
2182        _ => gradient.interval(rest, Stop{offset, ink}, t))
2183
2184def gradient.spread(+t: F32, +mode: U32) -> F32:
2185  +repeat = (t - F32.floor(t) : F32)
2186  +period = (t - 2.0 * F32.floor((t / 2.0 : F32)) : F32)
2187  U.choose(F32, U32.is_eq(mode, 1), repeat,
2188    U.choose(F32, U32.is_eq(mode, 2), (1.0 - F32.abs((period - 1.0 : F32)) : F32), U.clamp(t, 0.0, 1.0)))
2189
2190def gradient.finish(c: Img.Color, opacity: F32, +linear: Bool) -> Img.Color:
2191  Img.Color{r, g, b, a} = c
2192  +rr = r
2193  +gg = g
2194  +bb = b
2195  Img.rgba(U.choose(F32, linear, linear.srgb(rr), rr), U.choose(F32, linear, linear.srgb(gg), gg),
2196    U.choose(F32, linear, linear.srgb(bb), bb), (a * opacity : F32))
2197
2198type Parameter is Data:
2199  Parameter{value: F32, valid: Bool}
2200
2201def radial.roots(+a: F32, +b: F32, +c: F32, +fr: F32, +dr: F32) -> Parameter:
2202  +disc = (b * b - 4.0 * a * c : F32)
2203  +root = F32.sqrt(U.max(0.0, disc))
2204  +t0 = ((0.0 - b - root) / (2.0 * a) : F32)
2205  +t1 = ((0.0 - b + root) / (2.0 * a) : F32)
2206  +t = U.max(t0, t1)
2207  Parameter{t, F32.is_ge(disc, 0.0) && F32.is_ge((fr + t * dr : F32), 0.0)}
2208
2209def radial.parameter(+p: Point, +start: Point, +end: Point, +r: F32, +fr: F32) -> Parameter:
2210  +q = point.sub(p, start)
2211  +d = point.sub(end, start)
2212  +dr = (r - fr : F32)
2213  +a = (point.dot(d, d) - dr * dr : F32)
2214  +b = (0.0 - 2.0 * (point.dot(q, d) + fr * dr) : F32)
2215  +c = (point.dot(q, q) - fr * fr : F32)
2216  U.branch(Parameter, F32.is_lt(r, 0.0000001), _ => Parameter{1.0, True{}},
2217    _ => U.branch(Parameter, F32.is_lt(F32.abs(a), 0.0000001),
2218      _ => Parameter{(0.0 - c / b : F32), F32.is_gt(F32.abs(b), 0.0000001)},
2219      _ => radial.roots(a, b, c, fr, dr)))
2220
2221def linear.parameter(+p: Point, +start: Point, +end: Point) -> Parameter:
2222  +d = point.sub(end, start)
2223  +den = point.dot(d, d)
2224  Parameter{U.choose(F32, F32.is_lt(den, 0.0000000001), 1.0,
2225    (point.dot(point.sub(p, start), d) / den : F32)), True{}}
2226
2227def gradient.at(param: Parameter, stops: List<&2, Stop>, spread: U32, opacity: F32, linear: Bool) -> Img.Color:
2228  Parameter{t, valid} = param
2229  U.branch(Img.Color, valid, _ => gradient.finish(gradient.color(stops, gradient.spread(t, spread)), opacity, linear), _ => Img.clear())
2230
2231# Wrap coverage and paint coordinates by the same tile, including negative tiles.
2232def pattern.at(p: Point, +paintPoint: Point, +w: F32, +h: F32, opacity: F32,
2233  nodes: List<&2, Scene<Brush>>, paint: List<&2, Scene<Brush>> -> Point -> Point -> Img.Color) -> Img.Color:
2234  Point{x, y} = p
2235  +px = x
2236  +py = y
2237  +shift = {Point{(F32.floor((px / w : F32)) * w : F32), (F32.floor((py / h : F32)) * h : F32)} : Point}
2238  color.fade(paint(nodes)(point.sub(Point{px, py}, shift))(point.sub(paintPoint, shift)), opacity)
2239
2240def brush.solid(+c: Img.Color, opacity: F32) -> Brush:
2241  Solid{color.fade(c, opacity), c}
2242
2243def color.space(+c: Img.Color, linear: Bool) -> Img.Color:
2244  Img.Color{r, g, b, a} = c
2245  U.branch(Img.Color, linear,
2246    _ => Img.Color{(srgb.linear((r / U.max(a, 0.0000001) : F32)) * a : F32),
2247      (srgb.linear((g / U.max(a, 0.0000001) : F32)) * a : F32),
2248      (srgb.linear((b / U.max(a, 0.0000001) : F32)) * a : F32), a}, _ => c)
2249
2250def raster.pixel(+r: Raster, +x: F32, +y: F32) -> Img.Color:
2251  Raster{origin, step, width, height, depth, pixelsLinear, pixels} = r
2252  U.branch(Img.Color, F32.is_ge(x, 0.0) && F32.is_ge(y, 0.0) && F32.is_lt(x, U32.to_f32(width)) && F32.is_lt(y, U32.to_f32(height)),
2253    _ => Img.pixels.get(pixels, U32.shln(1, depth), F32.to_u32(x), F32.to_u32(y)), _ => Img.clear())
2254
2255def color.mix(a: Img.Color, b: Img.Color, +t: F32) -> Img.Color:
2256  color.add(color.fade(a, (1.0 - t : F32)), color.fade(b, t))
2257
2258def raster.horizontal(+r: Raster, +x: F32, +y: F32, +dx: F32) -> Img.Color:
2259  U.branch(Img.Color, F32.is_eq(dx, 0.0), _ => raster.pixel(r, x, y),
2260    _ => color.mix(raster.pixel(r, x, y), raster.pixel(r, (x + 1.0 : F32), y), dx))
2261
2262def raster.sample(+r: Raster, p: Point) -> Img.Color:
2263  Raster{Point{ox, oy}, step, width, height, depth, pixelsLinear, pixels} = r
2264  Point{x, y} = p
2265  +fx = ((x - ox) / step - 0.5 : F32)
2266  +fy = ((y - oy) / step - 0.5 : F32)
2267  +ix = F32.floor(fx)
2268  +iy = F32.floor(fy)
2269  +dx = (fx - ix : F32)
2270  +dy = (fy - iy : F32)
2271  U.branch(Img.Color, F32.is_eq(dy, 0.0), _ => raster.horizontal(r, ix, iy, dx),
2272    _ => color.mix(raster.horizontal(r, ix, iy, dx), raster.horizontal(r, ix, (iy + 1.0 : F32), dx), dy))
2273
2274def color.from.linear(+c: Img.Color) -> Img.Color:
2275  Img.Color{r, g, b, a} = c
2276  Img.Color{(linear.srgb((r / U.max(a, 0.0000001) : F32)) * a : F32), (linear.srgb((g / U.max(a, 0.0000001) : F32)) * a : F32), (linear.srgb((b / U.max(a, 0.0000001) : F32)) * a : F32), a}
2277
2278def raster.color(+r: Raster, p: Point, +linear: Bool) -> Img.Color:
2279  Raster{origin, step, width, height, depth, pixelsLinear, pixels} = r
2280  +c = raster.sample(r, p)
2281  U.branch(Img.Color, U.choose(Bool, pixelsLinear, linear, Bool.not(linear)), _ => c,
2282    _ => U.branch(Img.Color, pixelsLinear, _ => color.from.linear(c), _ => color.space(c, True{})))
2283
2284def raster.image(image: Img.Picture) -> Raster:
2285  Img.Picture{w, h, depth, pixels} = image
2286  Raster{Point{0.0, 0.0}, 1.0, w, h, depth, False{}, pixels}
2287
2288def bitmap.bytes(+mime: String, +data: String) -> Bin.Bytes:
2289  U.branch(Bin.Bytes, U.string.contains(mime, ";base64"), _ => Bin.base64(data), _ => Bin.uri.bytes(data, Bin.bytes.empty()))
2290
2291def bitmap.empty() -> Raster:
2292  Raster{Point{0.0, 0.0}, 1.0, 0, 0, 0n, False{}, Img.Flat{Img.clear()}}
2293
2294def bitmap.uri(scan: U.Scan) -> Raster:
2295  U.Scan{header, content} = scan
2296  +mime = CSS.css.lower(header)
2297  +data = content
2298  U.branch(Raster, U.prefix(mime, "image/png") || U.prefix(mime, "image/apng"),
2299    _ => raster.image(PNG.png.decode(bitmap.bytes(mime, data))), _ => U.branch(Raster, U.prefix(mime, "image/jpeg") || U.prefix(mime, "image/jpg"), _ => raster.image(JPEG.jpeg.decode(bitmap.bytes(mime, data))), _ => bitmap.empty()))
2300
2301def bitmap.decode(+href: String) -> Raster:
2302  U.branch(Raster, U.prefix(href, "data:"), _ => bitmap.uri(U.until(U.drop(href, "data:"), 44, "")), _ => bitmap.empty())
2303
2304# Inverse F32 transforms can place an exact texel boundary a few ulps above
2305# its integer value. Snap only that rounding neighborhood before tie-breaking.
2306def bitmap.nearest(+q: F32) -> F32:
2307  +integer = F32.floor((q + 0.5 : F32))
2308  +close = F32.is_le(F32.abs((q - integer : F32)), (U.max(1.0, F32.abs(q)) * 0.000000476837158203125 : F32))
2309  F32.ceil((U.choose(F32, close, integer, q) - 1.0 : F32))
2310
2311def bitmap.sample(+raster: Raster, +p: Point, +center: Point, nearest: Bool) -> Img.Color:
2312  Raster{origin, step, width, height, depth, linear, pixels} = raster
2313  Point{x, y} = p
2314  Point{cx, cy} = center
2315  +w = U32.to_f32(width)
2316  +h = U32.to_f32(height)
2317  U.branch(Img.Color, nearest, _ => raster.pixel(raster, U.clamp(bitmap.nearest(cx), 0.0, (w - 1.0 : F32)), U.clamp(bitmap.nearest(cy), 0.0, (h - 1.0 : F32))),
2318    _ => raster.sample(raster, Point{U.clamp(cx, 0.5, (w - 0.5 : F32)), U.clamp(cy, 0.5, (h - 0.5 : F32))}))
2319
2320# Pattern tiles (see cover.brush): wrap into the tile and read its raster
2321# bilinearly, wrapping at the seams so neighbouring tiles join without a border.
2322def tile.wrap(+q: F32, +size: F32) -> F32:
2323  (q - F32.floor((q / size : F32)) * size : F32)
2324
2325def tile.index(+i: F32, +n: U32) -> U32:
2326  U32.mod(F32.to_u32((i + U32.to_f32(n) : F32)), n)
2327
2328def tile.corner(+pixels: Img.Pixels, +side: U32, +x: U32, +y: U32, +weight: F32) -> Img.Color:
2329  color.fade(Img.pixels.get(pixels, side, x, y), weight)
2330
2331def tile.blend(+pixels: Img.Pixels, +side: U32, +width: U32, +height: U32, +fx: F32, +fy: F32) -> Img.Color:
2332  +ix = F32.floor(fx)
2333  +iy = F32.floor(fy)
2334  +dx = (fx - ix : F32)
2335  +dy = (fy - iy : F32)
2336  +x0 = tile.index(ix, width)
2337  +x1 = tile.index((ix + 1.0 : F32), width)
2338  +y0 = tile.index(iy, height)
2339  +y1 = tile.index((iy + 1.0 : F32), height)
2340  color.add(color.add(tile.corner(pixels, side, x0, y0, ((1.0 - dx) * (1.0 - dy) : F32)), tile.corner(pixels, side, x1, y0, (dx * (1.0 - dy) : F32))),
2341    color.add(tile.corner(pixels, side, x0, y1, ((1.0 - dx) * dy : F32)), tile.corner(pixels, side, x1, y1, (dx * dy : F32))))
2342
2343def tile.cell(+x: F32, +w: F32, +n: U32) -> U32:
2344  U32.mod(F32.to_u32(F32.floor((tile.wrap(x, w) * U32.to_f32(n) / w : F32))), n)
2345
2346# Nearest tile pixel under a pattern-space point.
2347def tile.read(+pixels: Img.Pixels, +side: U32, +wd: U32, +ht: U32, +w: F32, +h: F32, q: Point) -> Img.Color:
2348  Point{x, y} = q
2349  Img.pixels.get(pixels, side, tile.cell(x, w, wd), tile.cell(y, h, ht))
2350
2351# Sum of grid by grid nearest reads spread over the pixel footprint (u, v).
2352@unsafe
2353def tile.grid(+pixels: Img.Pixels, +side: U32, +wd: U32, +ht: U32, +w: F32, +h: F32, +q: Point, +u: Point, +v: Point, +grid: U32, n: Nat, +i: U32, acc: Img.Color) -> Img.Color:
2354  match n:
2355    case 0n: acc
2356    case 1n+d:
2357      +gx = ((U32.to_f32(U32.mod(i, grid)) + 0.5) / U32.to_f32(grid) - 0.5 : F32)
2358      +gy = ((U32.to_f32(U32.div(i, grid)) + 0.5) / U32.to_f32(grid) - 0.5 : F32)
2359      tile.grid(pixels, side, wd, ht, w, h, q, u, v, grid, d, (i + 1 : U32), color.add(acc, tile.read(pixels, side, wd, ht, w, h, point.add(q, point.add(point.scale(u, gx), point.scale(v, gy))))))
2360
2361# A grid of one reads the tile bilinearly at the point (aligned tiles at device
2362# density); a larger grid averages nearest reads of a denser tile over the
2363# device pixel's footprint, the pattern-space vectors u and v.
2364def tile.sample(+r: Raster, +q: Point, +w: F32, +h: F32, +opacity: F32, +u: Point, +v: Point, +grid: U32) -> Img.Color:
2365  Raster{origin, step, width, height, depth, linear, pixels} = r
2366  Point{x, y} = q
2367  +wd = width
2368  +ht = height
2369  +px = pixels
2370  +side = U32.shln(1, depth)
2371  U.branch(Img.Color, U32.is_eq(wd, 0) || U32.is_eq(ht, 0), _ => Img.clear(),
2372    _ => color.fade(U.branch(Img.Color, U32.is_le(grid, 1),
2373      _ => tile.blend(px, side, wd, ht, (tile.wrap(x, w) * U32.to_f32(wd) / w - 0.5 : F32), (tile.wrap(y, h) * U32.to_f32(ht) / h - 0.5 : F32)),
2374      _ => color.fade(tile.grid(px, side, wd, ht, w, h, Point{x, y}, u, v, grid, U32.to_nat((grid * grid : U32)), 0, Img.clear()), (1.0 / U32.to_f32((grid * grid : U32)) : F32))), opacity))
2375
2376def brush.sample(brush: Brush, +p: Point, +paintPoint: Point, next: List<&2, Scene<Brush>> -> Point -> Point -> Img.Color) -> Img.Color:
2377  match brush:
2378    case Solid{c, source}: c
2379    case Bitmap{raster, inv, nearest}:
2380      +mat = inv
2381      bitmap.sample(raster, matrix.point(mat, p), matrix.point(mat, paintPoint), nearest)
2382    case PendingContext{stroke, opacity}: Img.clear()
2383    case ContextPaint{source, matrix, opacity}:
2384      +mat = matrix
2385      color.fade(brush.sample(source, matrix.point(mat, p), matrix.point(mat, paintPoint), next), opacity)
2386    case Pattern{inv, w, h, opacity, nodes}:
2387      +matrix = inv
2388      pattern.at(matrix.point(matrix, p), matrix.point(matrix, paintPoint), w, h, opacity, nodes, next)
2389    case Tile{raster, inv, w, h, opacity, u, v, grid}: tile.sample(raster, matrix.point(inv, p), w, h, opacity, u, v, grid)
2390    case Gradient{inv, radial, start, end, radius, fr, stops, spread, opacity, linear}:
2391      +q = matrix.point(inv, paintPoint)
2392      +a = start
2393      +b = end
2394      gradient.at(U.choose(Parameter, radial, radial.parameter(q, a, b, radius, fr), linear.parameter(q, a, b)),
2395        stops, spread, opacity, linear)
2396
2397def gradient.box(b: Bounds) -> Matrix:
2398  Bounds{x0, y0, x1, y1} = b
2399  +x = x0
2400  +y = y0
2401  matrix.mul(translate(x, y), scale((x1 - x : F32), (y1 - y : F32)))
2402
2403def gradient.build(e: XML.Element, +box: Bounds, +w: F32, +h: F32, opacity: F32) -> Brush:
2404  XML.Element{n, attrs, kids, txt} = e
2405  +as = CSS.properties(attrs)
2406  +user = U.string.equal(XML.attr.get(as, "gradientUnits", "objectBoundingBox"), "userSpaceOnUse")
2407  +vw = U.choose(F32, user, w, 1.0)
2408  +vh = U.choose(F32, user, h, 1.0)
2409  +linear = U.string.equal(XML.attr.get(as, "color-interpolation", "sRGB"), "linearRGB")
2410  +radial = U.string.equal(n, "radialGradient")
2411  +cx = dimension(as, "cx", vw, "50%")
2412  +cy = dimension(as, "cy", vh, "50%")
2413  +radiusRef = F32.sqrt(((vw * vw + vh * vh) / 2.0 : F32))
2414  Gradient{matrix.inverse(matrix.mul(U.choose(Matrix, user, identity(), gradient.box(box)),
2415    transform(XML.attr.get(as, "gradientTransform", ""), identity()))), radial,
2416    U.choose(Point, radial, Point{dimension(as, "fx", vw, F32.show(cx)), dimension(as, "fy", vh, F32.show(cy))},
2417      Point{dimension(as, "x1", vw, "0%"), dimension(as, "y1", vh, "0%")}),
2418    U.choose(Point, radial, Point{cx, cy}, Point{dimension(as, "x2", vw, "100%"), dimension(as, "y2", vh, "0%")}),
2419    dimension(as, "r", radiusRef, "50%"), dimension(as, "fr", radiusRef, "0%"),
2420    gradient.stops(kids, as, 0.0, linear),
2421    U.choose(U32, U.string.equal(XML.attr.get(as, "spreadMethod", "pad"), "repeat"), 1,
2422      U.choose(U32, U.string.equal(XML.attr.get(as, "spreadMethod", "pad"), "reflect"), 2, 0)), opacity, linear}
2423
2424def gradient.is.valid(e: XML.Element) -> Bool:
2425  XML.Element{n, as, kids, txt} = e
2426  +tag = n
2427  U.string.equal(tag, "linearGradient") || U.string.equal(tag, "radialGradient")
2428
2429def brush.resource(result: Maybe<&2, XML.Element>, +all: List<&2, XML.Element>, +box: Bounds, +w: F32, +h: F32,
2430  +opacity: F32, fallback: String, patterns: XML.Element -> Bounds -> F32 -> F32 -> F32 -> Brush) -> Brush:
2431  match result:
2432    case None{}: brush.solid(color(fallback), opacity)
2433    case Some{XML.Element{tagname, as, kids, txt}}:
2434      +tag = tagname
2435      +node = {XML.Element{tag, as, kids, txt} : XML.Element}
2436      U.branch(Brush, gradient.is.valid(node), _ => gradient.build(gradient.resolve(32n, node, all), box, w, h, opacity),
2437        _ => U.branch(Brush, U.string.equal(tag, "pattern"), _ => patterns(node)(box)(w)(h)(opacity),
2438          _ => brush.solid(color(fallback), opacity)))
2439
2440def brush.url(+scan: U.Scan, +all: List<&2, XML.Element>, box: Bounds, w: F32, h: F32, opacity: F32, patterns: XML.Element -> Bounds -> F32 -> F32 -> F32 -> Brush) -> Brush:
2441  U.Scan{url, fallback} = scan
2442  brush.resource(resource.find(all, U.drop(U.unquote(U.trim.both(url)), "#")), all, box, w, h, opacity,
2443    U.choose(String, U.string.equal(U.trim(fallback), ""), "none", U.trim(fallback)), patterns)
2444
2445# A paint's keyword, decided from its first letter: 1 currentcolor,
2446# 2 context-fill, 3 context-stroke, 0 a color or none.
2447def paint.keyword.head(s: String) -> U32:
2448  match s:
2449    case SNil{}: 0
2450    case SCon{Chr{c}, t}:
2451      +ch = c
2452      +rest = t
2453      U.choose(U32, U32.is_eq(U32.or(ch, 32), 99), U.choose(U32, keyword.rest("urrentcolor", rest), 1, U.choose(U32, keyword.rest("ontext-fill", rest), 2, U.choose(U32, keyword.rest("ontext-stroke", rest), 3, 0))), 0)
2454
2455def paint.keyword(s: String) -> U32:
2456  paint.keyword.head(U.trim(s))
2457
2458def brush.keyword(+k: U32, +s: String, current: String, +opacity: F32) -> Brush:
2459  U.choose(Brush, U32.is_eq(k, 2) || U32.is_eq(k, 3), PendingContext{U32.is_eq(k, 3), opacity},
2460    brush.solid(color(U.choose(String, U32.is_eq(k, 1), current, s)), opacity))
2461
2462def brush(+s: String, current: String, +opacity: F32, +all: List<&2, XML.Element>, box: Bounds, w: F32, h: F32, patterns: XML.Element -> Bounds -> F32 -> F32 -> F32 -> Brush) -> Brush:
2463  U.branch(Brush, U.prefix(s, "url("), _ => brush.url(U.until(U.drop(s, "url("), 41, ""), all, box, w, h, opacity, patterns),
2464    _ => brush.keyword(paint.keyword(s), s, current, opacity))
2465
2466def style.colors(+s: Style, +all: List<&2, XML.Element>, +box: Bounds, +w: F32, +h: F32, fills: XML.Element -> Bounds -> F32 -> F32 -> F32 -> Brush, strokes: XML.Element -> Bounds -> F32 -> F32 -> F32 -> Brush) -> Paint<Brush>:
2467  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
2468  Paint{brush(fill, col, fo, all, box, w, h, fills), brush(stroke, col, so, all, box, w, h, strokes)}
2469
2470# Clip paths combine geometry by union; nested clip paths intersect coverage.
2471def clip.inside(c: Clip, +p: Point) -> Bool:
2472  match c:
2473    case ClipAll{}: True{}
2474    case ClipShape{es, inv, even, cover}: edge.tree.inside(es, matrix.point(inv, p), even)
2475    case ClipUnion{a, b}: clip.inside(a, p) || clip.inside(b, p)
2476    case ClipIntersect{a, b}: clip.inside(a, p) && clip.inside(b, p)
2477
2478# Clip coverage: unions add as independent areas, intersections multiply.
2479def clip.cover(c: Clip, +p: Point) -> F32:
2480  match c:
2481    case ClipAll{}: 1.0
2482    case ClipShape{es, inv, even, cover}:
2483      +cov = cover
2484      U.branch(F32, cover.present(cov), _ => cover.at(cov, p), _ => U.choose(F32, edge.tree.inside(es, matrix.point(inv, p), even), 1.0, 0.0))
2485    case ClipUnion{a, b}:
2486      +u = clip.cover(a, p)
2487      +v = clip.cover(b, p)
2488      (u + v - u * v : F32)
2489    case ClipIntersect{a, b}: (clip.cover(a, p) * clip.cover(b, p) : F32)
2490
2491def clip.fade(+cov: F32, render: Unit -> Img.Color) -> Img.Color:
2492  U.branch(Img.Color, F32.is_gt(cov, 0.0), _ => color.fade(render(Unit{}), cov), _ => Img.clear())
2493
2494def clip.empty() -> Clip:
2495  ClipShape{edge.tree.empty(), identity(), False{}, cover.none()}
2496
2497def clip.reference(result: Maybe<&2, XML.Element>, +box: Bounds, +m: Matrix,
2498  build: List<&2, XML.Element> -> Matrix -> Clip) -> Clip:
2499  match result:
2500    case None{}: ClipAll{}
2501    case Some{XML.Element{n, as, kids, txt}}:
2502      +tag = n
2503      +attrs = as
2504      U.branch(Clip, U.string.equal(tag, "clipPath"),
2505        _ => build(Con{XML.Element{tag, attrs, kids, txt}, Nil{}})(matrix.mul(m,
2506          U.choose(Matrix, U.string.equal(XML.attr.get(attrs, "clipPathUnits", "userSpaceOnUse"), "objectBoundingBox"),
2507            gradient.box(box), identity()))), _ => ClipAll{})
2508
2509def clip.attach(+as: List<&2, XML.Attr>, +all: List<&2, XML.Element>, box: Bounds, m: Matrix,
2510  build: List<&2, XML.Element> -> Matrix -> Clip) -> Clip:
2511  +value = XML.attr.get(as, "clip-path", "none")
2512  U.branch(Clip, U.prefix(value, "url("),
2513    _ => clip.reference(resource.find(all, U.drop(U.unquote(U.trim.both(U.scan.head(U.until(U.drop(value, "url("), 41, "")))), "#")),
2514      box, m, build), _ => ClipAll{})
2515
2516@unsafe
2517def clip.build(fuel: Nat, +xs: List<&2, XML.Element>, +all: List<&2, XML.Element>, +m: Matrix,
2518  +w: F32, +h: F32, +even: Bool, +scale: F32) -> Clip:
2519  match fuel:
2520    case 0n: clip.empty()
2521    case 1n+d:
2522      match xs:
2523        case Nil{}: clip.empty()
2524        case Con{XML.Element{n, attrs, kids, txt}, rest}:
2525          +dd = d
2526          +as = CSS.properties(attrs)
2527          +matrix = matrix.mul(matrix.mul(m, transform(XML.attr.get(as, "transform", ""), identity())),
2528            U.choose(Matrix, U.string.equal(n, "use"), translate(dimension(as, "x", w, "0"), dimension(as, "y", h, "0")), identity()))
2529          +rule = U.string.equal(XML.attr.get(as, "clip-rule", U.choose(String, even, "evenodd", "nonzero")), "evenodd")
2530          +es = shape.children(n, as, kids, w, h, local.tolerance(matrix, scale), XML.attr.get(as, "d", ""))
2531          +box = bounds(es, Bounds{1000000000.0, 1000000000.0, F32.neg(1000000000.0), F32.neg(1000000000.0)})
2532          U.branch(Clip, U.string.equal(XML.attr.get(as, "display", "inline"), "none"),
2533            _ => clip.build(1n+dd, rest, all, m, w, h, even, scale),
2534            _ => ClipUnion{ClipIntersect{
2535              ClipUnion{ClipShape{edge.tree(es), matrix.inverse(matrix), rule, cover.none()}, clip.build(1n+dd, kids, all, matrix, w, h, rule, scale)},
2536              clip.attach(as, all, box, matrix, nodes => mat => clip.build(dd, nodes, all, mat, w, h, False{}, scale))},
2537              clip.build(1n+dd, rest, all, m, w, h, even, scale)})
2538
2539def clip.compile(as: List<&2, XML.Attr>, +all: List<&2, XML.Element>, box: Bounds, m: Matrix, +w: F32, +h: F32, +scale: F32) -> Clip:
2540  clip.attach(as, all, box, m, nodes => mat => clip.build(128n, nodes, all, mat, w, h, False{}, scale))
2541
2542def style.nonScaling(s: Style) -> Bool:
2543  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
2544  StrokeSettings{cap, join, limit, dash, offset, vector} = ss
2545  U.string.equal(vector, "non-scaling-stroke")
2546
2547def style.outline(+s: Style, +es: List<&2, Edge>, +m: Matrix, ref: F32, declared: F32) -> StrokeMesh:
2548  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
2549  +screen = style.nonScaling(s)
2550  stroke.mesh(U.branch(List<&2, Edge>, U.string.equal(stroke, "none") || F32.is_le(width, 0.0), _ => Nil{},
2551    _ => stroke.compile(U.choose(List<&2, Edge>, screen, edges.transform(es, m), es), (width / 2.0 : F32), ss, ref, declared, U.choose(F32, screen, 1.0, matrix.norm(m)))),
2552    U.choose(Matrix, screen, identity(), matrix.inverse(m)))
2553
2554def bounds.transformed(es: List<&2, Edge>, +m: Matrix, box: Bounds) -> Bounds:
2555  match es:
2556    case Nil{}: box
2557    case Con{Edge{ax, ay, bx, by, stroked, closed}, rest}:
2558      bounds.transformed(rest, m, bounds.point(bounds.point(box, matrix.point(m, Point{ax, ay})), matrix.point(m, Point{bx, by})))
2559
2560# These bounds enclose rendered coverage, including descendant strokes and
2561# markers. They are distinct from objectBoundingBox paint coordinates.
2562def bounds.finite(b: Bounds) -> Bool:
2563  Bounds{x0, y0, x1, y1} = b
2564  F32.is_lt(F32.abs(x0), 1.0e25) && F32.is_lt(F32.abs(y0), 1.0e25) && F32.is_lt(F32.abs(x1), 1.0e25) && F32.is_lt(F32.abs(y1), 1.0e25)
2565
2566def bounds.safe(+box: Bounds) -> Bounds:
2567  U.choose(Bounds, bounds.finite(box), box, Bounds{F32.neg(1.0e30), F32.neg(1.0e30), 1.0e30, 1.0e30})
2568
2569def bounds.transform(box: Bounds, +mat: Matrix) -> Bounds:
2570  Bounds{x0, y0, x1, y1} = box
2571  +left = x0
2572  +top = y0
2573  +right = x1
2574  +bottom = y1
2575  # Include the local stroke tests' tolerances before transforming the corners.
2576  +a = (left - 0.0001 : F32)
2577  +b = (top - 0.0001 : F32)
2578  +c = (right + 0.0001 : F32)
2579  +d = (bottom + 0.0001 : F32)
2580  U.branch(Bounds, F32.is_gt(left, right) || F32.is_gt(top, bottom), _ => bounds.empty(),
2581    _ => bounds.safe(bounds.point(bounds.point(bounds.point(bounds.point(bounds.empty(), matrix.point(mat, Point{a, b})), matrix.point(mat, Point{c, b})), matrix.point(mat, Point{a, d})), matrix.point(mat, Point{c, d}))))
2582
2583# A child's box already holds its own children, so the parent's box is the
2584# union of the children's boxes brought into its space.
2585def scene.bounds(xs: List<&2, Scene<Brush>>, +parentInverse: Matrix, box: Bounds) -> Bounds:
2586  match xs:
2587    case Nil{}: box
2588    case Con{Scene{es, childbox, inv, style, opacity, kids, id, ink, mask, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect}, rest}:
2589      scene.bounds(rest, parentInverse, bounds.merge(box, bounds.transform(childbox, matrix.mul(parentInverse, matrix.inverse(inv)))))
2590
2591def bounds.spec(+as: List<&2, XML.Attr>, box: Bounds) -> Bounds:
2592  +value = XML.attr.get(as, "#paint-box", "")
2593  +vs = U.numbers(value)
2594  U.choose(Bounds, U.string.equal(value, ""), box, Bounds{U.nth(vs, 0n, 0.0), U.nth(vs, 1n, 0.0), U.nth(vs, 2n, 0.0), U.nth(vs, 3n, 0.0)})
2595
2596def filter.dimension(+as: List<&2, XML.Attr>, +key: String, +ref: F32, +fallback: String, user: Bool) -> F32:
2597  U.branch(F32, user, _ => dimension(as, key, ref, fallback), _ => (dimension(as, key, 1.0, fallback) * ref : F32))
2598
2599def filter.region(+as: List<&2, XML.Attr>, box: Bounds, +w: F32, +h: F32, +primitiveBox: Bool) -> Bounds:
2600  Bounds{x0, y0, x1, y1} = box
2601  +bx = x0
2602  +by = y0
2603  +bw = (x1 - bx : F32)
2604  +bh = (y1 - by : F32)
2605  +user = U.string.equal(XML.attr.get(as, "filterUnits", "objectBoundingBox"), "userSpaceOnUse")
2606  +fw = U.choose(F32, user, w, bw)
2607  +fh = U.choose(F32, user, h, bh)
2608  +x = (U.choose(F32, user, 0.0, bx) + filter.dimension(as, "x", fw, "-10%", user) : F32)
2609  +y = (U.choose(F32, user, 0.0, by) + filter.dimension(as, "y", fh, "-10%", user) : F32)
2610  +width = filter.dimension(as, "width", fw, "120%", user)
2611  +height = filter.dimension(as, "height", fh, "120%", user)
2612  U.branch(Bounds, (user || (F32.is_gt(bw, 0.0) && F32.is_gt(bh, 0.0))) && (Bool.not(primitiveBox) || (F32.is_gt(bw, 0.0) && F32.is_gt(bh, 0.0))),
2613    _ => U.branch(Bounds, primitiveBox, _ => Bounds{((x - bx) / bw : F32), ((y - by) / bh : F32), ((x + width - bx) / bw : F32), ((y + height - by) / bh : F32)},
2614      _ => Bounds{x, y, (x + width : F32), (y + height : F32)}), _ => bounds.empty())
2615
2616def filter.resource(result: Maybe<&2, XML.Element>, +box: Bounds, m: Matrix, +w: F32, +h: F32) -> FilterEffect:
2617  match result:
2618    case None{}: NoFilter{}
2619    case Some{XML.Element{name, attrs, kids, txt}}:
2620      +as = CSS.properties(attrs)
2621      +object = U.string.equal(XML.attr.get(as, "primitiveUnits", "userSpaceOnUse"), "objectBoundingBox")
2622      U.branch(FilterEffect, U.string.equal(name, "filter"), _ => FilterSpec{filter.region(as, box, w, h, object), matrix.mul(m, U.choose(Matrix, object, gradient.box(box), identity())), Point{U.choose(F32, object, 1.0, w), U.choose(F32, object, 1.0, h)}, as, kids}, _ => NoFilter{})
2623
2624def filter.spec(+as: List<&2, XML.Attr>, all: List<&2, XML.Element>, box: Bounds, m: Matrix, w: F32, h: F32) -> FilterEffect:
2625  +value = U.trim.both(XML.attr.get(as, "filter", "none"))
2626  +id = U.drop(U.unquote(U.trim.both(U.scan.head(U.until(U.drop(value, "url("), 41, "")))), "#")
2627  U.branch(FilterEffect, U.prefix(value, "url("), _ => filter.resource(resource.find(all, id), box, m, w, h), _ => NoFilter{})
2628
2629def scene.make(+es: List<&2, Edge>, +m: Matrix, +s: Style, opacity: F32, +children: List<&2, Scene<Brush>>, id: String,
2630  +all: List<&2, XML.Element>, +w: F32, +h: F32, +as: List<&2, XML.Attr>, fills: XML.Element -> Bounds -> F32 -> F32 -> F32 -> Brush, strokes: XML.Element -> Bounds -> F32 -> F32 -> F32 -> Brush, +scale: F32) -> Scene<Brush>:
2631  +f = fill.new(es)
2632  +box = bounds.spec(as, scene.bounds(children, matrix.inverse(m), fill.bounds(f)))
2633  Scene{es, box, matrix.inverse(m), s, opacity, children, id, style.colors(s, all, box, w, h, fills, strokes), clip.compile(as, all, box, m, w, h, scale), style.outline(s, es, m, F32.sqrt(((w * w + h * h) / 2.0 : F32)), U.scalar(XML.attr.get(as, "pathLength", "0"), 0.0)), f, Nil{}, 0, Nil{}, bounds.empty(), filter.spec(as, all, box, m, w, h)}
2634
2635# SVG viewports establish local units and intersect their children with overflow clips.
2636def viewbox.matrix(+v: List<&2, F32>, +w: F32, +h: F32, +aspect: String) -> Matrix:
2637  +x = U.nth(v, 0n, 0.0)
2638  +y = U.nth(v, 1n, 0.0)
2639  +vw = U.nth(v, 2n, w)
2640  +vh = U.nth(v, 3n, h)
2641  +sx = (w / U.max(0.000001, vw) : F32)
2642  +sy = (h / U.max(0.000001, vh) : F32)
2643  +s = U.choose(F32, U.prefix(U.through(aspect, " "), "slice"), U.max(sx, sy), U.min(sx, sy))
2644  +tx = U.choose(F32, U.prefix(aspect, "xMin"), 0.0, U.choose(F32, U.prefix(aspect, "xMax"), 1.0, 0.5))
2645  +ty = U.choose(F32, U.prefix(U.drop(aspect, "xxxx"), "YMin"), 0.0,
2646    U.choose(F32, U.prefix(U.drop(aspect, "xxxx"), "YMax"), 1.0, 0.5))
2647  U.branch(Matrix, U.string.equal(aspect, "none"), _ => matrix.mul(scale(sx, sy), translate(F32.neg(x), F32.neg(y))),
2648    _ => matrix.mul(translate(((w - vw * s) * tx : F32), ((h - vh * s) * ty : F32)),
2649      matrix.mul(scale(s, s), translate(F32.neg(x), F32.neg(y)))))
2650
2651type EmbeddedImage is Data:
2652  NoImage{}
2653  EmbeddedImage{raster: Raster, attrs: List<&2, XML.Attr>, inverse: Matrix, nearest: Bool}
2654
2655def bitmap.layout(+raster: Raster, +as: List<&2, XML.Attr>, +vw: F32, +vh: F32) -> EmbeddedImage:
2656  Raster{origin, step, width, height, depth, linear, pixels} = raster
2657  +iw = U32.to_f32(width)
2658  +ih = U32.to_f32(height)
2659  +aw = XML.attr.get(as, "width", "auto")
2660  +ah = XML.attr.get(as, "height", "auto")
2661  +autoW = U.string.equal(aw, "auto")
2662  +autoH = U.string.equal(ah, "auto")
2663  +rw = dimension(as, "width", vw, "0")
2664  +rh = dimension(as, "height", vh, "0")
2665  +w = U.choose(F32, autoW, U.choose(F32, autoH, iw, (rh * iw / U.max(ih, 0.000001) : F32)), rw)
2666  +h = U.choose(F32, autoH, U.choose(F32, autoW, ih, (rw * ih / U.max(iw, 0.000001) : F32)), rh)
2667  +attrs = {Con{XML.Attr{"width", F32.show(w)}, Con{XML.Attr{"height", F32.show(h)}, as}} : List<&2, XML.Attr>}
2668  +inv = matrix.inverse(matrix.mul(translate(dimension(as, "x", vw, "0"), dimension(as, "y", vh, "0")), viewbox.matrix(Con{0.0, Con{0.0, Con{iw, Con{ih, Nil{}}}}}, w, h, U.trim.both(XML.attr.get(as, "preserveAspectRatio", "xMidYMid meet")))))
2669  +rendering = XML.attr.get(as, "image-rendering", "auto")
2670  EmbeddedImage{raster, attrs, inv, U.string.equal(rendering, "pixelated") || U.string.equal(rendering, "crisp-edges") || U.string.equal(rendering, "optimizeSpeed")}
2671
2672def style.image.rendering(s: Style) -> String:
2673  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
2674  rendering
2675
2676def bitmap.element(+name: String, +as: List<&2, XML.Attr>, w: F32, h: F32) -> EmbeddedImage:
2677  U.branch(EmbeddedImage, U.string.equal(name, "image"), _ => bitmap.layout(bitmap.decode(XML.attr.get(as, "href", XML.attr.get(as, "xlink:href", ""))), as, w, h), _ => NoImage{})
2678
2679def bitmap.attrs(image: EmbeddedImage, fallback: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
2680  match image:
2681    case NoImage{}: fallback
2682    case EmbeddedImage{raster, attrs, inv, nearest}: attrs
2683
2684def style.image(s: Style) -> Style:
2685  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
2686  Style{"white", "none", col, 1.0, 1.0, 0.0, False{}, visible, ss, MarkerStyle{"none", "none", "none"}, rendering, order}
2687
2688def bitmap.style(image: EmbeddedImage, style: Style) -> Style:
2689  match image:
2690    case NoImage{}: style
2691    case EmbeddedImage{raster, attrs, inv, nearest}: style.image(style)
2692
2693def style.is.visible(s: Style) -> Bool:
2694  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
2695  visible
2696
2697def style.embedded(s: Style) -> Style:
2698  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
2699  Style{"none", "none", col, 1.0, 1.0, 0.0, False{}, visible, ss, MarkerStyle{"none", "none", "none"}, rendering, order}
2700
2701def bitmap.paint(image: EmbeddedImage, ink: Paint<Brush>) -> Paint<Brush>:
2702  match image:
2703    case NoImage{}: ink
2704    case EmbeddedImage{raster, attrs, inv, nearest}: Paint{Bitmap{raster, inv, nearest}, brush.solid(Img.clear(), 1.0)}
2705
2706def scene.bitmap(scene: Scene<Brush>, image: EmbeddedImage) -> Scene<Brush>:
2707  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, masks, maskMode, markers, coverage, effect} = scene
2708  Scene{es, box, inv, style, opacity, kids, id, bitmap.paint(image, ink), clip, outline, fillTree, masks, maskMode, markers, coverage, effect}
2709
2710type Viewport is Data:
2711  Viewport{matrix: Matrix, width: F32, height: F32, clip: Clip, valid: Bool}
2712
2713def viewport.clip(+w: F32, +h: F32, m: Matrix) -> Clip:
2714  ClipShape{edge.tree(path(shape.rect.path(0.0, 0.0, w, h, 0.0, 0.0), 1.0)), matrix.inverse(m), False{}, cover.none()}
2715
2716def viewport.dimension(+as: List<&2, XML.Attr>, key: String, +ref: F32) -> F32:
2717  +value = XML.attr.get(as, key, "auto")
2718  U.branch(F32, U.string.equal(value, "auto"), _ => ref, _ => U.unit(U.number(value), ref))
2719
2720def image.layout.sized(+as: List<&2, XML.Attr>, +values: List<&2, F32>, +vw: F32, +vh: F32) -> List<&2, XML.Attr>:
2721  +iw = U.nth(values, 0n, 300.0)
2722  +ih = U.nth(values, 1n, 150.0)
2723  +ratio = U.nth(values, 2n, 0.0)
2724  +aw = U.string.equal(XML.attr.get(as, "width", "auto"), "auto")
2725  +ah = U.string.equal(XML.attr.get(as, "height", "auto"), "auto")
2726  +rw = dimension(as, "width", vw, "0")
2727  +rh = dimension(as, "height", vh, "0")
2728  +w = U.choose(F32, aw, U.choose(F32, ah, iw, U.choose(F32, F32.is_gt(ratio, 0.0), (rh * ratio : F32), iw)), rw)
2729  +h = U.choose(F32, ah, U.choose(F32, aw, ih, U.choose(F32, F32.is_gt(ratio, 0.0), (rw / U.max(ratio, 0.000001) : F32), ih)), rh)
2730  Con{XML.Attr{"width", F32.show(w)}, Con{XML.Attr{"height", F32.show(h)}, as}}
2731
2732def image.layout.attrs(+as: List<&2, XML.Attr>, +w: F32, +h: F32) -> List<&2, XML.Attr>:
2733  +size = XML.attr.get(as, "#image-size", "")
2734  U.branch(List<&2, XML.Attr>, U.string.equal(size, ""), _ => as, _ => image.layout.sized(as, U.numbers(size), w, h))
2735
2736def element.viewport(+name: String, raw: List<&2, XML.Attr>, +m: Matrix, +w: F32, +h: F32, +root: Bool) -> Viewport:
2737  +as = image.layout.attrs(raw, w, h)
2738  +base = matrix.mul(m, transform(XML.attr.get(as, "transform", ""), identity()))
2739  +svg = U.string.equal(name, "svg")
2740  +width = U.choose(F32, root, w, viewport.dimension(as, "width", w))
2741  +height = U.choose(F32, root, h, viewport.dimension(as, "height", h))
2742  +origin = matrix.mul(base, U.choose(Matrix, root, identity(),
2743    translate(dimension(as, "x", w, "0"), dimension(as, "y", h, "0"))))
2744  +view = U.numbers(XML.attr.get(as, "viewBox", ""))
2745  +vw = U.nth(view, 2n, width)
2746  +vh = U.nth(view, 3n, height)
2747  U.branch(Viewport, svg, _ => Viewport{
2748      matrix.mul(origin, viewbox.matrix(view, width, height, U.trim.both(XML.attr.get(as, "preserveAspectRatio", "xMidYMid meet")))), vw, vh,
2749      U.branch(Clip, U.string.equal(XML.attr.get(as, "overflow", "hidden"), "visible"), _ => ClipAll{}, _ => viewport.clip(width, height, origin)),
2750      F32.is_gt(width, 0.0) && F32.is_gt(height, 0.0) && F32.is_gt(vw, 0.0) && F32.is_gt(vh, 0.0)},
2751    _ => Viewport{U.branch(Matrix, U.string.equal(name, "use"),
2752      _ => matrix.mul(base, translate(dimension(as, "x", w, "0"), dimension(as, "y", h, "0"))), _ => base), w, h, ClipAll{}, True{}})
2753
2754def scene.clipped(scene: Scene<Brush>, viewportClip: Clip) -> Scene<Brush>:
2755  Scene{es, box, inv, style, opacity, kids, id, ink, mask, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect} = scene
2756  Scene{es, box, inv, style, opacity, kids, id, ink, ClipIntersect{mask, viewportClip}, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect}
2757
2758def element.hidden(+name: String, as: List<&2, XML.Attr>) -> Bool:
2759  U.string.equal(name, "#text") || U.string.equal(name, "defs") || U.string.equal(name, "title") || U.string.equal(name, "desc")
2760    || U.string.equal(name, "metadata") || U.string.equal(name, "clipPath") || U.string.equal(name, "mask")
2761    || U.string.equal(name, "symbol") || U.string.equal(name, "linearGradient") || U.string.equal(name, "radialGradient")
2762    || U.string.equal(name, "marker") || U.string.equal(name, "pattern") || U.string.equal(name, "filter") || U.string.equal(name, "style")
2763    || U.string.equal(XML.attr.get(as, "display", "inline"), "none")
2764
2765def instance.anonymous(xs: List<&2, XML.Element>) -> List<&2, XML.Element>:
2766  match xs:
2767    case Nil{}: Nil{}
2768    case Con{XML.Element{n, as, kids, txt}, rest}:
2769      Con{XML.Element{n, Con{XML.Attr{"#editor-id", ""}, as}, instance.anonymous(kids), txt}, instance.anonymous(rest)}
2770
2771def instance.root(result: Maybe<&2, XML.Element>, +useAttrs: List<&2, XML.Attr>) -> List<&2, XML.Element>:
2772  match result:
2773    case None{}: Nil{}
2774    case Some{XML.Element{n, attrs, kids, txt}}:
2775      +tagname = n
2776      +as = CSS.properties(attrs)
2777      +width = XML.attr.get(useAttrs, "width", "auto")
2778      +height = XML.attr.get(useAttrs, "height", "auto")
2779      +viewport = U.string.equal(tagname, "svg") || U.string.equal(tagname, "symbol")
2780      Con{XML.Element{U.choose(String, U.string.equal(tagname, "symbol"), "svg", tagname),
2781        U.choose(List<&2, XML.Attr>, viewport,
2782          Con{XML.Attr{"style", ""}, Con{XML.Attr{"width", U.choose(String, U.string.equal(width, "auto"), XML.attr.get(as, "width", "auto"), width)},
2783            Con{XML.Attr{"height", U.choose(String, U.string.equal(height, "auto"), XML.attr.get(as, "height", "auto"), height)}, as}}}, as), kids, txt}, Nil{}}
2784
2785def use.children(+as: List<&2, XML.Attr>, all: List<&2, XML.Element>, ancestors: List<&2, String>,
2786  expand: List<&2, XML.Element> -> List<&2, XML.Element>) -> List<&2, XML.Element>:
2787  +href = XML.attr.get(as, "href", XML.attr.get(as, "xlink:href", ""))
2788  +id = U.drop(href, "#")
2789  U.branch(List<&2, XML.Element>, U.prefix(href, "#") && Bool.not(U.string.equal(id, "")) && Bool.not(U.strings.has(ancestors, id)),
2790    _ => instance.anonymous(expand(instance.root(XML.element.find(all, id), as))), _ => Nil{})
2791
2792@unsafe
2793def use.expand(+fuel: Nat, +xs: List<&2, XML.Element>, +all: List<&2, XML.Element>, +ancestors: List<&2, String>) -> List<&2, XML.Element>:
2794  match fuel xs:
2795    case 0n _: Nil{}
2796    case 1n+d Nil{}: Nil{}
2797    case 1n+d Con{XML.Element{n, attrs, kids, txt}, rest}:
2798      +as = attrs
2799      +tagname = n
2800      +parents = {Con{XML.attr.get(as, "id", ""), ancestors} : List<&2, String>}
2801      Con{XML.Element{tagname, as, U.branch(List<&2, XML.Element>, U.string.equal(tagname, "use"),
2802        _ => use.children(CSS.properties(as), all, parents, nodes => use.expand(d, nodes, all, parents)),
2803        _ => use.expand(fuel, kids, U.choose(List<&2, XML.Element>, U.string.equal(tagname, "#svg-image"), kids, all), U.choose(List<&2, String>, U.string.equal(tagname, "#svg-image"), Nil{}, parents))), txt}, use.expand(fuel, rest, all, ancestors)}
2804
2805def scenes.append(xs: List<&2, Scene<Brush>>, ys: List<&2, Scene<Brush>>) -> List<&2, Scene<Brush>>:
2806  match xs:
2807    case Nil{}: ys
2808    case Con{h, t}: Con{h, scenes.append(t, ys)}
2809
2810# A mask is rendered as a transparent subscene before its alpha/luminance is used.
2811type MaskData is Data:
2812  MaskData{nodes: List<&2, Scene<Brush>>, mode: U32}
2813
2814def mask.scene(scene: Scene<Brush>, data: MaskData) -> Scene<Brush>:
2815  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, oldNodes, oldMode, markerNodes, coverage, effect} = scene
2816  MaskData{nodes, mode} = data
2817  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, nodes, mode, markerNodes, coverage, effect}
2818
2819def mask.clip(nodes: List<&2, Scene<Brush>>, +region: Clip) -> List<&2, Scene<Brush>>:
2820  match nodes:
2821    case Nil{}: Nil{}
2822    case Con{h, t}: Con{scene.clipped(h, region), mask.clip(t, region)}
2823
2824def mask.region(+as: List<&2, XML.Attr>, +box: Bounds, +m: Matrix, +w: F32, +h: F32) -> Clip:
2825  +user = U.string.equal(XML.attr.get(as, "maskUnits", "objectBoundingBox"), "userSpaceOnUse")
2826  +vw = U.choose(F32, user, w, 1.0)
2827  +vh = U.choose(F32, user, h, 1.0)
2828  +width = dimension(as, "width", vw, "120%")
2829  +height = dimension(as, "height", vh, "120%")
2830  U.branch(Clip, F32.is_gt(width, 0.0) && F32.is_gt(height, 0.0),
2831    _ => ClipShape{edge.tree(path(shape.rect.path(dimension(as, "x", vw, "-10%"), dimension(as, "y", vh, "-10%"), width, height, 0.0, 0.0), 1.0)),
2832      matrix.inverse(matrix.mul(m, U.choose(Matrix, user, identity(), gradient.box(box)))), False{}, cover.none()}, _ => clip.empty())
2833
2834def mask.resource(result: Maybe<&2, XML.Element>, +id: String, +mode: String, +box: Bounds, +m: Matrix, +w: F32, +h: F32,
2835  build: String -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>) -> MaskData:
2836  match result:
2837    case None{}: MaskData{Nil{}, 2}
2838    case Some{XML.Element{n, attrs, kids, txt}}:
2839      +as = CSS.properties(attrs)
2840      +bbox = U.string.equal(XML.attr.get(as, "maskContentUnits", "userSpaceOnUse"), "objectBoundingBox")
2841      +kind = U.choose(String, U.string.equal(mode, "alpha") || U.string.equal(mode, "luminance"), mode, XML.attr.get(as, "mask-type", "luminance"))
2842      U.branch(MaskData, U.string.equal(n, "mask"),
2843        _ => MaskData{mask.clip(build(id)(Con{XML.Element{"g", as, kids, txt}, Nil{}})(matrix.mul(m, U.choose(Matrix, bbox, gradient.box(box), identity())))(U.choose(F32, bbox, 1.0, w))(U.choose(F32, bbox, 1.0, h)), mask.region(as, box, m, w, h)),
2844          U.choose(U32, U.string.equal(kind, "alpha"), 1, U.choose(U32, U.string.equal(XML.attr.get(as, "color-interpolation", "sRGB"), "linearRGB"), 3, 2))},
2845        _ => MaskData{Nil{}, 2})
2846
2847def mask.attach(+as: List<&2, XML.Attr>, all: List<&2, XML.Element>, active: List<&2, String>, box: Bounds, m: Matrix, w: F32, h: F32,
2848  build: String -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>) -> MaskData:
2849  +value = XML.attr.get(as, "mask-image", XML.attr.get(as, "mask", "none"))
2850  +id = U.drop(U.unquote(U.trim.both(U.scan.head(U.until(U.drop(value, "url("), 41, "")))), "#")
2851  U.branch(MaskData, U.prefix(value, "url("),
2852    _ => U.branch(MaskData, U.strings.has(active, id), _ => MaskData{Nil{}, 2},
2853      _ => mask.resource(resource.find(all, id), id, XML.attr.get(as, "mask-mode", "match-source"), box, m, w, h, build)),
2854    _ => MaskData{Nil{}, 0})
2855
2856def scene.masked(+scene: Scene<Brush>, as: List<&2, XML.Attr>, all: List<&2, XML.Element>, active: List<&2, String>, w: F32, h: F32,
2857  build: String -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>) -> Scene<Brush>:
2858  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect} = scene
2859  mask.scene(scene, mask.attach(as, all, active, box, matrix.inverse(inv), w, h, build))
2860
2861def mask.value(c: Img.Color, mode: U32) -> F32:
2862  Img.Color{r, g, b, a} = c
2863  U.choose(F32, U32.is_eq(mode, 1), a, (r * 0.2126 + g * 0.7152 + b * 0.0722 : F32))
2864
2865def mask.factor(+mode: U32, render: Unit -> Img.Color) -> F32:
2866  U.branch(F32, U32.is_eq(mode, 0), _ => 1.0, _ => mask.value(render(Unit{}), mode))
2867
2868# Transparent source pixels cannot contribute, so avoid rendering their masks.
2869def mask.apply(+c: Img.Color, +opacity: F32, mode: U32, render: Unit -> Img.Color) -> Img.Color:
2870  Img.Color{r, g, b, a} = c
2871  U.branch(Img.Color, F32.is_gt(a, 0.0) && F32.is_gt(opacity, 0.0),
2872    _ => color.fade(c, (opacity * mask.factor(mode, render) : F32)), _ => Img.clear())
2873
2874def style.paint(s: String, +current: String, opacity: F32) -> Img.Color:
2875  +paint = s
2876  color.fade(color(U.choose(String, U.string.equal(CSS.css.lower(U.trim.both(paint)), "currentcolor"), current, paint)), opacity)
2877
2878def filter.sample(effect: FilterEffect, p: Point, linear: Bool, original: Unit -> Img.Color) -> Img.Color:
2879  match effect:
2880    case NoFilter{}: original(Unit{})
2881    case FilterSpec{region, units, viewport, attrs, primitives}: original(Unit{})
2882    case FilterImage{raster}: raster.color(raster, p, linear)
2883
2884def paint.order.composite(+order: U32, +fill: Img.Color, +stroke: Img.Color, +markers: Img.Color) -> Img.Color:
2885  U.choose(Img.Color, U32.is_eq(order, 1), color.over(stroke, color.over(markers, fill)),
2886    U.choose(Img.Color, U32.is_eq(order, 2), color.over(markers, color.over(fill, stroke)),
2887      U.choose(Img.Color, U32.is_eq(order, 3), color.over(fill, color.over(markers, stroke)),
2888        U.choose(Img.Color, U32.is_eq(order, 4), color.over(stroke, color.over(fill, markers)),
2889          U.choose(Img.Color, U32.is_eq(order, 5), color.over(fill, color.over(stroke, markers)), color.over(markers, color.over(stroke, fill)))))))
2890
2891# `limit` is the coverage the enclosing clips leave at this pixel; taking the
2892# minimum with the shape's own coverage keeps an edge that coincides with a clip
2893# edge (marker and image viewports) at its true coverage instead of squaring it.
2894def scene.shape(f: Fill, es: List<&2, Edge>, +s: Style, ink: Paint<Brush>, outline: StrokeMesh, +p: Point, +paintPoint: Point, +worldPoint: Point, +linear: Bool, markers: Img.Color, fills: List<&2, Scene<Brush>> -> Point -> Point -> Img.Color, strokes: List<&2, Scene<Brush>> -> Point -> Point -> Img.Color, +limit: F32) -> Img.Color:
2895  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
2896  Paint{fc, sc} = ink
2897  +fillCover = U.choose(F32, visible && Bool.not(U.string.equal(fill, "none")), U.min(fill.coverage(f, es, p, worldPoint, even), limit), 0.0)
2898  +strokeCover = U.choose(F32, visible && Bool.not(U.string.equal(stroke, "none")) && F32.is_gt(width, 0.0), U.min(stroke.coverage(outline, worldPoint), limit), 0.0)
2899  paint.order.composite(order,
2900    U.branch(Img.Color, F32.is_gt(fillCover, 0.0), _ => color.fade(color.space(brush.sample(fc, p, paintPoint, fills), linear), fillCover), _ => Img.clear()),
2901    U.branch(Img.Color, F32.is_gt(strokeCover, 0.0), _ => color.fade(color.space(brush.sample(sc, p, paintPoint, strokes), linear), strokeCover), _ => Img.clear()), markers)
2902
2903def style.margin(s: Style) -> F32:
2904  Style{fill, stroke, col, fo, so, w, even, visible, ss, ms, rendering, order} = s
2905  StrokeSettings{cap, join, limit, dash, offset, vector} = ss
2906  (w * U.max(1.41421356237, limit) / 2.0 : F32)
2907
2908def scene.local(+scene: Scene<Brush>, +p: Point, paintPoint: Point, linear: Bool, +markers: Img.Color, fills: List<&2, Scene<Brush>> -> Point -> Point -> Img.Color, strokes: List<&2, Scene<Brush>> -> Point -> Point -> Img.Color, +limit: F32) -> Img.Color:
2909  Scene{es, box, inv, style, opacity, kids, id, ink, mask, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect} = scene
2910  +q = matrix.point(inv, p)
2911  U.branch(Img.Color, bounds.inside(box, q, style.margin(style)) || style.nonScaling(style), _ => scene.shape(fillTree, es, style, ink, outline, q, matrix.point(inv, paintPoint), p, linear, markers, fills, strokes, limit), _ => markers)
2912
2913# A rendered filter carries its clip as a plain factor; otherwise the clip
2914# coverage travels down as the limit.
2915def filter.rendered(effect: FilterEffect) -> Bool:
2916  match effect:
2917    case NoFilter{}: False{}
2918    case FilterSpec{region, units, viewport, attrs, primitives}: False{}
2919    case FilterImage{raster}: True{}
2920
2921# Pattern scenes are finite compiled subtrees; the callback obscures structural descent.
2922@unsafe
2923def sample(+scenes: List<&2, Scene<Brush>>, +p: Point, +paintPoint: Point, +back: Img.Color, +linear: Bool, +limit: F32) -> Img.Color:
2924  match scenes:
2925    case Nil{}: back
2926    case Con{Scene{es, box, inv, style, opacity, kids, id, ink, mask, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect}, rest}:
2927      +cc = clip.cover(mask, p)
2928      +inner = U.min(limit, cc)
2929      +gate = U.choose(F32, filter.rendered(effect), cc, U.choose(F32, F32.is_gt(inner, 0.0), 1.0, 0.0))
2930      sample(rest, p, paintPoint, U.branch(Img.Color, bounds.inside(coverage, p, 0.01), _ => color.over(mask.apply(clip.fade(gate, _ => filter.sample(effect, paintPoint, linear, _ => sample(kids, p, paintPoint,
2931        scene.local(Scene{es, box, inv, style, opacity, kids, id, ink, mask, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect}, p, paintPoint, linear, sample(markerNodes, p, paintPoint, Img.clear(), linear, inner),
2932          nodes => q => center => sample(nodes, q, center, Img.clear(), False{}, 1.0),
2933          nodes => q => center => sample(nodes, q, center, Img.clear(), False{}, 1.0), inner), linear, inner))),
2934        opacity, maskMode, _ => sample(maskNodes, p, paintPoint, Img.clear(), U32.is_eq(maskMode, 3), 1.0)), back), _ => back), linear, limit)
2935
2936# Patterns compile their content once and reuse it for every tile sample.
2937def pattern.has.content(xs: List<&2, XML.Element>) -> Bool:
2938  match xs:
2939    case Nil{}: False{}
2940    case Con{XML.Element{n, as, kids, txt}, rest}:
2941      +tagname = n
2942      Bool.not(U.string.equal(tagname, "title") || U.string.equal(tagname, "desc") || U.string.equal(tagname, "metadata") || U.string.equal(tagname, "#text")) || pattern.has.content(rest)
2943
2944def pattern.attributes(+xs: List<&2, XML.Attr>) -> List<&2, XML.Attr>:
2945  match xs:
2946    case Nil{}: Nil{}
2947    case Con{XML.Attr{k, v}, rest}:
2948      +key = k
2949      U.branch(List<&2, XML.Attr>, U.string.equal(key, "x") || U.string.equal(key, "y") || U.string.equal(key, "width") || U.string.equal(key, "height")
2950        || U.string.equal(key, "viewBox") || U.string.equal(key, "preserveAspectRatio") || U.string.equal(key, "patternTransform")
2951        || U.string.equal(key, "patternUnits") || U.string.equal(key, "patternContentUnits"),
2952        _ => Con{XML.Attr{key, v}, pattern.attributes(rest)}, _ => pattern.attributes(rest))
2953
2954def pattern.merge(local: XML.Element, template: XML.Element) -> XML.Element:
2955  XML.Element{n, as, kids, txt} = local
2956  XML.Element{tn, tas, tkids, ttxt} = template
2957  +children = kids
2958  XML.Element{n, XML.attrs.append(as, pattern.attributes(tas)), U.choose(List<&2, XML.Element>, pattern.has.content(children), children, tkids), txt}
2959
2960def pattern.reference(+node: XML.Element, result: Maybe<&2, XML.Element>, next: XML.Element -> XML.Element) -> XML.Element:
2961  match result:
2962    case None{}: node
2963    case Some{XML.Element{n, as, kids, txt}}:
2964      +tagname = n
2965      U.branch(XML.Element, U.string.equal(tagname, "pattern"), _ => pattern.merge(node, next(XML.Element{tagname, as, kids, txt})), _ => node)
2966
2967def pattern.resolve(depth: Nat, +node: XML.Element, +all: List<&2, XML.Element>) -> XML.Element:
2968  match depth:
2969    case 0n: node
2970    case 1n+d:
2971      XML.Element{n, as, kids, txt} = node
2972      +href = XML.attr.get(as, "href", XML.attr.get(as, "xlink:href", ""))
2973      U.branch(XML.Element, U.prefix(href, "#"),
2974        _ => pattern.reference(node, resource.find(all, U.drop(href, "#")), e => pattern.resolve(d, e, all)), _ => node)
2975
2976def pattern.paint(+nodes: List<&2, Scene<Brush>>, inv: Matrix, width: F32, height: F32, opacity: F32) -> Brush:
2977  Pattern{inv, width, height, opacity, nodes}
2978
2979# pixels is the device pixels per user unit of the referencing element; the
2980# content is compiled for the pattern frame's own device scale.
2981# Pattern space to the referencing element's user space.
2982def pattern.frame(+as: List<&2, XML.Attr>, +units: Matrix, +uw: F32, +uh: F32) -> Matrix:
2983  matrix.mul(transform(XML.attr.get(as, "patternTransform", ""), identity()), matrix.mul(units,
2984    translate(dimension(as, "x", uw, "0"), dimension(as, "y", uh, "0"))))
2985
2986def pattern.tile(e: XML.Element, box: Bounds, +w: F32, +h: F32, opacity: F32,
2987  build: String -> F32 -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>, +pixels: F32) -> Brush:
2988  XML.Element{n, attrs, kids, txt} = e
2989  Bounds{x0, y0, x1, y1} = box
2990  +as = CSS.properties(attrs)
2991  +bx = x0
2992  +by = y0
2993  +bw = (x1 - bx : F32)
2994  +bh = (y1 - by : F32)
2995  +user = U.string.equal(XML.attr.get(as, "patternUnits", "objectBoundingBox"), "userSpaceOnUse")
2996  +contentUser = U.string.equal(XML.attr.get(as, "patternContentUnits", "userSpaceOnUse"), "userSpaceOnUse")
2997  +uw = U.choose(F32, user, w, 1.0)
2998  +uh = U.choose(F32, user, h, 1.0)
2999  +width = dimension(as, "width", uw, "0")
3000  +height = dimension(as, "height", uh, "0")
3001  +view = U.numbers(XML.attr.get(as, "viewBox", ""))
3002  +hasView = Bool.not(U.string.equal(XML.attr.get(as, "viewBox", ""), ""))
3003  +vw = U.choose(F32, hasView, U.nth(view, 2n, 0.0), U.choose(F32, contentUser, w, 1.0))
3004  +vh = U.choose(F32, hasView, U.nth(view, 3n, 0.0), U.choose(F32, contentUser, h, 1.0))
3005  +units = U.choose(Matrix, user, identity(), matrix.mul(translate(bx, by), scale(bw, bh)))
3006  +content = U.choose(Matrix, hasView,
3007    viewbox.matrix(view, width, height, XML.attr.get(as, "preserveAspectRatio", "xMidYMid meet")),
3008    scale((U.choose(F32, contentUser, 1.0, bw) / U.choose(F32, user, 1.0, U.max(bw, 0.0000001)) : F32),
3009      (U.choose(F32, contentUser, 1.0, bh) / U.choose(F32, user, 1.0, U.max(bh, 0.0000001)) : F32)))
3010  U.branch(Brush, F32.is_gt(width, 0.0) && F32.is_gt(height, 0.0) && F32.is_gt(vw, 0.0) && F32.is_gt(vh, 0.0)
3011      && (user || (F32.is_gt(bw, 0.0) && F32.is_gt(bh, 0.0))),
3012    _ => pattern.paint(build(XML.attr.get(as, "id", ""))((pixels * matrix.norm(pattern.frame(as, units, uw, uh)) : F32))(Con{XML.Element{"g", Con{XML.Attr{"display", "inline"}, as}, kids, txt}, Nil{}})(content)(vw)(vh),
3013      matrix.inverse(pattern.frame(as, units, uw, uh)), width, height, opacity),
3014    _ => Solid{Img.clear(), Img.clear()})
3015
3016def pattern.build(+node: XML.Element, +all: List<&2, XML.Element>, active: List<&2, String>, box: Bounds, w: F32, h: F32, opacity: F32,
3017  build: String -> F32 -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>, +pixels: F32) -> Brush:
3018  XML.Element{n, as, kids, txt} = node
3019  U.branch(Brush, U.strings.has(active, XML.attr.get(as, "id", "")), _ => Solid{Img.clear(), Img.clear()},
3020    _ => pattern.tile(pattern.resolve(32n, node, all), box, w, h, opacity, build, pixels))
3021
3022def matrix.show(m: Matrix) -> String:
3023  Matrix{a, b, c, d, e, f} = m
3024  # Commas between all six values are required in CSS style declarations.
3025  "matrix(" ++ F32.show(a) ++ "," ++ F32.show(b) ++ "," ++ F32.show(c)
3026    ++ "," ++ F32.show(d) ++ "," ++ F32.show(e) ++ "," ++ F32.show(f) ++ ")"
3027
3028# Marker instances have independent styles and viewports. Their geometry does not
3029# enlarge the referring shape's object bounding box.
3030def scene.marked(scene: Scene<Brush>, nodes: List<&2, Scene<Brush>>) -> Scene<Brush>:
3031  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, maskNodes, maskMode, oldMarkers, coverage, effect} = scene
3032  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, maskNodes, maskMode, nodes, coverage, effect}
3033
3034def marker.angle.number(n: U.Number) -> F32:
3035  U.Number{x, t, ok} = n
3036  +suffix = U.trim.both(t)
3037  (x * U.choose(F32, U.string.equal(suffix, "rad"), 57.29577951308232, U.choose(F32, U.string.equal(suffix, "grad"), 0.9, U.choose(F32, U.string.equal(suffix, "turn"), 360.0, 1.0))) : F32)
3038
3039def marker.orient(+s: String, angle: F32, start: Bool) -> F32:
3040  +auto = U.string.equal(s, "auto") || U.string.equal(s, "auto-start-reverse")
3041  U.choose(F32, auto, (angle + U.choose(F32, start && U.string.equal(s, "auto-start-reverse"), 180.0, 0.0) : F32), marker.angle.number(U.number(s)))
3042
3043def marker.reference.point(p: Point, +width: F32, +height: F32) -> List<&2, XML.Attr>:
3044  Point{x, y} = p
3045  Con{XML.Attr{"x", F32.show(F32.neg(x))}, Con{XML.Attr{"y", F32.show(F32.neg(y))}, Con{XML.Attr{"width", F32.show(width)}, Con{XML.Attr{"height", F32.show(height)}, Nil{}}}}}
3046
3047def marker.resource(+result: Maybe<&2, XML.Element>, id: String, vertex: MarkerVertex, start: Bool, strokeWidth: F32, +w: F32, +h: F32) -> List<&2, XML.Element>:
3048  match result:
3049    case None{}: Nil{}
3050    case Some{XML.Element{n, as, kids, txt}}:
3051      MarkerVertex{Point{x, y}, angle} = vertex
3052      +width = dimension(as, "markerWidth", w, "3")
3053      +height = dimension(as, "markerHeight", h, "3")
3054      +view = U.numbers(XML.attr.get(as, "viewBox", ""))
3055      +vw = U.nth(view, 2n, width)
3056      +vh = U.nth(view, 3n, height)
3057      +unitScale = U.choose(F32, U.string.equal(XML.attr.get(as, "markerUnits", "strokeWidth"), "userSpaceOnUse"), 1.0, strokeWidth)
3058      +mat = matrix.mul(translate(x, y), matrix.mul(rotate(marker.orient(XML.attr.get(as, "orient", "0"), angle, start)), scale(unitScale, unitScale)))
3059      +ref = matrix.point(viewbox.matrix(view, width, height, XML.attr.get(as, "preserveAspectRatio", "xMidYMid meet")), Point{dimension(as, "refX", vw, "0"), dimension(as, "refY", vh, "0")})
3060      U.branch(List<&2, XML.Element>, U.string.equal(n, "marker") && F32.is_gt(width, 0.0) && F32.is_gt(height, 0.0) && F32.is_gt(vw, 0.0) && F32.is_gt(vh, 0.0) && F32.is_gt(unitScale, 0.0),
3061        _ => Con{XML.Element{"g", Con{XML.Attr{"transform", matrix.show(mat)}, Nil{}}, Con{XML.Element{"svg", Con{XML.Attr{"#marker-source", id}, Con{XML.Attr{"#editor-id", ""}, Con{XML.Attr{"transform", ""}, XML.attrs.append(marker.reference.point(ref, width, height), as)}}}, instance.anonymous(kids), txt}, Nil{}}, ""}, Nil{}}, _ => Nil{})
3062
3063def marker.instance(+value: String, vertex: MarkerVertex, start: Bool, +all: List<&2, XML.Element>, active: List<&2, String>, width: F32, w: F32, h: F32) -> List<&2, XML.Element>:
3064  +id = U.drop(U.unquote(U.trim.both(U.scan.head(U.until(U.drop(value, "url("), 41, "")))), "#")
3065  U.branch(List<&2, XML.Element>, U.prefix(value, "url(") && Bool.not(U.strings.has(active, id)),
3066    _ => marker.resource(resource.find(all, id), id, vertex, start, width, w, h), _ => Nil{})
3067
3068def marker.elements.append(xs: List<&2, XML.Element>, ys: List<&2, XML.Element>) -> List<&2, XML.Element>:
3069  match xs:
3070    case Nil{}: ys
3071    case Con{a, rest}: Con{a, marker.elements.append(rest, ys)}
3072
3073@unsafe
3074def marker.instances.loop(xs: List<&2, MarkerVertex>, +first: Bool, +ms: MarkerStyle, +all: List<&2, XML.Element>, +active: List<&2, String>, +width: F32, +w: F32, +h: F32) -> List<&2, XML.Element>:
3075  match xs:
3076    case Nil{}: Nil{}
3077    case Con{vertex, Nil{}}:
3078      MarkerStyle{start, middle, end} = ms
3079      +v = vertex
3080      marker.elements.append(U.branch(List<&2, XML.Element>, first, _ => marker.instance(start, v, True{}, all, active, width, w, h), _ => Nil{}), marker.instance(end, v, False{}, all, active, width, w, h))
3081    case Con{vertex, Con{head, tail}}:
3082      MarkerStyle{start, middle, end} = ms
3083      marker.elements.append(marker.instance(U.choose(String, first, start, middle), vertex, first, all, active, width, w, h), marker.instances.loop(Con{head, tail}, False{}, ms, all, active, width, w, h))
3084
3085def marker.enabled(ms: MarkerStyle) -> Bool:
3086  MarkerStyle{start, middle, end} = ms
3087  U.prefix(start, "url(") || U.prefix(middle, "url(") || U.prefix(end, "url(")
3088
3089def marker.shape(+name: String) -> Bool:
3090  U.string.equal(name, "path") || U.string.equal(name, "line") || U.string.equal(name, "polyline") || U.string.equal(name, "polygon")
3091
3092def marker.wanted(+name: String, s: Style) -> Bool:
3093  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
3094  marker.enabled(ms) && marker.shape(name)
3095
3096def marker.instances(+name: String, +as: List<&2, XML.Attr>, +s: Style, all: List<&2, XML.Element>, active: List<&2, String>, +w: F32, +h: F32) -> List<&2, XML.Element>:
3097  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
3098  U.branch(List<&2, XML.Element>, marker.enabled(ms) && marker.shape(name),
3099    _ => marker.instances.loop(marker.trace(shape.data(name, as, w, h, XML.attr.get(as, "d", ""))), True{}, ms, all, active, width, w, h), _ => Nil{})
3100
3101# A shape's edges and its marker instances. Path data is shared with the
3102# marker trace only when markers are wanted.
3103def compile.geometry(marked: Bool, data: String, +n: String, +attrs: List<&2, XML.Attr>, glyphs: List<&2, XML.Element>, +vw: F32, +vh: F32, +tol: F32, +style: Style, all: List<&2, XML.Element>, active: List<&2, String>) -> List<&2, Edge> & List<&2, XML.Element>:
3104  match marked:
3105    case False{}: (shape.children(n, attrs, glyphs, vw, vh, tol, data), Nil{})
3106    case True{}:
3107      +dd = data
3108      (shape.children(n, attrs, glyphs, vw, vh, tol, dd), marker.instances(n, Con{XML.Attr{"d", dd}, attrs}, style, all, active, vw, vh))
3109
3110def compile.visible(geo: List<&2, Edge> & List<&2, XML.Element>, +n: String, +matrix: Matrix, +vw: F32, +vh: F32, viewportClip: Clip, +style: Style, embedded: EmbeddedImage, +attrs: List<&2, XML.Attr>, +all: List<&2, XML.Element>, +active: List<&2, String>, +scale: F32,
3111  masks: String -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>, fills: String -> F32 -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>, strokes: String -> F32 -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>,
3112  markers: List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>,
3113  children: Style -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>) -> List<&2, Scene<Brush>>:
3114  (edges, markerNodes) = geo
3115  Con{scene.marked(scene.masked(scene.clipped(scene.bitmap(scene.make(edges, matrix, style,
3116    color.opacity(XML.attr.get(attrs, "opacity", "1"), 1.0), U.branch(List<&2, Scene<Brush>>, U.string.equal(n, "#glyph-run"), _ => Nil{}, _ => children(style)(matrix)(vw)(vh)),
3117    XML.attr.get(attrs, "#editor-id", ""), all, vw, vh, attrs,
3118      node => box => pw => ph => alpha => pattern.build(node, all, active, box, pw, ph, alpha, fills, (scale * matrix.norm(matrix) : F32)),
3119      node => box => pw => ph => alpha => pattern.build(node, all, active, box, pw, ph, alpha, strokes, (scale * matrix.norm(matrix) : F32)), scale), embedded), viewportClip), attrs, all, active, vw, vh, masks), markers(markerNodes)(matrix)(vw)(vh)), Nil{}}
3120
3121def compile.node(+n: String, +as: List<&2, XML.Attr>, +glyphs: List<&2, XML.Element>, +vp: Viewport, inherited: Style, +all: List<&2, XML.Element>, +active: List<&2, String>, +scale: F32, data: String,
3122  masks: String -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>, fills: String -> F32 -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>, strokes: String -> F32 -> List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>,
3123  markers: List<&2, XML.Element> -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>,
3124  children: Style -> Matrix -> F32 -> F32 -> List<&2, Scene<Brush>>) -> List<&2, Scene<Brush>>:
3125  Viewport{matrix, vw, vh, viewportClip, valid} = vp
3126  +baseStyle = style.inherit(inherited, as, F32.sqrt(((vw * vw + vh * vh) / 2.0 : F32)))
3127  +embedded = bitmap.element(n, Con{XML.Attr{"image-rendering", style.image.rendering(baseStyle)}, as}, vw, vh)
3128  +attrs = bitmap.attrs(embedded, as)
3129  +style = U.choose(Style, U.string.equal(n, "#svg-image"), style.embedded(baseStyle), bitmap.style(embedded, baseStyle))
3130  U.branch(List<&2, Scene<Brush>>, element.hidden(n, as) || Bool.not(valid) || (U.string.equal(n, "#svg-image") && Bool.not(style.is.visible(baseStyle))), _ => Nil{},
3131    _ => compile.visible(compile.geometry(marker.wanted(n, style), data, n, attrs, glyphs, vw, vh, local.tolerance(matrix, scale), style, all, active),
3132      n, matrix, vw, vh, viewportClip, style, embedded, attrs, all, active, scale, masks, fills, strokes, markers, children))
3133
3134# An element's path data is taken out of its attributes before they are
3135# shared, so the path parser owns that text; the loop carries the element in
3136# that state as a step of its own.
3137type Work is Data:
3138  Pending{xs: List<&2, XML.Element>}
3139  Prepared{taken: XML.Taken, tagname: String, kids: List<&2, XML.Element>, rest: List<&2, XML.Element>}
3140
3141@unsafe
3142def compile.work(+fuel: Nat, work: Work, +inherited: Style, +m: Matrix, +w: F32, +h: F32,
3143  +all: List<&2, XML.Element>, +root: Bool, +active: List<&2, String>, +scale: F32) -> List<&2, Scene<Brush>>:
3144  match fuel work:
3145    case 0n _: Nil{}
3146    case 1n+d Pending{xs}:
3147      match xs:
3148        case Nil{}: Nil{}
3149        case Con{XML.Element{tagname, attrs, kids, txt}, rest}:
3150          compile.work(fuel, Prepared{XML.attrs.take(image.layout.attrs(CSS.properties(attrs), w, h), "d"), tagname, kids, rest}, inherited, m, w, h, all, root, active, scale)
3151    case 1n+d Prepared{XML.Taken{data, as0}, tagname, kids, rest}:
3152      +as = as0
3153      +ks = kids
3154      +n = tagname
3155      scenes.append(compile.node(n, as, ks, element.viewport(n, as, m, w, h, root), inherited, all, active, scale, data,
3156        used => nodes => mat => vw => vh => compile.work(d, Pending{nodes}, style.default(), mat, vw, vh, all, False{}, Con{used, active}, scale),
3157        used => sc => nodes => mat => vw => vh => compile.work(d, Pending{nodes}, style.default(), mat, vw, vh, all, False{}, Con{used, active}, sc),
3158        used => sc => nodes => mat => vw => vh => compile.work(d, Pending{nodes}, style.default(), mat, vw, vh, all, False{}, Con{used, active}, sc),
3159        nodes => mat => vw => vh => compile.work(d, Pending{nodes}, style.default(), mat, vw, vh, all, False{}, active, scale),
3160        style => mat => vw => vh => compile.work(U.choose(Nat, U.string.equal(XML.attr.get(as, "#marker-source", ""), ""), fuel, d), Pending{ks}, U.choose(Style, U.string.equal(n, "#svg-image"), style.default(), style), mat, vw, vh, U.choose(List<&2, XML.Element>, U.string.equal(n, "#svg-image"), resources(ks, Nil{}), all), False{}, U.choose(List<&2, String>, U.string.equal(n, "#svg-image"), Nil{}, U.choose(List<&2, String>, U.string.equal(XML.attr.get(as, "#marker-source", ""), ""), active, Con{XML.attr.get(as, "#marker-source", ""), active})), scale)),
3161        compile.work(fuel, Pending{rest}, inherited, m, w, h, all, root, active, scale))
3162
3163def compile(+fuel: Nat, xs: List<&2, XML.Element>, +inherited: Style, +m: Matrix, +w: F32, +h: F32,
3164  +all: List<&2, XML.Element>, +root: Bool, +active: List<&2, String>, +scale: F32) -> List<&2, Scene<Brush>>:
3165  compile.work(fuel, Pending{xs}, inherited, m, w, h, all, root, active, scale)
3166
3167# XML.Text layout produces internal glyph geometry while preserving text/tspan groups.
3168type TextFont is Data:
3169  TextFont{size: F32, face: U32, weight: String, slant: String, anchor: String,
3170    letter: F32, word: F32, shift: F32, baseline: String, kern: Bool, preserve: Bool, book: Font.FontBook}
3171
3172def text.font.em(font: TextFont) -> F32:
3173  TextFont{size, face, weight, slant, anchor, letter, word, shift, baseline, kern, preserve, book} = font
3174  size
3175
3176def text.font.default(book: Font.FontBook) -> TextFont:
3177  TextFont{16.0, 0, "normal", "normal", "start", 0.0, 0.0, 0.0, "alphabetic", True{}, False{}, book}
3178
3179def text.measure(+num: U.Number, ref: F32, +size: F32) -> F32:
3180  U.Number{x, tail, ok} = num
3181  +t = tail
3182  U.choose(F32, U.prefix(t, "em"), (x * size : F32), U.choose(F32, U.prefix(t, "ex"), (x * size * 0.536 : F32),
3183    U.choose(F32, U.prefix(t, "rem"), (x * 16.0 : F32), U.unit(num, ref))))
3184
3185def text.font.size(+s: String, +parent: F32) -> F32:
3186  U.choose(F32, U.string.equal(s, "smaller"), (parent / 1.2 : F32), U.choose(F32, U.string.equal(s, "larger"), (parent * 1.2 : F32),
3187    U.choose(F32, U.string.equal(s, "medium"), 16.0, U.choose(F32, U.string.equal(s, "small"), 13.0,
3188      U.choose(F32, U.string.equal(s, "large"), 18.0, U.max(0.0, text.measure(U.number(s), parent, parent)))))))
3189
3190def text.font.inherit(+font: TextFont, +as: List<&2, XML.Attr>) -> TextFont:
3191  TextFont{size, face, weight, slant, anchor, letter, word, shift, baseline, kern, preserve, book} = font
3192  +sz = text.font.size(style.value(as, "font-size", F32.show(size), "16", True{}), size)
3193  +wt = style.value(as, "font-weight", weight, "normal", True{})
3194  +it = style.value(as, "font-style", slant, "normal", True{})
3195  +bold = U.string.equal(wt, "bold") || U.string.equal(wt, "bolder") || F32.is_ge(U.scalar(wt, 400.0), 600.0)
3196  +italic = U.string.equal(it, "italic") || U.prefix(it, "oblique")
3197  +selectedFace = (U.choose(U32, bold, 1, 0) + U.choose(U32, italic, 2, 0) : U32)
3198  +bs = XML.attr.get(as, "baseline-shift", "0")
3199  +ws = style.value(as, "white-space", U.choose(String, preserve, "pre", "normal"), "normal", True{})
3200  +xml = XML.attr.get(as, "xml:space", "")
3201  TextFont{sz, selectedFace, wt, it,
3202    style.value(as, "text-anchor", anchor, "start", True{}),
3203    text.measure(U.number(style.value(as, "letter-spacing", F32.show(letter), "0", True{})), sz, sz),
3204    text.measure(U.number(style.value(as, "word-spacing", F32.show(word), "0", True{})), sz, sz),
3205    (shift + U.choose(F32, U.string.equal(bs, "super"), (Font.font.metric(book, selectedFace, "superscript", 0.4) * sz : F32), U.choose(F32, U.string.equal(bs, "sub"), (F32.neg(Font.font.metric(book, selectedFace, "subscript", 0.2)) * sz : F32), text.measure(U.number(bs), sz, sz))) : F32),
3206    style.value(as, "dominant-baseline", baseline, "alphabetic", True{}),
3207    Bool.not(U.string.equal(style.value(as, "font-kerning", U.choose(String, kern, "auto", "none"), "auto", True{}), "none")),
3208    U.choose(Bool, U.string.equal(xml, "preserve"), True{}, U.choose(Bool, U.string.equal(xml, "default"), False{}, U.prefix(ws, "pre") || U.string.equal(ws, "break-spaces"))), book}
3209
3210def text.length.value(+num: U.Number, +ref: F32, +size: F32, next: String -> List<&2, F32>) -> List<&2, F32>:
3211  U.Number{x, tail, ok} = num
3212  U.branch(List<&2, F32>, ok, _ => Con{text.measure(num, ref, size), next(dash.skip.units(tail))}, _ => Nil{})
3213
3214@unsafe
3215def text.lengths(s: String, +ref: F32, +size: F32) -> List<&2, F32>:
3216  text.length.value(U.number(s), ref, size, rest => text.lengths(U.separators(rest), ref, size))
3217
3218type TextFrame is Data:
3219  TextFrame{start: U32, xs: List<&2, F32>, ys: List<&2, F32>, dxs: List<&2, F32>, dys: List<&2, F32>, angles: List<&2, F32>}
3220
3221def text.frame(+as: List<&2, XML.Attr>, start: U32, +w: F32, +h: F32, +size: F32) -> TextFrame:
3222  TextFrame{start, text.lengths(XML.attr.get(as, "x", ""), w, size), text.lengths(XML.attr.get(as, "y", ""), h, size),
3223    text.lengths(XML.attr.get(as, "dx", ""), w, size), text.lengths(XML.attr.get(as, "dy", ""), h, size), U.numbers(XML.attr.get(as, "rotate", ""))}
3224
3225type TextValue is Data:
3226  TextValue{value: F32, set: Bool}
3227
3228def text.at(xs: List<&2, F32>, +index: U32, +repeat: Bool, previous: TextValue) -> TextValue:
3229  match xs:
3230    case Nil{}: U.choose(TextValue, repeat, previous, TextValue{0.0, False{}})
3231    case Con{head, tail}:
3232      +h = head
3233      U.branch(TextValue, U32.is_eq(index, 0), _ => TextValue{h, True{}}, _ => text.at(tail, (index - 1 : U32), repeat, TextValue{h, True{}}))
3234
3235def text.axis.first(+value: TextValue, fallback: Unit -> TextValue) -> TextValue:
3236  TextValue{x, set} = value
3237  U.branch(TextValue, set, _ => value, _ => fallback(Unit{}))
3238
3239def text.axis(xs: List<&2, TextFrame>, +index: U32, +axis: U32) -> TextValue:
3240  match xs:
3241    case Nil{}: TextValue{0.0, False{}}
3242    case Con{TextFrame{start, xx, yy, dx, dy, angles}, rest}:
3243      text.axis.first(text.at(U.choose(List<&2, F32>, U32.is_eq(axis, 0), xx, U.choose(List<&2, F32>, U32.is_eq(axis, 1), yy,
3244        U.choose(List<&2, F32>, U32.is_eq(axis, 2), dx, U.choose(List<&2, F32>, U32.is_eq(axis, 3), dy, angles)))),
3245        (index - start : U32), U32.is_eq(axis, 4), TextValue{0.0, False{}}), _ => text.axis(rest, index, axis))
3246
3247type TextPosition is Data:
3248  TextPosition{x: TextValue, y: TextValue, dx: TextValue, dy: TextValue, angle: TextValue}
3249
3250def text.position(+frames: List<&2, TextFrame>, +index: U32) -> TextPosition:
3251  TextPosition{text.axis(frames, index, 0), text.axis(frames, index, 1), text.axis(frames, index, 2), text.axis(frames, index, 3), text.axis(frames, index, 4)}
3252
3253type TextChunk is Data:
3254  TextChunk{key: U32, delta: F32}
3255
3256type TextCursor is Data:
3257  TextCursor{x: F32, y: F32, index: U32, chunk: U32, start: F32, anchor: String,
3258    previous: U32, previousClass: U32, previousFace: U32, previousSize: F32,
3259    seen: Bool, space: Bool, trailing: F32, spacing: F32, chunks: List<&2, TextChunk>}
3260
3261def text.cursor() -> TextCursor:
3262  TextCursor{0.0, 0.0, 0, 0, 0.0, "start", 0, 0, 99, 0.0, False{}, True{}, 0.0, 0.0, Nil{}}
3263
3264def text.chunk(+cursor: TextCursor) -> List<&2, TextChunk>:
3265  TextCursor{x, y, index, chunk, start, anchor, prev, pc, pf, ps, seen, blankState, trailing, spacing, chunks} = cursor
3266  +width = (x - start - trailing - spacing : F32)
3267  Con{TextChunk{chunk, U.choose(F32, U.string.equal(anchor, "middle"), (width / 2.0 : F32), U.choose(F32, U.string.equal(anchor, "end"), width, 0.0))}, chunks}
3268
3269def text.index(cursor: TextCursor) -> U32:
3270  TextCursor{x, y, index, chunk, start, anchor, prev, pc, pf, ps, seen, blankState, trailing, spacing, chunks} = cursor
3271  index
3272
3273# XML.Text paths use the same flattened geometry as fills and strokes.
3274def text.path.target(target: Maybe<&2, XML.Element>, +w: F32, +h: F32) -> List<&2, XML.Attr>:
3275  match target:
3276    case None{}: Nil{}
3277    case Some{XML.Element{n, as, kids, txt}}:
3278      +attrs = CSS.properties(as)
3279      Con{XML.Attr{"#text-path-data", shape.data(n, attrs, w, h, XML.attr.get(attrs, "d", ""))}, Con{XML.Attr{"#text-path-transform", XML.attr.get(attrs, "transform", "")}, Con{XML.Attr{"#text-path-length", XML.attr.get(attrs, "pathLength", "0")}, Nil{}}}}
3280
3281def text.path.attrs(+as: List<&2, XML.Attr>, all: List<&2, XML.Element>, +w: F32, +h: F32) -> List<&2, XML.Attr>:
3282  +direct = XML.attr.get(as, "path", "")
3283  +href = XML.attr.get(as, "href", XML.attr.get(as, "xlink:href", ""))
3284  +found = text.path.target(U.branch(Maybe<&2, XML.Element>, U.prefix(href, "#"), _ => XML.element.find(all, U.drop(href, "#")), _ => None{}), w, h)
3285  XML.attrs.append(U.choose(List<&2, XML.Attr>, U.string.equal(direct, ""), found, Con{XML.Attr{"#text-path-data", direct}, Nil{}}), as)
3286
3287def text.path.resolve.node(+n: String, +as: List<&2, XML.Attr>, +kids: List<&2, XML.Element>, txt: String, +all: List<&2, XML.Element>, w: F32, h: F32, vp: Viewport,
3288  expand: List<&2, XML.Element> -> List<&2, XML.Element> -> F32 -> F32 -> List<&2, XML.Element>) -> XML.Element:
3289  Viewport{mat, vw, vh, clip, valid} = vp
3290  XML.Element{n, U.choose(List<&2, XML.Attr>, U.string.equal(n, "textPath"), text.path.attrs(as, all, w, h), as), expand(kids)(U.choose(List<&2, XML.Element>, U.string.equal(n, "#svg-image"), kids, all))(vw)(vh), txt}
3291
3292@unsafe
3293def text.path.resolve(xs: List<&2, XML.Element>, +all: List<&2, XML.Element>, +w: F32, +h: F32, +root: Bool) -> List<&2, XML.Element>:
3294  match xs:
3295    case Nil{}: Nil{}
3296    case Con{XML.Element{n, attrs, kids, txt}, rest}:
3297      +as = CSS.properties(attrs)
3298      +tagname = n
3299      Con{text.path.resolve.node(tagname, as, kids, txt, all, w, h, element.viewport(tagname, as, identity(), w, h, root), nodes => scope => vw => vh => text.path.resolve(nodes, scope, vw, vh, False{})), text.path.resolve(rest, all, w, h, root)}
3300
3301def text.path.edges(+xs: List<&2, Edge>, +acc: List<&2, Edge>) -> List<&2, Edge>:
3302  match xs:
3303    case Nil{}: acc
3304    case Con{Edge{ax, ay, bx, by, stroked, closed}, rest}: text.path.edges(rest, U.choose(List<&2, Edge>, stroked && F32.is_gt(point.distance(Point{ax, ay}, Point{bx, by}), 0.000001), Con{Edge{ax, ay, bx, by, stroked, closed}, acc}, acc))
3305
3306def text.path.geometry(+as: List<&2, XML.Attr>) -> List<&2, Edge>:
3307  text.path.edges(edges.transform(path(XML.attr.get(as, "#text-path-data", ""), 0.001), transform(XML.attr.get(as, "#text-path-transform", ""), identity())), Nil{})
3308
3309def text.path.total(xs: List<&2, Edge>) -> F32:
3310  match xs:
3311    case Nil{}: 0.0
3312    case Con{Edge{ax, ay, bx, by, stroked, closed}, rest}: (point.distance(Point{ax, ay}, Point{bx, by}) + text.path.total(rest) : F32)
3313
3314def text.path.at(+xs: List<&2, Edge>, +distance: F32, fallback: Matrix) -> Matrix:
3315  match xs:
3316    case Nil{}: fallback
3317    case Con{Edge{x, y, u, v, stroked, closed}, rest}:
3318      +dx = (u - x : F32)
3319      +dy = (v - y : F32)
3320      +len = F32.sqrt((dx * dx + dy * dy : F32))
3321      +tx = (dx / len : F32)
3322      +ty = (dy / len : F32)
3323      U.branch(Matrix, F32.is_le(distance, len), _ => Matrix{tx, ty, F32.neg(ty), tx, (x + tx * distance : F32), (y + ty * distance : F32)},
3324        _ => text.path.at(rest, (distance - len : F32), Matrix{tx, ty, F32.neg(ty), tx, u, v}))
3325
3326def text.path.offset(+num: U.Number, +total: F32, +calibration: F32, em: F32) -> F32:
3327  U.Number{x, suffix, valid} = num
3328  (text.measure(num, total, em) * U.choose(F32, U.prefix(suffix, "%") || F32.is_le(calibration, 0.0), 1.0, (total / calibration : F32)) : F32)
3329
3330def text.path.begin(+cursor: TextCursor) -> TextCursor:
3331  TextCursor{x, y, index, chunk, start, anchor, prev, pc, pf, ps, seen, blankState, trailing, spacing, chunks} = cursor
3332  TextCursor{0.0, 0.0, index, chunk, 0.0, anchor, 0, 0, 99, 0.0, False{}, blankState, 0.0, 0.0, text.chunk(cursor)}
3333
3334def text.path.end(+cursor: TextCursor, +matrix: Matrix) -> TextCursor:
3335  TextCursor{x, y, index, chunk, start, anchor, prev, pc, pf, ps, seen, blankState, trailing, spacing, chunks} = cursor
3336  Matrix{a, b, c, d, e, f} = matrix
3337  TextCursor{e, f, index, U32.inc(chunk), e, "start", 0, 0, 99, 0.0, True{}, blankState, 0.0, 0.0, text.chunk(cursor)}
3338
3339def text.path.frames(xs: List<&2, TextFrame>) -> List<&2, TextFrame>:
3340  match xs:
3341    case Nil{}: Nil{}
3342    case Con{TextFrame{start, xx, yy, dx, dy, angles}, rest}: Con{TextFrame{start, xx, Nil{}, dx, dy, angles}, text.path.frames(rest)}
3343
3344type TextLayout is Data:
3345  TextLayout{nodes: List<&2, XML.Element>, cursor: TextCursor}
3346
3347def text.nodes.append(xs: List<&2, XML.Element>, ys: List<&2, XML.Element>) -> List<&2, XML.Element>:
3348  match xs:
3349    case Nil{}: ys
3350    case Con{h, t}: Con{h, text.nodes.append(t, ys)}
3351
3352def text.layout.combine(left: List<&2, XML.Element>, result: TextLayout) -> TextLayout:
3353  TextLayout{right, cursor} = result
3354  TextLayout{text.nodes.append(left, right), cursor}
3355
3356def text.layout.then(result: TextLayout, next: TextCursor -> TextLayout) -> TextLayout:
3357  TextLayout{nodes, cursor} = result
3358  text.layout.combine(nodes, next(cursor))
3359
3360def text.baseline(+font: TextFont) -> F32:
3361  TextFont{size, face, weight, slant, anchor, letter, word, shift, baseline, kern, preserve, book} = font
3362  +base = baseline
3363  +ascender = Font.font.metric(book, face, "ascender", 1.069)
3364  +descender = Font.font.metric(book, face, "descender", F32.neg(0.293))
3365  (U.choose(F32, U.string.equal(base, "middle"), (size * Font.font.metric(book, face, "xheight", 0.536) / 2.0 : F32),
3366    U.choose(F32, U.string.equal(base, "central"), (size * (ascender + descender) / 2.0 : F32),
3367      U.choose(F32, U.string.equal(base, "text-before-edge") || U.string.equal(base, "before-edge"), (size * ascender : F32),
3368        U.choose(F32, U.string.equal(base, "text-after-edge") || U.string.equal(base, "after-edge"), (size * descender : F32), 0.0)))) - shift : F32)
3369
3370def text.glyph.node(outline: String, advance: F32, width: F32, extra: F32, x: F32, +matrix: Matrix, chunk: U32, index: U32) -> XML.Element:
3371  XML.Element{"#glyph", Con{XML.Attr{"#length-x", F32.show(x)}, Con{XML.Attr{"#length-width", F32.show(width)}, Con{XML.Attr{"#length-extra", F32.show(extra)}, Con{XML.Attr{"d", outline}, Con{XML.Attr{"#glyph-transform", matrix.show(matrix)}, Con{XML.Attr{"#glyph-advance", F32.show(advance)},
3372    Con{XML.Attr{"#text-chunk", U32.show(chunk)}, Con{XML.Attr{"#text-index", U32.show(index)}, Nil{}}}}}}}}}, Nil{}, ""}
3373
3374def text.place(+cursor: TextCursor, +font: TextFont, +glyph: Font.FontGlyph, +pos: TextPosition, +ch: U32) -> TextLayout:
3375  TextCursor{oldx, oldy, index, oldchunk, start, oldanchor, prev, pc, pf, ps, seen, wasSpace, trailing, spacing, chunks} = cursor
3376  TextFont{size, face, weight, slant, anchor, letter, word, shift, baseline, kern, preserve, book} = font
3377  Font.FontGlyph{advance, outline, gid, leftClass, rightClass} = glyph
3378  TextPosition{TextValue{xx, setx}, TextValue{yy, sety}, TextValue{dx, setdx}, TextValue{dy, setdy}, TextValue{angle, seta}} = pos
3379  +fresh = setx || sety || Bool.not(seen)
3380  +factor = (size / 1000.0 : F32)
3381  +kerning = U.choose(F32, kern && Bool.not(fresh) && U32.is_eq(face, pf) && F32.is_eq(size, ps),
3382    (Font.font.kern(book, face, prev, pc, gid, rightClass) * factor : F32), 0.0)
3383  +x = (U.choose(F32, setx, xx, oldx) + dx + kerning : F32)
3384  +y = (U.choose(F32, sety, yy, oldy) + dy : F32)
3385  +chunk = U.choose(U32, fresh, U32.inc(oldchunk), oldchunk)
3386  +blank = U32.is_eq(ch, 32)
3387  +width = (advance * factor + letter + U.choose(F32, blank, word, 0.0) : F32)
3388  TextLayout{Con{text.glyph.node(outline, advance, width, (letter + U.choose(F32, blank, word, 0.0) : F32), x, matrix.mul(translate(x, (y + text.baseline(font) : F32)),
3389    matrix.mul(rotate(angle), scale(factor, F32.neg(factor)))), chunk, index), Nil{}},
3390    TextCursor{(x + width : F32), y, U32.inc(index), chunk, U.choose(F32, fresh, x, start), U.choose(String, fresh, anchor, oldanchor),
3391      gid, leftClass, face, size, True{}, blank, U.choose(F32, blank && Bool.not(preserve), width, 0.0), U.choose(F32, blank && Bool.not(preserve), spacing, letter),
3392      U.choose(List<&2, TextChunk>, fresh, text.chunk(cursor), chunks)}}
3393
3394def text.character(+cursor: TextCursor, +font: TextFont, frames: List<&2, TextFrame>, +ch: U32) -> TextLayout:
3395  TextCursor{x, y, index, chunk, start, anchor, prev, pc, pf, ps, seen, previousSpace, trailing, spacing, chunks} = cursor
3396  TextFont{size, face, weight, slant, fa, letter, word, shift, baseline, kern, preserve, book} = font
3397  +blank = U.space(ch)
3398  +value = U.choose(U32, blank, 32, ch)
3399  U.branch(TextLayout, blank && previousSpace && Bool.not(preserve), _ => TextLayout{Nil{}, cursor},
3400    _ => text.place(cursor, font, Font.font.glyph(book, face, value), text.position(frames, index), value))
3401
3402@unsafe
3403def text.characters(s: String, +font: TextFont, +frames: List<&2, TextFrame>, cursor: TextCursor) -> TextLayout:
3404  match s:
3405    case SNil{}: TextLayout{Nil{}, cursor}
3406    case SCon{Chr{ch}, rest}: text.layout.then(text.character(cursor, font, frames, ch), next => text.characters(rest, font, frames, next))
3407
3408# Length fitting is applied inside-out; an explicitly fitted child is one fixed unit.
3409type TextMeasure is Data:
3410  TextMeasure{count: U32, lo: F32, hi: F32, extra: F32, fixed: F32}
3411
3412def text.measure.empty() -> TextMeasure:
3413  TextMeasure{0, 1000000000.0, F32.neg(1000000000.0), 0.0, 0.0}
3414
3415def text.measure.add(m: TextMeasure, +x: F32, +width: F32, extra: F32, fixed: Bool) -> TextMeasure:
3416  TextMeasure{count, lo, hi, oldExtra, oldFixed} = m
3417  TextMeasure{U32.inc(count), U.min(lo, x), U.max(hi, (x + width : F32)), extra, (oldFixed + U.choose(F32, fixed, width, 0.0) : F32)}
3418
3419def text.measure.nodes(xs: List<&2, XML.Element>, +skip: String, +acc: TextMeasure) -> TextMeasure:
3420  match xs:
3421    case Nil{}: acc
3422    case Con{XML.Element{n, as, kids, txt}, rest}:
3423      +attrs = as
3424      +glyph = U.string.equal(n, "#glyph")
3425      +fixed = U.string.equal(XML.attr.get(attrs, "#length-fixed", ""), "yes")
3426      text.measure.nodes(rest, skip, U.branch(TextMeasure, (glyph && Bool.not(U.string.equal(XML.attr.get(attrs, "#text-index", ""), skip))) || fixed,
3427        _ => text.measure.add(acc, U.scalar(XML.attr.get(attrs, "#length-x", "0"), 0.0), U.scalar(XML.attr.get(attrs, "#length-width", "0"), 0.0), U.scalar(XML.attr.get(attrs, "#length-extra", "0"), 0.0), fixed),
3428        _ => text.measure.nodes(kids, skip, acc)))
3429
3430def text.length.matrix(m: Matrix, +ratio: F32, delta: F32) -> Matrix:
3431  Matrix{a, b, c, d, e, f} = m
3432  Matrix{(a * ratio : F32), (b * ratio : F32), c, d, (e + delta : F32), f}
3433
3434def text.length.attrs(+as: List<&2, XML.Attr>, +ratio: F32, +delta: F32) -> List<&2, XML.Attr>:
3435  Con{XML.Attr{"#glyph-transform", matrix.show(text.length.matrix(transform(XML.attr.get(as, "#glyph-transform", ""), identity()), ratio, delta))},
3436    Con{XML.Attr{"#length-x", F32.show((U.scalar(XML.attr.get(as, "#length-x", "0"), 0.0) + delta : F32))},
3437      Con{XML.Attr{"#length-width", F32.show((U.scalar(XML.attr.get(as, "#length-width", "0"), 0.0) * ratio : F32))},
3438        Con{XML.Attr{"#length-extra", F32.show((U.scalar(XML.attr.get(as, "#length-extra", "0"), 0.0) * ratio : F32))}, as}}}}
3439
3440def text.length.translate(xs: List<&2, XML.Element>, +delta: F32) -> List<&2, XML.Element>:
3441  match xs:
3442    case Nil{}: Nil{}
3443    case Con{XML.Element{n, as, kids, txt}, rest}:
3444      +attrs = as
3445      +tagname = n
3446      Con{XML.Element{tagname, U.choose(List<&2, XML.Attr>, U.string.equal(tagname, "#glyph") || U.string.equal(XML.attr.get(attrs, "#length-fixed", ""), "yes"), text.length.attrs(attrs, 1.0, delta), attrs),
3447        text.length.translate(kids, delta), txt}, text.length.translate(rest, delta)}
3448
3449type TextFit is Data:
3450  TextFit{nodes: List<&2, XML.Element>, count: U32, fixed: F32}
3451
3452def text.fit.append(nodes: List<&2, XML.Element>, result: TextFit) -> TextFit:
3453  TextFit{rest, endCount, endFixed} = result
3454  TextFit{text.nodes.append(nodes, rest), endCount, endFixed}
3455
3456def text.fit.then(result: TextFit, next: U32 -> F32 -> TextFit) -> TextFit:
3457  TextFit{nodes, count, fixed} = result
3458  text.fit.append(nodes, next(count)(fixed))
3459
3460def text.fit.group(n: String, as: List<&2, XML.Attr>, txt: String, result: TextFit) -> TextFit:
3461  TextFit{nodes, count, fixed} = result
3462  TextFit{Con{XML.Element{n, as, nodes, txt}, Nil{}}, count, fixed}
3463
3464@unsafe
3465def text.fit.nodes(+xs: List<&2, XML.Element>, +origin: F32, +ratio: F32, +step: F32, +skip: String, +count: U32, +fixed: F32) -> TextFit:
3466  match xs:
3467    case Nil{}: TextFit{Nil{}, count, fixed}
3468    case Con{XML.Element{n, as, kids, txt}, rest}:
3469      +attrs = as
3470      +tagname = n
3471      +locked = U.string.equal(XML.attr.get(attrs, "#length-fixed", ""), "yes")
3472      +glyph = U.string.equal(tagname, "#glyph")
3473      +x = U.scalar(XML.attr.get(attrs, "#length-x", "0"), 0.0)
3474      +delta = ((x - origin - fixed) * (ratio - 1.0) + U32.to_f32(count) * step : F32)
3475      text.fit.then(U.branch(TextFit, locked || (glyph && Bool.not(U.string.equal(XML.attr.get(attrs, "#text-index", ""), skip))),
3476        _ => TextFit{Con{XML.Element{tagname, text.length.attrs(attrs, U.choose(F32, locked, 1.0, ratio), delta),
3477          text.length.translate(kids, delta), txt}, Nil{}}, U32.inc(count), (fixed + U.choose(F32, locked, U.scalar(XML.attr.get(attrs, "#length-width", "0"), 0.0), 0.0) : F32)},
3478        _ => text.fit.group(tagname, attrs, txt, text.fit.nodes(kids, origin, ratio, step, skip, count, fixed))),
3479        nextCount => nextFixed => text.fit.nodes(rest, origin, ratio, step, skip, nextCount, nextFixed))
3480
3481def text.length.finish(fit: TextFit, cursor: TextCursor, delta: F32) -> TextLayout:
3482  TextFit{nodes, count, fixed} = fit
3483  TextCursor{x, y, index, chunk, start, anchor, prev, pc, pf, ps, seen, blankState, trailing, spacing, chunks} = cursor
3484  TextLayout{nodes, TextCursor{(x + delta : F32), y, index, chunk, start, anchor, prev, pc, pf, ps, seen, blankState, trailing, spacing, chunks}}
3485
3486def text.length.fit(+result: TextLayout, +m: TextMeasure, +target: F32, +stretch: Bool, +skip: String) -> TextLayout:
3487  TextLayout{nodes, cursor} = result
3488  TextMeasure{count, lo, hi, extra, fixed} = m
3489  +natural = (hi - lo - extra : F32)
3490  +free = (natural - fixed : F32)
3491  +ratio = U.choose(F32, stretch && F32.is_gt(free, 0.0), (U.max(0.0, (target - fixed : F32)) / U.max(free, 0.000001) : F32), 1.0)
3492  +step = U.choose(F32, Bool.not(stretch) && U32.is_gt(count, 1), ((target - natural) / U.max(1.0, (U32.to_f32(count) - 1.0 : F32)) : F32), 0.0)
3493  +delta = U.choose(F32, stretch, (free * (ratio - 1.0) - extra : F32), U.choose(F32, U32.is_gt(count, 1), (target - natural - extra : F32), 0.0))
3494  U.branch(TextLayout, U32.is_gt(count, 0) && F32.is_gt(natural, 0.0),
3495    _ => text.length.finish(text.fit.nodes(nodes, lo, ratio, step, skip, 0, 0.0), cursor, delta), _ => result)
3496
3497def text.length.valid(num: U.Number) -> Bool:
3498  U.Number{x, tail, ok} = num
3499  ok && F32.is_ge(x, 0.0)
3500
3501def text.length(+result: TextLayout, +as: List<&2, XML.Attr>, w: F32, size: F32, trimEnd: Bool) -> TextLayout:
3502  TextLayout{nodes, cursor} = result
3503  TextCursor{x, y, index, chunk, start, anchor, prev, pc, pf, ps, seen, blankState, trailing, spacing, chunks} = cursor
3504  +target = text.measure(U.number(XML.attr.get(as, "textLength", "-1")), w, size)
3505  +skip = U.choose(String, trimEnd && F32.is_gt(trailing, 0.0), U32.show((index - 1 : U32)), "#none")
3506  U.branch(TextLayout, text.length.valid(U.number(XML.attr.get(as, "textLength", "-1"))) && F32.is_ge(target, 0.0),
3507    _ => text.length.fit(result, text.measure.nodes(nodes, skip, text.measure.empty()), target, U.string.equal(XML.attr.get(as, "lengthAdjust", "spacing"), "spacingAndGlyphs"), skip), _ => result)
3508
3509def text.length.group.attrs(+as: List<&2, XML.Attr>, +m: TextMeasure) -> List<&2, XML.Attr>:
3510  TextMeasure{count, lo, hi, extra, fixed} = m
3511  U.branch(List<&2, XML.Attr>, U32.is_gt(count, 0) && F32.is_ge(U.scalar(XML.attr.get(as, "textLength", "-1"), F32.neg(1.0)), 0.0),
3512    _ => Con{XML.Attr{"#length-fixed", "yes"}, Con{XML.Attr{"#length-x", F32.show(lo)}, Con{XML.Attr{"#length-width", F32.show((hi - lo - extra : F32))}, as}}}, _ => as)
3513
3514def text.layout.group(+as: List<&2, XML.Attr>, +result: TextLayout) -> TextLayout:
3515  TextLayout{nodes, cursor} = result
3516  TextLayout{Con{XML.Element{"g", text.length.group.attrs(as, text.measure.nodes(nodes, "#none", text.measure.empty())), nodes, ""}, Nil{}}, cursor}
3517
3518def text.path.layout(+as: List<&2, XML.Attr>, result: TextLayout, em: F32) -> TextLayout:
3519  TextLayout{nodes, cursor} = result
3520  +edges = text.path.geometry(as)
3521  TextLayout{instance.anonymous(Con{XML.Element{"g", Con{XML.Attr{"#text-path", "1"}, Con{XML.Attr{"#text-path-em", F32.show(em)}, as}}, nodes, ""}, Nil{}}), text.path.end(cursor, text.path.at(edges, text.path.total(edges), identity()))}
3522
3523@unsafe
3524def text.layout(+xs: List<&2, XML.Element>, +font: TextFont, +frames: List<&2, TextFrame>, +cursor: TextCursor, +w: F32, +h: F32, +onPath: Bool) -> TextLayout:
3525  match xs:
3526    case Nil{}: TextLayout{Nil{}, cursor}
3527    case Con{XML.Element{n, attrs, kids, txt}, rest}:
3528      +as = CSS.properties(attrs)
3529      +childFont = text.font.inherit(font, as)
3530      text.layout.then(U.branch(TextLayout, U.string.equal(n, "#text"), _ => text.characters(txt, font, U.choose(List<&2, TextFrame>, onPath, text.path.frames(frames), frames), cursor),
3531        _ => U.branch(TextLayout, U.string.equal(n, "textPath") && Bool.not(U.string.equal(XML.attr.get(as, "display", ""), "none")),
3532          _ => text.path.layout(as, text.length(text.layout(kids, childFont, frames, text.path.begin(cursor), w, h, True{}), as, w, text.font.em(childFont), False{}), text.font.em(childFont)),
3533        _ => U.branch(TextLayout, (U.string.equal(n, "tspan") || U.string.equal(n, "a")) && Bool.not(U.string.equal(XML.attr.get(as, "display", ""), "none")),
3534          _ => text.layout.group(as, text.length(text.layout(kids, childFont, Con{text.frame(as, text.index(cursor), w, h, text.font.em(childFont)), frames}, cursor, w, h, onPath), as, w, text.font.em(childFont), False{})),
3535          _ => TextLayout{Nil{}, cursor}))),
3536        next => text.layout(rest, font, frames, next, w, h, onPath))
3537
3538def text.chunk.shift(xs: List<&2, TextChunk>, +key: U32) -> F32:
3539  match xs:
3540    case Nil{}: 0.0
3541    case Con{TextChunk{id, shift}, rest}: U.branch(F32, U32.is_eq(id, key), _ => shift, _ => text.chunk.shift(rest, key))
3542
3543def text.anchor.nodes(+xs: List<&2, XML.Element>, +chunks: List<&2, TextChunk>, +skip: String) -> List<&2, XML.Element>:
3544  match xs:
3545    case Nil{}: Nil{}
3546    case Con{XML.Element{n, attrs, kids, txt}, rest}:
3547      +tagname = n
3548      +as = attrs
3549      +glyph = U.string.equal(tagname, "#glyph")
3550      +shift = text.chunk.shift(chunks, F32.to_u32(U.scalar(XML.attr.get(as, "#text-chunk", "0"), 0.0)))
3551      +mat = matrix.mul(translate(F32.neg(shift), 0.0), transform(XML.attr.get(as, "#glyph-transform", ""), identity()))
3552      U.branch(List<&2, XML.Element>, glyph && U.string.equal(XML.attr.get(as, "#text-index", ""), skip),
3553        _ => text.anchor.nodes(rest, chunks, skip),
3554        _ => Con{XML.Element{tagname, U.choose(List<&2, XML.Attr>, glyph, Con{XML.Attr{"#glyph-transform", matrix.show(mat)}, as}, as),
3555          text.anchor.nodes(kids, chunks, skip), txt}, text.anchor.nodes(rest, chunks, skip)})
3556
3557def text.path.glyph.matrix(+mat: Matrix, +position: Matrix, +advance: F32) -> Matrix:
3558  Matrix{a, b, c, d, e, f} = mat
3559  Matrix{tx, ty, nx, ny, px, py} = position
3560  +aa = (tx * a + nx * b : F32)
3561  +bb = (ty * a + ny * b : F32)
3562  Matrix{aa, bb, (tx * c + nx * d : F32), (ty * c + ny * d : F32), (px + nx * f - aa * advance / 2.0 : F32), (py + ny * f - bb * advance / 2.0 : F32)}
3563
3564def text.path.glyph(+as: List<&2, XML.Attr>, +edges: List<&2, Edge>, +total: F32, offset: F32, +mat: Matrix) -> List<&2, XML.Element>:
3565  Matrix{a, b, c, d, e, f} = mat
3566  +advance = U.scalar(XML.attr.get(as, "#glyph-advance", "0"), 0.0)
3567  +mid = (e + advance * F32.sqrt((a * a + b * b : F32)) / 2.0 + offset : F32)
3568  U.branch(List<&2, XML.Element>, F32.is_lt(mid, 0.0) || F32.is_gt(mid, total) || F32.is_le(total, 0.0), _ => Nil{},
3569    _ => Con{XML.Element{"#glyph", Con{XML.Attr{"#glyph-transform", matrix.show(text.path.glyph.matrix(mat, text.path.at(edges, mid, identity()), advance))}, as}, Nil{}, ""}, Nil{}})
3570
3571def text.path.map(+xs: List<&2, XML.Element>, +edges: List<&2, Edge>, +total: F32, +offset: F32) -> List<&2, XML.Element>:
3572  match xs:
3573    case Nil{}: Nil{}
3574    case Con{XML.Element{n, as, kids, txt}, rest}:
3575      text.nodes.append(U.branch(List<&2, XML.Element>, U.string.equal(n, "#glyph"), _ => text.path.glyph(as, edges, total, offset, transform(XML.attr.get(as, "#glyph-transform", ""), identity())),
3576        _ => Con{XML.Element{n, as, text.path.map(kids, edges, total, offset), txt}, Nil{}}), text.path.map(rest, edges, total, offset))
3577
3578def text.path.mapped(kids: List<&2, XML.Element>, +as: List<&2, XML.Attr>) -> List<&2, XML.Element>:
3579  +edges = text.path.geometry(as)
3580  +total = text.path.total(edges)
3581  text.path.map(kids, edges, total, text.path.offset(U.number(XML.attr.get(as, "startOffset", "0")), total, U.scalar(XML.attr.get(as, "#text-path-length", "0"), 0.0), U.scalar(XML.attr.get(as, "#text-path-em", "16"), 16.0)))
3582
3583def text.path.finish(+xs: List<&2, XML.Element>) -> List<&2, XML.Element>:
3584  match xs:
3585    case Nil{}: Nil{}
3586    case Con{XML.Element{n, as, kids, txt}, rest}:
3587      Con{XML.Element{n, as, U.branch(List<&2, XML.Element>, U.string.equal(XML.attr.get(as, "#text-path", ""), "1"), _ => text.path.mapped(kids, as), _ => text.path.finish(kids)), txt}, text.path.finish(rest)}
3588
3589def text.cell(+mat: Matrix, +width: F32, box: Bounds) -> Bounds:
3590  bounds.point(bounds.point(bounds.point(bounds.point(box, matrix.point(mat, Point{0.0, F32.neg(293.0)})),
3591    matrix.point(mat, Point{width, F32.neg(293.0)})), matrix.point(mat, Point{width, 1069.0})), matrix.point(mat, Point{0.0, 1069.0}))
3592
3593def text.bounds(xs: List<&2, XML.Element>, +matrix: Matrix, +box: Bounds) -> Bounds:
3594  match xs:
3595    case Nil{}: box
3596    case Con{XML.Element{n, as, kids, txt}, rest}:
3597      +attrs = as
3598      +mat = matrix.mul(matrix, transform(XML.attr.get(attrs, "transform", ""), identity()))
3599      text.bounds(rest, matrix, text.bounds(kids, mat,
3600        U.branch(Bounds, U.string.equal(n, "#glyph"), _ => text.cell(matrix.mul(mat, transform(XML.attr.get(attrs, "#glyph-transform", ""), identity())),
3601          U.scalar(XML.attr.get(attrs, "#glyph-advance", "0"), 0.0), box), _ => box)))
3602
3603def text.bounds.show(box: Bounds) -> String:
3604  Bounds{x0, y0, x1, y1} = box
3605  xy(x0, y0) ++ xy(x1, y1)
3606
3607def text.paint.bounds(xs: List<&2, XML.Element>, +box: String) -> List<&2, XML.Element>:
3608  match xs:
3609    case Nil{}: Nil{}
3610    case Con{XML.Element{n, as, kids, txt}, rest}: Con{XML.Element{n, Con{XML.Attr{"#paint-box", box}, as}, text.paint.bounds(kids, box), txt}, text.paint.bounds(rest, box)}
3611
3612def text.finished.nodes(+nodes: List<&2, XML.Element>, as: List<&2, XML.Attr>) -> XML.Element:
3613  +box = text.bounds.show(text.bounds(nodes, identity(), Bounds{1000000000.0, 1000000000.0, F32.neg(1000000000.0), F32.neg(1000000000.0)}))
3614  XML.Element{"g", Con{XML.Attr{"#paint-box", box}, as}, text.paint.bounds(nodes, box), ""}
3615
3616def text.finish(+result: TextLayout, as: List<&2, XML.Attr>) -> XML.Element:
3617  TextLayout{nodes, cursor} = result
3618  TextCursor{x, y, index, chunk, start, anchor, prev, pc, pf, ps, seen, blankState, trailing, spacing, chunks} = cursor
3619  text.finished.nodes(text.path.finish(text.anchor.nodes(nodes, text.chunk(cursor), U.choose(String, F32.is_gt(trailing, 0.0), U32.show((index - 1 : U32)), "#none"))), as)
3620
3621def text.font.reset(font: TextFont) -> TextFont:
3622  TextFont{size, face, weight, slant, anchor, letter, word, shift, baseline, kern, preserve, book} = font
3623  text.font.default(book)
3624
3625def text.expand.node(+n: String, +as: List<&2, XML.Attr>, +kids: List<&2, XML.Element>, txt: String, +font: TextFont, +vp: Viewport,
3626  expand: List<&2, XML.Element> -> TextFont -> F32 -> F32 -> List<&2, XML.Element>) -> XML.Element:
3627  Viewport{matrix, w, h, clip, valid} = vp
3628  +f = U.choose(TextFont, U.string.equal(n, "#svg-image"), text.font.reset(font), text.font.inherit(font, as))
3629  U.branch(XML.Element, U.string.equal(n, "text"),
3630    _ => text.finish(text.length(text.layout(kids, f, Con{text.frame(as, 0, w, h, text.font.em(f)), Nil{}}, text.cursor(), w, h, False{}), as, w, text.font.em(f), True{}), as),
3631    _ => XML.Element{n, as, expand(kids)(f)(w)(h), txt})
3632
3633@unsafe
3634def text.expand(xs: List<&2, XML.Element>, +font: TextFont, +w: F32, +h: F32, +root: Bool) -> List<&2, XML.Element>:
3635  match xs:
3636    case Nil{}: Nil{}
3637    case Con{XML.Element{n, as, kids, txt}, rest}:
3638      +tagname = n
3639      +attrs = as
3640      Con{text.expand.node(tagname, attrs, kids, txt, font, element.viewport(tagname, attrs, identity(), w, h, root),
3641        nodes => f => vw => vh => text.expand(nodes, f, vw, vh, False{})), text.expand(rest, font, w, h, root)}
3642
3643type TextPaintRun is Data:
3644  TextPaintRun{nodes: List<&2, XML.Element>, rest: List<&2, XML.Element>}
3645
3646def text.paint.reverse(xs: List<&2, XML.Element>, acc: List<&2, XML.Element>) -> List<&2, XML.Element>:
3647  match xs:
3648    case Nil{}: acc
3649    case Con{h, t}: text.paint.reverse(t, Con{h, acc})
3650
3651@unsafe
3652def text.paint.take(+xs: List<&2, XML.Element>, +acc: List<&2, XML.Element>) -> TextPaintRun:
3653  match xs:
3654    case Nil{}: TextPaintRun{text.paint.reverse(acc, Nil{}), Nil{}}
3655    case Con{XML.Element{n, as, kids, txt}, rest}:
3656      +name = n
3657      U.branch(TextPaintRun, U.string.equal(name, "#glyph"),
3658        _ => text.paint.take(rest, Con{XML.Element{name, as, kids, txt}, acc}), _ => TextPaintRun{text.paint.reverse(acc, Nil{}), xs})
3659
3660def text.paint.attrs(xs: List<&2, XML.Element>) -> List<&2, XML.Attr>:
3661  match xs:
3662    case Nil{}: Nil{}
3663    case Con{XML.Element{n, as, kids, txt}, rest}: Con{XML.Attr{"fill-rule", "nonzero"}, as}
3664
3665def text.paint.run(run: TextPaintRun, next: List<&2, XML.Element> -> List<&2, XML.Element>) -> List<&2, XML.Element>:
3666  TextPaintRun{nodes, rest} = run
3667  +glyphs = nodes
3668  Con{XML.Element{"#glyph-run", text.paint.attrs(glyphs), glyphs, ""}, next(rest)}
3669
3670@unsafe
3671def text.paint.group(+xs: List<&2, XML.Element>) -> List<&2, XML.Element>:
3672  match xs:
3673    case Nil{}: Nil{}
3674    case Con{XML.Element{n, as, kids, txt}, rest}:
3675      +name = n
3676      U.branch(List<&2, XML.Element>, U.string.equal(name, "#glyph"), _ => text.paint.run(text.paint.take(xs, Nil{}), remaining => text.paint.group(remaining)),
3677        _ => Con{XML.Element{name, as, text.paint.group(kids), txt}, text.paint.group(rest)})
3678
3679# Resolve context paints after compiling the finite scene tree. Paint server
3680# coordinates stay in the referencing shape's space, including bounding boxes.
3681def context.unfaded(+brush: Brush) -> Brush:
3682  match brush:
3683    case Bitmap{raster, inv, nearest}: Bitmap{raster, inv, nearest}
3684    case Solid{col, source}: Solid{source, source}
3685    case PendingContext{stroke, opacity}: PendingContext{stroke, 1.0}
3686    case ContextPaint{source, matrix, opacity}: ContextPaint{source, matrix, 1.0}
3687    case Pattern{inverse, width, height, opacity, nodes}: Pattern{inverse, width, height, 1.0, nodes}
3688    case Tile{raster, inverse, width, height, opacity, u, v, grid}: Tile{raster, inverse, width, height, 1.0, u, v, grid}
3689    case Gradient{inv, radial, start, end, radius, fr, stops, spread, opacity, linear}: Gradient{inv, radial, start, end, radius, fr, stops, spread, 1.0, linear}
3690
3691def context.select(paint: Paint<Brush>, stroke: Bool) -> Brush:
3692  Paint{f, s} = paint
3693  context.unfaded(U.choose(Brush, stroke, s, f))
3694
3695def context.brush(brush: Brush, paint: Paint<Brush>, matrix: Matrix, patterns: List<&2, Scene<Brush>> -> List<&2, Scene<Brush>>) -> Brush:
3696  match brush:
3697    case Bitmap{raster, inv, nearest}: Bitmap{raster, inv, nearest}
3698    case Solid{col, source}: Solid{col, source}
3699    case PendingContext{stroke, opacity}: ContextPaint{context.select(paint, stroke), matrix, opacity}
3700    case ContextPaint{source, matrix, opacity}: ContextPaint{source, matrix, opacity}
3701    case Pattern{inverse, width, height, opacity, nodes}: Pattern{inverse, width, height, opacity, patterns(nodes)}
3702    case Tile{raster, inverse, width, height, opacity, u, v, grid}: Tile{raster, inverse, width, height, opacity, u, v, grid}
3703    case Gradient{inv, radial, start, end, radius, fr, stops, spread, opacity, linear}: Gradient{inv, radial, start, end, radius, fr, stops, spread, opacity, linear}
3704
3705def context.empty() -> Paint<Brush>:
3706  Paint{Solid{Img.clear(), Img.clear()}, Solid{Img.clear(), Img.clear()}}
3707
3708def context.paint(ink: Paint<Brush>, +paint: Paint<Brush>, +matrix: Matrix, fills: List<&2, Scene<Brush>> -> List<&2, Scene<Brush>>, strokes: List<&2, Scene<Brush>> -> List<&2, Scene<Brush>>) -> Paint<Brush>:
3709  Paint{f, s} = ink
3710  Paint{context.brush(f, paint, matrix, fills), context.brush(s, paint, matrix, strokes)}
3711
3712def stroke.mesh.bounds(mesh: StrokeMesh) -> Bounds:
3713  StrokeMesh{stree, inv, cover} = mesh
3714  bounds.transform(edge.tree.bounds(stree), matrix.inverse(inv))
3715
3716def scene.count(xs: List<&2, Scene<Brush>>) -> U32:
3717  match xs:
3718    case Nil{}: 0
3719    case Con{node, rest}: (1 + scene.count(rest) : U32)
3720
3721def scene.coverage(xs: List<&2, Scene<Brush>>, box: Bounds) -> Bounds:
3722  match xs:
3723    case Nil{}: box
3724    case Con{Scene{es, objectBox, inv, style, opacity, kids, id, ink, clip, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect}, rest}:
3725      scene.coverage(rest, bounds.merge(box, coverage))
3726
3727def scene.effect.coverage(effect: FilterEffect, cached: Bounds, computed: Bounds) -> Bounds:
3728  match effect:
3729    case FilterImage{raster}: cached
3730    case NoFilter{}: computed
3731    case FilterSpec{region, units, viewport, attrs, primitives}: computed
3732
3733def scene.covered(+scene: Scene<Brush>) -> Scene<Brush>:
3734  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect} = scene
3735  +own = bounds.merge(bounds.transform(fill.bounds(fillTree), matrix.inverse(inv)), stroke.mesh.bounds(outline))
3736  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, maskNodes, maskMode, markerNodes, scene.effect.coverage(effect, coverage, scene.coverage(markerNodes, scene.coverage(kids, own))), effect}
3737
3738def context.node(scene: Scene<Brush>, +ink: Paint<Brush>, kids: List<&2, Scene<Brush>>, masks: List<&2, Scene<Brush>>, markers: Paint<Brush> -> List<&2, Scene<Brush>>) -> Scene<Brush>:
3739  Scene{es, box, inv, style, opacity, oldKids, id, oldInk, clip, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect} = scene
3740  scene.covered(Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, masks, maskMode, markers(ink), bounds.empty(), effect})
3741
3742@unsafe
3743def context.scenes(+xs: List<&2, Scene<Brush>>, +paint: Paint<Brush>, +parent: Matrix) -> List<&2, Scene<Brush>>:
3744  match xs:
3745    case Nil{}: Nil{}
3746    case Con{Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect}, rest}:
3747      Con{context.node(Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect},
3748        context.paint(ink, paint, matrix.mul(parent, matrix.inverse(inv)),
3749          nodes => context.scenes(nodes, context.empty(), identity()), nodes => context.scenes(nodes, context.empty(), identity())),
3750        context.scenes(kids, paint, parent), context.scenes(maskNodes, context.empty(), identity()),
3751        source => context.scenes(markerNodes, source, inv)), context.scenes(rest, paint, parent)}
3752
3753def image.svg.stack.empty(stack: List<&2, String>) -> Bool:
3754  match stack:
3755    case Nil{}: True{}
3756    case Con{head, tail}: False{}
3757
3758@unsafe
3759def image.svg.wellformed(+tokens: List<&2, XML.Token>, +stack: List<&2, String>, +seen: Bool) -> Bool:
3760  match tokens stack:
3761    case Nil{} Nil{}: seen
3762    case Nil{} Con{head, tail}: False{}
3763    case Con{XML.Chars{content}, rest} stack:
3764      (Bool.not(image.svg.stack.empty(stack)) || U.string.equal(U.trim.both(content), "")) && image.svg.wellformed(rest, stack, seen)
3765    case Con{XML.Open{name, attrs, closed}, rest} stack:
3766      +top = image.svg.stack.empty(stack)
3767      U.branch(Bool, Bool.not(top) || (Bool.not(seen) && U.string.equal(name, "svg")),
3768        _ => image.svg.wellformed(rest, U.choose(List<&2, String>, closed, stack, Con{name, stack}), seen || top), _ => False{})
3769    case Con{XML.Shut{name}, rest} Nil{}: False{}
3770    case Con{XML.Shut{name}, rest} Con{head, tail}:
3771      U.string.equal(U.trim.both(name), head) && image.svg.wellformed(rest, tail, seen)
3772
3773def image.svg.read(+text: String) -> XML.Element:
3774  +source = U.choose(String, U.prefix(text, ""), U.drop(text, ""), text)
3775  U.branch(XML.Element, image.svg.wellformed(XML.tokenize(source), Nil{}, False{}), _ => XML.parse(source), _ => XML.Element{"#document", Nil{}, Nil{}, ""})
3776
3777# Expand data-URI SVG images into isolated vector subdocuments.
3778type ImageCodepoint is Data:
3779  ImageCodepoint{point: U32, step: U32}
3780
3781@unsafe
3782def image.svg.utf8.tail(+data: Bin.Bytes, +at: U32, left: Nat, +acc: U32, +minimum: U32, +step: U32) -> ImageCodepoint:
3783  match left:
3784    case Zero{}: U.choose(ImageCodepoint, U32.is_ge(acc, minimum) && U32.is_le(acc, 1114111) && Bool.not(U32.is_ge(acc, 55296) && U32.is_le(acc, 57343)), ImageCodepoint{acc, step}, ImageCodepoint{65533, 1})
3785    case Succ{rest}:
3786      +v = Bin.bytes.get(data, at)
3787      U.branch(ImageCodepoint, U32.is_ge(v, 128) && U32.is_le(v, 191), _ => image.svg.utf8.tail(data, U32.inc(at), rest, (acc * 64 + v % 64 : U32), minimum, step), _ => ImageCodepoint{65533, 1})
3788
3789def image.svg.utf8(+data: Bin.Bytes, +at: U32) -> ImageCodepoint:
3790  +v = Bin.bytes.get(data, at)
3791  U.branch(ImageCodepoint, U32.is_lt(v, 128), _ => ImageCodepoint{v, 1},
3792    _ => U.branch(ImageCodepoint, U32.is_ge(v, 194) && U32.is_le(v, 223), _ => image.svg.utf8.tail(data, U32.inc(at), 1n, (v % 32 : U32), 128, 2),
3793      _ => U.branch(ImageCodepoint, U32.is_ge(v, 224) && U32.is_le(v, 239), _ => image.svg.utf8.tail(data, U32.inc(at), 2n, (v % 16 : U32), 2048, 3),
3794        _ => U.branch(ImageCodepoint, U32.is_ge(v, 240) && U32.is_le(v, 244), _ => image.svg.utf8.tail(data, U32.inc(at), 3n, (v % 8 : U32), 65536, 4), _ => ImageCodepoint{65533, 1}))))
3795
3796def image.svg.utf8.more(decoded: ImageCodepoint, acc: String, next: U32 -> String -> String) -> String:
3797  ImageCodepoint{point, step} = decoded
3798  next(step)(SCon{Chr{point}, acc})
3799
3800@unsafe
3801def image.svg.byte.text(+data: Bin.Bytes, +at: U32, +acc: String) -> String:
3802  U.branch(String, U32.is_ge(at, Bin.bytes.size(data)), _ => U.rev(acc, ""), _ => image.svg.utf8.more(image.svg.utf8(data, at), acc, step => text => image.svg.byte.text(data, (at + step : U32), text)))
3803
3804def image.svg.utf8.push(+out: Bin.Bytes, +c: U32) -> Bin.Bytes:
3805  U.branch(Bin.Bytes, U32.is_lt(c, 128), _ => Bin.bytes.push(out, c),
3806    _ => U.branch(Bin.Bytes, U32.is_lt(c, 2048), _ => Bin.bytes.push(Bin.bytes.push(out, (192 + c / 64 : U32)), (128 + c % 64 : U32)),
3807      _ => U.branch(Bin.Bytes, U32.is_lt(c, 65536), _ => Bin.bytes.push(Bin.bytes.push(Bin.bytes.push(out, (224 + c / 4096 : U32)), (128 + (c / 64) % 64 : U32)), (128 + c % 64 : U32)),
3808        _ => Bin.bytes.push(Bin.bytes.push(Bin.bytes.push(Bin.bytes.push(out, (240 + c / 262144 : U32)), (128 + (c / 4096) % 64 : U32)), (128 + (c / 64) % 64 : U32)), (128 + c % 64 : U32)))))
3809
3810def image.svg.uri.bytes(+s: String, +out: Bin.Bytes) -> Bin.Bytes:
3811  match s:
3812    case SNil{}: out
3813    case SCon{Chr{37}, SCon{Chr{a}, SCon{Chr{b}, rest}}}:
3814      U.branch(Bin.Bytes, Bin.uri.hex.valid(a) && Bin.uri.hex.valid(b), _ => image.svg.uri.bytes(rest, Bin.bytes.push(out, (U.hex.digit(a) * 16 + U.hex.digit(b) : U32))), _ => Bin.bytes.empty())
3815    case SCon{Chr{c}, rest}:
3816      U.branch(Bin.Bytes, Bool.not(U32.is_eq(c, 37)), _ => image.svg.uri.bytes(rest, image.svg.utf8.push(out, c)), _ => Bin.bytes.empty())
3817
3818def image.svg.source(+scan: U.Scan) -> String:
3819  U.Scan{mime, data} = scan
3820  +body = U.branch(Bin.Bytes, U.string.contains(CSS.css.lower(mime), ";base64"), _ => Bin.base64(data), _ => image.svg.uri.bytes(data, Bin.bytes.empty()))
3821  image.svg.byte.text(body, 0, "")
3822
3823def image.svg.root(+xs: List<&2, XML.Element>) -> XML.Element:
3824  match xs:
3825    case Nil{}: XML.Element{"#empty", Nil{}, Nil{}, ""}
3826    case Con{XML.Element{n, as, kids, txt}, rest}:
3827      U.branch(XML.Element, U.string.equal(n, "svg"), _ => XML.Element{n, as, kids, txt}, _ => image.svg.root(rest))
3828
3829def image.svg.document(root: XML.Element) -> XML.Element:
3830  XML.Element{n, as, kids, txt} = root
3831  image.svg.root(kids)
3832
3833# Intrinsic dimensions come from the root's own width/height attributes; the
3834# document stylesheet only styles the content once the viewport is known.
3835def image.svg.attrs(root: XML.Element) -> List<&2, XML.Attr>:
3836  XML.Element{n, as, kids, txt} = root
3837  as
3838
3839def image.svg.absolute(+s: String) -> F32:
3840  U.branch(F32, U.string.equal(s, "auto") || U.string.contains(s, "%") || Bool.not(text.length.valid(U.number(s))), _ => F32.neg(1.0), _ => U.unit(U.number(s), 0.0))
3841
3842def image.svg.ratio(+as: List<&2, XML.Attr>) -> F32:
3843  +w = image.svg.absolute(XML.attr.get(as, "width", "auto"))
3844  +h = image.svg.absolute(XML.attr.get(as, "height", "auto"))
3845  +vb = U.numbers(XML.attr.get(as, "viewBox", ""))
3846  +vw = U.nth(vb, 2n, 0.0)
3847  +vh = U.nth(vb, 3n, 0.0)
3848  U.choose(F32, F32.is_ge(w, 0.0) && F32.is_gt(h, 0.0), (w / U.max(h, 0.000001) : F32), U.choose(F32, F32.is_gt(vw, 0.0) && F32.is_gt(vh, 0.0), (vw / U.max(vh, 0.000001) : F32), 0.0))
3849
3850def image.svg.size(+as: List<&2, XML.Attr>) -> Point:
3851  +w = image.svg.absolute(XML.attr.get(as, "width", "auto"))
3852  +h = image.svg.absolute(XML.attr.get(as, "height", "auto"))
3853  +ratio = image.svg.ratio(as)
3854  +hasW = F32.is_ge(w, 0.0)
3855  +hasH = F32.is_ge(h, 0.0)
3856  +hasRatio = F32.is_gt(ratio, 0.0)
3857  +dw = U.choose(F32, hasRatio, U.min(300.0, (150.0 * ratio : F32)), 300.0)
3858  +dh = U.choose(F32, hasRatio, (dw / U.max(ratio, 0.000001) : F32), 150.0)
3859  Point{U.choose(F32, hasW, w, U.choose(F32, hasH && hasRatio, (h * ratio : F32), dw)), U.choose(F32, hasH, h, U.choose(F32, hasW && hasRatio, (w / U.max(ratio, 0.000001) : F32), dh))}
3860
3861def image.svg.normalized(root: XML.Element, size: Point) -> XML.Element:
3862  XML.Element{n, as, kids, txt} = root
3863  Point{w, h} = size
3864  XML.Element{n, Con{XML.Attr{"width", F32.show(w)}, Con{XML.Attr{"height", F32.show(h)}, as}}, kids, txt}
3865
3866def image.svg.place(+as: List<&2, XML.Attr>, +size: Point, +root: XML.Element,
3867  expand: List<&2, XML.Element> -> F32 -> F32 -> List<&2, XML.Element>) -> XML.Element:
3868  Point{iw, ih} = size
3869  +ratio = image.svg.ratio(image.svg.attrs(root))
3870  +metadata = F32.show(iw) ++ " " ++ F32.show(ih) ++ " " ++ F32.show(ratio)
3871  +inside = {Con{root, Nil{}} : List<&2, XML.Element>}
3872  +styled = {Con{image.svg.normalized(image.svg.root(CSS.css.tree(inside, CSS.css.rules(CSS.css.sheets(inside, iw, ih), iw, ih), Nil{}, Nil{}, 1, 1)), size), Nil{}} : List<&2, XML.Element>}
3873  +frame = {Con{XML.Attr{"#image-size", metadata}, Con{XML.Attr{"x", XML.attr.get(as, "x", "0")}, Con{XML.Attr{"y", XML.attr.get(as, "y", "0")}, Con{XML.Attr{"width", XML.attr.get(as, "width", "auto")}, Con{XML.Attr{"height", XML.attr.get(as, "height", "auto")}, Con{XML.Attr{"viewBox", "0 0 " ++ F32.show(iw) ++ " " ++ F32.show(ih)}, Con{XML.Attr{"preserveAspectRatio", U.choose(String, F32.is_gt(ratio, 0.0), XML.attr.get(as, "preserveAspectRatio", "xMidYMid meet"), "none")}, Con{XML.Attr{"overflow", XML.attr.get(as, "overflow", "hidden")}, Nil{}}}}}}}}} : List<&2, XML.Attr>}
3874  XML.Element{"#svg-image", Con{XML.Attr{"#image-size", metadata}, as}, Con{XML.Element{"svg", frame, instance.anonymous(expand(styled)(iw)(ih)), ""}, Nil{}}, ""}
3875
3876def image.svg.sized(as: List<&2, XML.Attr>, +root: XML.Element,
3877  expand: List<&2, XML.Element> -> F32 -> F32 -> List<&2, XML.Element>) -> XML.Element:
3878  image.svg.place(as, image.svg.size(image.svg.attrs(root)), root, expand)
3879
3880def image.svg.node(as: List<&2, XML.Attr>, +root: XML.Element,
3881  expand: List<&2, XML.Element> -> F32 -> F32 -> List<&2, XML.Element>) -> XML.Element:
3882  image.svg.sized(as, root, expand)
3883
3884def image.svg.mime.name(scan: U.Scan) -> Bool:
3885  U.Scan{name, rest} = scan
3886  U.string.equal(CSS.css.lower(U.trim.both(name)), "image/svg+xml")
3887
3888def image.svg.mime(scan: U.Scan) -> Bool:
3889  U.Scan{name, data} = scan
3890  image.svg.mime.name(U.until(name, 59, ""))
3891
3892def image.svg.scheme(scan: U.Scan) -> Bool:
3893  U.Scan{name, rest} = scan
3894  U.string.equal(CSS.css.lower(name), "data") && image.svg.mime(U.until(rest, 44, ""))
3895
3896def image.svg.reference(href: String) -> Bool:
3897  image.svg.scheme(U.until(href, 58, ""))
3898
3899@unsafe
3900def image.svg.expand(+fuel: Nat, +xs: List<&2, XML.Element>, +w: F32, +h: F32) -> List<&2, XML.Element>:
3901  match fuel xs:
3902    case Zero{} _: Nil{}
3903    case Succ{d} Nil{}: Nil{}
3904    case Succ{d} Con{XML.Element{n, attrs, kids, txt}, rest}:
3905      +as = CSS.properties(attrs)
3906      +href = U.trim.both(XML.attr.get(as, "href", XML.attr.get(as, "xlink:href", "")))
3907      Con{U.branch(XML.Element, U.string.equal(n, "image") && image.svg.reference(href),
3908        _ => image.svg.node(as, image.svg.document(image.svg.read(image.svg.source(U.until(U.drop(href, "data:"), 44, "")))), nodes => iw => ih => image.svg.expand(d, nodes, iw, ih)),
3909        _ => XML.Element{n, attrs, image.svg.expand(fuel, kids, w, h), txt}), image.svg.expand(fuel, rest, w, h)}
3910
3911# Font loading must include text hidden inside an SVG image data URI.
3912def text.present(+xs: List<&2, XML.Element>) -> Bool:
3913  match xs:
3914    case Nil{}: False{}
3915    case Con{XML.Element{n, as, kids, txt}, rest}: U.string.equal(n, "text") || (U.string.equal(n, "image") && image.svg.reference(U.trim.both(XML.attr.get(as, "href", XML.attr.get(as, "xlink:href", ""))))) || text.present(kids) || text.present(rest)
3916
3917# The expansion passes each rebuild the whole tree, so a document without
3918# images, uses or text skips them; an embedded SVG image may bring any of the
3919# three, so images imply all passes.
3920def expand.images(has: Bool, xs: List<&2, XML.Element>, +w: F32, +h: F32) -> List<&2, XML.Element>:
3921  match has:
3922    case True{}: image.svg.expand(16n, xs, w, h)
3923    case False{}: xs
3924
3925def expand.uses(has: Bool, +xs: List<&2, XML.Element>) -> List<&2, XML.Element>:
3926  match has:
3927    case True{}: use.expand(64n, xs, xs, Nil{})
3928    case False{}: xs
3929
3930def expand.text(has: Bool, +xs: List<&2, XML.Element>, book: Font.FontBook, +w: F32, +h: F32) -> List<&2, XML.Element>:
3931  match has:
3932    case True{}: text.paint.group(text.expand(text.path.resolve(xs, xs, w, h, True{}), text.font.default(book), w, h, True{}))
3933    case False{}: xs
3934
3935# Without a stylesheet the CSS pass would only merge style attributes, which
3936# CSS.properties does on demand, so the tree is used as parsed.
3937def document.styled(has: Bool, +kids: List<&2, XML.Element>, +w: F32, +h: F32) -> List<&2, XML.Element>:
3938  match has:
3939    case True{}: CSS.css.tree(kids, CSS.css.rules(CSS.css.sheets(kids, w, h), w, h), Nil{}, Nil{}, 1, CSS.css.element.count(kids))
3940    case False{}: kids
3941
3942def document.camera.fonts(+root: XML.Element, +w: F32, +h: F32, matrix: Matrix, book: Font.FontBook) -> List<&2, Scene<Brush>>:
3943  XML.Element{n, a, kids, txt} = root
3944  +kinds = XML.elements.kinds(kids, 0)
3945  +images = U32.is_gt(U32.and(kinds, 1), 0)
3946  +styled = document.styled(images || U32.is_gt(U32.and(kinds, 8), 0), kids, w, h)
3947  +nodes = expand.text(images || U32.is_gt(U32.and(kinds, 4), 0), expand.uses(images || U32.is_gt(U32.and(kinds, 2), 0), expand.images(images, styled, w, h)), book, w, h)
3948  context.scenes(compile(64n, nodes, style.default(), matrix, w, h, resources(nodes, a), True{}, Nil{}, 1.0), context.empty(), identity())
3949
3950def document.fonts(root: XML.Element, w: F32, h: F32, book: Font.FontBook) -> List<&2, Scene<Brush>>:
3951  document.camera.fonts(root, w, h, identity(), book)
3952
3953# The document viewport and the output matrix have independent dimensions.
3954def document.size(+root: XML.Element, +w: F32, +h: F32) -> Point:
3955  XML.Element{n, attrs, kids, txt} = root
3956  +kinds = XML.elements.kinds(kids, 0)
3957  +styled = document.styled(U32.is_gt(U32.and(kinds, 9), 0), kids, w, h)
3958  +as = image.svg.attrs(image.svg.root(styled))
3959  Point{U.max(0.0, viewport.dimension(as, "width", w)), U.max(0.0, viewport.dimension(as, "height", h))}
3960
3961def document.fit.matrix(size: Point, +w: F32, +h: F32) -> Matrix:
3962  Point{width, height} = size
3963  +vw = width
3964  +vh = height
3965  +zoom = U.min((w / U.max(vw, 0.000001) : F32), (h / U.max(vh, 0.000001) : F32))
3966  Matrix{zoom, 0.0, 0.0, zoom, ((w - vw * zoom) * 0.5 : F32), ((h - vh * zoom) * 0.5 : F32)}
3967
3968def document.natural.size(root: XML.Element, matrix: Matrix, book: Font.FontBook, size: Point) -> List<&2, Scene<Brush>>:
3969  Point{vw, vh} = size
3970  document.camera.fonts(root, vw, vh, matrix, book)
3971
3972def document.natural.fonts(+root: XML.Element, +w: F32, +h: F32, matrix: Matrix, book: Font.FontBook) -> List<&2, Scene<Brush>>:
3973  document.natural.size(root, matrix, book, document.size(root, w, h))
3974
3975def document.fitted.size(root: XML.Element, w: F32, h: F32, book: Font.FontBook, +size: Point) -> List<&2, Scene<Brush>>:
3976  Point{vw, vh} = size
3977  document.camera.fonts(root, vw, vh, document.fit.matrix(size, w, h), book)
3978
3979def document.fitted.fonts(+root: XML.Element, +w: F32, +h: F32, book: Font.FontBook) -> List<&2, Scene<Brush>>:
3980  document.fitted.size(root, w, h, book, document.size(root, w, h))
3981
3982def document(root: XML.Element, w: F32, h: F32) -> List<&2, Scene<Brush>>:
3983  document.fonts(root, w, h, Font.font.empty())
3984
3985# Every fill, stroke and clip carries its analytic coverage (see cover.scenes), so
3986# one sample at the pixel center reads exact area coverage; paint is evaluated there.
3987def pixel.color(+scene: List<&2, Scene<Brush>>, x: U32, y: U32, +aa: U32) -> Img.Color:
3988  +center = {Point{(U32.to_f32(x) + 0.5 : F32), (U32.to_f32(y) + 0.5 : F32)} : Point}
3989  sample(scene, center, center, Img.clear(), False{}, 1.0)
3990
3991def channel(x: F32) -> U32:
3992  F32.to_u32(F32.floor((U.clamp(x, 0.0, 1.0) * 255.0 + 0.5 : F32)))
3993
3994def color.pack(c: Img.Color) -> U32:
3995  Img.Color{r, g, b, a} = c
3996  (channel(r) * 65536 + channel(g) * 256 + channel(b) : U32)
3997
3998def pixel(+scene: List<&2, Scene<Brush>>, x: U32, y: U32, aa: U32) -> U32:
3999  color.pack(color.over(pixel.color(scene, x, y, aa), color.rgb(16777215)))
4000
4001def bounds.nonempty(b: Bounds) -> Bool:
4002  Bounds{x0, y0, x1, y1} = b
4003  F32.is_gt(x1, x0) && F32.is_gt(y1, y0)
4004
4005def bounds.expand(b: Bounds, +margin: F32) -> Bounds:
4006  Bounds{x0, y0, x1, y1} = b
4007  Bounds{(x0 - margin : F32), (y0 - margin : F32), (x1 + margin : F32), (y1 + margin : F32)}
4008
4009def bounds.overlap(a: Bounds, b: Bounds) -> Bool:
4010  Bounds{x0, y0, x1, y1} = a
4011  Bounds{u0, v0, u1, v1} = b
4012  F32.is_le(x0, u1) && F32.is_le(u0, x1) && F32.is_le(y0, v1) && F32.is_le(v0, y1)
4013
4014@unsafe
4015def raster.depth(+size: U32, +side: U32, +depth: Nat) -> Nat:
4016  U.branch(Nat, U32.is_ge(side, size), _ => depth, _ => raster.depth(size, (side * 2 : U32), 1n+depth))
4017
4018def raster.format(box: Bounds, requested: F32) -> Raster:
4019  Bounds{x0, y0, x1, y1} = box
4020  +x = x0
4021  +y = y0
4022  +right = x1
4023  +bottom = y1
4024  +step = U.max(U.max(requested, 0.000001), (U.max((right - x : F32), (bottom - y : F32)) / 1022.0 : F32))
4025  +ox = (F32.floor((x / step : F32)) * step : F32)
4026  +oy = (F32.floor((y / step : F32)) * step : F32)
4027  +w = F32.to_u32(U.clamp(F32.ceil(((right - ox) / step : F32)), 0.0, 1024.0))
4028  +h = F32.to_u32(U.clamp(F32.ceil(((bottom - oy) / step : F32)), 0.0, 1024.0))
4029  Raster{Point{ox, oy}, step, w, h, raster.depth(U.choose(U32, U32.is_gt(w, h), w, h), 1, 0n), False{}, Img.Flat{Img.clear()}}
4030
4031def raster.empty() -> Raster:
4032  Raster{Point{0.0, 0.0}, 1.0, 0, 0, 0n, False{}, Img.Flat{Img.clear()}}
4033
4034def raster.store(format: Raster, pixels: Img.Pixels, linear: Bool) -> Raster:
4035  Raster{origin, step, width, height, depth, oldLinear, oldPixels} = format
4036  Raster{origin, step, width, height, depth, linear, pixels}
4037
4038def raster.position(+format: Raster, x: F32, y: F32) -> Point:
4039  Raster{Point{ox, oy}, step, width, height, depth, pixelsLinear, pixels} = format
4040  Point{(ox + x * step : F32), (oy + y * step : F32)}
4041
4042def raster.scale(format: Raster) -> F32:
4043  Raster{origin, step, width, height, depth, pixelsLinear, pixels} = format
4044  step
4045
4046def raster.is.linear(format: Raster) -> Bool:
4047  Raster{origin, step, width, height, depth, pixelsLinear, pixels} = format
4048  pixelsLinear
4049
4050def raster.tile.box(+format: Raster, +x: U32, +y: U32, size: U32) -> Bounds:
4051  +n = U32.to_f32(size)
4052  +xx = U32.to_f32(x)
4053  +yy = U32.to_f32(y)
4054  bounds.point(bounds.point(bounds.empty(), raster.position(format, xx, yy)), raster.position(format, (xx + n : F32), (yy + n : F32)))
4055
4056def raster.in.frame(format: Raster, x: U32, y: U32) -> Bool:
4057  Raster{origin, step, width, height, depth, pixelsLinear, pixels} = format
4058  U32.is_lt(x, width) && U32.is_lt(y, height)
4059
4060def raster.scene.pixel(+format: Raster, +x: U32, +y: U32, nodes: List<&2, Scene<Brush>>, +aa: U32) -> Img.Color:
4061  +center = raster.position(format, (U32.to_f32(x) + 0.5 : F32), (U32.to_f32(y) + 0.5 : F32))
4062  sample(nodes, center, center, Img.clear(), False{}, 1.0)
4063
4064def raster.scene.tiles(depth: Nat, +x: U32, +y: U32, +format: Raster, +nodes: List<&2, Scene<Brush>>, +box: Bounds, +aa: U32) -> Img.Pixels:
4065  match depth:
4066    case 0n:
4067      U.branch(Img.Pixels, raster.in.frame(format, x, y) && bounds.overlap(box, raster.tile.box(format, x, y, 1)),
4068        _ => Img.Flat{raster.scene.pixel(format, x, y, nodes, aa)}, _ => Img.Flat{Img.clear()})
4069    case 1n+d:
4070      +dd = d
4071      +half = U32.shln(1, dd)
4072      U.branch(Img.Pixels, raster.in.frame(format, x, y) && bounds.overlap(box, raster.tile.box(format, x, y, (half * 2 : U32))),
4073        _ => Img.pixels.join(raster.scene.tiles(dd, x, y, format, nodes, box, aa), raster.scene.tiles(dd, (x + half : U32), y, format, nodes, box, aa), raster.scene.tiles(dd, x, (y + half : U32), format, nodes, box, aa), raster.scene.tiles(dd, (x + half : U32), (y + half : U32), format, nodes, box, aa)), _ => Img.Flat{Img.clear()})
4074
4075# ---- coverage pass ----
4076# Before a raster is painted, every fill, stroke outline and clip shape gets a
4077# coverage mask on that raster's grid (cover.bend), and every pattern brush is
4078# rendered once into a tile raster at the grid's resolution; sampling then reads
4079# one value per pixel instead of testing a grid of sub-samples.
4080
4081# Sub-scanlines per pixel row: the requested AA level, never below four.
4082def cover.sub(+aa: U32) -> U32:
4083  U.choose(U32, U32.is_lt(aa, 4), 4, aa)
4084
4085def cover.pixel(+frame: Raster, p: Point) -> Point:
4086  Raster{Point{ox, oy}, step, width, height, depth, linear, pixels} = frame
4087  Point{x, y} = p
4088  +stepDup = step
4089  Point{((x - ox) / stepDup : F32), ((y - oy) / stepDup : F32)}
4090
4091def cover.line(a: Point, b: Point) -> Cov.CovLine:
4092  Point{ax, ay} = a
4093  Point{bx, by} = b
4094  Cov.CovLine{ax, ay, bx, by}
4095
4096def cover.lines(es: List<&2, Edge>, +frame: Raster) -> List<&2, Cov.CovLine>:
4097  match es:
4098    case Nil{}: Nil{}
4099    case Con{Edge{ax, ay, bx, by, stroked, closed}, rest}: Con{cover.line(cover.pixel(frame, Point{ax, ay}), cover.pixel(frame, Point{bx, by})), cover.lines(rest, frame)}
4100
4101# Local edges through m onto the frame's pixel grid in one pass.
4102def cover.lines.add(acc: Cov.CovLines, a: Point, b: Point) -> Cov.CovLines:
4103  Point{ax, ay} = a
4104  Point{bx, by} = b
4105  Cov.lines.add(acc, ax, ay, bx, by)
4106
4107def cover.lines.through(es: List<&2, Edge>, +m: Matrix, +frame: Raster, acc: Cov.CovLines) -> Cov.CovLines:
4108  match es:
4109    case Nil{}: acc
4110    case Con{Edge{ax, ay, bx, by, stroked, closed}, rest}: cover.lines.through(rest, m, frame, cover.lines.add(acc, cover.pixel(frame, matrix.point(m, Point{ax, ay})), cover.pixel(frame, matrix.point(m, Point{bx, by}))))
4111
4112# With `trees` the mask carries the quadtree that clip and pixel queries
4113# need; a fill painted directly keeps only its rows of runs.
4114def cover.build(lines: Cov.CovLines, +even: Bool, +frame: Raster, +sub: U32, +trees: Bool) -> Cover:
4115  Raster{origin, step, width, height, depth, linear, pixels} = frame
4116  Cover{Cov.mask.lined(lines, even, width, height, sub, trees), origin, step}
4117
4118# Local edges through m (local to device) onto the frame's pixel grid.
4119def cover.edges(es: List<&2, Edge>, +m: Matrix, +even: Bool, +frame: Raster, +sub: U32, +trees: Bool) -> Cover:
4120  cover.build(cover.lines.through(es, m, frame, Cov.lines.empty()), even, frame, sub, trees)
4121
4122def cover.tree.edges(t: EdgeTree, acc: List<&2, Edge>) -> List<&2, Edge>:
4123  match t:
4124    case EdgeFlat{box, edges}: edges
4125
4126def cover.reverse(pts: List<&2, Point>, acc: List<&2, Point>) -> List<&2, Point>:
4127  match pts:
4128    case Nil{}: acc
4129    case Con{p, rest}: cover.reverse(rest, Con{p, acc})
4130
4131def cover.area(pts: List<&2, Point>, +first: Point, +acc: F32) -> F32:
4132  match pts:
4133    case Nil{}: acc
4134    case Con{p, Nil{}}: (acc + point.cross(p, first) : F32)
4135    case Con{p, Con{q, rest}}:
4136      +qq = q
4137      cover.area(Con{qq, rest}, first, (acc + point.cross(p, qq) : F32))
4138
4139# Every part polygon is oriented the same way so nonzero winding unions them.
4140def cover.oriented(pts: List<&2, Point>) -> List<&2, Point>:
4141  match pts:
4142    case Nil{}: Nil{}
4143    case Con{p, rest}:
4144      +pp = p
4145      +ps = {Con{pp, rest} : List<&2, Point>}
4146      U.branch(List<&2, Point>, F32.is_lt(cover.area(ps, pp, 0.0), 0.0), _ => cover.reverse(ps, Nil{}), _ => ps)
4147
4148def cover.close(pts: List<&2, Point>, +first: Point, acc: List<&2, Edge>) -> List<&2, Edge>:
4149  match pts:
4150    case Nil{}: acc
4151    case Con{p, Nil{}}: Con{edge.of(p, first, True{}, True{}), acc}
4152    case Con{p, Con{q, rest}}:
4153      +qq = q
4154      Con{edge.of(p, qq, True{}, True{}), cover.close(Con{qq, rest}, first, acc)}
4155
4156def cover.polygon(pts: List<&2, Point>, acc: List<&2, Edge>) -> List<&2, Edge>:
4157  match pts:
4158    case Nil{}: acc
4159    case Con{p, rest}:
4160      +pp = p
4161      cover.close(Con{pp, rest}, pp, acc)
4162
4163def cover.arc(n: Nat, +c: Point, +r: F32, +angle: F32, +step: F32, acc: List<&2, Point>) -> List<&2, Point>:
4164  match n:
4165    case 0n: cover.reverse(Con{point.add(c, Point{(r * F32.cos(angle) : F32), (r * F32.sin(angle) : F32)}), acc}, Nil{})
4166    case 1n+d: cover.arc(d, c, r, (angle + step : F32), step, Con{point.add(c, Point{(r * F32.cos(angle) : F32), (r * F32.sin(angle) : F32)}), acc})
4167
4168def cover.segments(+extent: F32, +radius: F32) -> U32:
4169  F32.to_u32(U.clamp((F32.ceil((extent * F32.sqrt((radius * 2.5 : F32)) : F32)) + 2.0 : F32), 3.0, 64.0))
4170
4171def cover.wrap(+a: F32) -> F32:
4172  (a - 6.283185307179586 * F32.floor((a / 6.283185307179586 : F32)) : F32)
4173
4174def cover.angle(p: Point) -> F32:
4175  Point{x, y} = p
4176  F32.atan2(y, x)
4177
4178def cover.scale(+m: Matrix, +frame: Raster) -> F32:
4179  Matrix{a, b, c, d, e, f} = m
4180  Raster{origin, step, width, height, depth, linear, pixels} = frame
4181  (F32.sqrt(F32.abs((a * d - b * c : F32))) / step : F32)
4182
4183# The covered mesh keeps its bounds and cover; its edges are consumed.
4184def cover.stroke(mesh: StrokeMesh, +frame: Raster, +sub: U32, +trees: Bool) -> StrokeMesh:
4185  StrokeMesh{stree, inv, old} = mesh
4186  EdgeFlat{box, edges} = stree
4187  +iv = inv
4188  StrokeMesh{EdgeFlat{box, Nil{}}, iv, cover.edges(edges, matrix.inverse(iv), False{}, frame, sub, trees)}
4189
4190def cover.clip(c: Clip, +frame: Raster, +sub: U32) -> Clip:
4191  match c:
4192    case ClipAll{}: ClipAll{}
4193    case ClipShape{es, inv, even, old}:
4194      +tree = es
4195      +iv = inv
4196      +ev = even
4197      ClipShape{tree, iv, ev, cover.edges(cover.tree.edges(tree, Nil{}), matrix.inverse(iv), ev, frame, sub, True{})}
4198    case ClipUnion{a, b}: ClipUnion{cover.clip(a, frame, sub), cover.clip(b, frame, sub)}
4199    case ClipIntersect{a, b}: ClipIntersect{cover.clip(a, frame, sub), cover.clip(b, frame, sub)}
4200
4201def cover.fill.if(paints: Bool, f: Fill, es: List<&2, Edge>, +m: Matrix, +even: Bool, +frame: Raster, +sub: U32, +trees: Bool) -> Fill:
4202  match paints:
4203    case True{}:
4204      Fill{box, old} = f
4205      Fill{box, cover.edges(es, m, even, frame, sub, trees)}
4206    case False{}: f
4207
4208def cover.stroke.if(paints: Bool, mesh: StrokeMesh, +frame: Raster, +sub: U32, +trees: Bool) -> StrokeMesh:
4209  match paints:
4210    case True{}: cover.stroke(mesh, frame, sub, trees)
4211    case False{}: mesh
4212
4213# Whether a style paints its fill or its stroke at all; masks are only built for those.
4214def style.fills(+s: Style) -> Bool:
4215  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
4216  visible && Bool.not(U.string.equal(fill, "none"))
4217
4218def style.strokes(+s: Style) -> Bool:
4219  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
4220  visible && Bool.not(U.string.equal(stroke, "none")) && F32.is_gt(width, 0.0)
4221
4222def cover.rule(+s: Style) -> Bool:
4223  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
4224  even
4225
4226# ---- pattern tiles ----
4227# Device pixels per pattern unit along the denser tile axis; m maps pattern space to device.
4228def cover.density(+m: Matrix, +frame: Raster) -> F32:
4229  Matrix{a, b, c, d, e, f} = m
4230  (U.max(F32.sqrt((a * a + b * b : F32)), F32.sqrt((c * c + d * d : F32))) / raster.scale(frame) : F32)
4231
4232def cover.tile.frame(+w: F32, +h: F32, +density: F32) -> Raster:
4233  raster.format(Bounds{0.0, 0.0, w, h}, (1.0 / U.max(density, 0.000001) : F32))
4234
4235def cover.tile.wrapped(p: Point, +w: F32, +h: F32) -> Point:
4236  Point{x, y} = p
4237  Point{tile.wrap(x, w), tile.wrap(y, h)}
4238
4239# Pixel centers wrap into the tile, so a partial last column or row shows the tile's start.
4240def cover.tile.center(+frame: Raster, +x: U32, +y: U32, +w: F32, +h: F32) -> Point:
4241  cover.tile.wrapped(raster.position(frame, (U32.to_f32(x) + 0.5 : F32), (U32.to_f32(y) + 0.5 : F32)), w, h)
4242
4243@unsafe
4244def cover.tile.pixels(depth: Nat, +x: U32, +y: U32, +frame: Raster, +nodes: List<&2, Scene<Brush>>, +box: Bounds, +w: F32, +h: F32) -> Img.Pixels:
4245  match depth:
4246    case 0n:
4247      +center = cover.tile.center(frame, x, y, w, h)
4248      U.branch(Img.Pixels, raster.in.frame(frame, x, y) && bounds.inside(box, center, 0.01), _ => Img.Flat{sample(nodes, center, center, Img.clear(), False{}, 1.0)}, _ => Img.Flat{Img.clear()})
4249    case 1n+d:
4250      +dd = d
4251      +half = U32.shln(1, dd)
4252      U.branch(Img.Pixels, raster.in.frame(frame, x, y),
4253        _ => Img.pixels.join(cover.tile.pixels(dd, x, y, frame, nodes, box, w, h), cover.tile.pixels(dd, (x + half : U32), y, frame, nodes, box, w, h), cover.tile.pixels(dd, x, (y + half : U32), frame, nodes, box, w, h), cover.tile.pixels(dd, (x + half : U32), (y + half : U32), frame, nodes, box, w, h)),
4254        _ => Img.Flat{Img.clear()})
4255
4256def cover.tile(+frame: Raster, nodes: List<&2, Scene<Brush>>, +w: F32, +h: F32) -> Raster:
4257  Raster{origin, step, width, height, depth, linear, pixels} = frame
4258  +ns = nodes
4259  raster.store(frame, cover.tile.pixels(depth, 0, 0, frame, ns, scene.coverage(ns, bounds.empty()), w, h), False{})
4260
4261# ---- bitmap minification ----
4262# An image drawn smaller than its pixels is box-filtered down to its device
4263# footprint first (area weights over the source pixels each output pixel
4264# covers), so a bilinear read at the pixel center approximates area filtering.
4265
4266# Overlap of [lo, hi) with source pixel i.
4267def reduce.weight(+lo: F32, +hi: F32, +i: U32) -> F32:
4268  U.max((U.min(hi, (U32.to_f32(i) + 1.0 : F32)) - U.max(lo, U32.to_f32(i)) : F32), 0.0)
4269
4270@unsafe
4271def reduce.cols(+pixels: Img.Pixels, +side: U32, +x: U32, +y: U32, +lo: F32, +hi: F32, +wy: F32, n: Nat, acc: Img.Color) -> Img.Color:
4272  match n:
4273    case 0n: acc
4274    case 1n+d: reduce.cols(pixels, side, (x + 1 : U32), y, lo, hi, wy, d, color.add(acc, color.fade(Img.pixels.get(pixels, side, x, y), (reduce.weight(lo, hi, x) * wy : F32))))
4275
4276@unsafe
4277def reduce.rows(+pixels: Img.Pixels, +side: U32, +x: U32, +y: U32, +x0: F32, +x1: F32, +y0: F32, +y1: F32, +nx: Nat, n: Nat, acc: Img.Color) -> Img.Color:
4278  match n:
4279    case 0n: acc
4280    case 1n+d: reduce.rows(pixels, side, x, (y + 1 : U32), x0, x1, y0, y1, nx, d, reduce.cols(pixels, side, x, y, x0, x1, reduce.weight(y0, y1, y), nx, acc))
4281
4282# Output pixel (x, y) covers source [x fx, (x + 1) fx) by [y fy, (y + 1) fy), clipped to the image.
4283def reduce.cell(+pixels: Img.Pixels, +side: U32, +x: U32, +y: U32, +w: U32, +h: U32, +fx: F32, +fy: F32) -> Img.Color:
4284  +x0 = (U32.to_f32(x) * fx : F32)
4285  +y0 = (U32.to_f32(y) * fy : F32)
4286  +x1 = U.min((x0 + fx : F32), U32.to_f32(w))
4287  +y1 = U.min((y0 + fy : F32), U32.to_f32(h))
4288  +ix = F32.to_u32(F32.floor(x0))
4289  +iy = F32.to_u32(F32.floor(y0))
4290  +area = ((x1 - x0) * (y1 - y0) : F32)
4291  U.branch(Img.Color, F32.is_le(area, 0.0), _ => Img.clear(),
4292    _ => color.fade(reduce.rows(pixels, side, ix, iy, x0, x1, y0, y1, U32.to_nat((F32.to_u32(F32.ceil(x1)) - ix : U32)), U32.to_nat((F32.to_u32(F32.ceil(y1)) - iy : U32)), Img.clear()), (1.0 / area : F32)))
4293
4294@unsafe
4295def reduce.pixels(depth: Nat, +x: U32, +y: U32, +pixels: Img.Pixels, +side: U32, +w: U32, +h: U32, +fx: F32, +fy: F32, +rw: U32, +rh: U32) -> Img.Pixels:
4296  match depth:
4297    case 0n: U.branch(Img.Pixels, U32.is_lt(x, rw) && U32.is_lt(y, rh), _ => Img.Flat{reduce.cell(pixels, side, x, y, w, h, fx, fy)}, _ => Img.Flat{Img.clear()})
4298    case 1n+d:
4299      +dd = d
4300      +half = U32.shln(1, dd)
4301      U.branch(Img.Pixels, U32.is_lt(x, rw) && U32.is_lt(y, rh),
4302        _ => Img.pixels.join(reduce.pixels(dd, x, y, pixels, side, w, h, fx, fy, rw, rh), reduce.pixels(dd, (x + half : U32), y, pixels, side, w, h, fx, fy, rw, rh), reduce.pixels(dd, x, (y + half : U32), pixels, side, w, h, fx, fy, rw, rh), reduce.pixels(dd, (x + half : U32), (y + half : U32), pixels, side, w, h, fx, fy, rw, rh)),
4303        _ => Img.Flat{Img.clear()})
4304
4305# Resample to rw by rh pixels, each covering fx by fy source pixels.
4306def raster.resample(+r: Raster, +rw: U32, +rh: U32, +fx: F32, +fy: F32) -> Raster:
4307  Raster{origin, step, width, height, depth, linear, pixels} = r
4308  +rd = raster.depth(U.choose(U32, U32.is_gt(rw, rh), rw, rh), 1, 0n)
4309  Raster{origin, step, rw, rh, rd, linear, reduce.pixels(rd, 0, 0, pixels, U32.shln(1, depth), width, height, fx, fy, rw, rh)}
4310
4311# Reduce a whole image to rw by rh pixels; the caller rescales the brush inverse to match.
4312def raster.reduce(+r: Raster, +rw: U32, +rh: U32) -> Raster:
4313  Raster{origin, step, width, height, depth, linear, pixels} = r
4314  raster.resample(r, rw, rh, (U32.to_f32(width) / U32.to_f32(rw) : F32), (U32.to_f32(height) / U32.to_f32(rh) : F32))
4315
4316# An axis-aligned tile of at least four device pixels is rendered at device
4317# density and read bilinearly (nearest-neighbour images stay crisp). A rotated,
4318# skewed or tiny tile is rendered at `sub` times the density, or at least four
4319# pixels across, and read with a sub by sub grid of nearest lookups over each
4320# device pixel's footprint: rotated content keeps supersampled edges and a tile
4321# smaller than a pixel averages out.
4322def cover.aligned(+m: Matrix) -> Bool:
4323  Matrix{a, b, c, d, e, f} = m
4324  F32.is_le((F32.abs(b) + F32.abs(c) : F32), (0.001 * (F32.abs(a) + F32.abs(d) : F32) : F32))
4325
4326# The linear part of m applied to (x, y).
4327def cover.vector(+m: Matrix, +x: F32, +y: F32) -> Point:
4328  Matrix{a, b, c, d, e, f} = m
4329  Point{(a * x + c * y : F32), (b * x + d * y : F32)}
4330
4331# Device pixels per image pixel along each image axis; m maps image pixels to device.
4332def cover.axes(+m: Matrix, +frame: Raster) -> Point:
4333  Matrix{a, b, c, d, e, f} = m
4334  +st = raster.scale(frame)
4335  Point{(F32.sqrt((a * a + b * b : F32)) / st : F32), (F32.sqrt((c * c + d * d : F32)) / st : F32)}
4336
4337def cover.reduced(+size: U32, +density: F32) -> U32:
4338  +n = F32.to_u32(F32.ceil((U32.to_f32(size) * U.min(density, 1.0) : F32)))
4339  U.choose(U32, U32.is_lt(n, 1), 1, n)
4340
4341def cover.bitmap.sized(+raster: Raster, +inv: Matrix, +nearest: Bool, +w: U32, +h: U32, +rw: U32, +rh: U32) -> Brush:
4342  U.branch(Brush, Bool.not(nearest) && (U32.is_lt(rw, w) || U32.is_lt(rh, h)),
4343    _ => Bitmap{raster.reduce(raster, rw, rh), matrix.mul(scale((U32.to_f32(rw) / U32.to_f32(w) : F32), (U32.to_f32(rh) / U32.to_f32(h) : F32)), inv), nearest},
4344    _ => Bitmap{raster, inv, nearest})
4345
4346# A minified image is box-filtered down to its device footprint (see raster.resample).
4347def cover.bitmap(+raster: Raster, +inv: Matrix, +nearest: Bool, axes: Point) -> Brush:
4348  Raster{origin, step, width, height, depth, linear, pixels} = raster
4349  Point{dx, dy} = axes
4350  cover.bitmap.sized(raster, inv, nearest, width, height, cover.reduced(width, dx), cover.reduced(height, dy))
4351
4352# A pattern brush becomes a tile raster at the frame's resolution, its content
4353# covered on that tile grid first; m maps the brush's local space to device space.
4354def cover.brush(brush: Brush, +m: Matrix, +frame: Raster, +sub: U32, cover: List<&2, Scene<Brush>> -> Raster -> U32 -> List<&2, Scene<Brush>>) -> Brush:
4355  match brush:
4356    case Bitmap{raster, inv, nearest}:
4357      +matrix = inv
4358      cover.bitmap(raster, matrix, nearest, cover.axes(matrix.mul(m, matrix.inverse(matrix)), frame))
4359    case Solid{c, source}: Solid{c, source}
4360    case PendingContext{stroke, opacity}: PendingContext{stroke, opacity}
4361    case ContextPaint{source, mat, opacity}:
4362      +matrix = mat
4363      ContextPaint{cover.brush(source, matrix.mul(m, matrix.inverse(matrix)), frame, sub, cover), matrix, opacity}
4364    case Pattern{inv, w, h, opacity, nodes}:
4365      +matrix = inv
4366      +ww = w
4367      +hh = h
4368      +pm = matrix.mul(m, matrix.inverse(matrix))
4369      +density = cover.density(pm, frame)
4370      +direct = cover.aligned(pm) && F32.is_ge((U.min(ww, hh) * density : F32), 4.0)
4371      +tile = cover.tile.frame(ww, hh, U.choose(F32, direct, density, U.max((density * U32.to_f32(sub) : F32), (4.0 / U.max(U.min(ww, hh), 0.000001) : F32))))
4372      +back = matrix.inverse(pm)
4373      +st = raster.scale(frame)
4374      Tile{cover.tile(tile, cover(nodes)(tile)(sub), ww, hh), matrix, ww, hh, opacity, cover.vector(back, st, 0.0), cover.vector(back, 0.0, st), U.choose(U32, direct, 1, sub)}
4375    case Tile{raster, inv, w, h, opacity, u, v, grid}: Tile{raster, inv, w, h, opacity, u, v, grid}
4376    case Gradient{inv, radial, start, end, radius, focal, stops, spread, opacity, linear}: Gradient{inv, radial, start, end, radius, focal, stops, spread, opacity, linear}
4377
4378def cover.paint(ink: Paint<Brush>, +m: Matrix, +frame: Raster, +sub: U32, fills: List<&2, Scene<Brush>> -> Raster -> U32 -> List<&2, Scene<Brush>>, strokes: List<&2, Scene<Brush>> -> Raster -> U32 -> List<&2, Scene<Brush>>) -> Paint<Brush>:
4379  Paint{fill, stroke} = ink
4380  Paint{cover.brush(fill, m, frame, sub, fills), cover.brush(stroke, m, frame, sub, strokes)}
4381
4382def paint.fill(+ink: Paint<Brush>) -> Brush:
4383  Paint{f, st} = ink
4384  f
4385
4386def cover.image.rect(+raster: Raster) -> List<&2, Edge>:
4387  Raster{origin, step, width, height, depth, linear, pixels} = raster
4388  path(shape.rect.path(0.0, 0.0, U32.to_f32(width), U32.to_f32(height), 0.0, 0.0), 1.0)
4389
4390# An image's own rectangle joins the scene clip, so its edges get coverage like
4391# any shape (the bitmap read clamps to its edge pixels); iv maps device to local.
4392def cover.image.clip(+brush: Brush, clip: Clip, +iv: Matrix, +frame: Raster, +sub: U32) -> Clip:
4393  match brush:
4394    case Bitmap{raster, inv, nearest}:
4395      +to = matrix.mul(inv, iv)
4396      +rect = cover.image.rect(raster)
4397      ClipIntersect{clip, ClipShape{edge.tree(rect), to, False{}, cover.edges(rect, matrix.inverse(to), False{}, frame, sub, True{})}}
4398    case _: clip
4399
4400# Half a device pixel in local units, so a scene whose box only reaches into part
4401# of a pixel is still painted there: the pixel center may lie outside the box.
4402def cover.reach(+inv: Matrix, +half: F32) -> F32:
4403  Matrix{a, b, c, d, e, f} = inv
4404  (half * U.max(F32.sqrt((a * a + b * b : F32)), F32.sqrt((c * c + d * d : F32))) + 0.0001 : F32)
4405
4406@unsafe
4407# Scenes painted directly keep rows-only fill masks (trees false); scenes
4408# sampled per pixel (pattern tiles, filter and mask rasters) need trees.
4409def cover.scenes(xs: List<&2, Scene<Brush>>, +frame: Raster, +sub: U32, +trees: Bool) -> List<&2, Scene<Brush>>:
4410  match xs:
4411    case Nil{}: Nil{}
4412    case Con{Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fill, maskNodes, maskMode, markerNodes, coverage, effect}, rest}:
4413      +iv = inv
4414      +st = style
4415      +m = matrix.inverse(iv)
4416      +half = (raster.scale(frame) * 0.5 + 0.01 : F32)
4417      +ik = ink
4418      Con{Scene{Nil{}, bounds.expand(box, cover.reach(iv, half)), iv, st, opacity, cover.scenes(kids, frame, sub, trees), id, cover.paint(ik, m, frame, sub, nodes => f => n => cover.scenes(nodes, f, n, True{}), nodes => f => n => cover.scenes(nodes, f, n, True{})), cover.image.clip(paint.fill(ik), cover.clip(clip, frame, sub), iv, frame, sub), cover.stroke.if(style.strokes(st), outline, frame, sub, trees),
4419        cover.fill.if(style.fills(st), fill, es, m, cover.rule(st), frame, sub, trees), cover.scenes(maskNodes, frame, sub, trees), maskMode, cover.scenes(markerNodes, frame, sub, trees), bounds.expand(coverage, half), effect}, cover.scenes(rest, frame, sub, trees)}
4420
4421def raster.scene(+format: Raster, +nodes: List<&2, Scene<Brush>>, +aa: U32) -> Raster:
4422  Raster{origin, step, width, height, depth, pixelsLinear, pixels} = format
4423  +covered = cover.scenes(nodes, format, cover.sub(aa), True{})
4424  raster.store(format, raster.scene.tiles(depth, 0, 0, format, covered, scene.coverage(covered, bounds.empty()), aa), False{})
4425
4426# A source alpha input shares its buffer; only the returned RGB channels change.
4427type FilterInput is Data:
4428  FilterInput{raster: Raster, region: Bounds, alpha: Bool, standard: Bool}
4429
4430def color.alpha.only(c: Img.Color) -> Img.Color:
4431  Img.Color{r, g, b, a} = c
4432  Img.Color{0.0, 0.0, 0.0, a}
4433
4434def filter.input.color(input: FilterInput, p: Point, linear: Bool) -> Img.Color:
4435  FilterInput{raster, region, alpha, standard} = input
4436  +c = raster.color(raster, p, linear)
4437  U.branch(Img.Color, alpha, _ => color.alpha.only(c), _ => c)
4438
4439def filter.input.raster(input: FilterInput) -> Raster:
4440  FilterInput{raster, region, alpha, standard} = input
4441  raster
4442
4443type ColorRow is Data:
4444  ColorRow{r: F32, g: F32, b: F32, a: F32, bias: F32}
4445
4446type ColorMatrix is Data:
4447  ColorMatrix{r: ColorRow, g: ColorRow, b: ColorRow, a: ColorRow}
4448
4449def color.straight(c: Img.Color) -> Img.Color:
4450  Img.Color{r, g, b, a} = c
4451  +alpha = a
4452  Img.Color{(r / U.max(alpha, 0.0000001) : F32), (g / U.max(alpha, 0.0000001) : F32), (b / U.max(alpha, 0.0000001) : F32), alpha}
4453
4454def color.premultiply(c: Img.Color) -> Img.Color:
4455  Img.Color{r, g, b, a} = c
4456  +alpha = U.clamp(a, 0.0, 1.0)
4457  Img.Color{(U.clamp(r, 0.0, 1.0) * alpha : F32), (U.clamp(g, 0.0, 1.0) * alpha : F32), (U.clamp(b, 0.0, 1.0) * alpha : F32), alpha}
4458
4459def color.premul.clamp(c: Img.Color) -> Img.Color:
4460  Img.Color{r, g, b, a} = c
4461  +alpha = U.clamp(a, 0.0, 1.0)
4462  Img.Color{U.clamp(r, 0.0, alpha), U.clamp(g, 0.0, alpha), U.clamp(b, 0.0, alpha), alpha}
4463
4464def filter.row(row: ColorRow, c: Img.Color) -> F32:
4465  ColorRow{rr, rg, rb, ra, bias} = row
4466  Img.Color{r, g, b, a} = c
4467  (rr * r + rg * g + rb * b + ra * a + bias : F32)
4468
4469def filter.matrix(rows: ColorMatrix, c: Img.Color) -> Img.Color:
4470  ColorMatrix{r, g, b, a} = rows
4471  +straight = color.straight(c)
4472  color.premultiply(Img.Color{filter.row(r, straight), filter.row(g, straight), filter.row(b, straight), filter.row(a, straight)})
4473
4474def filter.matrix.row(+vs: List<&2, F32>, +start: U32, +diagonal: U32) -> ColorRow:
4475  ColorRow{U.nth(vs, U32.to_nat(start), U.choose(F32, U32.is_eq(diagonal, 0), 1.0, 0.0)), U.nth(vs, U32.to_nat((start + 1 : U32)), U.choose(F32, U32.is_eq(diagonal, 1), 1.0, 0.0)), U.nth(vs, U32.to_nat((start + 2 : U32)), U.choose(F32, U32.is_eq(diagonal, 2), 1.0, 0.0)), U.nth(vs, U32.to_nat((start + 3 : U32)), U.choose(F32, U32.is_eq(diagonal, 3), 1.0, 0.0)), U.nth(vs, U32.to_nat((start + 4 : U32)), 0.0)}
4476
4477def filter.matrix.identity() -> ColorMatrix:
4478  ColorMatrix{ColorRow{1.0, 0.0, 0.0, 0.0, 0.0}, ColorRow{0.0, 1.0, 0.0, 0.0, 0.0}, ColorRow{0.0, 0.0, 1.0, 0.0, 0.0}, ColorRow{0.0, 0.0, 0.0, 1.0, 0.0}}
4479
4480def filter.matrix.saturate(+s: F32) -> ColorMatrix:
4481  ColorMatrix{ColorRow{(0.213 + 0.787 * s : F32), (0.715 - 0.715 * s : F32), (0.072 - 0.072 * s : F32), 0.0, 0.0}, ColorRow{(0.213 - 0.213 * s : F32), (0.715 + 0.285 * s : F32), (0.072 - 0.072 * s : F32), 0.0, 0.0}, ColorRow{(0.213 - 0.213 * s : F32), (0.715 - 0.715 * s : F32), (0.072 + 0.928 * s : F32), 0.0, 0.0}, ColorRow{0.0, 0.0, 0.0, 1.0, 0.0}}
4482
4483def filter.matrix.hue(angle: F32) -> ColorMatrix:
4484  +rad = (angle * 0.017453292519943295 : F32)
4485  +c = F32.cos(rad)
4486  +s = F32.sin(rad)
4487  ColorMatrix{ColorRow{(0.213 + 0.787 * c - 0.213 * s : F32), (0.715 - 0.715 * c - 0.715 * s : F32), (0.072 - 0.072 * c + 0.928 * s : F32), 0.0, 0.0}, ColorRow{(0.213 - 0.213 * c + 0.143 * s : F32), (0.715 + 0.285 * c + 0.140 * s : F32), (0.072 - 0.072 * c - 0.283 * s : F32), 0.0, 0.0}, ColorRow{(0.213 - 0.213 * c - 0.787 * s : F32), (0.715 - 0.715 * c + 0.715 * s : F32), (0.072 + 0.928 * c + 0.072 * s : F32), 0.0, 0.0}, ColorRow{0.0, 0.0, 0.0, 1.0, 0.0}}
4488
4489def filter.matrix.parse(+as: List<&2, XML.Attr>) -> ColorMatrix:
4490  +kind = XML.attr.get(as, "type", "matrix")
4491  +vs = U.numbers(XML.attr.get(as, "values", ""))
4492  U.branch(ColorMatrix, U.string.equal(kind, "saturate"), _ => filter.matrix.saturate(U.nth(vs, 0n, 1.0)),
4493    _ => U.branch(ColorMatrix, U.string.equal(kind, "hueRotate"), _ => filter.matrix.hue(U.nth(vs, 0n, 0.0)),
4494      _ => U.branch(ColorMatrix, U.string.equal(kind, "luminanceToAlpha"), _ => ColorMatrix{ColorRow{0.0, 0.0, 0.0, 0.0, 0.0}, ColorRow{0.0, 0.0, 0.0, 0.0, 0.0}, ColorRow{0.0, 0.0, 0.0, 0.0, 0.0}, ColorRow{0.2125, 0.7154, 0.0721, 0.0, 0.0}},
4495        _ => U.choose(ColorMatrix, U32.is_eq(floats.count(vs), 20), ColorMatrix{filter.matrix.row(vs, 0, 0), filter.matrix.row(vs, 5, 1), filter.matrix.row(vs, 10, 2), filter.matrix.row(vs, 15, 3)}, filter.matrix.identity()))))
4496
4497type Transfer is Data:
4498  Transfer{kind: U32, values: List<&2, F32>, count: U32, a: F32, b: F32, c: F32}
4499
4500def transfer.identity() -> Transfer:
4501  Transfer{0, Nil{}, 0, 1.0, 0.0, 0.0}
4502
4503def transfer.parse(+as: List<&2, XML.Attr>) -> Transfer:
4504  +kind = XML.attr.get(as, "type", "identity")
4505  +vs = U.numbers(XML.attr.get(as, "tableValues", ""))
4506  Transfer{U.choose(U32, U.string.equal(kind, "table"), 1, U.choose(U32, U.string.equal(kind, "discrete"), 2, U.choose(U32, U.string.equal(kind, "linear"), 3, U.choose(U32, U.string.equal(kind, "gamma"), 4, 0)))), vs, floats.count(vs), U.scalar(XML.attr.get(as, U.choose(String, U.string.equal(kind, "gamma"), "amplitude", "slope"), "1"), 1.0), U.scalar(XML.attr.get(as, U.choose(String, U.string.equal(kind, "gamma"), "exponent", "intercept"), U.choose(String, U.string.equal(kind, "gamma"), "1", "0")), 0.0), U.scalar(XML.attr.get(as, "offset", "0"), 0.0)}
4507
4508def transfer.find(xs: List<&2, XML.Element>, +name: String) -> Transfer:
4509  match xs:
4510    case Nil{}: transfer.identity()
4511    case Con{XML.Element{n, as, kids, txt}, rest}:
4512      U.branch(Transfer, U.string.equal(n, name), _ => transfer.parse(as), _ => transfer.find(rest, name))
4513
4514def transfer.apply(+t: Transfer, +x: F32) -> F32:
4515  Transfer{kind, vs, count, a, b, c} = t
4516  +last = U.choose(U32, U32.is_eq(count, 0), 0, (count - 1 : U32))
4517  +position = (U.clamp(x, 0.0, 1.0) * U32.to_f32(last) : F32)
4518  +index = F32.to_u32(F32.floor(position))
4519  +left = U.nth(vs, U32.to_nat(index), x)
4520  U.branch(F32, U32.is_eq(kind, 1) && U32.is_gt(count, 0), _ => (left + (U.nth(vs, U32.to_nat(U32.inc(index)), left) - left) * (position - U32.to_f32(index)) : F32),
4521    _ => U.branch(F32, U32.is_eq(kind, 2) && U32.is_gt(count, 0), _ => U.nth(vs, U32.to_nat(F32.to_u32(U.min(U32.to_f32(last), F32.floor((U.clamp(x, 0.0, 1.0) * U32.to_f32(count) : F32))))), x),
4522      _ => U.branch(F32, U32.is_eq(kind, 3), _ => (a * x + b : F32), _ => U.branch(F32, U32.is_eq(kind, 4), _ => (a * F32.pow(U.max(x, 0.0), b) + c : F32), _ => x))))
4523
4524def filter.transfer(r: Transfer, g: Transfer, b: Transfer, a: Transfer, c: Img.Color) -> Img.Color:
4525  Img.Color{cr, cg, cb, ca} = c
4526  +alpha = ca
4527  color.premultiply(Img.Color{transfer.apply(r, (cr / U.max(alpha, 0.0000001) : F32)), transfer.apply(g, (cg / U.max(alpha, 0.0000001) : F32)), transfer.apply(b, (cb / U.max(alpha, 0.0000001) : F32)), transfer.apply(a, alpha)})
4528
4529def color.alpha(c: Img.Color) -> F32:
4530  Img.Color{r, g, b, a} = c
4531  a
4532
4533def filter.arithmetic(+a: F32, +b: F32, k1: F32, k2: F32, k3: F32, k4: F32) -> F32:
4534  (k1 * a * b + k2 * a + k3 * b + k4 : F32)
4535
4536def filter.composite(+a: Img.Color, +b: Img.Color, +kind: U32, +k1: F32, +k2: F32, +k3: F32, +k4: F32) -> Img.Color:
4537  Img.Color{ar, ag, ab, aa} = a
4538  Img.Color{br, bg, bb, ba} = b
4539  U.branch(Img.Color, U32.is_eq(kind, 5), _ => color.premul.clamp(Img.Color{filter.arithmetic(ar, br, k1, k2, k3, k4), filter.arithmetic(ag, bg, k1, k2, k3, k4), filter.arithmetic(ab, bb, k1, k2, k3, k4), filter.arithmetic(aa, ba, k1, k2, k3, k4)}),
4540    _ => U.branch(Img.Color, U32.is_eq(kind, 1), _ => color.fade(a, ba),
4541      _ => U.branch(Img.Color, U32.is_eq(kind, 2), _ => color.fade(a, (1.0 - ba : F32)),
4542        _ => U.branch(Img.Color, U32.is_eq(kind, 3), _ => color.add(color.fade(a, ba), color.fade(b, (1.0 - aa : F32))),
4543          _ => U.branch(Img.Color, U32.is_eq(kind, 4), _ => color.add(color.fade(a, (1.0 - ba : F32)), color.fade(b, (1.0 - aa : F32))), _ => color.over(a, b))))))
4544
4545def filter.blend.hard(+a: F32, +b: F32) -> F32:
4546  U.choose(F32, F32.is_le(a, 0.5), (2.0 * a * b : F32), (1.0 - 2.0 * (1.0 - a) * (1.0 - b) : F32))
4547
4548def filter.blend.soft(+a: F32, +b: F32) -> F32:
4549  +curve = U.choose(F32, F32.is_le(b, 0.25), (((16.0 * b - 12.0) * b + 4.0) * b : F32), F32.sqrt(b))
4550  U.choose(F32, F32.is_le(a, 0.5), (b - (1.0 - 2.0 * a) * b * (1.0 - b) : F32), (b + (2.0 * a - 1.0) * (curve - b) : F32))
4551
4552def filter.blend.channel(+a: F32, +b: F32, kind: U32) -> F32:
4553  match kind:
4554    case 1: (a * b : F32)
4555    case 2: (a + b - a * b : F32)
4556    case 3: U.min(a, b)
4557    case 4: U.max(a, b)
4558    case 5: filter.blend.hard(b, a)
4559    case 6: U.choose(F32, F32.is_le(b, 0.0), 0.0, U.choose(F32, F32.is_ge(a, 1.0), 1.0, U.min(1.0, (b / U.max(0.0000001, (1.0 - a : F32)) : F32))))
4560    case 7: U.choose(F32, F32.is_ge(b, 1.0), 1.0, U.choose(F32, F32.is_le(a, 0.0), 0.0, (1.0 - U.min(1.0, ((1.0 - b) / U.max(0.0000001, a) : F32)) : F32)))
4561    case 8: filter.blend.hard(a, b)
4562    case 9: filter.blend.soft(a, b)
4563    case 10: F32.abs((a - b : F32))
4564    case 11: (a + b - 2.0 * a * b : F32)
4565    case _: a
4566
4567def filter.blend.lum(c: Img.Color) -> F32:
4568  Img.Color{r, g, b, a} = c
4569  (0.3 * r + 0.59 * g + 0.11 * b : F32)
4570
4571def filter.blend.sat(+c: Img.Color) -> F32:
4572  Img.Color{r, g, b, a} = c
4573  (U.max(r, U.max(g, b)) - U.min(r, U.min(g, b)) : F32)
4574
4575def filter.blend.setsat(+c: Img.Color, +s: F32) -> Img.Color:
4576  Img.Color{r, g, b, a} = c
4577  +lo = U.min(r, U.min(g, b))
4578  +span = (U.max(r, U.max(g, b)) - lo : F32)
4579  +scale = U.choose(F32, F32.is_gt(span, 0.0), (s / U.max(span, 0.0000001) : F32), 0.0)
4580  Img.Color{((r - lo) * scale : F32), ((g - lo) * scale : F32), ((b - lo) * scale : F32), 1.0}
4581
4582def filter.blend.clip.scale(c: Img.Color, +l: F32, +scale: F32) -> Img.Color:
4583  Img.Color{r, g, b, a} = c
4584  Img.Color{(l + (r - l) * scale : F32), (l + (g - l) * scale : F32), (l + (b - l) * scale : F32), 1.0}
4585
4586def filter.blend.clip(+c: Img.Color) -> Img.Color:
4587  Img.Color{r, g, b, a} = c
4588  +l = filter.blend.lum(c)
4589  +lo = U.min(r, U.min(g, b))
4590  +hi = U.max(r, U.max(g, b))
4591  +scaleLow = U.choose(F32, F32.is_lt(lo, 0.0), (l / U.max(0.0000001, (l - lo : F32)) : F32), 1.0)
4592  +scaleHigh = U.choose(F32, F32.is_gt(hi, 1.0), ((1.0 - l) / U.max(0.0000001, (hi - l : F32)) : F32), 1.0)
4593  filter.blend.clip.scale(c, l, (scaleLow * scaleHigh : F32))
4594
4595def filter.blend.setlum(+c: Img.Color, l: F32) -> Img.Color:
4596  Img.Color{r, g, b, a} = c
4597  +delta = (l - filter.blend.lum(c) : F32)
4598  filter.blend.clip(Img.Color{(r + delta : F32), (g + delta : F32), (b + delta : F32), 1.0})
4599
4600def filter.blend.rgb(+a: Img.Color, +b: Img.Color, +kind: U32) -> Img.Color:
4601  Img.Color{ar, ag, ab, aa} = a
4602  Img.Color{br, bg, bb, ba} = b
4603  U.branch(Img.Color, U32.is_eq(kind, 12), _ => filter.blend.setlum(filter.blend.setsat(a, filter.blend.sat(b)), filter.blend.lum(b)),
4604    _ => U.branch(Img.Color, U32.is_eq(kind, 13), _ => filter.blend.setlum(filter.blend.setsat(b, filter.blend.sat(a)), filter.blend.lum(b)),
4605      _ => U.branch(Img.Color, U32.is_eq(kind, 14), _ => filter.blend.setlum(a, filter.blend.lum(b)),
4606        _ => U.branch(Img.Color, U32.is_eq(kind, 15), _ => filter.blend.setlum(b, filter.blend.lum(a)),
4607          _ => Img.Color{filter.blend.channel(ar, br, kind), filter.blend.channel(ag, bg, kind), filter.blend.channel(ab, bb, kind), 1.0}))))
4608
4609def filter.blend(+a: Img.Color, +b: Img.Color, +kind: U32) -> Img.Color:
4610  Img.Color{ar, ag, ab, aa} = a
4611  Img.Color{br, bg, bb, ba} = b
4612  +alpha = (aa * ba : F32)
4613  color.add(color.add(color.fade(a, (1.0 - ba : F32)), color.fade(b, (1.0 - aa : F32))), color.fade(filter.blend.rgb(Img.Color{(ar / U.max(aa, 0.0000001) : F32), (ag / U.max(aa, 0.0000001) : F32), (ab / U.max(aa, 0.0000001) : F32), 1.0}, Img.Color{(br / U.max(ba, 0.0000001) : F32), (bg / U.max(ba, 0.0000001) : F32), (bb / U.max(ba, 0.0000001) : F32), 1.0}, kind), alpha))
4614
4615type FilterTap is Data:
4616  FilterTap{delta: Point, weight: F32}
4617
4618# Convolution kernels operate on a pixel grid; edge extension precedes interpolation.
4619type Convolution is Data:
4620  Convolution{kernel: List<&2, FilterTap>, divisor: F32, bias: F32, preserve: Bool, edge: U32, limits: Bounds}
4621
4622type FilterOp is Data:
4623  FilterCopy{}
4624  FilterFlood{color: Img.Color}
4625  FilterOffset{delta: Point}
4626  FilterMatrix{rows: ColorMatrix}
4627  FilterTransfer{r: Transfer, g: Transfer, b: Transfer, a: Transfer}
4628  FilterComposite{kind: U32, k1: F32, k2: F32, k3: F32, k4: F32}
4629  FilterBlend{kind: U32}
4630  FilterMerge{inputs: List<&2, FilterInput>}
4631  FilterBlur{kernel: List<&2, FilterTap>}
4632  FilterMorphology{dilate: Bool, radius: U32, direction: Point}
4633  FilterConvolution{spec: Convolution}
4634
4635def filter.merge(xs: List<&2, FilterInput>, +p: Point, +linear: Bool, back: Img.Color) -> Img.Color:
4636  match xs:
4637    case Nil{}: back
4638    case Con{h, t}: filter.merge(t, p, linear, color.over(filter.input.color(h, p, linear), back))
4639
4640def filter.convolve(xs: List<&2, FilterTap>, +input: FilterInput, +p: Point, +linear: Bool, acc: Img.Color) -> Img.Color:
4641  match xs:
4642    case Nil{}: acc
4643    case Con{FilterTap{delta, weight}, rest}: filter.convolve(rest, input, p, linear, color.add(acc, color.fade(filter.input.color(input, point.add(p, delta), linear), weight)))
4644
4645# Morphology takes component-wise extrema of premultiplied channels.
4646def filter.morphology.join(+a: Img.Color, +b: Img.Color, +dilate: Bool) -> Img.Color:
4647  Img.Color{ar, ag, ab, aa} = a
4648  Img.Color{br, bg, bb, ba} = b
4649  Img.Color{U.choose(F32, dilate, U.max(ar, br), U.min(ar, br)), U.choose(F32, dilate, U.max(ag, bg), U.min(ag, bg)), U.choose(F32, dilate, U.max(ab, bb), U.min(ab, bb)), U.choose(F32, dilate, U.max(aa, ba), U.min(aa, ba))}
4650
4651def filter.morphology.sample(left: Nat, +index: F32, +direction: Point, +dilate: Bool, +input: FilterInput, +p: Point, +linear: Bool, acc: Img.Color) -> Img.Color:
4652  match left:
4653    case 0n: acc
4654    case 1n+rest:
4655      filter.morphology.sample(rest, (index + 1.0 : F32), direction, dilate, input, p, linear,
4656        filter.morphology.join(acc, filter.input.color(input, point.add(p, point.scale(direction, index)), linear), dilate))
4657
4658def filter.convolution.index(+n: F32, +low: F32, +high: F32, edge: U32) -> F32:
4659  +length = U.max((high - low : F32), 1.0)
4660  U.choose(F32, U32.is_eq(edge, 1), (low + (n - low) - F32.floor(((n - low) / length : F32)) * length : F32), U.clamp(n, low, (high - 1.0 : F32)))
4661
4662def filter.convolution.raw(+input: FilterInput, x: F32, y: F32, +linear: Bool) -> Img.Color:
4663  FilterInput{raster, region, alpha, standard} = input
4664  +stored = raster.is.linear(raster)
4665  +c = raster.pixel(raster, x, y)
4666  +converted = U.branch(Img.Color, U.choose(Bool, stored, linear, Bool.not(linear)), _ => c,
4667    _ => U.branch(Img.Color, stored, _ => color.from.linear(c), _ => color.space(c, True{})))
4668  U.choose(Img.Color, alpha, color.alpha.only(converted), converted)
4669
4670def filter.convolution.pixel(+input: FilterInput, +limits: Bounds, +edge: U32, +x: F32, +y: F32, linear: Bool) -> Img.Color:
4671  Bounds{x0, y0, x1, y1} = limits
4672  U.branch(Img.Color, bounds.nonempty(limits) && (Bool.not(U32.is_eq(edge, 2)) || (F32.is_ge(x, x0) && F32.is_lt(x, x1) && F32.is_ge(y, y0) && F32.is_lt(y, y1))),
4673    _ => filter.convolution.raw(input, filter.convolution.index(x, x0, x1, edge), filter.convolution.index(y, y0, y1, edge), linear), _ => Img.clear())
4674
4675def filter.convolution.horizontal(+input: FilterInput, +limits: Bounds, +edge: U32, +x: F32, +y: F32, +dx: F32, +linear: Bool) -> Img.Color:
4676  U.branch(Img.Color, F32.is_eq(dx, 0.0), _ => filter.convolution.pixel(input, limits, edge, x, y, linear),
4677    _ => color.mix(filter.convolution.pixel(input, limits, edge, x, y, linear), filter.convolution.pixel(input, limits, edge, (x + 1.0 : F32), y, linear), dx))
4678
4679def filter.convolution.at(+input: FilterInput, +limits: Bounds, +edge: U32, +raster: Raster, p: Point, +linear: Bool) -> Img.Color:
4680  Raster{Point{ox, oy}, step, width, height, depth, stored, pixels} = raster
4681  Point{x, y} = p
4682  +fx = ((x - ox) / step - 0.5 : F32)
4683  +fy = ((y - oy) / step - 0.5 : F32)
4684  +ix = F32.floor(fx)
4685  +iy = F32.floor(fy)
4686  +dx = (fx - ix : F32)
4687  +dy = (fy - iy : F32)
4688  U.branch(Img.Color, F32.is_eq(dy, 0.0), _ => filter.convolution.horizontal(input, limits, edge, ix, iy, dx, linear),
4689    _ => color.mix(filter.convolution.horizontal(input, limits, edge, ix, iy, dx, linear), filter.convolution.horizontal(input, limits, edge, ix, (iy + 1.0 : F32), dx, linear), dy))
4690
4691def filter.convolution.color(+input: FilterInput, limits: Bounds, edge: U32, p: Point, linear: Bool) -> Img.Color:
4692  filter.convolution.at(input, limits, edge, filter.input.raster(input), p, linear)
4693
4694def filter.convolution.sum(xs: List<&2, FilterTap>, +input: FilterInput, +limits: Bounds, +edge: U32, +preserve: Bool, +p: Point, +linear: Bool, acc: Img.Color) -> Img.Color:
4695  match xs:
4696    case Nil{}: acc
4697    case Con{FilterTap{delta, weight}, rest}:
4698      +sample = filter.convolution.color(input, limits, edge, point.add(p, delta), linear)
4699      filter.convolution.sum(rest, input, limits, edge, preserve, p, linear, color.add(acc, color.fade(U.choose(Img.Color, preserve, color.straight(sample), sample), weight)))
4700
4701def filter.convolution.finish(+c: Img.Color, +divisor: F32, +bias: F32, +preserve: Bool, original: Img.Color) -> Img.Color:
4702  Img.Color{r, g, b, a} = c
4703  Img.Color{rr, gg, bb, aa} = original
4704  +alpha = U.choose(F32, preserve, aa, (a / divisor + bias : F32))
4705  U.branch(Img.Color, preserve,
4706    _ => color.premultiply(Img.Color{(r / divisor + bias : F32), (g / divisor + bias : F32), (b / divisor + bias : F32), alpha}),
4707    _ => color.premul.clamp(Img.Color{(r / divisor + bias * alpha : F32), (g / divisor + bias * alpha : F32), (b / divisor + bias * alpha : F32), alpha}))
4708
4709def filter.convolution.sample(+spec: Convolution, +input: FilterInput, +p: Point, +linear: Bool) -> Img.Color:
4710  Convolution{kernel, divisor, bias, preserve, edge, limits} = spec
4711  filter.convolution.finish(filter.convolution.sum(kernel, input, limits, edge, preserve, p, linear, Img.clear()), divisor, bias, preserve, filter.input.color(input, p, linear))
4712
4713def filter.op.sample(op: FilterOp, +a: FilterInput, +b: FilterInput, +p: Point, +linear: Bool) -> Img.Color:
4714  match op:
4715    case FilterCopy{}: filter.input.color(a, p, linear)
4716    case FilterFlood{c}: c
4717    case FilterOffset{delta}: filter.input.color(a, point.sub(p, delta), linear)
4718    case FilterMatrix{rows}: filter.matrix(rows, filter.input.color(a, p, linear))
4719    case FilterTransfer{r, g, b, alpha}: filter.transfer(r, g, b, alpha, filter.input.color(a, p, linear))
4720    case FilterComposite{kind, k1, k2, k3, k4}: filter.composite(filter.input.color(a, p, linear), filter.input.color(b, p, linear), kind, k1, k2, k3, k4)
4721    case FilterBlend{kind}: filter.blend(filter.input.color(a, p, linear), filter.input.color(b, p, linear), kind)
4722    case FilterMerge{inputs}: filter.merge(inputs, p, linear, Img.clear())
4723    case FilterBlur{kernel}: filter.convolve(kernel, a, p, linear, Img.clear())
4724    case FilterConvolution{spec}: filter.convolution.sample(spec, a, p, linear)
4725    case FilterMorphology{dilate, radius, direction}:
4726      +n = radius
4727      +expand = dilate
4728      filter.morphology.sample(U32.to_nat((n * 2 + 1 : U32)), F32.neg(U32.to_f32(n)), direction, expand, a, p, linear, U.choose(Img.Color, expand, Img.clear(), Img.Color{1.0, 1.0, 1.0, 1.0}))
4729
4730type FilterWindow is Data:
4731  FilterWindow{region: Bounds, inverse: Matrix, exact: Bool}
4732
4733def filter.region.inside(b: Bounds, p: Point) -> Bool:
4734  Bounds{x0, y0, x1, y1} = b
4735  Point{x, y} = p
4736  +xx = x
4737  +yy = y
4738  F32.is_ge(xx, x0) && F32.is_ge(yy, y0) && F32.is_lt(xx, x1) && F32.is_lt(yy, y1)
4739
4740def filter.window.contains(region: Bounds, tile: Bounds) -> U32:
4741  Bounds{x0, y0, x1, y1} = region
4742  Bounds{a, b, c, d} = tile
4743  U.choose(U32, F32.is_ge(a, x0) && F32.is_ge(b, y0) && F32.is_le(c, x1) && F32.is_le(d, y1), 1, 2)
4744
4745def filter.window.span(window: FilterWindow, tile: Bounds) -> U32:
4746  FilterWindow{region, inverse, exact} = window
4747  +local = bounds.transform(tile, inverse)
4748  +box = region
4749  U.choose(U32, bounds.overlap(box, local), filter.window.contains(box, local), 0)
4750
4751def filter.window.inside(window: FilterWindow, p: Point) -> Bool:
4752  FilterWindow{region, inverse, exact} = window
4753  filter.region.inside(region, matrix.point(inverse, p))
4754
4755def filter.window.exact(window: FilterWindow) -> Bool:
4756  FilterWindow{region, inverse, exact} = window
4757  exact
4758
4759# An axis-aligned window clips a buffer pixel by its exact overlap with the region.
4760def filter.window.aligned(window: FilterWindow) -> Bool:
4761  FilterWindow{region, inverse, exact} = window
4762  Matrix{a, b, c, d, e, f} = inverse
4763  F32.is_eq(b, 0.0) && F32.is_eq(c, 0.0)
4764
4765def filter.window.overlap(region: Bounds, local: Bounds) -> F32:
4766  Bounds{x0, y0, x1, y1} = region
4767  Bounds{u0, v0, u1, v1} = local
4768  +a0 = u0
4769  +b0 = v0
4770  +a1 = u1
4771  +b1 = v1
4772  +w = U.max(0.0, (U.min(x1, a1) - U.max(x0, a0) : F32))
4773  +h = U.max(0.0, (U.min(y1, b1) - U.max(y0, b0) : F32))
4774  (w * h / U.max(((a1 - a0) * (b1 - b0) : F32), 0.000000001) : F32)
4775
4776def filter.window.area(window: FilterWindow, tile: Bounds) -> F32:
4777  FilterWindow{region, inverse, exact} = window
4778  Bounds{x0, y0, x1, y1} = tile
4779  +m = inverse
4780  filter.window.overlap(region, bounds.point(bounds.point(bounds.empty(), matrix.point(m, Point{x0, y0})), matrix.point(m, Point{x1, y1})))
4781
4782def filter.window.samples(n: Nat, +i: U32, +aa: U32, +window: FilterWindow, +point: Point, +step: F32, acc: F32) -> F32:
4783  match n:
4784    case 0n: acc
4785    case 1n+d:
4786      filter.window.samples(d, U32.inc(i), aa, window, point, step,
4787        (acc + U.choose(F32, filter.window.inside(window, point.add(point, Point{((U32.to_f32(U32.mod(i, aa)) + 0.5) * step : F32), ((U32.to_f32(U32.div(i, aa)) + 0.5) * step : F32)})), 1.0, 0.0) : F32))
4788
4789def filter.draw.pixel(+format: Raster, +x: U32, +y: U32, op: FilterOp, a: FilterInput, b: FilterInput, +window: FilterWindow, linear: Bool, +aa: U32, span: U32) -> Img.Pixels:
4790  +c = filter.op.sample(op, a, b, raster.position(format, (U32.to_f32(x) + 0.5 : F32), (U32.to_f32(y) + 0.5 : F32)), linear)
4791  # Intermediate offscreens include each pixel intersecting the hard clipping
4792  # region; the final filter region clips by the pixel's covered fraction.
4793  +covered = U.branch(F32, filter.window.exact(window) && filter.window.aligned(window), _ => filter.window.area(window, raster.tile.box(format, x, y, 1)),
4794    _ => (filter.window.samples(U32.to_nat((aa * aa : U32)), 0, aa, window, raster.position(format, U32.to_f32(x), U32.to_f32(y)), (raster.scale(format) / U32.to_f32(aa) : F32), 0.0) / U32.to_f32((aa * aa : U32)) : F32))
4795  Img.Flat{U.branch(Img.Color, U32.is_eq(span, 1), _ => c,
4796    _ => U.choose(Img.Color, filter.window.exact(window), color.fade(c, covered), U.choose(Img.Color, F32.is_gt(covered, 0.0), c, Img.clear())))}
4797
4798def filter.draw.tiles(depth: Nat, +x: U32, +y: U32, +format: Raster, +op: FilterOp, +a: FilterInput, +b: FilterInput, +window: FilterWindow, +linear: Bool, +aa: U32) -> Img.Pixels:
4799  match depth:
4800    case 0n:
4801      +span = filter.window.span(window, raster.tile.box(format, x, y, 1))
4802      U.branch(Img.Pixels, raster.in.frame(format, x, y) && Bool.not(U32.is_eq(span, 0)),
4803        _ => filter.draw.pixel(format, x, y, op, a, b, window, linear, aa, span), _ => Img.Flat{Img.clear()})
4804    case 1n+d:
4805      +dd = d
4806      +half = U32.shln(1, dd)
4807      +span = filter.window.span(window, raster.tile.box(format, x, y, (half * 2 : U32)))
4808      U.branch(Img.Pixels, raster.in.frame(format, x, y) && Bool.not(U32.is_eq(span, 0)),
4809        _ => Img.pixels.join(filter.draw.tiles(dd, x, y, format, op, a, b, window, linear, aa), filter.draw.tiles(dd, (x + half : U32), y, format, op, a, b, window, linear, aa), filter.draw.tiles(dd, x, (y + half : U32), format, op, a, b, window, linear, aa), filter.draw.tiles(dd, (x + half : U32), (y + half : U32), format, op, a, b, window, linear, aa)), _ => Img.Flat{Img.clear()})
4810
4811def filter.draw(+format: Raster, op: FilterOp, a: FilterInput, b: FilterInput, window: FilterWindow, +linear: Bool, aa: U32) -> Raster:
4812  Raster{origin, step, width, height, depth, pixelsLinear, pixels} = format
4813  raster.store(format, filter.draw.tiles(depth, 0, 0, format, op, a, b, window, linear, aa), linear)
4814
4815def filter.kernel.raw(n: Nat, +i: F32, +radius: F32, +sigma: F32, +direction: Point) -> List<&2, FilterTap>:
4816  match n:
4817    case 0n: Nil{}
4818    case 1n+d:
4819      +x = (i - radius : F32)
4820      Con{FilterTap{point.scale(direction, x), F32.exp((F32.neg(0.5) * x * x / (sigma * sigma) : F32))}, filter.kernel.raw(d, (i + 1.0 : F32), radius, sigma, direction)}
4821
4822def filter.kernel.sum(xs: List<&2, FilterTap>) -> F32:
4823  match xs:
4824    case Nil{}: 0.0
4825    case Con{FilterTap{delta, weight}, rest}: (weight + filter.kernel.sum(rest) : F32)
4826
4827def filter.kernel.normalize(xs: List<&2, FilterTap>, +total: F32) -> List<&2, FilterTap>:
4828  match xs:
4829    case Nil{}: Nil{}
4830    case Con{FilterTap{delta, weight}, rest}: Con{FilterTap{delta, (weight / total : F32)}, filter.kernel.normalize(rest, total)}
4831
4832def filter.kernel(+vector: Point, +step: F32) -> List<&2, FilterTap>:
4833  +sigma = point.distance(vector, Point{0.0, 0.0})
4834  +radius = U.min(512.0, F32.ceil((3.0 * sigma / step : F32)))
4835  +spacing = U.max(step, (3.0 * sigma / U.max(radius, 1.0) : F32))
4836  +kernel = filter.kernel.raw(U32.to_nat(F32.to_u32((radius * 2.0 + 1.0 : F32))), 0.0, radius, (U.max(sigma, 0.000001) / spacing : F32), point.scale(vector, (spacing / U.max(sigma, 0.000001) : F32)))
4837  U.branch(List<&2, FilterTap>, F32.is_lt(sigma, 0.000001), _ => Con{FilterTap{Point{0.0, 0.0}, 1.0}, Nil{}}, _ => filter.kernel.normalize(kernel, filter.kernel.sum(kernel)))
4838
4839# Filter inputs form an ordered graph. Results only see preceding primitives.
4840type FilterResult is Data:
4841  FilterResult{name: String, input: FilterInput}
4842
4843def filter.result(xs: List<&2, FilterResult>, +name: String, +fallback: FilterInput) -> FilterInput:
4844  match xs:
4845    case Nil{}: fallback
4846    case Con{FilterResult{key, input}, rest}:
4847      U.branch(FilterInput, U.string.equal(key, name), _ => input, _ => filter.result(rest, name, fallback))
4848
4849def filter.input.alpha(input: FilterInput) -> FilterInput:
4850  FilterInput{raster, region, alpha, standard} = input
4851  FilterInput{raster, region, True{}, standard}
4852
4853def filter.resolve(+name: String, +source: FilterInput, previous: FilterInput, results: List<&2, FilterResult>) -> FilterInput:
4854  U.branch(FilterInput, U.string.equal(name, "SourceGraphic"), _ => source,
4855    _ => U.branch(FilterInput, U.string.equal(name, "SourceAlpha"), _ => filter.input.alpha(source), _ => filter.result(results, name, previous)))
4856
4857def filter.input.region(input: FilterInput) -> Bounds:
4858  FilterInput{raster, region, alpha, standard} = input
4859  region
4860
4861def filter.merge.inputs(+xs: List<&2, XML.Element>, +source: FilterInput, +previous: FilterInput, +results: List<&2, FilterResult>) -> List<&2, FilterInput>:
4862  match xs:
4863    case Nil{}: Nil{}
4864    case Con{XML.Element{name, as, kids, txt}, rest}:
4865      U.branch(List<&2, FilterInput>, U.string.equal(name, "feMergeNode"), _ => Con{filter.resolve(XML.attr.get(as, "in", ""), source, previous, results), filter.merge.inputs(rest, source, previous, results)}, _ => filter.merge.inputs(rest, source, previous, results))
4866
4867def filter.inputs.region(xs: List<&2, FilterInput>, box: Bounds) -> Bounds:
4868  match xs:
4869    case Nil{}: box
4870    case Con{h, t}: filter.inputs.region(t, bounds.merge(box, filter.input.region(h)))
4871
4872def bounds.intersection(a: Bounds, b: Bounds) -> Bounds:
4873  Bounds{x0, y0, x1, y1} = a
4874  Bounds{u0, v0, u1, v1} = b
4875  Bounds{U.max(x0, u0), U.max(y0, v0), U.min(x1, u1), U.min(y1, v1)}
4876
4877def filter.subregion(+as: List<&2, XML.Attr>, box: Bounds, viewport: Point) -> Bounds:
4878  Bounds{x0, y0, x1, y1} = box
4879  Point{vw, vh} = viewport
4880  +w = vw
4881  +h = vh
4882  +left = x0
4883  +top = y0
4884  +x = U.branch(F32, Bool.not(U.string.equal(XML.attr.get(as, "x", ""), "")), _ => dimension(as, "x", w, "0"), _ => left)
4885  +y = U.branch(F32, Bool.not(U.string.equal(XML.attr.get(as, "y", ""), "")), _ => dimension(as, "y", h, "0"), _ => top)
4886  Bounds{x, y, (x + U.branch(F32, Bool.not(U.string.equal(XML.attr.get(as, "width", ""), "")), _ => dimension(as, "width", w, "0"), _ => (x1 - left : F32)) : F32), (y + U.branch(F32, Bool.not(U.string.equal(XML.attr.get(as, "height", ""), "")), _ => dimension(as, "height", h, "0"), _ => (y1 - top : F32)) : F32)}
4887
4888def matrix.vector(+m: Matrix, p: Point) -> Point:
4889  point.sub(matrix.point(m, p), matrix.point(m, Point{0.0, 0.0}))
4890
4891def filter.offset(+as: List<&2, XML.Attr>, viewport: Point) -> Point:
4892  Point{w, h} = viewport
4893  Point{dimension(as, "dx", w, "0"), dimension(as, "dy", h, "0")}
4894
4895def filter.operator(+name: String) -> U32:
4896  U.choose(U32, U.string.equal(name, "in"), 1, U.choose(U32, U.string.equal(name, "out"), 2, U.choose(U32, U.string.equal(name, "atop"), 3, U.choose(U32, U.string.equal(name, "xor"), 4, U.choose(U32, U.string.equal(name, "arithmetic"), 5, 0)))))
4897
4898def filter.blend.mode(+name: String) -> U32:
4899  U.choose(U32, U.string.equal(name, "multiply"), 1, U.choose(U32, U.string.equal(name, "screen"), 2, U.choose(U32, U.string.equal(name, "darken"), 3, U.choose(U32, U.string.equal(name, "lighten"), 4, U.choose(U32, U.string.equal(name, "overlay"), 5, U.choose(U32, U.string.equal(name, "color-dodge"), 6, U.choose(U32, U.string.equal(name, "color-burn"), 7, U.choose(U32, U.string.equal(name, "hard-light"), 8, U.choose(U32, U.string.equal(name, "soft-light"), 9, U.choose(U32, U.string.equal(name, "difference"), 10, U.choose(U32, U.string.equal(name, "exclusion"), 11, U.choose(U32, U.string.equal(name, "hue"), 12, U.choose(U32, U.string.equal(name, "saturation"), 13, U.choose(U32, U.string.equal(name, "color"), 14, U.choose(U32, U.string.equal(name, "luminosity"), 15, 0)))))))))))))))
4900
4901def filter.flood(+as: List<&2, XML.Attr>) -> Img.Color:
4902  +ink = XML.attr.get(as, "flood-color", "black")
4903  color.fade(color(U.choose(String, U.string.equal(CSS.css.lower(ink), "currentcolor"), XML.attr.get(as, "color", "black"), ink)), color.opacity(XML.attr.get(as, "flood-opacity", "1"), 1.0))
4904
4905def filter.op.parse(+name: String, +as: List<&2, XML.Attr>, +kids: List<&2, XML.Element>, units: Matrix, viewport: Point, merges: List<&2, FilterInput>) -> FilterOp:
4906  U.branch(FilterOp, U.string.equal(name, "feFlood"), _ => FilterFlood{filter.flood(as)},
4907    _ => U.branch(FilterOp, U.string.equal(name, "feOffset"), _ => FilterOffset{matrix.vector(units, filter.offset(as, viewport))},
4908      _ => U.branch(FilterOp, U.string.equal(name, "feColorMatrix"), _ => FilterMatrix{filter.matrix.parse(as)},
4909        _ => U.branch(FilterOp, U.string.equal(name, "feComponentTransfer"), _ => FilterTransfer{transfer.find(kids, "feFuncR"), transfer.find(kids, "feFuncG"), transfer.find(kids, "feFuncB"), transfer.find(kids, "feFuncA")},
4910          _ => U.branch(FilterOp, U.string.equal(name, "feComposite"), _ => FilterComposite{filter.operator(XML.attr.get(as, "operator", "over")), U.scalar(XML.attr.get(as, "k1", "0"), 0.0), U.scalar(XML.attr.get(as, "k2", "0"), 0.0), U.scalar(XML.attr.get(as, "k3", "0"), 0.0), U.scalar(XML.attr.get(as, "k4", "0"), 0.0)},
4911            _ => U.branch(FilterOp, U.string.equal(name, "feBlend"), _ => FilterBlend{filter.blend.mode(XML.attr.get(as, "mode", "normal"))},
4912              _ => U.branch(FilterOp, U.string.equal(name, "feMerge"), _ => FilterMerge{merges}, _ => FilterCopy{})))))))
4913
4914def filter.blur(+as: List<&2, XML.Attr>, +format: Raster, +input: FilterInput, +units: Matrix, window: FilterWindow, +linear: Bool, +aa: U32) -> Raster:
4915  +vs = U.numbers(XML.attr.get(as, "stdDeviation", "0"))
4916  +sx = U.nth(vs, 0n, 0.0)
4917  +sy = U.nth(vs, 1n, sx)
4918  +region = filter.input.region(input)
4919  +first = {FilterInput{filter.draw(format, FilterBlur{filter.kernel(matrix.vector(units, Point{U.max(sx, 0.0), 0.0}), raster.scale(format))}, input, input, FilterWindow{Bounds{F32.neg(1.0e30), F32.neg(1.0e30), 1.0e30, 1.0e30}, identity(), False{}}, linear, aa), region, False{}, False{}} : FilterInput}
4920  U.branch(Raster, F32.is_lt(sx, 0.0) || F32.is_lt(sy, 0.0), _ => raster.store(format, Img.Flat{Img.clear()}, linear),
4921    _ => filter.draw(format, FilterBlur{filter.kernel(matrix.vector(units, Point{0.0, sy}), raster.scale(format))}, first, first, window, linear, aa))
4922
4923# Rectangular min/max is separable along the transformed primitive axes.
4924# A radius beyond the finite buffer diagonal only visits transparent samples.
4925def filter.morphology.axis(+vector: Point, +step: F32, dilate: Bool) -> FilterOp:
4926  +length = point.distance(vector, Point{0.0, 0.0})
4927  +radius = F32.to_u32(U.clamp(F32.floor((length / step + 0.5 : F32)), 0.0, 2048.0))
4928  FilterMorphology{dilate, radius, point.scale(vector, (step / U.max(length, 0.000001) : F32))}
4929
4930def filter.morphology(+as: List<&2, XML.Attr>, +format: Raster, +input: FilterInput, +units: Matrix, window: FilterWindow, +linear: Bool, +aa: U32) -> Raster:
4931  +values = U.numbers(XML.attr.get(as, "radius", "0"))
4932  +rx = U.max(U.nth(values, 0n, 0.0), 0.0)
4933  +ry = U.max(U.nth(values, 1n, U.nth(values, 0n, 0.0)), 0.0)
4934  +dilate = U.string.equal(XML.attr.get(as, "operator", "erode"), "dilate")
4935  +region = filter.input.region(input)
4936  +first = {FilterInput{filter.draw(format, filter.morphology.axis(matrix.vector(units, Point{rx, 0.0}), raster.scale(format), dilate), input, input, FilterWindow{Bounds{F32.neg(1.0e30), F32.neg(1.0e30), 1.0e30, 1.0e30}, identity(), False{}}, linear, aa), region, False{}, False{}} : FilterInput}
4937  filter.draw(format, filter.morphology.axis(matrix.vector(units, Point{0.0, ry}), raster.scale(format), dilate), first, first, window, linear, aa)
4938
4939def filter.shadow(+attrs: List<&2, XML.Attr>, +format: Raster, +input: FilterInput, +units: Matrix, +viewport: Point, +window: FilterWindow, +linear: Bool, +aa: U32) -> Raster:
4940  +as = {Con{XML.Attr{"stdDeviation", XML.attr.get(attrs, "stdDeviation", "2")}, Con{XML.Attr{"dx", XML.attr.get(attrs, "dx", "2")}, Con{XML.Attr{"dy", XML.attr.get(attrs, "dy", "2")}, attrs}}} : List<&2, XML.Attr>}
4941  +region = filter.input.region(input)
4942  +blur = {FilterInput{filter.blur(as, format, filter.input.alpha(input), units, window, linear, aa), region, False{}, False{}} : FilterInput}
4943  +offset = {FilterInput{filter.draw(format, FilterOffset{matrix.vector(units, filter.offset(as, viewport))}, blur, blur, window, linear, aa), region, False{}, False{}} : FilterInput}
4944  +flood = {FilterInput{filter.draw(format, FilterFlood{color.space(filter.flood(as), linear)}, input, input, window, linear, aa), region, False{}, False{}} : FilterInput}
4945  +shade = {FilterInput{filter.draw(format, FilterComposite{1, 0.0, 0.0, 0.0, 0.0}, flood, offset, window, linear, aa), region, False{}, False{}} : FilterInput}
4946  filter.draw(format, FilterMerge{Con{shade, Con{input, Nil{}}}}, input, input, window, linear, aa)
4947
4948# The row-major kernel is rotated 180 degrees; target offsets refer to input cells.
4949def filter.convolution.taps(+xs: List<&2, F32>, +index: U32, +width: U32, +height: U32, +tx: F32, +ty: F32, +dx: Point, +dy: Point) -> List<&2, FilterTap>:
4950  match xs:
4951    case Nil{}: Nil{}
4952    case Con{weight, rest}:
4953      +x = (U32.to_f32((width - 1 - U32.mod(index, width) : U32)) - tx : F32)
4954      +y = (U32.to_f32((height - 1 - U32.div(index, width) : U32)) - ty : F32)
4955      U.branch(List<&2, FilterTap>, F32.is_eq(weight, 0.0), _ => filter.convolution.taps(rest, U32.inc(index), width, height, tx, ty, dx, dy),
4956        _ => Con{FilterTap{point.add(point.scale(dx, x), point.scale(dy, y)), weight}, filter.convolution.taps(rest, U32.inc(index), width, height, tx, ty, dx, dy)})
4957
4958def filter.convolution.limits.box(+raster: Raster, box: Bounds) -> Bounds:
4959  Raster{Point{ox, oy}, step, width, height, depth, stored, pixels} = raster
4960  Bounds{x0, y0, x1, y1} = box
4961  Bounds{U.max(0.0, F32.floor(((x0 - ox) / step : F32))), U.max(0.0, F32.floor(((y0 - oy) / step : F32))), U.min(U32.to_f32(width), F32.ceil(((x1 - ox) / step : F32))), U.min(U32.to_f32(height), F32.ceil(((y1 - oy) / step : F32)))}
4962
4963# Resource domains need exact edges; geometric coverage bounds include a tolerance.
4964def filter.convolution.bounds(box: Bounds, +units: Matrix) -> Bounds:
4965  Bounds{x0, y0, x1, y1} = box
4966  +left = x0
4967  +top = y0
4968  +right = x1
4969  +bottom = y1
4970  bounds.point(bounds.point(bounds.point(bounds.point(bounds.empty(), matrix.point(units, Point{left, top})), matrix.point(units, Point{right, top})), matrix.point(units, Point{left, bottom})), matrix.point(units, Point{right, bottom}))
4971
4972def filter.convolution.limits(input: FilterInput, units: Matrix) -> Bounds:
4973  FilterInput{raster, region, alpha, standard} = input
4974  filter.convolution.limits.box(raster, filter.convolution.bounds(region, units))
4975
4976def filter.convolution.parse(+as: List<&2, XML.Attr>, +units: Matrix, +input: FilterInput, +format: Raster) -> FilterOp:
4977  +order = U.numbers(XML.attr.get(as, "order", "3"))
4978  +ox = F32.floor(U.nth(order, 0n, 3.0))
4979  +oy = F32.floor(U.nth(order, 1n, U.nth(order, 0n, 3.0)))
4980  +width = F32.to_u32(U.clamp(ox, 1.0, 4096.0))
4981  +height = F32.to_u32(U.clamp(oy, 1.0, 4096.0))
4982  +tx = F32.floor(U.scalar(XML.attr.get(as, "targetX", ""), F32.floor((ox * 0.5 : F32))))
4983  +ty = F32.floor(U.scalar(XML.attr.get(as, "targetY", ""), F32.floor((oy * 0.5 : F32))))
4984  +vs = U.numbers(XML.attr.get(as, "kernelMatrix", ""))
4985  +sum = floats.sum(vs)
4986  +defaultDivisor = U.choose(F32, F32.is_eq(sum, 0.0), 1.0, sum)
4987  +divisor = U.scalar(XML.attr.get(as, "divisor", ""), defaultDivisor)
4988  +lengths = U.numbers(XML.attr.get(as, "kernelUnitLength", ""))
4989  +ux = U.nth(lengths, 0n, 0.0)
4990  +uy = U.nth(lengths, 1n, ux)
4991  +explicit = F32.is_gt(ux, 0.0) && F32.is_gt(uy, 0.0)
4992  +step = raster.scale(format)
4993  +dx = U.choose(Point, explicit, matrix.vector(units, Point{ux, 0.0}), Point{step, 0.0})
4994  +dy = U.choose(Point, explicit, matrix.vector(units, Point{0.0, uy}), Point{0.0, step})
4995  +edge = XML.attr.get(as, "edgeMode", "duplicate")
4996  +valid = F32.is_ge(ox, 1.0) && F32.is_ge(oy, 1.0) && F32.is_le(ox, 4096.0) && F32.is_le(oy, 4096.0) && F32.is_le((ox * oy : F32), 4096.0) && U32.is_eq(floats.count(vs), (width * height : U32)) && F32.is_ge(tx, 0.0) && F32.is_lt(tx, ox) && F32.is_ge(ty, 0.0) && F32.is_lt(ty, oy)
4997  U.branch(FilterOp, valid, _ => FilterConvolution{Convolution{filter.convolution.taps(vs, 0, width, height, tx, ty, dx, dy), U.choose(F32, F32.is_eq(divisor, 0.0), defaultDivisor, divisor), U.scalar(XML.attr.get(as, "bias", "0"), 0.0), U.string.equal(XML.attr.get(as, "preserveAlpha", "false"), "true"), U.choose(U32, U.string.equal(edge, "wrap"), 1, U.choose(U32, U.string.equal(edge, "none"), 2, 0)), filter.convolution.limits(input, units)}}, _ => FilterCopy{})
4998
4999def filter.primitive(+name: String, +as: List<&2, XML.Attr>, +kids: List<&2, XML.Element>, +format: Raster, +source: FilterInput, +previous: FilterInput, +results: List<&2, FilterResult>, +region: Bounds, +units: Matrix, +viewport: Point, +aa: U32) -> FilterInput:
5000  +a = filter.resolve(XML.attr.get(as, "in", ""), source, previous, results)
5001  +b = filter.resolve(XML.attr.get(as, "in2", ""), source, previous, results)
5002  +merges = filter.merge.inputs(kids, source, previous, results)
5003  +two = U.string.equal(name, "feComposite") || U.string.equal(name, "feBlend")
5004  +defaultBox = U.branch(Bounds, U.string.equal(name, "feFlood"), _ => region,
5005    _ => U.branch(Bounds, U.string.equal(name, "feMerge"), _ => filter.inputs.region(merges, bounds.empty()), _ => U.choose(Bounds, two, bounds.merge(filter.input.region(a), filter.input.region(b)), filter.input.region(a))))
5006  +box = bounds.intersection(region, filter.subregion(as, defaultBox, viewport))
5007  +window = {FilterWindow{box, matrix.inverse(units), False{}} : FilterWindow}
5008  +linear = U.branch(Bool, U.string.equal(name, "feFlood"), _ => False{}, _ => U.branch(Bool, U.string.equal(name, "feOffset"), _ => raster.is.linear(filter.input.raster(a)), _ => Bool.not(U.string.equal(XML.attr.get(as, "color-interpolation-filters", "linearRGB"), "sRGB"))))
5009  FilterInput{U.branch(Raster, U.string.equal(name, "feGaussianBlur"), _ => filter.blur(as, format, a, units, window, linear, aa), _ => U.branch(Raster, U.string.equal(name, "feDropShadow"), _ => filter.shadow(as, format, a, units, viewport, window, linear, aa), _ => U.branch(Raster, U.string.equal(name, "feMorphology"), _ => filter.morphology(as, format, a, units, window, linear, aa), _ => filter.draw(format, U.branch(FilterOp, U.string.equal(name, "feConvolveMatrix"), _ => filter.convolution.parse(as, units, a, format), _ => filter.op.parse(name, as, kids, units, viewport, merges)), a, b, window, linear, aa)))), box, False{}, False{}}
5010
5011@unsafe
5012def filter.chain(+xs: List<&2, XML.Element>, +format: Raster, +source: FilterInput, +previous: FilterInput, +results: List<&2, FilterResult>, +region: Bounds, +units: Matrix, +viewport: Point, +aa: U32) -> FilterInput:
5013  match xs:
5014    case Nil{}: previous
5015    case Con{XML.Element{name, attrs, kids, txt}, rest}:
5016      +as = CSS.properties(attrs)
5017      +next = filter.primitive(name, as, kids, format, source, previous, results, region, units, viewport, aa)
5018      U.branch(FilterInput, U.prefix(name, "fe"), _ => filter.chain(rest, format, source, next, Con{FilterResult{XML.attr.get(as, "result", ""), next}, results}, region, units, viewport, aa), _ => filter.chain(rest, format, source, previous, results, region, units, viewport, aa))
5019
5020def filter.padding(+xs: List<&2, XML.Element>, +units: Matrix, +margin: F32) -> F32:
5021  match xs:
5022    case Nil{}: margin
5023    case Con{XML.Element{name, as, kids, txt}, rest}:
5024      +vs = U.numbers(XML.attr.get(as, "stdDeviation", U.choose(String, U.string.equal(name, "feDropShadow"), "2", "0")))
5025      +sx = U.nth(vs, 0n, 0.0)
5026      +sy = U.nth(vs, 1n, sx)
5027      +pad = (3.0 * (point.distance(matrix.vector(units, Point{sx, 0.0}), Point{0.0, 0.0}) + point.distance(matrix.vector(units, Point{0.0, sy}), Point{0.0, 0.0})) : F32)
5028      +radii = U.numbers(XML.attr.get(as, "radius", "0"))
5029      +rx = U.max(U.nth(radii, 0n, 0.0), 0.0)
5030      +ry = U.max(U.nth(radii, 1n, U.nth(radii, 0n, 0.0)), 0.0)
5031      +morphPad = (point.distance(matrix.vector(units, Point{rx, 0.0}), Point{0.0, 0.0}) + point.distance(matrix.vector(units, Point{0.0, ry}), Point{0.0, 0.0}) + 2.0 : F32)
5032      filter.padding(rest, units, U.choose(F32, U.string.equal(name, "feGaussianBlur") || U.string.equal(name, "feDropShadow"), U.max(margin, pad), U.choose(F32, U.string.equal(name, "feMorphology"), U.max(margin, morphPad), margin)))
5033
5034def filter.present(xs: List<&2, XML.Element>) -> Bool:
5035  match xs:
5036    case Nil{}: False{}
5037    case Con{XML.Element{name, as, kids, txt}, rest}: U.prefix(name, "fe") || filter.present(rest)
5038
5039def filter.density(+m: Matrix) -> F32:
5040  Matrix{a, b, c, d, e, f} = m
5041  +x = (a * a + b * b : F32)
5042  +y = (c * c + d * d : F32)
5043  +dot = (a * c + b * d : F32)
5044  # Largest singular value keeps transformed pattern buffers at device density.
5045  F32.sqrt(((x + y + F32.sqrt(((x - y) * (x - y) + 4.0 * dot * dot : F32))) * 0.5 : F32))
5046
5047def filter.source(scene: Scene<Brush>) -> Scene<Brush>:
5048  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, masks, maskMode, markers, coverage, effect} = scene
5049  Scene{es, box, inv, style, 1.0, kids, id, ink, ClipAll{}, outline, fillTree, Nil{}, 0, markers, coverage, NoFilter{}}
5050
5051def filter.render(+scene: Scene<Brush>, +region: Bounds, +units: Matrix, +viewport: Point, +primitives: List<&2, XML.Element>, space: Matrix, +aa: U32) -> Raster:
5052  +world = bounds.transform(region, units)
5053  +area = bounds.expand(bounds.merge(world, scene.coverage(Con{scene, Nil{}}, bounds.empty())), filter.padding(primitives, units, 0.0))
5054  +format = raster.format(area, (1.0 / U.max(filter.density(space), 0.000001) : F32))
5055  +raw = {FilterInput{raster.scene(format, Con{filter.source(scene), Nil{}}, aa), region, False{}, True{}} : FilterInput}
5056  +source = {FilterInput{filter.draw(format, FilterCopy{}, raw, raw, FilterWindow{region, matrix.inverse(units), False{}}, False{}, aa), region, False{}, True{}} : FilterInput}
5057  +result = filter.chain(primitives, format, source, source, Nil{}, region, units, viewport, aa)
5058  U.branch(Raster, bounds.nonempty(region) && filter.present(primitives),
5059    _ => filter.draw(format, FilterCopy{}, result, result, FilterWindow{region, matrix.inverse(units), True{}}, raster.is.linear(filter.input.raster(result)), aa), _ => raster.empty())
5060
5061def filter.cache(scene: Scene<Brush>, raster: Raster, area: Bounds) -> Scene<Brush>:
5062  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, masks, maskMode, markers, coverage, effect} = scene
5063  Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, masks, maskMode, markers, area, FilterImage{raster}}
5064
5065def prepare.effect(effect: FilterEffect, +scene: Scene<Brush>, space: Matrix, aa: U32) -> Scene<Brush>:
5066  match effect:
5067    case NoFilter{}: scene
5068    case FilterImage{raster}: scene
5069    case FilterSpec{region, units, viewport, attrs, primitives}:
5070      +box = region
5071      +mat = units
5072      +buffer = filter.render(scene, box, mat, viewport, primitives, space, aa)
5073      filter.cache(scene, buffer, bounds.expand(bounds.transform(box, mat), raster.scale(buffer)))
5074
5075def prepare.brush(brush: Brush, +space: Matrix, next: List<&2, Scene<Brush>> -> Matrix -> List<&2, Scene<Brush>>) -> Brush:
5076  match brush:
5077    case Bitmap{raster, inv, nearest}: Bitmap{raster, inv, nearest}
5078    case Solid{c, source}: Solid{c, source}
5079    case PendingContext{stroke, opacity}: PendingContext{stroke, opacity}
5080    case ContextPaint{source, mat, opacity}:
5081      +matrix = mat
5082      ContextPaint{prepare.brush(source, matrix.mul(space, matrix.inverse(matrix)), next), matrix, opacity}
5083    case Pattern{inv, width, height, opacity, nodes}:
5084      +matrix = inv
5085      Pattern{matrix, width, height, opacity, next(nodes)(matrix.mul(space, matrix.inverse(matrix)))}
5086    case Tile{raster, inv, width, height, opacity, u, v, grid}: Tile{raster, inv, width, height, opacity, u, v, grid}
5087    case Gradient{inv, radial, start, end, radius, focal, stops, spread, opacity, linear}: Gradient{inv, radial, start, end, radius, focal, stops, spread, opacity, linear}
5088
5089def prepare.paint(ink: Paint<Brush>, +space: Matrix, fills: List<&2, Scene<Brush>> -> Matrix -> List<&2, Scene<Brush>>, strokes: List<&2, Scene<Brush>> -> Matrix -> List<&2, Scene<Brush>>) -> Paint<Brush>:
5090  Paint{fill, stroke} = ink
5091  Paint{prepare.brush(fill, space, fills), prepare.brush(stroke, space, strokes)}
5092
5093@unsafe
5094def prepare.scenes(+xs: List<&2, Scene<Brush>>, +space: Matrix, +aa: U32) -> List<&2, Scene<Brush>>:
5095  match xs:
5096    case Nil{}: Nil{}
5097    case Con{Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fillTree, masks, maskMode, markers, coverage, effect}, rest}:
5098      +node = scene.covered(Scene{es, box, inv, style, opacity, prepare.scenes(kids, space, aa), id, prepare.paint(ink, matrix.mul(space, matrix.inverse(inv)), nodes => mat => prepare.scenes(nodes, mat, aa), nodes => mat => prepare.scenes(nodes, mat, aa)), clip, outline, fillTree, prepare.scenes(masks, space, aa), maskMode, prepare.scenes(markers, space, aa), coverage, effect})
5099      Con{prepare.effect(effect, node, space, aa), prepare.scenes(rest, space, aa)}
5100
5101# Portable image export for headless rendering and reference comparisons.
5102def pixel.ppm(c: U32) -> String:
5103  +rgb = c
5104  U32.show((rgb / 65536 % 256 : U32)) ++ " " ++ U32.show((rgb / 256 % 256 : U32))
5105    ++ " " ++ U32.show((rgb % 256 : U32)) ++ "\n"
5106
5107# ---- painter ----
5108# Nodes are painted back to front into a flat layer of packed 8-bit
5109# premultiplied RGBA pixels (one U32 per pixel) straight from their coverage
5110# masks: a mask leaf that spans a block paints the block with one coverage
5111# value, so for a solid brush the blended color is computed once per block and
5112# each pixel is a read, an integer blend and a write, with no constructor or
5113# closure on the way. Nodes with opacity, a mask or a rendered filter are
5114# painted into a layer of their own and composited; enclosing clips travel down
5115# as the coverage limit the per-pixel sampler also uses, so both paths agree up
5116# to the 8-bit rounding of the layers.
5117
5118def frame(+w: U32, +h: U32) -> Raster:
5119  Raster{Point{0.0, 0.0}, 1.0, w, h, raster.depth(U.choose(U32, U32.is_gt(w, h), w, h), 1, 0n), False{}, Img.Flat{Img.clear()}}
5120
5121# A layer covers the device rectangle at (x, y) of w by h pixels; pixel (px, py)
5122# lives at index (py - y) * w + (px - x). The frame is the target at the origin;
5123# a group's layer is only as large as the group's coverage box.
5124type Target is Data:
5125  Target{x: U32, y: U32, w: U32, h: U32}
5126
5127def layer.new(+t: Target) -> Array<U32>:
5128  Target{x, y, w, h} = t
5129  Array.new(U32, raster.depth(U.choose(U32, U32.is_eq((w * h : U32), 0), 1, (w * h : U32)), 1, 0n), 0)
5130
5131def target.index(+t: Target, +px: U32, +py: U32) -> U32:
5132  Target{x, y, w, h} = t
5133  ((py - y) * w + px - x : U32)
5134
5135# The target's rectangle intersected with a device box, as a target.
5136def target.within(+t: Target, +box: Bounds) -> Target:
5137  Target{x, y, w, h} = t
5138  Bounds{bx0, by0, bx1, by1} = box
5139  +x0 = F32.to_u32(U.clamp(F32.floor(bx0), U32.to_f32(x), U32.to_f32((x + w : U32))))
5140  +y0 = F32.to_u32(U.clamp(F32.floor(by0), U32.to_f32(y), U32.to_f32((y + h : U32))))
5141  +x1 = F32.to_u32(U.clamp(F32.ceil(bx1), U32.to_f32(x), U32.to_f32((x + w : U32))))
5142  +y1 = F32.to_u32(U.clamp(F32.ceil(by1), U32.to_f32(y), U32.to_f32((y + h : U32))))
5143  Target{x0, y0, U.choose(U32, U32.is_gt(x1, x0), (x1 - x0 : U32), 0), U.choose(U32, U32.is_gt(y1, y0), (y1 - y0 : U32), 0)}
5144
5145# ---- packed pixels ----
5146
5147def pixel.lane(+px: U32, +unit: U32) -> U32:
5148  U32.mod(U32.div(px, unit), 256)
5149
5150def pixel.make(+r: U32, +g: U32, +b: U32, +a: U32) -> U32:
5151  (r + g * 256 + b * 65536 + a * 16777216 : U32)
5152
5153# A premultiplied color scaled by f, rounded to 8 bits.
5154def pixel.pack(c: Img.Color, +f: F32) -> U32:
5155  Img.Color{r, g, b, a} = c
5156  pixel.make(channel((r * f : F32)), channel((g * f : F32)), channel((b * f : F32)), channel((a * f : F32)))
5157
5158def pixel.round(+v: U32, +f: F32) -> U32:
5159  F32.to_u32(F32.floor((U32.to_f32(v) * f + 0.5 : F32)))
5160
5161# A packed pixel scaled by f in every channel.
5162def pixel.scale(+px: U32, +f: F32) -> U32:
5163  pixel.make(pixel.round(pixel.lane(px, 1), f), pixel.round(pixel.lane(px, 256), f), pixel.round(pixel.lane(px, 65536), f), pixel.round(pixel.lane(px, 16777216), f))
5164
5165# front over back, both premultiplied: front + back * (255 - alpha) / 255 per
5166# channel, written as one expression over the four lanes.
5167def pixel.over(+front: U32, +back: U32) -> U32:
5168  +fa = U32.div(front, 16777216)
5169  +keep = (255 - fa : U32)
5170  +fb = U32.mod(U32.div(front, 65536), 256)
5171  +fg = U32.mod(U32.div(front, 256), 256)
5172  +fr = U32.mod(front, 256)
5173  +ba = U32.div(back, 16777216)
5174  +bb = U32.mod(U32.div(back, 65536), 256)
5175  +bg = U32.mod(U32.div(back, 256), 256)
5176  +br = U32.mod(back, 256)
5177  (fr + U32.div((br * keep + 127 : U32), 255) + (fg + U32.div((bg * keep + 127 : U32), 255)) * 256 + (fb + U32.div((bb * keep + 127 : U32), 255)) * 65536 + (fa + U32.div((ba * keep + 127 : U32), 255)) * 16777216 : U32)
5178
5179# The pixel over white as the packed 0xRRGGBB the outputs use.
5180def pixel.white(+px: U32) -> U32:
5181  +a = pixel.lane(px, 16777216)
5182  ((pixel.lane(px, 1) + 255 - a) * 65536 + (pixel.lane(px, 256) + 255 - a) * 256 + pixel.lane(px, 65536) + 255 - a : U32)
5183
5184# The mask factor a packed pixel gives: its alpha, or the luminance of its premultiplied color.
5185def pixel.mask(+px: U32, +mode: U32) -> F32:
5186  U.choose(F32, U32.is_eq(mode, 1), (U32.to_f32(pixel.lane(px, 16777216)) / 255.0 : F32),
5187    ((U32.to_f32(pixel.lane(px, 1)) * 0.2126 + U32.to_f32(pixel.lane(px, 256)) * 0.7152 + U32.to_f32(pixel.lane(px, 65536)) * 0.0722) / 255.0 : F32))
5188
5189def blend.got(got: Array<U32> & U32, +i: U32, +front: U32) -> Array<U32>:
5190  (a, back) = got
5191  Array.set(U32, a, i, pixel.over(front, back))
5192
5193# Composite a packed front over the pixel at index i.
5194def blend(a: Array<U32>, +i: U32, +front: U32) -> Array<U32>:
5195  blend.got(Array.get(U32, a, i), i, front)
5196
5197def paint.with(cond: Bool, a: Array<U32>, yes: Array<U32> -> Array<U32>, no: Array<U32> -> Array<U32>) -> Array<U32>:
5198  match cond:
5199    case True{}: yes(a)
5200    case False{}: no(a)
5201
5202# ---- clip limits without lambdas ----
5203
5204def cover.value(present: Bool, +cov: Cover, +es: EdgeTree, +inv: Matrix, +even: Bool, +p: Point) -> F32:
5205  match present:
5206    case True{}: cover.at(cov, p)
5207    case False{}: U.choose(F32, edge.tree.inside(es, matrix.point(inv, p), even), 1.0, 0.0)
5208
5209# clip.cover, branching through matches since it runs once per clipped pixel.
5210def clip.limit(c: Clip, +p: Point) -> F32:
5211  match c:
5212    case ClipAll{}: 1.0
5213    case ClipShape{es, inv, even, cover}:
5214      +cov = cover
5215      cover.value(cover.present(cov), cov, es, inv, even, p)
5216    case ClipUnion{a, b}:
5217      +u = clip.limit(a, p)
5218      +v = clip.limit(b, p)
5219      (u + v - u * v : F32)
5220    case ClipIntersect{a, b}: (clip.limit(a, p) * clip.limit(b, p) : F32)
5221
5222# The least coverage the enclosing clips leave at p.
5223def paint.limit(clips: List<&2, Clip>, +p: Point, +acc: F32) -> F32:
5224  match clips:
5225    case Nil{}: acc
5226    case Con{c, rest}: paint.limit(rest, p, U.min(acc, clip.limit(c, p)))
5227
5228# A clip's coverage over the device pixel block [x0, x1) by [y0, y1) when it is
5229# uniform there, else -1; the frame grid is the device grid.
5230def cover.block(+cov: Cover, +x0: U32, +y0: U32, +x1: U32, +y1: U32) -> F32:
5231  Cover{mask, origin, step} = cov
5232  Cov.mask.uniform(mask, x0, y0, x1, y1)
5233
5234def cover.block.if(present: Bool, +cov: Cover, +x0: U32, +y0: U32, +x1: U32, +y1: U32) -> F32:
5235  match present:
5236    case True{}: cover.block(cov, x0, y0, x1, y1)
5237    case False{}: F32.neg(1.0)
5238
5239def clip.block(c: Clip, +x0: U32, +y0: U32, +x1: U32, +y1: U32) -> F32:
5240  match c:
5241    case ClipAll{}: 1.0
5242    case ClipShape{es, inv, even, cover}:
5243      +cov = cover
5244      cover.block.if(cover.present(cov), cov, x0, y0, x1, y1)
5245    case ClipUnion{a, b}:
5246      +u = clip.block(a, x0, y0, x1, y1)
5247      +v = clip.block(b, x0, y0, x1, y1)
5248      U.choose(F32, F32.is_lt(u, 0.0) || F32.is_lt(v, 0.0), F32.neg(1.0), (u + v - u * v : F32))
5249    case ClipIntersect{a, b}:
5250      +u = clip.block(a, x0, y0, x1, y1)
5251      +v = clip.block(b, x0, y0, x1, y1)
5252      U.choose(F32, F32.is_lt(u, 0.0) || F32.is_lt(v, 0.0), F32.neg(1.0), (u * v : F32))
5253
5254# The least clip coverage over a block when every clip is uniform there, else -1.
5255def paint.limit.block(clips: List<&2, Clip>, +x0: U32, +y0: U32, +x1: U32, +y1: U32, +acc: F32) -> F32:
5256  match clips:
5257    case Nil{}: acc
5258    case Con{c, rest}:
5259      +u = clip.block(c, x0, y0, x1, y1)
5260      paint.limit.block(rest, x0, y0, x1, y1, U.choose(F32, F32.is_lt(u, 0.0) || F32.is_lt(acc, 0.0), F32.neg(1.0), U.min(acc, u)))
5261
5262def paint.clipped(clips: List<&2, Clip>) -> Bool:
5263  match clips:
5264    case Nil{}: False{}
5265    case Con{c, rest}: True{}
5266
5267# ---- shape pixels ----
5268
5269# What one shape's pixels need: the brush (a solid color is kept as a premultiplied
5270# color to scale per block), the device-to-local matrix, the color space and the
5271# enclosing clips.
5272type PaintCtx is Data:
5273  PaintCtx{brush: Brush, inverse: Matrix, linear: Bool, clips: List<&2, Clip>, solid: Bool, tint: Img.Color, clipped: Bool}
5274
5275def paint.solid(+brush: Brush) -> Bool:
5276  match brush:
5277    case Solid{c, source}: True{}
5278    case _: False{}
5279
5280def paint.solid.color(+brush: Brush, +linear: Bool) -> Img.Color:
5281  match brush:
5282    case Solid{c, source}: color.space(c, linear)
5283    case _: Img.clear()
5284
5285def paint.context(+brush: Brush, +inv: Matrix, +linear: Bool, +clips: List<&2, Clip>) -> PaintCtx:
5286  PaintCtx{brush, inv, linear, clips, paint.solid(brush), paint.solid.color(brush, linear), paint.clipped(clips)}
5287
5288def paint.sampled(+br: Brush, +inv: Matrix, +linear: Bool, +p: Point) -> Img.Color:
5289  color.space(brush.sample(br, matrix.point(inv, p), matrix.point(inv, p), nodes => q => center => Img.clear()), linear)
5290
5291def paint.sampled.ctx(+ctx: PaintCtx, +p: Point) -> Img.Color:
5292  PaintCtx{br, inv, linear, clips, solid, tint, clipped} = ctx
5293  paint.sampled(br, inv, linear, p)
5294
5295def paint.tint.ctx(+ctx: PaintCtx) -> Img.Color:
5296  PaintCtx{br, inv, linear, clips, solid, tint, clipped} = ctx
5297  tint
5298
5299def paint.color(solid: Bool, +ctx: PaintCtx, +p: Point) -> Img.Color:
5300  match solid:
5301    case True{}: paint.tint.ctx(ctx)
5302    case False{}: paint.sampled.ctx(ctx, p)
5303
5304def paint.ctx.solid(+ctx: PaintCtx) -> Bool:
5305  PaintCtx{br, inv, linear, clips, solid, tint, clipped} = ctx
5306  solid
5307
5308def paint.ctx.direct(+ctx: PaintCtx) -> Bool:
5309  PaintCtx{br, inv, linear, clips, solid, tint, clipped} = ctx
5310  solid && Bool.not(clipped)
5311
5312def paint.ctx.tint(+ctx: PaintCtx) -> Img.Color:
5313  PaintCtx{br, inv, linear, clips, solid, tint, clipped} = ctx
5314  tint
5315
5316def paint.ctx.clips(+ctx: PaintCtx) -> List<&2, Clip>:
5317  PaintCtx{br, inv, linear, clips, solid, tint, clipped} = ctx
5318  clips
5319
5320# The general pixel: brush sampled or solid, coverage limited by the clips; a
5321# known block limit (0 or more) replaces the per-pixel clip reads.
5322def paint.pixel(fb: Array<U32>, +x: U32, +y: U32, +v: F32, +known: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5323  +p = {Point{(U32.to_f32(x) + 0.5 : F32), (U32.to_f32(y) + 0.5 : F32)} : Point}
5324  blend(fb, target.index(t, x, y), pixel.pack(paint.color(paint.ctx.solid(ctx), ctx, p), U.min(v, U.choose(F32, F32.is_lt(known, 0.0), paint.limit(paint.ctx.clips(ctx), p, 1.0), known))))
5325
5326@unsafe
5327def paint.row(fb: Array<U32>, n: Nat, +x: U32, +y: U32, +v: F32, +known: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5328  match n:
5329    case 0n: fb
5330    case 1n+d: paint.row(paint.pixel(fb, x, y, v, known, t, ctx), d, (x + 1 : U32), y, v, known, t, ctx)
5331
5332@unsafe
5333def paint.rows(fb: Array<U32>, n: Nat, +x0: U32, +y: U32, +cols: Nat, +v: F32, +known: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5334  match n:
5335    case 0n: fb
5336    case 1n+d: paint.rows(paint.row(fb, cols, x0, y, v, known, t, ctx), d, x0, (y + 1 : U32), cols, v, known, t, ctx)
5337
5338# The fast pixel: a constant packed front blended along the row, its
5339# channels split once per row.
5340def pixel.under(+fr: U32, +fg: U32, +fb: U32, +fa: U32, +keep: U32, +back: U32) -> U32:
5341  (fr + U32.div((U32.mod(back, 256) * keep + 127 : U32), 255) + (fg + U32.div((U32.mod(U32.div(back, 256), 256) * keep + 127 : U32), 255)) * 256
5342    + (fb + U32.div((U32.mod(U32.div(back, 65536), 256) * keep + 127 : U32), 255)) * 65536 + (fa + U32.div((U32.div(back, 16777216) * keep + 127 : U32), 255)) * 16777216 : U32)
5343
5344def blend.pre.got(got: Array<U32> & U32, +i: U32, +fr: U32, +fg: U32, +fb: U32, +fa: U32, +keep: U32) -> Array<U32>:
5345  (a, back) = got
5346  Array.set(U32, a, i, pixel.under(fr, fg, fb, fa, keep, back))
5347
5348@unsafe
5349def fill.row.pre(fb: Array<U32>, n: Nat, +i: U32, +fr: U32, +fg: U32, +fbl: U32, +fa: U32, +keep: U32) -> Array<U32>:
5350  match n:
5351    case 0n: fb
5352    case 1n+d: fill.row.pre(blend.pre.got(Array.get(U32, fb, i), i, fr, fg, fbl, fa, keep), d, (i + 1 : U32), fr, fg, fbl, fa, keep)
5353
5354def fill.row(fb: Array<U32>, n: Nat, +i: U32, +front: U32) -> Array<U32>:
5355  +fa = U32.div(front, 16777216)
5356  fill.row.pre(fb, n, i, U32.mod(front, 256), U32.mod(U32.div(front, 256), 256), U32.mod(U32.div(front, 65536), 256), fa, (255 - fa : U32))
5357
5358@unsafe
5359def fill.rows(fb: Array<U32>, n: Nat, +x0: U32, +y: U32, +cols: Nat, +t: Target, +front: U32) -> Array<U32>:
5360  match n:
5361    case 0n: fb
5362    case 1n+d: fill.rows(fill.row(fb, cols, target.index(t, x0, y), front), d, x0, (y + 1 : U32), cols, t, front)
5363
5364# A block's rows or columns within the target: start and count.
5365def paint.from(+at: U32, +lo: U32) -> U32:
5366  U.choose(U32, U32.is_lt(at, lo), lo, at)
5367
5368def paint.span(+at: U32, +size: U32, +lo: U32, +hi: U32) -> U32:
5369  +a = paint.from(at, lo)
5370  +b = U.choose(U32, U32.is_lt((at + size : U32), hi), (at + size : U32), hi)
5371  U.choose(U32, U32.is_gt(b, a), (b - a : U32), 0)
5372
5373# An opaque front replaces the pixels outright, no read and no blend.
5374@unsafe
5375def set.row(fb: Array<U32>, n: Nat, +i: U32, +front: U32) -> Array<U32>:
5376  match n:
5377    case 0n: fb
5378    case 1n+d: set.row(Array.set(U32, fb, i, front), d, (i + 1 : U32), front)
5379
5380@unsafe
5381def set.rows(fb: Array<U32>, n: Nat, +x0: U32, +y: U32, +cols: Nat, +t: Target, +front: U32) -> Array<U32>:
5382  match n:
5383    case 0n: fb
5384    case 1n+d: set.rows(set.row(fb, cols, target.index(t, x0, y), front), d, x0, (y + 1 : U32), cols, t, front)
5385
5386def fill.rows.mode(opaque: Bool, fb: Array<U32>, n: Nat, +x0: U32, +y: U32, +cols: Nat, +t: Target, +front: U32) -> Array<U32>:
5387  match opaque:
5388    case True{}: set.rows(fb, n, x0, y, cols, t, front)
5389    case False{}: fill.rows(fb, n, x0, y, cols, t, front)
5390
5391def paint.block.mode(direct: Bool, fb: Array<U32>, +x0: U32, +y0: U32, +rows: Nat, +cols: Nat, +v: F32, +known: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5392  match direct:
5393    case True{}:
5394      +front = pixel.pack(paint.ctx.tint(ctx), U.min(v, known))
5395      fill.rows.mode(U32.is_eq(U32.div(front, 16777216), 255), fb, rows, x0, y0, cols, t, front)
5396    case False{}: paint.rows(fb, rows, x0, y0, cols, v, known, t, ctx)
5397
5398# A block whose clips are uniform paints with that limit folded in: nothing
5399# when it is zero, the constant front when the brush is solid, the general row
5400# without clip reads otherwise; a block a clip edge crosses reads clips per pixel.
5401def paint.block.known(fb: Array<U32>, +x0: U32, +y0: U32, +rows: Nat, +cols: Nat, +v: F32, +known: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5402  paint.block.mode(F32.is_ge(known, 0.0) && paint.ctx.solid(ctx), fb, x0, y0, rows, cols, v, known, t, ctx)
5403
5404def paint.block.some(skip: Bool, fb: Array<U32>, +x0: U32, +y0: U32, +rows: Nat, +cols: Nat, +v: F32, +known: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5405  match skip:
5406    case True{}: fb
5407    case False{}: paint.block.known(fb, x0, y0, rows, cols, v, known, t, ctx)
5408
5409def paint.block.limit(clipped: Bool, +clips: List<&2, Clip>, +x0: U32, +y0: U32, +x1: U32, +y1: U32) -> F32:
5410  match clipped:
5411    case True{}: paint.limit.block(clips, x0, y0, x1, y1, 1.0)
5412    case False{}: 1.0
5413
5414def paint.ctx.clipped(+ctx: PaintCtx) -> Bool:
5415  PaintCtx{br, inv, linear, clips, solid, tint, clipped} = ctx
5416  clipped
5417
5418# One uniform block of a mask, clipped to the target. A block a clip edge
5419# crosses is split in four down to eight pixels, so the per-pixel clip reads
5420# stay near the edge and the rest of the block still fills at once.
5421@unsafe
5422def paint.block(fb: Array<U32>, +x0: U32, +y0: U32, +size: U32, +v: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5423  Target{tx, ty, tw, th} = t
5424  +bx = paint.from(x0, tx)
5425  +by = paint.from(y0, ty)
5426  +cols = paint.span(x0, size, tx, (tx + tw : U32))
5427  +rows = paint.span(y0, size, ty, (ty + th : U32))
5428  +known = paint.block.limit(paint.ctx.clipped(ctx), paint.ctx.clips(ctx), bx, by, (bx + cols : U32), (by + rows : U32))
5429  +half = U32.div(size, 2)
5430  paint.with(F32.is_lt(known, 0.0) && U32.is_gt(size, 8) && U32.is_gt(cols, 0) && U32.is_gt(rows, 0), fb,
5431    fb1 => paint.block(paint.block(paint.block(paint.block(fb1, x0, y0, half, v, t, ctx), (x0 + half : U32), y0, half, v, t, ctx), x0, (y0 + half : U32), half, v, t, ctx), (x0 + half : U32), (y0 + half : U32), half, v, t, ctx),
5432    fb1 => paint.block.some(U32.is_eq(cols, 0) || U32.is_eq(rows, 0) || F32.is_eq(known, 0.0), fb1, bx, by, U32.to_nat(rows), U32.to_nat(cols), v, known, t, ctx))
5433
5434def paint.block.if(cond: Bool, fb: Array<U32>, +x0: U32, +y0: U32, +size: U32, +v: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5435  match cond:
5436    case True{}: paint.block(fb, x0, y0, size, v, t, ctx)
5437    case False{}: fb
5438
5439@unsafe
5440def paint.tree(fb: Array<U32>, t: Cov.CovTree, +x0: U32, +y0: U32, +size: U32, +tg: Target, +ctx: PaintCtx) -> Array<U32>:
5441  match t:
5442    case Cov.CovFlat{v}:
5443      +vv = v
5444      paint.block.if(F32.is_gt(vv, 0.0), fb, x0, y0, size, vv, tg, ctx)
5445    case Cov.CovQuad{a, b, c, d}:
5446      +half = U32.div(size, 2)
5447      paint.tree(paint.tree(paint.tree(paint.tree(fb, a, x0, y0, half, tg, ctx), b, (x0 + half : U32), y0, half, tg, ctx), c, x0, (y0 + half : U32), half, tg, ctx), d, (x0 + half : U32), (y0 + half : U32), half, tg, ctx)
5448
5449def paint.tree.if(cond: Bool, fb: Array<U32>, t: Cov.CovTree, +x0: U32, +y0: U32, +size: U32, +tg: Target, +ctx: PaintCtx) -> Array<U32>:
5450  match cond:
5451    case True{}: paint.tree(fb, t, x0, y0, size, tg, ctx)
5452    case False{}: fb
5453
5454# One run of a row: the columns within the target, the clip limit over them
5455# (per pixel when a clip edge crosses the run), then the row painter.
5456# One run of a row: the columns within the target, the clip limit over
5457# them (the box's limit when that is uniform, else read per run and per
5458# pixel when a clip edge crosses the run), then the row painter.
5459def paint.run(fb: Array<U32>, +x0: U32, +x1: U32, +v: F32, +y: U32, +fixed: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5460  Target{tx, ty, tw, th} = t
5461  +bx = paint.from(x0, tx)
5462  +cols = paint.span(x0, (x1 - x0 : U32), tx, (tx + tw : U32))
5463  +known = U.choose(F32, F32.is_lt(fixed, 0.0), paint.block.limit(paint.ctx.clipped(ctx), paint.ctx.clips(ctx), bx, y, (bx + cols : U32), (y + 1 : U32)), fixed)
5464  paint.block.some(U32.is_eq(cols, 0) || F32.is_eq(known, 0.0), fb, bx, y, 1n, U32.to_nat(cols), v, known, t, ctx)
5465
5466@unsafe
5467def paint.runs(fb: Array<U32>, runs: List<&2, Cov.Run>, +gx: U32, +y: U32, +fixed: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5468  match runs:
5469    case Nil{}: fb
5470    case Con{Cov.Run{x0, x1, v}, rest}: paint.runs(paint.run(fb, (gx + x0 : U32), (gx + x1 : U32), v, y, fixed, t, ctx), rest, gx, y, fixed, t, ctx)
5471
5472def paint.row.if(inside: Bool, fb: Array<U32>, runs: List<&2, Cov.Run>, +gx: U32, +y: U32, +fixed: F32, +t: Target, +ctx: PaintCtx) -> Array<U32>:
5473  match inside:
5474    case True{}: paint.runs(fb, runs, gx, y, fixed, t, ctx)
5475    case False{}: fb
5476
5477# Rows of runs painted straight into the target, no tree needed.
5478@unsafe
5479def paint.rows.go(fb: Array<U32>, rows: List<&2, List<&2, Cov.Run>>, +gx: U32, +y: U32, +fixed: F32, +tx: U32, +ty: U32, +tw: U32, +th: U32, +ctx: PaintCtx) -> Array<U32>:
5480  match rows:
5481    case Nil{}: fb
5482    case Con{r, rest}:
5483      paint.rows.go(paint.row.if(U32.is_ge(y, ty) && U32.is_lt(y, (ty + th : U32)), fb, r, gx, y, fixed, Target{tx, ty, tw, th}, ctx), rest, gx, (y + 1 : U32), fixed, tx, ty, tw, th, ctx)
5484
5485def paint.rows.of(fb: Array<U32>, rows: List<&2, List<&2, Cov.Run>>, +gx: U32, +y: U32, +fixed: F32, t: Target, +ctx: PaintCtx) -> Array<U32>:
5486  Target{tx, ty, tw, th} = t
5487  paint.rows.go(fb, rows, gx, y, fixed, tx, ty, tw, th, ctx)
5488
5489# A solid brush with a uniform clip limit paints runs straight: the front
5490# for full coverage is packed once per shape, a partial run packs its own,
5491# and an opaque front is written without a read.
5492def paint.solid.row.mode(opaque: Bool, fb: Array<U32>, +i: U32, +cols: Nat, +front: U32) -> Array<U32>:
5493  match opaque:
5494    case True{}: set.row(fb, cols, i, front)
5495    case False{}: fill.row(fb, cols, i, front)
5496
5497# The target rectangle and the tint travel as plain numbers through the run
5498# loops: a record passed along would be shared and read again at every run.
5499def paint.solid.run.some(skip: Bool, fb: Array<U32>, +bx: U32, +cols: U32, +v: F32, +y: U32, +tx: U32, +ty: U32, +tw: U32, +full: U32, +tr: F32, +tg: F32, +tb: F32, +ta: F32, +limit: F32) -> Array<U32>:
5500  match skip:
5501    case True{}: fb
5502    case False{}:
5503      +front = U.choose(U32, F32.is_ge(v, 1.0), full, pixel.pack(Img.Color{tr, tg, tb, ta}, U.min(v, limit)))
5504      paint.solid.row.mode(U32.is_eq(U32.div(front, 16777216), 255), fb, ((y - ty) * tw + bx - tx : U32), U32.to_nat(cols), front)
5505
5506def paint.solid.run(fb: Array<U32>, +x0: U32, +x1: U32, +v: F32, +y: U32, +tx: U32, +ty: U32, +tw: U32, +full: U32, +tr: F32, +tg: F32, +tb: F32, +ta: F32, +limit: F32) -> Array<U32>:
5507  +bx = paint.from(x0, tx)
5508  +cols = paint.span(x0, (x1 - x0 : U32), tx, (tx + tw : U32))
5509  paint.solid.run.some(U32.is_eq(cols, 0), fb, bx, cols, v, y, tx, ty, tw, full, tr, tg, tb, ta, limit)
5510
5511@unsafe
5512def paint.solid.runs(fb: Array<U32>, runs: List<&2, Cov.Run>, +gx: U32, +y: U32, +tx: U32, +ty: U32, +tw: U32, +full: U32, +tr: F32, +tg: F32, +tb: F32, +ta: F32, +limit: F32) -> Array<U32>:
5513  match runs:
5514    case Nil{}: fb
5515    case Con{Cov.Run{x0, x1, v}, rest}: paint.solid.runs(paint.solid.run(fb, (gx + x0 : U32), (gx + x1 : U32), v, y, tx, ty, tw, full, tr, tg, tb, ta, limit), rest, gx, y, tx, ty, tw, full, tr, tg, tb, ta, limit)
5516
5517def paint.solid.row.if(inside: Bool, fb: Array<U32>, runs: List<&2, Cov.Run>, +gx: U32, +y: U32, +tx: U32, +ty: U32, +tw: U32, +full: U32, +tr: F32, +tg: F32, +tb: F32, +ta: F32, +limit: F32) -> Array<U32>:
5518  match inside:
5519    case True{}: paint.solid.runs(fb, runs, gx, y, tx, ty, tw, full, tr, tg, tb, ta, limit)
5520    case False{}: fb
5521
5522@unsafe
5523def paint.solid.rows.go(fb: Array<U32>, rows: List<&2, List<&2, Cov.Run>>, +gx: U32, +y: U32, +tx: U32, +ty: U32, +tw: U32, +th: U32, +full: U32, +tr: F32, +tg: F32, +tb: F32, +ta: F32, +limit: F32) -> Array<U32>:
5524  match rows:
5525    case Nil{}: fb
5526    case Con{r, rest}:
5527      paint.solid.rows.go(paint.solid.row.if(U32.is_ge(y, ty) && U32.is_lt(y, (ty + th : U32)), fb, r, gx, y, tx, ty, tw, full, tr, tg, tb, ta, limit), rest, gx, (y + 1 : U32), tx, ty, tw, th, full, tr, tg, tb, ta, limit)
5528
5529def paint.solid.rows(fb: Array<U32>, rows: List<&2, List<&2, Cov.Run>>, +gx: U32, +y: U32, t: Target, +full: U32, tint: Img.Color, +limit: F32) -> Array<U32>:
5530  Target{tx, ty, tw, th} = t
5531  Img.Color{tr, tg, tb, ta} = tint
5532  paint.solid.rows.go(fb, rows, gx, y, tx, ty, tw, th, full, tr, tg, tb, ta, limit)
5533
5534def paint.rows.mode(solid: Bool, fb: Array<U32>, rows: List<&2, List<&2, Cov.Run>>, +gx: U32, +gy: U32, +fixed: F32, +tg: Target, +ctx: PaintCtx) -> Array<U32>:
5535  match solid:
5536    case True{}:
5537      +tint = paint.ctx.tint(ctx)
5538      paint.solid.rows(fb, rows, gx, gy, tg, pixel.pack(tint, fixed), tint, fixed)
5539    case False{}: paint.rows.of(fb, rows, gx, gy, fixed, tg, ctx)
5540
5541# The clips are read once over the mask's whole box: uniform there (the
5542# usual case, a shape inside the viewport clip), that limit serves every
5543# run and nothing reads the clips again; zero there paints nothing.
5544def paint.data(d: Cov.CovData, fb: Array<U32>, +gx: U32, +gy: U32, +size: U32, +fixed: F32, +tg: Target, +ctx: PaintCtx) -> Array<U32>:
5545  match d:
5546    case Cov.CovRows{rows}: paint.rows.mode(F32.is_ge(fixed, 0.0) && paint.ctx.solid(ctx), fb, rows, gx, gy, fixed, tg, ctx)
5547    case Cov.CovTreed{tree}: paint.tree(fb, tree, gx, gy, size, tg, ctx)
5548
5549def paint.data.known(empty: Bool, +known: F32, d: Cov.CovData, fb: Array<U32>, +gx: U32, +gy: U32, +size: U32, +tg: Target, +ctx: PaintCtx) -> Array<U32>:
5550  match empty:
5551    case True{}: fb
5552    case False{}: paint.data(d, fb, gx, gy, size, known, tg, ctx)
5553
5554def paint.data.if(present: Bool, d: Cov.CovData, fb: Array<U32>, +gx: U32, +gy: U32, +w: U32, +h: U32, +size: U32, +tg: Target, +ctx: PaintCtx) -> Array<U32>:
5555  match present:
5556    case True{}:
5557      Target{tx, ty, tw, th} = tg
5558      +x0 = U.max.u32(gx, tx)
5559      +y0 = U.max.u32(gy, ty)
5560      +x1 = U.min.u32((gx + w : U32), (tx + tw : U32))
5561      +y1 = U.min.u32((gy + h : U32), (ty + th : U32))
5562      +known = U.choose(F32, U32.is_ge(x0, x1) || U32.is_ge(y0, y1), 0.0, paint.block.limit(paint.ctx.clipped(ctx), paint.ctx.clips(ctx), x0, y0, x1, y1))
5563      paint.data.known(F32.is_eq(known, 0.0), known, d, fb, gx, gy, size, Target{tx, ty, tw, th}, ctx)
5564    case False{}: fb
5565
5566# A shape's coverage mask on the frame grid, painted with its brush.
5567def paint.cover(fb: Array<U32>, +cov: Cover, +tg: Target, +ctx: PaintCtx) -> Array<U32>:
5568  Cover{Cov.CovMask{gx, gy, w, h, size, depth, data, present}, origin, step} = cov
5569  paint.data.if(present, data, fb, gx, gy, w, h, size, tg, ctx)
5570
5571def paint.fill.if(cond: Bool, fb: Array<U32>, f: Fill, +brush: Brush, +inv: Matrix, +linear: Bool, +clips: List<&2, Clip>, +tg: Target) -> Array<U32>:
5572  match cond:
5573    case True{}:
5574      Fill{box, cover} = f
5575      paint.cover(fb, cover, tg, paint.context(brush, inv, linear, clips))
5576    case False{}: fb
5577
5578def paint.stroke.if(cond: Bool, fb: Array<U32>, mesh: StrokeMesh, +brush: Brush, +inv: Matrix, +linear: Bool, +clips: List<&2, Clip>, +tg: Target) -> Array<U32>:
5579  match cond:
5580    case True{}:
5581      StrokeMesh{tree, meshInverse, cover} = mesh
5582      paint.cover(fb, cover, tg, paint.context(brush, inv, linear, clips))
5583    case False{}: fb
5584
5585# The fill, stroke and markers of a shape in its paint order (see
5586# paint.order.first): each arm hands the fill's and the stroke's masks to
5587# exactly one painter, so the masks are never shared and walked as copies.
5588def paint.pieces(order: U32, fb: Array<U32>, +fills: Bool, +strokes: Bool, ink: Paint<Brush>, +inv: Matrix, outline: StrokeMesh, f: Fill, markers: Array<U32> -> Array<U32>, +linear: Bool, +clips: List<&2, Clip>, +tg: Target) -> Array<U32>:
5589  match order:
5590    case 0:
5591      Paint{fc, sc} = ink
5592      markers(paint.stroke.if(strokes, paint.fill.if(fills, fb, f, fc, inv, linear, clips, tg), outline, sc, inv, linear, clips, tg))
5593    case 1:
5594      Paint{fc, sc} = ink
5595      paint.stroke.if(strokes, markers(paint.fill.if(fills, fb, f, fc, inv, linear, clips, tg)), outline, sc, inv, linear, clips, tg)
5596    case 2:
5597      Paint{fc, sc} = ink
5598      markers(paint.fill.if(fills, paint.stroke.if(strokes, fb, outline, sc, inv, linear, clips, tg), f, fc, inv, linear, clips, tg))
5599    case 3:
5600      Paint{fc, sc} = ink
5601      paint.fill.if(fills, markers(paint.stroke.if(strokes, fb, outline, sc, inv, linear, clips, tg)), f, fc, inv, linear, clips, tg)
5602    case 4:
5603      Paint{fc, sc} = ink
5604      paint.stroke.if(strokes, paint.fill.if(fills, markers(fb), f, fc, inv, linear, clips, tg), outline, sc, inv, linear, clips, tg)
5605    case _:
5606      Paint{fc, sc} = ink
5607      paint.fill.if(fills, paint.stroke.if(strokes, markers(fb), outline, sc, inv, linear, clips, tg), f, fc, inv, linear, clips, tg)
5608
5609# paint-order as three steps: the first piece is order / 2, the other two in
5610# ascending order for an even value and descending for an odd one.
5611def paint.order.first(+order: U32) -> U32:
5612  U32.div(order, 2)
5613
5614def paint.order.second(+order: U32) -> U32:
5615  +first = U32.div(order, 2)
5616  +lo = U.choose(U32, U32.is_eq(first, 0), 1, 0)
5617  +hi = U.choose(U32, U32.is_eq(first, 2), 1, 2)
5618  U.choose(U32, U32.is_eq(U32.mod(order, 2), 0), lo, hi)
5619
5620def paint.order.third(+order: U32) -> U32:
5621  +first = U32.div(order, 2)
5622  +lo = U.choose(U32, U32.is_eq(first, 0), 1, 0)
5623  +hi = U.choose(U32, U32.is_eq(first, 2), 1, 2)
5624  U.choose(U32, U32.is_eq(U32.mod(order, 2), 0), hi, lo)
5625
5626def paint.order.of(+s: Style) -> U32:
5627  Style{fill, stroke, col, fo, so, width, even, visible, ss, ms, rendering, order} = s
5628  order
5629
5630def paint.markers.at(+which: U32, +nodes: List<&2, Scene<Brush>>) -> List<&2, Scene<Brush>>:
5631  U.choose(List<&2, Scene<Brush>>, U32.is_eq(which, 2), nodes, Nil{})
5632
5633def paint.clips(clip: Clip, +clips: List<&2, Clip>) -> List<&2, Clip>:
5634  match clip:
5635    case ClipAll{}: clips
5636    case _: Con{clip, clips}
5637
5638# ---- layer compositing ----
5639
5640def comp.factor.got(got: Array<U32> & U32, +mode: U32) -> Array<U32> & F32:
5641  (a, px) = got
5642  (a, pixel.mask(px, mode))
5643
5644# The mask factor at index i: one without a mask, else from the mask layer's pixel.
5645def comp.factor.if(read: Bool, mask: Array<U32>, +i: U32, +mode: U32) -> Array<U32> & F32:
5646  match read:
5647    case True{}: comp.factor.got(Array.get(U32, mask, i), mode)
5648    case False{}: (mask, 1.0)
5649
5650def comp.factor(mask: Array<U32>, +i: U32, +mode: U32) -> Array<U32> & F32:
5651  comp.factor.if(U32.is_ne(mode, 0), mask, i, mode)
5652
5653def comp.pixel.mask(got: Array<U32> & F32, fb: Array<U32>, layer: Array<U32>, +i: U32, +opacity: F32, +px: U32) -> Array<U32> & (Array<U32> & Array<U32>):
5654  (mask, f) = got
5655  (blend(fb, i, pixel.scale(px, (opacity * f : F32))), (layer, mask))
5656
5657def comp.pixel.layer(got: Array<U32> & U32, fb: Array<U32>, mask: Array<U32>, +i: U32, +opacity: F32, +mode: U32) -> Array<U32> & (Array<U32> & Array<U32>):
5658  (layer, px) = got
5659  comp.pixel.mask(comp.factor(mask, i, mode), fb, layer, i, opacity, px)
5660
5661def comp.pixel.mask.at(got: Array<U32> & F32, fb: Array<U32>, layer: Array<U32>, +i: U32, +opacity: F32, +px: U32) -> Array<U32> & (Array<U32> & Array<U32>):
5662  (mask, f) = got
5663  (blend(fb, i, pixel.scale(px, (opacity * f : F32))), (layer, mask))
5664
5665def comp.pixel.layer.at(got: Array<U32> & U32, fb: Array<U32>, mask: Array<U32>, +i: U32, +j: U32, +opacity: F32, +mode: U32) -> Array<U32> & (Array<U32> & Array<U32>):
5666  (layer, px) = got
5667  comp.pixel.mask.at(comp.factor(mask, j, mode), fb, layer, i, opacity, px)
5668
5669# One pixel: the layer's pixel (index j in the layer and mask) scaled by opacity
5670# and the mask, over the frame pixel at index i.
5671def comp.step2(rest: Array<U32> & Array<U32>, fb: Array<U32>, +i: U32, +j: U32, +opacity: F32, +mode: U32) -> Array<U32> & (Array<U32> & Array<U32>):
5672  (layer, mask) = rest
5673  comp.pixel.layer.at(Array.get(U32, layer, j), fb, mask, i, j, opacity, mode)
5674
5675def comp.step(state: Array<U32> & (Array<U32> & Array<U32>), +i: U32, +j: U32, +opacity: F32, +mode: U32) -> Array<U32> & (Array<U32> & Array<U32>):
5676  (fb, rest) = state
5677  comp.step2(rest, fb, i, j, opacity, mode)
5678
5679@unsafe
5680def comp.row(state: Array<U32> & (Array<U32> & Array<U32>), n: Nat, +i: U32, +j: U32, +opacity: F32, +mode: U32) -> Array<U32> & (Array<U32> & Array<U32>):
5681  match n:
5682    case 0n: state
5683    case 1n+d: comp.row(comp.step(state, i, j, opacity, mode), d, (i + 1 : U32), (j + 1 : U32), opacity, mode)
5684
5685@unsafe
5686def comp.rows(state: Array<U32> & (Array<U32> & Array<U32>), n: Nat, +y: U32, +x0: U32, +cols: Nat, +ft: Target, +lt: Target, +opacity: F32, +mode: U32) -> Array<U32> & (Array<U32> & Array<U32>):
5687  match n:
5688    case 0n: state
5689    case 1n+d: comp.rows(comp.row(state, cols, target.index(ft, x0, y), target.index(lt, x0, y), opacity, mode), d, (y + 1 : U32), x0, cols, ft, lt, opacity, mode)
5690
5691def comp.finish(state: Array<U32> & (Array<U32> & Array<U32>)) -> Array<U32>:
5692  (fb, rest) = state
5693  fb
5694
5695# The pixel range of a device box within the frame: start and count per axis.
5696def comp.start(+lo: F32, +limit: U32) -> U32:
5697  F32.to_u32(U.clamp(F32.floor(lo), 0.0, U32.to_f32(limit)))
5698
5699def comp.count(+lo: F32, +hi: F32, +limit: U32) -> U32:
5700  +a = comp.start(lo, limit)
5701  +b = F32.to_u32(U.clamp(F32.ceil(hi), 0.0, U32.to_f32(limit)))
5702  U.choose(U32, U32.is_gt(b, a), (b - a : U32), 0)
5703
5704# A finished layer (and its mask layer, both on the layer target), scaled by
5705# opacity and the mask, over the frame target across the layer's rectangle.
5706def comp.layer(fb: Array<U32>, layer: Array<U32>, mask: Array<U32>, +ft: Target, +lt: Target, +opacity: F32, +mode: U32) -> Array<U32>:
5707  Target{x, y, w, h} = lt
5708  comp.finish(comp.rows((fb, (layer, mask)), U32.to_nat(h), y, x, U32.to_nat(w), ft, lt, opacity, mode))
5709
5710# A rendered filter raster, faded by the node's clip, opacity and mask, over the frame.
5711def rcomp.pixel.mask(got: Array<U32> & F32, fb: Array<U32>, +i: U32, +opacity: F32, c: Img.Color, +limit: F32) -> Array<U32> & Array<U32>:
5712  (mask, f) = got
5713  (blend(fb, i, pixel.pack(c, (limit * opacity * f : F32))), mask)
5714
5715def rcomp.step(state: Array<U32> & Array<U32>, +x: U32, +y: U32, +ft: Target, +lt: Target, +raster: Raster, +clip: Clip, +linear: Bool, +opacity: F32, +mode: U32) -> Array<U32> & Array<U32>:
5716  (fb, mask) = state
5717  +p = {Point{(U32.to_f32(x) + 0.5 : F32), (U32.to_f32(y) + 0.5 : F32)} : Point}
5718  rcomp.pixel.mask(comp.factor(mask, target.index(lt, x, y), mode), fb, target.index(ft, x, y), opacity, raster.color(raster, p, linear), clip.limit(clip, p))
5719
5720@unsafe
5721def rcomp.row(state: Array<U32> & Array<U32>, n: Nat, +x: U32, +y: U32, +ft: Target, +lt: Target, +raster: Raster, +clip: Clip, +linear: Bool, +opacity: F32, +mode: U32) -> Array<U32> & Array<U32>:
5722  match n:
5723    case 0n: state
5724    case 1n+d: rcomp.row(rcomp.step(state, x, y, ft, lt, raster, clip, linear, opacity, mode), d, (x + 1 : U32), y, ft, lt, raster, clip, linear, opacity, mode)
5725
5726@unsafe
5727def rcomp.rows(state: Array<U32> & Array<U32>, n: Nat, +y: U32, +x0: U32, +cols: Nat, +ft: Target, +lt: Target, +raster: Raster, +clip: Clip, +linear: Bool, +opacity: F32, +mode: U32) -> Array<U32> & Array<U32>:
5728  match n:
5729    case 0n: state
5730    case 1n+d: rcomp.rows(rcomp.row(state, cols, x0, y, ft, lt, raster, clip, linear, opacity, mode), d, (y + 1 : U32), x0, cols, ft, lt, raster, clip, linear, opacity, mode)
5731
5732def rcomp.finish(state: Array<U32> & Array<U32>) -> Array<U32>:
5733  (fb, mask) = state
5734  fb
5735
5736# A rendered filter raster over the frame target across the layer target's
5737# rectangle, faded by the node's clip, opacity and mask (a layer on lt).
5738def comp.raster(fb: Array<U32>, mask: Array<U32>, +raster: Raster, +clip: Clip, +ft: Target, +lt: Target, +linear: Bool, +opacity: F32, +mode: U32) -> Array<U32>:
5739  Target{x, y, w, h} = lt
5740  rcomp.finish(rcomp.rows((fb, mask), U32.to_nat(h), y, x, U32.to_nat(w), ft, lt, raster, clip, linear, opacity, mode))
5741
5742def filter.raster(effect: FilterEffect) -> Raster:
5743  match effect:
5744    case NoFilter{}: raster.empty()
5745    case FilterSpec{region, units, viewport, attrs, primitives}: raster.empty()
5746    case FilterImage{raster}: raster
5747
5748# ---- nodes ----
5749
5750# Paint a list of scenes onto fb, back to front. A node is direct when it has
5751# no opacity, mask or rendered filter: fill, stroke and markers in paint order,
5752# then its children, all limited by the enclosing clips plus its own. Otherwise
5753# the same content goes into a fresh layer (a copy of the node without those
5754# three) and the layer, or the filter raster, is composited within the node's
5755# coverage box; a mask is a layer painted from the mask nodes.
5756@unsafe
5757# A scene with an effect, a mask or partial opacity is painted into a layer
5758# of its own first (the scene without those, over a fresh layer), then
5759# composited; a filtered scene's raster stands in for the layer.
5760def paint.layered(rendered: Bool, fb: Array<U32>, es: List<&2, Edge>, box: Bounds, inv: Matrix, style: Style, +opacity: F32, kids: List<&2, Scene<Brush>>, id: String, ink: Paint<Brush>, clip: Clip, outline: StrokeMesh, fill: Fill,
5761  maskNodes: List<&2, Scene<Brush>>, +maskMode: U32, markerNodes: List<&2, Scene<Brush>>, coverage: Bounds, effect: FilterEffect, +t: Target, +lt: Target, +linear: Bool, +clips: List<&2, Clip>,
5762  again: List<&2, Scene<Brush>> -> Target -> Bool -> List<&2, Clip> -> Array<U32> -> Array<U32>, more: List<&2, Scene<Brush>> -> Target -> Bool -> List<&2, Clip> -> Array<U32> -> Array<U32>) -> Array<U32>:
5763  match rendered:
5764    case True{}: comp.raster(fb, again(paint.markers.at(U.choose(U32, U32.is_ne(maskMode, 0), 2, 0), maskNodes))(lt)(U32.is_eq(maskMode, 3))(Nil{})(layer.new(lt)), filter.raster(effect), clip, t, lt, linear, opacity, maskMode)
5765    case False{}: comp.layer(fb,
5766      again(Con{Scene{es, box, inv, style, 1.0, kids, id, ink, clip, outline, fill, Nil{}, 0, markerNodes, coverage, NoFilter{}}, Nil{}})(lt)(linear)(clips)(layer.new(lt)),
5767      more(paint.markers.at(U.choose(U32, U32.is_ne(maskMode, 0), 2, 0), maskNodes))(lt)(U32.is_eq(maskMode, 3))(Nil{})(layer.new(lt)),
5768      t, lt, opacity, maskMode)
5769
5770# A scene painted directly: its pieces in paint order, then its children.
5771def paint.node(direct: Bool, fb: Array<U32>, es: List<&2, Edge>, box: Bounds, +inv: Matrix, +style: Style, +opacity: F32, kids: List<&2, Scene<Brush>>, id: String, ink: Paint<Brush>, clip: Clip, outline: StrokeMesh, fill: Fill,
5772  maskNodes: List<&2, Scene<Brush>>, +maskMode: U32, markerNodes: List<&2, Scene<Brush>>, coverage: Bounds, +effect: FilterEffect, +t: Target, +linear: Bool, +clips: List<&2, Clip>, +inner: List<&2, Clip>, +lt: Target,
5773  again: List<&2, Scene<Brush>> -> Target -> Bool -> List<&2, Clip> -> Array<U32> -> Array<U32>, more: List<&2, Scene<Brush>> -> Target -> Bool -> List<&2, Clip> -> Array<U32> -> Array<U32>) -> Array<U32>:
5774  match direct:
5775    case True{}: again(kids)(t)(linear)(inner)(paint.pieces(paint.order.of(style), fb, style.fills(style), style.strokes(style), ink, inv, outline, fill, fbm => more(markerNodes)(t)(linear)(inner)(fbm), linear, inner, t))
5776    case False{}: paint.layered(filter.rendered(effect), fb, es, box, inv, style, opacity, kids, id, ink, clip, outline, fill, maskNodes, maskMode, markerNodes, coverage, effect, t, lt, linear, clips, again, more)
5777
5778@unsafe
5779def paint.scenes(fb: Array<U32>, xs: List<&2, Scene<Brush>>, +t: Target, +linear: Bool, +clips: List<&2, Clip>) -> Array<U32>:
5780  match xs:
5781    case Nil{}: fb
5782    case Con{Scene{es, box, inv, style, opacity, kids, id, ink, clip, outline, fill, maskNodes, maskMode, markerNodes, coverage, effect}, rest}:
5783      +cl = clip
5784      +eff = effect
5785      +mm = maskMode
5786      +op = opacity
5787      +cov = coverage
5788      paint.scenes(paint.node(F32.is_ge(op, 1.0) && U32.is_eq(mm, 0) && Bool.not(filter.rendered(eff)), fb, es, box, inv, style, op, kids, id, ink, cl, outline, fill, maskNodes, mm, markerNodes, cov, eff, t, linear, clips, paint.clips(cl, clips), target.within(t, cov),
5789        nodes => tt => lin => cls => fbx => paint.scenes(fbx, nodes, tt, lin, cls), nodes => tt => lin => cls => fbx => paint.scenes(fbx, nodes, tt, lin, cls)), rest, t, linear, clips)
5790
5791# The whole document at w by h device pixels, packed premultiplied RGBA.
5792def paint.frame(scene: List<&2, Scene<Brush>>, +w: U32, +h: U32, +aa: U32) -> Array<U32>:
5793  paint.scenes(layer.new(Target{0, 0, w, h}), cover.scenes(prepare.scenes(scene, identity(), aa), frame(w, h), cover.sub(aa), False{}), Target{0, 0, w, h}, False{}, Nil{})
5794
5795# ---- packed output ----
5796
5797def pack.got(got: Array<U32> & U32, +j: U32) -> Array<U32>:
5798  (fb, px) = got
5799  Array.set(U32, fb, j, pixel.white(px))
5800
5801# Every pixel over white, in place, as the 0xRRGGBB the outputs use.
5802@unsafe
5803def pack.loop(fb: Array<U32>, n: Nat, +j: U32) -> Array<U32>:
5804  match n:
5805    case 0n: fb
5806    case 1n+d: pack.loop(pack.got(Array.get(U32, fb, j), j), d, (j + 1 : U32))
5807
5808# The document as packed 0xRRGGBB pixels over white, row-major, w * h entries.
5809def paint.packed(scene: List<&2, Scene<Brush>>, +w: U32, +h: U32, +aa: U32) -> Array<U32>:
5810  pack.loop(paint.frame(scene, w, h, aa), U32.to_nat((w * h : U32)), 0)
5811
5812# Threads an array through a continuation after one read.
5813def packed.then(-R: Type, got: Array<U32> & U32, next: Array<U32> -> U32 -> R) -> R:
5814  (a, v) = got
5815  next(a)(v)
5816
5817# The window image: a quadtree over a 2^depth square, read from the packed pixels.
5818@unsafe
5819def image.of(a: Array<U32>, depth: Nat, +x: U32, +y: U32, +side: U32, next: Array<U32> -> Image -> Image) -> Image:
5820  match depth:
5821    case 0n: packed.then(Image, Array.get(U32, a, (y * side + x : U32)), a1 => v => next(a1)(Pix{v}))
5822    case 1n+d:
5823      +dd = d
5824      +half = U32.shln(1, dd)
5825      image.of(a, dd, x, y, side, a1 => ia => image.of(a1, dd, (x + half : U32), y, side, a2 => ib => image.of(a2, dd, x, (y + half : U32), side,
5826        a3 => ic => image.of(a3, dd, (x + half : U32), (y + half : U32), side, a4 => id => next(a4)(Qua{ia, ib, ic, id})))))
5827
5828def image(+depth: Nat, x: U32, y: U32, scene: List<&2, Scene<Brush>>, +aa: U32) -> Image:
5829  +side = U32.shln(1, depth)
5830  image.of(paint.packed(scene, side, side, aa), depth, 0, 0, side, a => im => im)
5831
5832@unsafe
5833def ppm.cols(a: Array<U32>, n: Nat, +j: U32, next: Array<U32> -> String -> String) -> String:
5834  match n:
5835    case 0n: next(a)("")
5836    case 1n+d: packed.then(String, Array.get(U32, a, j), a1 => v => ppm.cols(a1, d, (j + 1 : U32), a2 => rest => next(a2)(pixel.ppm(v) ++ rest)))
5837
5838@unsafe
5839def ppm.rows(a: Array<U32>, n: Nat, +y: U32, +w: U32, next: Array<U32> -> String -> String) -> String:
5840  match n:
5841    case 0n: next(a)("")
5842    case 1n+d: ppm.cols(a, U32.to_nat(w), (y * w : U32), a1 => row => ppm.rows(a1, d, (y + 1 : U32), w, a2 => rest => next(a2)(row ++ rest)))
5843
5844def ppm(+w: U32, +h: U32, scene: List<&2, Scene<Brush>>, +aa: U32) -> String:
5845  "P3\n" ++ U32.show(w) ++ " " ++ U32.show(h) ++ "\n255\n" ++ ppm.rows(paint.packed(scene, w, h, aa), U32.to_nat(h), 0, w, a => text => text)
5846
5847type Pick is Data:
5848  Pick{found: Bool, id: String, inverse: Matrix}
5849
5850def pick.prefer(+a: Pick, b: Pick) -> Pick:
5851  Pick{found, id, inv} = a
5852  U.choose(Pick, found, a, b)
5853
5854def pick.alpha(c: Img.Color) -> Bool:
5855  Img.Color{r, g, b, a} = c
5856  F32.is_gt(a, 0.0)
5857
5858def pick.node(c: Img.Color, id: String, inv: Matrix) -> Pick:
5859  U.choose(Pick, pick.alpha(c), Pick{True{}, id, inv}, Pick{False{}, "", identity()})
5860
5861def pick.instance(+hit: Pick, id: String, inv: Matrix) -> Pick:
5862  Pick{found, target, local} = hit
5863  U.choose(Pick, found && U.string.equal(target, ""), Pick{found, id, inv}, hit)
5864
5865def pick(+scenes: List<&2, Scene<Brush>>, +p: Point) -> Pick:
5866  match scenes:
5867    case Nil{}: Pick{False{}, "", identity()}
5868    case Con{Scene{es, box, inv, style, opacity, kids, id, ink, mask, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect}, rest}:
5869      node = {Scene{es, box, inv, style, opacity, kids, id, ink, mask, outline, fillTree, maskNodes, maskMode, markerNodes, coverage, effect} : Scene<Brush>}
5870      pick.prefer(pick(rest, p), U.branch(Pick, bounds.inside(coverage, p, 0.01) && clip.inside(mask, p) && F32.is_gt(mask.factor(maskMode, _ => sample(maskNodes, p, p, Img.clear(), U32.is_eq(maskMode, 3), 1.0)), 0.0), _ => pick.prefer(pick.instance(pick(kids, p), id, inv),
5871        pick.node(scene.local(node, p, p, False{}, sample(markerNodes, p, p, Img.clear(), False{}, 1.0), nodes => q => center => sample(nodes, q, center, Img.clear(), False{}, 1.0), nodes => q => center => sample(nodes, q, center, Img.clear(), False{}, 1.0), 1.0), id, inv)), _ => Pick{False{}, "", identity()}))