canvas-painter.mjs
722 lines · kit/canvas-painter.mjs at 1482e84
1const CP_Q = 64;
2const CP_SVG = "http://www.w3.org/2000/svg";
4function cpScratch() {
5 const c = document.createElement("canvas");
6 c.width = c.height = 1;
7 return c.getContext("2d");
8}
10function cpRgba(css, probe) {
11 if (!css || css === "none" || css.startsWith("url(")) return null;
12 probe.fillStyle = "#000";
13 probe.fillStyle = css;
14 const out = probe.fillStyle;
15 if (out[0] === "#") return [parseInt(out.slice(1, 3), 16), parseInt(out.slice(3, 5), 16), parseInt(out.slice(5, 7), 16), 1];
16 const nums = out.match(/-?[\d.]+(e-?\d+)?/g);
17 if (!nums) return null;
18 if (out.startsWith("color(")) {
19 const v = nums.map(Number);
20 return [v[0] * 255, v[1] * 255, v[2] * 255, v.length > 3 ? v[3] : 1];
21 }
22 const v = nums.map(Number);
23 return [v[0], v[1], v[2], v.length > 3 ? v[3] : 1];
24}
26function cpCss(rgba, alpha = 1) {
27 const a = rgba[3] * alpha;
28 const r = Math.round(rgba[0]);
29 const g = Math.round(rgba[1]);
30 const b = Math.round(rgba[2]);
31 return a >= 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${Math.round(a * 1000) / 1000})`;
32}
34function cpMatrixOf(text) {
35 if (!text) return null;
36 const nums = text.match(/-?[\d.]+(e-?\d+)?/g);
37 if (!nums) return null;
38 const v = nums.map(Number);
39 if (text.startsWith("translate")) return [1, 0, 0, 1, v[0], v[1] ?? 0];
40 if (text.startsWith("matrix")) return v;
41 const m = new DOMMatrix(text);
42 return [m.a, m.b, m.c, m.d, m.e, m.f];
43}
45function cpMul(A, B) {
46 if (!A) return B;
47 if (!B) return A;
48 return [A[0] * B[0] + A[2] * B[1], A[1] * B[0] + A[3] * B[1], A[0] * B[2] + A[2] * B[3], A[1] * B[2] + A[3] * B[3], A[0] * B[4] + A[2] * B[5] + A[4], A[1] * B[4] + A[3] * B[5] + A[5]];
49}
51function cpStaticMatrix(el) {
52 const list = el.transform && el.transform.baseVal;
53 if (!list || !list.numberOfItems) return null;
54 const m = list.consolidate().matrix;
55 return [m.a, m.b, m.c, m.d, m.e, m.f];
56}
58function cpGeometry(el) {
59 const tag = el.localName;
60 const num = (name) => parseFloat(el.getAttribute(name) ?? "0") || 0;
61 if (tag === "path") return el.getAttribute("d") ? new Path2D(el.getAttribute("d")) : null;
62 const p = new Path2D();
63 if (tag === "circle") p.arc(num("cx"), num("cy"), num("r"), 0, Math.PI * 2);
64 else if (tag === "ellipse") p.ellipse(num("cx"), num("cy"), num("rx"), num("ry"), 0, 0, Math.PI * 2);
65 else if (tag === "rect") {
66 const rx = num("rx") || num("ry");
67 if (rx && p.roundRect) p.roundRect(num("x"), num("y"), num("width"), num("height"), rx);
68 else p.rect(num("x"), num("y"), num("width"), num("height"));
69 } else if (tag === "line") {
70 p.moveTo(num("x1"), num("y1"));
71 p.lineTo(num("x2"), num("y2"));
72 } else if (tag === "polyline" || tag === "polygon") {
73 const v = (el.getAttribute("points") ?? "").trim().split(/[\s,]+/).map(Number);
74 for (let i = 0; i + 1 < v.length; i += 2) i ? p.lineTo(v[i], v[i + 1]) : p.moveTo(v[i], v[i + 1]);
75 if (tag === "polygon") p.closePath();
76 } else return null;
77 return p;
78}
80function cpStyle(el, probe) {
81 const cs = getComputedStyle(el);
82 if (cs.display === "none" || cs.visibility === "hidden") return null;
83 const fill = cpRgba(cs.fill, probe);
84 const stroke = cpRgba(cs.stroke, probe);
85 const fillOpacity = parseFloat(cs.fillOpacity);
86 const strokeOpacity = parseFloat(cs.strokeOpacity);
87 const dash = cs.strokeDasharray && cs.strokeDasharray !== "none" ? cs.strokeDasharray.split(/[\s,]+/).map(parseFloat).filter((v) => !Number.isNaN(v)) : null;
88 return {
89 cs,
90 fill: fill ? cpCss(fill, isNaN(fillOpacity) ? 1 : fillOpacity) : null,
91 fillRaw: fill ? cpCss(fill) : null,
92 stroke: stroke ? cpCss(stroke, isNaN(strokeOpacity) ? 1 : strokeOpacity) : null,
93 width: parseFloat(cs.strokeWidth) || 0,
94 cap: cs.strokeLinecap || "butt",
95 join: cs.strokeLinejoin || "miter",
96 miter: parseFloat(cs.strokeMiterlimit) || 4,
97 dash,
98 dashOffset: parseFloat(cs.strokeDashoffset) || 0,
99 rule: cs.fillRule === "evenodd" ? "evenodd" : "nonzero",
100 opacity: parseFloat(cs.opacity),
101 nonScaling: cs.vectorEffect === "non-scaling-stroke",
102 };
103}
105function cpCompilePart(node, run, probe, unsupported, vary) {
106 const ops = [];
107 const roleOf = (el) => {
108 const name = el === node ? "move" : el.getAttribute("data-r");
109 if (name === null || run.role[name] === undefined) return -1;
110 return run.role[name];
111 };
112 const varyOf = (el) => {
113 if (!vary || !el.classList) return null;
114 for (const cls of el.classList) if (vary[cls]) return vary[cls];
115 return null;
116 };
117 const walk = (el, chain, alphaRoles, alpha, varies = []) => {
118 const varied = varyOf(el);
119 const ownVaries = varied ? varies.concat([varied]) : varies;
120 const r = roleOf(el);
121 const type = r >= 0 ? run.roles[r].type : null;
122 const tag = el.localName;
123 let nextChain = chain;
124 if (type === "self" || type === "plane" || type === "xform") nextChain = chain.concat([{ r }]);
125 else {
126 const m = el === node ? null : cpStaticMatrix(el);
127 if (m) nextChain = chain.concat([{ m }]);
128 }
129 if (tag === "g" || tag === "svg") {
130 let groupAlpha = alpha;
131 let groupRoles = alphaRoles;
132 if (type === "plane") groupRoles = alphaRoles.concat([r]);
133 else if (varied) {
134 } else if (el !== node) {
135 const op = parseFloat(getComputedStyle(el).opacity);
136 if (!Number.isNaN(op)) groupAlpha *= op;
137 }
138 for (const child of el.children) walk(child, nextChain, groupRoles, groupAlpha, ownVaries);
139 return;
140 }
141 if (tag === "title" || tag === "desc") return;
142 const style = cpStyle(el, probe);
143 if (!style) return;
144 let ownAlpha = alpha;
145 let ownRoles = alphaRoles;
146 if (type === "slot") ownRoles = alphaRoles.concat([r]);
147 else if (!varied && !Number.isNaN(style.opacity)) ownAlpha *= style.opacity;
148 const op = { tag, r, type, chain: nextChain, alpha: ownAlpha, alphaRoles: ownRoles, varies: ownVaries, style, path: null, size: 0, last: null };
149 if (type === "face") {
150 const cs = style.cs;
151 op.ramp = [0, 1, 2, 3, 4].map((i) => cpRgba(cs.getPropertyValue(`--tn-${i}`).trim(), probe) ?? [0, 0, 0, 1]);
152 op.rampKey = op.ramp.map((c) => c.join(",")).join("|");
153 op.baseWidth = style.width;
154 } else if (type === "dot") {
155 op.size = parseFloat(el.getAttribute("r")) || 0.5;
156 } else if (type === "path" || type === "slot") {
157 } else if (tag === "text") {
158 const cs = style.cs;
159 op.text = el.textContent;
160 op.x = parseFloat(el.getAttribute("x") ?? "0") || 0;
161 op.y = parseFloat(el.getAttribute("y") ?? "0") || 0;
162 op.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
163 op.align = cs.textAnchor === "middle" ? "center" : cs.textAnchor === "end" ? "right" : "left";
164 op.spacing = cs.letterSpacing === "normal" ? "0px" : cs.letterSpacing;
165 op.baseline = cs.dominantBaseline === "central" || cs.dominantBaseline === "middle" ? "middle" : cs.dominantBaseline === "hanging" ? "hanging" : "alphabetic";
166 } else {
167 op.path = cpGeometry(el);
168 if (!op.path) {
169 unsupported.add(tag);
170 return;
171 }
172 }
173 ops.push(op);
174 };
175 walk(node, [], [], 1);
176 return ops;
177}
179function cpTrace(ctx, d, arena) {
180 ctx.beginPath();
181 for (let i = 0; i + 4 < d.length; i += 5) {
182 const at = 2 * (d.charCodeAt(i + 1) | (d.charCodeAt(i + 2) << 16));
183 const count = d.charCodeAt(i + 3);
184 ctx.moveTo(arena[at], arena[at + 1]);
185 for (let k = 1; k < count; k++) ctx.lineTo(arena[at + 2 * k], arena[at + 2 * k + 1]);
186 if (d.charCodeAt(i + 4)) ctx.closePath();
187 }
188}
190function cpMount(stage, controller, { keepSvg = false, vary = null, only = null, effects = null, below = null, mask = null } = {}) {
191 const probe = cpScratch();
192 const scene = () => controller.scene();
193 const unsupported = new Set();
194 const layers = [];
195 const programs = [];
196 const wanted = only ?? controller.canvasLayers ?? null;
197 const svgs = [...stage.querySelectorAll("svg[data-live]")].filter((svg) => !wanted || wanted.includes(svg.dataset.live));
198 const first = svgs[0];
199 const vb = first.viewBox.baseVal;
200 const W = vb.width;
201 const H = vb.height;
202 const runs = scene().runs;
203 const t0 = performance.now();
204 for (const svg of svgs) {
205 const name = svg.dataset.live;
206 const canvas = document.createElement("canvas");
207 canvas.className = "iso-layer iso-canvas";
208 canvas.dataset.canvasLive = name;
209 canvas.setAttribute("aria-hidden", "true");
210 svg.before(canvas);
211 const ctx = canvas.getContext("2d", { alpha: true, desynchronized: false });
212 const container = svg.querySelector(".iso-parts") ?? svg;
213 for (const node of container.querySelectorAll(":scope > [data-p]")) {
214 const index = Number(node.dataset.p);
215 const effect = effects && effects[runs[index].name];
216 programs[index] = effect === false ? [] : effect ? [{ effect }] : cpCompilePart(node, runs[index], probe, unsupported, vary);
217 }
218 layers.push({ name, canvas, ctx, svg });
219 }
220 const compileMs = performance.now() - t0;
221 const effectRuns = effects ? runs.filter((run) => typeof effects[run.name] === "function" && effects[run.name].always !== false && programs[run.index]).map((run) => run.index) : [];
222 const view = { zoom: 1, cx: W / 2, cy: H / 2 };
223 let cssPerUnit = 1;
224 let dpr = window.devicePixelRatio || 1;
225 let base = [1, 0, 0, 1, 0, 0];
226 const resize = () => {
227 forceFull = true;
228 const box = stage.getBoundingClientRect();
229 dpr = window.devicePixelRatio || 1;
230 const cssW = box.width;
231 const cssH = box.height;
232 const snapX = box.left - Math.round(box.left);
233 const snapY = box.top - Math.round(box.top);
234 const boxW = Math.ceil(cssW + snapX - 1e-6);
235 const boxH = Math.ceil(cssH + snapY - 1e-6);
236 const w = Math.max(1, Math.round(boxW * dpr));
237 const h = Math.max(1, Math.round(boxH * dpr));
238 for (const layer of layers) {
239 if (layer.canvas.width !== w) layer.canvas.width = w;
240 if (layer.canvas.height !== h) layer.canvas.height = h;
241 layer.canvas.style.width = `${boxW}px`;
242 layer.canvas.style.height = `${boxH}px`;
243 layer.canvas.style.left = `${-snapX}px`;
244 layer.canvas.style.top = `${-snapY}px`;
245 }
246 const fit = Math.min(cssW / W, cssH / H);
247 const offX = (cssW - W * fit) / 2;
248 const offY = (cssH - H * fit) / 2;
249 const s = fit * view.zoom;
250 cssPerUnit = s;
251 const tx = offX + (W / 2) * fit - view.cx * s;
252 const ty = offY + (H / 2) * fit - view.cy * s;
253 base = [s * dpr, 0, 0, s * dpr, (tx + snapX) * dpr, (ty + snapY) * dpr];
254 const unit = Math.round(Math.pow(2, Math.round(Math.log2(s) * 4) / 4) * 100) / 100;
255 if (controller.setUnit) controller.setUnit(unit);
256 };
257 const faceCache = new Map();
258 const faceColour = (op, q) => {
259 let table = faceCache.get(op.rampKey);
260 if (!table) {
261 table = new Array(4 * CP_Q + 1);
262 faceCache.set(op.rampKey, table);
263 }
264 let css = table[q];
265 if (css === undefined) {
266 const band = Math.min(3, Math.floor(q / CP_Q));
267 const f = (q - CP_Q * band) / CP_Q;
268 const a = op.ramp[band];
269 const b = op.ramp[band + 1];
270 css = cpCss([a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f, a[3] + (b[3] - a[3]) * f]);
271 table[q] = css;
272 }
273 return css;
274 };
275 const stats = { ms: 0, ops: 0, paths: 0, parsed: 0, touched: 0, rects: 0, full: 0 };
276 const lastBox = [];
277 let painted = false;
278 const PAD = 3;
279 const rectsOf = (frames, emitted) => {
280 const list = [];
281 const pad = PAD / cssPerUnit;
282 for (const index of emitted) {
283 const b = frames[index].box;
284 const old = lastBox[index];
285 let x0 = b[0];
286 let y0 = b[1];
287 let x1 = b[2];
288 let y1 = b[3];
289 if (old) {
290 x0 = Math.min(x0, old[0]);
291 y0 = Math.min(y0, old[1]);
292 x1 = Math.max(x1, old[2]);
293 y1 = Math.max(y1, old[3]);
294 }
295 if (!(x1 >= x0)) continue;
296 list.push([x0 - pad, y0 - pad, x1 + pad, y1 + pad]);
297 }
298 let merged = true;
299 while (merged) {
300 merged = false;
301 for (let i = 0; i < list.length && !merged; i++)
302 for (let j = i + 1; j < list.length; j++) {
303 const a = list[i];
304 const c = list[j];
305 if (a[0] <= c[2] && c[0] <= a[2] && a[1] <= c[3] && c[1] <= a[3]) {
306 list[i] = [Math.min(a[0], c[0]), Math.min(a[1], c[1]), Math.max(a[2], c[2]), Math.max(a[3], c[3])];
307 list.splice(j, 1);
308 merged = true;
309 break;
310 }
311 }
312 }
313 return list.map(([x0, y0, x1, y1]) => {
314 const dx0 = Math.floor(base[0] * x0 + base[4]);
315 const dy0 = Math.floor(base[3] * y0 + base[5]);
316 const dx1 = Math.ceil(base[0] * x1 + base[4]);
317 const dy1 = Math.ceil(base[3] * y1 + base[5]);
318 return { dx0, dy0, dx1, dy1, x0: (dx0 - base[4]) / base[0] - pad, y0: (dy0 - base[5]) / base[3] - pad, x1: (dx1 - base[4]) / base[0] + pad, y1: (dy1 - base[5]) / base[3] + pad };
319 });
320 };
321 const keepBoxes = (frames, list) => {
322 for (const index of list) {
323 const b = frames[index].box;
324 const slot = lastBox[index] ?? (lastBox[index] = new Float64Array(4));
325 slot[0] = b[0];
326 slot[1] = b[1];
327 slot[2] = b[2];
328 slot[3] = b[3];
329 }
330 };
331 const pathOf = (op, d) => {
332 if (op.last === d) return op.path;
333 op.last = d;
334 op.path = d ? new Path2D(d) : null;
335 stats.parsed++;
336 return op.path;
337 };
338 const linkMatrix = (link, f) => {
339 if (link.m) return link.m;
340 const t = f.t[link.r];
341 if (t === "\u0002") return f.tm[link.r];
342 return cpMatrixOf(t);
343 };
344 const chainMatrix = (chain, f) => {
345 if (chain.length === 1) return linkMatrix(chain[0], f);
346 let M = null;
347 for (let i = 0; i < chain.length; i++) {
348 const m = linkMatrix(chain[i], f);
349 if (m) M = cpMul(M, m);
350 }
351 return M;
352 };
353 const tableOf = (op) => {
354 let table = faceCache.get(op.rampKey);
355 if (!table) {
356 table = new Array(4 * CP_Q + 1);
357 for (let q = 0; q <= 4 * CP_Q; q++) {
358 const band = Math.min(3, Math.floor(q / CP_Q));
359 const f = (q - CP_Q * band) / CP_Q;
360 const a = op.ramp[band];
361 const b = op.ramp[band + 1];
362 table[q] = cpCss([a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f, a[3] + (b[3] - a[3]) * f]);
363 }
364 faceCache.set(op.rampKey, table);
365 }
366 return table;
367 };
368 const flat = (op) => ({
369 kind: op.effect ? 4 : op.tag === "text" ? 3 : op.type === "dot" ? 2 : op.type === "path" || op.type === "slot" || op.type === "face" ? 1 : 0,
370 face: op.type === "face",
371 r: op.r ?? -1,
372 chain: op.chain && op.chain.length ? op.chain : null,
373 alpha: op.alpha ?? 1,
374 roles: op.alphaRoles && op.alphaRoles.length ? Int32Array.from(op.alphaRoles) : null,
375 varies: op.varies && op.varies.length ? op.varies : null,
376 fill: op.style ? (op.type === "dot" ? op.style.fillRaw : op.style.fill) : null,
377 stroke: op.style ? op.style.stroke : null,
378 width: op.style ? op.style.width : 0,
379 cap: op.style ? op.style.cap : "butt",
380 join: op.style ? op.style.join : "miter",
381 miter: op.style ? op.style.miter : 4,
382 dash: op.style && op.style.dash ? op.style.dash : null,
383 dashOffset: op.style ? op.style.dashOffset : 0,
384 evenodd: Boolean(op.style && op.style.rule === "evenodd"),
385 nonScaling: Boolean(op.style && op.style.nonScaling),
386 table: op.type === "face" ? tableOf(op) : null,
387 size: op.size ?? 0,
388 path: op.path ?? null,
389 last: null,
390 text: op.text ?? "",
391 x: op.x ?? 0,
392 y: op.y ?? 0,
393 font: op.font ?? "",
394 align: op.align ?? "left",
395 spacing: op.spacing ?? "0px",
396 baseline: op.baseline ?? "alphabetic",
397 effect: op.effect ?? null,
398 });
399 for (let i = 0; i < programs.length; i++) if (programs[i]) programs[i] = programs[i].map(flat);
400 const paintLayer = (layer, frames, rect = null) => {
401 const ctx = layer.ctx;
402 ctx.setTransform(1, 0, 0, 1, 0, 0);
403 if (rect) {
404 ctx.save();
405 ctx.beginPath();
406 ctx.rect(rect.dx0, rect.dy0, rect.dx1 - rect.dx0, rect.dy1 - rect.dy0);
407 ctx.clip();
408 ctx.clearRect(rect.dx0, rect.dy0, rect.dx1 - rect.dx0, rect.dy1 - rect.dy0);
409 } else ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
410 const b0 = base[0];
411 const b3 = base[3];
412 const b4 = base[4];
413 const b5 = base[5];
414 ctx.setTransform(b0, 0, 0, b3, b4, b5);
415 const order = controller.orderIndex(layer.name);
416 if (!order) {
417 if (rect) ctx.restore();
418 return;
419 }
420 const unitLine = 1 / cssPerUnit;
421 const seen = { x0: -b4 / b0 - 4, y0: -b5 / b3 - 4, x1: (layer.canvas.width - b4) / b0 + 4, y1: (layer.canvas.height - b5) / b3 + 4 };
422 let curFill = null;
423 let curStroke = null;
424 let curWidth = -1;
425 let curCap = null;
426 let curJoin = null;
427 let curMiter = -1;
428 let curAlpha = 1;
429 let curTransformed = false;
430 let curBx = b4;
431 let curBy = b5;
432 let dashed = false;
433 let opCount = 0;
434 ctx.globalAlpha = 1;
435 const drawRun = (index) => {
436 const ops = programs[index];
437 if (ops === undefined) return;
438 const f = frames[index];
439 const fb = f.box;
440 if (fb[0] > seen.x1 || fb[2] < seen.x0 || fb[1] > seen.y1 || fb[3] < seen.y0) return;
441 if (rect !== null) {
442 if (fb[0] > rect.x1 || fb[2] < rect.x0 || fb[1] > rect.y1 || fb[3] < rect.y0) return;
443 stats.touched++;
444 }
445 const bx = b4 + b0 * (f.sx || 0);
446 const by = b5 + b3 * (f.sy || 0);
447 if (bx !== curBx || by !== curBy) {
448 curBx = bx;
449 curBy = by;
450 curTransformed = true;
451 }
452 for (let o = 0; o < ops.length; o++) {
453 const op = ops[o];
454 const kind = op.kind;
455 if (kind === 4) {
456 ctx.setTransform(1, 0, 0, 1, 0, 0);
457 ctx.globalAlpha = 1;
458 curAlpha = 1;
459 op.effect(ctx, layer.canvas.width, layer.canvas.height, base, rect);
460 ctx.setTransform(b0, 0, 0, b3, bx, by);
461 curTransformed = false;
462 curFill = null;
463 curStroke = null;
464 curWidth = -1;
465 curCap = null;
466 curJoin = null;
467 curMiter = -1;
468 continue;
469 }
470 let alpha = op.alpha;
471 const roles = op.roles;
472 if (roles !== null) for (let k = 0; k < roles.length; k++) alpha *= f.o[roles[k]];
473 const varies = op.varies;
474 if (varies !== null) for (let k = 0; k < varies.length; k++) alpha *= varies[k]();
475 if (alpha <= 0.001) continue;
476 let path = null;
477 let token = null;
478 let fill = op.fill;
479 let stroke = op.stroke;
480 let width = op.width;
481 if (kind === 1) {
482 const d = f.d[op.r];
483 if (d === "" || d === undefined) continue;
484 if (d.charCodeAt(0) === 1) token = d;
485 else {
486 path = pathOf(op, d);
487 if (path === null) continue;
488 }
489 if (op.face) {
490 fill = stroke = op.table[f.q[op.r]];
491 const w = f.w[op.r];
492 if (w >= 0) width = w;
493 }
494 } else if (kind === 2) {
495 alpha *= f.o[op.r];
496 if (alpha <= 0.001) continue;
497 } else path = op.path;
498 opCount++;
499 if (alpha !== curAlpha) {
500 ctx.globalAlpha = alpha;
501 curAlpha = alpha;
502 }
503 const M = op.chain !== null ? chainMatrix(op.chain, f) : null;
504 if (kind === 3) {
505 const T = cpMul(base, M);
506 ctx.setTransform(T[0], T[1], T[2], T[3], T[4], T[5]);
507 curTransformed = true;
508 ctx.font = op.font;
509 ctx.textAlign = op.align;
510 ctx.textBaseline = op.baseline;
511 if ("letterSpacing" in ctx) ctx.letterSpacing = op.spacing;
512 if (fill) {
513 ctx.fillStyle = fill;
514 curFill = fill;
515 ctx.fillText(op.text, op.x, op.y);
516 }
517 continue;
518 }
519 let drawPath = path;
520 const plain = M === null || (M[0] === 1 && M[1] === 0 && M[2] === 0 && M[3] === 1);
521 if (kind === 2) {
522 if (M === null) {
523 if (curTransformed) {
524 ctx.setTransform(b0, 0, 0, b3, bx, by);
525 curTransformed = false;
526 }
527 } else {
528 ctx.setTransform(b0 * M[0], b3 * M[1], b0 * M[2], b3 * M[3], b0 * M[4] + bx, b3 * M[5] + by);
529 curTransformed = true;
530 }
531 ctx.beginPath();
532 ctx.arc(f.cx[op.r], f.cy[op.r], op.size, 0, 6.283185307179586);
533 drawPath = null;
534 } else if (M === null) {
535 if (curTransformed) {
536 ctx.setTransform(b0, 0, 0, b3, bx, by);
537 curTransformed = false;
538 }
539 if (token !== null) cpTrace(ctx, token, f.arena);
540 } else if (plain || !(stroke && op.nonScaling)) {
541 ctx.setTransform(b0 * M[0], b3 * M[1], b0 * M[2], b3 * M[3], b0 * M[4] + bx, b3 * M[5] + by);
542 curTransformed = true;
543 if (token !== null) cpTrace(ctx, token, f.arena);
544 } else {
545 if (curTransformed) {
546 ctx.setTransform(b0, 0, 0, b3, bx, by);
547 curTransformed = false;
548 }
549 drawPath = new Path2D();
550 drawPath.addPath(path, new DOMMatrix(M));
551 }
552 if (fill !== null) {
553 if (fill !== curFill) {
554 ctx.fillStyle = fill;
555 curFill = fill;
556 }
557 if (drawPath !== null) op.evenodd ? ctx.fill(drawPath, "evenodd") : ctx.fill(drawPath);
558 else op.evenodd ? ctx.fill("evenodd") : ctx.fill();
559 }
560 if (stroke !== null && width > 0) {
561 if (stroke !== curStroke) {
562 ctx.strokeStyle = stroke;
563 curStroke = stroke;
564 }
565 const scaleOf = M === null || plain ? 1 : Math.sqrt(Math.abs(M[0] * M[3] - M[1] * M[2]));
566 const lw = op.nonScaling ? (width * unitLine) / (drawPath === path ? scaleOf : 1) : width;
567 if (lw !== curWidth) {
568 ctx.lineWidth = lw;
569 curWidth = lw;
570 }
571 if (op.cap !== curCap) {
572 ctx.lineCap = op.cap;
573 curCap = op.cap;
574 }
575 if (op.join !== curJoin) {
576 ctx.lineJoin = op.join;
577 curJoin = op.join;
578 }
579 if (op.miter !== curMiter) {
580 ctx.miterLimit = op.miter;
581 curMiter = op.miter;
582 }
583 if (op.dash !== null || dashed) {
584 ctx.setLineDash(op.dash !== null ? op.dash.map((v) => v * (op.nonScaling ? unitLine : 1)) : []);
585 ctx.lineDashOffset = op.dashOffset;
586 dashed = op.dash !== null;
587 }
588 if (drawPath !== null) ctx.stroke(drawPath);
589 else ctx.stroke();
590 }
591 }
592 };
593 if (below) {
594 for (let at = 0; at < order.length; at++) if (below(order[at])) drawRun(order[at]);
595 if (mask) {
596 mask(ctx, rect);
597 ctx.setTransform(b0, 0, 0, b3, b4, b5);
598 curTransformed = false;
599 curBx = b4;
600 curBy = b5;
601 curFill = null;
602 curStroke = null;
603 curWidth = -1;
604 curAlpha = -1;
605 ctx.globalAlpha = 1;
606 }
607 for (let at = 0; at < order.length; at++) if (!below(order[at])) drawRun(order[at]);
608 } else for (let at = 0; at < order.length; at++) drawRun(order[at]);
609 stats.ops += opCount;
610 stats.paths += opCount;
611 if (dashed) ctx.setLineDash([]);
612 if (rect) ctx.restore();
613 };
614 let numeric = Boolean(controller.numeric);
615 let incremental = false;
616 let forceFull = true;
617 const paint = () => {
618 const t = performance.now();
619 stats.ops = 0;
620 stats.paths = 0;
621 stats.parsed = 0;
622 stats.touched = 0;
623 stats.rects = 0;
624 stats.full = 0;
625 const frames = controller.frames();
626 const emitted = controller.emitted ? controller.emitted() : null;
627 const all = runs.length;
628 const full = !incremental || forceFull || !painted || !emitted || emitted.length > all * 0.25;
629 if (full) {
630 for (const layer of layers) paintLayer(layer, frames);
631 stats.full = 1;
632 keepBoxes(frames, runs.map((run) => run.index));
633 } else if (emitted.length || effectRuns.length) {
634 const dirty = effectRuns.length ? emitted.concat(effectRuns) : emitted;
635 const rects = rectsOf(frames, dirty);
636 stats.rects = rects.length;
637 for (const rect of rects) for (const layer of layers) paintLayer(layer, frames, rect);
638 keepBoxes(frames, dirty);
639 }
640 painted = true;
641 forceFull = false;
642 stats.ms = performance.now() - t;
643 return stats;
644 };
645 resize();
646 for (const layer of layers) {
647 if (keepSvg) layer.svg.style.visibility = "hidden";
648 else layer.svg.remove();
649 }
650 let watching = null;
651 if (typeof ResizeObserver === "function") {
652 watching = new ResizeObserver(() => {
653 resize();
654 paint();
655 });
656 watching.observe(stage);
657 }
658 const dprQuery = () => {
659 const mq = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
660 mq.addEventListener("change", () => {
661 resize();
662 paint();
663 dprQuery();
664 }, { once: true });
665 };
666 dprQuery();
667 stage.setAttribute("data-painter", "canvas");
668 return {
669 paint,
670 base: () => base.slice(),
671 stats: () => ({ ...stats }),
672 compileMs,
673 exportSvg({ gl = true, background = true } = {}) {
674 controller.snapshot();
675 const out = [];
676 for (const child of stage.children) {
677 const tag = child.localName;
678 if (tag === "svg") {
679 if (child.style.visibility === "hidden") continue;
680 out.push(`<g class="${child.getAttribute("class") || ""}">${child.innerHTML}</g>`);
681 } else if (tag === "canvas") {
682 const layer = layers.find((l) => l.canvas === child);
683 if (layer) out.push(`<g class="${layer.svg.getAttribute("class") || ""}">${layer.svg.innerHTML}</g>`);
684 else if (gl && child.width > 1 && getComputedStyle(child).display !== "none") out.push(`<image href="${child.toDataURL("image/png")}" x="0" y="0" width="${W}" height="${H}" preserveAspectRatio="none"/>`);
685 }
686 }
687 const css = [...document.querySelectorAll("style")].map((s) => s.textContent).join("\n");
688 const holder = stage.closest("[data-theme]");
689 const theme = holder ? holder.getAttribute("data-theme") : "dark";
690 const card = getComputedStyle(stage.closest(".iso-plate") || stage).getPropertyValue("--anatomy-card").trim() || "#fff";
691 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}"><style>${css.replace(/<\/style/gi, "")}</style><g class="iso" data-theme="${theme}"><g class="iso-plate" style="padding:0;box-shadow:none">${background ? `<rect width="${W}" height="${H}" fill="${card}"/>` : ""}<g class="${stage.getAttribute("class") || ""}">${out.join("")}</g></g></g></svg>`;
692 },
693 set incremental(on) {
694 incremental = Boolean(on);
695 forceFull = true;
696 },
697 get incremental() {
698 return incremental;
699 },
700 flush() {
701 for (const layer of layers) layer.ctx.getImageData(0, 0, 1, 1);
702 },
703 get numeric() {
704 return numeric;
705 },
706 unsupported: [...unsupported],
707 layers: layers.map((l) => l.name),
708 ops: programs.reduce((sum, ops) => sum + (ops ? ops.length : 0), 0),
709 zoom(z, cx = W / 2, cy = H / 2) {
710 view.zoom = z;
711 view.cx = cx;
712 view.cy = cy;
713 resize();
714 return paint();
715 },
716 destroy() {
717 if (watching) watching.disconnect();
718 },
719 };
720}
722export const CANVAS2D = { mount: cpMount };