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()}))