1 # SVG parsing, geometry, painting and rasterization. No foreign code. 2 import Base 3 import gralha-text-scan@0.1.0.0/scan.bend as U 4 import gralha-xml-doc@0.1.0.0/xml.bend as XML 5 import gralha-css-style@0.1.0.0/css.bend as CSS 6 import gralha-bytes@0.1.0.0/bytes.bend as Bin 7 import gralha-image@0.1.0.0/image.bend as Img 8 import gralha-png-decode@0.1.0.0/png.bend as PNG 9 import gralha-jpeg-decode@0.1.0.0/jpeg.bend as JPEG 10 import gralha-coverage@0.1.0.0/cover.bend as Cov 11 import gralha-truetype@0.1.0.0/font.bend as Font 12 13 def 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 18 def length(s: String) -> U32: 19 length.go(s, 0) 20 21 # Affine transforms, composed in SVG's column - vector order. 22 type Matrix is Data: 23 Matrix{a: F32, b: F32, c: F32, d: F32, e: F32, f: F32} 24 25 def identity() -> Matrix: 26 Matrix{1.0, 0.0, 0.0, 1.0, 0.0, 0.0} 27 28 def 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 34 def 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 39 def 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 43 type Point is Data: 44 Point{x: F32, y: F32} 45 46 def 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 51 def translate(x: F32, y: F32) -> Matrix: 52 Matrix{1.0, 0.0, 0.0, 1.0, x, y} 53 54 def scale(x: F32, y: F32) -> Matrix: 55 Matrix{x, 0.0, 0.0, y, 0.0, 0.0} 56 57 def 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 61 def 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 77 def 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 81 def 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 86 def 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. 95 type Edge is Data: 96 Edge{x0: F32, y0: F32, x1: F32, y1: F32, stroke: Bool, closed: Bool} 97 98 def 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. 106 type Cover is Data: 107 Cover{mask: Cov.CovMask, origin: Point, step: F32} 108 109 def cover.none() -> Cover: 110 Cover{Cov.mask.none(), Point{0.0, 0.0}, 1.0} 111 112 def cover.present(+c: Cover) -> Bool: 113 Cover{mask, origin, step} = c 114 Cov.mask.present(mask) 115 116 def 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 121 def 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 126 def 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 131 def point.scale(p: Point, +s: F32) -> Point: 132 Point{x, y} = p 133 Point{(x * s : F32), (y * s : F32)} 134 135 def point.dot(p: Point, q: Point) -> F32: 136 Point{x, y} = p 137 Point{u, v} = q 138 (x * u + y * v : F32) 139 140 def point.mid(p: Point, q: Point) -> Point: 141 point.scale(point.add(p, q), 0.5) 142 143 def 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. 150 def 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. 155 def 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). 161 def local.tolerance(+m: Matrix, +scale: F32) -> F32: 162 (flat.tolerance() / (scale * matrix.norm(m)) : F32) 163 164 def 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. 171 def 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. 183 def 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. 192 def 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 196 def 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 202 def 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 209 def 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. 216 type PathNext is Data: 217 PathNext{kind: U32, ch: U32, rest: String} 218 219 type Params is Data: 220 Params{valid: Bool, values: List<&2, F32>, next: PathNext} 221 222 def 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 225 def 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 232 def 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 237 def 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 242 def 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. 248 def 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 254 def 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 264 def 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. 270 type MarkerSegment is Data: 271 MarkerSegment{p: Point, q: Point, outgoing: Point, incoming: Point, kind: U32} 272 273 def 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 278 def marker.nonzero(+v: Point) -> Bool: 279 F32.is_gt(point.dot(v, v), 0.000000000001) 280 281 def marker.vector(+a: Point, b: Point) -> Point: 282 U.choose(Point, marker.nonzero(a), a, b) 283 284 def 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 288 def marker.line(+p: Point, +q: Point, kind: U32) -> MarkerSegment: 289 MarkerSegment{p, q, point.sub(q, p), point.sub(q, p), kind} 290 291 def 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 296 type 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 299 def upper(+c: U32) -> U32: 300 U.choose(U32, U32.is_ge(c, 97) && U32.is_le(c, 122), (c - 32 : U32), c) 301 302 def 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 305 def 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 310 def 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 314 def 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 319 def 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 326 def 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 330 def 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 334 def 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. 339 def 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 343 def 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 351 def 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 362 def 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 374 def 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 383 def 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 393 def 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 401 def 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. 411 def 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. 417 def 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 423 def 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 472 def 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. 480 def 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. 488 def 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 491 def 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 499 def 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 516 def path.loop(s: String, p: Path) -> Path: 517 path.run(path.arguments(0n, s, False{}, 0), 0, p) 518 519 def 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 523 def 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. 527 def 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 534 def 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 539 def 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 543 def 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 551 def 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 558 type Bounds is Data: 559 Bounds{x0: F32, y0: F32, x1: F32, y1: F32} 560 561 def 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 566 def 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 570 def 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 575 def 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 581 def 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 584 def 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 588 def 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 594 def 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 599 def byte(+x: U32, n: U32) -> F32: 600 (U32.to_f32(U32.mod(U32.div(x, n), 256)) / 255.0 : F32) 601 602 type Raster is Data: 603 Raster{origin: Point, step: F32, width: U32, height: U32, depth: Nat, linear: Bool, pixels: Img.Pixels} 604 605 def color.rgb(+x: U32) -> Img.Color: 606 Img.rgba(byte(x, 65536), byte(x, 256), byte(x, 1), 1.0) 607 608 def nibble(+x: U32, n: U32) -> F32: 609 (U32.to_f32(U32.mod(U32.div(x, n), 16)) / 15.0 : F32) 610 611 def 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 615 def 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 620 def 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 623 def 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 626 def 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 629 def 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 632 def 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 635 def 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 638 def 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 641 def 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 644 def color.named.k(+s: String) -> U32: 645 U.branch(U32, U.string.equal(s, "khaki"), _ => 15787660, _ => 16777216) 646 647 def 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 650 def 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 653 def 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 656 def 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 659 def 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 662 def 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 665 def 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 668 def 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 671 def color.named.v(+s: String) -> U32: 672 U.branch(U32, U.string.equal(s, "violet"), _ => 15631086, _ => 16777216) 673 674 def 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 677 def 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 680 def 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 683 def 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. 689 type ColorToken is Data: 690 ColorToken{value: F32, kind: U32, separator: U32} 691 692 def 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 699 def 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 706 def 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 709 def 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 721 def 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 726 def color.component(+kind: U32) -> Bool: 727 U32.is_eq(kind, 0) || U32.is_eq(kind, 1) || U32.is_eq(kind, 6) 728 729 def 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 733 def 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 738 def 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 743 def 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 748 def 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 755 def 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 774 def 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 780 def 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 787 def 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 792 def 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. 801 def 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 807 def 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 817 def 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 824 def 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 830 def 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 839 def color(s: String) -> Img.Color: 840 color.head(U.trim(s)) 841 842 def 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 847 def color.opacity(s: String, fallback: F32) -> F32: 848 color.opacity.number(U.number(s), fallback) 849 850 def 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 853 def xy(x: F32, y: F32) -> String: 854 F32.show(x) ++ "," ++ F32.show(y) ++ " " 855 856 def 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 864 def 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 872 def 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 877 def 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. 885 def 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 902 def glyph.path(+d: String, +gm: Matrix, +tol: F32) -> List<&2, Edge>: 903 edges.transform(path(d, (tol / matrix.norm(gm) : F32)), gm) 904 905 def 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 910 def 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. 915 def 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 920 def 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. 928 def 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 931 type MarkerSubpath is Data: 932 MarkerSubpath{start: Point, segments: List<&2, MarkerSegment>, closed: Bool} 933 934 def 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 939 def 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 944 def 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 956 def 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 963 def 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 969 def marker.angle.vector(v: Point) -> F32: 970 Point{x, y} = v 971 (F32.atan2(y, x) * 57.29577951308232 : F32) 972 973 def 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 977 def 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 980 type MarkerVertex is Data: 981 MarkerVertex{point: Point, angle: F32} 982 983 @unsafe 984 def 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 993 def 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 1003 def 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 1008 def 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 1013 def 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 1018 def 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 1022 def 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. 1027 type StrokeSettings is Data: 1028 StrokeSettings{cap: String, join: String, limit: F32, dash: String, offset: String, vector: String} 1029 1030 def 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. 1039 def 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 1044 def 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 1052 def 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 1055 def 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 1062 def style.keyword(s: String) -> U32: 1063 keyword.head(U.trim(s)) 1064 1065 def 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 1074 def 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. 1079 def 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 1088 def 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. 1095 type 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 1101 def 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 1104 def 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. 1110 def 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 1113 def 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 1118 def 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 1121 def 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 1126 def 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 1134 def 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 1158 def 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 1163 def 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 1171 def point.cross(p: Point, q: Point) -> F32: 1172 Point{x, y} = p 1173 Point{u, v} = q 1174 (x * v - y * u : F32) 1175 1176 def point.normal(p: Point) -> Point: 1177 Point{x, y} = p 1178 Point{F32.neg(y), x} 1179 1180 def point.unit(+p: Point) -> Point: 1181 point.scale(p, (1.0 / U.max(0.0000001, F32.sqrt(point.dot(p, p))) : F32)) 1182 1183 type Subpath is Data: 1184 Subpath{points: List<&2, Point>, closed: Bool} 1185 1186 def 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 1194 def 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 1209 def 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 1216 def 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 1223 def 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 1228 def 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. 1235 type 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). 1240 def edge.kind(+stroked: Bool, +closed: Bool) -> U32: 1241 U.choose(U32, stroked, U.choose(U32, closed, 2, 1), 0) 1242 1243 def 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 1255 def subpaths.finish(st: Sub) -> List<&2, Subpath>: 1256 Sub{points, paths} = st 1257 subpath.finish(points, False{}, paths) 1258 1259 @unsafe 1260 def 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 1265 def 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 1270 type Segment is Data: 1271 Segment{px: F32, py: F32, qx: F32, qy: F32, ux: F32, uy: F32, length: F32, distance: F32} 1272 1273 def 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 1279 def 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 1287 def 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. 1294 type DashPattern is Data: 1295 DashPattern{values: List<&2, F32>, period: F32, offset: F32} 1296 1297 type DashCursor is Data: 1298 DashCursor{remaining: F32, rest: List<&2, F32>, painted: Bool} 1299 1300 type DashRun is Data: 1301 DashRun{start: F32, end: F32} 1302 1303 def 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 1308 def 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 1313 def 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 1318 def 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 1323 def 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 1330 def 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 1335 def dash.values(s: String, +ref: F32) -> List<&2, F32>: 1336 dash.value(U.number(s), ref, t => dash.values(t, ref)) 1337 1338 def 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 1342 def 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 1347 def 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. 1352 def dash.some(pattern: DashPattern) -> Bool: 1353 DashPattern{values, period, offset} = pattern 1354 F32.is_gt(period, 0.0000001) 1355 1356 def 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 1365 def 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 1371 def 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 1378 def 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 1383 def 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 1389 def 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 1395 def 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 1401 def segment.dir(s: Segment) -> Point: 1402 Segment{px, py, qx, qy, ux, uy, len, distance} = s 1403 Point{ux, uy} 1404 1405 def segment.start(s: Segment) -> Point: 1406 Segment{px, py, qx, qy, ux, uy, len, distance} = s 1407 Point{px, py} 1408 1409 def 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. 1426 type Joins is Data: 1427 Joins{cap: U32, join: U32, limit: F32} 1428 1429 def 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 1432 def 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 1435 def joins.of(settings: StrokeSettings) -> Joins: 1436 StrokeSettings{cap, join, limit, dash, offset, vector} = settings 1437 Joins{cap.code(cap), join.code(join), limit} 1438 1439 def 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. 1443 def 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 1448 def 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. 1453 def 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). 1460 def 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. 1466 def 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. 1473 def 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 1483 def 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 1490 def 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 1496 def 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. 1501 def 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 1506 def 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. 1512 type Prev is Data: 1513 Prev{dir: Point, end: Point, len: F32} 1514 1515 def 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. 1524 def 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 1531 def 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). 1538 def 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 1546 def 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 1550 def 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. 1557 def 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). 1568 def 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. 1580 def 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 1589 def 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. 1597 def 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 1604 def 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. 1611 def joins.cap(+s: Joins) -> U32: 1612 Joins{cap, join, limit} = s 1613 cap 1614 1615 def 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. 1629 def 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 1634 def 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 1637 def 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. 1641 def 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 1648 def 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 1651 def 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. 1655 def 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 1662 def 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. 1667 def 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 1671 def 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 1676 def 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 1685 def 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 1693 def 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`. 1699 def 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. 1707 def 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 1710 def 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. 1717 def 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 1722 def 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 1727 def 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 1732 def dash.drop.first(xs: List<&2, DashRun>) -> List<&2, DashRun>: 1733 match xs: 1734 case Nil{}: Nil{} 1735 case Con{h, t}: t 1736 1737 def 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 1743 def 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 1749 def 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 1752 def 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. 1759 def 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 1765 def 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. 1775 def 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 1780 def 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 1785 def dash.any(xs: List<&2, DashRun>) -> Bool: 1786 match xs: 1787 case Nil{}: False{} 1788 case Con{h, t}: True{} 1789 1790 def 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. 1797 def 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 1803 def 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 1808 def 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 1813 def 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 1820 def 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. 1824 def 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 1827 def bounds.empty() -> Bounds: 1828 Bounds{1000000000.0, 1000000000.0, F32.neg(1000000000.0), F32.neg(1000000000.0)} 1829 1830 def 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. 1837 type EdgeTree is Data: 1838 EdgeFlat{box: Bounds, edges: List<&2, Edge>} 1839 1840 def edge.tree.bounds(t: EdgeTree) -> Bounds: 1841 match t: 1842 case EdgeFlat{box, edges}: box 1843 1844 def edge.tree(+es: List<&2, Edge>) -> EdgeTree: 1845 EdgeFlat{bounds(es, bounds.empty()), es} 1846 1847 def edge.tree.empty() -> EdgeTree: 1848 EdgeFlat{bounds.empty(), Nil{}} 1849 1850 def 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 1856 def 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 1861 def 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 1865 def 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. 1874 type Fill is Data: 1875 Fill{box: Bounds, cover: Cover} 1876 1877 def fill.new(es: List<&2, Edge>) -> Fill: 1878 Fill{bounds(es, bounds.empty()), cover.none()} 1879 1880 def fill.bounds(f: Fill) -> Bounds: 1881 Fill{box, cover} = f 1882 box 1883 1884 def 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 1889 def 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. 1897 type StrokeMesh is Data: 1898 StrokeMesh{tree: EdgeTree, inverse: Matrix, cover: Cover} 1899 1900 def stroke.mesh(+edges: List<&2, Edge>, inverse: Matrix) -> StrokeMesh: 1901 StrokeMesh{edge.tree(edges), inverse, cover.none()} 1902 1903 def 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 1907 def 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 1912 type MarkerStyle is Data: 1913 MarkerStyle{start: String, middle: String, end: String} 1914 1915 def 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. 1921 def 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 1930 def 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 1935 def 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. 1945 def 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. 1954 type 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 1958 def 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 1961 def 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 1972 def 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. 1979 type Prop is Data: 1980 Prop{key: String, initial: String, inherited: Bool} 1981 1982 def 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 1991 def 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. 1997 def 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}: Lines 1-2000 of 5871. More: https://jend.leeeet.dev/read.txt?path=gralha-svg-render@0.1.0.0/svg.bend&start_line=2001