@idle-screens/schema 3.12.1 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FORMAT.md +95 -8
- package/dist/index.d.ts +209 -10
- package/dist/index.js +2069 -422
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/saver-spec.schema.json +105 -9
package/dist/index.js
CHANGED
|
@@ -255,6 +255,35 @@ function applyDeltasToSpec(spec, deltas) {
|
|
|
255
255
|
}
|
|
256
256
|
return copy;
|
|
257
257
|
}
|
|
258
|
+
function applyDeltasShared(spec, deltas) {
|
|
259
|
+
const out = { ...spec };
|
|
260
|
+
const copied = new WeakSet([out]);
|
|
261
|
+
for (const d of deltas) {
|
|
262
|
+
const cp = canonicalSpecPath(out, d.path);
|
|
263
|
+
if (!cp) continue;
|
|
264
|
+
const parts = cp.split(".");
|
|
265
|
+
let node = out;
|
|
266
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
267
|
+
const k = parts[i];
|
|
268
|
+
const child = node[k];
|
|
269
|
+
if (!child || typeof child !== "object") {
|
|
270
|
+
node = null;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
if (!copied.has(child)) {
|
|
274
|
+
const clone = Array.isArray(child) ? child.slice() : { ...child };
|
|
275
|
+
copied.add(clone);
|
|
276
|
+
node[k] = clone;
|
|
277
|
+
}
|
|
278
|
+
node = node[k];
|
|
279
|
+
}
|
|
280
|
+
if (node) {
|
|
281
|
+
const last = parts[parts.length - 1];
|
|
282
|
+
node[Array.isArray(node) ? Number(last) : last] = d.value;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
258
287
|
var HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
259
288
|
function hexToRgb(hex) {
|
|
260
289
|
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
@@ -290,6 +319,10 @@ function lerpValue(from, to, k, specRoot = false, rootKey) {
|
|
|
290
319
|
if (typeof a === "string" && typeof b === "string" && HEX.test(a) && HEX.test(b)) {
|
|
291
320
|
return lerpHex(a, b, kk);
|
|
292
321
|
}
|
|
322
|
+
if (key === "path" && typeof a === "string" && typeof b === "string" && a !== b) {
|
|
323
|
+
const m = lerpPathD(a, b, kk);
|
|
324
|
+
if (m !== null) return m;
|
|
325
|
+
}
|
|
293
326
|
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
294
327
|
return b.map((bv, i) => walk(a[i], bv, i));
|
|
295
328
|
}
|
|
@@ -306,6 +339,33 @@ function lerpValue(from, to, k, specRoot = false, rootKey) {
|
|
|
306
339
|
};
|
|
307
340
|
return walk(from, to, rootKey, specRoot);
|
|
308
341
|
}
|
|
342
|
+
var PATH_TOKEN = /[a-zA-Z]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g;
|
|
343
|
+
function lerpPathD(a, b, k) {
|
|
344
|
+
const ta = a.match(PATH_TOKEN) ?? [];
|
|
345
|
+
const tb = b.match(PATH_TOKEN) ?? [];
|
|
346
|
+
if (ta.length !== tb.length) return null;
|
|
347
|
+
const out = [];
|
|
348
|
+
let cmd = "";
|
|
349
|
+
let argIndex = 0;
|
|
350
|
+
for (let i = 0; i < ta.length; i++) {
|
|
351
|
+
const x = ta[i], y = tb[i];
|
|
352
|
+
const isCmdA = /^[a-zA-Z]$/.test(x), isCmdB = /^[a-zA-Z]$/.test(y);
|
|
353
|
+
if (isCmdA !== isCmdB) return null;
|
|
354
|
+
if (isCmdA) {
|
|
355
|
+
if (x !== y) return null;
|
|
356
|
+
cmd = x.toUpperCase();
|
|
357
|
+
argIndex = 0;
|
|
358
|
+
out.push(x);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const va = Number(x), vb = Number(y);
|
|
362
|
+
const flag = cmd === "A" && (argIndex % 7 === 3 || argIndex % 7 === 4);
|
|
363
|
+
argIndex++;
|
|
364
|
+
const v = va + (vb - va) * k;
|
|
365
|
+
out.push(flag ? String(Math.round(v)) : String(+v.toFixed(5)));
|
|
366
|
+
}
|
|
367
|
+
return out.join(" ");
|
|
368
|
+
}
|
|
309
369
|
function textStringsOf(sprite) {
|
|
310
370
|
if (sprite.kind === "text") return sprite.strings;
|
|
311
371
|
if (sprite.kind === "textBlock") return [sprite.text];
|
|
@@ -362,7 +422,7 @@ function steerablePaths(spec) {
|
|
|
362
422
|
if (!spec || typeof spec !== "object") return [];
|
|
363
423
|
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer", "group", "origin"]);
|
|
364
424
|
const SKIP_ROOT = /* @__PURE__ */ new Set(["timeline"]);
|
|
365
|
-
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops", "bands"]);
|
|
425
|
+
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops", "bands", "parts"]);
|
|
366
426
|
const layers = spec.layers;
|
|
367
427
|
const shadowedRootKeys = new Set(
|
|
368
428
|
Array.isArray(layers) ? layers.map((l) => l && typeof l === "object" ? l.key : void 0).filter((k) => typeof k === "string" && STEERABLE_ROOT_KEYS.has(k)) : []
|
|
@@ -610,7 +670,7 @@ function evalTrack(keys, start, tt, leaf) {
|
|
|
610
670
|
}
|
|
611
671
|
return at(tt);
|
|
612
672
|
}
|
|
613
|
-
function resolveTimelineAt(spec, t) {
|
|
673
|
+
function resolveTimelineAt(spec, t, shared = false) {
|
|
614
674
|
const tl = spec.timeline;
|
|
615
675
|
if (!tl) return spec;
|
|
616
676
|
const base = withoutTimeline(spec);
|
|
@@ -622,7 +682,7 @@ function resolveTimelineAt(spec, t) {
|
|
|
622
682
|
const start = tl.loop ? keys[keys.length - 1].value : readSpecPath(base, path);
|
|
623
683
|
deltas.push({ t: 0, path, value: evalTrack(keys, start, tt, path.slice(path.lastIndexOf(".") + 1)) });
|
|
624
684
|
}
|
|
625
|
-
return applyDeltasToSpec(base, deltas);
|
|
685
|
+
return shared ? applyDeltasShared(base, deltas) : applyDeltasToSpec(base, deltas);
|
|
626
686
|
}
|
|
627
687
|
function nextKeyAfter(spec, path, t) {
|
|
628
688
|
const tl = spec.timeline;
|
|
@@ -674,6 +734,689 @@ function timelineSampleTimesAfter(spec, now, dense = false, only) {
|
|
|
674
734
|
return local.filter((s) => s > now);
|
|
675
735
|
}
|
|
676
736
|
|
|
737
|
+
// src/shapes.ts
|
|
738
|
+
function isShapedSprite(s) {
|
|
739
|
+
return s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect" || s.kind === "polygon" || s.kind === "stroke" || s.kind === "bar";
|
|
740
|
+
}
|
|
741
|
+
function barFraction(s, index) {
|
|
742
|
+
const n = s.values.length;
|
|
743
|
+
if (n === 0) return 0;
|
|
744
|
+
const v = s.values[(index % n + n) % n] ?? 0;
|
|
745
|
+
const max = s.max ?? s.values.reduce((m, x) => Math.max(m, x), 0);
|
|
746
|
+
if (!(max > 0)) return 0;
|
|
747
|
+
return Math.max(0, Math.min(1, v / max));
|
|
748
|
+
}
|
|
749
|
+
function barBox(direction, len, thick) {
|
|
750
|
+
switch (direction) {
|
|
751
|
+
case "left":
|
|
752
|
+
return { cx: -len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
753
|
+
case "up":
|
|
754
|
+
return { cx: 0, cy: -len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
755
|
+
case "down":
|
|
756
|
+
return { cx: 0, cy: len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
757
|
+
default:
|
|
758
|
+
return { cx: len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
var DEFAULT_POLYGON_SIDES = 6;
|
|
762
|
+
var STROKE_SAMPLES = 24;
|
|
763
|
+
function polygonPoints(s, radius) {
|
|
764
|
+
if (isPathPolygon(s)) {
|
|
765
|
+
const unit = flattenSprite(s);
|
|
766
|
+
if (unit.points.length >= 3) return unit.points.map(([x, y]) => ({ x: x * radius, y: y * radius }));
|
|
767
|
+
}
|
|
768
|
+
if (s.points && s.points.length >= 3) {
|
|
769
|
+
return s.points.map(([x, y]) => ({ x: x * radius, y: y * radius }));
|
|
770
|
+
}
|
|
771
|
+
const n = Math.max(3, Math.round(s.sides ?? DEFAULT_POLYGON_SIDES));
|
|
772
|
+
const out = [];
|
|
773
|
+
for (let k = 0; k < n; k++) {
|
|
774
|
+
const a = -Math.PI / 2 + 2 * Math.PI * k / n;
|
|
775
|
+
out.push({ x: Math.cos(a) * radius, y: Math.sin(a) * radius });
|
|
776
|
+
}
|
|
777
|
+
return out;
|
|
778
|
+
}
|
|
779
|
+
function polygonArea(pts) {
|
|
780
|
+
let a = 0;
|
|
781
|
+
for (let i = 0; i < pts.length; i++) {
|
|
782
|
+
const p = pts[i];
|
|
783
|
+
const q = pts[(i + 1) % pts.length];
|
|
784
|
+
a += p.x * q.y - q.x * p.y;
|
|
785
|
+
}
|
|
786
|
+
return Math.abs(a) / 2;
|
|
787
|
+
}
|
|
788
|
+
function polygonFill(s, radius) {
|
|
789
|
+
if (radius <= 0) return 0;
|
|
790
|
+
const disc = Math.PI * radius * radius;
|
|
791
|
+
return Math.min(1, polygonArea(polygonPoints(s, radius)) / disc);
|
|
792
|
+
}
|
|
793
|
+
function catmullRom(p0, p1, p2, p3, u) {
|
|
794
|
+
const u2 = u * u;
|
|
795
|
+
const u3 = u2 * u;
|
|
796
|
+
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
797
|
+
}
|
|
798
|
+
function strokeSamples(s, halfSize, n) {
|
|
799
|
+
const pts = s.points.map(([x, y]) => ({ x: x * halfSize, y: y * halfSize }));
|
|
800
|
+
const m = pts.length;
|
|
801
|
+
if (m < 2) return pts;
|
|
802
|
+
const smooth = s.curve !== "linear" && m >= 3;
|
|
803
|
+
const segs = m - 1;
|
|
804
|
+
n = n ?? Math.max(STROKE_SAMPLES, segs * 6 + 1);
|
|
805
|
+
const at = (k) => pts[Math.max(0, Math.min(m - 1, k))];
|
|
806
|
+
const out = [];
|
|
807
|
+
for (let i = 0; i < n; i++) {
|
|
808
|
+
const u = i / (n - 1) * segs;
|
|
809
|
+
const seg = Math.min(segs - 1, Math.floor(u));
|
|
810
|
+
const local = u - seg;
|
|
811
|
+
const p1 = at(seg);
|
|
812
|
+
const p2 = at(seg + 1);
|
|
813
|
+
if (!smooth) {
|
|
814
|
+
out.push({ x: p1.x + (p2.x - p1.x) * local, y: p1.y + (p2.y - p1.y) * local });
|
|
815
|
+
} else {
|
|
816
|
+
const p0 = at(seg - 1);
|
|
817
|
+
const p3 = at(seg + 2);
|
|
818
|
+
out.push({ x: catmullRom(p0.x, p1.x, p2.x, p3.x, local), y: catmullRom(p0.y, p1.y, p2.y, p3.y, local) });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
function pathLength(pts) {
|
|
824
|
+
let l = 0;
|
|
825
|
+
for (let i = 1; i < pts.length; i++) {
|
|
826
|
+
const dx = pts[i].x - pts[i - 1].x;
|
|
827
|
+
const dy = pts[i].y - pts[i - 1].y;
|
|
828
|
+
l += Math.sqrt(dx * dx + dy * dy);
|
|
829
|
+
}
|
|
830
|
+
return l;
|
|
831
|
+
}
|
|
832
|
+
function strokeTaper(u) {
|
|
833
|
+
return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
|
|
834
|
+
}
|
|
835
|
+
function strokeWidthPx(s, scale) {
|
|
836
|
+
return Math.max(0.5, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
837
|
+
}
|
|
838
|
+
function featherRamp(u, feather) {
|
|
839
|
+
const band = Math.max(0, Math.min(1, feather)) / 2;
|
|
840
|
+
const d = Math.min(u, 1 - u);
|
|
841
|
+
if (band <= 0) return d >= 0 ? 1 : 0;
|
|
842
|
+
return Math.max(0, Math.min(1, d / band));
|
|
843
|
+
}
|
|
844
|
+
var FEATHER_MASK_SIZE = 128;
|
|
845
|
+
function ribbonSides(pts, half) {
|
|
846
|
+
const n = pts.length;
|
|
847
|
+
const left = [];
|
|
848
|
+
const right = [];
|
|
849
|
+
if (n < 2) return { left, right };
|
|
850
|
+
const dir = (a, b) => {
|
|
851
|
+
const dx = b.x - a.x;
|
|
852
|
+
const dy = b.y - a.y;
|
|
853
|
+
const l = Math.hypot(dx, dy);
|
|
854
|
+
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
855
|
+
};
|
|
856
|
+
for (let i = 0; i < n; i++) {
|
|
857
|
+
const tIn = i > 0 ? dir(pts[i - 1], pts[i]) : dir(pts[0], pts[1]);
|
|
858
|
+
const tOut = i < n - 1 ? dir(pts[i], pts[i + 1]) : tIn;
|
|
859
|
+
let tx = tIn.x + tOut.x;
|
|
860
|
+
let ty = tIn.y + tOut.y;
|
|
861
|
+
const tl = Math.hypot(tx, ty);
|
|
862
|
+
if (tl < 1e-9) {
|
|
863
|
+
tx = tOut.x;
|
|
864
|
+
ty = tOut.y;
|
|
865
|
+
} else {
|
|
866
|
+
tx /= tl;
|
|
867
|
+
ty /= tl;
|
|
868
|
+
}
|
|
869
|
+
const hw = half[i] / Math.max(0.5, tx * tOut.x + ty * tOut.y);
|
|
870
|
+
const p = pts[i];
|
|
871
|
+
left.push({ x: p.x - ty * hw, y: p.y + tx * hw });
|
|
872
|
+
right.push({ x: p.x + ty * hw, y: p.y - tx * hw });
|
|
873
|
+
}
|
|
874
|
+
return { left, right };
|
|
875
|
+
}
|
|
876
|
+
function strokeOutline(pts, lw, taper = true) {
|
|
877
|
+
const n = pts.length;
|
|
878
|
+
if (n < 2) return [];
|
|
879
|
+
const half = (i) => Math.max(0.5, taper ? lw * strokeTaper(i / (n - 1)) : lw) / 2;
|
|
880
|
+
const dir = (a, b) => {
|
|
881
|
+
const dx = b.x - a.x;
|
|
882
|
+
const dy = b.y - a.y;
|
|
883
|
+
const l = Math.hypot(dx, dy);
|
|
884
|
+
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
885
|
+
};
|
|
886
|
+
const { left, right } = ribbonSides(pts, Array.from({ length: n }, (_, i) => half(i)));
|
|
887
|
+
const cap = (c, r, from) => {
|
|
888
|
+
const out = [];
|
|
889
|
+
const steps = 6;
|
|
890
|
+
for (let k = 1; k < steps; k++) {
|
|
891
|
+
const a = from - Math.PI * k / steps;
|
|
892
|
+
out.push({ x: c.x + Math.cos(a) * r, y: c.y + Math.sin(a) * r });
|
|
893
|
+
}
|
|
894
|
+
return out;
|
|
895
|
+
};
|
|
896
|
+
const endT = dir(pts[n - 2], pts[n - 1]);
|
|
897
|
+
const startT = dir(pts[0], pts[1]);
|
|
898
|
+
const endA = Math.atan2(endT.y, endT.x) + Math.PI / 2;
|
|
899
|
+
const startA = Math.atan2(startT.y, startT.x) - Math.PI / 2;
|
|
900
|
+
return [
|
|
901
|
+
...left,
|
|
902
|
+
...cap(pts[n - 1], half(n - 1), endA),
|
|
903
|
+
...right.reverse(),
|
|
904
|
+
...cap(pts[0], half(0), startA)
|
|
905
|
+
];
|
|
906
|
+
}
|
|
907
|
+
var MAX_PATH_POINTS = 4096;
|
|
908
|
+
var MAX_SCENE_PATH_POINTS = 16384;
|
|
909
|
+
var PATH_TOLERANCE = 5e-4;
|
|
910
|
+
var pathCache = { auto: /* @__PURE__ */ new Map(), nonzero: /* @__PURE__ */ new Map(), evenodd: /* @__PURE__ */ new Map() };
|
|
911
|
+
function flattenPath(d, rule = "auto") {
|
|
912
|
+
const cache = pathCache[rule] ?? pathCache.auto;
|
|
913
|
+
const hit = cache.get(d);
|
|
914
|
+
if (hit) return hit;
|
|
915
|
+
let out;
|
|
916
|
+
try {
|
|
917
|
+
const parsed = parseAndFlatten(d);
|
|
918
|
+
const rings = parsed.rings.map(simplifyRing).filter((r) => r.length >= 3);
|
|
919
|
+
const lines = parsed.lines.map((l) => ({ points: l.closed ? simplifyRing(l.points) : simplifyLine(l.points), closed: l.closed }));
|
|
920
|
+
out = { points: rule === "auto" ? joinRings(rings) : bridge(rings), subpaths: rings.length, lines };
|
|
921
|
+
} catch (e) {
|
|
922
|
+
out = { points: [], subpaths: 0, lines: [], error: e instanceof Error ? e.message : String(e) };
|
|
923
|
+
}
|
|
924
|
+
if (cache.size > 256) cache.clear();
|
|
925
|
+
cache.set(d, out);
|
|
926
|
+
return out;
|
|
927
|
+
}
|
|
928
|
+
var spriteFlat = /* @__PURE__ */ new WeakMap();
|
|
929
|
+
function flattenSprite(s) {
|
|
930
|
+
const d = s.parts ? s.parts.filter((p) => p.fill !== false).map((p) => p.path).join(" ") : s.path ?? "";
|
|
931
|
+
const rule = s.parts ? "auto" : s.fillRule ?? "auto";
|
|
932
|
+
const hit = spriteFlat.get(s);
|
|
933
|
+
if (hit && hit.d === d && hit.rule === rule) return hit.flat;
|
|
934
|
+
const flat = flattenPath(d, rule);
|
|
935
|
+
spriteFlat.set(s, { d, rule, flat });
|
|
936
|
+
return flat;
|
|
937
|
+
}
|
|
938
|
+
function polygonFillRule(s) {
|
|
939
|
+
return typeof s.path === "string" && s.fillRule === "evenodd" ? "evenodd" : "nonzero";
|
|
940
|
+
}
|
|
941
|
+
function isPathPolygon(s) {
|
|
942
|
+
return s.kind === "polygon" && (typeof s.path === "string" || Array.isArray(s.parts));
|
|
943
|
+
}
|
|
944
|
+
var spriteParts = /* @__PURE__ */ new WeakMap();
|
|
945
|
+
function pathParts(s) {
|
|
946
|
+
const key = s.parts ?? s.path;
|
|
947
|
+
const hit = spriteParts.get(s);
|
|
948
|
+
if (hit && hit.key === key) return hit.parts;
|
|
949
|
+
const src = s.parts ?? (typeof s.path === "string" ? [{ path: s.path, fillRule: s.fillRule, fill: s.fill, gradient: s.gradient, stroke: s.stroke }] : []);
|
|
950
|
+
const parts = src.map((p) => {
|
|
951
|
+
const flat = flattenPath(p.path, p.fillRule ?? "auto");
|
|
952
|
+
return {
|
|
953
|
+
flat,
|
|
954
|
+
color: s.parts ? p.color ?? null : null,
|
|
955
|
+
fill: p.fill !== false,
|
|
956
|
+
rule: p.fillRule === "evenodd" ? "evenodd" : "nonzero",
|
|
957
|
+
gradient: p.gradient,
|
|
958
|
+
stroke: p.stroke,
|
|
959
|
+
opacity: p.opacity ?? 1,
|
|
960
|
+
box: boxOfFlat(flat)
|
|
961
|
+
};
|
|
962
|
+
});
|
|
963
|
+
spriteParts.set(s, { key, parts });
|
|
964
|
+
return parts;
|
|
965
|
+
}
|
|
966
|
+
function partInkArea(part, sx, sy, r) {
|
|
967
|
+
if (part.opacity <= 0) return { fill: 0, stroke: 0 };
|
|
968
|
+
const fill = part.fill ? polygonArea(part.flat.points.map(([x, y]) => ({ x: x * sx, y: y * sy }))) * part.opacity : 0;
|
|
969
|
+
let stroke = 0;
|
|
970
|
+
if (part.stroke && part.stroke.width > 0) {
|
|
971
|
+
for (const l of part.flat.lines) {
|
|
972
|
+
const pts = l.points.map(([x, y]) => ({ x: x * sx, y: y * sy }));
|
|
973
|
+
if (l.closed && pts.length > 1) pts.push(pts[0]);
|
|
974
|
+
stroke += pathLength(pts) * part.stroke.width * r;
|
|
975
|
+
}
|
|
976
|
+
stroke *= (part.stroke.opacity ?? 1) * part.opacity;
|
|
977
|
+
}
|
|
978
|
+
return { fill, stroke };
|
|
979
|
+
}
|
|
980
|
+
function gradientStopWeights(stops) {
|
|
981
|
+
const at = stops.map((st) => Math.min(1, Math.max(0, st.at)));
|
|
982
|
+
const order = at.map((_, i) => i).sort((a, b) => at[a] - at[b]);
|
|
983
|
+
const out = new Array(stops.length).fill(0);
|
|
984
|
+
order.forEach((i, rank) => {
|
|
985
|
+
const prev = rank > 0 ? at[order[rank - 1]] : null;
|
|
986
|
+
const next = rank < order.length - 1 ? at[order[rank + 1]] : null;
|
|
987
|
+
const a = at[i];
|
|
988
|
+
out[i] = (prev === null ? a : (a - prev) / 2) + (next === null ? 1 - a : (next - a) / 2);
|
|
989
|
+
});
|
|
990
|
+
const sum = out.reduce((x, y) => x + y, 0);
|
|
991
|
+
return sum > 0 ? out.map((x) => x / sum) : out.map(() => 1 / Math.max(1, stops.length));
|
|
992
|
+
}
|
|
993
|
+
function pathInkArea(s, sx, sy, r) {
|
|
994
|
+
let area = 0;
|
|
995
|
+
for (const part of pathParts(s)) {
|
|
996
|
+
const a = partInkArea(part, sx, sy, r);
|
|
997
|
+
area += a.fill + a.stroke;
|
|
998
|
+
}
|
|
999
|
+
return area;
|
|
1000
|
+
}
|
|
1001
|
+
function pathFrame(s, r, w, h) {
|
|
1002
|
+
const span = s.span ?? "box";
|
|
1003
|
+
const sx = span === "width" || span === "screen" ? w / 2 : r;
|
|
1004
|
+
const sy = span === "height" || span === "screen" ? h / 2 : r;
|
|
1005
|
+
const [ox, oy] = s.origin ?? [0, 0];
|
|
1006
|
+
return { sx, sy, ox, oy, uniform: sx === sy };
|
|
1007
|
+
}
|
|
1008
|
+
var flatBoxes = /* @__PURE__ */ new WeakMap();
|
|
1009
|
+
function boxOfFlat(flat) {
|
|
1010
|
+
const hit = flatBoxes.get(flat);
|
|
1011
|
+
if (hit) return hit;
|
|
1012
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
1013
|
+
for (const [x, y] of flat.points.length ? flat.points : flat.lines.flatMap((l) => l.points)) {
|
|
1014
|
+
x0 = Math.min(x0, x);
|
|
1015
|
+
x1 = Math.max(x1, x);
|
|
1016
|
+
y0 = Math.min(y0, y);
|
|
1017
|
+
y1 = Math.max(y1, y);
|
|
1018
|
+
}
|
|
1019
|
+
if (!Number.isFinite(x0)) {
|
|
1020
|
+
x0 = -1;
|
|
1021
|
+
x1 = 1;
|
|
1022
|
+
y0 = -1;
|
|
1023
|
+
y1 = 1;
|
|
1024
|
+
}
|
|
1025
|
+
const box = { cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, hw: Math.max((x1 - x0) / 2, 1e-6), hh: Math.max((y1 - y0) / 2, 1e-6) };
|
|
1026
|
+
flatBoxes.set(flat, box);
|
|
1027
|
+
return box;
|
|
1028
|
+
}
|
|
1029
|
+
var pathIds = /* @__PURE__ */ new Map();
|
|
1030
|
+
var pathSeq = 0;
|
|
1031
|
+
var paintSigs = /* @__PURE__ */ new WeakMap();
|
|
1032
|
+
function pathPaintSig(s) {
|
|
1033
|
+
const hit = paintSigs.get(s);
|
|
1034
|
+
if (hit !== void 0) return hit;
|
|
1035
|
+
const sig = JSON.stringify(s, (k, v) => {
|
|
1036
|
+
if (k !== "path" || typeof v !== "string") return v;
|
|
1037
|
+
let id = pathIds.get(v);
|
|
1038
|
+
if (id === void 0) {
|
|
1039
|
+
if (pathIds.size > 4096) pathIds.clear();
|
|
1040
|
+
id = ++pathSeq;
|
|
1041
|
+
pathIds.set(v, id);
|
|
1042
|
+
}
|
|
1043
|
+
return `#${id}`;
|
|
1044
|
+
});
|
|
1045
|
+
paintSigs.set(s, sig);
|
|
1046
|
+
return sig;
|
|
1047
|
+
}
|
|
1048
|
+
function gradientSpace(part, u, v) {
|
|
1049
|
+
if (part.gradient?.units !== "part") return [u, v];
|
|
1050
|
+
return [(u - part.box.cx) / part.box.hw, (v - part.box.cy) / part.box.hh];
|
|
1051
|
+
}
|
|
1052
|
+
function segmentsFor(chord, curvature) {
|
|
1053
|
+
const n = Math.ceil(Math.sqrt(Math.max(curvature, chord * 0.05) / (4 * PATH_TOLERANCE)));
|
|
1054
|
+
return Math.max(2, Math.min(64, n));
|
|
1055
|
+
}
|
|
1056
|
+
function parseAndFlatten(d) {
|
|
1057
|
+
const tokens = d.match(/[a-zA-Z]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g) ?? [];
|
|
1058
|
+
let i = 0;
|
|
1059
|
+
const num = () => {
|
|
1060
|
+
const t = tokens[i++];
|
|
1061
|
+
if (t === void 0 || /^[a-zA-Z]$/.test(t)) throw new Error("expected a number");
|
|
1062
|
+
return Number(t);
|
|
1063
|
+
};
|
|
1064
|
+
const flag = () => {
|
|
1065
|
+
const t = tokens[i];
|
|
1066
|
+
if (t !== void 0 && /^[01]{2,}/.test(t)) {
|
|
1067
|
+
tokens[i] = t.slice(1);
|
|
1068
|
+
return Number(t[0]);
|
|
1069
|
+
}
|
|
1070
|
+
return num();
|
|
1071
|
+
};
|
|
1072
|
+
const rings = [];
|
|
1073
|
+
let ring = [];
|
|
1074
|
+
let cx = 0, cy = 0, sx = 0, sy = 0;
|
|
1075
|
+
let lastCtrl = null;
|
|
1076
|
+
let lastCmd = "";
|
|
1077
|
+
const push = (x, y) => {
|
|
1078
|
+
ring.push([x, y]);
|
|
1079
|
+
cx = x;
|
|
1080
|
+
cy = y;
|
|
1081
|
+
};
|
|
1082
|
+
const lines = [];
|
|
1083
|
+
const close = (closed = false) => {
|
|
1084
|
+
if (ring.length >= 2) lines.push({ points: ring.slice(), closed });
|
|
1085
|
+
if (ring.length >= 3) rings.push(ring);
|
|
1086
|
+
ring = [];
|
|
1087
|
+
};
|
|
1088
|
+
const cubic = (x1, y1, x2, y2, x, y) => {
|
|
1089
|
+
const dev = Math.hypot(x1 - cx, y1 - cy) + Math.hypot(x2 - x1, y2 - y1) + Math.hypot(x - x2, y - y2) - Math.hypot(x - cx, y - cy);
|
|
1090
|
+
const n = segmentsFor(Math.hypot(x - cx, y - cy), dev * 4);
|
|
1091
|
+
const x0 = cx, y0 = cy;
|
|
1092
|
+
for (let k = 1; k <= n; k++) {
|
|
1093
|
+
const t = k / n, u = 1 - t;
|
|
1094
|
+
push(
|
|
1095
|
+
u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x,
|
|
1096
|
+
u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
lastCtrl = [x2, y2];
|
|
1100
|
+
};
|
|
1101
|
+
const quad = (x1, y1, x, y) => {
|
|
1102
|
+
const dev = Math.hypot(x1 - cx, y1 - cy) + Math.hypot(x - x1, y - y1) - Math.hypot(x - cx, y - cy);
|
|
1103
|
+
const n = segmentsFor(Math.hypot(x - cx, y - cy), dev * 4);
|
|
1104
|
+
const x0 = cx, y0 = cy;
|
|
1105
|
+
for (let k = 1; k <= n; k++) {
|
|
1106
|
+
const t = k / n, u = 1 - t;
|
|
1107
|
+
push(u * u * x0 + 2 * u * t * x1 + t * t * x, u * u * y0 + 2 * u * t * y1 + t * t * y);
|
|
1108
|
+
}
|
|
1109
|
+
lastCtrl = [x1, y1];
|
|
1110
|
+
};
|
|
1111
|
+
const arc = (rx, ry, phiDeg, large, sweep, x, y) => {
|
|
1112
|
+
const x0 = cx, y0 = cy;
|
|
1113
|
+
if (rx === 0 || ry === 0 || x0 === x && y0 === y) {
|
|
1114
|
+
push(x, y);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
rx = Math.abs(rx);
|
|
1118
|
+
ry = Math.abs(ry);
|
|
1119
|
+
const phi = phiDeg * Math.PI / 180, cp = Math.cos(phi), sp = Math.sin(phi);
|
|
1120
|
+
const dx = (x0 - x) / 2, dy = (y0 - y) / 2;
|
|
1121
|
+
const x1p = cp * dx + sp * dy, y1p = -sp * dx + cp * dy;
|
|
1122
|
+
const lam = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
|
|
1123
|
+
if (lam > 1) {
|
|
1124
|
+
rx *= Math.sqrt(lam);
|
|
1125
|
+
ry *= Math.sqrt(lam);
|
|
1126
|
+
}
|
|
1127
|
+
const num2 = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
1128
|
+
const den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
1129
|
+
let co = Math.sqrt(Math.max(0, num2 / den));
|
|
1130
|
+
if (large === sweep) co = -co;
|
|
1131
|
+
const cxp = co * rx * y1p / ry, cyp = -co * ry * x1p / rx;
|
|
1132
|
+
const ccx = cp * cxp - sp * cyp + (x0 + x) / 2, ccy = sp * cxp + cp * cyp + (y0 + y) / 2;
|
|
1133
|
+
const ang = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
|
|
1134
|
+
const t1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
1135
|
+
let dt = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
1136
|
+
if (!sweep && dt > 0) dt -= 2 * Math.PI;
|
|
1137
|
+
if (sweep && dt < 0) dt += 2 * Math.PI;
|
|
1138
|
+
const r = Math.max(rx, ry);
|
|
1139
|
+
const n = Math.max(2, Math.min(96, Math.ceil(Math.abs(dt) / (2 * Math.acos(Math.max(0, 1 - PATH_TOLERANCE / r))))));
|
|
1140
|
+
for (let k = 1; k <= n; k++) {
|
|
1141
|
+
const t = t1 + dt * k / n;
|
|
1142
|
+
const ex = rx * Math.cos(t), ey = ry * Math.sin(t);
|
|
1143
|
+
push(cp * ex - sp * ey + ccx, sp * ex + cp * ey + ccy);
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
while (i < tokens.length) {
|
|
1147
|
+
let cmd = tokens[i];
|
|
1148
|
+
if (/^[a-zA-Z]$/.test(cmd)) i++;
|
|
1149
|
+
else if (lastCmd) cmd = lastCmd === "M" ? "L" : lastCmd === "m" ? "l" : lastCmd;
|
|
1150
|
+
else throw new Error("path must start with M");
|
|
1151
|
+
const rel = cmd === cmd.toLowerCase() && cmd !== "z" ? true : false;
|
|
1152
|
+
const ox = rel ? cx : 0, oy = rel ? cy : 0;
|
|
1153
|
+
const C = cmd.toUpperCase();
|
|
1154
|
+
const prevCmd = lastCmd.toUpperCase();
|
|
1155
|
+
if (C !== "S" && C !== "T") lastCtrl = null;
|
|
1156
|
+
switch (C) {
|
|
1157
|
+
case "M": {
|
|
1158
|
+
close();
|
|
1159
|
+
const x = num() + ox, y = num() + oy;
|
|
1160
|
+
push(x, y);
|
|
1161
|
+
sx = x;
|
|
1162
|
+
sy = y;
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
case "L":
|
|
1166
|
+
push(num() + ox, num() + oy);
|
|
1167
|
+
break;
|
|
1168
|
+
case "H":
|
|
1169
|
+
push(num() + ox, cy);
|
|
1170
|
+
break;
|
|
1171
|
+
case "V":
|
|
1172
|
+
push(cx, num() + oy);
|
|
1173
|
+
break;
|
|
1174
|
+
case "C": {
|
|
1175
|
+
const a = num() + ox, b = num() + oy, c = num() + ox, e = num() + oy;
|
|
1176
|
+
cubic(a, b, c, e, num() + ox, num() + oy);
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
case "S": {
|
|
1180
|
+
const r = lastCtrl && (prevCmd === "C" || prevCmd === "S") ? [2 * cx - lastCtrl[0], 2 * cy - lastCtrl[1]] : [cx, cy];
|
|
1181
|
+
const c = num() + ox, e = num() + oy;
|
|
1182
|
+
cubic(r[0], r[1], c, e, num() + ox, num() + oy);
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
case "Q": {
|
|
1186
|
+
const a = num() + ox, b = num() + oy;
|
|
1187
|
+
quad(a, b, num() + ox, num() + oy);
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
case "T": {
|
|
1191
|
+
const r = lastCtrl && (prevCmd === "Q" || prevCmd === "T") ? [2 * cx - lastCtrl[0], 2 * cy - lastCtrl[1]] : [cx, cy];
|
|
1192
|
+
quad(r[0], r[1], num() + ox, num() + oy);
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
case "A": {
|
|
1196
|
+
const rx = num(), ry = num(), ph = num(), la = flag(), sw = flag();
|
|
1197
|
+
arc(rx, ry, ph, la, sw, num() + ox, num() + oy);
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
case "Z":
|
|
1201
|
+
cx = sx;
|
|
1202
|
+
cy = sy;
|
|
1203
|
+
close(true);
|
|
1204
|
+
break;
|
|
1205
|
+
default:
|
|
1206
|
+
throw new Error(`unsupported path command '${cmd}'`);
|
|
1207
|
+
}
|
|
1208
|
+
lastCmd = cmd;
|
|
1209
|
+
}
|
|
1210
|
+
close();
|
|
1211
|
+
for (const r of rings) {
|
|
1212
|
+
const a = r[0], b = r[r.length - 1];
|
|
1213
|
+
if (Math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-9) r.pop();
|
|
1214
|
+
}
|
|
1215
|
+
for (const l of lines) {
|
|
1216
|
+
const a = l.points[0], b = l.points[l.points.length - 1];
|
|
1217
|
+
if (l.closed && l.points.length > 2 && Math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-9) l.points.pop();
|
|
1218
|
+
}
|
|
1219
|
+
return { rings, lines };
|
|
1220
|
+
}
|
|
1221
|
+
function simplifyLine(r) {
|
|
1222
|
+
if (r.length <= 2) return r;
|
|
1223
|
+
const keep = new Uint8Array(r.length);
|
|
1224
|
+
keep[0] = 1;
|
|
1225
|
+
keep[r.length - 1] = 1;
|
|
1226
|
+
const stack = [[0, r.length - 1]];
|
|
1227
|
+
while (stack.length) {
|
|
1228
|
+
const [a, b] = stack.pop();
|
|
1229
|
+
const A = r[a], B = r[b];
|
|
1230
|
+
const dx = B[0] - A[0], dy = B[1] - A[1], len = Math.hypot(dx, dy);
|
|
1231
|
+
let best = -1, bd = 0;
|
|
1232
|
+
for (let k = a + 1; k < b; k++) {
|
|
1233
|
+
const P = r[k];
|
|
1234
|
+
const d = len > 1e-12 ? Math.abs(dx * (A[1] - P[1]) - dy * (A[0] - P[0])) / len : Math.hypot(P[0] - A[0], P[1] - A[1]);
|
|
1235
|
+
if (d > bd) {
|
|
1236
|
+
bd = d;
|
|
1237
|
+
best = k;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (best >= 0 && bd > PATH_TOLERANCE) {
|
|
1241
|
+
keep[best] = 1;
|
|
1242
|
+
stack.push([a, best], [best, b]);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return r.filter((_, k) => keep[k] === 1);
|
|
1246
|
+
}
|
|
1247
|
+
function simplifyRing(r) {
|
|
1248
|
+
if (r.length <= 4) return r;
|
|
1249
|
+
let far = 0, fd = -1;
|
|
1250
|
+
for (let k = 1; k < r.length; k++) {
|
|
1251
|
+
const d = (r[k][0] - r[0][0]) ** 2 + (r[k][1] - r[0][1]) ** 2;
|
|
1252
|
+
if (d > fd) {
|
|
1253
|
+
fd = d;
|
|
1254
|
+
far = k;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const keep = new Uint8Array(r.length);
|
|
1258
|
+
keep[0] = 1;
|
|
1259
|
+
keep[far] = 1;
|
|
1260
|
+
const at = (k) => r[k % r.length];
|
|
1261
|
+
const stack = [[0, far], [far, r.length]];
|
|
1262
|
+
while (stack.length) {
|
|
1263
|
+
const [a, b] = stack.pop();
|
|
1264
|
+
const A = at(a), B = at(b);
|
|
1265
|
+
const dx = B[0] - A[0], dy = B[1] - A[1], len = Math.hypot(dx, dy);
|
|
1266
|
+
let best = -1, bd = 0;
|
|
1267
|
+
for (let k = a + 1; k < b; k++) {
|
|
1268
|
+
const P = at(k);
|
|
1269
|
+
const d = len > 1e-12 ? Math.abs(dx * (A[1] - P[1]) - dy * (A[0] - P[0])) / len : Math.hypot(P[0] - A[0], P[1] - A[1]);
|
|
1270
|
+
if (d > bd) {
|
|
1271
|
+
bd = d;
|
|
1272
|
+
best = k;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (best >= 0 && bd > PATH_TOLERANCE) {
|
|
1276
|
+
keep[best % r.length] = 1;
|
|
1277
|
+
stack.push([a, best], [best, b]);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return r.filter((_, k) => keep[k] === 1);
|
|
1281
|
+
}
|
|
1282
|
+
function signedArea(r) {
|
|
1283
|
+
let a = 0;
|
|
1284
|
+
for (let k = 0; k < r.length; k++) {
|
|
1285
|
+
const p = r[k], q = r[(k + 1) % r.length];
|
|
1286
|
+
a += p[0] * q[1] - q[0] * p[1];
|
|
1287
|
+
}
|
|
1288
|
+
return a / 2;
|
|
1289
|
+
}
|
|
1290
|
+
function inside(pt, r) {
|
|
1291
|
+
let c = false;
|
|
1292
|
+
for (let k = 0, j = r.length - 1; k < r.length; j = k++) {
|
|
1293
|
+
const a = r[k], b = r[j];
|
|
1294
|
+
if (a[1] > pt[1] !== b[1] > pt[1] && pt[0] < (b[0] - a[0]) * (pt[1] - a[1]) / (b[1] - a[1]) + a[0]) c = !c;
|
|
1295
|
+
}
|
|
1296
|
+
return c;
|
|
1297
|
+
}
|
|
1298
|
+
function containsRing(r, rb, sub, sb) {
|
|
1299
|
+
if (sb.x0 < rb.x0 || sb.x1 > rb.x1 || sb.y0 < rb.y0 || sb.y1 > rb.y1) return false;
|
|
1300
|
+
for (const p of sub) if (!inside(p, r)) return false;
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
function boxOf(r) {
|
|
1304
|
+
const b = { x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity };
|
|
1305
|
+
for (const [x, y] of r) {
|
|
1306
|
+
b.x0 = Math.min(b.x0, x);
|
|
1307
|
+
b.x1 = Math.max(b.x1, x);
|
|
1308
|
+
b.y0 = Math.min(b.y0, y);
|
|
1309
|
+
b.y1 = Math.max(b.y1, y);
|
|
1310
|
+
}
|
|
1311
|
+
return b;
|
|
1312
|
+
}
|
|
1313
|
+
function joinRings(rings) {
|
|
1314
|
+
if (rings.length === 0) return [];
|
|
1315
|
+
const boxes = rings.map(boxOf);
|
|
1316
|
+
const areas = rings.map((r) => Math.abs(signedArea(r)));
|
|
1317
|
+
const order = rings.map((_, i) => i).sort((p, q) => areas[q] - areas[p]);
|
|
1318
|
+
const mult = new Array(rings.length).fill(0);
|
|
1319
|
+
const net = new Array(rings.length).fill(0);
|
|
1320
|
+
const done = [];
|
|
1321
|
+
for (const n of order) {
|
|
1322
|
+
const containers = done.filter((m) => areas[m] > areas[n] && containsRing(rings[m], boxes[m], rings[n], boxes[n]));
|
|
1323
|
+
const around = containers.reduce((sum, m) => sum + mult[m], 0);
|
|
1324
|
+
let want = 1;
|
|
1325
|
+
if (containers.length) {
|
|
1326
|
+
const parent = containers.reduce((p, m) => areas[m] < areas[p] ? m : p);
|
|
1327
|
+
want = net[parent] !== 0 ? 0 : 1;
|
|
1328
|
+
}
|
|
1329
|
+
net[n] = want;
|
|
1330
|
+
mult[n] = want - around;
|
|
1331
|
+
done.push(n);
|
|
1332
|
+
}
|
|
1333
|
+
const traced = [];
|
|
1334
|
+
const trace = (r, m) => {
|
|
1335
|
+
const o = Math.sign(signedArea(r)) === Math.sign(m) ? r : r.slice().reverse();
|
|
1336
|
+
for (let k = 0; k < Math.abs(m); k++) traced.push(o);
|
|
1337
|
+
};
|
|
1338
|
+
for (let n = 0; n < rings.length; n++) {
|
|
1339
|
+
if (mult[n] !== 0) trace(rings[n], mult[n]);
|
|
1340
|
+
}
|
|
1341
|
+
const holes = rings.map((_, n) => n).filter((n) => net[n] === 0 && mult[n] !== 0 && isConvex(rings[n]));
|
|
1342
|
+
for (let m = 0; m < rings.length; m++) {
|
|
1343
|
+
if (mult[m] <= 0) continue;
|
|
1344
|
+
const bm = boxes[m];
|
|
1345
|
+
const crossing = holes.filter((n) => {
|
|
1346
|
+
const bn = boxes[n];
|
|
1347
|
+
if (n === m || bm.x1 <= bn.x0 || bm.x0 >= bn.x1 || bm.y1 <= bn.y0 || bm.y0 >= bn.y1) return false;
|
|
1348
|
+
return !containsRing(rings[m], bm, rings[n], bn) && !containsRing(rings[n], bn, rings[m], bm);
|
|
1349
|
+
});
|
|
1350
|
+
for (const n of crossing) {
|
|
1351
|
+
if (crossing.some((o) => o !== n && areas[o] > areas[n] && containsRing(rings[o], boxes[o], rings[n], boxes[n]))) continue;
|
|
1352
|
+
const piece = clipToConvex(rings[m], rings[n]);
|
|
1353
|
+
if (piece.length >= 3 && Math.abs(signedArea(piece)) > 1e-9) trace(piece, -mult[m]);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return bridge(traced);
|
|
1357
|
+
}
|
|
1358
|
+
function isConvex(r) {
|
|
1359
|
+
let sign = 0;
|
|
1360
|
+
for (let k = 0; k < r.length; k++) {
|
|
1361
|
+
const a = r[k], b = r[(k + 1) % r.length], c = r[(k + 2) % r.length];
|
|
1362
|
+
const z = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
|
|
1363
|
+
if (Math.abs(z) < 1e-12) continue;
|
|
1364
|
+
if (sign === 0) sign = Math.sign(z);
|
|
1365
|
+
else if (Math.sign(z) !== sign) return false;
|
|
1366
|
+
}
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
function clipToConvex(subject, clip) {
|
|
1370
|
+
const o = Math.sign(signedArea(clip)) || 1;
|
|
1371
|
+
let out = subject;
|
|
1372
|
+
for (let k = 0; k < clip.length && out.length; k++) {
|
|
1373
|
+
const a = clip[k], b = clip[(k + 1) % clip.length];
|
|
1374
|
+
const side = (p) => o * ((b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]));
|
|
1375
|
+
const cut = (p, q) => {
|
|
1376
|
+
const sp = side(p), sq = side(q), t = sp / (sp - sq);
|
|
1377
|
+
return [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
|
|
1378
|
+
};
|
|
1379
|
+
const next = [];
|
|
1380
|
+
for (let i = 0; i < out.length; i++) {
|
|
1381
|
+
const p = out[i], q = out[(i + 1) % out.length];
|
|
1382
|
+
const pin = side(p) >= 0, qin = side(q) >= 0;
|
|
1383
|
+
if (pin) next.push(p);
|
|
1384
|
+
if (pin !== qin) next.push(cut(p, q));
|
|
1385
|
+
}
|
|
1386
|
+
out = next;
|
|
1387
|
+
}
|
|
1388
|
+
return out;
|
|
1389
|
+
}
|
|
1390
|
+
function bridge(rings) {
|
|
1391
|
+
if (rings.length === 0) return [];
|
|
1392
|
+
let ring = rings[0].slice();
|
|
1393
|
+
for (let n = 1; n < rings.length; n++) {
|
|
1394
|
+
const sub = rings[n];
|
|
1395
|
+
let bi = 0, bj = 0, best = Infinity;
|
|
1396
|
+
for (let a = 0; a < ring.length; a++) {
|
|
1397
|
+
for (let b = 0; b < sub.length; b++) {
|
|
1398
|
+
const dd = (ring[a][0] - sub[b][0]) ** 2 + (ring[a][1] - sub[b][1]) ** 2;
|
|
1399
|
+
if (dd < best) {
|
|
1400
|
+
best = dd;
|
|
1401
|
+
bi = a;
|
|
1402
|
+
bj = b;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const rot = sub.slice(bj).concat(sub.slice(0, bj));
|
|
1407
|
+
ring = ring.slice(0, bi + 1).concat(rot, [rot[0], ring[bi]], ring.slice(bi + 1));
|
|
1408
|
+
}
|
|
1409
|
+
return ring;
|
|
1410
|
+
}
|
|
1411
|
+
function pathDrawCost(s) {
|
|
1412
|
+
let c = 0;
|
|
1413
|
+
for (const p of pathParts(s)) {
|
|
1414
|
+
if (p.fill) c += p.flat.points.length * (p.gradient ? 2 : 1);
|
|
1415
|
+
if (p.stroke) for (const l of p.flat.lines) c += l.points.length * 3;
|
|
1416
|
+
}
|
|
1417
|
+
return c;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
677
1420
|
// src/validate.ts
|
|
678
1421
|
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
679
1422
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -719,7 +1462,8 @@ var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "c
|
|
|
719
1462
|
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
720
1463
|
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "feather", "colors", "colorWeights"]);
|
|
721
1464
|
var KNOWN_BAR = /* @__PURE__ */ new Set(["kind", "values", "length", "thickness", "color", "max", "direction", "colors", "colorWeights"]);
|
|
722
|
-
var
|
|
1465
|
+
var KNOWN_PATH_PART = /* @__PURE__ */ new Set(["path", "color", "fillRule", "fill", "gradient", "stroke", "opacity"]);
|
|
1466
|
+
var KNOWN_POLYGON = /* @__PURE__ */ new Set(["kind", "radius", "color", "sides", "points", "path", "fillRule", "fill", "gradient", "stroke", "parts", "span", "origin", "soft", "colors", "colorWeights"]);
|
|
723
1467
|
var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "colors", "colorWeights"]);
|
|
724
1468
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
725
1469
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle", "role"]);
|
|
@@ -776,7 +1520,7 @@ function validateSpec(spec) {
|
|
|
776
1520
|
}
|
|
777
1521
|
function ignoredPropertyPaths(spec) {
|
|
778
1522
|
if (!isObj(spec)) return [];
|
|
779
|
-
return (validateSpecCore(spec).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
|
|
1523
|
+
return (validateSpecCore(spec, false).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
|
|
780
1524
|
}
|
|
781
1525
|
function validateSpecPaths(spec, paths) {
|
|
782
1526
|
const layers = /* @__PURE__ */ new Set();
|
|
@@ -814,7 +1558,7 @@ function validateSpecPaths(spec, paths) {
|
|
|
814
1558
|
}
|
|
815
1559
|
return ok;
|
|
816
1560
|
}
|
|
817
|
-
function validateSpecCore(spec) {
|
|
1561
|
+
function validateSpecCore(spec, checkApplied = true) {
|
|
818
1562
|
const errors = [];
|
|
819
1563
|
const warnings = [];
|
|
820
1564
|
const err = (path, message) => void errors.push({ path, message });
|
|
@@ -859,6 +1603,23 @@ function validateSpecCore(spec) {
|
|
|
859
1603
|
err("layers", "must be a non-empty array");
|
|
860
1604
|
} else {
|
|
861
1605
|
if (spec.layers.length > LIMITS.maxLayers) err("layers", `at most ${LIMITS.maxLayers} layers`);
|
|
1606
|
+
{
|
|
1607
|
+
let pathPoints = 0;
|
|
1608
|
+
for (const l of spec.layers) {
|
|
1609
|
+
const sp = isObj(l) ? l.sprite : void 0;
|
|
1610
|
+
if (!sp || sp.kind !== "polygon") continue;
|
|
1611
|
+
const pieces = Array.isArray(sp.parts) ? sp.parts : typeof sp.path === "string" ? [sp] : [];
|
|
1612
|
+
const n = typeof l.count === "number" && l.count > 0 ? l.count : 1;
|
|
1613
|
+
for (const pc of pieces) {
|
|
1614
|
+
if (!isObj(pc) || typeof pc.path !== "string") continue;
|
|
1615
|
+
const fl = flattenPath(pc.path, pc.fillRule ?? "auto");
|
|
1616
|
+
pathPoints += ((pc.fill === false ? 0 : fl.points.length) + (isObj(pc.stroke) ? fl.lines.reduce((m, x) => m + x.points.length, 0) : 0)) * n;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
if (pathPoints > MAX_SCENE_PATH_POINTS) {
|
|
1620
|
+
err("layers", `path outlines total ${pathPoints} points (flattened points \xD7 count); the scene budget is ${MAX_SCENE_PATH_POINTS}`);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
862
1623
|
let total = 0;
|
|
863
1624
|
spec.layers.forEach((layer, i) => {
|
|
864
1625
|
total += isObj(layer) && isNum(layer.count) ? layer.count : 0;
|
|
@@ -866,7 +1627,7 @@ function validateSpecCore(spec) {
|
|
|
866
1627
|
});
|
|
867
1628
|
if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
|
|
868
1629
|
}
|
|
869
|
-
if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn);
|
|
1630
|
+
if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn, checkApplied);
|
|
870
1631
|
return { valid: errors.length === 0, errors, warnings };
|
|
871
1632
|
}
|
|
872
1633
|
function validateTransform(tf, path, err, warn, spec, anchorOk) {
|
|
@@ -904,7 +1665,7 @@ function validateGroups(groups, err, warn, spec) {
|
|
|
904
1665
|
if (g.transform !== void 0) validateTransform(g.transform, `${p}.transform`, err, warn, spec, false);
|
|
905
1666
|
}
|
|
906
1667
|
}
|
|
907
|
-
function validateTimeline(spec, errOuter, warn) {
|
|
1668
|
+
function validateTimeline(spec, errOuter, warn, checkApplied = true) {
|
|
908
1669
|
let failed = false;
|
|
909
1670
|
const err = (p, m) => {
|
|
910
1671
|
failed = true;
|
|
@@ -958,20 +1719,22 @@ function validateTimeline(spec, errOuter, warn) {
|
|
|
958
1719
|
if (!ok || !cp) return;
|
|
959
1720
|
const t = key.t;
|
|
960
1721
|
latest = Math.max(latest, t);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1722
|
+
if (checkApplied) {
|
|
1723
|
+
const applied = applyDeltasShared(base, [{ t: 0, path: cp, value: key.value }]);
|
|
1724
|
+
const r = validateSpecPaths(applied, [cp]) ? { valid: true, errors: [] } : validateSpec(structuredClone(applied));
|
|
1725
|
+
if (!r.valid) {
|
|
1726
|
+
for (const e of r.errors) err(`${p}.value`, `applied at '${key.path}' \u2192 ${e.path || "<root>"}: ${e.message}`);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
if (isObj(key.value) && cp.endsWith("transform")) {
|
|
1730
|
+
const before = readSpecPath(base, cp);
|
|
1731
|
+
const o0 = isObj(before) ? before.origin ?? "viewport" : "viewport";
|
|
1732
|
+
const o1 = key.value.origin ?? "viewport";
|
|
1733
|
+
if (o0 !== o1) warn(`${p}.value`, "timeline-origin-steps", `this key changes the transform's origin (${String(o0)} \u2192 ${String(o1)}): the pivot switches on the key's first frame, so the layer jumps \u2014 keep one origin per layer`);
|
|
1734
|
+
}
|
|
1735
|
+
if (structuralSignature(applied) !== baseSig) {
|
|
1736
|
+
warn(`${p}.path`, "timeline-structural-key", `'${key.path}' is structural (placement, count, motion, size\u2026): the scene rebuilds at this key and entities re-seed \u2014 a pop, not a glide. Animate paint (colour, opacity, transform, polygon points) instead`);
|
|
1737
|
+
}
|
|
975
1738
|
}
|
|
976
1739
|
const list = times.get(cp) ?? [];
|
|
977
1740
|
list.push(t);
|
|
@@ -997,16 +1760,16 @@ function validateTimeline(spec, errOuter, warn) {
|
|
|
997
1760
|
if (wrapGap < minGap) err("timeline.keys", `keys on '${path}' at ${distinct[distinct.length - 1]} ms and ${distinct[0]} ms (next lap) are closer than ${minGap} ms across the loop wrap (flash safety)`);
|
|
998
1761
|
}
|
|
999
1762
|
}
|
|
1000
|
-
if (failed) return;
|
|
1763
|
+
if (failed || !checkApplied) return;
|
|
1001
1764
|
const keyed = [...timelineTracks(spec).keys()];
|
|
1002
1765
|
const seen = /* @__PURE__ */ new Set();
|
|
1003
1766
|
for (const t of timelineSampleTimes(spec, true, 120)) {
|
|
1004
|
-
const r = resolveTimelineAt(spec, t);
|
|
1767
|
+
const r = resolveTimelineAt(spec, t, true);
|
|
1005
1768
|
const state = JSON.stringify(keyed.map((p) => readSpecPath(r, p)));
|
|
1006
1769
|
if (seen.has(state)) continue;
|
|
1007
1770
|
seen.add(state);
|
|
1008
1771
|
if (validateSpecPaths(r, keyed)) continue;
|
|
1009
|
-
const e = validateSpec(r).errors[0];
|
|
1772
|
+
const e = validateSpec(structuredClone(r)).errors[0];
|
|
1010
1773
|
err("timeline", `at t = ${Math.round(t)} ms the keys combine into an invalid scene \u2014 ${e ? `${e.path || "<root>"}: ${e.message}` : "invalid"}`);
|
|
1011
1774
|
return;
|
|
1012
1775
|
}
|
|
@@ -1232,10 +1995,20 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
1232
1995
|
if (layer.links !== void 0) {
|
|
1233
1996
|
if (!isObj(layer.links)) err(`${path}.links`, "must be an object");
|
|
1234
1997
|
else {
|
|
1235
|
-
|
|
1236
|
-
|
|
1998
|
+
const chain = layer.links.mode === "chain";
|
|
1999
|
+
if (!chain || layer.links.k !== void 0) {
|
|
2000
|
+
if (!isNum(layer.links.k) || !Number.isInteger(layer.links.k) || layer.links.k < 1 || layer.links.k > LIMITS.maxLinksK) {
|
|
2001
|
+
err(`${path}.links.k`, `must be an integer 1..${LIMITS.maxLinksK}`);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (!chain || layer.links.maxDist !== void 0 || layer.links.falloff === true) {
|
|
2005
|
+
positiveScalar(
|
|
2006
|
+
layer.links.maxDist,
|
|
2007
|
+
`${path}.links.maxDist`,
|
|
2008
|
+
err,
|
|
2009
|
+
chain && layer.links.maxDist === void 0 ? "is required (> 0) when a chain sets falloff: true \u2014 it is the fade distance" : void 0
|
|
2010
|
+
);
|
|
1237
2011
|
}
|
|
1238
|
-
positiveScalar(layer.links.maxDist, `${path}.links.maxDist`, err);
|
|
1239
2012
|
if (layer.links.color !== void 0) color(layer.links.color, `${path}.links.color`, err);
|
|
1240
2013
|
if (layer.links.alpha !== void 0 && (!isNum(layer.links.alpha) || layer.links.alpha < 0 || layer.links.alpha > 1)) {
|
|
1241
2014
|
err(`${path}.links.alpha`, "must be 0..1");
|
|
@@ -1476,6 +2249,34 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
1476
2249
|
err(`${path}.sides`, `must be an integer ${LIMITS.minPolygonSides}..${LIMITS.maxPolygonSides}`);
|
|
1477
2250
|
}
|
|
1478
2251
|
if (sprite.points !== void 0) validateShapePoints(sprite.points, `${path}.points`, 3, err);
|
|
2252
|
+
if (sprite.parts !== void 0) {
|
|
2253
|
+
for (const k of ["path", "points", "sides", "gradient", "stroke", "fill", "fillRule"]) {
|
|
2254
|
+
if (sprite[k] !== void 0) err(`${path}.${k}`, `with parts, put ${k} on each part (or drop parts)`);
|
|
2255
|
+
}
|
|
2256
|
+
if (!Array.isArray(sprite.parts) || sprite.parts.length === 0 || sprite.parts.length > MAX_PATH_PARTS) {
|
|
2257
|
+
err(`${path}.parts`, `must be 1..${MAX_PATH_PARTS} parts, each { path, color?, gradient?, stroke?, fill?, fillRule?, opacity? }`);
|
|
2258
|
+
} else {
|
|
2259
|
+
sprite.parts.forEach((part, i) => {
|
|
2260
|
+
const pp = `${path}.parts[${i}]`;
|
|
2261
|
+
if (!isObj(part)) return err(pp, "must be { path, color?, gradient?, stroke?, fill?, fillRule?, opacity? }");
|
|
2262
|
+
validatePathPiece(part, pp, err, warn);
|
|
2263
|
+
if (part.color !== void 0) color(part.color, `${pp}.color`, err);
|
|
2264
|
+
if (part.opacity !== void 0 && (!isNum(part.opacity) || part.opacity < 0 || part.opacity > 1)) err(`${pp}.opacity`, "must be 0..1");
|
|
2265
|
+
for (const k of unknownKeys(part, KNOWN_PATH_PART)) warn(`${pp}.${k}`, "unknown-property", `unknown part property '${k}' \u2014 will be ignored`);
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
} else if (sprite.path !== void 0) {
|
|
2269
|
+
if (sprite.points !== void 0 || sprite.sides !== void 0) err(`${path}.path`, "use path, points or sides, not more than one");
|
|
2270
|
+
validatePathPiece(sprite, path, err, warn);
|
|
2271
|
+
} else {
|
|
2272
|
+
for (const k of ["gradient", "stroke", "fill"]) if (sprite[k] !== void 0) err(`${path}.${k}`, `${k} applies only to a polygon with a path (or parts)`);
|
|
2273
|
+
if (sprite.fillRule !== void 0) warn(`${path}.fillRule`, "unknown-property", "fillRule applies only to a path \u2014 ignored");
|
|
2274
|
+
}
|
|
2275
|
+
if (sprite.span !== void 0 || sprite.origin !== void 0) {
|
|
2276
|
+
if (sprite.path === void 0 && sprite.parts === void 0) err(`${path}.span`, "span and origin apply only to a polygon with a path (or parts)");
|
|
2277
|
+
if (sprite.span !== void 0 && !["box", "width", "height", "screen"].includes(sprite.span)) err(`${path}.span`, "must be 'box' | 'width' | 'height' | 'screen'");
|
|
2278
|
+
if (sprite.origin !== void 0 && !(Array.isArray(sprite.origin) && sprite.origin.length === 2 && sprite.origin.every((v) => isNum(v) && Math.abs(v) <= 1))) err(`${path}.origin`, "must be [x, y] in the unit box (-1..1): the point placed at the position");
|
|
2279
|
+
}
|
|
1479
2280
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
1480
2281
|
color(sprite.color, `${path}.color`, err);
|
|
1481
2282
|
validatePalette(sprite, path, err);
|
|
@@ -1732,6 +2533,70 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
1732
2533
|
warn(`${path}.${k}`, "unknown-property", `unknown motion property '${k}' \u2014 will be ignored`);
|
|
1733
2534
|
}
|
|
1734
2535
|
}
|
|
2536
|
+
var isPair = (v, lim = 2) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && Math.abs(v[0]) <= lim && Math.abs(v[1]) <= lim;
|
|
2537
|
+
var MAX_PATH_PARTS = 256;
|
|
2538
|
+
var pathVerdicts = /* @__PURE__ */ new Map();
|
|
2539
|
+
var pathVerdictBytes = 0;
|
|
2540
|
+
function pathGeometryProblem(d, rule, filled) {
|
|
2541
|
+
const key = `${rule}|${filled ? 1 : 0}|${d}`;
|
|
2542
|
+
const hit = pathVerdicts.get(key);
|
|
2543
|
+
if (hit !== void 0) return hit;
|
|
2544
|
+
const flat = flattenPath(d, rule);
|
|
2545
|
+
const inBox = ([x, y]) => Math.abs(x) <= 1.0001 && Math.abs(y) <= 1.0001;
|
|
2546
|
+
const problem = flat.error ? `not a usable path: ${flat.error}` : filled && flat.points.length < 3 ? "must enclose an area (at least 3 points), or set fill: false and a stroke for line art" : !filled && flat.lines.length === 0 ? "must draw at least one line (2+ points)" : flat.points.length > MAX_PATH_POINTS ? `flattens to ${flat.points.length} points; at most ${MAX_PATH_POINTS}` : !flat.points.every(inBox) || !flat.lines.every((l) => l.points.every(inBox)) ? "must stay inside the -1..1 unit box (curves included)" : null;
|
|
2547
|
+
if (pathVerdicts.size >= 1024 || pathVerdictBytes + key.length > 2e6) {
|
|
2548
|
+
pathVerdicts.clear();
|
|
2549
|
+
pathVerdictBytes = 0;
|
|
2550
|
+
}
|
|
2551
|
+
pathVerdicts.set(key, problem);
|
|
2552
|
+
pathVerdictBytes += key.length;
|
|
2553
|
+
return problem;
|
|
2554
|
+
}
|
|
2555
|
+
function validatePathPiece(piece, path, err, warn) {
|
|
2556
|
+
if (typeof piece.path !== "string" || piece.path.length === 0 || piece.path.length > 65536) {
|
|
2557
|
+
err(`${path}.path`, "must be an SVG path d string (1..65536 chars) in the -1..1 unit box");
|
|
2558
|
+
} else {
|
|
2559
|
+
const problem = pathGeometryProblem(piece.path, piece.fillRule ?? "auto", piece.fill !== false);
|
|
2560
|
+
if (problem) err(`${path}.path`, problem);
|
|
2561
|
+
}
|
|
2562
|
+
if (piece.fill !== void 0 && typeof piece.fill !== "boolean") err(`${path}.fill`, "must be a boolean");
|
|
2563
|
+
if (piece.fill === false && piece.stroke === void 0) err(`${path}.fill`, "fill: false draws nothing without a stroke");
|
|
2564
|
+
if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err);
|
|
2565
|
+
if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err);
|
|
2566
|
+
if (piece.fillRule !== void 0 && !["auto", "nonzero", "evenodd"].includes(piece.fillRule)) err(`${path}.fillRule`, "must be 'auto' | 'nonzero' | 'evenodd'");
|
|
2567
|
+
void warn;
|
|
2568
|
+
}
|
|
2569
|
+
function validatePathGradient(g, path, err) {
|
|
2570
|
+
if (!isObj(g)) return err(path, "must be { type: 'linear', from, to, stops } or { type: 'radial', center, radius, stops }");
|
|
2571
|
+
if (g.type === "linear") {
|
|
2572
|
+
if (!isPair(g.from)) err(`${path}.from`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
2573
|
+
if (!isPair(g.to)) err(`${path}.to`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
2574
|
+
} else if (g.type === "radial") {
|
|
2575
|
+
if (!isPair(g.center)) err(`${path}.center`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
2576
|
+
if (!isNum(g.radius) || g.radius <= 0 || g.radius > 4) err(`${path}.radius`, "must be > 0 and <= 4 (unit-box units)");
|
|
2577
|
+
if (g.focus !== void 0 && !isPair(g.focus)) err(`${path}.focus`, "must be [x, y] in unit-box coordinates (-2..2)");
|
|
2578
|
+
} else {
|
|
2579
|
+
return err(`${path}.type`, "must be 'linear' | 'radial'");
|
|
2580
|
+
}
|
|
2581
|
+
if (g.units !== void 0 && g.units !== "box" && g.units !== "part") err(`${path}.units`, "must be 'box' (the drawing's unit box, default) | 'part' (the part's own box, -1..1)");
|
|
2582
|
+
if (!Array.isArray(g.stops) || g.stops.length < 2 || g.stops.length > 8) return err(`${path}.stops`, "must be 2..8 stops");
|
|
2583
|
+
let last = -Infinity;
|
|
2584
|
+
g.stops.forEach((st, i) => {
|
|
2585
|
+
if (!isObj(st)) return err(`${path}.stops[${i}]`, "must be { at, color, opacity? }");
|
|
2586
|
+
if (!isNum(st.at) || st.at < 0 || st.at > 1 || st.at < last) err(`${path}.stops[${i}].at`, "must be 0..1, not decreasing");
|
|
2587
|
+
else last = st.at;
|
|
2588
|
+
color(st.color, `${path}.stops[${i}].color`, err);
|
|
2589
|
+
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.stops[${i}].opacity`, "must be 0..1");
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
function validatePathStroke(st, path, err) {
|
|
2593
|
+
if (!isObj(st)) return err(path, "must be { color, width, opacity?, join?, cap? }");
|
|
2594
|
+
color(st.color, `${path}.color`, err);
|
|
2595
|
+
if (!isNum(st.width) || st.width <= 0 || st.width > 0.5) err(`${path}.width`, "must be > 0 and <= 0.5 (unit-box units: 0.01 is 1% of the drawing's half-size)");
|
|
2596
|
+
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.opacity`, "must be 0..1");
|
|
2597
|
+
if (st.join !== void 0 && !["round", "miter", "bevel"].includes(st.join)) err(`${path}.join`, "must be 'round' | 'miter' | 'bevel'");
|
|
2598
|
+
if (st.cap !== void 0 && !["round", "butt", "square"].includes(st.cap)) err(`${path}.cap`, "must be 'round' | 'butt' | 'square'");
|
|
2599
|
+
}
|
|
1735
2600
|
function validateShapePoints(points, path, min, err) {
|
|
1736
2601
|
if (!Array.isArray(points) || points.length < min || points.length > LIMITS.maxShapePoints) {
|
|
1737
2602
|
return err(path, `must be ${min}..${LIMITS.maxShapePoints} [x, y] pairs in unit coordinates (-1..1)`);
|
|
@@ -1900,199 +2765,32 @@ function validateSequence(seq) {
|
|
|
1900
2765
|
code: "morph-nothing-morphable",
|
|
1901
2766
|
message: `segments ${i}\u2192${i + 1} differ only in values morph cannot interpolate (strings such as textBlock.text step on the first frame): the morph will look like a cut \u2014 fade text via colour or reveal.progress`
|
|
1902
2767
|
});
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
if (errors.length === 0 && seq.wrapMorph === true) {
|
|
1907
|
-
const sq = seq;
|
|
1908
|
-
const last = sq.segments.length - 1;
|
|
1909
|
-
const lastTr = sq.segments[last]?.transition;
|
|
1910
|
-
let why = null;
|
|
1911
|
-
if (sq.loop !== true) why = "the sequence does not loop";
|
|
1912
|
-
else if (last < 1) why = "there is only one segment";
|
|
1913
|
-
else if (lastTr?.type !== "morph") why = "the last segment declares no morph transition";
|
|
1914
|
-
else if (structuralSignature(sq.segments[last].scene) !== structuralSignature(sq.segments[0].scene)) why = `segment ${last} and segment 0 differ structurally`;
|
|
1915
|
-
else if (morphChainRoot(sq, last) !== 0) why = `the lap is not one morph chain (segment ${morphChainRoot(sq, last)} starts a new chain), so the wrap would jump seeds`;
|
|
1916
|
-
if (why || !canWrapMorph(sq)) {
|
|
1917
|
-
warnings.push({ path: "wrapMorph", code: "wrap-morph-inactive", message: `wrapMorph has no effect: ${why ?? "the wrap cannot morph"} \u2014 the wrap stays a cut` });
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
1921
|
-
}
|
|
1922
|
-
function assertValidSequence(seq) {
|
|
1923
|
-
const r = validateSequence(seq);
|
|
1924
|
-
if (!r.valid) {
|
|
1925
|
-
throw new Error(`invalid sequence:
|
|
1926
|
-
${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
1927
|
-
}
|
|
1928
|
-
return seq;
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
// src/shapes.ts
|
|
1932
|
-
function isShapedSprite(s) {
|
|
1933
|
-
return s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect" || s.kind === "polygon" || s.kind === "stroke" || s.kind === "bar";
|
|
1934
|
-
}
|
|
1935
|
-
function barFraction(s, index) {
|
|
1936
|
-
const n = s.values.length;
|
|
1937
|
-
if (n === 0) return 0;
|
|
1938
|
-
const v = s.values[(index % n + n) % n] ?? 0;
|
|
1939
|
-
const max = s.max ?? s.values.reduce((m, x) => Math.max(m, x), 0);
|
|
1940
|
-
if (!(max > 0)) return 0;
|
|
1941
|
-
return Math.max(0, Math.min(1, v / max));
|
|
1942
|
-
}
|
|
1943
|
-
function barBox(direction, len, thick) {
|
|
1944
|
-
switch (direction) {
|
|
1945
|
-
case "left":
|
|
1946
|
-
return { cx: -len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
1947
|
-
case "up":
|
|
1948
|
-
return { cx: 0, cy: -len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
1949
|
-
case "down":
|
|
1950
|
-
return { cx: 0, cy: len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
1951
|
-
default:
|
|
1952
|
-
return { cx: len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
var DEFAULT_POLYGON_SIDES = 6;
|
|
1956
|
-
var STROKE_SAMPLES = 24;
|
|
1957
|
-
function polygonPoints(s, radius) {
|
|
1958
|
-
if (s.points && s.points.length >= 3) {
|
|
1959
|
-
return s.points.map(([x, y]) => ({ x: x * radius, y: y * radius }));
|
|
1960
|
-
}
|
|
1961
|
-
const n = Math.max(3, Math.round(s.sides ?? DEFAULT_POLYGON_SIDES));
|
|
1962
|
-
const out = [];
|
|
1963
|
-
for (let k = 0; k < n; k++) {
|
|
1964
|
-
const a = -Math.PI / 2 + 2 * Math.PI * k / n;
|
|
1965
|
-
out.push({ x: Math.cos(a) * radius, y: Math.sin(a) * radius });
|
|
1966
|
-
}
|
|
1967
|
-
return out;
|
|
1968
|
-
}
|
|
1969
|
-
function polygonArea(pts) {
|
|
1970
|
-
let a = 0;
|
|
1971
|
-
for (let i = 0; i < pts.length; i++) {
|
|
1972
|
-
const p = pts[i];
|
|
1973
|
-
const q = pts[(i + 1) % pts.length];
|
|
1974
|
-
a += p.x * q.y - q.x * p.y;
|
|
1975
|
-
}
|
|
1976
|
-
return Math.abs(a) / 2;
|
|
1977
|
-
}
|
|
1978
|
-
function polygonFill(s, radius) {
|
|
1979
|
-
if (radius <= 0) return 0;
|
|
1980
|
-
const disc = Math.PI * radius * radius;
|
|
1981
|
-
return Math.min(1, polygonArea(polygonPoints(s, radius)) / disc);
|
|
1982
|
-
}
|
|
1983
|
-
function catmullRom(p0, p1, p2, p3, u) {
|
|
1984
|
-
const u2 = u * u;
|
|
1985
|
-
const u3 = u2 * u;
|
|
1986
|
-
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
1987
|
-
}
|
|
1988
|
-
function strokeSamples(s, halfSize, n) {
|
|
1989
|
-
const pts = s.points.map(([x, y]) => ({ x: x * halfSize, y: y * halfSize }));
|
|
1990
|
-
const m = pts.length;
|
|
1991
|
-
if (m < 2) return pts;
|
|
1992
|
-
const smooth = s.curve !== "linear" && m >= 3;
|
|
1993
|
-
const segs = m - 1;
|
|
1994
|
-
n = n ?? Math.max(STROKE_SAMPLES, segs * 6 + 1);
|
|
1995
|
-
const at = (k) => pts[Math.max(0, Math.min(m - 1, k))];
|
|
1996
|
-
const out = [];
|
|
1997
|
-
for (let i = 0; i < n; i++) {
|
|
1998
|
-
const u = i / (n - 1) * segs;
|
|
1999
|
-
const seg = Math.min(segs - 1, Math.floor(u));
|
|
2000
|
-
const local = u - seg;
|
|
2001
|
-
const p1 = at(seg);
|
|
2002
|
-
const p2 = at(seg + 1);
|
|
2003
|
-
if (!smooth) {
|
|
2004
|
-
out.push({ x: p1.x + (p2.x - p1.x) * local, y: p1.y + (p2.y - p1.y) * local });
|
|
2005
|
-
} else {
|
|
2006
|
-
const p0 = at(seg - 1);
|
|
2007
|
-
const p3 = at(seg + 2);
|
|
2008
|
-
out.push({ x: catmullRom(p0.x, p1.x, p2.x, p3.x, local), y: catmullRom(p0.y, p1.y, p2.y, p3.y, local) });
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
return out;
|
|
2012
|
-
}
|
|
2013
|
-
function pathLength(pts) {
|
|
2014
|
-
let l = 0;
|
|
2015
|
-
for (let i = 1; i < pts.length; i++) {
|
|
2016
|
-
const dx = pts[i].x - pts[i - 1].x;
|
|
2017
|
-
const dy = pts[i].y - pts[i - 1].y;
|
|
2018
|
-
l += Math.sqrt(dx * dx + dy * dy);
|
|
2019
|
-
}
|
|
2020
|
-
return l;
|
|
2021
|
-
}
|
|
2022
|
-
function strokeTaper(u) {
|
|
2023
|
-
return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
|
|
2024
|
-
}
|
|
2025
|
-
function strokeWidthPx(s, scale) {
|
|
2026
|
-
return Math.max(0.5, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
2027
|
-
}
|
|
2028
|
-
function featherRamp(u, feather) {
|
|
2029
|
-
const band = Math.max(0, Math.min(1, feather)) / 2;
|
|
2030
|
-
const d = Math.min(u, 1 - u);
|
|
2031
|
-
if (band <= 0) return d >= 0 ? 1 : 0;
|
|
2032
|
-
return Math.max(0, Math.min(1, d / band));
|
|
2033
|
-
}
|
|
2034
|
-
var FEATHER_MASK_SIZE = 128;
|
|
2035
|
-
function ribbonSides(pts, half) {
|
|
2036
|
-
const n = pts.length;
|
|
2037
|
-
const left = [];
|
|
2038
|
-
const right = [];
|
|
2039
|
-
if (n < 2) return { left, right };
|
|
2040
|
-
const dir = (a, b) => {
|
|
2041
|
-
const dx = b.x - a.x;
|
|
2042
|
-
const dy = b.y - a.y;
|
|
2043
|
-
const l = Math.hypot(dx, dy);
|
|
2044
|
-
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
2045
|
-
};
|
|
2046
|
-
for (let i = 0; i < n; i++) {
|
|
2047
|
-
const tIn = i > 0 ? dir(pts[i - 1], pts[i]) : dir(pts[0], pts[1]);
|
|
2048
|
-
const tOut = i < n - 1 ? dir(pts[i], pts[i + 1]) : tIn;
|
|
2049
|
-
let tx = tIn.x + tOut.x;
|
|
2050
|
-
let ty = tIn.y + tOut.y;
|
|
2051
|
-
const tl = Math.hypot(tx, ty);
|
|
2052
|
-
if (tl < 1e-9) {
|
|
2053
|
-
tx = tOut.x;
|
|
2054
|
-
ty = tOut.y;
|
|
2055
|
-
} else {
|
|
2056
|
-
tx /= tl;
|
|
2057
|
-
ty /= tl;
|
|
2058
|
-
}
|
|
2059
|
-
const hw = half[i] / Math.max(0.5, tx * tOut.x + ty * tOut.y);
|
|
2060
|
-
const p = pts[i];
|
|
2061
|
-
left.push({ x: p.x - ty * hw, y: p.y + tx * hw });
|
|
2062
|
-
right.push({ x: p.x + ty * hw, y: p.y - tx * hw });
|
|
2063
|
-
}
|
|
2064
|
-
return { left, right };
|
|
2065
|
-
}
|
|
2066
|
-
function strokeOutline(pts, lw, taper = true) {
|
|
2067
|
-
const n = pts.length;
|
|
2068
|
-
if (n < 2) return [];
|
|
2069
|
-
const half = (i) => Math.max(0.5, taper ? lw * strokeTaper(i / (n - 1)) : lw) / 2;
|
|
2070
|
-
const dir = (a, b) => {
|
|
2071
|
-
const dx = b.x - a.x;
|
|
2072
|
-
const dy = b.y - a.y;
|
|
2073
|
-
const l = Math.hypot(dx, dy);
|
|
2074
|
-
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
2075
|
-
};
|
|
2076
|
-
const { left, right } = ribbonSides(pts, Array.from({ length: n }, (_, i) => half(i)));
|
|
2077
|
-
const cap = (c, r, from) => {
|
|
2078
|
-
const out = [];
|
|
2079
|
-
const steps = 6;
|
|
2080
|
-
for (let k = 1; k < steps; k++) {
|
|
2081
|
-
const a = from - Math.PI * k / steps;
|
|
2082
|
-
out.push({ x: c.x + Math.cos(a) * r, y: c.y + Math.sin(a) * r });
|
|
2768
|
+
}
|
|
2083
2769
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2770
|
+
}
|
|
2771
|
+
if (errors.length === 0 && seq.wrapMorph === true) {
|
|
2772
|
+
const sq = seq;
|
|
2773
|
+
const last = sq.segments.length - 1;
|
|
2774
|
+
const lastTr = sq.segments[last]?.transition;
|
|
2775
|
+
let why = null;
|
|
2776
|
+
if (sq.loop !== true) why = "the sequence does not loop";
|
|
2777
|
+
else if (last < 1) why = "there is only one segment";
|
|
2778
|
+
else if (lastTr?.type !== "morph") why = "the last segment declares no morph transition";
|
|
2779
|
+
else if (structuralSignature(sq.segments[last].scene) !== structuralSignature(sq.segments[0].scene)) why = `segment ${last} and segment 0 differ structurally`;
|
|
2780
|
+
else if (morphChainRoot(sq, last) !== 0) why = `the lap is not one morph chain (segment ${morphChainRoot(sq, last)} starts a new chain), so the wrap would jump seeds`;
|
|
2781
|
+
if (why || !canWrapMorph(sq)) {
|
|
2782
|
+
warnings.push({ path: "wrapMorph", code: "wrap-morph-inactive", message: `wrapMorph has no effect: ${why ?? "the wrap cannot morph"} \u2014 the wrap stays a cut` });
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
2786
|
+
}
|
|
2787
|
+
function assertValidSequence(seq) {
|
|
2788
|
+
const r = validateSequence(seq);
|
|
2789
|
+
if (!r.valid) {
|
|
2790
|
+
throw new Error(`invalid sequence:
|
|
2791
|
+
${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
2792
|
+
}
|
|
2793
|
+
return seq;
|
|
2096
2794
|
}
|
|
2097
2795
|
|
|
2098
2796
|
// src/simulate.ts
|
|
@@ -2616,9 +3314,13 @@ function wrapDist(pi, pj, doWrap, w, h) {
|
|
|
2616
3314
|
}
|
|
2617
3315
|
return Math.sqrt(dx * dx + dy * dy);
|
|
2618
3316
|
}
|
|
3317
|
+
function linkMaxDistPx(links, scale) {
|
|
3318
|
+
return links.maxDist === void 0 ? Infinity : links.maxDist * scale;
|
|
3319
|
+
}
|
|
2619
3320
|
function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
2620
3321
|
const n = positions.length;
|
|
2621
3322
|
const mode = links.mode ?? "nearest";
|
|
3323
|
+
const k = links.k ?? 1;
|
|
2622
3324
|
if (mode === "chain") {
|
|
2623
3325
|
const out = [];
|
|
2624
3326
|
for (let i = 0; i < n - 1; i++) {
|
|
@@ -2632,7 +3334,7 @@ function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
|
2632
3334
|
const edges = /* @__PURE__ */ new Set();
|
|
2633
3335
|
const out = [];
|
|
2634
3336
|
for (let i = 0; i < n; i++) {
|
|
2635
|
-
for (let mIdx = 1; mIdx <=
|
|
3337
|
+
for (let mIdx = 1; mIdx <= k; mIdx++) {
|
|
2636
3338
|
const j = (i + mIdx * stride) % n;
|
|
2637
3339
|
if (j === i) continue;
|
|
2638
3340
|
const dist = wrapDist(positions[i], positions[j], doWrap, w, h);
|
|
@@ -2646,7 +3348,7 @@ function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
|
2646
3348
|
}
|
|
2647
3349
|
return out;
|
|
2648
3350
|
}
|
|
2649
|
-
return linkPairs(positions,
|
|
3351
|
+
return linkPairs(positions, k, maxDistPx, doWrap, w, h).map(([i, j]) => ({
|
|
2650
3352
|
i,
|
|
2651
3353
|
j,
|
|
2652
3354
|
dist: wrapDist(positions[i], positions[j], doWrap, w, h)
|
|
@@ -2886,6 +3588,10 @@ function paintOpacity(spec, layer) {
|
|
|
2886
3588
|
const g = groupOf(spec, layer);
|
|
2887
3589
|
return g?.opacity === void 0 ? own : own * clamp01(g.opacity);
|
|
2888
3590
|
}
|
|
3591
|
+
function paintRotation(spec, layer) {
|
|
3592
|
+
const deg = (layer.transform?.rotate ?? 0) + (groupOf(spec, layer)?.transform?.rotate ?? 0);
|
|
3593
|
+
return deg * Math.PI / 180;
|
|
3594
|
+
}
|
|
2889
3595
|
function paintSize(spec, layer) {
|
|
2890
3596
|
return transformSize(layer.transform) * transformSize(groupOf(spec, layer)?.transform);
|
|
2891
3597
|
}
|
|
@@ -3325,6 +4031,45 @@ function overridesStayValid(authored, overrides, now) {
|
|
|
3325
4031
|
}
|
|
3326
4032
|
return true;
|
|
3327
4033
|
}
|
|
4034
|
+
var PATH_BLIT_MIN_POINTS = 128;
|
|
4035
|
+
var PLATE_MIN_POINTS = 2500;
|
|
4036
|
+
var PATH_BLIT_PER_SPRITE = 8;
|
|
4037
|
+
var PATH_SLOT_MAX = 96;
|
|
4038
|
+
var PATH_SPRITE_PER_SPRITE = 96;
|
|
4039
|
+
var PATH_SPRITE_MAX_PIXELS = 512 * 512;
|
|
4040
|
+
var PATH_BLIT_MIN_DENSITY = 1500;
|
|
4041
|
+
var PATH_BLIT_MAX_PIXELS = 16e6;
|
|
4042
|
+
function ringPath2D(lines) {
|
|
4043
|
+
const p2 = new Path2D();
|
|
4044
|
+
for (const l of lines) {
|
|
4045
|
+
if (l.points.length < 2) continue;
|
|
4046
|
+
p2.moveTo(l.points[0][0], l.points[0][1]);
|
|
4047
|
+
for (let i = 1; i < l.points.length; i++) p2.lineTo(l.points[i][0], l.points[i][1]);
|
|
4048
|
+
if (l.closed) p2.closePath();
|
|
4049
|
+
}
|
|
4050
|
+
return p2;
|
|
4051
|
+
}
|
|
4052
|
+
function traceLines(g, lines) {
|
|
4053
|
+
g.beginPath();
|
|
4054
|
+
for (const l of lines) {
|
|
4055
|
+
if (l.points.length < 2) continue;
|
|
4056
|
+
g.moveTo(l.points[0][0], l.points[0][1]);
|
|
4057
|
+
for (let i = 1; i < l.points.length; i++) g.lineTo(l.points[i][0], l.points[i][1]);
|
|
4058
|
+
if (l.closed) g.closePath();
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
function pathGradientFill(g, gr) {
|
|
4062
|
+
const grad = gr.type === "linear" ? g.createLinearGradient(gr.from[0], gr.from[1], gr.to[0], gr.to[1]) : g.createRadialGradient((gr.focus ?? gr.center)[0], (gr.focus ?? gr.center)[1], 0, gr.center[0], gr.center[1], gr.radius);
|
|
4063
|
+
for (const st of gr.stops) grad.addColorStop(st.at, st.opacity === void 0 ? st.color : hexToRgba(st.color, st.opacity));
|
|
4064
|
+
return grad;
|
|
4065
|
+
}
|
|
4066
|
+
function softFill(g, color2, r) {
|
|
4067
|
+
const grad = g.createRadialGradient(0, 0, 0, 0, 0, r);
|
|
4068
|
+
grad.addColorStop(0, color2);
|
|
4069
|
+
grad.addColorStop(0.35, hexToRgba(color2, 0.75));
|
|
4070
|
+
grad.addColorStop(1, hexToRgba(color2, 0));
|
|
4071
|
+
return grad;
|
|
4072
|
+
}
|
|
3328
4073
|
var SpecInstance = class {
|
|
3329
4074
|
constructor(spec, ctx, opts = {}) {
|
|
3330
4075
|
this.spec = spec;
|
|
@@ -3435,6 +4180,20 @@ var SpecInstance = class {
|
|
|
3435
4180
|
paintSeq = 0;
|
|
3436
4181
|
/** Per layer: ribbon area painted this frame (px²), and whether its trails ribbon (see ribbonAllowed). */
|
|
3437
4182
|
ribbonBudget = /* @__PURE__ */ new WeakMap();
|
|
4183
|
+
/**
|
|
4184
|
+
* Render-once cache for path polygons (polygon.path). A detailed drawing is
|
|
4185
|
+
* rasterised once per (colour, device size, rotation, sub-pixel offset)
|
|
4186
|
+
* into its own bitmap and copied 1:1 at a whole-pixel offset every frame
|
|
4187
|
+
* after — so a static 7,000-point drawing costs a blit, not a re-fill.
|
|
4188
|
+
* Keyed by the sprite object, so a steer or republish (a new spec object)
|
|
4189
|
+
* starts clean. A drawing whose key keeps changing (spinning, trailing,
|
|
4190
|
+
* growing) stops being cached and draws from a reused Path2D instead.
|
|
4191
|
+
*/
|
|
4192
|
+
pathBlits = /* @__PURE__ */ new Map();
|
|
4193
|
+
/** Test/bench switch: `globalThis.__idleNoPathCache = true` before compiling draws every path directly. */
|
|
4194
|
+
noPathCache = globalThis.__idleNoPathCache === true;
|
|
4195
|
+
/** Device pixels held by every pathBlits bitmap — capped so a TV never pays more than PATH_BLIT_MAX_PIXELS of memory. */
|
|
4196
|
+
pathBlitPixels = 0;
|
|
3438
4197
|
featherTints = /* @__PURE__ */ new Map();
|
|
3439
4198
|
/**
|
|
3440
4199
|
* The presentation pass when this instance applies a `finish`: the
|
|
@@ -3713,6 +4472,277 @@ var SpecInstance = class {
|
|
|
3713
4472
|
return rb.ribbon;
|
|
3714
4473
|
}
|
|
3715
4474
|
/** A fresh 2D canvas (DOM or Offscreen), or null where none can be made. */
|
|
4475
|
+
/**
|
|
4476
|
+
* A drawing's cache slot, keyed by what it draws (pathPaintSig), not by the
|
|
4477
|
+
* sprite object — glides and timelines replace that object every frame.
|
|
4478
|
+
* Least-recently-used slots are evicted past PATH_SLOT_MAX, and their
|
|
4479
|
+
* bitmaps' pixels returned to the budget.
|
|
4480
|
+
*/
|
|
4481
|
+
pathSlot(sprite) {
|
|
4482
|
+
const sig = pathPaintSig(sprite);
|
|
4483
|
+
let slot = this.pathBlits.get(sig);
|
|
4484
|
+
if (slot) {
|
|
4485
|
+
this.pathBlits.delete(sig);
|
|
4486
|
+
this.pathBlits.set(sig, slot);
|
|
4487
|
+
return slot;
|
|
4488
|
+
}
|
|
4489
|
+
if (this.pathBlits.size >= PATH_SLOT_MAX) {
|
|
4490
|
+
const [oldSig, old] = this.pathBlits.entries().next().value;
|
|
4491
|
+
for (const ent of old.entries.values()) this.pathBlitPixels -= ent.px;
|
|
4492
|
+
this.pathBlits.delete(oldSig);
|
|
4493
|
+
}
|
|
4494
|
+
slot = { entries: /* @__PURE__ */ new Map(), flushes: 0, off: false };
|
|
4495
|
+
this.pathBlits.set(sig, slot);
|
|
4496
|
+
return slot;
|
|
4497
|
+
}
|
|
4498
|
+
/**
|
|
4499
|
+
* Whether a layer's path drawing looks the same as last frame — the caches
|
|
4500
|
+
* serve only steady drawings; one that is gliding (colour, gradient, shape)
|
|
4501
|
+
* draws directly until it settles, instead of rasterising a bitmap a frame.
|
|
4502
|
+
*/
|
|
4503
|
+
// Keyed by the layer's place in the scene, never the per-layer record: a
|
|
4504
|
+
// scene that animates a baked field (alpha, size) rebuilds every record
|
|
4505
|
+
// every frame, and a drawing beside it is still steady.
|
|
4506
|
+
pathSteady = /* @__PURE__ */ new Map();
|
|
4507
|
+
drawingSteady(built, sprite) {
|
|
4508
|
+
const sig = pathPaintSig(sprite);
|
|
4509
|
+
const li = this.layers.indexOf(built);
|
|
4510
|
+
const prev = this.pathSteady.get(li);
|
|
4511
|
+
if (prev && prev.seq === this.paintSeq && prev.sig === sig) return prev.steady;
|
|
4512
|
+
const steady = prev !== void 0 && prev.sig === sig;
|
|
4513
|
+
this.pathSteady.set(li, { sig, seq: this.paintSeq, steady });
|
|
4514
|
+
return steady;
|
|
4515
|
+
}
|
|
4516
|
+
/**
|
|
4517
|
+
* Draw a path polygon in its own unit box: the caller has moved to the
|
|
4518
|
+
* entity and rotated; this scales by r. Parts paint back to front (a single
|
|
4519
|
+
* `path` is one part). Each fill is its gradient (drawing-box coordinates,
|
|
4520
|
+
* or with units 'part' the part's own box, filled under that box's
|
|
4521
|
+
* transform so a radial reads as the part's ellipse), the soft radial
|
|
4522
|
+
* falloff, or flat colour; each stroke traces the part's subpaths as drawn,
|
|
4523
|
+
* width in unit-box units. Outlines are Path2Ds built once per sprite.
|
|
4524
|
+
*/
|
|
4525
|
+
drawPathUnit(g, sprite, color2, fr, r) {
|
|
4526
|
+
const slot = this.pathSlot(sprite);
|
|
4527
|
+
const parts = pathParts(sprite);
|
|
4528
|
+
if (!slot.parts2d) slot.parts2d = parts.map(() => ({}));
|
|
4529
|
+
if (fr.ox || fr.oy) g.translate(-fr.ox * fr.sx, -fr.oy * fr.sy);
|
|
4530
|
+
g.scale(fr.sx, fr.sy);
|
|
4531
|
+
const a0 = g.globalAlpha;
|
|
4532
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4533
|
+
const part = parts[i];
|
|
4534
|
+
const cache = slot.parts2d[i];
|
|
4535
|
+
const pc = part.color ?? color2;
|
|
4536
|
+
if (part.fill && part.flat.points.length >= 3) {
|
|
4537
|
+
g.globalAlpha = a0 * part.opacity;
|
|
4538
|
+
if (part.gradient?.units === "part") {
|
|
4539
|
+
const { cx, cy, hw, hh } = part.box;
|
|
4540
|
+
if (cache.fillPart === void 0) {
|
|
4541
|
+
cache.fillPart = typeof Path2D === "undefined" ? null : ringPath2D([{ points: part.flat.points.map(([x, y]) => [(x - cx) / hw, (y - cy) / hh]), closed: true }]);
|
|
4542
|
+
}
|
|
4543
|
+
g.save();
|
|
4544
|
+
g.transform(hw, 0, 0, hh, cx, cy);
|
|
4545
|
+
g.fillStyle = pathGradientFill(g, part.gradient);
|
|
4546
|
+
if (cache.fillPart) g.fill(cache.fillPart, part.rule);
|
|
4547
|
+
else {
|
|
4548
|
+
traceLines(g, [{ points: part.flat.points.map(([x, y]) => [(x - cx) / hw, (y - cy) / hh]), closed: true }]);
|
|
4549
|
+
g.fill(part.rule);
|
|
4550
|
+
}
|
|
4551
|
+
g.restore();
|
|
4552
|
+
} else {
|
|
4553
|
+
g.fillStyle = part.gradient ? pathGradientFill(g, part.gradient) : sprite.soft && !sprite.parts ? softFill(g, pc, 1) : pc;
|
|
4554
|
+
if (cache.fill === void 0) cache.fill = typeof Path2D === "undefined" ? null : ringPath2D([{ points: part.flat.points, closed: true }]);
|
|
4555
|
+
if (cache.fill) g.fill(cache.fill, part.rule);
|
|
4556
|
+
else {
|
|
4557
|
+
traceLines(g, [{ points: part.flat.points, closed: true }]);
|
|
4558
|
+
g.fill(part.rule);
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
const st = part.stroke;
|
|
4563
|
+
if (st && part.flat.lines.length) {
|
|
4564
|
+
g.globalAlpha = a0 * part.opacity * (st.opacity ?? 1);
|
|
4565
|
+
g.strokeStyle = st.color;
|
|
4566
|
+
g.lineJoin = st.join ?? "round";
|
|
4567
|
+
g.lineCap = st.cap ?? "round";
|
|
4568
|
+
if (fr.uniform) {
|
|
4569
|
+
g.lineWidth = st.width * r / fr.sx;
|
|
4570
|
+
if (cache.stroke === void 0) cache.stroke = typeof Path2D === "undefined" ? null : ringPath2D(part.flat.lines);
|
|
4571
|
+
if (cache.stroke) g.stroke(cache.stroke);
|
|
4572
|
+
else {
|
|
4573
|
+
traceLines(g, part.flat.lines);
|
|
4574
|
+
g.stroke();
|
|
4575
|
+
}
|
|
4576
|
+
} else {
|
|
4577
|
+
const scaled = part.flat.lines.map((l) => ({ closed: l.closed, points: l.points.map(([x, y]) => [x * fr.sx, y * fr.sy]) }));
|
|
4578
|
+
g.save();
|
|
4579
|
+
g.scale(1 / fr.sx, 1 / fr.sy);
|
|
4580
|
+
g.lineWidth = st.width * r;
|
|
4581
|
+
traceLines(g, scaled);
|
|
4582
|
+
g.stroke();
|
|
4583
|
+
g.restore();
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
g.globalAlpha = a0;
|
|
4588
|
+
}
|
|
4589
|
+
/**
|
|
4590
|
+
* Draw a path polygon from its render-once bitmap. False when it cannot be
|
|
4591
|
+
* blitted exactly — a rotated or skewed layer transform, a bitmap past the
|
|
4592
|
+
* memory cap, a drawing too sparse for its area, or one that keeps
|
|
4593
|
+
* changing — and the caller draws it directly.
|
|
4594
|
+
*/
|
|
4595
|
+
blitPath(sprite, p, r, rot, color2, fr) {
|
|
4596
|
+
const ctx = this.ctx;
|
|
4597
|
+
const slot = this.pathSlot(sprite);
|
|
4598
|
+
if (slot.off) return false;
|
|
4599
|
+
if (slot.sprite) return this.spriteBlit(sprite, slot, p, r, rot, color2, fr);
|
|
4600
|
+
const m = ctx.getTransform();
|
|
4601
|
+
if (Math.abs(m.b) > 1e-9 || Math.abs(m.c) > 1e-9 || Math.abs(m.a - m.d) > 1e-9 || !(m.a > 0)) {
|
|
4602
|
+
slot.sprite = true;
|
|
4603
|
+
return this.spriteBlit(sprite, slot, p, r, rot, color2, fr);
|
|
4604
|
+
}
|
|
4605
|
+
const k = m.a;
|
|
4606
|
+
const dx = k * p.x + m.e;
|
|
4607
|
+
const dy = k * p.y + m.f;
|
|
4608
|
+
const ix = Math.floor(dx);
|
|
4609
|
+
const iy = Math.floor(dy);
|
|
4610
|
+
const fx = Math.round((dx - ix) * 8) / 8;
|
|
4611
|
+
const fy = Math.round((dy - iy) * 8) / 8;
|
|
4612
|
+
const key = `${color2}|${(r * k).toFixed(3)}|${(fr.sx * k).toFixed(3)}|${(fr.sy * k).toFixed(3)}|${fr.ox},${fr.oy}|${rot.toFixed(5)}|${fx}|${fy}`;
|
|
4613
|
+
let hit = slot.entries.get(key);
|
|
4614
|
+
if (!hit) {
|
|
4615
|
+
if (slot.entries.size >= PATH_BLIT_PER_SPRITE) {
|
|
4616
|
+
for (const old of slot.entries.values()) this.pathBlitPixels -= old.px;
|
|
4617
|
+
slot.entries.clear();
|
|
4618
|
+
if (++slot.flushes > 1) {
|
|
4619
|
+
slot.sprite = true;
|
|
4620
|
+
slot.flushes = 0;
|
|
4621
|
+
return this.spriteBlit(sprite, slot, p, r, rot, color2, fr);
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
const c = Math.cos(rot);
|
|
4625
|
+
const sn = Math.sin(rot);
|
|
4626
|
+
const s = r * k;
|
|
4627
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
4628
|
+
const grow = (u) => {
|
|
4629
|
+
const ux = (u[0] - fr.ox) * fr.sx * k, uy = (u[1] - fr.oy) * fr.sy * k;
|
|
4630
|
+
const x = ux * c - uy * sn;
|
|
4631
|
+
const y = ux * sn + uy * c;
|
|
4632
|
+
if (x < x0) x0 = x;
|
|
4633
|
+
if (x > x1) x1 = x;
|
|
4634
|
+
if (y < y0) y0 = y;
|
|
4635
|
+
if (y > y1) y1 = y;
|
|
4636
|
+
};
|
|
4637
|
+
let sw = 0;
|
|
4638
|
+
for (const part of pathParts(sprite)) {
|
|
4639
|
+
if (part.fill) for (const u of part.flat.points) grow(u);
|
|
4640
|
+
if (part.stroke) {
|
|
4641
|
+
sw = Math.max(sw, part.stroke.width);
|
|
4642
|
+
for (const l of part.flat.lines) for (const u of l.points) grow(u);
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
const pad = 2 + Math.ceil(sw * s);
|
|
4646
|
+
const bx = Math.floor(fx + x0) - pad;
|
|
4647
|
+
const by = Math.floor(fy + y0) - pad;
|
|
4648
|
+
const w = Math.ceil(fx + x1) - bx + pad;
|
|
4649
|
+
const h = Math.ceil(fy + y1) - by + pad;
|
|
4650
|
+
if (!(w > 0 && h > 0) || !Number.isFinite(w + h)) return false;
|
|
4651
|
+
if (pathDrawCost(sprite) / (w * h / 1e6) < PATH_BLIT_MIN_DENSITY) {
|
|
4652
|
+
slot.off = true;
|
|
4653
|
+
return false;
|
|
4654
|
+
}
|
|
4655
|
+
if (this.pathBlitPixels + w * h > PATH_BLIT_MAX_PIXELS) return false;
|
|
4656
|
+
const made = this.makeCanvas(1);
|
|
4657
|
+
if (!made) return false;
|
|
4658
|
+
made.canvas.width = w;
|
|
4659
|
+
made.canvas.height = h;
|
|
4660
|
+
const g = made.ctx;
|
|
4661
|
+
g.setTransform(k, 0, 0, k, fx - bx, fy - by);
|
|
4662
|
+
if (rot) g.rotate(rot);
|
|
4663
|
+
this.drawPathUnit(g, sprite, color2, fr, r);
|
|
4664
|
+
hit = { canvas: made.canvas, x0: bx, y0: by, px: w * h };
|
|
4665
|
+
this.pathBlitPixels += hit.px;
|
|
4666
|
+
slot.entries.set(key, hit);
|
|
4667
|
+
}
|
|
4668
|
+
ctx.save();
|
|
4669
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4670
|
+
ctx.drawImage(hit.canvas, ix + hit.x0, iy + hit.y0);
|
|
4671
|
+
ctx.restore();
|
|
4672
|
+
return true;
|
|
4673
|
+
}
|
|
4674
|
+
/**
|
|
4675
|
+
* The moving-sprite cache: for a drawing whose exact key keeps changing. It
|
|
4676
|
+
* is rasterised once per colour, UNROTATED, at a device radius rounded UP to
|
|
4677
|
+
* the next 1/8-octave step (so it is only ever shrunk, never enlarged), then
|
|
4678
|
+
* drawn every frame at its true sub-pixel position, rotation and size under
|
|
4679
|
+
* the context's own transform, with smoothing. Not pixel-exact like the
|
|
4680
|
+
* still cache (edges resample by well under a pixel), but a 7-part drifting
|
|
4681
|
+
* balloon costs one image draw instead of seven fills and strokes.
|
|
4682
|
+
*/
|
|
4683
|
+
spriteBlit(sprite, slot, p, r, rot, color2, fr) {
|
|
4684
|
+
const ctx = this.ctx;
|
|
4685
|
+
if (!fr.uniform || fr.sx !== r) return false;
|
|
4686
|
+
const m = ctx.getTransform();
|
|
4687
|
+
const k = Math.sqrt(Math.abs(m.a * m.d - m.b * m.c));
|
|
4688
|
+
const s = r * k;
|
|
4689
|
+
if (!(s >= 1)) return false;
|
|
4690
|
+
const bucket = 2 ** (Math.ceil(Math.log2(s) * 8) / 8);
|
|
4691
|
+
const key = `S|${color2}|${bucket.toFixed(3)}`;
|
|
4692
|
+
let hit = slot.entries.get(key);
|
|
4693
|
+
if (!hit) {
|
|
4694
|
+
if (slot.entries.size >= PATH_SPRITE_PER_SPRITE) {
|
|
4695
|
+
for (const old of slot.entries.values()) this.pathBlitPixels -= old.px;
|
|
4696
|
+
slot.entries.clear();
|
|
4697
|
+
if (++slot.flushes > 2) {
|
|
4698
|
+
slot.off = true;
|
|
4699
|
+
return false;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity, sw = 0;
|
|
4703
|
+
const grow = (u2) => {
|
|
4704
|
+
if (u2[0] < x0) x0 = u2[0];
|
|
4705
|
+
if (u2[0] > x1) x1 = u2[0];
|
|
4706
|
+
if (u2[1] < y0) y0 = u2[1];
|
|
4707
|
+
if (u2[1] > y1) y1 = u2[1];
|
|
4708
|
+
};
|
|
4709
|
+
for (const part of pathParts(sprite)) {
|
|
4710
|
+
if (part.fill) for (const u2 of part.flat.points) grow([u2[0] - fr.ox, u2[1] - fr.oy]);
|
|
4711
|
+
if (part.stroke) {
|
|
4712
|
+
sw = Math.max(sw, part.stroke.width);
|
|
4713
|
+
for (const l of part.flat.lines) for (const u2 of l.points) grow([u2[0] - fr.ox, u2[1] - fr.oy]);
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
const pad = 2 + Math.ceil(sw * bucket);
|
|
4717
|
+
const ox = Math.ceil(-x0 * bucket) + pad;
|
|
4718
|
+
const oy = Math.ceil(-y0 * bucket) + pad;
|
|
4719
|
+
const w = ox + Math.ceil(x1 * bucket) + pad;
|
|
4720
|
+
const h = oy + Math.ceil(y1 * bucket) + pad;
|
|
4721
|
+
if (!(w > 0 && h > 0) || !Number.isFinite(w + h)) return false;
|
|
4722
|
+
if (w * h > PATH_SPRITE_MAX_PIXELS) {
|
|
4723
|
+
slot.off = true;
|
|
4724
|
+
return false;
|
|
4725
|
+
}
|
|
4726
|
+
if (this.pathBlitPixels + w * h > PATH_BLIT_MAX_PIXELS) return false;
|
|
4727
|
+
const made = this.makeCanvas(1);
|
|
4728
|
+
if (!made) return false;
|
|
4729
|
+
made.canvas.width = w;
|
|
4730
|
+
made.canvas.height = h;
|
|
4731
|
+
made.ctx.setTransform(1, 0, 0, 1, ox, oy);
|
|
4732
|
+
this.drawPathUnit(made.ctx, sprite, color2, { sx: bucket, sy: bucket, ox: fr.ox, oy: fr.oy, uniform: true }, bucket);
|
|
4733
|
+
hit = { canvas: made.canvas, x0: ox, y0: oy, px: w * h };
|
|
4734
|
+
this.pathBlitPixels += hit.px;
|
|
4735
|
+
slot.entries.set(key, hit);
|
|
4736
|
+
}
|
|
4737
|
+
const u = r / bucket;
|
|
4738
|
+
ctx.save();
|
|
4739
|
+
ctx.translate(p.x, p.y);
|
|
4740
|
+
if (rot) ctx.rotate(rot);
|
|
4741
|
+
ctx.imageSmoothingEnabled = true;
|
|
4742
|
+
ctx.drawImage(hit.canvas, -hit.x0 * u, -hit.y0 * u, hit.canvas.width * u, hit.canvas.height * u);
|
|
4743
|
+
ctx.restore();
|
|
4744
|
+
return true;
|
|
4745
|
+
}
|
|
3716
4746
|
makeCanvas(n) {
|
|
3717
4747
|
try {
|
|
3718
4748
|
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(n, n);
|
|
@@ -3944,24 +4974,26 @@ var SpecInstance = class {
|
|
|
3944
4974
|
if (sprite.kind === "polygon") {
|
|
3945
4975
|
const r = sz / 2;
|
|
3946
4976
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
3947
|
-
|
|
4977
|
+
if (isPathPolygon(sprite)) {
|
|
4978
|
+
const fr = pathFrame(sprite, r, this.w, this.h);
|
|
4979
|
+
if (pathDrawCost(sprite) >= PATH_BLIT_MIN_POINTS && !this.noPathCache && this.drawingSteady(built, sprite) && this.blitPath(sprite, p, r, rot, resolvedColor, fr)) return;
|
|
4980
|
+
ctx.save();
|
|
4981
|
+
ctx.translate(p.x, p.y);
|
|
4982
|
+
if (rot) ctx.rotate(rot);
|
|
4983
|
+
if (r > 0 && fr.sx > 0 && fr.sy > 0) this.drawPathUnit(ctx, sprite, resolvedColor, fr, r);
|
|
4984
|
+
ctx.restore();
|
|
4985
|
+
return;
|
|
4986
|
+
}
|
|
3948
4987
|
ctx.save();
|
|
3949
4988
|
ctx.translate(p.x, p.y);
|
|
3950
4989
|
if (rot) ctx.rotate(rot);
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
g.addColorStop(0, resolvedColor);
|
|
3954
|
-
g.addColorStop(0.35, hexToRgba(resolvedColor, 0.75));
|
|
3955
|
-
g.addColorStop(1, hexToRgba(resolvedColor, 0));
|
|
3956
|
-
ctx.fillStyle = g;
|
|
3957
|
-
} else {
|
|
3958
|
-
ctx.fillStyle = resolvedColor;
|
|
3959
|
-
}
|
|
4990
|
+
ctx.fillStyle = sprite.soft ? softFill(ctx, resolvedColor, r) : resolvedColor;
|
|
4991
|
+
const pts = polygonPoints(sprite, r);
|
|
3960
4992
|
ctx.beginPath();
|
|
3961
4993
|
ctx.moveTo(pts[0].x, pts[0].y);
|
|
3962
4994
|
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
|
|
3963
4995
|
ctx.closePath();
|
|
3964
|
-
ctx.fill();
|
|
4996
|
+
ctx.fill(polygonFillRule(sprite));
|
|
3965
4997
|
ctx.restore();
|
|
3966
4998
|
return;
|
|
3967
4999
|
}
|
|
@@ -4200,9 +5232,9 @@ var SpecInstance = class {
|
|
|
4200
5232
|
const motionWraps = ["drift", "rise", "wander"].includes(built.layer.motion.type);
|
|
4201
5233
|
const wrap2 = built.layer.wrap !== false && motionWraps;
|
|
4202
5234
|
const positions = built.entities.map((e) => this.entityPos(e, t, parentE));
|
|
4203
|
-
const maxDistPx = links.maxDist * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h));
|
|
4204
|
-
const edges = linkEdges(links, positions, maxDistPx, wrap2, this.w, this.h);
|
|
4205
5235
|
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
5236
|
+
const maxDistPx = linkMaxDistPx(links, unitScale);
|
|
5237
|
+
const edges = linkEdges(links, positions, maxDistPx, wrap2, this.w, this.h);
|
|
4206
5238
|
const defaultWidth = unitScale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
4207
5239
|
const lw = (links.width ?? defaultWidth) * unitScale;
|
|
4208
5240
|
ctx.lineWidth = lw;
|
|
@@ -4219,7 +5251,7 @@ var SpecInstance = class {
|
|
|
4219
5251
|
} else resolvedColor = "#e6e8ef";
|
|
4220
5252
|
}
|
|
4221
5253
|
let a = links.alpha ?? alphaAt(ei, t);
|
|
4222
|
-
if (links.falloff) a *= Math.max(0, 1 - dist / maxDistPx);
|
|
5254
|
+
if (links.falloff && Number.isFinite(maxDistPx)) a *= Math.max(0, 1 - dist / maxDistPx);
|
|
4223
5255
|
ctx.globalAlpha = a * lifeA;
|
|
4224
5256
|
ctx.strokeStyle = resolvedColor;
|
|
4225
5257
|
let dx = pj.x - pi.x;
|
|
@@ -4246,38 +5278,212 @@ var SpecInstance = class {
|
|
|
4246
5278
|
this.drawBackground(t);
|
|
4247
5279
|
}
|
|
4248
5280
|
ctx.globalAlpha = 1;
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
const parentE = this.parentEntityFor(built);
|
|
4257
|
-
const tf = built.layer.transform;
|
|
4258
|
-
const gtf = group?.transform;
|
|
4259
|
-
if (tf || gtf) {
|
|
4260
|
-
ctx.save();
|
|
4261
|
-
if (gtf) this.applyLayerTransform(gtf, null);
|
|
4262
|
-
if (tf) this.applyLayerTransform(tf, built.layer);
|
|
4263
|
-
}
|
|
4264
|
-
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
4265
|
-
this.drawLinks(built, t, lifeA, parentE);
|
|
4266
|
-
const override = this.paintOverrides?.get(li) ?? null;
|
|
4267
|
-
for (const e of built.entities) {
|
|
4268
|
-
this.drawTrail(built, e, t, lifeA, parentE);
|
|
4269
|
-
if (override === null) {
|
|
4270
|
-
this.drawEntity(built, e, t, lifeA, parentE);
|
|
5281
|
+
const n = this.layers.length;
|
|
5282
|
+
for (let li = 0; li < n; li++) {
|
|
5283
|
+
if (!this.noPathCache && this.plateable(li, t)) {
|
|
5284
|
+
let j = li;
|
|
5285
|
+
while (j + 1 < n && this.plateable(j + 1, t)) j++;
|
|
5286
|
+
if (j > li && this.runPoints(li, j) >= PLATE_MIN_POINTS && this.paintPlate(li, j, t)) {
|
|
5287
|
+
li = j;
|
|
4271
5288
|
continue;
|
|
4272
5289
|
}
|
|
4273
|
-
const [aOut, aIn] = override.mode === "dip" ? [Math.max(0, 1 - 2 * override.k), Math.max(0, 2 * override.k - 1)] : [1 - override.k, override.k];
|
|
4274
|
-
if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
|
|
4275
|
-
if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
|
|
4276
5290
|
}
|
|
4277
|
-
|
|
5291
|
+
this.paintLayer(li, t);
|
|
5292
|
+
}
|
|
5293
|
+
ctx.globalAlpha = 1;
|
|
5294
|
+
ctx.globalCompositeOperation = "source-over";
|
|
5295
|
+
}
|
|
5296
|
+
/** Paint layer `li` at time t onto this.ctx (the scene canvas, or a plate being rebuilt). */
|
|
5297
|
+
paintLayer(li, t) {
|
|
5298
|
+
const { ctx } = this;
|
|
5299
|
+
const built = this.layers[li];
|
|
5300
|
+
const opacity = built.layer.opacity;
|
|
5301
|
+
const group = built.layer.group === void 0 ? void 0 : this.effSpec.groups?.[built.layer.group];
|
|
5302
|
+
let lifeA = opacity === void 0 ? lifeAlphaAt(built.layer.life, t) : lifeAlphaAt(built.layer.life, t) * clamp014(opacity);
|
|
5303
|
+
if (group?.opacity !== void 0) lifeA *= clamp014(group.opacity);
|
|
5304
|
+
if (lifeA <= 0) return;
|
|
5305
|
+
const parentE = this.parentEntityFor(built);
|
|
5306
|
+
const tf = built.layer.transform;
|
|
5307
|
+
const gtf = group?.transform;
|
|
5308
|
+
if (tf || gtf) {
|
|
5309
|
+
ctx.save();
|
|
5310
|
+
if (gtf) this.applyLayerTransform(gtf, null);
|
|
5311
|
+
if (tf) this.applyLayerTransform(tf, built.layer);
|
|
5312
|
+
}
|
|
5313
|
+
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
5314
|
+
this.drawLinks(built, t, lifeA, parentE);
|
|
5315
|
+
const override = this.paintOverrides?.get(li) ?? null;
|
|
5316
|
+
for (const e of built.entities) {
|
|
5317
|
+
this.drawTrail(built, e, t, lifeA, parentE);
|
|
5318
|
+
if (override === null) {
|
|
5319
|
+
this.drawEntity(built, e, t, lifeA, parentE);
|
|
5320
|
+
continue;
|
|
5321
|
+
}
|
|
5322
|
+
const [aOut, aIn] = override.mode === "dip" ? [Math.max(0, 1 - 2 * override.k), Math.max(0, 2 * override.k - 1)] : [1 - override.k, override.k];
|
|
5323
|
+
if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
|
|
5324
|
+
if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
|
|
5325
|
+
}
|
|
5326
|
+
if (tf || gtf) ctx.restore();
|
|
5327
|
+
}
|
|
5328
|
+
/**
|
|
5329
|
+
* Static plates. Consecutive path-polygon layers painted source-over, with
|
|
5330
|
+
* no trails, links or text overrides, are composited into ONE full-frame
|
|
5331
|
+
* bitmap and copied once per frame while nothing they draw has changed —
|
|
5332
|
+
* the change test is the drawn values themselves (each entity's position,
|
|
5333
|
+
* size, rotation, alpha and colour, the layer's life alpha and transforms),
|
|
5334
|
+
* so anything animated simply repaints its plate that frame. Source-over is
|
|
5335
|
+
* associative, so the plate over the frame equals the layers over it.
|
|
5336
|
+
*/
|
|
5337
|
+
plates = /* @__PURE__ */ new Map();
|
|
5338
|
+
/** Outline points a run of path layers fills per frame (points × entities). */
|
|
5339
|
+
runPoints(a, b) {
|
|
5340
|
+
let n = 0;
|
|
5341
|
+
for (let li = a; li <= b; li++) {
|
|
5342
|
+
const built = this.layers[li];
|
|
5343
|
+
const sp = built.layer.sprite;
|
|
5344
|
+
if (sp.kind === "polygon" && isPathPolygon(sp)) n += pathDrawCost(sp) * built.entities.length;
|
|
4278
5345
|
}
|
|
5346
|
+
return n;
|
|
5347
|
+
}
|
|
5348
|
+
/** Last drawn-value signature per layer, for plate stability. */
|
|
5349
|
+
layerSigs = /* @__PURE__ */ new Map();
|
|
5350
|
+
/**
|
|
5351
|
+
* A layer joins a plate only if it can (path polygon, source-over, no
|
|
5352
|
+
* trails/links/text override) AND nothing it draws changed since the last
|
|
5353
|
+
* frame — so a pulsing or moving layer splits the run and paints live, while
|
|
5354
|
+
* the static layers around it keep their plates.
|
|
5355
|
+
*/
|
|
5356
|
+
plateable(li, t) {
|
|
5357
|
+
const built = this.layers[li];
|
|
5358
|
+
const l = built.layer;
|
|
5359
|
+
const sp = l.sprite;
|
|
5360
|
+
if (!isPathPolygon(sp)) return false;
|
|
5361
|
+
if ((l.blend ?? "source-over") !== "source-over") return false;
|
|
5362
|
+
if (l.trail || l.links || this.paintOverrides?.has(li)) return false;
|
|
5363
|
+
if (built.entities.length > 64) return false;
|
|
5364
|
+
const sig = this.layerSig(li, t);
|
|
5365
|
+
const prev = this.layerSigs.get(li);
|
|
5366
|
+
this.layerSigs.set(li, sig);
|
|
5367
|
+
return prev === sig;
|
|
5368
|
+
}
|
|
5369
|
+
layerSig(li, t) {
|
|
5370
|
+
const built = this.layers[li];
|
|
5371
|
+
const group = built.layer.group === void 0 ? void 0 : this.effSpec.groups?.[built.layer.group];
|
|
5372
|
+
const opacity = built.layer.opacity;
|
|
5373
|
+
let lifeA = opacity === void 0 ? lifeAlphaAt(built.layer.life, t) : lifeAlphaAt(built.layer.life, t) * clamp014(opacity);
|
|
5374
|
+
if (group?.opacity !== void 0) lifeA *= clamp014(group.opacity);
|
|
5375
|
+
let sig = `${pathPaintSig(built.layer.sprite)}:${lifeA.toFixed(4)}:${JSON.stringify(built.layer.transform ?? null)}:${JSON.stringify(group?.transform ?? null)}`;
|
|
5376
|
+
const parentE = this.parentEntityFor(built);
|
|
5377
|
+
for (const e of built.entities) {
|
|
5378
|
+
const p = this.entityPos(e, t, parentE);
|
|
5379
|
+
sig += `|${p.x.toFixed(2)},${p.y.toFixed(2)},${sizeAt(e, t).toFixed(3)},${rotationAt(e, t).toFixed(5)},${alphaAt(e, t).toFixed(4)},${e.colorIndex}`;
|
|
5380
|
+
}
|
|
5381
|
+
return sig;
|
|
5382
|
+
}
|
|
5383
|
+
plateSig(a, b) {
|
|
5384
|
+
let sig = `${a}-${b}`;
|
|
5385
|
+
for (let li = a; li <= b; li++) sig += `#${this.layerSigs.get(li)}`;
|
|
5386
|
+
return sig;
|
|
5387
|
+
}
|
|
5388
|
+
/** Max |point| of a path polygon's unit outline (its reach as a fraction of the entity radius). */
|
|
5389
|
+
pathReach = /* @__PURE__ */ new WeakMap();
|
|
5390
|
+
reachOf(sp) {
|
|
5391
|
+
let r = this.pathReach.get(sp);
|
|
5392
|
+
if (r === void 0) {
|
|
5393
|
+
r = 0;
|
|
5394
|
+
let sw = 0;
|
|
5395
|
+
for (const part of pathParts(sp)) {
|
|
5396
|
+
for (const [x, y] of part.flat.points) r = Math.max(r, Math.hypot(x, y));
|
|
5397
|
+
for (const l of part.flat.lines) for (const [x, y] of l.points) r = Math.max(r, Math.hypot(x, y));
|
|
5398
|
+
if (part.stroke) sw = Math.max(sw, part.stroke.width);
|
|
5399
|
+
}
|
|
5400
|
+
r += sw;
|
|
5401
|
+
this.pathReach.set(sp, r || 1);
|
|
5402
|
+
}
|
|
5403
|
+
return r || 1;
|
|
5404
|
+
}
|
|
5405
|
+
/**
|
|
5406
|
+
* Device-pixel box a run of layers can paint into, or null for "the whole
|
|
5407
|
+
* frame" (a layer/group transform moves things in ways this does not model).
|
|
5408
|
+
*/
|
|
5409
|
+
runBox(a, b, t) {
|
|
5410
|
+
const m = this.ctx.getTransform();
|
|
5411
|
+
if (Math.abs(m.b) > 1e-9 || Math.abs(m.c) > 1e-9) return null;
|
|
5412
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
5413
|
+
for (let li = a; li <= b; li++) {
|
|
5414
|
+
const built = this.layers[li];
|
|
5415
|
+
const group = built.layer.group === void 0 ? void 0 : this.effSpec.groups?.[built.layer.group];
|
|
5416
|
+
if (built.layer.transform || group?.transform) return null;
|
|
5417
|
+
const reach = this.reachOf(built.layer.sprite);
|
|
5418
|
+
const parentE = this.parentEntityFor(built);
|
|
5419
|
+
for (const e of built.entities) {
|
|
5420
|
+
const p = this.entityPos(e, t, parentE);
|
|
5421
|
+
const sp = built.layer.sprite;
|
|
5422
|
+
const fr = pathFrame(sp, sizeAt(e, t) / 2, this.w, this.h);
|
|
5423
|
+
const ext = Math.max(fr.sx, fr.sy) * (reach + Math.hypot(fr.ox, fr.oy));
|
|
5424
|
+
x0 = Math.min(x0, p.x - ext);
|
|
5425
|
+
x1 = Math.max(x1, p.x + ext);
|
|
5426
|
+
y0 = Math.min(y0, p.y - ext);
|
|
5427
|
+
y1 = Math.max(y1, p.y + ext);
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5430
|
+
const W = this.canvas.width, H = this.canvas.height;
|
|
5431
|
+
const bx = Math.max(0, Math.floor(m.a * x0 + m.e) - 2);
|
|
5432
|
+
const by = Math.max(0, Math.floor(m.d * y0 + m.f) - 2);
|
|
5433
|
+
const bw = Math.min(W, Math.ceil(m.a * x1 + m.e) + 2) - bx;
|
|
5434
|
+
const bh = Math.min(H, Math.ceil(m.d * y1 + m.f) + 2) - by;
|
|
5435
|
+
return bw > 0 && bh > 0 ? { x: bx, y: by, w: bw, h: bh } : { x: 0, y: 0, w: 0, h: 0 };
|
|
5436
|
+
}
|
|
5437
|
+
/** Paint layers a..b from their plate, rebuilding it if anything drawn changed. False = paint them normally. */
|
|
5438
|
+
paintPlate(a, b, t) {
|
|
5439
|
+
const sig = this.plateSig(a, b);
|
|
5440
|
+
let plate = this.plates.get(a);
|
|
5441
|
+
if (!plate || plate.sig !== sig || plate.end !== b || plate.frameW !== this.canvas.width || plate.frameH !== this.canvas.height) {
|
|
5442
|
+
const box = this.runBox(a, b, t) ?? { x: 0, y: 0, w: this.canvas.width, h: this.canvas.height };
|
|
5443
|
+
if (!plate) {
|
|
5444
|
+
if (this.plates.size >= 4) this.plates.delete(this.plates.keys().next().value);
|
|
5445
|
+
const made = this.makeCanvas(1);
|
|
5446
|
+
if (!made) return false;
|
|
5447
|
+
plate = { ...made, end: b, sig: "", box, frameW: 0, frameH: 0 };
|
|
5448
|
+
this.plates.set(a, plate);
|
|
5449
|
+
}
|
|
5450
|
+
plate.box = box;
|
|
5451
|
+
plate.frameW = this.canvas.width;
|
|
5452
|
+
plate.frameH = this.canvas.height;
|
|
5453
|
+
if (box.w === 0 || box.h === 0) {
|
|
5454
|
+
plate.sig = sig;
|
|
5455
|
+
plate.end = b;
|
|
5456
|
+
return true;
|
|
5457
|
+
}
|
|
5458
|
+
if (plate.canvas.width !== box.w) plate.canvas.width = box.w;
|
|
5459
|
+
if (plate.canvas.height !== box.h) plate.canvas.height = box.h;
|
|
5460
|
+
const pc = plate.ctx;
|
|
5461
|
+
pc.setTransform(1, 0, 0, 1, 0, 0);
|
|
5462
|
+
pc.globalAlpha = 1;
|
|
5463
|
+
pc.globalCompositeOperation = "source-over";
|
|
5464
|
+
pc.clearRect(0, 0, box.w, box.h);
|
|
5465
|
+
const m = this.ctx.getTransform();
|
|
5466
|
+
pc.setTransform(m.a, m.b, m.c, m.d, m.e - box.x, m.f - box.y);
|
|
5467
|
+
const self = this;
|
|
5468
|
+
const main = self.ctx;
|
|
5469
|
+
self.ctx = pc;
|
|
5470
|
+
try {
|
|
5471
|
+
for (let li = a; li <= b; li++) this.paintLayer(li, t);
|
|
5472
|
+
} finally {
|
|
5473
|
+
self.ctx = main;
|
|
5474
|
+
}
|
|
5475
|
+
plate.sig = sig;
|
|
5476
|
+
plate.end = b;
|
|
5477
|
+
}
|
|
5478
|
+
if (plate.box.w === 0 || plate.box.h === 0) return true;
|
|
5479
|
+
const ctx = this.ctx;
|
|
5480
|
+
ctx.save();
|
|
5481
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4279
5482
|
ctx.globalAlpha = 1;
|
|
4280
5483
|
ctx.globalCompositeOperation = "source-over";
|
|
5484
|
+
ctx.drawImage(plate.canvas, plate.box.x, plate.box.y);
|
|
5485
|
+
ctx.restore();
|
|
5486
|
+
return true;
|
|
4281
5487
|
}
|
|
4282
5488
|
/**
|
|
4283
5489
|
* A `transform`, in min(w,h) units (px under `units: 'px'`), about the
|
|
@@ -5129,6 +6335,7 @@ function compileSequence(spec) {
|
|
|
5129
6335
|
|
|
5130
6336
|
// src/advise.ts
|
|
5131
6337
|
import { createRng as createRng3 } from "@idle-screens/core";
|
|
6338
|
+
import { analyzeFlashes as analyzeFlashes2, srgbToLinear as srgbToLinear3 } from "@idle-screens/validator";
|
|
5132
6339
|
|
|
5133
6340
|
// src/luma.ts
|
|
5134
6341
|
function fieldRenderedRgbs(bg) {
|
|
@@ -5150,12 +6357,53 @@ function hexLuma(hex) {
|
|
|
5150
6357
|
}
|
|
5151
6358
|
function spriteLuma(layer, e) {
|
|
5152
6359
|
const s = layer.sprite;
|
|
6360
|
+
if (s.kind === "polygon" && isPathPolygon(s)) {
|
|
6361
|
+
const base = s.colors?.[e.colorIndex] ?? s.color;
|
|
6362
|
+
let sum = 0, wsum = 0;
|
|
6363
|
+
for (const part of pathParts(s)) {
|
|
6364
|
+
const pc = hexLuma(part.color ?? base);
|
|
6365
|
+
let l;
|
|
6366
|
+
if (!part.fill) l = part.stroke ? hexLuma(part.stroke.color) : pc;
|
|
6367
|
+
else if (part.gradient) {
|
|
6368
|
+
let g = 0;
|
|
6369
|
+
for (const st of part.gradient.stops) {
|
|
6370
|
+
const o = st.opacity ?? 1;
|
|
6371
|
+
g += hexLuma(st.color) * o + pc * (1 - o);
|
|
6372
|
+
}
|
|
6373
|
+
l = g / part.gradient.stops.length;
|
|
6374
|
+
} else l = pc;
|
|
6375
|
+
const w = Math.max(1e-4, part.fill ? polygonArea(part.flat.points.map(([x, y]) => ({ x, y }))) : part.box.hw * part.box.hh * 0.1) * part.opacity;
|
|
6376
|
+
sum += l * w;
|
|
6377
|
+
wsum += w;
|
|
6378
|
+
}
|
|
6379
|
+
if (wsum > 0) return sum / wsum;
|
|
6380
|
+
}
|
|
5153
6381
|
if (isShapedSprite(s)) {
|
|
5154
6382
|
return hexLuma(s.colors?.[e.colorIndex] ?? s.color);
|
|
5155
6383
|
}
|
|
5156
6384
|
if (s.kind === "text" || s.kind === "textBlock") return hexLuma(s.color ?? "#e6e8ef");
|
|
5157
6385
|
return 0.75;
|
|
5158
6386
|
}
|
|
6387
|
+
function spriteInks(layer, e) {
|
|
6388
|
+
const s = layer.sprite;
|
|
6389
|
+
if (s.kind === "polygon" && isPathPolygon(s)) {
|
|
6390
|
+
const base = s.colors?.[e.colorIndex] ?? s.color;
|
|
6391
|
+
const inks = [];
|
|
6392
|
+
for (const part of pathParts(s)) {
|
|
6393
|
+
if (part.opacity <= 0) continue;
|
|
6394
|
+
if (part.fill) {
|
|
6395
|
+
if (part.gradient) for (const st of part.gradient.stops) {
|
|
6396
|
+
if ((st.opacity ?? 1) > 0) inks.push(st.color);
|
|
6397
|
+
}
|
|
6398
|
+
else inks.push(part.color ?? base);
|
|
6399
|
+
}
|
|
6400
|
+
if (part.stroke && part.stroke.width > 0 && (part.stroke.opacity ?? 1) > 0) inks.push(part.stroke.color);
|
|
6401
|
+
}
|
|
6402
|
+
return inks;
|
|
6403
|
+
}
|
|
6404
|
+
const hex = spriteHex(layer, e);
|
|
6405
|
+
return hex === null ? null : [hex];
|
|
6406
|
+
}
|
|
5159
6407
|
function spriteHex(layer, e) {
|
|
5160
6408
|
const s = layer.sprite;
|
|
5161
6409
|
if (isShapedSprite(s)) {
|
|
@@ -5271,6 +6519,23 @@ var WASH_ALPHA_MAX = 0.45;
|
|
|
5271
6519
|
var COHESION_T = 5e3;
|
|
5272
6520
|
var OUTLINE_SAMPLES = 24;
|
|
5273
6521
|
var MAX_TRACED = 240;
|
|
6522
|
+
function discSamples(cx, cy, r, n) {
|
|
6523
|
+
const golden = Math.PI * (3 - Math.sqrt(5));
|
|
6524
|
+
return Array.from({ length: n }, (_, i) => {
|
|
6525
|
+
const rr = r * Math.sqrt((i + 0.5) / n);
|
|
6526
|
+
return { x: cx + rr * Math.cos(i * golden), y: cy + rr * Math.sin(i * golden) };
|
|
6527
|
+
});
|
|
6528
|
+
}
|
|
6529
|
+
function gridSamples(uw, vh, n, map) {
|
|
6530
|
+
if (!(uw > 0) || !(vh > 0)) return [];
|
|
6531
|
+
const nv = Math.min(n, Math.max(8, Math.round(Math.sqrt(n * vh / uw))));
|
|
6532
|
+
const nu = Math.max(8, Math.round(n / nv));
|
|
6533
|
+
const out = [];
|
|
6534
|
+
for (let i = 0; i < nu; i++) {
|
|
6535
|
+
for (let j = 0; j < nv; j++) out.push(map(((i + 0.5) / nu - 0.5) * uw, ((j + 0.5) / nv - 0.5) * vh));
|
|
6536
|
+
}
|
|
6537
|
+
return out;
|
|
6538
|
+
}
|
|
5274
6539
|
var EDGE_INSET = 1e-4;
|
|
5275
6540
|
function circleShape(cx, cy, r) {
|
|
5276
6541
|
return {
|
|
@@ -5279,6 +6544,7 @@ function circleShape(cx, cy, r) {
|
|
|
5279
6544
|
br: r,
|
|
5280
6545
|
area: Math.PI * r * r,
|
|
5281
6546
|
contains: (x, y) => (x - cx) * (x - cx) + (y - cy) * (y - cy) < r * r,
|
|
6547
|
+
interior: (n) => discSamples(cx, cy, r, n),
|
|
5282
6548
|
outline: (n) => Array.from({ length: n }, (_, i) => {
|
|
5283
6549
|
const a = i / n * Math.PI * 2;
|
|
5284
6550
|
const inner = r * (1 - EDGE_INSET);
|
|
@@ -5294,7 +6560,7 @@ function polyShape(cx, cy, raw) {
|
|
|
5294
6560
|
if (abs.length < 3) return null;
|
|
5295
6561
|
let br = 0;
|
|
5296
6562
|
for (const p of abs) br = Math.max(br, Math.hypot(p.x - cx, p.y - cy));
|
|
5297
|
-
const
|
|
6563
|
+
const inside2 = (x, y) => {
|
|
5298
6564
|
let hit = false;
|
|
5299
6565
|
for (let i = 0, j = abs.length - 1; i < abs.length; j = i++) {
|
|
5300
6566
|
const a = abs[i];
|
|
@@ -5312,12 +6578,21 @@ function polyShape(cx, cy, raw) {
|
|
|
5312
6578
|
const step = Math.min(br * EDGE_INSET, shortest * 0.25);
|
|
5313
6579
|
const area = polygonArea(abs);
|
|
5314
6580
|
if (!(area > 0)) return null;
|
|
6581
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
6582
|
+
for (const p of abs) {
|
|
6583
|
+
x0 = Math.min(x0, p.x);
|
|
6584
|
+
y0 = Math.min(y0, p.y);
|
|
6585
|
+
x1 = Math.max(x1, p.x);
|
|
6586
|
+
y1 = Math.max(y1, p.y);
|
|
6587
|
+
}
|
|
5315
6588
|
return {
|
|
5316
6589
|
cx,
|
|
5317
6590
|
cy,
|
|
5318
6591
|
br,
|
|
5319
6592
|
area,
|
|
5320
|
-
contains:
|
|
6593
|
+
contains: inside2,
|
|
6594
|
+
// Its bounding box's grid, kept to the cells inside the outline.
|
|
6595
|
+
interior: (n) => gridSamples(x1 - x0, y1 - y0, n, (u, v) => ({ x: (x0 + x1) / 2 + u, y: (y0 + y1) / 2 + v })).filter((p) => inside2(p.x, p.y)),
|
|
5321
6596
|
/**
|
|
5322
6597
|
* `n` points spread along the perimeter by ARC LENGTH, not one share per
|
|
5323
6598
|
* edge — a long skinny rectangle has two edges carrying almost all of its
|
|
@@ -5346,13 +6621,13 @@ function polyShape(cx, cy, raw) {
|
|
|
5346
6621
|
for (let t = step; t > 0; t /= 8) {
|
|
5347
6622
|
const qx1 = px - uy * t;
|
|
5348
6623
|
const qy1 = py + ux * t;
|
|
5349
|
-
if (
|
|
6624
|
+
if (inside2(qx1, qy1)) {
|
|
5350
6625
|
out.push({ x: qx1, y: qy1 });
|
|
5351
6626
|
break;
|
|
5352
6627
|
}
|
|
5353
6628
|
const qx2 = px + uy * t;
|
|
5354
6629
|
const qy2 = py - ux * t;
|
|
5355
|
-
if (
|
|
6630
|
+
if (inside2(qx2, qy2)) {
|
|
5356
6631
|
out.push({ x: qx2, y: qy2 });
|
|
5357
6632
|
break;
|
|
5358
6633
|
}
|
|
@@ -5367,12 +6642,13 @@ function rectShape(cx, cy, halfW, halfH, rot) {
|
|
|
5367
6642
|
const cos = Math.cos(rot);
|
|
5368
6643
|
const sin = Math.sin(rot);
|
|
5369
6644
|
const corner = (lx, ly) => ({ x: cx + lx * cos - ly * sin, y: cy + lx * sin + ly * cos });
|
|
5370
|
-
|
|
6645
|
+
const shape = polyShape(cx, cy, [
|
|
5371
6646
|
corner(-halfW, -halfH),
|
|
5372
6647
|
corner(halfW, -halfH),
|
|
5373
6648
|
corner(halfW, halfH),
|
|
5374
6649
|
corner(-halfW, halfH)
|
|
5375
6650
|
]);
|
|
6651
|
+
return shape && { ...shape, interior: (n) => gridSamples(halfW * 2, halfH * 2, n, corner) };
|
|
5376
6652
|
}
|
|
5377
6653
|
function polygonShape(cx, cy, pts, rot) {
|
|
5378
6654
|
const cos = Math.cos(rot);
|
|
@@ -5432,8 +6708,7 @@ function cohesionOf(layers, t, w, h) {
|
|
|
5432
6708
|
seamCause
|
|
5433
6709
|
};
|
|
5434
6710
|
if (lifeAlphaAt(layer.life, t) <= 0) return { ...base, overlap: null, reads: null };
|
|
5435
|
-
const
|
|
5436
|
-
const solid = shapes.filter((x) => x !== null && x.area > 0);
|
|
6711
|
+
const solid = solidShapes(layer, entities, t, w, h);
|
|
5437
6712
|
if (solid.length < 2) return { ...base, overlap: null, reads: null };
|
|
5438
6713
|
const stride = Math.max(1, Math.ceil(solid.length / MAX_TRACED));
|
|
5439
6714
|
let covered = 0;
|
|
@@ -5465,16 +6740,58 @@ function cohesionOf(layers, t, w, h) {
|
|
|
5465
6740
|
return { ...base, overlap, reads };
|
|
5466
6741
|
});
|
|
5467
6742
|
}
|
|
5468
|
-
function
|
|
6743
|
+
function solidShapes(layer, entities, t, w, h) {
|
|
6744
|
+
return entities.map((e) => shapeOf(layer, e, t, w, h)).filter((x) => x !== null && x.area > 0);
|
|
6745
|
+
}
|
|
6746
|
+
var AREA_SAMPLES = 128;
|
|
6747
|
+
var TILED_AREA_MAX = 0.15;
|
|
6748
|
+
function buriedAreaOf(layer, entities, t, w, h) {
|
|
6749
|
+
if (lifeAlphaAt(layer.life, t) <= 0) return null;
|
|
6750
|
+
const solid = solidShapes(layer, entities, t, w, h);
|
|
6751
|
+
if (solid.length < 2) return null;
|
|
6752
|
+
const stride = Math.max(1, Math.ceil(solid.length / MAX_TRACED));
|
|
6753
|
+
let covered = 0;
|
|
6754
|
+
let traced = 0;
|
|
6755
|
+
for (let i = 0; i < solid.length; i += stride) {
|
|
6756
|
+
const pts = solid[i].interior(AREA_SAMPLES);
|
|
6757
|
+
if (pts.length === 0) continue;
|
|
6758
|
+
let hit = 0;
|
|
6759
|
+
for (const p of pts) {
|
|
6760
|
+
for (let j = 0; j < solid.length; j++) {
|
|
6761
|
+
if (j === i) continue;
|
|
6762
|
+
const other = solid[j];
|
|
6763
|
+
const dx = p.x - other.cx;
|
|
6764
|
+
const dy = p.y - other.cy;
|
|
6765
|
+
if (dx * dx + dy * dy > other.br * other.br) continue;
|
|
6766
|
+
if (other.contains(p.x, p.y)) {
|
|
6767
|
+
hit++;
|
|
6768
|
+
break;
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
covered += hit / pts.length;
|
|
6773
|
+
traced++;
|
|
6774
|
+
}
|
|
6775
|
+
return traced === 0 ? null : Number((covered / traced).toFixed(4));
|
|
6776
|
+
}
|
|
6777
|
+
function seamsWorthWarning(layer, c, entityCount, buriedArea) {
|
|
5469
6778
|
if (c.reads !== "seamed" || entityCount < 6) return false;
|
|
5470
6779
|
const [lo, hi] = layer.alpha ?? [1, 1];
|
|
5471
6780
|
if ((lo + hi) / 2 < WASH_ALPHA_MAX) return false;
|
|
5472
|
-
|
|
6781
|
+
if (layer.blend === "lighter" || layer.blend === "screen") return false;
|
|
6782
|
+
if (buriedArea && paletteIsTheOnlySeam(layer)) {
|
|
6783
|
+
const a = buriedArea();
|
|
6784
|
+
if (a !== null && a < TILED_AREA_MAX) return false;
|
|
6785
|
+
}
|
|
6786
|
+
return true;
|
|
6787
|
+
}
|
|
6788
|
+
function paletteIsTheOnlySeam(layer) {
|
|
6789
|
+
if (seamCauseOf(layer) === null) return false;
|
|
6790
|
+
return seamCauseOf({ ...layer, sprite: { ...layer.sprite, colors: void 0 } }) === null;
|
|
5473
6791
|
}
|
|
5474
6792
|
|
|
5475
6793
|
// src/perceive.ts
|
|
5476
6794
|
import { createRng as createRng2 } from "@idle-screens/core";
|
|
5477
|
-
var GLOW_SPREAD = 2.4;
|
|
5478
6795
|
var LINE_SALIENCE = 3.5;
|
|
5479
6796
|
var GHOST_TAPS = 12;
|
|
5480
6797
|
var POLYGON_OUTLINE_CELLS = 3;
|
|
@@ -5509,11 +6826,18 @@ function posOf(scene, e, t) {
|
|
|
5509
6826
|
const map = scene.transforms?.get(e);
|
|
5510
6827
|
return map ? map(p) : p;
|
|
5511
6828
|
}
|
|
5512
|
-
function
|
|
6829
|
+
function fontScaleFor(spec, w, h) {
|
|
6830
|
+
return spec.units === "px" ? 1 : Math.min(w, h) / (spec.referenceViewport ?? LIMITS.referenceViewport);
|
|
6831
|
+
}
|
|
6832
|
+
function textFontPx(s, size, fontScale) {
|
|
5513
6833
|
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
5514
|
-
|
|
6834
|
+
return m ? Number(m[1]) * fontScale : size;
|
|
6835
|
+
}
|
|
6836
|
+
function textBox(s, e, p, fontScale, unitScale) {
|
|
6837
|
+
const fh = textFontPx(s, e.size, fontScale);
|
|
5515
6838
|
const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
5516
|
-
const
|
|
6839
|
+
const est = Math.max(fh, 0.62 * fh * Math.min(str.length, 48));
|
|
6840
|
+
const fw = s.maxWidth ? Math.min(est, s.maxWidth * unitScale) : est;
|
|
5517
6841
|
const align = s.align ?? "center";
|
|
5518
6842
|
const baseline = s.baseline ?? "middle";
|
|
5519
6843
|
return {
|
|
@@ -5559,7 +6883,24 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
5559
6883
|
}
|
|
5560
6884
|
return rs.progress;
|
|
5561
6885
|
}
|
|
5562
|
-
function
|
|
6886
|
+
function gradientAt(gr, u, v) {
|
|
6887
|
+
let t;
|
|
6888
|
+
if (gr.type === "linear") {
|
|
6889
|
+
const dx = gr.to[0] - gr.from[0], dy = gr.to[1] - gr.from[1];
|
|
6890
|
+
const len2 = dx * dx + dy * dy || 1;
|
|
6891
|
+
t = ((u - gr.from[0]) * dx + (v - gr.from[1]) * dy) / len2;
|
|
6892
|
+
} else {
|
|
6893
|
+
t = Math.hypot(u - gr.center[0], v - gr.center[1]) / gr.radius;
|
|
6894
|
+
}
|
|
6895
|
+
const st = gr.stops;
|
|
6896
|
+
t = Math.max(st[0].at, Math.min(st[st.length - 1].at, t));
|
|
6897
|
+
let k = 0;
|
|
6898
|
+
while (k < st.length - 2 && t > st[k + 1].at) k++;
|
|
6899
|
+
const A = st[k], B = st[Math.min(k + 1, st.length - 1)];
|
|
6900
|
+
const w = B.at > A.at ? (t - A.at) / (B.at - A.at) : 0;
|
|
6901
|
+
return { luma: hexLuma(A.color) * (1 - w) + hexLuma(B.color) * w, opacity: (A.opacity ?? 1) * (1 - w) + (B.opacity ?? 1) * w };
|
|
6902
|
+
}
|
|
6903
|
+
function polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1, evenodd = false) {
|
|
5563
6904
|
const nc = c1 - c0 + 1;
|
|
5564
6905
|
const cov = new Float64Array(nc * (r1 - r0 + 1));
|
|
5565
6906
|
const xMin = c0 * cellW;
|
|
@@ -5592,30 +6933,52 @@ function polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1) {
|
|
|
5592
6933
|
hits.sort((m, n) => m.x - n.x);
|
|
5593
6934
|
let wind = 0;
|
|
5594
6935
|
let start = 0;
|
|
6936
|
+
const filled = (w) => evenodd ? (w & 1) !== 0 : w !== 0;
|
|
5595
6937
|
for (const hit of hits) {
|
|
5596
|
-
const before = wind;
|
|
6938
|
+
const before = filled(wind);
|
|
5597
6939
|
wind += hit.dir;
|
|
5598
|
-
|
|
5599
|
-
|
|
6940
|
+
const after = filled(wind);
|
|
6941
|
+
if (!before && after) start = hit.x;
|
|
6942
|
+
else if (before && !after) addSpan(r - r0, start, hit.x);
|
|
5600
6943
|
}
|
|
5601
6944
|
}
|
|
5602
6945
|
}
|
|
5603
6946
|
return cov;
|
|
5604
6947
|
}
|
|
5605
|
-
function
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
6948
|
+
function softProfile(u) {
|
|
6949
|
+
if (u <= 0) return 1;
|
|
6950
|
+
if (u >= 1) return 0;
|
|
6951
|
+
return u <= 0.35 ? 1 - 0.25 * u / 0.35 : 0.75 * (1 - (u - 0.35) / 0.65);
|
|
6952
|
+
}
|
|
6953
|
+
var SOFT_DISC_MEAN = 0.3783;
|
|
6954
|
+
function discCellInk(cx, cy, rOuter, rInner, soft, x0, y0, cw, ch) {
|
|
6955
|
+
const nx = Math.max(x0, Math.min(cx, x0 + cw)) - cx;
|
|
6956
|
+
const ny = Math.max(y0, Math.min(cy, y0 + ch)) - cy;
|
|
6957
|
+
const near = Math.hypot(nx, ny);
|
|
6958
|
+
const fx = Math.max(Math.abs(x0 - cx), Math.abs(x0 + cw - cx));
|
|
6959
|
+
const fy = Math.max(Math.abs(y0 - cy), Math.abs(y0 + ch - cy));
|
|
6960
|
+
const far = Math.hypot(fx, fy);
|
|
6961
|
+
if (near >= rOuter || far <= rInner) return 0;
|
|
6962
|
+
if (far <= rOuter && near >= rInner) return soft ? softProfile(Math.hypot(x0 + cw / 2 - cx, y0 + ch / 2 - cy) / rOuter) : 1;
|
|
6963
|
+
if (cx - rOuter >= x0 && cx + rOuter <= x0 + cw && cy - rOuter >= y0 && cy + rOuter <= y0 + ch) {
|
|
6964
|
+
return Math.PI * (soft ? rOuter * rOuter * SOFT_DISC_MEAN : rOuter * rOuter - rInner * rInner) / (cw * ch);
|
|
6965
|
+
}
|
|
6966
|
+
const bx0 = Math.max(x0, cx - rOuter);
|
|
6967
|
+
const by0 = Math.max(y0, cy - rOuter);
|
|
6968
|
+
const sx = Math.min(x0 + cw, cx + rOuter) - bx0;
|
|
6969
|
+
const sy = Math.min(y0 + ch, cy + rOuter) - by0;
|
|
6970
|
+
if (sx <= 0 || sy <= 0) return 0;
|
|
6971
|
+
const step = Math.max(rOuter / 4, 1e-6);
|
|
6972
|
+
const ni = Math.max(4, Math.min(16, Math.ceil(sx / step)));
|
|
6973
|
+
const nj = Math.max(4, Math.min(16, Math.ceil(sy / step)));
|
|
6974
|
+
let ink = 0;
|
|
6975
|
+
for (let j = 0; j < nj; j++) {
|
|
6976
|
+
for (let i = 0; i < ni; i++) {
|
|
6977
|
+
const d = Math.hypot(bx0 + (i + 0.5) * sx / ni - cx, by0 + (j + 0.5) * sy / nj - cy);
|
|
6978
|
+
if (d <= rOuter && d >= rInner) ink += soft ? softProfile(d / rOuter) : 1;
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6981
|
+
return ink / (ni * nj) * sx * sy / (cw * ch);
|
|
5619
6982
|
}
|
|
5620
6983
|
function luminanceGrid(spec, opts = {}) {
|
|
5621
6984
|
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
@@ -5712,11 +7075,14 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5712
7075
|
const r1 = Math.min(rows - 1, Math.floor((pos.y + rad) / cellH));
|
|
5713
7076
|
for (let r = r0; r <= r1; r++) {
|
|
5714
7077
|
for (let c = c0; c <= c1; c++) {
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
7078
|
+
let wgt = 1;
|
|
7079
|
+
if (soft) {
|
|
7080
|
+
wgt = discCellInk(pos.x, pos.y, rad, 0, true, c * cellW, r * cellH, cellW, cellH);
|
|
7081
|
+
if (wgt <= 0) continue;
|
|
7082
|
+
} else {
|
|
7083
|
+
const d = Math.hypot((c + 0.5) * cellW - pos.x, (r + 0.5) * cellH - pos.y);
|
|
7084
|
+
if (d > rad + Math.min(cellW, cellH) / 2) continue;
|
|
7085
|
+
}
|
|
5720
7086
|
compose(r * cols + c, lum, Math.min(1, a * wgt), layer.blend);
|
|
5721
7087
|
}
|
|
5722
7088
|
}
|
|
@@ -5724,20 +7090,15 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5724
7090
|
};
|
|
5725
7091
|
const splatPolygonOutline = (layer, s, e, p, sz, a, lum, tPass) => {
|
|
5726
7092
|
const radius = sz / 2;
|
|
5727
|
-
const rot = rotationAt(e, tPass);
|
|
7093
|
+
const rot = rotationAt(e, tPass) + paintRotation(spec, layer);
|
|
5728
7094
|
const cr = Math.cos(rot);
|
|
5729
7095
|
const sr = Math.sin(rot);
|
|
5730
|
-
const
|
|
5731
|
-
const
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
const n = pts[(i + 1) % pts.length];
|
|
5737
|
-
return acc + Math.hypot(n.x - q.x, n.y - q.y);
|
|
5738
|
-
}, 0);
|
|
5739
|
-
band = perimeter > 0 ? (GLOW_SPREAD - 1) * (2 * polygonArea(pts) / perimeter) : 0;
|
|
5740
|
-
}
|
|
7096
|
+
const fr = isPathPolygon(s) ? pathFrame(s, radius, w, h) : { sx: radius, sy: radius, ox: 0, oy: 0, uniform: true };
|
|
7097
|
+
const mapUnit = (u, v) => {
|
|
7098
|
+
const qx = (u - fr.ox) * fr.sx, qy = (v - fr.oy) * fr.sy;
|
|
7099
|
+
return { x: p.x + qx * cr - qy * sr, y: p.y + qx * sr + qy * cr };
|
|
7100
|
+
};
|
|
7101
|
+
const pts = isPathPolygon(s) ? flattenSprite(s).points.map(([u, v]) => mapUnit(u, v)) : polygonPoints(s, radius).map((q) => ({ x: p.x + q.x * cr - q.y * sr, y: p.y + q.x * sr + q.y * cr }));
|
|
5741
7102
|
let minX = Infinity;
|
|
5742
7103
|
let maxX = -Infinity;
|
|
5743
7104
|
let minY = Infinity;
|
|
@@ -5748,34 +7109,97 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5748
7109
|
minY = Math.min(minY, q.y);
|
|
5749
7110
|
maxY = Math.max(maxY, q.y);
|
|
5750
7111
|
}
|
|
5751
|
-
const
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
7112
|
+
const strokeQuadsPx = (part) => {
|
|
7113
|
+
if (!part.stroke) return [];
|
|
7114
|
+
const hw = part.stroke.width * radius / 2;
|
|
7115
|
+
const out = [];
|
|
7116
|
+
for (const l of part.flat.lines) {
|
|
7117
|
+
const m = l.points.map(([u, v]) => mapUnit(u, v));
|
|
7118
|
+
const n = l.closed ? m.length : m.length - 1;
|
|
7119
|
+
for (let i = 0; i < n; i++) {
|
|
7120
|
+
const A = m[i], B = m[(i + 1) % m.length];
|
|
7121
|
+
const dx = B.x - A.x, dy = B.y - A.y, len = Math.hypot(dx, dy) || 1;
|
|
7122
|
+
const nx = -dy / len * hw, ny = dx / len * hw;
|
|
7123
|
+
out.push([{ x: A.x + nx, y: A.y + ny }, { x: B.x + nx, y: B.y + ny }, { x: B.x - nx, y: B.y - ny }, { x: A.x - nx, y: A.y - ny }]);
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
return out;
|
|
7127
|
+
};
|
|
7128
|
+
if (isPathPolygon(s)) {
|
|
7129
|
+
for (const part of pathParts(s)) for (const quad of strokeQuadsPx(part)) for (const q of quad) {
|
|
7130
|
+
minX = Math.min(minX, q.x);
|
|
7131
|
+
maxX = Math.max(maxX, q.x);
|
|
7132
|
+
minY = Math.min(minY, q.y);
|
|
7133
|
+
maxY = Math.max(maxY, q.y);
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
const c0 = Math.max(0, Math.floor(minX / cellW));
|
|
7137
|
+
const c1 = Math.min(cols - 1, Math.floor(maxX / cellW));
|
|
7138
|
+
const r0 = Math.max(0, Math.floor(minY / cellH));
|
|
7139
|
+
const r1 = Math.min(rows - 1, Math.floor(maxY / cellH));
|
|
5755
7140
|
if (c0 > c1 || r0 > r1) return;
|
|
5756
|
-
const cov = polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1);
|
|
5757
7141
|
const nc = c1 - c0 + 1;
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
7142
|
+
const unitAt = (x, y) => {
|
|
7143
|
+
const dx = x - p.x, dy = y - p.y;
|
|
7144
|
+
return [(dx * cr + dy * sr) / Math.max(fr.sx, 1e-6) + fr.ox, (-dx * sr + dy * cr) / Math.max(fr.sy, 1e-6) + fr.oy];
|
|
7145
|
+
};
|
|
7146
|
+
const parts = isPathPolygon(s) ? pathParts(s) : null;
|
|
7147
|
+
const fillPass = (ring, rule, partLum, partA, part) => {
|
|
7148
|
+
const cov = polygonCellCoverage(ring, cellW, cellH, c0, c1, r0, r1, rule);
|
|
7149
|
+
for (let r = r0; r <= r1; r++) {
|
|
7150
|
+
for (let c = c0; c <= c1; c++) {
|
|
7151
|
+
const f = cov[(r - r0) * nc + (c - c0)];
|
|
7152
|
+
if (!(f > 0)) continue;
|
|
7153
|
+
const x = (c + 0.5) * cellW;
|
|
7154
|
+
const y = (r + 0.5) * cellH;
|
|
7155
|
+
let cl = partLum, ca = 1;
|
|
7156
|
+
if (part?.gradient) {
|
|
7157
|
+
const [u, v] = unitAt(x, y);
|
|
7158
|
+
const [gu, gv] = gradientSpace(part, u, v);
|
|
7159
|
+
const g = gradientAt(part.gradient, gu, gv);
|
|
7160
|
+
cl = g.luma;
|
|
7161
|
+
ca = g.opacity;
|
|
7162
|
+
}
|
|
7163
|
+
const wgt = s.soft && !parts ? f * softProfile(Math.hypot(x - p.x, y - p.y) / Math.max(radius, 1e-6)) : f;
|
|
7164
|
+
if (wgt > 0) compose(r * cols + c, cl, a * partA * ca * Math.min(1, wgt), layer.blend);
|
|
7165
|
+
}
|
|
7166
|
+
}
|
|
7167
|
+
};
|
|
7168
|
+
if (!parts) {
|
|
7169
|
+
fillPass(pts, polygonFillRule(s) === "evenodd", lum, 1, null);
|
|
7170
|
+
return;
|
|
7171
|
+
}
|
|
7172
|
+
for (const part of parts) {
|
|
7173
|
+
const partLum = part.color ? hexLuma(part.color) : lum;
|
|
7174
|
+
if (part.fill && part.flat.points.length >= 3) {
|
|
7175
|
+
fillPass(part.flat.points.map(([u, v]) => mapUnit(u, v)), part.rule === "evenodd", partLum, part.opacity, part);
|
|
7176
|
+
}
|
|
7177
|
+
if (part.stroke) {
|
|
7178
|
+
const sa = (part.stroke.opacity ?? 1) * part.opacity;
|
|
7179
|
+
const sl = hexLuma(part.stroke.color);
|
|
7180
|
+
const scov = new Float64Array(nc * (r1 - r0 + 1));
|
|
7181
|
+
for (const qp of strokeQuadsPx(part)) {
|
|
7182
|
+
let qx0 = Infinity, qx1 = -Infinity, qy0 = Infinity, qy1 = -Infinity;
|
|
7183
|
+
for (const q of qp) {
|
|
7184
|
+
qx0 = Math.min(qx0, q.x);
|
|
7185
|
+
qx1 = Math.max(qx1, q.x);
|
|
7186
|
+
qy0 = Math.min(qy0, q.y);
|
|
7187
|
+
qy1 = Math.max(qy1, q.y);
|
|
7188
|
+
}
|
|
7189
|
+
const a0 = Math.max(c0, Math.floor(qx0 / cellW)), a1 = Math.min(c1, Math.floor(qx1 / cellW));
|
|
7190
|
+
const b0 = Math.max(r0, Math.floor(qy0 / cellH)), b1 = Math.min(r1, Math.floor(qy1 / cellH));
|
|
7191
|
+
if (a0 > a1 || b0 > b1) continue;
|
|
7192
|
+
const qc = polygonCellCoverage(qp, cellW, cellH, a0, a1, b0, b1);
|
|
7193
|
+
const qn = a1 - a0 + 1;
|
|
7194
|
+
for (let r = b0; r <= b1; r++) for (let c = a0; c <= a1; c++) {
|
|
7195
|
+
const i = (r - r0) * nc + (c - c0);
|
|
7196
|
+
scov[i] = Math.min(1, scov[i] + qc[(r - b0) * qn + (c - a0)]);
|
|
5772
7197
|
}
|
|
5773
|
-
} else if (s.soft) {
|
|
5774
|
-
wgt = f * Math.max(0.1, 1 - Math.hypot(x - p.x, y - p.y) / Math.max(radius, 1e-6));
|
|
5775
|
-
} else {
|
|
5776
|
-
wgt = f;
|
|
5777
7198
|
}
|
|
5778
|
-
|
|
7199
|
+
for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) {
|
|
7200
|
+
const f = scov[(r - r0) * nc + (c - c0)];
|
|
7201
|
+
if (f > 0) compose(r * cols + c, sl, a * sa * f, layer.blend);
|
|
7202
|
+
}
|
|
5779
7203
|
}
|
|
5780
7204
|
}
|
|
5781
7205
|
};
|
|
@@ -5791,13 +7215,13 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5791
7215
|
const p = posOf(scene, e, tPass);
|
|
5792
7216
|
const sz = layer.transform || layer.group !== void 0 ? sizeAt(e, tPass) * paintSize(spec, layer) : sizeAt(e, tPass);
|
|
5793
7217
|
const s = layer.sprite;
|
|
5794
|
-
if (s.kind === "polygon" && sz >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH)) {
|
|
7218
|
+
if (s.kind === "polygon" && (sz >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH) || isPathPolygon(s) && ((s.span ?? "box") !== "box" || s.origin !== void 0))) {
|
|
5795
7219
|
splatPolygonOutline(layer, s, e, p, sz, a, lum, tPass);
|
|
5796
7220
|
continue;
|
|
5797
7221
|
}
|
|
5798
7222
|
if (s.kind === "stroke") {
|
|
5799
7223
|
const pts = strokeSamples(s, sz / 2);
|
|
5800
|
-
let angle = rotationAt(e, tPass);
|
|
7224
|
+
let angle = rotationAt(e, tPass) + paintRotation(spec, layer);
|
|
5801
7225
|
if (s.orient) angle += headingAt(e, tPass, w, h) ?? 0;
|
|
5802
7226
|
const ca = Math.cos(angle);
|
|
5803
7227
|
const sa = Math.sin(angle);
|
|
@@ -5826,7 +7250,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5826
7250
|
continue;
|
|
5827
7251
|
}
|
|
5828
7252
|
if (s.kind === "streak") {
|
|
5829
|
-
const heading = (headingAt(e, tPass, w, h) ?? 0) + rotationAt(e, tPass);
|
|
7253
|
+
const heading = (headingAt(e, tPass, w, h) ?? 0) + (rotationAt(e, tPass) + paintRotation(spec, layer));
|
|
5830
7254
|
const steps = Math.max(2, Math.ceil(sz / Math.min(cellW, cellH)));
|
|
5831
7255
|
const wgt = Math.min(1, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale / cellH);
|
|
5832
7256
|
for (let i = 0; i <= steps; i++) {
|
|
@@ -5845,7 +7269,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5845
7269
|
let halfX = sz / 2;
|
|
5846
7270
|
let halfY = s.kind === "rect" ? (e.size2 ?? sz) * (e.size > 0 ? sz / e.size : 1) / 2 : sz / 2;
|
|
5847
7271
|
if (s.kind === "text") {
|
|
5848
|
-
const box = textBox(s, e, p);
|
|
7272
|
+
const box = textBox(s, e, p, fontScaleFor(spec, w, h), scale);
|
|
5849
7273
|
centerX = box.cx;
|
|
5850
7274
|
centerY = box.cy;
|
|
5851
7275
|
halfX = box.halfX;
|
|
@@ -5861,7 +7285,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5861
7285
|
if (len <= 0) continue;
|
|
5862
7286
|
const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
|
|
5863
7287
|
const box = barBox(s.direction ?? "right", len, th);
|
|
5864
|
-
const rot = rotationAt(e, tPass);
|
|
7288
|
+
const rot = rotationAt(e, tPass) + paintRotation(spec, layer);
|
|
5865
7289
|
const cr = Math.cos(rot);
|
|
5866
7290
|
const sr = Math.sin(rot);
|
|
5867
7291
|
centerX = p.x + box.cx * cr - box.cy * sr;
|
|
@@ -5872,45 +7296,24 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5872
7296
|
const circular = s.kind === "circle" || s.kind === "ring" || s.kind === "polygon";
|
|
5873
7297
|
const soft = (s.kind === "circle" || s.kind === "polygon") && !!s.soft;
|
|
5874
7298
|
const shapeWeight = s.kind === "polygon" ? polygonFill(s, sz / 2) : s.kind === "rect" && s.feather ? (1 - s.feather / 2) * (1 - s.feather / 2) : 1;
|
|
5875
|
-
const
|
|
5876
|
-
const
|
|
5877
|
-
const
|
|
5878
|
-
const
|
|
5879
|
-
const c0 = Math.max(0, Math.floor((centerX - reachX) / cellW));
|
|
5880
|
-
const c1 = Math.min(cols - 1, Math.floor((centerX + reachX) / cellW));
|
|
5881
|
-
const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
|
|
5882
|
-
const r1 = Math.min(rows - 1, Math.floor((centerY + reachY) / cellH));
|
|
7299
|
+
const c0 = Math.max(0, Math.floor((centerX - halfX) / cellW));
|
|
7300
|
+
const c1 = Math.min(cols - 1, Math.floor((centerX + halfX) / cellW));
|
|
7301
|
+
const r0 = Math.max(0, Math.floor((centerY - halfY) / cellH));
|
|
7302
|
+
const r1 = Math.min(rows - 1, Math.floor((centerY + halfY) / cellH));
|
|
5883
7303
|
const inkWeight = (s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1) * (s.kind === "textBlock" ? textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1) : 1);
|
|
5884
|
-
const byArea = s.kind === "rect" || s.kind === "bar";
|
|
7304
|
+
const byArea = s.kind === "rect" || s.kind === "bar" || s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock";
|
|
5885
7305
|
for (let r = r0; r <= r1; r++) {
|
|
5886
7306
|
const fy = byArea ? Math.max(0, Math.min((r + 1) * cellH, centerY + halfY) - Math.max(r * cellH, centerY - halfY)) / cellH : 1;
|
|
5887
7307
|
if (fy <= 0) continue;
|
|
5888
7308
|
for (let c = c0; c <= c1; c++) {
|
|
5889
|
-
const dx = (c + 0.5) * cellW - centerX;
|
|
5890
|
-
const dy = (r + 0.5) * cellH - centerY;
|
|
5891
7309
|
if (circular) {
|
|
5892
|
-
const
|
|
5893
|
-
const
|
|
5894
|
-
if (
|
|
5895
|
-
if (s.kind === "ring") {
|
|
5896
|
-
const strokeW = (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale;
|
|
5897
|
-
if (d < halfX - strokeW) continue;
|
|
5898
|
-
}
|
|
5899
|
-
let wgt;
|
|
5900
|
-
if (glow) {
|
|
5901
|
-
const core = halfX;
|
|
5902
|
-
if (d <= core) wgt = 1;
|
|
5903
|
-
else {
|
|
5904
|
-
const f = Math.max(0, 1 - (d - core) / Math.max(reachX - core, 1e-6));
|
|
5905
|
-
wgt = f * f;
|
|
5906
|
-
}
|
|
5907
|
-
} else {
|
|
5908
|
-
wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
|
|
5909
|
-
}
|
|
7310
|
+
const inner = s.kind === "ring" ? Math.max(0, halfX - (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale) : 0;
|
|
7311
|
+
const wgt = discCellInk(centerX, centerY, halfX, inner, soft, c * cellW, r * cellH, cellW, cellH);
|
|
7312
|
+
if (wgt <= 0) continue;
|
|
5910
7313
|
compose(r * cols + c, lum, a * wgt * shapeWeight, layer.blend);
|
|
5911
7314
|
} else if (byArea) {
|
|
5912
7315
|
const fx = Math.max(0, Math.min((c + 1) * cellW, centerX + halfX) - Math.max(c * cellW, centerX - halfX)) / cellW;
|
|
5913
|
-
if (fx > 0) compose(r * cols + c, lum, a * shapeWeight * fx * fy, layer.blend);
|
|
7316
|
+
if (fx > 0) compose(r * cols + c, lum, a * inkWeight * shapeWeight * fx * fy, layer.blend);
|
|
5914
7317
|
} else {
|
|
5915
7318
|
compose(r * cols + c, lum, a * inkWeight * shapeWeight, layer.blend);
|
|
5916
7319
|
}
|
|
@@ -5919,9 +7322,8 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5919
7322
|
}
|
|
5920
7323
|
if (layer.links) {
|
|
5921
7324
|
const positions = entities.map((e) => posOf(scene, e, tPass));
|
|
5922
|
-
const maxDistPx = layer.links.maxDist * scale;
|
|
5923
7325
|
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
5924
|
-
const edges = linkEdges(layer.links, positions,
|
|
7326
|
+
const edges = linkEdges(layer.links, positions, linkMaxDistPx(layer.links, scale), layer.wrap !== false && motionWraps, w, h);
|
|
5925
7327
|
const la = Math.min(1, (layer.links.alpha ?? 0.6) * lifeA);
|
|
5926
7328
|
const lum = layer.links.color ? hexLuma(layer.links.color) : 0.7;
|
|
5927
7329
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
@@ -6062,8 +7464,7 @@ function textSprites(spec, opts = {}) {
|
|
|
6062
7464
|
}
|
|
6063
7465
|
if (layer.sprite.kind !== "text") return;
|
|
6064
7466
|
const s = layer.sprite;
|
|
6065
|
-
const
|
|
6066
|
-
const sizePx = m ? Number(m[1]) : entities[0] ? sizeAt(entities[0], t) : 0;
|
|
7467
|
+
const sizePx = textFontPx(s, entities[0] ? sizeAt(entities[0], t) : 0, fontScaleFor(spec, scene.w, scene.h));
|
|
6067
7468
|
out.push({ layerIndex, key: layer.key, strings: [...s.strings], count: entities.length, sizePx: Math.round(sizePx) });
|
|
6068
7469
|
});
|
|
6069
7470
|
return out;
|
|
@@ -6083,6 +7484,42 @@ function rankDominance(raw) {
|
|
|
6083
7484
|
factors: r.factors
|
|
6084
7485
|
}));
|
|
6085
7486
|
}
|
|
7487
|
+
function onScreenFraction(scene, s, e, sz, k, t, paintRot = 0) {
|
|
7488
|
+
if (s.kind === "text" || s.kind === "textBlock") return 1;
|
|
7489
|
+
const { w, h } = scene;
|
|
7490
|
+
const p = posOf(scene, e, t);
|
|
7491
|
+
let cx = p.x, cy = p.y, hx = sz / 2 * k, hy = hx;
|
|
7492
|
+
if (s.kind === "bar") {
|
|
7493
|
+
const len = sz * barFraction(s, e.barIndex ?? 0);
|
|
7494
|
+
const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
|
|
7495
|
+
const b = barBox(s.direction ?? "right", len * k, th * k);
|
|
7496
|
+
if (!(b.halfX > 0) || !(b.halfY > 0)) return 1;
|
|
7497
|
+
const rot = rotationAt(e, t);
|
|
7498
|
+
const cr = Math.cos(rot), sr = Math.sin(rot);
|
|
7499
|
+
cx = p.x + b.cx * cr - b.cy * sr;
|
|
7500
|
+
cy = p.y + b.cx * sr + b.cy * cr;
|
|
7501
|
+
const ex = Math.abs(b.halfX * cr) + Math.abs(b.halfY * sr);
|
|
7502
|
+
const ey = Math.abs(b.halfX * sr) + Math.abs(b.halfY * cr);
|
|
7503
|
+
const fx2 = Math.max(0, Math.min(cx + ex, w) - Math.max(cx - ex, 0)) / (2 * ex);
|
|
7504
|
+
const fy2 = Math.max(0, Math.min(cy + ey, h) - Math.max(cy - ey, 0)) / (2 * ey);
|
|
7505
|
+
return fx2 * fy2;
|
|
7506
|
+
}
|
|
7507
|
+
if (s.kind === "polygon" && isPathPolygon(s)) {
|
|
7508
|
+
const fr = pathFrame(s, sz / 2, w, h);
|
|
7509
|
+
const rot = rotationAt(e, t) + paintRot;
|
|
7510
|
+
const cr = Math.cos(rot), sr = Math.sin(rot);
|
|
7511
|
+
const ox = -fr.ox * fr.sx * k, oy = -fr.oy * fr.sy * k;
|
|
7512
|
+
cx += ox * cr - oy * sr;
|
|
7513
|
+
cy += ox * sr + oy * cr;
|
|
7514
|
+
const qx = fr.sx * k, qy = fr.sy * k;
|
|
7515
|
+
hx = Math.abs(qx * cr) + Math.abs(qy * sr);
|
|
7516
|
+
hy = Math.abs(qx * sr) + Math.abs(qy * cr);
|
|
7517
|
+
}
|
|
7518
|
+
if (!(hx > 0) || !(hy > 0)) return 1;
|
|
7519
|
+
const fx = Math.max(0, Math.min(cx + hx, w) - Math.max(cx - hx, 0)) / (2 * hx);
|
|
7520
|
+
const fy = Math.max(0, Math.min(cy + hy, h) - Math.max(cy - hy, 0)) / (2 * hy);
|
|
7521
|
+
return fx * fy;
|
|
7522
|
+
}
|
|
6086
7523
|
function rawDominance(spec, opts = {}) {
|
|
6087
7524
|
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
6088
7525
|
const scene = buildScene(spec, opts);
|
|
@@ -6126,20 +7563,25 @@ function rawDominance(spec, opts = {}) {
|
|
|
6126
7563
|
const h2 = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
6127
7564
|
const f = s.feather ? (1 - s.feather / 2) * (1 - s.feather / 2) : 1;
|
|
6128
7565
|
entArea = sz * h2 * f;
|
|
6129
|
-
} else if (s.kind === "polygon")
|
|
6130
|
-
|
|
7566
|
+
} else if (s.kind === "polygon") {
|
|
7567
|
+
if (isPathPolygon(s)) {
|
|
7568
|
+
const fr = pathFrame(s, sz / 2, w, h);
|
|
7569
|
+
entArea = pathInkArea(s, fr.sx, fr.sy, sz / 2);
|
|
7570
|
+
} else entArea = polygonArea(polygonPoints(s, sz / 2));
|
|
7571
|
+
} else if (s.kind === "bar") {
|
|
6131
7572
|
const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
|
|
6132
7573
|
entArea = sz * barFraction(s, e.barIndex ?? 0) * th;
|
|
6133
7574
|
} else if (s.kind === "stroke") {
|
|
6134
7575
|
const meanTaper = s.taper ? 0.64 : 1;
|
|
6135
7576
|
entArea = pathLength(strokeSamples(s, sz / 2)) * strokeWidthPx(s, scale) * meanTaper * LINE_SALIENCE;
|
|
6136
7577
|
} else if (s.kind === "text") {
|
|
6137
|
-
const box = textBox(s, e, { x: 0, y: 0 });
|
|
7578
|
+
const box = textBox(s, e, { x: 0, y: 0 }, fontScaleFor(spec, w, h), scale);
|
|
6138
7579
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
6139
7580
|
} else if (s.kind === "textBlock") {
|
|
6140
7581
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
6141
7582
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
|
|
6142
7583
|
} else entArea = sz * sz * 0.55;
|
|
7584
|
+
entArea *= onScreenFraction(scene, s, e, sz, painted ? paintSize(spec, layer) : 1, t, painted ? paintRotation(spec, layer) : 0);
|
|
6143
7585
|
area += painted ? entArea * areaScale * a : entArea * a;
|
|
6144
7586
|
if (layer.trail) {
|
|
6145
7587
|
const fade = layer.trail.fade ?? 1;
|
|
@@ -6154,7 +7596,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
6154
7596
|
if (layer.links && entities.length > 1) {
|
|
6155
7597
|
const positions = entities.map((e) => posOf(scene, e, t));
|
|
6156
7598
|
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
6157
|
-
const edges = linkEdges(layer.links, positions, layer.links
|
|
7599
|
+
const edges = linkEdges(layer.links, positions, linkMaxDistPx(layer.links, scale), layer.wrap !== false && motionWraps, w, h);
|
|
6158
7600
|
const la = (layer.links.alpha ?? 0.6) * lifeA;
|
|
6159
7601
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
6160
7602
|
const lw = (layer.links.width ?? defaultWidth) * scale;
|
|
@@ -6184,6 +7626,7 @@ function motionStats(spec, opts = {}) {
|
|
|
6184
7626
|
const unit = Math.min(w, h);
|
|
6185
7627
|
return scene0.layers.map(({ layer, entities }, layerIndex) => {
|
|
6186
7628
|
const entities1 = scene1.layers[layerIndex]?.entities ?? entities;
|
|
7629
|
+
const spinVisible = layer.sprite.kind !== "circle" && layer.sprite.kind !== "ring";
|
|
6187
7630
|
let acc = 0;
|
|
6188
7631
|
let max = 0;
|
|
6189
7632
|
let n = 0;
|
|
@@ -6197,7 +7640,8 @@ function motionStats(spec, opts = {}) {
|
|
|
6197
7640
|
const dy = p1.y - p0.y;
|
|
6198
7641
|
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) return;
|
|
6199
7642
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
6200
|
-
const
|
|
7643
|
+
const turn = spinVisible ? Math.abs(rotationAt(entities1[i] ?? e, t + dt) - rotationAt(e, t)) * (sizeAt(e, t) / 3) * paintSize(spec, layer) : 0;
|
|
7644
|
+
const speed = Math.sqrt(dist * dist + turn * turn) / dt * 1e3;
|
|
6201
7645
|
acc += speed;
|
|
6202
7646
|
max = Math.max(max, speed);
|
|
6203
7647
|
n++;
|
|
@@ -6254,17 +7698,36 @@ function sceneColour(spec, opts = {}) {
|
|
|
6254
7698
|
let wsum = 0;
|
|
6255
7699
|
for (const { layer, entities } of scene.layers) {
|
|
6256
7700
|
const lifeA = lifeAlphaAt(layer.life, t) * (layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer));
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
if (
|
|
6260
|
-
const sz = sizeAt(e, t);
|
|
6261
|
-
const weight = alphaAt(e, t) * lifeA * (layer.sprite.kind === "circle" ? Math.PI * (sz / 2) * (sz / 2) : sz * sz);
|
|
6262
|
-
if (weight <= 0) continue;
|
|
7701
|
+
const k = layer.transform !== void 0 || layer.group !== void 0 ? paintSize(spec, layer) : 1;
|
|
7702
|
+
const add = (hex, weight) => {
|
|
7703
|
+
if (weight <= 0) return;
|
|
6263
7704
|
const c = hexRgb(hex);
|
|
6264
7705
|
wr += c.r * weight;
|
|
6265
7706
|
wg += c.g * weight;
|
|
6266
7707
|
wb += c.b * weight;
|
|
6267
7708
|
wsum += weight;
|
|
7709
|
+
};
|
|
7710
|
+
for (const e of entities) {
|
|
7711
|
+
const sz = sizeAt(e, t);
|
|
7712
|
+
const s = layer.sprite;
|
|
7713
|
+
const a = alphaAt(e, t) * lifeA * onScreenFraction(scene, s, e, sz, k, t, paintRotation(spec, layer));
|
|
7714
|
+
if (a <= 0) continue;
|
|
7715
|
+
if (s.kind === "polygon" && isPathPolygon(s)) {
|
|
7716
|
+
const fr = pathFrame(s, sz / 2, scene.w, scene.h);
|
|
7717
|
+
const base = s.colors?.[e.colorIndex] ?? s.color;
|
|
7718
|
+
for (const part of pathParts(s)) {
|
|
7719
|
+
const ink = partInkArea(part, fr.sx, fr.sy, sz / 2);
|
|
7720
|
+
if (part.gradient) {
|
|
7721
|
+
const gw = gradientStopWeights(part.gradient.stops);
|
|
7722
|
+
part.gradient.stops.forEach((st, i) => add(st.color, a * ink.fill * (st.opacity ?? 1) * gw[i]));
|
|
7723
|
+
} else add(part.color ?? base, a * ink.fill);
|
|
7724
|
+
if (part.stroke) add(part.stroke.color, a * ink.stroke);
|
|
7725
|
+
}
|
|
7726
|
+
continue;
|
|
7727
|
+
}
|
|
7728
|
+
const hex = spriteHex(layer, e);
|
|
7729
|
+
if (hex === null) continue;
|
|
7730
|
+
add(hex, a * (s.kind === "circle" ? Math.PI * (sz / 2) * (sz / 2) : sz * sz));
|
|
6268
7731
|
}
|
|
6269
7732
|
}
|
|
6270
7733
|
const bg = backgroundRgb(spec);
|
|
@@ -6457,12 +7920,92 @@ function perceiveSequenceFrame(seq, T, opts = {}) {
|
|
|
6457
7920
|
};
|
|
6458
7921
|
}
|
|
6459
7922
|
|
|
7923
|
+
// src/flash-timeline.ts
|
|
7924
|
+
import { analyzeFlashes, srgbToLinear as srgbToLinear2 } from "@idle-screens/validator";
|
|
7925
|
+
var LUMA_PATH = /(^|\.)(opacity|alpha|color|colors|stops|bands|gradient|stroke|fill|background)(\.|$)|(^|\.)(sprite|parts?)$|^layers\.\d+$/;
|
|
7926
|
+
var MIN_BOUNDARIES = 7;
|
|
7927
|
+
var MAX_SAMPLES = 64;
|
|
7928
|
+
var MAX_TOTAL_SAMPLES = 512;
|
|
7929
|
+
var WINDOW = 1e3;
|
|
7930
|
+
function lumaKeyTimes(spec) {
|
|
7931
|
+
const tl = spec.timeline;
|
|
7932
|
+
if (!tl) return { times: [], lap: 0 };
|
|
7933
|
+
const lap = tl.loop && tl.duration ? tl.duration : 0;
|
|
7934
|
+
const bounds = [];
|
|
7935
|
+
for (const [path, keys] of timelineTracks(spec)) {
|
|
7936
|
+
if (!LUMA_PATH.test(path)) continue;
|
|
7937
|
+
for (const k of keys) {
|
|
7938
|
+
const dur = k.dur ?? DEFAULT_KEY_DUR;
|
|
7939
|
+
bounds.push(k.t);
|
|
7940
|
+
if (dur > 0) bounds.push(k.t + dur);
|
|
7941
|
+
}
|
|
7942
|
+
}
|
|
7943
|
+
return { times: [...new Set(lap ? [...bounds, ...bounds.map((b) => b + lap)] : bounds)].sort((a, b) => a - b), lap };
|
|
7944
|
+
}
|
|
7945
|
+
function flashSampleTimes(boundaries) {
|
|
7946
|
+
const ts = /* @__PURE__ */ new Set();
|
|
7947
|
+
boundaries.forEach((t, i) => {
|
|
7948
|
+
ts.add(t - 1);
|
|
7949
|
+
ts.add(t + 1);
|
|
7950
|
+
const next = boundaries[i + 1];
|
|
7951
|
+
if (next !== void 0 && next - t > 4) ts.add((t + next) / 2);
|
|
7952
|
+
});
|
|
7953
|
+
return [...ts].sort((a, b) => a - b);
|
|
7954
|
+
}
|
|
7955
|
+
function timelineFlashReport(spec) {
|
|
7956
|
+
const pass = { fails: false, flashesPerSecond: 0, flashingArea: 0, at: null };
|
|
7957
|
+
if (!spec.timeline) return pass;
|
|
7958
|
+
const { times, lap } = lumaKeyTimes(spec);
|
|
7959
|
+
const dense = [];
|
|
7960
|
+
for (let i = 0, e = 0; i < times.length; i++) {
|
|
7961
|
+
while (e < times.length && times[e] < times[i] + WINDOW) e++;
|
|
7962
|
+
if (e - i < MIN_BOUNDARIES) continue;
|
|
7963
|
+
const last = dense[dense.length - 1];
|
|
7964
|
+
if (last && times[i] <= last.to) {
|
|
7965
|
+
last.to = Math.max(last.to, times[e - 1]);
|
|
7966
|
+
last.n = Math.max(last.n, e - i);
|
|
7967
|
+
} else dense.push({ from: times[i], to: times[e - 1], n: e - i });
|
|
7968
|
+
}
|
|
7969
|
+
if (dense.length === 0) return pass;
|
|
7970
|
+
dense.sort((a, b) => b.n - a.n);
|
|
7971
|
+
let budget = MAX_TOTAL_SAMPLES;
|
|
7972
|
+
let worst = pass;
|
|
7973
|
+
for (const w of dense) {
|
|
7974
|
+
const sampleTimes = flashSampleTimes(times.filter((t) => t >= w.from && t <= w.to));
|
|
7975
|
+
for (let from = 0; from < sampleTimes.length - 1; ) {
|
|
7976
|
+
const chunk = sampleTimes.slice(from, from + MAX_SAMPLES);
|
|
7977
|
+
if (chunk.length < 2) break;
|
|
7978
|
+
if (chunk.length > budget) return { ...worst, fails: true, at: lap ? w.from % lap : w.from, incomplete: true };
|
|
7979
|
+
budget -= chunk.length;
|
|
7980
|
+
const samples = chunk.map((t) => ({ t, lum: luminanceGrid(spec, { t, cols: 16, rows: 9 }).cells.flat().map((v) => srgbToLinear2(v * 255)) }));
|
|
7981
|
+
const r = analyzeFlashes(samples);
|
|
7982
|
+
if (r.general.worstTileFlashesPerSecond > worst.flashesPerSecond || !worst.fails && r.general.fails) {
|
|
7983
|
+
worst = {
|
|
7984
|
+
fails: r.general.fails,
|
|
7985
|
+
flashesPerSecond: r.general.worstTileFlashesPerSecond,
|
|
7986
|
+
flashingArea: r.general.flashingAreaFraction,
|
|
7987
|
+
at: lap ? w.from % lap : w.from
|
|
7988
|
+
};
|
|
7989
|
+
}
|
|
7990
|
+
if (worst.fails) return worst;
|
|
7991
|
+
if (from + MAX_SAMPLES >= sampleTimes.length) break;
|
|
7992
|
+
const resume = chunk[chunk.length - 1] - WINDOW;
|
|
7993
|
+
let next = from + 1;
|
|
7994
|
+
while (sampleTimes[next] < resume) next++;
|
|
7995
|
+
from = next;
|
|
7996
|
+
}
|
|
7997
|
+
}
|
|
7998
|
+
return worst;
|
|
7999
|
+
}
|
|
8000
|
+
|
|
6460
8001
|
// src/advise.ts
|
|
6461
8002
|
var LOW_CONTRAST_FLOOR = 0.05;
|
|
6462
8003
|
var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
6463
8004
|
var READ_LEGIBILITY_FLOOR = 4.5;
|
|
6464
8005
|
var READ_SAFE_AREA = 0.05;
|
|
6465
8006
|
var ADDITIVE_INK_CEILING = 1;
|
|
8007
|
+
var CALM_MAX_SPIN = 90;
|
|
8008
|
+
var NEAR_FLASH_MAX_BOUNDARIES = 10;
|
|
6466
8009
|
function edgeAnchored(range) {
|
|
6467
8010
|
if (!range) return false;
|
|
6468
8011
|
const [a, b] = range;
|
|
@@ -6470,6 +8013,7 @@ function edgeAnchored(range) {
|
|
|
6470
8013
|
}
|
|
6471
8014
|
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
6472
8015
|
const hidden = hiddenLayers(spec);
|
|
8016
|
+
const authored = spec;
|
|
6473
8017
|
spec = resolveTimelineAt(spec, opts.t ?? COHESION_T);
|
|
6474
8018
|
const warnings = [];
|
|
6475
8019
|
const w = viewport.width;
|
|
@@ -6496,7 +8040,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6496
8040
|
if (isStaticText) textLayerCount++;
|
|
6497
8041
|
if (layer.motion.type !== "static") motionLayerCount++;
|
|
6498
8042
|
const coh = cohesion[li];
|
|
6499
|
-
if (seamsWorthWarning(layer, coh, entities.length)) {
|
|
8043
|
+
if (seamsWorthWarning(layer, coh, entities.length, () => buriedAreaOf(layer, entities, opts.t ?? COHESION_T, w, h))) {
|
|
6500
8044
|
const s = layer.sprite;
|
|
6501
8045
|
const softEdged = (s.kind === "circle" || s.kind === "polygon") && s.soft || s.kind === "rect" && !!s.feather;
|
|
6502
8046
|
warnings.push({
|
|
@@ -6544,13 +8088,17 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6544
8088
|
return { rgb, luma: 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b };
|
|
6545
8089
|
});
|
|
6546
8090
|
const seps = entities.map((e) => {
|
|
6547
|
-
const
|
|
6548
|
-
if (
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
8091
|
+
const inks = spriteInks(layer, e);
|
|
8092
|
+
if (inks === null || inks.length === 0) return Infinity;
|
|
8093
|
+
return Math.max(...inks.map((hex) => {
|
|
8094
|
+
const ink = { rgb: hexRgb(hex), luma: hexLuma(hex) };
|
|
8095
|
+
const sep = Math.max(...plates.map((bg) => colourSeparation(layer, ink, bg)));
|
|
8096
|
+
if (sep >= LOW_CONTRAST_FLOOR || layer.blend === "lighter" || layer.blend === "screen") return sep;
|
|
8097
|
+
const at = positionAt(e, 0, w, h);
|
|
8098
|
+
const under = groundUnder(spec, allEntities, li, hidden, at, w, h);
|
|
8099
|
+
const framed = holeFrame(spec, allEntities, li, hidden, at, w, h);
|
|
8100
|
+
return Math.max(under ? colourSeparation(layer, ink, under) : sep, framed ? colourSeparation(layer, ink, framed) : sep);
|
|
8101
|
+
}));
|
|
6554
8102
|
});
|
|
6555
8103
|
const best = Math.max(...seps);
|
|
6556
8104
|
if (best < LOW_CONTRAST_FLOOR) {
|
|
@@ -6579,7 +8127,10 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6579
8127
|
if (layer.sprite.kind === "circle") {
|
|
6580
8128
|
pixArea = Math.PI * r * r;
|
|
6581
8129
|
} else if (layer.sprite.kind === "polygon") {
|
|
6582
|
-
|
|
8130
|
+
if (isPathPolygon(layer.sprite)) {
|
|
8131
|
+
const fr = pathFrame(layer.sprite, r, w, h);
|
|
8132
|
+
pixArea = pathInkArea(layer.sprite, fr.sx, fr.sy, r);
|
|
8133
|
+
} else pixArea = polygonArea(polygonPoints(layer.sprite, r));
|
|
6583
8134
|
} else if (layer.sprite.kind === "bar") {
|
|
6584
8135
|
pixArea = e.size * barFraction(layer.sprite, e.barIndex ?? 0) * (e.size2 ?? e.size * 0.2);
|
|
6585
8136
|
} else if (layer.sprite.kind === "textBlock") {
|
|
@@ -6608,8 +8159,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6608
8159
|
}
|
|
6609
8160
|
if (layer.links) {
|
|
6610
8161
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
6611
|
-
const
|
|
6612
|
-
const edges = linkEdges(layer.links, positions, maxDistPx, layer.wrap !== false, w, h);
|
|
8162
|
+
const edges = linkEdges(layer.links, positions, linkMaxDistPx(layer.links, scale), layer.wrap !== false, w, h);
|
|
6613
8163
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
6614
8164
|
const lwPx = (layer.links.width ?? defaultWidth) * scale;
|
|
6615
8165
|
const la = layer.links.alpha ?? 1;
|
|
@@ -6662,15 +8212,39 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6662
8212
|
});
|
|
6663
8213
|
}
|
|
6664
8214
|
}
|
|
8215
|
+
if (spec.motionIntensity === "calm") {
|
|
8216
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
8217
|
+
if (hidden?.[li]) continue;
|
|
8218
|
+
const layer = spec.layers[li];
|
|
8219
|
+
if (!layer.spin || layer.sprite.kind === "circle" || layer.sprite.kind === "ring") continue;
|
|
8220
|
+
const entities = allEntities[li];
|
|
8221
|
+
if (entities.length === 0) continue;
|
|
8222
|
+
const rate = entities.reduce((s, e) => s + Math.abs(e.spinSpeed), 0) / entities.length * (180 / Math.PI);
|
|
8223
|
+
if (rate <= CALM_MAX_SPIN) continue;
|
|
8224
|
+
warnings.push({
|
|
8225
|
+
path: `layers[${li}].spin`,
|
|
8226
|
+
code: "motion-intensity-mismatch",
|
|
8227
|
+
message: `declared motionIntensity: 'calm' but ${layer.key ? `\`${layer.key}\`` : `layers[${li}]`} spins at ~${Math.round(rate)}\xB0/s on average \u2014 that reads busy, not calm (calm spins sit at \u2264 ${CALM_MAX_SPIN}\xB0/s); slow the spin or declare 'moderate' / 'energetic'`
|
|
8228
|
+
});
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8231
|
+
const flicker = nearLimitFlash(authored);
|
|
8232
|
+
if (flicker) {
|
|
8233
|
+
warnings.push({
|
|
8234
|
+
path: "timeline",
|
|
8235
|
+
code: "flash-near-limit",
|
|
8236
|
+
message: `the timeline flickers ${flicker.flashesPerSecond} times a second over ${Math.round(flicker.area * 100)}% of the frame around ${Math.round(flicker.at)} ms \u2014 at or near WCAG 2.3.1's limit of 3; space opposing luminance changes \u2265 500 ms apart, or lower the contrast between the two states`
|
|
8237
|
+
});
|
|
8238
|
+
}
|
|
6665
8239
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
6666
8240
|
if (hidden?.[li]) continue;
|
|
6667
8241
|
const layer = spec.layers[li];
|
|
6668
8242
|
if (!layer.links || layer.links.mode === "chain") continue;
|
|
6669
8243
|
const entities = allEntities[li];
|
|
6670
8244
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
6671
|
-
const
|
|
6672
|
-
const pairs = linkPairs(positions, layer.links
|
|
6673
|
-
const maxPossible = Math.min(entities.length *
|
|
8245
|
+
const k = layer.links.k ?? 1;
|
|
8246
|
+
const pairs = linkPairs(positions, k, linkMaxDistPx(layer.links, scale), layer.wrap !== false, w, h);
|
|
8247
|
+
const maxPossible = Math.min(entities.length * k, entities.length * (entities.length - 1) / 2);
|
|
6674
8248
|
if (maxPossible > 0 && pairs.length / maxPossible < 0.1) {
|
|
6675
8249
|
warnings.push({
|
|
6676
8250
|
path: `layers[${li}].links`,
|
|
@@ -6729,9 +8303,13 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6729
8303
|
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
6730
8304
|
for (const e of allEntities[li]) {
|
|
6731
8305
|
const p = positionAt(e, 0, w, h);
|
|
6732
|
-
const
|
|
8306
|
+
const unturned = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
8307
|
+
const rot = rotationAt(e, 0);
|
|
8308
|
+
const turn = (q) => ({ x: p.x + (q.x - p.x) * Math.cos(rot) - (q.y - p.y) * Math.sin(rot), y: p.y + (q.x - p.x) * Math.sin(rot) + (q.y - p.y) * Math.cos(rot) });
|
|
6733
8309
|
const map = paintMap(spec, layer, w, h, scale);
|
|
6734
|
-
const
|
|
8310
|
+
const both = rot && map ? (q) => map(turn(q)) : rot ? turn : map;
|
|
8311
|
+
const box = both ? { ...unturned, ...mapBox(both, unturned) } : unturned;
|
|
8312
|
+
const raw = rot ? { ...unturned, ...mapBox(turn, unturned) } : unturned;
|
|
6735
8313
|
const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
6736
8314
|
const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
|
|
6737
8315
|
const str = s.kind === "textBlock" ? s.text : s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
@@ -6853,20 +8431,34 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6853
8431
|
for (const box of boxes) {
|
|
6854
8432
|
const cy = (box.y0 + box.y1) / 2;
|
|
6855
8433
|
const cx = (box.x0 + box.x1) / 2;
|
|
6856
|
-
const
|
|
6857
|
-
const
|
|
6858
|
-
const
|
|
6859
|
-
const
|
|
6860
|
-
|
|
6861
|
-
|
|
8434
|
+
const bgAt = (x, y) => backgroundRgbAt(spec, y, h, scale, x, w, opts.backgroundSeed ?? opts.seed, opts.t ?? COHESION_T);
|
|
8435
|
+
const ix = (box.x1 - box.x0) * 0.02;
|
|
8436
|
+
const iy = (box.y1 - box.y0) * 0.02;
|
|
8437
|
+
const probes = [[cx, cy], [box.x0 + ix, box.y0 + iy], [box.x1 - ix, box.y0 + iy], [box.x0 + ix, box.y1 - iy], [box.x1 - ix, box.y1 - iy]];
|
|
8438
|
+
const grounds = [];
|
|
8439
|
+
let bare = false;
|
|
8440
|
+
for (const [x, y] of probes) {
|
|
8441
|
+
const under = groundUnder(spec, allEntities, box.li, hidden, { x, y }, w, h, bgAt(x, y));
|
|
8442
|
+
if (under) grounds.push({ rgb: under.rgb, onSolid: true });
|
|
8443
|
+
else bare = true;
|
|
8444
|
+
}
|
|
8445
|
+
if (bare) grounds.push({ rgb: bgAt(cx, cy), onSolid: false });
|
|
8446
|
+
for (const g of grounds) {
|
|
8447
|
+
const ground = legibilityRatio(textPlate(g.rgb, box.alpha), g.rgb);
|
|
8448
|
+
const lit = brightestAdditivePlate(spec, allEntities, g.rgb, box, w, h);
|
|
8449
|
+
const plate = lit ? legibilityRatio(textPlate(lit.rgb, box.alpha), lit.rgb) : Infinity;
|
|
8450
|
+
if (!worst || Math.min(ground, plate) < Math.min(worst.ground, worst.plate)) {
|
|
8451
|
+
worst = { ground, plate, plateLabel: lit?.label ?? null, onSolid: g.onSolid };
|
|
8452
|
+
}
|
|
6862
8453
|
}
|
|
6863
8454
|
}
|
|
6864
8455
|
if (worst && Math.min(worst.ground, worst.plate) < READ_LEGIBILITY_FLOOR) {
|
|
6865
8456
|
const under = worst.plateLabel ? ` and ${worst.plate.toFixed(1)}:1 under ${worst.plateLabel} at its brightest` : "";
|
|
8457
|
+
const against = worst.onSolid ? "against the solid layer painted under it" : "against the background at the box centre";
|
|
6866
8458
|
warnings.push({
|
|
6867
8459
|
path: `layers[${b.li}].sprite`,
|
|
6868
8460
|
code: "text-legibility",
|
|
6869
|
-
message: `${b.label} is declared role: 'read' but its colour reads at ${worst.ground.toFixed(1)}:1 against
|
|
8461
|
+
message: `${b.label} is declared role: 'read' but its colour reads at ${worst.ground.toFixed(1)}:1 ${against}${under} \u2014 below ${READ_LEGIBILITY_FLOOR}:1; brighten the text, darken the ground under it, or declare role: 'atmosphere' if it is texture`
|
|
6870
8462
|
});
|
|
6871
8463
|
}
|
|
6872
8464
|
const edges = [];
|
|
@@ -6889,6 +8481,21 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6889
8481
|
}
|
|
6890
8482
|
return warnings;
|
|
6891
8483
|
}
|
|
8484
|
+
function nearLimitFlash(spec) {
|
|
8485
|
+
if (!spec.timeline) return null;
|
|
8486
|
+
const { times, lap } = lumaKeyTimes(spec);
|
|
8487
|
+
let best = null;
|
|
8488
|
+
for (let i = 0, e = 0; i < times.length; i++) {
|
|
8489
|
+
while (e < times.length && times[e] < times[i] + 1e3) e++;
|
|
8490
|
+
if (e - i >= 4 && (!best || e - i > best.n)) best = { from: i, n: e - i };
|
|
8491
|
+
}
|
|
8492
|
+
if (!best) return null;
|
|
8493
|
+
const window = times.slice(best.from, best.from + Math.min(best.n, NEAR_FLASH_MAX_BOUNDARIES));
|
|
8494
|
+
const samples = flashSampleTimes(window).map((t) => ({ t, lum: luminanceGrid(spec, { t, cols: 16, rows: 9 }).cells.flat().map((v) => srgbToLinear3(v * 255)) }));
|
|
8495
|
+
const r = analyzeFlashes2(samples, { maxFlashesPerSecond: 1 }).general;
|
|
8496
|
+
if (!r.fails) return null;
|
|
8497
|
+
return { flashesPerSecond: r.worstTileFlashesPerSecond, area: r.flashingAreaFraction, at: lap ? window[0] % lap : window[0] };
|
|
8498
|
+
}
|
|
6892
8499
|
function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
6893
8500
|
let best = null;
|
|
6894
8501
|
for (let lj = 0; lj < spec.layers.length; lj++) {
|
|
@@ -6916,38 +8523,68 @@ function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
|
6916
8523
|
}
|
|
6917
8524
|
return best ? { rgb: best.rgb, label: best.label } : null;
|
|
6918
8525
|
}
|
|
6919
|
-
function
|
|
6920
|
-
let
|
|
8526
|
+
function holeFrame(spec, allEntities, li, hidden, pt, w, h) {
|
|
8527
|
+
for (let lj = li + 1; lj < spec.layers.length; lj++) {
|
|
8528
|
+
if (hidden?.[lj]) continue;
|
|
8529
|
+
const layer = spec.layers[lj];
|
|
8530
|
+
const sp = layer.sprite;
|
|
8531
|
+
if (sp.kind !== "polygon" || !isPathPolygon(sp) || sp.soft) continue;
|
|
8532
|
+
if (layer.motion.type !== "static" || layer.blend !== void 0 || layer.transform !== void 0 || groupOf(spec, layer)?.transform !== void 0) continue;
|
|
8533
|
+
if (flattenSprite(sp).subpaths < 2) continue;
|
|
8534
|
+
const a = layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer);
|
|
8535
|
+
for (const e of allEntities[lj]) {
|
|
8536
|
+
if (e.spinSpeed !== 0 || e.rotate || e.alpha * a < 0.9) continue;
|
|
8537
|
+
const c = positionAt(e, 0, w, h);
|
|
8538
|
+
const fr = pathFrame(sp, e.size / 2, w, h);
|
|
8539
|
+
const pts = flattenSprite(sp).points.map(([u, v]) => ({ x: (u - fr.ox) * fr.sx, y: (v - fr.oy) * fr.sy }));
|
|
8540
|
+
if (pts.length < 3) continue;
|
|
8541
|
+
const dx = pt.x - c.x;
|
|
8542
|
+
const dy = pt.y - c.y;
|
|
8543
|
+
if (dx < Math.min(...pts.map((q) => q.x)) || dx > Math.max(...pts.map((q) => q.x))) continue;
|
|
8544
|
+
if (dy < Math.min(...pts.map((q) => q.y)) || dy > Math.max(...pts.map((q) => q.y))) continue;
|
|
8545
|
+
if (pointInPolygon(dx, dy, pts)) continue;
|
|
8546
|
+
const ink = spriteInks(layer, e)?.[0];
|
|
8547
|
+
if (ink === void 0) continue;
|
|
8548
|
+
const rgb = hexRgb(ink);
|
|
8549
|
+
return { rgb, luma: 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b };
|
|
8550
|
+
}
|
|
8551
|
+
}
|
|
8552
|
+
return null;
|
|
8553
|
+
}
|
|
8554
|
+
function groundUnder(spec, allEntities, li, hidden, pt, w, h, base = backgroundRgb(spec)) {
|
|
8555
|
+
let plate = base;
|
|
6921
8556
|
let covered = false;
|
|
6922
8557
|
for (let lj = 0; lj < li; lj++) {
|
|
6923
8558
|
if (hidden?.[lj]) continue;
|
|
6924
8559
|
const layer = spec.layers[lj];
|
|
6925
8560
|
const sp = layer.sprite;
|
|
6926
8561
|
if (layer.motion.type !== "static" || layer.blend !== void 0 || layer.transform !== void 0 || groupOf(spec, layer)?.transform !== void 0) continue;
|
|
8562
|
+
if (layer.life !== void 0 || layer.emit !== void 0) continue;
|
|
6927
8563
|
if ((sp.kind === "circle" || sp.kind === "polygon") && sp.soft) continue;
|
|
6928
8564
|
if (sp.kind !== "circle" && sp.kind !== "rect" && sp.kind !== "polygon") continue;
|
|
8565
|
+
if (sp.kind === "polygon" && isPathPolygon(sp)) continue;
|
|
6929
8566
|
const a = layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer);
|
|
6930
8567
|
for (const e of allEntities[lj]) {
|
|
6931
|
-
if (e.spinSpeed !== 0 || e.alpha * a <= 0) continue;
|
|
8568
|
+
if (e.spinSpeed !== 0 || e.pulseAmp > 0 || e.growAmp > 0 || e.alpha * a <= 0) continue;
|
|
6932
8569
|
const hex = spriteHex(layer, e);
|
|
6933
8570
|
if (hex === null) continue;
|
|
6934
8571
|
const c = positionAt(e, 0, w, h);
|
|
6935
8572
|
const dx = pt.x - c.x;
|
|
6936
8573
|
const dy = pt.y - c.y;
|
|
6937
|
-
let
|
|
6938
|
-
if (sp.kind === "circle")
|
|
8574
|
+
let inside2 = false;
|
|
8575
|
+
if (sp.kind === "circle") inside2 = Math.hypot(dx, dy) <= e.size / 2;
|
|
6939
8576
|
else if (sp.kind === "rect") {
|
|
6940
8577
|
const rot = ((e.rotate ?? 0) % Math.PI + Math.PI) % Math.PI;
|
|
6941
8578
|
const quarter = Math.abs(rot - Math.PI / 2) < 0.01;
|
|
6942
8579
|
if (rot > 0.01 && rot < Math.PI - 0.01 && !quarter) continue;
|
|
6943
8580
|
const hx = (quarter ? e.size2 ?? e.size : e.size) / 2;
|
|
6944
8581
|
const hy = (quarter ? e.size : e.size2 ?? e.size) / 2;
|
|
6945
|
-
|
|
8582
|
+
inside2 = Math.abs(dx) <= hx && Math.abs(dy) <= hy;
|
|
6946
8583
|
} else {
|
|
6947
8584
|
if (e.rotate) continue;
|
|
6948
|
-
|
|
8585
|
+
inside2 = pointInPolygon(dx, dy, polygonPoints(sp, e.size / 2));
|
|
6949
8586
|
}
|
|
6950
|
-
if (!
|
|
8587
|
+
if (!inside2) continue;
|
|
6951
8588
|
plate = sourceOverPlate(plate, hexRgb(hex), Math.min(1, e.alpha * a));
|
|
6952
8589
|
covered = true;
|
|
6953
8590
|
}
|
|
@@ -6955,13 +8592,13 @@ function groundUnder(spec, allEntities, li, hidden, pt, w, h) {
|
|
|
6955
8592
|
return covered ? { rgb: plate, luma: 0.2126 * plate.r + 0.7152 * plate.g + 0.0722 * plate.b } : null;
|
|
6956
8593
|
}
|
|
6957
8594
|
function pointInPolygon(x, y, pts) {
|
|
6958
|
-
let
|
|
8595
|
+
let inside2 = false;
|
|
6959
8596
|
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
|
6960
8597
|
const a = pts[i];
|
|
6961
8598
|
const b = pts[j];
|
|
6962
|
-
if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x)
|
|
8599
|
+
if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) inside2 = !inside2;
|
|
6963
8600
|
}
|
|
6964
|
-
return
|
|
8601
|
+
return inside2;
|
|
6965
8602
|
}
|
|
6966
8603
|
function entityReachesBox(e, r, box, w, h) {
|
|
6967
8604
|
if (e.motion === "static") {
|
|
@@ -6981,7 +8618,7 @@ function isOutlineStack(a, b) {
|
|
|
6981
8618
|
if (fs <= 0 || Math.abs(a.fs - b.fs) > 0.1 * Math.max(a.fs, b.fs)) return false;
|
|
6982
8619
|
const dx = (a.x0 + a.x1) / 2 - (b.x0 + b.x1) / 2;
|
|
6983
8620
|
const dy = (a.y0 + a.y1) / 2 - (b.y0 + b.y1) / 2;
|
|
6984
|
-
return Math.
|
|
8621
|
+
return Math.max(Math.abs(dx), Math.abs(dy)) <= OUTLINE_STACK_OFFSET * fs;
|
|
6985
8622
|
}
|
|
6986
8623
|
function textBoxAt(s, e, p, spec, w, h) {
|
|
6987
8624
|
const m = s.font ? /(\d{1,5}(?:\.\d{1,4})?|\.\d{1,4})px/.exec(s.font) : null;
|
|
@@ -7113,10 +8750,10 @@ function describeScene(spec, options = {}) {
|
|
|
7113
8750
|
let connectedComponents = null;
|
|
7114
8751
|
let isolatedNodes = null;
|
|
7115
8752
|
if (layer.links) {
|
|
7116
|
-
const
|
|
7117
|
-
const pairs =
|
|
8753
|
+
const n = entities.length;
|
|
8754
|
+
const pairs = linkEdges(layer.links, positions, linkMaxDistPx(layer.links, scale), layer.wrap !== false, w, h).map(({ i, j }) => [i, j]);
|
|
7118
8755
|
linksDrawn = pairs.length;
|
|
7119
|
-
linksExpected = Math.
|
|
8756
|
+
linksExpected = layer.links.mode === "chain" ? Math.max(0, n - 1) + (layer.links.closed && n > 2 ? 1 : 0) : Math.min(n * (layer.links.k ?? 1), n * (n - 1) / 2);
|
|
7120
8757
|
const parent = Array.from({ length: entities.length }, (_, i) => i);
|
|
7121
8758
|
const find = (x) => {
|
|
7122
8759
|
while (parent[x] !== x) {
|
|
@@ -9617,6 +11254,7 @@ export {
|
|
|
9617
11254
|
LIMITS,
|
|
9618
11255
|
LOBBY_TALK_SPEC,
|
|
9619
11256
|
MATRIX_RAIN_SPEC,
|
|
11257
|
+
MAX_PATH_POINTS,
|
|
9620
11258
|
MURMUR_SPEC,
|
|
9621
11259
|
NOSTALGHIA_CANDLE_SPEC,
|
|
9622
11260
|
ORRERY_SPEC,
|
|
@@ -9643,6 +11281,7 @@ export {
|
|
|
9643
11281
|
adviseSequence,
|
|
9644
11282
|
adviseSpec,
|
|
9645
11283
|
alphaAt,
|
|
11284
|
+
applyDeltasShared,
|
|
9646
11285
|
applyDeltasToSpec,
|
|
9647
11286
|
assertValidSequence,
|
|
9648
11287
|
assertValidSpec,
|
|
@@ -9651,6 +11290,7 @@ export {
|
|
|
9651
11290
|
bedRenderSeed,
|
|
9652
11291
|
breakTextBlock,
|
|
9653
11292
|
buildEntities,
|
|
11293
|
+
buriedAreaOf,
|
|
9654
11294
|
canMorph,
|
|
9655
11295
|
canWrapMorph,
|
|
9656
11296
|
canonicalSpecPath,
|
|
@@ -9672,6 +11312,7 @@ export {
|
|
|
9672
11312
|
fieldRgbAt,
|
|
9673
11313
|
fieldSampleTime,
|
|
9674
11314
|
finishStrength,
|
|
11315
|
+
flattenPath,
|
|
9675
11316
|
grainOffset,
|
|
9676
11317
|
grainTilePixels,
|
|
9677
11318
|
graphemeClusters,
|
|
@@ -9679,12 +11320,15 @@ export {
|
|
|
9679
11320
|
headingAt,
|
|
9680
11321
|
hexToRgb255,
|
|
9681
11322
|
ignoredPropertyPaths,
|
|
11323
|
+
isPathPolygon,
|
|
9682
11324
|
latticeHash,
|
|
9683
11325
|
layerCohesion,
|
|
11326
|
+
lerpPathD,
|
|
9684
11327
|
lerpSpec,
|
|
9685
11328
|
lerpValue,
|
|
9686
11329
|
lifeAlphaAt,
|
|
9687
11330
|
linkEdges,
|
|
11331
|
+
linkMaxDistPx,
|
|
9688
11332
|
linkPairs,
|
|
9689
11333
|
luminanceGrid,
|
|
9690
11334
|
manifestFor,
|
|
@@ -9693,6 +11337,8 @@ export {
|
|
|
9693
11337
|
motionStats,
|
|
9694
11338
|
nextKeyAfter,
|
|
9695
11339
|
normalizeSeed,
|
|
11340
|
+
pathFrame,
|
|
11341
|
+
pathParts,
|
|
9696
11342
|
perceiveScene,
|
|
9697
11343
|
perceiveSequenceFrame,
|
|
9698
11344
|
pickScreenOp,
|
|
@@ -9726,6 +11372,7 @@ export {
|
|
|
9726
11372
|
textStringsDiffer,
|
|
9727
11373
|
textStringsOf,
|
|
9728
11374
|
textWidthEm,
|
|
11375
|
+
timelineFlashReport,
|
|
9729
11376
|
timelineLocalTime,
|
|
9730
11377
|
timelineSampleTimes,
|
|
9731
11378
|
timelineSampleTimesAfter,
|