@idle-screens/schema 3.13.0 → 3.15.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 +183 -22
- package/dist/index.d.ts +362 -18
- package/dist/index.js +1419 -174
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/saver-spec.schema.json +69 -5
package/dist/index.js
CHANGED
|
@@ -96,6 +96,8 @@ var LIMITS = {
|
|
|
96
96
|
// ms — a `clock`ed layer breathes in unison, so 1 Hz not 2
|
|
97
97
|
minClockRate: 0.1,
|
|
98
98
|
maxClockRate: 4,
|
|
99
|
+
maxWobble: 0.25,
|
|
100
|
+
// min(w,h) units — drift `bob` / rise `sway` above this warn `motion-amplitude` (× referenceViewport in px specs)
|
|
99
101
|
defaultListGap: 0.06,
|
|
100
102
|
// viewport units between list/table cells (40 px in px specs)
|
|
101
103
|
defaultListGapPx: 40,
|
|
@@ -103,6 +105,17 @@ var LIMITS = {
|
|
|
103
105
|
maxPolygonSides: 12,
|
|
104
106
|
maxShapePoints: 24,
|
|
105
107
|
// polygon.points / stroke.points
|
|
108
|
+
maxTextNumberDecimals: 6,
|
|
109
|
+
maxTextAffix: 64,
|
|
110
|
+
// characters of a counter's prefix / suffix
|
|
111
|
+
maxRevealRise: 1,
|
|
112
|
+
// em
|
|
113
|
+
maxRockAmp: 45,
|
|
114
|
+
// degrees either side of rest — a swing, not a spin
|
|
115
|
+
minRockPeriod: 1e3,
|
|
116
|
+
// ms — a calm swing; at most once a second
|
|
117
|
+
maxLifeStagger: 6e4,
|
|
118
|
+
// ms between successive entities' entrances
|
|
106
119
|
minEaseTau: 100,
|
|
107
120
|
// ms
|
|
108
121
|
maxEaseTau: 12e4
|
|
@@ -255,6 +268,35 @@ function applyDeltasToSpec(spec, deltas) {
|
|
|
255
268
|
}
|
|
256
269
|
return copy;
|
|
257
270
|
}
|
|
271
|
+
function applyDeltasShared(spec, deltas) {
|
|
272
|
+
const out = { ...spec };
|
|
273
|
+
const copied = new WeakSet([out]);
|
|
274
|
+
for (const d of deltas) {
|
|
275
|
+
const cp = canonicalSpecPath(out, d.path);
|
|
276
|
+
if (!cp) continue;
|
|
277
|
+
const parts = cp.split(".");
|
|
278
|
+
let node = out;
|
|
279
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
280
|
+
const k = parts[i];
|
|
281
|
+
const child = node[k];
|
|
282
|
+
if (!child || typeof child !== "object") {
|
|
283
|
+
node = null;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
if (!copied.has(child)) {
|
|
287
|
+
const clone = Array.isArray(child) ? child.slice() : { ...child };
|
|
288
|
+
copied.add(clone);
|
|
289
|
+
node[k] = clone;
|
|
290
|
+
}
|
|
291
|
+
node = node[k];
|
|
292
|
+
}
|
|
293
|
+
if (node) {
|
|
294
|
+
const last = parts[parts.length - 1];
|
|
295
|
+
node[Array.isArray(node) ? Number(last) : last] = d.value;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
258
300
|
var HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
259
301
|
function hexToRgb(hex) {
|
|
260
302
|
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
@@ -339,7 +381,7 @@ function lerpPathD(a, b, k) {
|
|
|
339
381
|
}
|
|
340
382
|
function textStringsOf(sprite) {
|
|
341
383
|
if (sprite.kind === "text") return sprite.strings;
|
|
342
|
-
if (sprite.kind === "textBlock") return [sprite.text];
|
|
384
|
+
if (sprite.kind === "textBlock") return [sprite.number ? `${sprite.number.prefix ?? ""}\0${sprite.number.suffix ?? ""}` : sprite.text];
|
|
343
385
|
return null;
|
|
344
386
|
}
|
|
345
387
|
function textStringsDiffer(a, b, i) {
|
|
@@ -433,6 +475,28 @@ function easeSmooth(k) {
|
|
|
433
475
|
const c = Math.max(0, Math.min(1, k));
|
|
434
476
|
return c * c * (3 - 2 * c);
|
|
435
477
|
}
|
|
478
|
+
var SPRING_OMEGA = 8;
|
|
479
|
+
var SPRING_END = 1 - (1 + SPRING_OMEGA) * Math.exp(-SPRING_OMEGA);
|
|
480
|
+
function easeGlide(ease2, k) {
|
|
481
|
+
const c = Math.max(0, Math.min(1, k));
|
|
482
|
+
switch (ease2) {
|
|
483
|
+
case "step":
|
|
484
|
+
return c >= 1 ? 1 : 0;
|
|
485
|
+
case "linear":
|
|
486
|
+
return c;
|
|
487
|
+
case "in":
|
|
488
|
+
return c * c * c;
|
|
489
|
+
case "out": {
|
|
490
|
+
const u = 1 - c;
|
|
491
|
+
return 1 - u * u * u;
|
|
492
|
+
}
|
|
493
|
+
case "spring":
|
|
494
|
+
return c >= 1 ? 1 : (1 - (1 + SPRING_OMEGA * c) * Math.exp(-SPRING_OMEGA * c)) / SPRING_END;
|
|
495
|
+
default:
|
|
496
|
+
return c * c * (3 - 2 * c);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
var STEER_EASES = /* @__PURE__ */ new Set(["in", "out", "spring"]);
|
|
436
500
|
function structuralSignature(spec) {
|
|
437
501
|
return JSON.stringify([
|
|
438
502
|
spec.units,
|
|
@@ -468,7 +532,9 @@ function structuralSignature(spec) {
|
|
|
468
532
|
l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : l.sprite.kind === "textBlock" ? l.sprite.anchor ? [l.sprite.fontSize, l.sprite.anchor] : [l.sprite.fontSize] : void 0,
|
|
469
533
|
// Static rotation is baked into entities (a range draws a seeded
|
|
470
534
|
// angle). Appended only when set — same rule as `anchor` above.
|
|
471
|
-
...l.rotate !== void 0 ? [l.rotate] : []
|
|
535
|
+
...l.rotate !== void 0 ? [l.rotate] : [],
|
|
536
|
+
// `rock` is baked per entity like `pulse` (and draws a seeded phase).
|
|
537
|
+
...l.rock !== void 0 ? [{ rock: l.rock }] : []
|
|
472
538
|
];
|
|
473
539
|
})
|
|
474
540
|
]);
|
|
@@ -584,6 +650,7 @@ function sequenceSwapCompatible(prev, next) {
|
|
|
584
650
|
function transitionKey(tr) {
|
|
585
651
|
if (!tr || tr.type === "cut") return "cut";
|
|
586
652
|
if (tr.type === "morph") return `morph:${tr.dur}:${tr.text ?? "step"}`;
|
|
653
|
+
if (tr.type === "wipe") return `wipe:${tr.dur}:${JSON.stringify([tr.shape ?? "circle", tr.origin ?? null, tr.angle ?? 0, tr.feather ?? 0.08])}`;
|
|
587
654
|
return `fade:${tr.dur}`;
|
|
588
655
|
}
|
|
589
656
|
|
|
@@ -620,10 +687,7 @@ function timelineLocalTime(tl, t) {
|
|
|
620
687
|
return t;
|
|
621
688
|
}
|
|
622
689
|
function ease(k, x) {
|
|
623
|
-
|
|
624
|
-
if (k.ease === "step") return c >= 1 ? 1 : 0;
|
|
625
|
-
if (k.ease === "linear") return c;
|
|
626
|
-
return easeSmooth(c);
|
|
690
|
+
return easeGlide(k.ease, x);
|
|
627
691
|
}
|
|
628
692
|
function evalTrack(keys, start, tt, leaf) {
|
|
629
693
|
let from = start;
|
|
@@ -641,7 +705,7 @@ function evalTrack(keys, start, tt, leaf) {
|
|
|
641
705
|
}
|
|
642
706
|
return at(tt);
|
|
643
707
|
}
|
|
644
|
-
function resolveTimelineAt(spec, t) {
|
|
708
|
+
function resolveTimelineAt(spec, t, shared = false) {
|
|
645
709
|
const tl = spec.timeline;
|
|
646
710
|
if (!tl) return spec;
|
|
647
711
|
const base = withoutTimeline(spec);
|
|
@@ -653,7 +717,7 @@ function resolveTimelineAt(spec, t) {
|
|
|
653
717
|
const start = tl.loop ? keys[keys.length - 1].value : readSpecPath(base, path);
|
|
654
718
|
deltas.push({ t: 0, path, value: evalTrack(keys, start, tt, path.slice(path.lastIndexOf(".") + 1)) });
|
|
655
719
|
}
|
|
656
|
-
return applyDeltasToSpec(base, deltas);
|
|
720
|
+
return shared ? applyDeltasShared(base, deltas) : applyDeltasToSpec(base, deltas);
|
|
657
721
|
}
|
|
658
722
|
function nextKeyAfter(spec, path, t) {
|
|
659
723
|
const tl = spec.timeline;
|
|
@@ -772,7 +836,10 @@ function strokeSamples(s, halfSize, n) {
|
|
|
772
836
|
if (m < 2) return pts;
|
|
773
837
|
const smooth = s.curve !== "linear" && m >= 3;
|
|
774
838
|
const segs = m - 1;
|
|
775
|
-
|
|
839
|
+
if (n === void 0) {
|
|
840
|
+
n = Math.max(STROKE_SAMPLES, segs * 6 + 1);
|
|
841
|
+
if (!smooth) n = Math.ceil((n - 1) / segs) * segs + 1;
|
|
842
|
+
}
|
|
776
843
|
const at = (k) => pts[Math.max(0, Math.min(m - 1, k))];
|
|
777
844
|
const out = [];
|
|
778
845
|
for (let i = 0; i < n; i++) {
|
|
@@ -791,6 +858,22 @@ function strokeSamples(s, halfSize, n) {
|
|
|
791
858
|
}
|
|
792
859
|
return out;
|
|
793
860
|
}
|
|
861
|
+
function strokeDrawnSamples(s, halfSize) {
|
|
862
|
+
const pts = strokeSamples(s, halfSize);
|
|
863
|
+
const span = drawSpan(s.draw);
|
|
864
|
+
return span ? trimPolyline(pts, span[0], span[1]) : pts;
|
|
865
|
+
}
|
|
866
|
+
function strokeDrawnAt(s, halfSize, drawn) {
|
|
867
|
+
if (!s.draw) return void 0;
|
|
868
|
+
const total = pathLength(strokeSamples(s, halfSize));
|
|
869
|
+
if (!(total > 0)) return void 0;
|
|
870
|
+
const a = drawSpan(s.draw)?.[0] ?? 0;
|
|
871
|
+
let run = 0;
|
|
872
|
+
return drawn.map((p, i) => {
|
|
873
|
+
if (i > 0) run += Math.hypot(p.x - drawn[i - 1].x, p.y - drawn[i - 1].y);
|
|
874
|
+
return a + run / total;
|
|
875
|
+
});
|
|
876
|
+
}
|
|
794
877
|
function pathLength(pts) {
|
|
795
878
|
let l = 0;
|
|
796
879
|
for (let i = 1; i < pts.length; i++) {
|
|
@@ -800,6 +883,83 @@ function pathLength(pts) {
|
|
|
800
883
|
}
|
|
801
884
|
return l;
|
|
802
885
|
}
|
|
886
|
+
function drawSpan(d) {
|
|
887
|
+
if (!d) return null;
|
|
888
|
+
const a = Math.max(0, Math.min(1, d.start ?? 0));
|
|
889
|
+
const b = Math.max(0, Math.min(1, d.end ?? 1));
|
|
890
|
+
return a <= 0 && b >= 1 ? null : [a, b];
|
|
891
|
+
}
|
|
892
|
+
function trimPolyline(pts, a, b) {
|
|
893
|
+
if (pts.length < 2 || b <= a) return [];
|
|
894
|
+
const total = pathLength(pts);
|
|
895
|
+
if (!(total > 0)) return [];
|
|
896
|
+
const from = a * total;
|
|
897
|
+
const to = b * total;
|
|
898
|
+
const out = [];
|
|
899
|
+
let run = 0;
|
|
900
|
+
for (let i = 1; i < pts.length; i++) {
|
|
901
|
+
const p = pts[i - 1];
|
|
902
|
+
const q = pts[i];
|
|
903
|
+
const seg = Math.hypot(q.x - p.x, q.y - p.y);
|
|
904
|
+
const s0 = run;
|
|
905
|
+
const s1 = run + seg;
|
|
906
|
+
run = s1;
|
|
907
|
+
if (s1 < from || seg <= 0) continue;
|
|
908
|
+
if (s0 > to) break;
|
|
909
|
+
const at = (s) => {
|
|
910
|
+
const k = Math.max(0, Math.min(1, (s - s0) / seg));
|
|
911
|
+
return { x: p.x + (q.x - p.x) * k, y: p.y + (q.y - p.y) * k };
|
|
912
|
+
};
|
|
913
|
+
if (out.length === 0) out.push(s0 >= from ? { x: p.x, y: p.y } : at(from));
|
|
914
|
+
out.push(s1 <= to ? { x: q.x, y: q.y } : at(to));
|
|
915
|
+
if (s1 >= to) break;
|
|
916
|
+
}
|
|
917
|
+
return out.length >= 2 ? out : [];
|
|
918
|
+
}
|
|
919
|
+
function trimLines(lines, a, b) {
|
|
920
|
+
const key = `${a}|${b}`;
|
|
921
|
+
let byLines = trimCache.get(lines);
|
|
922
|
+
const hit = byLines?.get(key);
|
|
923
|
+
if (hit) return hit;
|
|
924
|
+
const lens = lines.map((l) => {
|
|
925
|
+
const pts = l.points.map(([x, y]) => ({ x, y }));
|
|
926
|
+
if (l.closed && pts.length > 1) pts.push(pts[0]);
|
|
927
|
+
return pathLength(pts);
|
|
928
|
+
});
|
|
929
|
+
const total = lens.reduce((m, v) => m + v, 0);
|
|
930
|
+
const out = [];
|
|
931
|
+
if (total > 0 && b > a) {
|
|
932
|
+
const from = a * total;
|
|
933
|
+
const to = b * total;
|
|
934
|
+
let run = 0;
|
|
935
|
+
lines.forEach((l, i) => {
|
|
936
|
+
const len = lens[i];
|
|
937
|
+
const s0 = run;
|
|
938
|
+
run += len;
|
|
939
|
+
if (len <= 0 || run < from || s0 > to) return;
|
|
940
|
+
if (s0 >= from && run <= to) {
|
|
941
|
+
out.push(l);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const pts = l.points.map(([x, y]) => ({ x, y }));
|
|
945
|
+
if (l.closed && pts.length > 1) pts.push(pts[0]);
|
|
946
|
+
const cut = trimPolyline(pts, (Math.max(from, s0) - s0) / len, (Math.min(to, run) - s0) / len);
|
|
947
|
+
if (cut.length >= 2) out.push({ points: cut.map((q) => [q.x, q.y]), closed: false });
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
if (!byLines) {
|
|
951
|
+
byLines = /* @__PURE__ */ new Map();
|
|
952
|
+
trimCache.set(lines, byLines);
|
|
953
|
+
}
|
|
954
|
+
if (byLines.size > 64) byLines.clear();
|
|
955
|
+
byLines.set(key, out);
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
958
|
+
var trimCache = /* @__PURE__ */ new WeakMap();
|
|
959
|
+
function strokeLines(part) {
|
|
960
|
+
const span = drawSpan(part.stroke?.draw);
|
|
961
|
+
return span ? trimLines(part.flat.lines, span[0], span[1]) : part.flat.lines;
|
|
962
|
+
}
|
|
803
963
|
function strokeTaper(u) {
|
|
804
964
|
return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
|
|
805
965
|
}
|
|
@@ -844,10 +1004,10 @@ function ribbonSides(pts, half) {
|
|
|
844
1004
|
}
|
|
845
1005
|
return { left, right };
|
|
846
1006
|
}
|
|
847
|
-
function strokeOutline(pts, lw, taper = true) {
|
|
1007
|
+
function strokeOutline(pts, lw, taper = true, at) {
|
|
848
1008
|
const n = pts.length;
|
|
849
1009
|
if (n < 2) return [];
|
|
850
|
-
const half = (i) => Math.max(0.5, taper ? lw * strokeTaper(i / (n - 1)) : lw) / 2;
|
|
1010
|
+
const half = (i) => Math.max(0.5, taper ? lw * strokeTaper(at?.[i] ?? i / (n - 1)) : lw) / 2;
|
|
851
1011
|
const dir = (a, b) => {
|
|
852
1012
|
const dx = b.x - a.x;
|
|
853
1013
|
const dy = b.y - a.y;
|
|
@@ -934,6 +1094,41 @@ function pathParts(s) {
|
|
|
934
1094
|
spriteParts.set(s, { key, parts });
|
|
935
1095
|
return parts;
|
|
936
1096
|
}
|
|
1097
|
+
function partInkArea(part, sx, sy, r) {
|
|
1098
|
+
if (part.opacity <= 0) return { fill: 0, stroke: 0 };
|
|
1099
|
+
const fill = part.fill ? polygonArea(part.flat.points.map(([x, y]) => ({ x: x * sx, y: y * sy }))) * part.opacity : 0;
|
|
1100
|
+
let stroke = 0;
|
|
1101
|
+
if (part.stroke && part.stroke.width > 0) {
|
|
1102
|
+
for (const l of strokeLines(part)) {
|
|
1103
|
+
const pts = l.points.map(([x, y]) => ({ x: x * sx, y: y * sy }));
|
|
1104
|
+
if (l.closed && pts.length > 1) pts.push(pts[0]);
|
|
1105
|
+
stroke += pathLength(pts) * part.stroke.width * r;
|
|
1106
|
+
}
|
|
1107
|
+
stroke *= (part.stroke.opacity ?? 1) * part.opacity;
|
|
1108
|
+
}
|
|
1109
|
+
return { fill, stroke };
|
|
1110
|
+
}
|
|
1111
|
+
function gradientStopWeights(stops) {
|
|
1112
|
+
const at = stops.map((st) => Math.min(1, Math.max(0, st.at)));
|
|
1113
|
+
const order = at.map((_, i) => i).sort((a, b) => at[a] - at[b]);
|
|
1114
|
+
const out = new Array(stops.length).fill(0);
|
|
1115
|
+
order.forEach((i, rank) => {
|
|
1116
|
+
const prev = rank > 0 ? at[order[rank - 1]] : null;
|
|
1117
|
+
const next = rank < order.length - 1 ? at[order[rank + 1]] : null;
|
|
1118
|
+
const a = at[i];
|
|
1119
|
+
out[i] = (prev === null ? a : (a - prev) / 2) + (next === null ? 1 - a : (next - a) / 2);
|
|
1120
|
+
});
|
|
1121
|
+
const sum = out.reduce((x, y) => x + y, 0);
|
|
1122
|
+
return sum > 0 ? out.map((x) => x / sum) : out.map(() => 1 / Math.max(1, stops.length));
|
|
1123
|
+
}
|
|
1124
|
+
function pathInkArea(s, sx, sy, r) {
|
|
1125
|
+
let area = 0;
|
|
1126
|
+
for (const part of pathParts(s)) {
|
|
1127
|
+
const a = partInkArea(part, sx, sy, r);
|
|
1128
|
+
area += a.fill + a.stroke;
|
|
1129
|
+
}
|
|
1130
|
+
return area;
|
|
1131
|
+
}
|
|
937
1132
|
function pathFrame(s, r, w, h) {
|
|
938
1133
|
const span = s.span ?? "box";
|
|
939
1134
|
const sx = span === "width" || span === "screen" ? w / 2 : r;
|
|
@@ -1231,10 +1426,33 @@ function inside(pt, r) {
|
|
|
1231
1426
|
}
|
|
1232
1427
|
return c;
|
|
1233
1428
|
}
|
|
1429
|
+
var ON_EDGE = 2 * PATH_TOLERANCE;
|
|
1430
|
+
function onEdge(pt, r) {
|
|
1431
|
+
for (let k = 0, j = r.length - 1; k < r.length; j = k++) {
|
|
1432
|
+
const a = r[j], b = r[k];
|
|
1433
|
+
const dx = b[0] - a[0], dy = b[1] - a[1];
|
|
1434
|
+
const len2 = dx * dx + dy * dy;
|
|
1435
|
+
const t = len2 > 0 ? Math.max(0, Math.min(1, ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / len2)) : 0;
|
|
1436
|
+
const ex = a[0] + dx * t - pt[0], ey = a[1] + dy * t - pt[1];
|
|
1437
|
+
if (ex * ex + ey * ey <= ON_EDGE * ON_EDGE) return true;
|
|
1438
|
+
}
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1234
1441
|
function containsRing(r, rb, sub, sb) {
|
|
1235
|
-
if (sb.x0 < rb.x0 || sb.x1 > rb.x1 || sb.y0 < rb.y0 || sb.y1 > rb.y1) return false;
|
|
1236
|
-
|
|
1237
|
-
|
|
1442
|
+
if (sb.x0 < rb.x0 - ON_EDGE || sb.x1 > rb.x1 + ON_EDGE || sb.y0 < rb.y0 - ON_EDGE || sb.y1 > rb.y1 + ON_EDGE) return false;
|
|
1443
|
+
let strictly = false;
|
|
1444
|
+
for (const p of sub) {
|
|
1445
|
+
if (inside(p, r)) strictly = true;
|
|
1446
|
+
else if (!onEdge(p, r)) return false;
|
|
1447
|
+
}
|
|
1448
|
+
if (strictly) return true;
|
|
1449
|
+
let cx = 0, cy = 0;
|
|
1450
|
+
for (const p of sub) {
|
|
1451
|
+
cx += p[0];
|
|
1452
|
+
cy += p[1];
|
|
1453
|
+
}
|
|
1454
|
+
const c = [cx / sub.length, cy / sub.length];
|
|
1455
|
+
return !onEdge(c, r) && inside(c, r);
|
|
1238
1456
|
}
|
|
1239
1457
|
function boxOf(r) {
|
|
1240
1458
|
const b = { x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity };
|
|
@@ -1383,6 +1601,7 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
1383
1601
|
"emit",
|
|
1384
1602
|
"clock",
|
|
1385
1603
|
"rotate",
|
|
1604
|
+
"rock",
|
|
1386
1605
|
"opacity",
|
|
1387
1606
|
"transform",
|
|
1388
1607
|
"group"
|
|
@@ -1393,6 +1612,7 @@ var GROUP_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
|
1393
1612
|
var KNOWN_POSITION = /* @__PURE__ */ new Set(["x", "y", "dx", "dy"]);
|
|
1394
1613
|
var KNOWN_TIMELINE = /* @__PURE__ */ new Set(["loop", "duration", "keys"]);
|
|
1395
1614
|
var KNOWN_TIMELINE_KEY = /* @__PURE__ */ new Set(["t", "path", "value", "ease", "dur"]);
|
|
1615
|
+
var GLIDE_EASES = /* @__PURE__ */ new Set(["step", "linear", "smooth", "in", "out", "spring"]);
|
|
1396
1616
|
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
|
|
1397
1617
|
var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
|
|
1398
1618
|
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
@@ -1400,28 +1620,89 @@ var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "f
|
|
|
1400
1620
|
var KNOWN_BAR = /* @__PURE__ */ new Set(["kind", "values", "length", "thickness", "color", "max", "direction", "colors", "colorWeights"]);
|
|
1401
1621
|
var KNOWN_PATH_PART = /* @__PURE__ */ new Set(["path", "color", "fillRule", "fill", "gradient", "stroke", "opacity"]);
|
|
1402
1622
|
var KNOWN_POLYGON = /* @__PURE__ */ new Set(["kind", "radius", "color", "sides", "points", "path", "fillRule", "fill", "gradient", "stroke", "parts", "span", "origin", "soft", "colors", "colorWeights"]);
|
|
1403
|
-
var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "colors", "colorWeights"]);
|
|
1623
|
+
var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "draw", "colors", "colorWeights"]);
|
|
1404
1624
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
1405
1625
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle", "role"]);
|
|
1406
|
-
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal", "anchor", "font", "opacity", "role"]);
|
|
1626
|
+
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal", "anchor", "font", "opacity", "role", "number"]);
|
|
1627
|
+
var KNOWN_TEXT_NUMBER = /* @__PURE__ */ new Set(["value", "decimals", "prefix", "suffix", "grouping"]);
|
|
1407
1628
|
var TEXT_ROLES = /* @__PURE__ */ new Set(["read", "atmosphere"]);
|
|
1408
1629
|
var TEXT_BLOCK_ANCHORS = /* @__PURE__ */ new Set(["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"]);
|
|
1409
1630
|
var FONT_SIZE_RE = /\d(?:px|pt|pc|em|rem|ex|ch|vw|vh|vmin|vmax|%)/i;
|
|
1410
|
-
var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret", "fade"]);
|
|
1631
|
+
var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret", "fade", "rise"]);
|
|
1632
|
+
var FOREIGN_FIELD_HINTS = {
|
|
1633
|
+
top: {
|
|
1634
|
+
duration: "a scene has no length \u2014 loop authored change with `timeline: { loop: true, duration }`, or give a sequence segment a `duration`",
|
|
1635
|
+
loop: "use `timeline: { loop: true, duration, keys }`",
|
|
1636
|
+
fps: "a scene is a pure function of time \u2014 there is no frame rate to set",
|
|
1637
|
+
frameRate: "a scene is a pure function of time \u2014 there is no frame rate to set",
|
|
1638
|
+
bpm: "there is no tempo unit \u2014 write times in ms (one beat = 60000 / bpm)",
|
|
1639
|
+
tempo: "there is no tempo unit \u2014 write times in ms (one beat = 60000 / bpm)",
|
|
1640
|
+
camera: "use a `groups.<name>.transform` (a camera over its member layers) or a layer `transform`",
|
|
1641
|
+
keyframes: "use `timeline: { keys: [{ t, path, value, dur, ease }] }`",
|
|
1642
|
+
palette: "colours live on sprites (`color`, or `colors` for a seeded per-entity pick)"
|
|
1643
|
+
},
|
|
1644
|
+
layer: {
|
|
1645
|
+
delay: "use `life.enter` (ms before the layer fades in)",
|
|
1646
|
+
stagger: "use `life.stagger` (ms between successive entities' entrances)",
|
|
1647
|
+
duration: "use `life.exit` to end a layer (ms), or a sequence segment's `duration`",
|
|
1648
|
+
ease: "eases live on timeline keys and steers (`ease: 'out'`), or on drift/rise/wander motion (`motion.ease`)",
|
|
1649
|
+
easing: "eases live on timeline keys and steers (`ease: 'out'`), or on drift/rise/wander motion (`motion.ease`)",
|
|
1650
|
+
rotation: "use `rotate` (static degrees), `spin` (degrees/sec) or `rock` (a swing back and forth)",
|
|
1651
|
+
wobble: "use `rock` (a swing back and forth) or the motion's `bob` / `sway`",
|
|
1652
|
+
sway: "use `rock` (a swing back and forth), or `sway` on a rise motion",
|
|
1653
|
+
scale: "use `transform.scale` (paint, glides) or `grow` (breathing)",
|
|
1654
|
+
fade: "use `life.fade` (ms) or the layer's paint `opacity`",
|
|
1655
|
+
blur: "there is no blur \u2014 soften with `soft: true` (circle/polygon), `rect.feather`, `trail` or `ghosting`",
|
|
1656
|
+
glow: "glow is `blend: 'lighter'` over soft circles",
|
|
1657
|
+
keyframes: "use the scene's `timeline` (keys on this layer's paths)",
|
|
1658
|
+
animation: "use the scene's `timeline` (keys on this layer's paths)"
|
|
1659
|
+
},
|
|
1660
|
+
key: {
|
|
1661
|
+
duration: "use `dur` (the glide length, ms)",
|
|
1662
|
+
delay: "use `t` (when the glide starts, ms)",
|
|
1663
|
+
time: "use `t` (when the glide starts, ms)",
|
|
1664
|
+
at: "use `t` (when the glide starts, ms)",
|
|
1665
|
+
start: "use `t` (when the glide starts, ms)",
|
|
1666
|
+
easing: "use `ease` ('smooth' | 'linear' | 'step' | 'in' | 'out' | 'spring')",
|
|
1667
|
+
to: "use `value`",
|
|
1668
|
+
target: "use `value`",
|
|
1669
|
+
from: "a key glides from wherever its path is \u2014 set the starting value on the base spec"
|
|
1670
|
+
},
|
|
1671
|
+
motion: {
|
|
1672
|
+
delay: "use the layer's `life.enter`",
|
|
1673
|
+
easing: "use `ease: { type: 'settle' | 'buoyant', tau }` on drift / rise / wander",
|
|
1674
|
+
duration: "only `path` motion has a `duration` (ms per lap)"
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
function unknownMessage(where, k, plain) {
|
|
1678
|
+
const hint = Object.prototype.hasOwnProperty.call(FOREIGN_FIELD_HINTS[where], k) ? FOREIGN_FIELD_HINTS[where][k] : void 0;
|
|
1679
|
+
return hint ? `${plain} \u2014 ${hint}` : plain;
|
|
1680
|
+
}
|
|
1411
1681
|
var KNOWN_REVEAL_CARET = /* @__PURE__ */ new Set(["blink", "color"]);
|
|
1412
|
-
var
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1682
|
+
var MOTION_FIELDS = {
|
|
1683
|
+
drift: ["speed", "angle", "bidirectional", "bob", "ease"],
|
|
1684
|
+
rise: ["speed", "sway", "ease"],
|
|
1685
|
+
bounce: ["speed"],
|
|
1686
|
+
static: [],
|
|
1687
|
+
orbit: ["speed", "radius", "center"],
|
|
1688
|
+
wander: ["speed", "angle", "meander", "coherence", "ease"],
|
|
1689
|
+
warp: ["speed", "center"],
|
|
1690
|
+
path: ["points", "duration", "curve", "closed", "scatter"]
|
|
1691
|
+
};
|
|
1692
|
+
var motionKnown = (t) => /* @__PURE__ */ new Set(["type", ...MOTION_FIELDS[t]]);
|
|
1693
|
+
var KNOWN_DRIFT = motionKnown("drift");
|
|
1694
|
+
var KNOWN_RISE = motionKnown("rise");
|
|
1695
|
+
var KNOWN_BOUNCE = motionKnown("bounce");
|
|
1696
|
+
var KNOWN_STATIC = motionKnown("static");
|
|
1697
|
+
var KNOWN_ORBIT = motionKnown("orbit");
|
|
1698
|
+
var KNOWN_WANDER = motionKnown("wander");
|
|
1699
|
+
var KNOWN_WARP = motionKnown("warp");
|
|
1700
|
+
var KNOWN_PATH = motionKnown("path");
|
|
1420
1701
|
var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
1421
1702
|
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
1422
1703
|
var KNOWN_BG_FIELD = /* @__PURE__ */ new Set(["type", "scale", "octaves", "warp", "quantize", "bands", "drift", "seed"]);
|
|
1423
1704
|
var KNOWN_FIELD_DRIFT = /* @__PURE__ */ new Set(["period", "amount"]);
|
|
1424
|
-
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key", "emit", "clock", "life", "layout", "rotate"]);
|
|
1705
|
+
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key", "emit", "clock", "life", "layout", "rotate", "rock"]);
|
|
1425
1706
|
function unknownKeys(obj, known) {
|
|
1426
1707
|
return Object.keys(obj).filter((k) => !known.has(k));
|
|
1427
1708
|
}
|
|
@@ -1456,9 +1737,18 @@ function validateSpec(spec) {
|
|
|
1456
1737
|
}
|
|
1457
1738
|
function ignoredPropertyPaths(spec) {
|
|
1458
1739
|
if (!isObj(spec)) return [];
|
|
1459
|
-
return (validateSpecCore(spec).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
|
|
1740
|
+
return (validateSpecCore(spec, false).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
|
|
1741
|
+
}
|
|
1742
|
+
function validateSpecPaths(spec, paths, keyed = false) {
|
|
1743
|
+
if (!keyed) return specPathsValid(spec, paths);
|
|
1744
|
+
keyedPathDepth++;
|
|
1745
|
+
try {
|
|
1746
|
+
return specPathsValid(spec, paths);
|
|
1747
|
+
} finally {
|
|
1748
|
+
keyedPathDepth--;
|
|
1749
|
+
}
|
|
1460
1750
|
}
|
|
1461
|
-
function
|
|
1751
|
+
function specPathsValid(spec, paths) {
|
|
1462
1752
|
const layers = /* @__PURE__ */ new Set();
|
|
1463
1753
|
let background = false;
|
|
1464
1754
|
let finish = false;
|
|
@@ -1494,7 +1784,7 @@ function validateSpecPaths(spec, paths) {
|
|
|
1494
1784
|
}
|
|
1495
1785
|
return ok;
|
|
1496
1786
|
}
|
|
1497
|
-
function validateSpecCore(spec) {
|
|
1787
|
+
function validateSpecCore(spec, checkApplied = true) {
|
|
1498
1788
|
const errors = [];
|
|
1499
1789
|
const warnings = [];
|
|
1500
1790
|
const err = (path, message) => void errors.push({ path, message });
|
|
@@ -1530,7 +1820,7 @@ function validateSpecCore(spec) {
|
|
|
1530
1820
|
err("ghosting", `must be a number 0..${LIMITS.maxGhosting}`);
|
|
1531
1821
|
}
|
|
1532
1822
|
for (const k of unknownKeys(spec, KNOWN_TOP)) {
|
|
1533
|
-
warn(k, "unknown-property", `unknown top-level property '${k}' \u2014 will be ignored`);
|
|
1823
|
+
warn(k, "unknown-property", unknownMessage("top", k, `unknown top-level property '${k}' \u2014 will be ignored`));
|
|
1534
1824
|
}
|
|
1535
1825
|
if (spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
1536
1826
|
if (spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
|
|
@@ -1563,7 +1853,7 @@ function validateSpecCore(spec) {
|
|
|
1563
1853
|
});
|
|
1564
1854
|
if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
|
|
1565
1855
|
}
|
|
1566
|
-
if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn);
|
|
1856
|
+
if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn, checkApplied);
|
|
1567
1857
|
return { valid: errors.length === 0, errors, warnings };
|
|
1568
1858
|
}
|
|
1569
1859
|
function validateTransform(tf, path, err, warn, spec, anchorOk) {
|
|
@@ -1601,7 +1891,15 @@ function validateGroups(groups, err, warn, spec) {
|
|
|
1601
1891
|
if (g.transform !== void 0) validateTransform(g.transform, `${p}.transform`, err, warn, spec, false);
|
|
1602
1892
|
}
|
|
1603
1893
|
}
|
|
1604
|
-
function validateTimeline(spec, errOuter, warn) {
|
|
1894
|
+
function validateTimeline(spec, errOuter, warn, checkApplied = true) {
|
|
1895
|
+
keyedPathDepth++;
|
|
1896
|
+
try {
|
|
1897
|
+
validateTimelineKeys(spec, errOuter, warn, checkApplied);
|
|
1898
|
+
} finally {
|
|
1899
|
+
keyedPathDepth--;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
function validateTimelineKeys(spec, errOuter, warn, checkApplied) {
|
|
1605
1903
|
let failed = false;
|
|
1606
1904
|
const err = (p, m) => {
|
|
1607
1905
|
failed = true;
|
|
@@ -1624,13 +1922,13 @@ function validateTimeline(spec, errOuter, warn) {
|
|
|
1624
1922
|
tl.keys.forEach((key, i) => {
|
|
1625
1923
|
const p = `timeline.keys[${i}]`;
|
|
1626
1924
|
if (!isObj(key)) return err(p, "must be an object {t, path, value, ease?, dur?}");
|
|
1627
|
-
for (const k of unknownKeys(key, KNOWN_TIMELINE_KEY)) warn(`${p}.${k}`, "unknown-property", `unknown key property '${k}' \u2014 will be ignored`);
|
|
1925
|
+
for (const k of unknownKeys(key, KNOWN_TIMELINE_KEY)) warn(`${p}.${k}`, "unknown-property", unknownMessage("key", k, `unknown key property '${k}' \u2014 will be ignored`));
|
|
1628
1926
|
let ok = true;
|
|
1629
1927
|
if (!isNum(key.t) || key.t < 0) {
|
|
1630
1928
|
err(`${p}.t`, "must be a number of ms >= 0");
|
|
1631
1929
|
ok = false;
|
|
1632
1930
|
}
|
|
1633
|
-
if (key.ease !== void 0 &&
|
|
1931
|
+
if (key.ease !== void 0 && !GLIDE_EASES.has(key.ease)) err(`${p}.ease`, "must be 'step' | 'linear' | 'smooth' | 'in' | 'out' | 'spring'");
|
|
1634
1932
|
if (key.dur !== void 0 && (!isNum(key.dur) || key.dur < 0 || key.dur > LIMITS.maxTimelineKeyDur)) {
|
|
1635
1933
|
err(`${p}.dur`, `must be a number of ms, 0..${LIMITS.maxTimelineKeyDur}`);
|
|
1636
1934
|
}
|
|
@@ -1655,20 +1953,28 @@ function validateTimeline(spec, errOuter, warn) {
|
|
|
1655
1953
|
if (!ok || !cp) return;
|
|
1656
1954
|
const t = key.t;
|
|
1657
1955
|
latest = Math.max(latest, t);
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1956
|
+
if (checkApplied) {
|
|
1957
|
+
const applied = applyDeltasShared(base, [{ t: 0, path: cp, value: key.value }]);
|
|
1958
|
+
const r = validateSpecPaths(applied, [cp]) ? { valid: true, errors: [] } : validateSpec(structuredClone(applied));
|
|
1959
|
+
if (!r.valid) {
|
|
1960
|
+
for (const e of r.errors) err(`${p}.value`, `applied at '${key.path}' \u2192 ${e.path || "<root>"}: ${e.message}`);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (isObj(key.value) && cp.endsWith("transform")) {
|
|
1964
|
+
const before = readSpecPath(base, cp);
|
|
1965
|
+
const o0 = isObj(before) ? before.origin ?? "viewport" : "viewport";
|
|
1966
|
+
const o1 = key.value.origin ?? "viewport";
|
|
1967
|
+
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`);
|
|
1968
|
+
}
|
|
1969
|
+
if (cp.endsWith(".color")) {
|
|
1970
|
+
const owner = readSpecPath(base, cp.slice(0, -".color".length));
|
|
1971
|
+
if (isObj(owner) && owner.gradient !== void 0) {
|
|
1972
|
+
warn(`${p}.path`, "timeline-key-no-effect", `'${key.path}' is overridden by ${cp.includes(".parts.") ? "that part's" : "the sprite's"} gradient, so this key changes nothing on screen \u2014 key its gradient's stops (\`\u2026gradient.stops.N.color\`) instead`);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (structuralSignature(applied) !== baseSig) {
|
|
1976
|
+
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`);
|
|
1977
|
+
}
|
|
1672
1978
|
}
|
|
1673
1979
|
const list = times.get(cp) ?? [];
|
|
1674
1980
|
list.push(t);
|
|
@@ -1694,16 +2000,16 @@ function validateTimeline(spec, errOuter, warn) {
|
|
|
1694
2000
|
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)`);
|
|
1695
2001
|
}
|
|
1696
2002
|
}
|
|
1697
|
-
if (failed) return;
|
|
2003
|
+
if (failed || !checkApplied) return;
|
|
1698
2004
|
const keyed = [...timelineTracks(spec).keys()];
|
|
1699
2005
|
const seen = /* @__PURE__ */ new Set();
|
|
1700
2006
|
for (const t of timelineSampleTimes(spec, true, 120)) {
|
|
1701
|
-
const r = resolveTimelineAt(spec, t);
|
|
2007
|
+
const r = resolveTimelineAt(spec, t, true);
|
|
1702
2008
|
const state = JSON.stringify(keyed.map((p) => readSpecPath(r, p)));
|
|
1703
2009
|
if (seen.has(state)) continue;
|
|
1704
2010
|
seen.add(state);
|
|
1705
2011
|
if (validateSpecPaths(r, keyed)) continue;
|
|
1706
|
-
const e = validateSpec(r).errors[0];
|
|
2012
|
+
const e = validateSpec(structuredClone(r)).errors[0];
|
|
1707
2013
|
err("timeline", `at t = ${Math.round(t)} ms the keys combine into an invalid scene \u2014 ${e ? `${e.path || "<root>"}: ${e.message}` : "invalid"}`);
|
|
1708
2014
|
return;
|
|
1709
2015
|
}
|
|
@@ -1809,7 +2115,7 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
1809
2115
|
} else if (k === "depth") {
|
|
1810
2116
|
warn(`${path}.depth`, "unknown-property", `'depth' is not a layer property \u2014 use motion type 'warp' for true perspective depth, or fake parallax with size/speed/alpha across layers`);
|
|
1811
2117
|
} else {
|
|
1812
|
-
warn(`${path}.${k}`, "unknown-property", `unknown layer property '${k}' \u2014 will be ignored`);
|
|
2118
|
+
warn(`${path}.${k}`, "unknown-property", unknownMessage("layer", k, `unknown layer property '${k}' \u2014 will be ignored`));
|
|
1813
2119
|
}
|
|
1814
2120
|
}
|
|
1815
2121
|
if (!isNum(layer.count) || !Number.isInteger(layer.count) || layer.count < 1) {
|
|
@@ -1972,14 +2278,17 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
1972
2278
|
if (lay.gap !== void 0) {
|
|
1973
2279
|
if (isNum(lay.gap)) {
|
|
1974
2280
|
if (!gapOk(lay.gap)) err(`${path}.layout.gap`, "must be > 0 (viewport units of min(w,h), or px)");
|
|
1975
|
-
} else if (
|
|
2281
|
+
} else if (isObj(lay.gap)) {
|
|
1976
2282
|
if (lay.gap.x !== void 0 && !gapOk(lay.gap.x)) err(`${path}.layout.gap.x`, "must be > 0");
|
|
1977
2283
|
if (lay.gap.y !== void 0 && !gapOk(lay.gap.y)) err(`${path}.layout.gap.y`, "must be > 0");
|
|
1978
2284
|
for (const k of unknownKeys(lay.gap, /* @__PURE__ */ new Set(["x", "y"]))) {
|
|
1979
2285
|
warn(`${path}.layout.gap.${k}`, "unknown-property", `unknown gap property '${k}' \u2014 will be ignored`);
|
|
1980
2286
|
}
|
|
2287
|
+
if (lay.type === "list" && lay.gap.x !== void 0) {
|
|
2288
|
+
warn(`${path}.layout.gap.x`, "no-effect", "a list is one column, so gap.x has no effect; gap.y is its row pitch (use a table for columns)");
|
|
2289
|
+
}
|
|
1981
2290
|
} else {
|
|
1982
|
-
err(`${path}.layout.gap`,
|
|
2291
|
+
err(`${path}.layout.gap`, "must be a number > 0 or { x?, y? }");
|
|
1983
2292
|
}
|
|
1984
2293
|
}
|
|
1985
2294
|
for (const k of unknownKeys(lay, new Set(lay.type === "list" ? ["type", "gap"] : ["type", "columns", "gap"]))) {
|
|
@@ -2030,6 +2339,26 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
2030
2339
|
if (layer.life.fade !== void 0 && (!isNum(layer.life.fade) || layer.life.fade < 0)) {
|
|
2031
2340
|
err(`${path}.life.fade`, "must be >= 0 (ms)");
|
|
2032
2341
|
}
|
|
2342
|
+
if (layer.life.stagger !== void 0 && (!isNum(layer.life.stagger) || layer.life.stagger < 0 || layer.life.stagger > LIMITS.maxLifeStagger)) {
|
|
2343
|
+
err(`${path}.life.stagger`, `must be 0..${LIMITS.maxLifeStagger} ms (between successive entities' entrances)`);
|
|
2344
|
+
}
|
|
2345
|
+
for (const k of unknownKeys(layer.life, /* @__PURE__ */ new Set(["enter", "exit", "fade", "stagger"]))) {
|
|
2346
|
+
warn(`${path}.life.${k}`, "unknown-property", `unknown life property '${k}' \u2014 will be ignored`);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
if (layer.rock !== void 0) {
|
|
2351
|
+
if (!isObj(layer.rock)) err(`${path}.rock`, "must be an object { amp, period }");
|
|
2352
|
+
else {
|
|
2353
|
+
if (!isNum(layer.rock.amp) || layer.rock.amp <= 0 || layer.rock.amp > LIMITS.maxRockAmp) {
|
|
2354
|
+
err(`${path}.rock.amp`, `must be > 0 and <= ${LIMITS.maxRockAmp} (degrees either side of rest)`);
|
|
2355
|
+
}
|
|
2356
|
+
if (!isNum(layer.rock.period) || layer.rock.period < LIMITS.minRockPeriod) {
|
|
2357
|
+
err(`${path}.rock.period`, `must be >= ${LIMITS.minRockPeriod} ms (one swing there and back)`);
|
|
2358
|
+
}
|
|
2359
|
+
for (const k of unknownKeys(layer.rock, /* @__PURE__ */ new Set(["amp", "period"]))) {
|
|
2360
|
+
warn(`${path}.rock.${k}`, "unknown-property", `unknown rock property '${k}' \u2014 will be ignored`);
|
|
2361
|
+
}
|
|
2033
2362
|
}
|
|
2034
2363
|
}
|
|
2035
2364
|
if (layer.trail !== void 0) {
|
|
@@ -2091,6 +2420,9 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
2091
2420
|
if (isObj(layer.grow) && isNum(layer.grow.period) && layer.grow.period < floor) {
|
|
2092
2421
|
err(`${path}.grow.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
|
|
2093
2422
|
}
|
|
2423
|
+
if (isObj(layer.rock) && isNum(layer.rock.period) && layer.rock.period < floor) {
|
|
2424
|
+
err(`${path}.rock.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
|
|
2425
|
+
}
|
|
2094
2426
|
const sp = layer.sprite;
|
|
2095
2427
|
if (isObj(sp) && isObj(sp.cycle) && isNum(sp.cycle.period) && sp.cycle.period < floor) {
|
|
2096
2428
|
err(`${path}.sprite.cycle.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
|
|
@@ -2222,6 +2554,7 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2222
2554
|
if (sprite.curve !== void 0 && sprite.curve !== "smooth" && sprite.curve !== "linear") err(`${path}.curve`, "must be 'smooth' | 'linear'");
|
|
2223
2555
|
if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
|
|
2224
2556
|
if (sprite.orient !== void 0 && typeof sprite.orient !== "boolean") err(`${path}.orient`, "must be a boolean");
|
|
2557
|
+
if (sprite.draw !== void 0) validateDraw(sprite.draw, `${path}.draw`, err, warn);
|
|
2225
2558
|
color(sprite.color, `${path}.color`, err);
|
|
2226
2559
|
validatePalette(sprite, path, err);
|
|
2227
2560
|
} else if (sprite.kind === "textBlock") {
|
|
@@ -2262,6 +2595,22 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2262
2595
|
err(`${path}.opacity`, "must be a number between 0 and 1");
|
|
2263
2596
|
}
|
|
2264
2597
|
validateTextRole(sprite, path, err);
|
|
2598
|
+
if (sprite.number !== void 0) {
|
|
2599
|
+
const n = sprite.number;
|
|
2600
|
+
if (!isObj(n)) err(`${path}.number`, "must be { value, decimals?, prefix?, suffix?, grouping? }");
|
|
2601
|
+
else {
|
|
2602
|
+
if (!isNum(n.value)) err(`${path}.number.value`, "must be a finite number");
|
|
2603
|
+
if (n.decimals !== void 0 && (!isNum(n.decimals) || !Number.isInteger(n.decimals) || n.decimals < 0 || n.decimals > LIMITS.maxTextNumberDecimals)) {
|
|
2604
|
+
err(`${path}.number.decimals`, `must be a whole number 0..${LIMITS.maxTextNumberDecimals}`);
|
|
2605
|
+
}
|
|
2606
|
+
for (const k of ["prefix", "suffix"]) {
|
|
2607
|
+
const v = n[k];
|
|
2608
|
+
if (v !== void 0 && (!isStr(v) || v.length > LIMITS.maxTextAffix)) err(`${path}.number.${k}`, `must be a string of at most ${LIMITS.maxTextAffix} characters`);
|
|
2609
|
+
}
|
|
2610
|
+
if (n.grouping !== void 0 && typeof n.grouping !== "boolean") err(`${path}.number.grouping`, "must be a boolean");
|
|
2611
|
+
for (const k of unknownKeys(n, KNOWN_TEXT_NUMBER)) warn(`${path}.number.${k}`, "unknown-property", `unknown number property '${k}' \u2014 will be ignored`);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2265
2614
|
if (sprite.reveal !== void 0) {
|
|
2266
2615
|
const rv = sprite.reveal;
|
|
2267
2616
|
if (typeof rv !== "object" || rv === null || Array.isArray(rv)) {
|
|
@@ -2282,6 +2631,10 @@ function validateSprite(sprite, path, err, warn, spec) {
|
|
|
2282
2631
|
if (rv.speed !== void 0 && (!isNum(rv.speed) || rv.speed < 0 || rv.speed > LIMITS.maxRevealSpeed)) {
|
|
2283
2632
|
err(`${path}.reveal.speed`, `must be between 0 and ${LIMITS.maxRevealSpeed} (graphemes/sec)`);
|
|
2284
2633
|
}
|
|
2634
|
+
if (rv.rise !== void 0) {
|
|
2635
|
+
if (!isNum(rv.rise) || rv.rise < 0 || rv.rise > LIMITS.maxRevealRise) err(`${path}.reveal.rise`, `must be 0..${LIMITS.maxRevealRise} (em below the line a glyph rises from)`);
|
|
2636
|
+
else if (rv.mode !== "glyphFade") warn(`${path}.reveal.rise`, "unknown-property", "rise applies to mode 'glyphFade' only \u2014 will be ignored");
|
|
2637
|
+
}
|
|
2285
2638
|
if (rv.caret !== void 0 && typeof rv.caret !== "boolean") {
|
|
2286
2639
|
const caret = rv.caret;
|
|
2287
2640
|
if (typeof caret !== "object" || caret === null || Array.isArray(caret)) {
|
|
@@ -2350,6 +2703,12 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
2350
2703
|
else if (v[0] < 0) err(p, "speed lower bound must be >= 0");
|
|
2351
2704
|
else if (v[1] > speedCap) err(p, `speed exceeds cap ${isViewport ? speedCap.toFixed(2) + " viewport-units/sec" : LIMITS.maxSpeed + " px/sec"}`);
|
|
2352
2705
|
};
|
|
2706
|
+
const wobbleCap = isViewport ? LIMITS.maxWobble : LIMITS.maxWobble * refVp;
|
|
2707
|
+
const wobbleCheck = (v, p, name, axis) => {
|
|
2708
|
+
if (!isNum(v) || Math.abs(v) <= wobbleCap) return;
|
|
2709
|
+
const px = isViewport ? ` (\u2248${Math.round(Math.abs(v) * refVp)} px at ${refVp} px)` : "";
|
|
2710
|
+
warn(p, "motion-amplitude", `${name} ${v} is a ${axis} swing of ${isViewport ? `${Math.abs(v)}\xD7 min(w, h)` : `${Math.abs(v)} px`}${px} each way, so entities leave the frame \u2014 ${name} is in ${isViewport ? "min(w, h) units, not px" : "px"}; typical ${isViewport ? "0.002\u20130.02" : "2\u201320 px"}, at most ${isViewport ? wobbleCap : Math.round(wobbleCap) + " px"} before this warning`);
|
|
2711
|
+
};
|
|
2353
2712
|
let knownSet;
|
|
2354
2713
|
if (motion.type === "drift") {
|
|
2355
2714
|
knownSet = KNOWN_DRIFT;
|
|
@@ -2357,6 +2716,7 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
2357
2716
|
if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
|
|
2358
2717
|
if (motion.bidirectional !== void 0 && typeof motion.bidirectional !== "boolean") err(`${path}.bidirectional`, "must be a boolean");
|
|
2359
2718
|
if (motion.bob !== void 0 && !isNum(motion.bob)) err(`${path}.bob`, "must be a number");
|
|
2719
|
+
else wobbleCheck(motion.bob, `${path}.bob`, "bob", "vertical");
|
|
2360
2720
|
validateEase(motion.ease, `${path}.ease`, err, warn);
|
|
2361
2721
|
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
2362
2722
|
warn(`${path}.speed`, "near-zero-speed", `max speed is ${motion.speed[1]} px/sec \u2014 entities will appear frozen. Typical range: 10\u2013200 px/sec`);
|
|
@@ -2365,6 +2725,7 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
2365
2725
|
knownSet = KNOWN_RISE;
|
|
2366
2726
|
speedOk(motion.speed, `${path}.speed`);
|
|
2367
2727
|
if (motion.sway !== void 0 && !isNum(motion.sway)) err(`${path}.sway`, "must be a number");
|
|
2728
|
+
else wobbleCheck(motion.sway, `${path}.sway`, "sway", "horizontal");
|
|
2368
2729
|
validateEase(motion.ease, `${path}.ease`, err, warn);
|
|
2369
2730
|
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
2370
2731
|
warn(`${path}.speed`, "near-zero-speed", `max speed is ${motion.speed[1]} px/sec \u2014 entities will appear frozen. Typical range: 5\u201380 px/sec`);
|
|
@@ -2464,29 +2825,43 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
2464
2825
|
return;
|
|
2465
2826
|
}
|
|
2466
2827
|
for (const k of unknownKeys(motion, knownSet)) {
|
|
2467
|
-
warn(`${path}.${k}`, "unknown-property", `unknown motion property '${k}' \u2014 will be ignored`);
|
|
2828
|
+
warn(`${path}.${k}`, "unknown-property", unknownMessage("motion", k, `unknown motion property '${k}' \u2014 will be ignored`));
|
|
2468
2829
|
}
|
|
2469
2830
|
}
|
|
2470
2831
|
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;
|
|
2471
2832
|
var MAX_PATH_PARTS = 256;
|
|
2833
|
+
var pathVerdicts = /* @__PURE__ */ new Map();
|
|
2834
|
+
var pathVerdictBytes = 0;
|
|
2835
|
+
var NO_AREA = "must enclose an area (at least 3 points), or set fill: false and a stroke for line art";
|
|
2836
|
+
var NO_LINE = "must draw at least one line (2+ points)";
|
|
2837
|
+
var keyedPathDepth = 0;
|
|
2838
|
+
function pathGeometryProblem(d, rule, filled) {
|
|
2839
|
+
const key = `${rule}|${filled ? 1 : 0}|${d}`;
|
|
2840
|
+
const hit = pathVerdicts.get(key);
|
|
2841
|
+
if (hit !== void 0) return hit;
|
|
2842
|
+
const flat = flattenPath(d, rule);
|
|
2843
|
+
const inBox = ([x, y]) => Math.abs(x) <= 1.0001 && Math.abs(y) <= 1.0001;
|
|
2844
|
+
const problem = flat.error ? `not a usable path: ${flat.error}` : 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)" : filled && flat.points.length < 3 ? NO_AREA : !filled && flat.lines.length === 0 ? NO_LINE : null;
|
|
2845
|
+
if (pathVerdicts.size >= 1024 || pathVerdictBytes + key.length > 2e6) {
|
|
2846
|
+
pathVerdicts.clear();
|
|
2847
|
+
pathVerdictBytes = 0;
|
|
2848
|
+
}
|
|
2849
|
+
pathVerdicts.set(key, problem);
|
|
2850
|
+
pathVerdictBytes += key.length;
|
|
2851
|
+
return problem;
|
|
2852
|
+
}
|
|
2472
2853
|
function validatePathPiece(piece, path, err, warn) {
|
|
2473
2854
|
if (typeof piece.path !== "string" || piece.path.length === 0 || piece.path.length > 65536) {
|
|
2474
2855
|
err(`${path}.path`, "must be an SVG path d string (1..65536 chars) in the -1..1 unit box");
|
|
2475
2856
|
} else {
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
if (flat.error) err(`${path}.path`, `not a usable path: ${flat.error}`);
|
|
2479
|
-
else if (filled && flat.points.length < 3) err(`${path}.path`, "must enclose an area (at least 3 points), or set fill: false and a stroke for line art");
|
|
2480
|
-
else if (!filled && flat.lines.length === 0) err(`${path}.path`, "must draw at least one line (2+ points)");
|
|
2481
|
-
else if (flat.points.length > MAX_PATH_POINTS) err(`${path}.path`, `flattens to ${flat.points.length} points; at most ${MAX_PATH_POINTS}`);
|
|
2482
|
-
else if ([...flat.points, ...flat.lines.flatMap((l) => l.points)].some(([x, y]) => Math.abs(x) > 1.0001 || Math.abs(y) > 1.0001)) err(`${path}.path`, "must stay inside the -1..1 unit box (curves included)");
|
|
2857
|
+
const problem = pathGeometryProblem(piece.path, piece.fillRule ?? "auto", piece.fill !== false);
|
|
2858
|
+
if (problem && !(keyedPathDepth > 0 && (problem === NO_AREA || problem === NO_LINE))) err(`${path}.path`, problem);
|
|
2483
2859
|
}
|
|
2484
2860
|
if (piece.fill !== void 0 && typeof piece.fill !== "boolean") err(`${path}.fill`, "must be a boolean");
|
|
2485
2861
|
if (piece.fill === false && piece.stroke === void 0) err(`${path}.fill`, "fill: false draws nothing without a stroke");
|
|
2486
2862
|
if (piece.gradient !== void 0) validatePathGradient(piece.gradient, `${path}.gradient`, err);
|
|
2487
|
-
if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err);
|
|
2863
|
+
if (piece.stroke !== void 0) validatePathStroke(piece.stroke, `${path}.stroke`, err, warn);
|
|
2488
2864
|
if (piece.fillRule !== void 0 && !["auto", "nonzero", "evenodd"].includes(piece.fillRule)) err(`${path}.fillRule`, "must be 'auto' | 'nonzero' | 'evenodd'");
|
|
2489
|
-
void warn;
|
|
2490
2865
|
}
|
|
2491
2866
|
function validatePathGradient(g, path, err) {
|
|
2492
2867
|
if (!isObj(g)) return err(path, "must be { type: 'linear', from, to, stops } or { type: 'radial', center, radius, stops }");
|
|
@@ -2511,13 +2886,38 @@ function validatePathGradient(g, path, err) {
|
|
|
2511
2886
|
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.stops[${i}].opacity`, "must be 0..1");
|
|
2512
2887
|
});
|
|
2513
2888
|
}
|
|
2514
|
-
function validatePathStroke(st, path, err) {
|
|
2515
|
-
if (!isObj(st)) return err(path, "must be { color, width, opacity?, join?, cap? }");
|
|
2889
|
+
function validatePathStroke(st, path, err, warn) {
|
|
2890
|
+
if (!isObj(st)) return err(path, "must be { color, width, opacity?, join?, cap?, draw? }");
|
|
2516
2891
|
color(st.color, `${path}.color`, err);
|
|
2517
2892
|
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)");
|
|
2518
2893
|
if (st.opacity !== void 0 && (!isNum(st.opacity) || st.opacity < 0 || st.opacity > 1)) err(`${path}.opacity`, "must be 0..1");
|
|
2519
2894
|
if (st.join !== void 0 && !["round", "miter", "bevel"].includes(st.join)) err(`${path}.join`, "must be 'round' | 'miter' | 'bevel'");
|
|
2520
2895
|
if (st.cap !== void 0 && !["round", "butt", "square"].includes(st.cap)) err(`${path}.cap`, "must be 'round' | 'butt' | 'square'");
|
|
2896
|
+
if (st.draw !== void 0) validateDraw(st.draw, `${path}.draw`, err, warn);
|
|
2897
|
+
}
|
|
2898
|
+
function validateWipe(tr, path, err, warn) {
|
|
2899
|
+
if (tr.shape !== void 0 && tr.shape !== "circle" && tr.shape !== "line") err(`${path}.shape`, "must be 'circle' | 'line'");
|
|
2900
|
+
if (tr.origin !== void 0) {
|
|
2901
|
+
const o = tr.origin;
|
|
2902
|
+
if (!isObj(o) || !isNum(o.x) || !isNum(o.y) || o.x < 0 || o.x > 1 || o.y < 0 || o.y > 1) err(`${path}.origin`, "must be { x, y } with each 0..1 (where the iris opens)");
|
|
2903
|
+
else if (tr.shape === "line") err(`${path}.origin`, "applies to shape 'circle' only \u2014 a line wipe sweeps along `angle`");
|
|
2904
|
+
}
|
|
2905
|
+
if (tr.angle !== void 0) {
|
|
2906
|
+
if (!isNum(tr.angle) || Math.abs(tr.angle) > 360) err(`${path}.angle`, "must be degrees, -360..360 (0 = left to right, 90 = top to bottom)");
|
|
2907
|
+
else if (tr.shape !== "line") err(`${path}.angle`, "applies to shape 'line' only \u2014 an iris opens from `origin`");
|
|
2908
|
+
}
|
|
2909
|
+
if (tr.feather !== void 0 && (!isNum(tr.feather) || tr.feather < 0 || tr.feather > 1)) err(`${path}.feather`, "must be 0..1 (the edge width, as a fraction of the distance it travels)");
|
|
2910
|
+
for (const k of Object.keys(tr)) {
|
|
2911
|
+
if (!["type", "dur", "shape", "origin", "angle", "feather"].includes(k)) warn(`${path}.${k}`, "unknown-property", `unknown wipe property '${k}' \u2014 will be ignored`);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
function validateDraw(d, path, err, warn) {
|
|
2915
|
+
if (!isObj(d)) return err(path, "must be { start?, end? } \u2014 fractions of the line, 0..1");
|
|
2916
|
+
for (const k of ["start", "end"]) {
|
|
2917
|
+
const v = d[k];
|
|
2918
|
+
if (v !== void 0 && (!isNum(v) || v < 0 || v > 1)) err(`${path}.${k}`, "must be 0..1 (a fraction of the line's length)");
|
|
2919
|
+
}
|
|
2920
|
+
for (const k of Object.keys(d)) if (k !== "start" && k !== "end") warn(`${path}.${k}`, "unknown-property", `unknown draw property '${k}' \u2014 draw takes start and end; will be ignored`);
|
|
2521
2921
|
}
|
|
2522
2922
|
function validateShapePoints(points, path, min, err) {
|
|
2523
2923
|
if (!Array.isArray(points) || points.length < min || points.length > LIMITS.maxShapePoints) {
|
|
@@ -2613,16 +3013,17 @@ function validateSequence(seq) {
|
|
|
2613
3013
|
if (s.transition !== void 0) {
|
|
2614
3014
|
if (!isObj(s.transition)) {
|
|
2615
3015
|
err(`${p}.transition`, "must be an object");
|
|
2616
|
-
} else if (s.transition.type === "morph" || s.transition.type === "fade") {
|
|
3016
|
+
} else if (s.transition.type === "morph" || s.transition.type === "fade" || s.transition.type === "wipe") {
|
|
2617
3017
|
if (!isNum(s.transition.dur) || s.transition.dur < LIMITS.minTransitionDur || s.transition.dur > LIMITS.maxTransitionDur) {
|
|
2618
3018
|
err(`${p}.transition.dur`, `must be a number between ${LIMITS.minTransitionDur} and ${LIMITS.maxTransitionDur}`);
|
|
2619
3019
|
}
|
|
3020
|
+
if (s.transition.type === "wipe") validateWipe(s.transition, `${p}.transition`, err, (wp, code, message) => void warnings.push({ path: wp, code, message }));
|
|
2620
3021
|
} else if (s.transition.type !== "cut") {
|
|
2621
|
-
err(`${p}.transition.type`, "must be 'cut', 'morph' or '
|
|
3022
|
+
err(`${p}.transition.type`, "must be 'cut', 'morph', 'fade' or 'wipe'");
|
|
2622
3023
|
}
|
|
2623
3024
|
if (isObj(s.transition) && s.transition.text !== void 0) {
|
|
2624
3025
|
if (s.transition.type !== "morph") {
|
|
2625
|
-
err(`${p}.transition.text`, "is a morph option (fade already
|
|
3026
|
+
err(`${p}.transition.text`, "is a morph option (fade and wipe already change whole frames; cut has no window)");
|
|
2626
3027
|
} else if (s.transition.text !== "step" && s.transition.text !== "crossfade" && s.transition.text !== "dip") {
|
|
2627
3028
|
err(`${p}.transition.text`, "must be 'step' | 'crossfade' | 'dip'");
|
|
2628
3029
|
}
|
|
@@ -3017,11 +3418,29 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
3017
3418
|
out[out.length - 1].pulsePhase = clockPhase - (x0 * Math.cos(ang) + y0 * Math.sin(ang)) / wl * Math.PI * 2;
|
|
3018
3419
|
}
|
|
3019
3420
|
}
|
|
3421
|
+
if (layer.rock) {
|
|
3422
|
+
const amp = layer.rock.amp * Math.PI / 180;
|
|
3423
|
+
for (const ent of out) {
|
|
3424
|
+
const ph = rng.range(0, Math.PI * 2);
|
|
3425
|
+
ent.rock = { amp, period: layer.rock.period, phase: layer.clock ? clockPhase : ph };
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3020
3428
|
if (layer.emit) {
|
|
3021
3429
|
for (let i = 0; i < out.length; i++) out[i].emit = emitParams(layer.emit, i, out.length);
|
|
3022
3430
|
}
|
|
3023
3431
|
return out;
|
|
3024
3432
|
}
|
|
3433
|
+
function textBlockText(s) {
|
|
3434
|
+
const n = s.number;
|
|
3435
|
+
if (!n) return s.text;
|
|
3436
|
+
const d = Math.max(0, Math.min(LIMITS.maxTextNumberDecimals, Math.round(n.decimals ?? 0)));
|
|
3437
|
+
const v = Number.isFinite(n.value) ? n.value : 0;
|
|
3438
|
+
const fixed = Math.abs(v).toFixed(d);
|
|
3439
|
+
const [whole, frac] = fixed.split(".");
|
|
3440
|
+
const grouped = n.grouping === false ? whole : whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
3441
|
+
const neg = v < 0 && Number(fixed) !== 0;
|
|
3442
|
+
return `${n.prefix ?? ""}${neg ? "-" : ""}${grouped}${frac !== void 0 ? `.${frac}` : ""}${n.suffix ?? ""}`;
|
|
3443
|
+
}
|
|
3025
3444
|
function warpZ(e, t) {
|
|
3026
3445
|
const wp = e.warp;
|
|
3027
3446
|
return wrap(wp.z0 - wp.vz * (t / 1e3), WARP_NEAR, 1);
|
|
@@ -3076,12 +3495,14 @@ function sizeAt(e, t) {
|
|
|
3076
3495
|
return s > 0 ? s : 0.1;
|
|
3077
3496
|
}
|
|
3078
3497
|
function rotationAt(e, t) {
|
|
3079
|
-
|
|
3498
|
+
let base = e.rotate ?? 0;
|
|
3499
|
+
if (e.rock) base += e.rock.amp * Math.sin(clockTime(e, t) * 2 * Math.PI / e.rock.period + e.rock.phase);
|
|
3080
3500
|
if (!e.spinSpeed) return base;
|
|
3081
3501
|
return base + e.spinPhase + e.spinSpeed * (t / 1e3);
|
|
3082
3502
|
}
|
|
3083
3503
|
function lifeAlphaAt(life, t) {
|
|
3084
3504
|
if (!life) return 1;
|
|
3505
|
+
if (life.stagger) return life.enter !== void 0 && t < life.enter ? 0 : 1;
|
|
3085
3506
|
const fade = life.fade ?? 1e3;
|
|
3086
3507
|
let a = 1;
|
|
3087
3508
|
if (life.enter !== void 0) {
|
|
@@ -3093,16 +3514,20 @@ function lifeAlphaAt(life, t) {
|
|
|
3093
3514
|
}
|
|
3094
3515
|
return a;
|
|
3095
3516
|
}
|
|
3517
|
+
function lifeStaggerAt(life, t, i) {
|
|
3518
|
+
if (!life?.stagger) return 1;
|
|
3519
|
+
return lifeAlphaAt({ enter: life.enter, exit: life.exit, fade: life.fade }, t - i * life.stagger);
|
|
3520
|
+
}
|
|
3096
3521
|
function wrap(v, min, max) {
|
|
3097
|
-
const
|
|
3098
|
-
return ((v - min) %
|
|
3522
|
+
const range2 = max - min;
|
|
3523
|
+
return ((v - min) % range2 + range2) % range2 + min;
|
|
3099
3524
|
}
|
|
3100
3525
|
function reflect(v, min, max) {
|
|
3101
|
-
const
|
|
3102
|
-
if (
|
|
3103
|
-
const period = 2 *
|
|
3526
|
+
const range2 = max - min;
|
|
3527
|
+
if (range2 <= 0) return min;
|
|
3528
|
+
const period = 2 * range2;
|
|
3104
3529
|
const p = ((v - min) % period + period) % period;
|
|
3105
|
-
return min + (p <
|
|
3530
|
+
return min + (p < range2 ? p : period - p);
|
|
3106
3531
|
}
|
|
3107
3532
|
function catmullRom2(p0, p1, p2, p3, u) {
|
|
3108
3533
|
const u2 = u * u;
|
|
@@ -3316,6 +3741,26 @@ function textMetricsClassFor(font) {
|
|
|
3316
3741
|
return font && MONO_FAMILY_RE.test(font) ? "mono" : "proportional";
|
|
3317
3742
|
}
|
|
3318
3743
|
var MONO_ADVANCE_EM = 0.6;
|
|
3744
|
+
var FULL_WIDTH_EM = 1;
|
|
3745
|
+
function isCombiningKana(cp) {
|
|
3746
|
+
return cp === 12441 || cp === 12442;
|
|
3747
|
+
}
|
|
3748
|
+
function isFullWidth(cp) {
|
|
3749
|
+
if (isCombiningKana(cp)) return false;
|
|
3750
|
+
return cp >= 4352 && cp <= 4447 || // Hangul Jamo leading consonants
|
|
3751
|
+
cp >= 11904 && cp <= 12350 || // CJK radicals, Kangxi, CJK symbols & punctuation
|
|
3752
|
+
cp >= 12353 && cp <= 13311 || // kana, Bopomofo, Hangul compat Jamo, enclosed CJK, CJK compat
|
|
3753
|
+
cp >= 13312 && cp <= 19903 || // CJK Ext A
|
|
3754
|
+
cp >= 19968 && cp <= 40959 || // CJK Unified Ideographs
|
|
3755
|
+
cp >= 40960 && cp <= 42191 || // Yi
|
|
3756
|
+
cp >= 43360 && cp <= 43391 || // Hangul Jamo Ext A
|
|
3757
|
+
cp >= 44032 && cp <= 55203 || // Hangul syllables
|
|
3758
|
+
cp >= 63744 && cp <= 64255 || // CJK compatibility ideographs
|
|
3759
|
+
cp >= 65072 && cp <= 65103 || // CJK compatibility forms
|
|
3760
|
+
cp >= 65281 && cp <= 65376 || // fullwidth ASCII variants
|
|
3761
|
+
cp >= 65504 && cp <= 65510 || // fullwidth signs
|
|
3762
|
+
cp >= 131072 && cp <= 262141;
|
|
3763
|
+
}
|
|
3319
3764
|
function charWidthEm(ch, metrics = "proportional") {
|
|
3320
3765
|
if (metrics === "mono") return MONO_ADVANCE_EM;
|
|
3321
3766
|
if (ch === " " || ch === " ") return 0.3;
|
|
@@ -3324,9 +3769,25 @@ function charWidthEm(ch, metrics = "proportional") {
|
|
|
3324
3769
|
return 0.55;
|
|
3325
3770
|
}
|
|
3326
3771
|
function textWidthEm(str, metrics = "proportional") {
|
|
3327
|
-
if (metrics === "mono")
|
|
3772
|
+
if (metrics === "mono") {
|
|
3773
|
+
let w2 = 0;
|
|
3774
|
+
for (const c of graphemeClusters(str)) {
|
|
3775
|
+
const cp = c.codePointAt(0);
|
|
3776
|
+
if (!isCombiningKana(cp)) w2 += isFullWidth(cp) ? FULL_WIDTH_EM : MONO_ADVANCE_EM;
|
|
3777
|
+
}
|
|
3778
|
+
return w2;
|
|
3779
|
+
}
|
|
3328
3780
|
let w = 0;
|
|
3329
|
-
for (let i = 0; i < str.length; i++)
|
|
3781
|
+
for (let i = 0; i < str.length; i++) {
|
|
3782
|
+
const cp = str.codePointAt(i);
|
|
3783
|
+
if (isCombiningKana(cp)) continue;
|
|
3784
|
+
if (isFullWidth(cp)) {
|
|
3785
|
+
w += FULL_WIDTH_EM;
|
|
3786
|
+
if (cp > 65535) i++;
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
w += charWidthEm(str[i], metrics);
|
|
3790
|
+
}
|
|
3330
3791
|
return w;
|
|
3331
3792
|
}
|
|
3332
3793
|
function breakTextBlock(text, maxWidthEm, metrics = "proportional") {
|
|
@@ -3510,6 +3971,10 @@ function paintOpacity(spec, layer) {
|
|
|
3510
3971
|
const g = groupOf(spec, layer);
|
|
3511
3972
|
return g?.opacity === void 0 ? own : own * clamp01(g.opacity);
|
|
3512
3973
|
}
|
|
3974
|
+
function paintRotation(spec, layer) {
|
|
3975
|
+
const deg = (layer.transform?.rotate ?? 0) + (groupOf(spec, layer)?.transform?.rotate ?? 0);
|
|
3976
|
+
return deg * Math.PI / 180;
|
|
3977
|
+
}
|
|
3513
3978
|
function paintSize(spec, layer) {
|
|
3514
3979
|
return transformSize(layer.transform) * transformSize(groupOf(spec, layer)?.transform);
|
|
3515
3980
|
}
|
|
@@ -3904,7 +4369,7 @@ function manifestFor(spec) {
|
|
|
3904
4369
|
var clamp014 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
|
|
3905
4370
|
var EXPIRED = /* @__PURE__ */ Symbol("expired");
|
|
3906
4371
|
function overrideHeldAt(o, t) {
|
|
3907
|
-
const k = o.dur > 0 ?
|
|
4372
|
+
const k = o.dur > 0 ? easeGlide(o.ease, clamp014((t - o.t0) / o.dur)) : 1;
|
|
3908
4373
|
return k >= 1 ? o.to : lerpValue(o.from, o.to, k, false, o.leaf);
|
|
3909
4374
|
}
|
|
3910
4375
|
function overrideValueAt(o, t, timelineValue) {
|
|
@@ -3913,9 +4378,20 @@ function overrideValueAt(o, t, timelineValue) {
|
|
|
3913
4378
|
return EXPIRED;
|
|
3914
4379
|
}
|
|
3915
4380
|
var steerId = (d) => `${d.t}|${JSON.stringify(d.value)}`;
|
|
3916
|
-
function armOverride(authored, path, from, to, now, glide) {
|
|
4381
|
+
function armOverride(authored, path, from, to, now, glide, ease2) {
|
|
3917
4382
|
const next = nextKeyAfter(authored, path, now);
|
|
3918
|
-
|
|
4383
|
+
const o = { leaf: path.slice(path.lastIndexOf(".") + 1), from, to, t0: now, dur: glide, until: next ? next.at : Number.POSITIVE_INFINITY, outDur: next ? next.dur : 0 };
|
|
4384
|
+
if (ease2) o.ease = ease2;
|
|
4385
|
+
return o;
|
|
4386
|
+
}
|
|
4387
|
+
function outgoingCounter(from, now) {
|
|
4388
|
+
if (from.number && now.number) return { text: from.text, number: { ...now.number, prefix: from.number.prefix, suffix: from.number.suffix } };
|
|
4389
|
+
return { text: textBlockText(from), number: void 0 };
|
|
4390
|
+
}
|
|
4391
|
+
function steerEase(deltas) {
|
|
4392
|
+
let best;
|
|
4393
|
+
for (const d of deltas) if (!best || (d.dur ?? DEFAULT_STEER_DUR) > (best.dur ?? DEFAULT_STEER_DUR)) best = d;
|
|
4394
|
+
return best?.ease !== void 0 && STEER_EASES.has(best.ease) ? best.ease : void 0;
|
|
3919
4395
|
}
|
|
3920
4396
|
function applyOverridesAt(resolved, overrides, t, expired) {
|
|
3921
4397
|
if (overrides.size === 0) return resolved;
|
|
@@ -3945,7 +4421,7 @@ function overridesStayValid(authored, overrides, now) {
|
|
|
3945
4421
|
const state = JSON.stringify(groups.map((g) => readSpecPath(r, g))) + "|" + [...held.keys()].join(",");
|
|
3946
4422
|
if (seen.has(state)) continue;
|
|
3947
4423
|
seen.add(state);
|
|
3948
|
-
if (!validateSpecPaths(r, paths)) return false;
|
|
4424
|
+
if (!validateSpecPaths(r, paths, true)) return false;
|
|
3949
4425
|
}
|
|
3950
4426
|
return true;
|
|
3951
4427
|
}
|
|
@@ -3988,6 +4464,34 @@ function softFill(g, color2, r) {
|
|
|
3988
4464
|
grad.addColorStop(1, hexToRgba(color2, 0));
|
|
3989
4465
|
return grad;
|
|
3990
4466
|
}
|
|
4467
|
+
function wipeMask(g, w, k, cw, ch) {
|
|
4468
|
+
const f = Math.max(0, Math.min(1, w.feather ?? 0.08));
|
|
4469
|
+
if ((w.shape ?? "circle") === "line") {
|
|
4470
|
+
const a = (w.angle ?? 0) * Math.PI / 180;
|
|
4471
|
+
const ux = Math.cos(a);
|
|
4472
|
+
const uy = Math.sin(a);
|
|
4473
|
+
const proj = [0, cw * ux, ch * uy, cw * ux + ch * uy];
|
|
4474
|
+
const p0 = Math.min(...proj);
|
|
4475
|
+
const span = Math.max(...proj) - p0;
|
|
4476
|
+
const fw2 = Math.max(1, f * span);
|
|
4477
|
+
const lead = p0 + k * (span + fw2);
|
|
4478
|
+
const trail = lead - fw2;
|
|
4479
|
+
const g22 = g.createLinearGradient(ux * trail, uy * trail, ux * lead, uy * lead);
|
|
4480
|
+
g22.addColorStop(0, "rgba(0,0,0,1)");
|
|
4481
|
+
g22.addColorStop(1, "rgba(0,0,0,0)");
|
|
4482
|
+
return g22;
|
|
4483
|
+
}
|
|
4484
|
+
const ox = Math.max(0, Math.min(1, w.origin?.x ?? 0.5)) * cw;
|
|
4485
|
+
const oy = Math.max(0, Math.min(1, w.origin?.y ?? 0.5)) * ch;
|
|
4486
|
+
const reach = Math.max(Math.hypot(ox, oy), Math.hypot(cw - ox, oy), Math.hypot(ox, ch - oy), Math.hypot(cw - ox, ch - oy));
|
|
4487
|
+
const fw = Math.max(1, f * reach);
|
|
4488
|
+
const outer = Math.max(1e-3, k * (reach + fw));
|
|
4489
|
+
const inner = Math.max(0, outer - fw);
|
|
4490
|
+
const g2 = g.createRadialGradient(ox, oy, inner, ox, oy, Math.max(outer, inner + 1e-3));
|
|
4491
|
+
g2.addColorStop(0, "rgba(0,0,0,1)");
|
|
4492
|
+
g2.addColorStop(1, "rgba(0,0,0,0)");
|
|
4493
|
+
return g2;
|
|
4494
|
+
}
|
|
3991
4495
|
var SpecInstance = class {
|
|
3992
4496
|
constructor(spec, ctx, opts = {}) {
|
|
3993
4497
|
this.spec = spec;
|
|
@@ -4478,21 +4982,27 @@ var SpecInstance = class {
|
|
|
4478
4982
|
}
|
|
4479
4983
|
}
|
|
4480
4984
|
const st = part.stroke;
|
|
4481
|
-
|
|
4985
|
+
const lines = st ? strokeLines(part) : part.flat.lines;
|
|
4986
|
+
if (st && lines.length) {
|
|
4482
4987
|
g.globalAlpha = a0 * part.opacity * (st.opacity ?? 1);
|
|
4483
4988
|
g.strokeStyle = st.color;
|
|
4484
4989
|
g.lineJoin = st.join ?? "round";
|
|
4485
4990
|
g.lineCap = st.cap ?? "round";
|
|
4486
4991
|
if (fr.uniform) {
|
|
4487
4992
|
g.lineWidth = st.width * r / fr.sx;
|
|
4488
|
-
if (
|
|
4489
|
-
|
|
4490
|
-
else {
|
|
4491
|
-
traceLines(g, part.flat.lines);
|
|
4993
|
+
if (lines !== part.flat.lines) {
|
|
4994
|
+
traceLines(g, lines);
|
|
4492
4995
|
g.stroke();
|
|
4996
|
+
} else {
|
|
4997
|
+
if (cache.stroke === void 0) cache.stroke = typeof Path2D === "undefined" ? null : ringPath2D(part.flat.lines);
|
|
4998
|
+
if (cache.stroke) g.stroke(cache.stroke);
|
|
4999
|
+
else {
|
|
5000
|
+
traceLines(g, part.flat.lines);
|
|
5001
|
+
g.stroke();
|
|
5002
|
+
}
|
|
4493
5003
|
}
|
|
4494
5004
|
} else {
|
|
4495
|
-
const scaled =
|
|
5005
|
+
const scaled = lines.map((l) => ({ closed: l.closed, points: l.points.map(([x, y]) => [x * fr.sx, y * fr.sy]) }));
|
|
4496
5006
|
g.save();
|
|
4497
5007
|
g.scale(1 / fr.sx, 1 / fr.sy);
|
|
4498
5008
|
g.lineWidth = st.width * r;
|
|
@@ -4917,7 +5427,8 @@ var SpecInstance = class {
|
|
|
4917
5427
|
}
|
|
4918
5428
|
if (sprite.kind === "stroke") {
|
|
4919
5429
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
4920
|
-
const pts =
|
|
5430
|
+
const pts = strokeDrawnSamples(sprite, sz / 2);
|
|
5431
|
+
if (pts.length < 2) return;
|
|
4921
5432
|
const lw = strokeWidthPx(sprite, unitScale);
|
|
4922
5433
|
ctx.save();
|
|
4923
5434
|
ctx.translate(p.x, p.y);
|
|
@@ -4928,7 +5439,7 @@ var SpecInstance = class {
|
|
|
4928
5439
|
ctx.lineCap = "round";
|
|
4929
5440
|
ctx.lineJoin = "round";
|
|
4930
5441
|
if (sprite.taper) {
|
|
4931
|
-
const outline = strokeOutline(pts, lw);
|
|
5442
|
+
const outline = strokeOutline(pts, lw, true, strokeDrawnAt(sprite, sz / 2, pts));
|
|
4932
5443
|
ctx.fillStyle = resolvedColor;
|
|
4933
5444
|
ctx.beginPath();
|
|
4934
5445
|
ctx.moveTo(outline[0].x, outline[0].y);
|
|
@@ -4975,7 +5486,7 @@ var SpecInstance = class {
|
|
|
4975
5486
|
const lh = (sprite.lineHeight ?? 1.4) * fsPx;
|
|
4976
5487
|
const maxWPx = sprite.maxWidth * unitScale2;
|
|
4977
5488
|
const maxWEm = maxWPx / fsPx;
|
|
4978
|
-
const lines = breakTextBlock(sprite
|
|
5489
|
+
const lines = breakTextBlock(textBlockText(sprite), maxWEm, textMetricsClassFor(sprite.font));
|
|
4979
5490
|
const align = sprite.align ?? "left";
|
|
4980
5491
|
const reveal = sprite.reveal;
|
|
4981
5492
|
const rs = reveal ? revealState(lines, reveal, t) : null;
|
|
@@ -4997,6 +5508,7 @@ var SpecInstance = class {
|
|
|
4997
5508
|
if (rs?.glyphAlphas) {
|
|
4998
5509
|
ctx.textAlign = "left";
|
|
4999
5510
|
const baseAlpha = ctx.globalAlpha;
|
|
5511
|
+
const riseEm = Math.max(0, Math.min(LIMITS.maxRevealRise, reveal.rise ?? 0));
|
|
5000
5512
|
for (let li = 0; li < lines.length; li++) {
|
|
5001
5513
|
const alphas = rs.glyphAlphas[li] ?? [];
|
|
5002
5514
|
if (alphas.length > 0 && alphas[0] <= 0) break;
|
|
@@ -5015,7 +5527,7 @@ var SpecInstance = class {
|
|
|
5015
5527
|
const a = alphas[gi] ?? 0;
|
|
5016
5528
|
if (a <= 0) break;
|
|
5017
5529
|
ctx.globalAlpha = baseAlpha * a;
|
|
5018
|
-
ctx.fillText(clusters[gi], x0 + ctx.measureText(prefix).width, li * lh);
|
|
5530
|
+
ctx.fillText(clusters[gi], x0 + ctx.measureText(prefix).width, li * lh + riseEm * fsPx * (1 - a));
|
|
5019
5531
|
prefix += clusters[gi];
|
|
5020
5532
|
}
|
|
5021
5533
|
}
|
|
@@ -5070,7 +5582,7 @@ var SpecInstance = class {
|
|
|
5070
5582
|
const tr = this.transition;
|
|
5071
5583
|
if (!tr) return;
|
|
5072
5584
|
const k = tr.dur <= 0 ? 1 : (t - tr.startT) / tr.dur;
|
|
5073
|
-
this.effSpec = k >= 1 ? tr.to : lerpSpec(tr.from, tr.to,
|
|
5585
|
+
this.effSpec = k >= 1 ? tr.to : lerpSpec(tr.from, tr.to, easeGlide(tr.ease, k));
|
|
5074
5586
|
if (k >= 1) this.transition = null;
|
|
5075
5587
|
const sig = structuralSignature(this.effSpec);
|
|
5076
5588
|
if (sig !== this.lastStructural) this.rebuild();
|
|
@@ -5099,6 +5611,8 @@ var SpecInstance = class {
|
|
|
5099
5611
|
startT: this.lastT,
|
|
5100
5612
|
dur
|
|
5101
5613
|
};
|
|
5614
|
+
const ease2 = steerEase(deltas);
|
|
5615
|
+
if (ease2) this.transition.ease = ease2;
|
|
5102
5616
|
if (this.paused) {
|
|
5103
5617
|
this.transition = null;
|
|
5104
5618
|
this.effSpec = target;
|
|
@@ -5144,7 +5658,7 @@ var SpecInstance = class {
|
|
|
5144
5658
|
});
|
|
5145
5659
|
}
|
|
5146
5660
|
drawLinks(built, t, lifeA, parentE) {
|
|
5147
|
-
const { links } = built.layer;
|
|
5661
|
+
const { links, life } = built.layer;
|
|
5148
5662
|
if (!links) return;
|
|
5149
5663
|
const { ctx } = this;
|
|
5150
5664
|
const motionWraps = ["drift", "rise", "wander"].includes(built.layer.motion.type);
|
|
@@ -5169,6 +5683,7 @@ var SpecInstance = class {
|
|
|
5169
5683
|
} else resolvedColor = "#e6e8ef";
|
|
5170
5684
|
}
|
|
5171
5685
|
let a = links.alpha ?? alphaAt(ei, t);
|
|
5686
|
+
if (life?.stagger) a *= Math.min(lifeStaggerAt(life, t, i), lifeStaggerAt(life, t, j));
|
|
5172
5687
|
if (links.falloff && Number.isFinite(maxDistPx)) a *= Math.max(0, 1 - dist / maxDistPx);
|
|
5173
5688
|
ctx.globalAlpha = a * lifeA;
|
|
5174
5689
|
ctx.strokeStyle = resolvedColor;
|
|
@@ -5231,15 +5746,19 @@ var SpecInstance = class {
|
|
|
5231
5746
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
5232
5747
|
this.drawLinks(built, t, lifeA, parentE);
|
|
5233
5748
|
const override = this.paintOverrides?.get(li) ?? null;
|
|
5234
|
-
|
|
5235
|
-
|
|
5749
|
+
const life = built.layer.life;
|
|
5750
|
+
for (let ei = 0; ei < built.entities.length; ei++) {
|
|
5751
|
+
const e = built.entities[ei];
|
|
5752
|
+
const entA = life?.stagger ? lifeA * lifeStaggerAt(life, t, ei) : lifeA;
|
|
5753
|
+
if (entA <= 0) continue;
|
|
5754
|
+
this.drawTrail(built, e, t, entA, parentE);
|
|
5236
5755
|
if (override === null) {
|
|
5237
|
-
this.drawEntity(built, e, t,
|
|
5756
|
+
this.drawEntity(built, e, t, entA, parentE);
|
|
5238
5757
|
continue;
|
|
5239
5758
|
}
|
|
5240
5759
|
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];
|
|
5241
|
-
if (aOut > 0) this.drawEntity(built, e, t,
|
|
5242
|
-
if (aIn > 0) this.drawEntity(built, e, t,
|
|
5760
|
+
if (aOut > 0) this.drawEntity(built, e, t, entA, parentE, { sprite: override.outgoing, alpha: aOut });
|
|
5761
|
+
if (aIn > 0) this.drawEntity(built, e, t, entA, parentE, { alpha: aIn });
|
|
5243
5762
|
}
|
|
5244
5763
|
if (tf || gtf) ctx.restore();
|
|
5245
5764
|
}
|
|
@@ -5292,10 +5811,12 @@ var SpecInstance = class {
|
|
|
5292
5811
|
if (group?.opacity !== void 0) lifeA *= clamp014(group.opacity);
|
|
5293
5812
|
let sig = `${pathPaintSig(built.layer.sprite)}:${lifeA.toFixed(4)}:${JSON.stringify(built.layer.transform ?? null)}:${JSON.stringify(group?.transform ?? null)}`;
|
|
5294
5813
|
const parentE = this.parentEntityFor(built);
|
|
5295
|
-
|
|
5814
|
+
const life = built.layer.life;
|
|
5815
|
+
built.entities.forEach((e, ei) => {
|
|
5296
5816
|
const p = this.entityPos(e, t, parentE);
|
|
5297
|
-
|
|
5298
|
-
|
|
5817
|
+
const a = life?.stagger ? alphaAt(e, t) * lifeStaggerAt(life, t, ei) : alphaAt(e, t);
|
|
5818
|
+
sig += `|${p.x.toFixed(2)},${p.y.toFixed(2)},${sizeAt(e, t).toFixed(3)},${rotationAt(e, t).toFixed(5)},${a.toFixed(4)},${e.colorIndex}`;
|
|
5819
|
+
});
|
|
5299
5820
|
return sig;
|
|
5300
5821
|
}
|
|
5301
5822
|
plateSig(a, b) {
|
|
@@ -5447,11 +5968,12 @@ var SpecInstance = class {
|
|
|
5447
5968
|
}
|
|
5448
5969
|
const next = new Map(this.overrides);
|
|
5449
5970
|
const armed = [];
|
|
5971
|
+
const batchEase = steerEase([...latest.values()]);
|
|
5450
5972
|
for (const [path, d] of latest) {
|
|
5451
5973
|
const id = steerId(d);
|
|
5452
5974
|
if (this.seenSteers.get(path) === id) continue;
|
|
5453
5975
|
armed.push([path, id]);
|
|
5454
|
-
next.set(path, armOverride(this.authored, path, glide > 0 ? readSpecPath(shown, path) : d.value, d.value, now, glide));
|
|
5976
|
+
next.set(path, armOverride(this.authored, path, glide > 0 ? readSpecPath(shown, path) : d.value, d.value, now, glide, batchEase));
|
|
5455
5977
|
}
|
|
5456
5978
|
if (armed.length === 0) return true;
|
|
5457
5979
|
if (!overridesStayValid(this.authored, next, now)) return false;
|
|
@@ -5821,16 +6343,80 @@ var SequenceInstance = class {
|
|
|
5821
6343
|
if (!textStringsDiffer(specA, specB, i)) continue;
|
|
5822
6344
|
const from = specA.layers[i].sprite;
|
|
5823
6345
|
const now = lerped.layers[i].sprite;
|
|
5824
|
-
const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now,
|
|
6346
|
+
const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now, ...outgoingCounter(from, now) } : now;
|
|
5825
6347
|
if (outgoing === now) continue;
|
|
5826
6348
|
(map ??= /* @__PURE__ */ new Map()).set(i, mode === "dip" ? { outgoing, k, mode } : { outgoing, k });
|
|
5827
6349
|
}
|
|
5828
6350
|
return map;
|
|
5829
6351
|
}
|
|
5830
|
-
/** Length of the fade out of segment `from`, or 0 (
|
|
6352
|
+
/** Length of the fade (or wipe) out of segment `from`, or 0 (none, or a tier that plays it as cut). */
|
|
5831
6353
|
fadeDur(from) {
|
|
5832
6354
|
const tr = this.seq.segments[from]?.transition;
|
|
5833
|
-
return this.fadeEnabled && tr?.type === "fade" ? tr.dur : 0;
|
|
6355
|
+
return this.fadeEnabled && (tr?.type === "fade" || tr?.type === "wipe") ? tr.dur : 0;
|
|
6356
|
+
}
|
|
6357
|
+
/** The wipe out of segment `from`, when that is its transition. */
|
|
6358
|
+
wipeOf(from) {
|
|
6359
|
+
const tr = this.seq.segments[from]?.transition;
|
|
6360
|
+
return tr?.type === "wipe" ? tr : null;
|
|
6361
|
+
}
|
|
6362
|
+
wipeScratch = null;
|
|
6363
|
+
wipeScratchIn = null;
|
|
6364
|
+
/** `canvas` masked to one side of the wipe edge at progress `k`, on a scratch canvas of its own. */
|
|
6365
|
+
wipeSide(slot, canvas, wipe, k, side) {
|
|
6366
|
+
const cw = Math.max(1, canvas.width);
|
|
6367
|
+
const ch = Math.max(1, canvas.height);
|
|
6368
|
+
const scratch = this[slot] ??= createSceneCanvas(cw, ch);
|
|
6369
|
+
if (scratch.width !== cw) scratch.width = cw;
|
|
6370
|
+
if (scratch.height !== ch) scratch.height = ch;
|
|
6371
|
+
const g = scratch.getContext("2d");
|
|
6372
|
+
if (!g) return null;
|
|
6373
|
+
g.save();
|
|
6374
|
+
g.setTransform(1, 0, 0, 1, 0, 0);
|
|
6375
|
+
g.globalCompositeOperation = "copy";
|
|
6376
|
+
g.drawImage(canvas, 0, 0);
|
|
6377
|
+
g.globalCompositeOperation = side === "out" ? "destination-out" : "destination-in";
|
|
6378
|
+
g.fillStyle = wipeMask(g, wipe, k, cw, ch);
|
|
6379
|
+
g.fillRect(0, 0, cw, ch);
|
|
6380
|
+
g.restore();
|
|
6381
|
+
return scratch;
|
|
6382
|
+
}
|
|
6383
|
+
/**
|
|
6384
|
+
* Composite one side of a transition out of segment `from` at eased
|
|
6385
|
+
* progress `k`. A fade draws the outgoing at 1 − k (and, over a bed, the
|
|
6386
|
+
* incoming at k). A wipe draws each side through the moving edge instead:
|
|
6387
|
+
* the outgoing everywhere the edge has not yet passed, the incoming where
|
|
6388
|
+
* it has — the canvas is masked on a scratch copy (`destination-out` /
|
|
6389
|
+
* `destination-in` with the edge's gradient), then drawn. `other`: the
|
|
6390
|
+
* opposite side's canvas, when it is drawn too (a wipe over a bed) — the
|
|
6391
|
+
* two masked sides are summed (`lighter`) on the scratch first, so a soft
|
|
6392
|
+
* edge keeps matching marks opaque instead of letting the bed show through.
|
|
6393
|
+
*/
|
|
6394
|
+
compositeTransition(canvas, k, from, side, other) {
|
|
6395
|
+
const wipe = this.wipeOf(from);
|
|
6396
|
+
if (!wipe) {
|
|
6397
|
+
if (other) this.composite(other, side === "out" ? k : 1 - k);
|
|
6398
|
+
return this.composite(canvas, side === "out" ? 1 - k : k);
|
|
6399
|
+
}
|
|
6400
|
+
const main = this.childCtx.surface?.getContext("2d");
|
|
6401
|
+
if (!main) return;
|
|
6402
|
+
const scratch = this.wipeSide("wipeScratch", canvas, wipe, k, side);
|
|
6403
|
+
if (!scratch) return this.composite(canvas, side === "out" ? 1 - k : k);
|
|
6404
|
+
if (other) {
|
|
6405
|
+
const rest = this.wipeSide("wipeScratchIn", other, wipe, k, side === "out" ? "in" : "out");
|
|
6406
|
+
const g = scratch.getContext("2d");
|
|
6407
|
+
if (rest && g) {
|
|
6408
|
+
g.save();
|
|
6409
|
+
g.setTransform(1, 0, 0, 1, 0, 0);
|
|
6410
|
+
g.globalCompositeOperation = "lighter";
|
|
6411
|
+
g.drawImage(rest, 0, 0);
|
|
6412
|
+
g.restore();
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
main.save();
|
|
6416
|
+
main.globalAlpha = 1;
|
|
6417
|
+
main.globalCompositeOperation = "source-over";
|
|
6418
|
+
main.drawImage(scratch, 0, 0, this.childCtx.width, this.childCtx.height);
|
|
6419
|
+
main.restore();
|
|
5834
6420
|
}
|
|
5835
6421
|
/** Sum of every timed segment — the lap length under `loop: true`. */
|
|
5836
6422
|
timedTotal() {
|
|
@@ -6096,15 +6682,14 @@ var SequenceInstance = class {
|
|
|
6096
6682
|
if (!this.fadingIn) this.fadingIn = this.offscreenChild(index, useMorphSeed ? chainRoot : index);
|
|
6097
6683
|
this.renderOffscreen(this.fadingIn, localT, effSeed);
|
|
6098
6684
|
const out = this.renderOutgoing(fadeFrom, localT, seed);
|
|
6099
|
-
this.
|
|
6100
|
-
this.composite(out.canvas, 1 - k);
|
|
6685
|
+
this.compositeTransition(out.canvas, k, fadeFrom, "out", this.fadingIn.canvas);
|
|
6101
6686
|
return;
|
|
6102
6687
|
}
|
|
6103
6688
|
const child = useMorphSeed ? this.ensureChild(index, this.childScene(chainRoot)) : this.ensureChild(index);
|
|
6104
6689
|
child.renderFrame(localT, effSeed);
|
|
6105
6690
|
if (fadeActive) {
|
|
6106
6691
|
const out = this.renderOutgoing(fadeFrom, localT, seed);
|
|
6107
|
-
this.
|
|
6692
|
+
this.compositeTransition(out.canvas, easeSmooth(localT / this.fadeDur(fadeFrom)), fadeFrom, "out");
|
|
6108
6693
|
}
|
|
6109
6694
|
}
|
|
6110
6695
|
}
|
|
@@ -6253,6 +6838,7 @@ function compileSequence(spec) {
|
|
|
6253
6838
|
|
|
6254
6839
|
// src/advise.ts
|
|
6255
6840
|
import { createRng as createRng3 } from "@idle-screens/core";
|
|
6841
|
+
import { analyzeFlashes as analyzeFlashes2, srgbToLinear as srgbToLinear3 } from "@idle-screens/validator";
|
|
6256
6842
|
|
|
6257
6843
|
// src/luma.ts
|
|
6258
6844
|
function fieldRenderedRgbs(bg) {
|
|
@@ -6301,6 +6887,26 @@ function spriteLuma(layer, e) {
|
|
|
6301
6887
|
if (s.kind === "text" || s.kind === "textBlock") return hexLuma(s.color ?? "#e6e8ef");
|
|
6302
6888
|
return 0.75;
|
|
6303
6889
|
}
|
|
6890
|
+
function spriteInks(layer, e) {
|
|
6891
|
+
const s = layer.sprite;
|
|
6892
|
+
if (s.kind === "polygon" && isPathPolygon(s)) {
|
|
6893
|
+
const base = s.colors?.[e.colorIndex] ?? s.color;
|
|
6894
|
+
const inks = [];
|
|
6895
|
+
for (const part of pathParts(s)) {
|
|
6896
|
+
if (part.opacity <= 0) continue;
|
|
6897
|
+
if (part.fill) {
|
|
6898
|
+
if (part.gradient) for (const st of part.gradient.stops) {
|
|
6899
|
+
if ((st.opacity ?? 1) > 0) inks.push(st.color);
|
|
6900
|
+
}
|
|
6901
|
+
else inks.push(part.color ?? base);
|
|
6902
|
+
}
|
|
6903
|
+
if (part.stroke && part.stroke.width > 0 && (part.stroke.opacity ?? 1) > 0) inks.push(part.stroke.color);
|
|
6904
|
+
}
|
|
6905
|
+
return inks;
|
|
6906
|
+
}
|
|
6907
|
+
const hex = spriteHex(layer, e);
|
|
6908
|
+
return hex === null ? null : [hex];
|
|
6909
|
+
}
|
|
6304
6910
|
function spriteHex(layer, e) {
|
|
6305
6911
|
const s = layer.sprite;
|
|
6306
6912
|
if (isShapedSprite(s)) {
|
|
@@ -6416,6 +7022,23 @@ var WASH_ALPHA_MAX = 0.45;
|
|
|
6416
7022
|
var COHESION_T = 5e3;
|
|
6417
7023
|
var OUTLINE_SAMPLES = 24;
|
|
6418
7024
|
var MAX_TRACED = 240;
|
|
7025
|
+
function discSamples(cx, cy, r, n) {
|
|
7026
|
+
const golden = Math.PI * (3 - Math.sqrt(5));
|
|
7027
|
+
return Array.from({ length: n }, (_, i) => {
|
|
7028
|
+
const rr = r * Math.sqrt((i + 0.5) / n);
|
|
7029
|
+
return { x: cx + rr * Math.cos(i * golden), y: cy + rr * Math.sin(i * golden) };
|
|
7030
|
+
});
|
|
7031
|
+
}
|
|
7032
|
+
function gridSamples(uw, vh, n, map) {
|
|
7033
|
+
if (!(uw > 0) || !(vh > 0)) return [];
|
|
7034
|
+
const nv = Math.min(n, Math.max(8, Math.round(Math.sqrt(n * vh / uw))));
|
|
7035
|
+
const nu = Math.max(8, Math.round(n / nv));
|
|
7036
|
+
const out = [];
|
|
7037
|
+
for (let i = 0; i < nu; i++) {
|
|
7038
|
+
for (let j = 0; j < nv; j++) out.push(map(((i + 0.5) / nu - 0.5) * uw, ((j + 0.5) / nv - 0.5) * vh));
|
|
7039
|
+
}
|
|
7040
|
+
return out;
|
|
7041
|
+
}
|
|
6419
7042
|
var EDGE_INSET = 1e-4;
|
|
6420
7043
|
function circleShape(cx, cy, r) {
|
|
6421
7044
|
return {
|
|
@@ -6424,6 +7047,7 @@ function circleShape(cx, cy, r) {
|
|
|
6424
7047
|
br: r,
|
|
6425
7048
|
area: Math.PI * r * r,
|
|
6426
7049
|
contains: (x, y) => (x - cx) * (x - cx) + (y - cy) * (y - cy) < r * r,
|
|
7050
|
+
interior: (n) => discSamples(cx, cy, r, n),
|
|
6427
7051
|
outline: (n) => Array.from({ length: n }, (_, i) => {
|
|
6428
7052
|
const a = i / n * Math.PI * 2;
|
|
6429
7053
|
const inner = r * (1 - EDGE_INSET);
|
|
@@ -6457,12 +7081,21 @@ function polyShape(cx, cy, raw) {
|
|
|
6457
7081
|
const step = Math.min(br * EDGE_INSET, shortest * 0.25);
|
|
6458
7082
|
const area = polygonArea(abs);
|
|
6459
7083
|
if (!(area > 0)) return null;
|
|
7084
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
7085
|
+
for (const p of abs) {
|
|
7086
|
+
x0 = Math.min(x0, p.x);
|
|
7087
|
+
y0 = Math.min(y0, p.y);
|
|
7088
|
+
x1 = Math.max(x1, p.x);
|
|
7089
|
+
y1 = Math.max(y1, p.y);
|
|
7090
|
+
}
|
|
6460
7091
|
return {
|
|
6461
7092
|
cx,
|
|
6462
7093
|
cy,
|
|
6463
7094
|
br,
|
|
6464
7095
|
area,
|
|
6465
7096
|
contains: inside2,
|
|
7097
|
+
// Its bounding box's grid, kept to the cells inside the outline.
|
|
7098
|
+
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)),
|
|
6466
7099
|
/**
|
|
6467
7100
|
* `n` points spread along the perimeter by ARC LENGTH, not one share per
|
|
6468
7101
|
* edge — a long skinny rectangle has two edges carrying almost all of its
|
|
@@ -6512,12 +7145,13 @@ function rectShape(cx, cy, halfW, halfH, rot) {
|
|
|
6512
7145
|
const cos = Math.cos(rot);
|
|
6513
7146
|
const sin = Math.sin(rot);
|
|
6514
7147
|
const corner = (lx, ly) => ({ x: cx + lx * cos - ly * sin, y: cy + lx * sin + ly * cos });
|
|
6515
|
-
|
|
7148
|
+
const shape = polyShape(cx, cy, [
|
|
6516
7149
|
corner(-halfW, -halfH),
|
|
6517
7150
|
corner(halfW, -halfH),
|
|
6518
7151
|
corner(halfW, halfH),
|
|
6519
7152
|
corner(-halfW, halfH)
|
|
6520
7153
|
]);
|
|
7154
|
+
return shape && { ...shape, interior: (n) => gridSamples(halfW * 2, halfH * 2, n, corner) };
|
|
6521
7155
|
}
|
|
6522
7156
|
function polygonShape(cx, cy, pts, rot) {
|
|
6523
7157
|
const cos = Math.cos(rot);
|
|
@@ -6577,8 +7211,7 @@ function cohesionOf(layers, t, w, h) {
|
|
|
6577
7211
|
seamCause
|
|
6578
7212
|
};
|
|
6579
7213
|
if (lifeAlphaAt(layer.life, t) <= 0) return { ...base, overlap: null, reads: null };
|
|
6580
|
-
const
|
|
6581
|
-
const solid = shapes.filter((x) => x !== null && x.area > 0);
|
|
7214
|
+
const solid = solidShapes(layer, entities, t, w, h);
|
|
6582
7215
|
if (solid.length < 2) return { ...base, overlap: null, reads: null };
|
|
6583
7216
|
const stride = Math.max(1, Math.ceil(solid.length / MAX_TRACED));
|
|
6584
7217
|
let covered = 0;
|
|
@@ -6610,11 +7243,54 @@ function cohesionOf(layers, t, w, h) {
|
|
|
6610
7243
|
return { ...base, overlap, reads };
|
|
6611
7244
|
});
|
|
6612
7245
|
}
|
|
6613
|
-
function
|
|
7246
|
+
function solidShapes(layer, entities, t, w, h) {
|
|
7247
|
+
return entities.map((e) => shapeOf(layer, e, t, w, h)).filter((x) => x !== null && x.area > 0);
|
|
7248
|
+
}
|
|
7249
|
+
var AREA_SAMPLES = 128;
|
|
7250
|
+
var TILED_AREA_MAX = 0.15;
|
|
7251
|
+
function buriedAreaOf(layer, entities, t, w, h) {
|
|
7252
|
+
if (lifeAlphaAt(layer.life, t) <= 0) return null;
|
|
7253
|
+
const solid = solidShapes(layer, entities, t, w, h);
|
|
7254
|
+
if (solid.length < 2) return null;
|
|
7255
|
+
const stride = Math.max(1, Math.ceil(solid.length / MAX_TRACED));
|
|
7256
|
+
let covered = 0;
|
|
7257
|
+
let traced = 0;
|
|
7258
|
+
for (let i = 0; i < solid.length; i += stride) {
|
|
7259
|
+
const pts = solid[i].interior(AREA_SAMPLES);
|
|
7260
|
+
if (pts.length === 0) continue;
|
|
7261
|
+
let hit = 0;
|
|
7262
|
+
for (const p of pts) {
|
|
7263
|
+
for (let j = 0; j < solid.length; j++) {
|
|
7264
|
+
if (j === i) continue;
|
|
7265
|
+
const other = solid[j];
|
|
7266
|
+
const dx = p.x - other.cx;
|
|
7267
|
+
const dy = p.y - other.cy;
|
|
7268
|
+
if (dx * dx + dy * dy > other.br * other.br) continue;
|
|
7269
|
+
if (other.contains(p.x, p.y)) {
|
|
7270
|
+
hit++;
|
|
7271
|
+
break;
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
7274
|
+
}
|
|
7275
|
+
covered += hit / pts.length;
|
|
7276
|
+
traced++;
|
|
7277
|
+
}
|
|
7278
|
+
return traced === 0 ? null : Number((covered / traced).toFixed(4));
|
|
7279
|
+
}
|
|
7280
|
+
function seamsWorthWarning(layer, c, entityCount, buriedArea) {
|
|
6614
7281
|
if (c.reads !== "seamed" || entityCount < 6) return false;
|
|
6615
7282
|
const [lo, hi] = layer.alpha ?? [1, 1];
|
|
6616
7283
|
if ((lo + hi) / 2 < WASH_ALPHA_MAX) return false;
|
|
6617
|
-
|
|
7284
|
+
if (layer.blend === "lighter" || layer.blend === "screen") return false;
|
|
7285
|
+
if (buriedArea && paletteIsTheOnlySeam(layer)) {
|
|
7286
|
+
const a = buriedArea();
|
|
7287
|
+
if (a !== null && a < TILED_AREA_MAX) return false;
|
|
7288
|
+
}
|
|
7289
|
+
return true;
|
|
7290
|
+
}
|
|
7291
|
+
function paletteIsTheOnlySeam(layer) {
|
|
7292
|
+
if (seamCauseOf(layer) === null) return false;
|
|
7293
|
+
return seamCauseOf({ ...layer, sprite: { ...layer.sprite, colors: void 0 } }) === null;
|
|
6618
7294
|
}
|
|
6619
7295
|
|
|
6620
7296
|
// src/perceive.ts
|
|
@@ -6663,7 +7339,9 @@ function textFontPx(s, size, fontScale) {
|
|
|
6663
7339
|
function textBox(s, e, p, fontScale, unitScale) {
|
|
6664
7340
|
const fh = textFontPx(s, e.size, fontScale);
|
|
6665
7341
|
const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
6666
|
-
const
|
|
7342
|
+
const head = Array.from(str).slice(0, 48);
|
|
7343
|
+
const wide = head.reduce((n, c) => n + (isFullWidth(c.codePointAt(0)) ? 1 : 0), 0);
|
|
7344
|
+
const est = Math.max(fh, fh * (0.62 * head.length + 0.38 * wide));
|
|
6667
7345
|
const fw = s.maxWidth ? Math.min(est, s.maxWidth * unitScale) : est;
|
|
6668
7346
|
const align = s.align ?? "center";
|
|
6669
7347
|
const baseline = s.baseline ?? "middle";
|
|
@@ -6680,7 +7358,7 @@ function textBlockBox(s, _e, p, w, h) {
|
|
|
6680
7358
|
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
6681
7359
|
const maxWPx = s.maxWidth * unitScale;
|
|
6682
7360
|
const maxWEm = maxWPx / fsPx;
|
|
6683
|
-
const lines = breakTextBlock(s
|
|
7361
|
+
const lines = breakTextBlock(textBlockText(s), maxWEm, textMetricsClassFor(s.font));
|
|
6684
7362
|
const totalH = lines.length * lh;
|
|
6685
7363
|
const maxLineW = lines.reduce((m, l) => Math.max(m, l.widthEm), 0) * fsPx;
|
|
6686
7364
|
const align = s.align ?? "left";
|
|
@@ -6697,7 +7375,7 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
6697
7375
|
if (!s.reveal) return 1;
|
|
6698
7376
|
const unitScale = Math.min(w, h);
|
|
6699
7377
|
const maxWEm = s.maxWidth * unitScale / (s.fontSize * unitScale);
|
|
6700
|
-
const lines = breakTextBlock(s
|
|
7378
|
+
const lines = breakTextBlock(textBlockText(s), maxWEm, textMetricsClassFor(s.font));
|
|
6701
7379
|
const rs = revealState(lines, s.reveal, t);
|
|
6702
7380
|
if (rs.glyphAlphas) {
|
|
6703
7381
|
let sum = 0;
|
|
@@ -6917,7 +7595,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
6917
7595
|
};
|
|
6918
7596
|
const splatPolygonOutline = (layer, s, e, p, sz, a, lum, tPass) => {
|
|
6919
7597
|
const radius = sz / 2;
|
|
6920
|
-
const rot = rotationAt(e, tPass);
|
|
7598
|
+
const rot = rotationAt(e, tPass) + paintRotation(spec, layer);
|
|
6921
7599
|
const cr = Math.cos(rot);
|
|
6922
7600
|
const sr = Math.sin(rot);
|
|
6923
7601
|
const fr = isPathPolygon(s) ? pathFrame(s, radius, w, h) : { sx: radius, sy: radius, ox: 0, oy: 0, uniform: true };
|
|
@@ -6940,7 +7618,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
6940
7618
|
if (!part.stroke) return [];
|
|
6941
7619
|
const hw = part.stroke.width * radius / 2;
|
|
6942
7620
|
const out = [];
|
|
6943
|
-
for (const l of part
|
|
7621
|
+
for (const l of strokeLines(part)) {
|
|
6944
7622
|
const m = l.points.map(([u, v]) => mapUnit(u, v));
|
|
6945
7623
|
const n = l.closed ? m.length : m.length - 1;
|
|
6946
7624
|
for (let i = 0; i < n; i++) {
|
|
@@ -7034,9 +7712,12 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
7034
7712
|
for (const { layer, entities } of scene.layers) {
|
|
7035
7713
|
const lifeA = layer.opacity === void 0 && layer.group === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * paintOpacity(spec, layer);
|
|
7036
7714
|
if (lifeA <= 0) continue;
|
|
7037
|
-
for (
|
|
7038
|
-
|
|
7039
|
-
const
|
|
7715
|
+
for (let ei = 0; ei < entities.length; ei++) {
|
|
7716
|
+
const e = entities[ei];
|
|
7717
|
+
const entA = layer.life?.stagger ? lifeA * lifeStaggerAt(layer.life, tPass, ei) : lifeA;
|
|
7718
|
+
if (entA <= 0) continue;
|
|
7719
|
+
splatTrail(layer, e, tPass, entA, alphaScale, entities.length);
|
|
7720
|
+
const a = Math.min(1, alphaAt(e, tPass) * entA) * alphaScale;
|
|
7040
7721
|
if (a <= 4e-3) continue;
|
|
7041
7722
|
const lum = spriteLuma(layer, e);
|
|
7042
7723
|
const p = posOf(scene, e, tPass);
|
|
@@ -7047,8 +7728,8 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
7047
7728
|
continue;
|
|
7048
7729
|
}
|
|
7049
7730
|
if (s.kind === "stroke") {
|
|
7050
|
-
const pts =
|
|
7051
|
-
let angle = rotationAt(e, tPass);
|
|
7731
|
+
const pts = strokeDrawnSamples(s, sz / 2);
|
|
7732
|
+
let angle = rotationAt(e, tPass) + paintRotation(spec, layer);
|
|
7052
7733
|
if (s.orient) angle += headingAt(e, tPass, w, h) ?? 0;
|
|
7053
7734
|
const ca = Math.cos(angle);
|
|
7054
7735
|
const sa = Math.sin(angle);
|
|
@@ -7063,6 +7744,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
7063
7744
|
compose(r * cols + c, lum, a * Math.min(1, lw * taper / cellH), layer.blend);
|
|
7064
7745
|
};
|
|
7065
7746
|
const step = Math.min(cellW, cellH) / 2;
|
|
7747
|
+
if (pts.length < 2) continue;
|
|
7066
7748
|
stampAt(pts[0], 0);
|
|
7067
7749
|
for (let i = 1; i < pts.length; i++) {
|
|
7068
7750
|
const q0 = pts[i - 1];
|
|
@@ -7077,7 +7759,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
7077
7759
|
continue;
|
|
7078
7760
|
}
|
|
7079
7761
|
if (s.kind === "streak") {
|
|
7080
|
-
const heading = (headingAt(e, tPass, w, h) ?? 0) + rotationAt(e, tPass);
|
|
7762
|
+
const heading = (headingAt(e, tPass, w, h) ?? 0) + (rotationAt(e, tPass) + paintRotation(spec, layer));
|
|
7081
7763
|
const steps = Math.max(2, Math.ceil(sz / Math.min(cellW, cellH)));
|
|
7082
7764
|
const wgt = Math.min(1, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale / cellH);
|
|
7083
7765
|
for (let i = 0; i <= steps; i++) {
|
|
@@ -7112,7 +7794,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
7112
7794
|
if (len <= 0) continue;
|
|
7113
7795
|
const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
|
|
7114
7796
|
const box = barBox(s.direction ?? "right", len, th);
|
|
7115
|
-
const rot = rotationAt(e, tPass);
|
|
7797
|
+
const rot = rotationAt(e, tPass) + paintRotation(spec, layer);
|
|
7116
7798
|
const cr = Math.cos(rot);
|
|
7117
7799
|
const sr = Math.sin(rot);
|
|
7118
7800
|
centerX = p.x + box.cx * cr - box.cy * sr;
|
|
@@ -7158,13 +7840,15 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
7158
7840
|
for (const edge of edges) {
|
|
7159
7841
|
const pi = positions[edge.i];
|
|
7160
7842
|
const pj = positions[edge.j];
|
|
7843
|
+
const env = layer.life?.stagger ? Math.min(lifeStaggerAt(layer.life, tPass, edge.i), lifeStaggerAt(layer.life, tPass, edge.j)) : 1;
|
|
7844
|
+
if (env <= 0) continue;
|
|
7161
7845
|
const steps = Math.max(1, Math.ceil(edge.dist / Math.min(cellW, cellH)));
|
|
7162
7846
|
for (let i = 0; i <= steps; i++) {
|
|
7163
7847
|
const k = i / steps;
|
|
7164
7848
|
const c = Math.floor((pi.x + (pj.x - pi.x) * k) / cellW);
|
|
7165
7849
|
const r = Math.floor((pi.y + (pj.y - pi.y) * k) / cellH);
|
|
7166
7850
|
if (c < 0 || c >= cols || r < 0 || r >= rows) continue;
|
|
7167
|
-
compose(r * cols + c, lum, wgt, layer.blend);
|
|
7851
|
+
compose(r * cols + c, lum, wgt * env, layer.blend);
|
|
7168
7852
|
}
|
|
7169
7853
|
}
|
|
7170
7854
|
}
|
|
@@ -7222,6 +7906,57 @@ function finishGrid(cols, rows, cells, bgRow, bgCells) {
|
|
|
7222
7906
|
colProfile
|
|
7223
7907
|
};
|
|
7224
7908
|
}
|
|
7909
|
+
function layerCellFootprints(spec, opts = {}) {
|
|
7910
|
+
const scene = buildScene(spec, opts);
|
|
7911
|
+
const cols = Math.max(8, Math.min(200, opts.cols ?? 80));
|
|
7912
|
+
const rows = Math.max(8, Math.min(200, opts.rows ?? 48));
|
|
7913
|
+
const { w, h, scale } = scene;
|
|
7914
|
+
const cellW = w / cols;
|
|
7915
|
+
const cellH = h / rows;
|
|
7916
|
+
const fontScale = fontScaleFor(spec, w, h);
|
|
7917
|
+
return scene.layers.map(({ layer, entities }) => {
|
|
7918
|
+
if (layer.motion.type !== "static" || layer.emit || layer.links || layer.trail) return null;
|
|
7919
|
+
const s = layer.sprite;
|
|
7920
|
+
const k = layer.transform !== void 0 || layer.group !== void 0 ? paintSize(spec, layer) : 1;
|
|
7921
|
+
const cells = /* @__PURE__ */ new Set();
|
|
7922
|
+
for (const e of entities) {
|
|
7923
|
+
if (e.orbitParent || e.cyclePeriod > 0) return null;
|
|
7924
|
+
const p = posOf(scene, e, 0);
|
|
7925
|
+
const grow = 1 + Math.abs(e.growAmp);
|
|
7926
|
+
const sz = e.size * grow * k;
|
|
7927
|
+
const per = e.size > 0 ? sz / e.size : 1;
|
|
7928
|
+
let reach;
|
|
7929
|
+
if (s.kind === "circle" || s.kind === "ring") reach = sz / 2;
|
|
7930
|
+
else if (s.kind === "polygon" && isPathPolygon(s)) {
|
|
7931
|
+
const fr = pathFrame(s, sz / 2, w, h);
|
|
7932
|
+
reach = 0;
|
|
7933
|
+
for (const part of pathParts(s)) {
|
|
7934
|
+
const { cx, cy, hw, hh } = part.box;
|
|
7935
|
+
const dx = Math.max(Math.abs(cx - hw - fr.ox), Math.abs(cx + hw - fr.ox)) * fr.sx;
|
|
7936
|
+
const dy = Math.max(Math.abs(cy - hh - fr.oy), Math.abs(cy + hh - fr.oy)) * fr.sy;
|
|
7937
|
+
reach = Math.max(reach, Math.hypot(dx, dy) + (part.stroke?.width ?? 0) * sz / 4);
|
|
7938
|
+
}
|
|
7939
|
+
} else if (s.kind === "polygon") reach = polygonPoints(s, sz / 2).reduce((m, q) => Math.max(m, Math.hypot(q.x, q.y)), sz / 2);
|
|
7940
|
+
else if (s.kind === "rect") reach = Math.hypot(sz, (e.size2 ?? e.size) * per) / 2;
|
|
7941
|
+
else if (s.kind === "bar") {
|
|
7942
|
+
const b = barBox(s.direction ?? "right", sz * barFraction(s, e.barIndex ?? 0), e.size2 !== void 0 ? e.size2 * per : sz * 0.2);
|
|
7943
|
+
reach = Math.hypot(Math.abs(b.cx) + b.halfX, Math.abs(b.cy) + b.halfY);
|
|
7944
|
+
} else if (s.kind === "streak") reach = sz + (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale;
|
|
7945
|
+
else if (s.kind === "stroke") reach = strokeSamples(s, sz / 2).reduce((m, q) => Math.max(m, Math.hypot(q.x, q.y)), 0) + strokeWidthPx(s, scale);
|
|
7946
|
+
else if (s.kind === "text" || s.kind === "textBlock") {
|
|
7947
|
+
const b = s.kind === "text" ? textBox(s, e, p, fontScale, scale) : textBlockBox(s, e, p, w, h);
|
|
7948
|
+
reach = Math.hypot(Math.abs(b.cx - p.x) + b.halfX, Math.abs(b.cy - p.y) + b.halfY) * grow * Math.max(1, k);
|
|
7949
|
+
} else reach = sz;
|
|
7950
|
+
if (!Number.isFinite(reach) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return null;
|
|
7951
|
+
const c0 = Math.max(0, Math.floor((p.x - reach - 1) / cellW));
|
|
7952
|
+
const c1 = Math.min(cols - 1, Math.floor((p.x + reach + 1) / cellW));
|
|
7953
|
+
const r0 = Math.max(0, Math.floor((p.y - reach - 1) / cellH));
|
|
7954
|
+
const r1 = Math.min(rows - 1, Math.floor((p.y + reach + 1) / cellH));
|
|
7955
|
+
for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) cells.add(r * cols + c);
|
|
7956
|
+
}
|
|
7957
|
+
return [...cells].sort((a, b) => a - b);
|
|
7958
|
+
});
|
|
7959
|
+
}
|
|
7225
7960
|
var DITHER = [
|
|
7226
7961
|
[0.5 / 8, 4.5 / 8],
|
|
7227
7962
|
[2.5 / 8, 6.5 / 8],
|
|
@@ -7284,7 +8019,7 @@ function textSprites(spec, opts = {}) {
|
|
|
7284
8019
|
const s2 = layer.sprite;
|
|
7285
8020
|
const unitScale = Math.min(scene.w, scene.h);
|
|
7286
8021
|
const sizePx2 = s2.fontSize * unitScale;
|
|
7287
|
-
const info = { layerIndex, key: layer.key, strings: [s2
|
|
8022
|
+
const info = { layerIndex, key: layer.key, strings: [textBlockText(s2)], count: entities.length, sizePx: Math.round(sizePx2) };
|
|
7288
8023
|
if (s2.reveal) info.revealed = Math.round(textBlockRevealFraction(s2, scene.w, scene.h, t) * 100) / 100;
|
|
7289
8024
|
out.push(info);
|
|
7290
8025
|
return;
|
|
@@ -7311,6 +8046,42 @@ function rankDominance(raw) {
|
|
|
7311
8046
|
factors: r.factors
|
|
7312
8047
|
}));
|
|
7313
8048
|
}
|
|
8049
|
+
function onScreenFraction(scene, s, e, sz, k, t, paintRot = 0) {
|
|
8050
|
+
if (s.kind === "text" || s.kind === "textBlock") return 1;
|
|
8051
|
+
const { w, h } = scene;
|
|
8052
|
+
const p = posOf(scene, e, t);
|
|
8053
|
+
let cx = p.x, cy = p.y, hx = sz / 2 * k, hy = hx;
|
|
8054
|
+
if (s.kind === "bar") {
|
|
8055
|
+
const len = sz * barFraction(s, e.barIndex ?? 0);
|
|
8056
|
+
const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
|
|
8057
|
+
const b = barBox(s.direction ?? "right", len * k, th * k);
|
|
8058
|
+
if (!(b.halfX > 0) || !(b.halfY > 0)) return 1;
|
|
8059
|
+
const rot = rotationAt(e, t);
|
|
8060
|
+
const cr = Math.cos(rot), sr = Math.sin(rot);
|
|
8061
|
+
cx = p.x + b.cx * cr - b.cy * sr;
|
|
8062
|
+
cy = p.y + b.cx * sr + b.cy * cr;
|
|
8063
|
+
const ex = Math.abs(b.halfX * cr) + Math.abs(b.halfY * sr);
|
|
8064
|
+
const ey = Math.abs(b.halfX * sr) + Math.abs(b.halfY * cr);
|
|
8065
|
+
const fx2 = Math.max(0, Math.min(cx + ex, w) - Math.max(cx - ex, 0)) / (2 * ex);
|
|
8066
|
+
const fy2 = Math.max(0, Math.min(cy + ey, h) - Math.max(cy - ey, 0)) / (2 * ey);
|
|
8067
|
+
return fx2 * fy2;
|
|
8068
|
+
}
|
|
8069
|
+
if (s.kind === "polygon" && isPathPolygon(s)) {
|
|
8070
|
+
const fr = pathFrame(s, sz / 2, w, h);
|
|
8071
|
+
const rot = rotationAt(e, t) + paintRot;
|
|
8072
|
+
const cr = Math.cos(rot), sr = Math.sin(rot);
|
|
8073
|
+
const ox = -fr.ox * fr.sx * k, oy = -fr.oy * fr.sy * k;
|
|
8074
|
+
cx += ox * cr - oy * sr;
|
|
8075
|
+
cy += ox * sr + oy * cr;
|
|
8076
|
+
const qx = fr.sx * k, qy = fr.sy * k;
|
|
8077
|
+
hx = Math.abs(qx * cr) + Math.abs(qy * sr);
|
|
8078
|
+
hy = Math.abs(qx * sr) + Math.abs(qy * cr);
|
|
8079
|
+
}
|
|
8080
|
+
if (!(hx > 0) || !(hy > 0)) return 1;
|
|
8081
|
+
const fx = Math.max(0, Math.min(cx + hx, w) - Math.max(cx - hx, 0)) / (2 * hx);
|
|
8082
|
+
const fy = Math.max(0, Math.min(cy + hy, h) - Math.max(cy - hy, 0)) / (2 * hy);
|
|
8083
|
+
return fx * fy;
|
|
8084
|
+
}
|
|
7314
8085
|
function rawDominance(spec, opts = {}) {
|
|
7315
8086
|
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
7316
8087
|
const scene = buildScene(spec, opts);
|
|
@@ -7342,8 +8113,9 @@ function rawDominance(spec, opts = {}) {
|
|
|
7342
8113
|
const areaScale = painted ? paintSize(spec, layer) ** 2 : 1;
|
|
7343
8114
|
let area = 0;
|
|
7344
8115
|
let lumAcc = 0;
|
|
7345
|
-
for (
|
|
7346
|
-
const
|
|
8116
|
+
for (let ei = 0; ei < entities.length; ei++) {
|
|
8117
|
+
const e = entities[ei];
|
|
8118
|
+
const a = alphaAt(e, t) * lifeA * lifeStaggerAt(layer.life, t, ei);
|
|
7347
8119
|
const sz = sizeAt(e, t);
|
|
7348
8120
|
const s = layer.sprite;
|
|
7349
8121
|
let entArea;
|
|
@@ -7354,13 +8126,17 @@ function rawDominance(spec, opts = {}) {
|
|
|
7354
8126
|
const h2 = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
7355
8127
|
const f = s.feather ? (1 - s.feather / 2) * (1 - s.feather / 2) : 1;
|
|
7356
8128
|
entArea = sz * h2 * f;
|
|
7357
|
-
} else if (s.kind === "polygon")
|
|
7358
|
-
|
|
8129
|
+
} else if (s.kind === "polygon") {
|
|
8130
|
+
if (isPathPolygon(s)) {
|
|
8131
|
+
const fr = pathFrame(s, sz / 2, w, h);
|
|
8132
|
+
entArea = pathInkArea(s, fr.sx, fr.sy, sz / 2);
|
|
8133
|
+
} else entArea = polygonArea(polygonPoints(s, sz / 2));
|
|
8134
|
+
} else if (s.kind === "bar") {
|
|
7359
8135
|
const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
|
|
7360
8136
|
entArea = sz * barFraction(s, e.barIndex ?? 0) * th;
|
|
7361
8137
|
} else if (s.kind === "stroke") {
|
|
7362
8138
|
const meanTaper = s.taper ? 0.64 : 1;
|
|
7363
|
-
entArea = pathLength(
|
|
8139
|
+
entArea = pathLength(strokeDrawnSamples(s, sz / 2)) * strokeWidthPx(s, scale) * meanTaper * LINE_SALIENCE;
|
|
7364
8140
|
} else if (s.kind === "text") {
|
|
7365
8141
|
const box = textBox(s, e, { x: 0, y: 0 }, fontScaleFor(spec, w, h), scale);
|
|
7366
8142
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
@@ -7368,6 +8144,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
7368
8144
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
7369
8145
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
|
|
7370
8146
|
} else entArea = sz * sz * 0.55;
|
|
8147
|
+
entArea *= onScreenFraction(scene, s, e, sz, painted ? paintSize(spec, layer) : 1, t, painted ? paintRotation(spec, layer) : 0);
|
|
7371
8148
|
area += painted ? entArea * areaScale * a : entArea * a;
|
|
7372
8149
|
if (layer.trail) {
|
|
7373
8150
|
const fade = layer.trail.fade ?? 1;
|
|
@@ -7386,7 +8163,10 @@ function rawDominance(spec, opts = {}) {
|
|
|
7386
8163
|
const la = (layer.links.alpha ?? 0.6) * lifeA;
|
|
7387
8164
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
7388
8165
|
const lw = (layer.links.width ?? defaultWidth) * scale;
|
|
7389
|
-
for (const edge of edges)
|
|
8166
|
+
for (const edge of edges) {
|
|
8167
|
+
const env = layer.life?.stagger ? Math.min(lifeStaggerAt(layer.life, t, edge.i), lifeStaggerAt(layer.life, t, edge.j)) : 1;
|
|
8168
|
+
area += edge.dist * lw * la * LINE_SALIENCE * env;
|
|
8169
|
+
}
|
|
7390
8170
|
}
|
|
7391
8171
|
const meanLuma = entities.length ? lumAcc / entities.length : 0;
|
|
7392
8172
|
const contrast = Math.abs(meanLuma - bgMean) + 0.1;
|
|
@@ -7412,6 +8192,7 @@ function motionStats(spec, opts = {}) {
|
|
|
7412
8192
|
const unit = Math.min(w, h);
|
|
7413
8193
|
return scene0.layers.map(({ layer, entities }, layerIndex) => {
|
|
7414
8194
|
const entities1 = scene1.layers[layerIndex]?.entities ?? entities;
|
|
8195
|
+
const spinVisible = layer.sprite.kind !== "circle" && layer.sprite.kind !== "ring";
|
|
7415
8196
|
let acc = 0;
|
|
7416
8197
|
let max = 0;
|
|
7417
8198
|
let n = 0;
|
|
@@ -7425,7 +8206,8 @@ function motionStats(spec, opts = {}) {
|
|
|
7425
8206
|
const dy = p1.y - p0.y;
|
|
7426
8207
|
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) return;
|
|
7427
8208
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
7428
|
-
const
|
|
8209
|
+
const turn = spinVisible ? Math.abs(rotationAt(entities1[i] ?? e, t + dt) - rotationAt(e, t)) * (sizeAt(e, t) / 3) * paintSize(spec, layer) : 0;
|
|
8210
|
+
const speed = Math.sqrt(dist * dist + turn * turn) / dt * 1e3;
|
|
7429
8211
|
acc += speed;
|
|
7430
8212
|
max = Math.max(max, speed);
|
|
7431
8213
|
n++;
|
|
@@ -7482,17 +8264,37 @@ function sceneColour(spec, opts = {}) {
|
|
|
7482
8264
|
let wsum = 0;
|
|
7483
8265
|
for (const { layer, entities } of scene.layers) {
|
|
7484
8266
|
const lifeA = lifeAlphaAt(layer.life, t) * (layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer));
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
if (
|
|
7488
|
-
const sz = sizeAt(e, t);
|
|
7489
|
-
const weight = alphaAt(e, t) * lifeA * (layer.sprite.kind === "circle" ? Math.PI * (sz / 2) * (sz / 2) : sz * sz);
|
|
7490
|
-
if (weight <= 0) continue;
|
|
8267
|
+
const k = layer.transform !== void 0 || layer.group !== void 0 ? paintSize(spec, layer) : 1;
|
|
8268
|
+
const add = (hex, weight) => {
|
|
8269
|
+
if (weight <= 0) return;
|
|
7491
8270
|
const c = hexRgb(hex);
|
|
7492
8271
|
wr += c.r * weight;
|
|
7493
8272
|
wg += c.g * weight;
|
|
7494
8273
|
wb += c.b * weight;
|
|
7495
8274
|
wsum += weight;
|
|
8275
|
+
};
|
|
8276
|
+
for (let ei = 0; ei < entities.length; ei++) {
|
|
8277
|
+
const e = entities[ei];
|
|
8278
|
+
const sz = sizeAt(e, t);
|
|
8279
|
+
const s = layer.sprite;
|
|
8280
|
+
const a = alphaAt(e, t) * lifeA * lifeStaggerAt(layer.life, t, ei) * onScreenFraction(scene, s, e, sz, k, t, paintRotation(spec, layer));
|
|
8281
|
+
if (a <= 0) continue;
|
|
8282
|
+
if (s.kind === "polygon" && isPathPolygon(s)) {
|
|
8283
|
+
const fr = pathFrame(s, sz / 2, scene.w, scene.h);
|
|
8284
|
+
const base = s.colors?.[e.colorIndex] ?? s.color;
|
|
8285
|
+
for (const part of pathParts(s)) {
|
|
8286
|
+
const ink = partInkArea(part, fr.sx, fr.sy, sz / 2);
|
|
8287
|
+
if (part.gradient) {
|
|
8288
|
+
const gw = gradientStopWeights(part.gradient.stops);
|
|
8289
|
+
part.gradient.stops.forEach((st, i) => add(st.color, a * ink.fill * (st.opacity ?? 1) * gw[i]));
|
|
8290
|
+
} else add(part.color ?? base, a * ink.fill);
|
|
8291
|
+
if (part.stroke) add(part.stroke.color, a * ink.stroke);
|
|
8292
|
+
}
|
|
8293
|
+
continue;
|
|
8294
|
+
}
|
|
8295
|
+
const hex = spriteHex(layer, e);
|
|
8296
|
+
if (hex === null) continue;
|
|
8297
|
+
add(hex, a * (s.kind === "circle" ? Math.PI * (sz / 2) * (sz / 2) : sz * sz));
|
|
7496
8298
|
}
|
|
7497
8299
|
}
|
|
7498
8300
|
const bg = backgroundRgb(spec);
|
|
@@ -7685,19 +8487,263 @@ function perceiveSequenceFrame(seq, T, opts = {}) {
|
|
|
7685
8487
|
};
|
|
7686
8488
|
}
|
|
7687
8489
|
|
|
8490
|
+
// src/flash-timeline.ts
|
|
8491
|
+
import { analyzeFlashes, srgbToLinear as srgbToLinear2 } from "@idle-screens/validator";
|
|
8492
|
+
var LUMA_PATH = /(^|\.)(opacity|alpha|color|colors|stops|bands|gradient|stroke|fill|background|draw)(\.|$)|(^|\.)(sprite|parts?)$|^layers\.\d+$/;
|
|
8493
|
+
var MIN_BOUNDARIES = 7;
|
|
8494
|
+
var MAX_SAMPLES = 64;
|
|
8495
|
+
var MAX_TOTAL_SAMPLES = 512;
|
|
8496
|
+
var WINDOW = 1e3;
|
|
8497
|
+
var COLS = 16;
|
|
8498
|
+
var ROWS = 9;
|
|
8499
|
+
var AREA_THRESHOLD = 0.25;
|
|
8500
|
+
function lumaTracks(spec) {
|
|
8501
|
+
const tl = spec.timeline;
|
|
8502
|
+
if (!tl) return { tracks: [], lap: 0 };
|
|
8503
|
+
const lap = tl.loop && tl.duration ? tl.duration : 0;
|
|
8504
|
+
const tracks = [];
|
|
8505
|
+
for (const [path, keys] of timelineTracks(spec)) {
|
|
8506
|
+
if (!LUMA_PATH.test(path)) continue;
|
|
8507
|
+
const bounds = [];
|
|
8508
|
+
for (const k of keys) {
|
|
8509
|
+
const dur = k.dur ?? DEFAULT_KEY_DUR;
|
|
8510
|
+
bounds.push(k.t);
|
|
8511
|
+
if (dur > 0) bounds.push(k.t + dur);
|
|
8512
|
+
}
|
|
8513
|
+
tracks.push({ path, times: lap ? [...bounds, ...bounds.map((b) => b + lap)] : bounds });
|
|
8514
|
+
}
|
|
8515
|
+
return { tracks, lap };
|
|
8516
|
+
}
|
|
8517
|
+
function lumaKeyTimes(spec) {
|
|
8518
|
+
const { tracks, lap } = lumaTracks(spec);
|
|
8519
|
+
return { times: [...new Set(tracks.flatMap((tr) => tr.times))].sort((a, b) => a - b), lap };
|
|
8520
|
+
}
|
|
8521
|
+
var PAINT_LEAF = /(^|\.)(opacity|alpha|color|fill)(\.\d+)?$|(^|\.)gradient(\.|$)|(^|\.)draw\.(start|end)$/;
|
|
8522
|
+
function pathCells(spec) {
|
|
8523
|
+
const layers = spec.layers ?? [];
|
|
8524
|
+
const volatile = /* @__PURE__ */ new Set();
|
|
8525
|
+
const members = (g) => layers.flatMap((l, i) => l.group === g ? [i] : []);
|
|
8526
|
+
const reach = /* @__PURE__ */ new Map();
|
|
8527
|
+
for (const path of timelineTracks(spec).keys()) {
|
|
8528
|
+
const [root, id, ...rest] = path.split(".");
|
|
8529
|
+
const leaf = rest.join(".");
|
|
8530
|
+
let to;
|
|
8531
|
+
if (root === "layers" && /^\d+$/.test(id ?? "")) {
|
|
8532
|
+
const n = Number(id);
|
|
8533
|
+
to = [n];
|
|
8534
|
+
if (!PAINT_LEAF.test(leaf)) {
|
|
8535
|
+
if (rest[0] === "transform") volatile.add(n);
|
|
8536
|
+
else for (let i = n; i < layers.length; i++) volatile.add(i);
|
|
8537
|
+
}
|
|
8538
|
+
} else if (root === "groups" && id !== void 0) {
|
|
8539
|
+
to = members(id);
|
|
8540
|
+
if (leaf !== "opacity") for (const i of to) volatile.add(i);
|
|
8541
|
+
} else if (root === "background") to = "all";
|
|
8542
|
+
else return null;
|
|
8543
|
+
if (LUMA_PATH.test(path)) reach.set(path, to);
|
|
8544
|
+
}
|
|
8545
|
+
if (reach.size === 0) return /* @__PURE__ */ new Map();
|
|
8546
|
+
const footprints = layerCellFootprints(withoutTimeline(spec), { cols: COLS, rows: ROWS });
|
|
8547
|
+
const out = /* @__PURE__ */ new Map();
|
|
8548
|
+
for (const [path, to] of reach) {
|
|
8549
|
+
if (to === "all" || to.some((i) => volatile.has(i) || !footprints[i])) out.set(path, null);
|
|
8550
|
+
else out.set(path, [...new Set(to.flatMap((i) => footprints[i]))]);
|
|
8551
|
+
}
|
|
8552
|
+
return out;
|
|
8553
|
+
}
|
|
8554
|
+
function cellsThatCanFlash(tracks, cellsOf, from, to) {
|
|
8555
|
+
const perCell = Array.from({ length: COLS * ROWS }, () => []);
|
|
8556
|
+
for (const tr of tracks) {
|
|
8557
|
+
const ts = tr.times.filter((t) => t >= from && t <= to);
|
|
8558
|
+
if (ts.length === 0) continue;
|
|
8559
|
+
const cells = cellsOf.get(tr.path);
|
|
8560
|
+
if (cells === null || cells === void 0) for (const list of perCell) list.push(...ts);
|
|
8561
|
+
else for (const c of cells) perCell[c].push(...ts);
|
|
8562
|
+
}
|
|
8563
|
+
let n = 0;
|
|
8564
|
+
for (const list of perCell) {
|
|
8565
|
+
if (list.length < MIN_BOUNDARIES) continue;
|
|
8566
|
+
const ts = [...new Set(list)].sort((a, b) => a - b);
|
|
8567
|
+
for (let i = 0; i + MIN_BOUNDARIES - 1 < ts.length; i++) {
|
|
8568
|
+
if (ts[i + MIN_BOUNDARIES - 1] - ts[i] <= WINDOW + 2) {
|
|
8569
|
+
n++;
|
|
8570
|
+
break;
|
|
8571
|
+
}
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
return n;
|
|
8575
|
+
}
|
|
8576
|
+
function flashSampleTimes(boundaries) {
|
|
8577
|
+
const ts = /* @__PURE__ */ new Set();
|
|
8578
|
+
boundaries.forEach((t, i) => {
|
|
8579
|
+
ts.add(t - 1);
|
|
8580
|
+
ts.add(t + 1);
|
|
8581
|
+
const next = boundaries[i + 1];
|
|
8582
|
+
if (next !== void 0 && next - t > 4) ts.add((t + next) / 2);
|
|
8583
|
+
});
|
|
8584
|
+
return [...ts].sort((a, b) => a - b);
|
|
8585
|
+
}
|
|
8586
|
+
function timelineFlashReport(spec) {
|
|
8587
|
+
const pass = { fails: false, flashesPerSecond: 0, flashingArea: 0, at: null };
|
|
8588
|
+
if (!spec.timeline) return pass;
|
|
8589
|
+
const { tracks, lap } = lumaTracks(spec);
|
|
8590
|
+
const times = [...new Set(tracks.flatMap((tr) => tr.times))].sort((a, b) => a - b);
|
|
8591
|
+
const dense = [];
|
|
8592
|
+
for (let i = 0, e = 0; i < times.length; i++) {
|
|
8593
|
+
while (e < times.length && times[e] < times[i] + WINDOW) e++;
|
|
8594
|
+
if (e - i < MIN_BOUNDARIES) continue;
|
|
8595
|
+
const last = dense[dense.length - 1];
|
|
8596
|
+
if (last && times[i] <= last.to) {
|
|
8597
|
+
last.to = Math.max(last.to, times[e - 1]);
|
|
8598
|
+
last.n = Math.max(last.n, e - i);
|
|
8599
|
+
} else dense.push({ from: times[i], to: times[e - 1], n: e - i });
|
|
8600
|
+
}
|
|
8601
|
+
if (dense.length === 0) return pass;
|
|
8602
|
+
dense.sort((a, b) => b.n - a.n);
|
|
8603
|
+
let budget = MAX_TOTAL_SAMPLES;
|
|
8604
|
+
let worst = pass;
|
|
8605
|
+
const cellsOf = pathCells(spec);
|
|
8606
|
+
for (const w of dense) {
|
|
8607
|
+
if (cellsOf && cellsThatCanFlash(tracks, cellsOf, w.from, w.to) < Math.ceil(AREA_THRESHOLD * COLS * ROWS)) continue;
|
|
8608
|
+
const sampleTimes = flashSampleTimes(times.filter((t) => t >= w.from && t <= w.to));
|
|
8609
|
+
for (let from = 0; from < sampleTimes.length - 1; ) {
|
|
8610
|
+
const chunk = sampleTimes.slice(from, from + MAX_SAMPLES);
|
|
8611
|
+
if (chunk.length < 2) break;
|
|
8612
|
+
if (chunk.length > budget) return { ...worst, fails: true, at: lap ? w.from % lap : w.from, incomplete: true, boundariesPerSecond: w.n };
|
|
8613
|
+
budget -= chunk.length;
|
|
8614
|
+
const samples = chunk.map((t) => ({ t, lum: luminanceGrid(spec, { t, cols: 16, rows: 9 }).cells.flat().map((v) => srgbToLinear2(v * 255)) }));
|
|
8615
|
+
const r = analyzeFlashes(samples);
|
|
8616
|
+
if (r.general.worstTileFlashesPerSecond > worst.flashesPerSecond || !worst.fails && r.general.fails) {
|
|
8617
|
+
worst = {
|
|
8618
|
+
fails: r.general.fails,
|
|
8619
|
+
flashesPerSecond: r.general.worstTileFlashesPerSecond,
|
|
8620
|
+
flashingArea: r.general.flashingAreaFraction,
|
|
8621
|
+
at: lap ? w.from % lap : w.from
|
|
8622
|
+
};
|
|
8623
|
+
}
|
|
8624
|
+
if (worst.fails) return worst;
|
|
8625
|
+
if (from + MAX_SAMPLES >= sampleTimes.length) break;
|
|
8626
|
+
const resume = chunk[chunk.length - 1] - WINDOW;
|
|
8627
|
+
let next = from + 1;
|
|
8628
|
+
while (sampleTimes[next] < resume) next++;
|
|
8629
|
+
from = next;
|
|
8630
|
+
}
|
|
8631
|
+
}
|
|
8632
|
+
return worst;
|
|
8633
|
+
}
|
|
8634
|
+
|
|
8635
|
+
// src/period.ts
|
|
8636
|
+
var MAX_SCENE_PERIOD = 36e5;
|
|
8637
|
+
var PERIOD_LEAF = /(^|\.)(period|rate|every|duration|spin|speed|blink|drift)$/;
|
|
8638
|
+
var gcd = (a, b) => {
|
|
8639
|
+
while (b) [a, b] = [b, a % b];
|
|
8640
|
+
return a;
|
|
8641
|
+
};
|
|
8642
|
+
function toRatio(ms) {
|
|
8643
|
+
for (let q = 1; q <= 1e3; q++) {
|
|
8644
|
+
const p = ms * q;
|
|
8645
|
+
if (Math.abs(p - Math.round(p)) < 1e-6) {
|
|
8646
|
+
const n = Math.round(p);
|
|
8647
|
+
const g = gcd(n, q);
|
|
8648
|
+
return [n / g, q / g];
|
|
8649
|
+
}
|
|
8650
|
+
}
|
|
8651
|
+
return null;
|
|
8652
|
+
}
|
|
8653
|
+
var range = (r) => r === void 0 ? null : typeof r === "number" ? r : r[0] === r[1] ? r[0] : NaN;
|
|
8654
|
+
function scenePeriod(spec) {
|
|
8655
|
+
const parts = [];
|
|
8656
|
+
const reasons = [];
|
|
8657
|
+
const add = (label, ms) => {
|
|
8658
|
+
if (!(ms > 0) || !Number.isFinite(ms)) return;
|
|
8659
|
+
const ratio = toRatio(ms);
|
|
8660
|
+
if (!ratio) reasons.push(`${label} repeats every ${Number(ms.toFixed(3))} ms, which has no exact ratio to combine`);
|
|
8661
|
+
else parts.push([label, ratio[0], ratio[1]]);
|
|
8662
|
+
};
|
|
8663
|
+
const tl = spec.timeline;
|
|
8664
|
+
if (tl && tl.keys.length > 0) {
|
|
8665
|
+
const keyed = tl.keys.filter((k) => PERIOD_LEAF.test(String(k.path)));
|
|
8666
|
+
if (keyed.length > 0) reasons.push(`timeline keys animate ${[...new Set(keyed.map((k) => String(k.path)))].join(", ")}, which sets the scene's own period`);
|
|
8667
|
+
if (tl.loop && (tl.duration ?? 0) > 0) add("timeline", tl.duration);
|
|
8668
|
+
else reasons.push("timeline does not loop (it plays once, then holds)");
|
|
8669
|
+
}
|
|
8670
|
+
const bg = spec.background;
|
|
8671
|
+
if (bg && bg.type !== "solid" && bg.drift) {
|
|
8672
|
+
const { period } = bg.drift;
|
|
8673
|
+
const field = bg.type === "field" && bg.drift.amount !== 0;
|
|
8674
|
+
const r = field ? toRatio(period) : null;
|
|
8675
|
+
add("background drift", r ? r[0] / gcd(r[0], FIELD_BUCKET_MS) * FIELD_BUCKET_MS : period);
|
|
8676
|
+
}
|
|
8677
|
+
if (spec.finish?.animate && ((spec.finish.grain ?? 0) > 0 || (spec.finish.dither ?? 0) > 0)) reasons.push("finish.animate steps the grain without repeating");
|
|
8678
|
+
spec.layers.forEach((layer, i) => {
|
|
8679
|
+
const at = `layers[${i}]${layer.key ? ` (${layer.key})` : ""}`;
|
|
8680
|
+
const rate = layer.clock?.rate ?? 1;
|
|
8681
|
+
const life = layer.life;
|
|
8682
|
+
if (life && (life.exit !== void 0 || life.enter !== void 0 && ((life.enter ?? 0) > 0 || (life.fade ?? 1e3) > 0))) reasons.push(`${at} life builds in or out once`);
|
|
8683
|
+
if (layer.pulse) add(`${at} pulse`, layer.pulse.period / rate);
|
|
8684
|
+
if (layer.grow) add(`${at} grow`, layer.grow.period / rate);
|
|
8685
|
+
if (layer.rock) add(`${at} rock`, layer.rock.period / rate);
|
|
8686
|
+
if (layer.emit) add(`${at} emit`, layer.emit.every);
|
|
8687
|
+
const spin = range(layer.spin);
|
|
8688
|
+
if (spin !== null) {
|
|
8689
|
+
if (Number.isNaN(spin)) reasons.push(`${at} spin range gives each entity its own speed`);
|
|
8690
|
+
else if (spin !== 0) add(`${at} spin`, 36e4 / Math.abs(spin));
|
|
8691
|
+
}
|
|
8692
|
+
const sp = layer.sprite;
|
|
8693
|
+
if ((sp.kind === "emoji" || sp.kind === "text") && sp.cycle && spriteVariants(sp) > 1) add(`${at} cycle`, sp.cycle.period * spriteVariants(sp) / rate);
|
|
8694
|
+
if (sp.kind === "textBlock" && sp.reveal) {
|
|
8695
|
+
if ((sp.reveal.speed ?? 0) > 0) reasons.push(`${at} reveal types itself once`);
|
|
8696
|
+
if (sp.reveal.caret) {
|
|
8697
|
+
const hz = Math.min(3, sp.reveal.caret === true ? 1.2 : sp.reveal.caret.blink ?? 1.2);
|
|
8698
|
+
if (hz > 0) add(`${at} caret`, 1e3 / hz);
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
const m = layer.motion;
|
|
8702
|
+
if (m.type === "path") add(`${at} path`, m.duration);
|
|
8703
|
+
else if (m.type === "orbit") {
|
|
8704
|
+
const s = range(m.speed);
|
|
8705
|
+
if (s === null || Number.isNaN(s)) reasons.push(`${at} orbit speed range gives each entity its own lap`);
|
|
8706
|
+
else if (s !== 0) add(`${at} orbit`, 36e4 / Math.abs(s));
|
|
8707
|
+
} else if (m.type !== "static") {
|
|
8708
|
+
if (m.type === "drift" && m.ease?.type === "settle" && !layer.emit) reasons.push(`${at} drift settles once`);
|
|
8709
|
+
else reasons.push(`${at} ${m.type} motion travels at seeded per-entity speeds`);
|
|
8710
|
+
}
|
|
8711
|
+
});
|
|
8712
|
+
const labelled = parts.map(([l, n, d]) => `${l}: ${Number((n / d).toFixed(3))} ms`);
|
|
8713
|
+
if (reasons.length > 0) return { period: null, parts: labelled, reasons };
|
|
8714
|
+
if (parts.length === 0) return { period: 0, parts: labelled, reasons };
|
|
8715
|
+
let num = 1;
|
|
8716
|
+
let den = 0;
|
|
8717
|
+
for (const [, n, d] of parts) {
|
|
8718
|
+
num = num / gcd(num, n) * n;
|
|
8719
|
+
den = gcd(den, d);
|
|
8720
|
+
if (num / den > MAX_SCENE_PERIOD) {
|
|
8721
|
+
return { period: null, parts: labelled, reasons: [`its parts share no cycle shorter than ${MAX_SCENE_PERIOD / 6e4} min`] };
|
|
8722
|
+
}
|
|
8723
|
+
}
|
|
8724
|
+
if (num % den !== 0) return { period: null, parts: labelled, reasons: [`its parts repeat together every ${Number((num / den).toFixed(3))} ms, not a whole number of milliseconds`] };
|
|
8725
|
+
return { period: num / den, parts: labelled, reasons };
|
|
8726
|
+
}
|
|
8727
|
+
function periodWithoutTimeline(spec) {
|
|
8728
|
+
return scenePeriod(withoutTimeline(spec));
|
|
8729
|
+
}
|
|
8730
|
+
|
|
7688
8731
|
// src/advise.ts
|
|
7689
8732
|
var LOW_CONTRAST_FLOOR = 0.05;
|
|
7690
8733
|
var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
7691
8734
|
var READ_LEGIBILITY_FLOOR = 4.5;
|
|
7692
8735
|
var READ_SAFE_AREA = 0.05;
|
|
7693
8736
|
var ADDITIVE_INK_CEILING = 1;
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
8737
|
+
var CALM_MAX_SPIN = 90;
|
|
8738
|
+
var NEAR_FLASH_MAX_BOUNDARIES = 10;
|
|
8739
|
+
function edgeAnchored(range2) {
|
|
8740
|
+
if (!range2) return false;
|
|
8741
|
+
const [a, b] = range2;
|
|
7697
8742
|
return (a <= 0.02 || b >= 0.98) && b - a <= 0.6;
|
|
7698
8743
|
}
|
|
7699
8744
|
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
7700
8745
|
const hidden = hiddenLayers(spec);
|
|
8746
|
+
const authored = spec;
|
|
7701
8747
|
spec = resolveTimelineAt(spec, opts.t ?? COHESION_T);
|
|
7702
8748
|
const warnings = [];
|
|
7703
8749
|
const w = viewport.width;
|
|
@@ -7724,7 +8770,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
7724
8770
|
if (isStaticText) textLayerCount++;
|
|
7725
8771
|
if (layer.motion.type !== "static") motionLayerCount++;
|
|
7726
8772
|
const coh = cohesion[li];
|
|
7727
|
-
if (seamsWorthWarning(layer, coh, entities.length)) {
|
|
8773
|
+
if (seamsWorthWarning(layer, coh, entities.length, () => buriedAreaOf(layer, entities, opts.t ?? COHESION_T, w, h))) {
|
|
7728
8774
|
const s = layer.sprite;
|
|
7729
8775
|
const softEdged = (s.kind === "circle" || s.kind === "polygon") && s.soft || s.kind === "rect" && !!s.feather;
|
|
7730
8776
|
warnings.push({
|
|
@@ -7772,13 +8818,17 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
7772
8818
|
return { rgb, luma: 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b };
|
|
7773
8819
|
});
|
|
7774
8820
|
const seps = entities.map((e) => {
|
|
7775
|
-
const
|
|
7776
|
-
if (
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
8821
|
+
const inks = spriteInks(layer, e);
|
|
8822
|
+
if (inks === null || inks.length === 0) return Infinity;
|
|
8823
|
+
return Math.max(...inks.map((hex) => {
|
|
8824
|
+
const ink = { rgb: hexRgb(hex), luma: hexLuma(hex) };
|
|
8825
|
+
const sep = Math.max(...plates.map((bg) => colourSeparation(layer, ink, bg)));
|
|
8826
|
+
if (sep >= LOW_CONTRAST_FLOOR || layer.blend === "lighter" || layer.blend === "screen") return sep;
|
|
8827
|
+
const at = positionAt(e, 0, w, h);
|
|
8828
|
+
const under = groundUnder(spec, allEntities, li, hidden, at, w, h);
|
|
8829
|
+
const framed = holeFrame(spec, allEntities, li, hidden, at, w, h);
|
|
8830
|
+
return Math.max(under ? colourSeparation(layer, ink, under) : sep, framed ? colourSeparation(layer, ink, framed) : sep);
|
|
8831
|
+
}));
|
|
7782
8832
|
});
|
|
7783
8833
|
const best = Math.max(...seps);
|
|
7784
8834
|
if (best < LOW_CONTRAST_FLOOR) {
|
|
@@ -7807,14 +8857,17 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
7807
8857
|
if (layer.sprite.kind === "circle") {
|
|
7808
8858
|
pixArea = Math.PI * r * r;
|
|
7809
8859
|
} else if (layer.sprite.kind === "polygon") {
|
|
7810
|
-
|
|
8860
|
+
if (isPathPolygon(layer.sprite)) {
|
|
8861
|
+
const fr = pathFrame(layer.sprite, r, w, h);
|
|
8862
|
+
pixArea = pathInkArea(layer.sprite, fr.sx, fr.sy, r);
|
|
8863
|
+
} else pixArea = polygonArea(polygonPoints(layer.sprite, r));
|
|
7811
8864
|
} else if (layer.sprite.kind === "bar") {
|
|
7812
8865
|
pixArea = e.size * barFraction(layer.sprite, e.barIndex ?? 0) * (e.size2 ?? e.size * 0.2);
|
|
7813
8866
|
} else if (layer.sprite.kind === "textBlock") {
|
|
7814
8867
|
const fsPx = layer.sprite.fontSize * scale;
|
|
7815
8868
|
const lh = (layer.sprite.lineHeight ?? 1.4) * fsPx;
|
|
7816
8869
|
const maxWPx = layer.sprite.maxWidth * scale;
|
|
7817
|
-
const lines = breakTextBlock(layer.sprite
|
|
8870
|
+
const lines = breakTextBlock(textBlockText(layer.sprite), maxWPx / fsPx, textMetricsClassFor(layer.sprite.font));
|
|
7818
8871
|
pixArea = maxWPx * lines.length * lh * 0.55 * (layer.sprite.opacity ?? 1);
|
|
7819
8872
|
} else if (layer.sprite.kind === "text") {
|
|
7820
8873
|
const b = textBoxAt(layer.sprite, e, positionAt(e, 0, w, h), spec, w, h);
|
|
@@ -7889,6 +8942,44 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
7889
8942
|
});
|
|
7890
8943
|
}
|
|
7891
8944
|
}
|
|
8945
|
+
if (spec.motionIntensity === "calm") {
|
|
8946
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
8947
|
+
if (hidden?.[li]) continue;
|
|
8948
|
+
const layer = spec.layers[li];
|
|
8949
|
+
if (!layer.spin || layer.sprite.kind === "circle" || layer.sprite.kind === "ring") continue;
|
|
8950
|
+
const entities = allEntities[li];
|
|
8951
|
+
if (entities.length === 0) continue;
|
|
8952
|
+
const rate = entities.reduce((s, e) => s + Math.abs(e.spinSpeed), 0) / entities.length * (180 / Math.PI);
|
|
8953
|
+
if (rate <= CALM_MAX_SPIN) continue;
|
|
8954
|
+
warnings.push({
|
|
8955
|
+
path: `layers[${li}].spin`,
|
|
8956
|
+
code: "motion-intensity-mismatch",
|
|
8957
|
+
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'`
|
|
8958
|
+
});
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
const flicker = nearLimitFlash(authored);
|
|
8962
|
+
if (flicker) {
|
|
8963
|
+
warnings.push({
|
|
8964
|
+
path: "timeline",
|
|
8965
|
+
code: "flash-near-limit",
|
|
8966
|
+
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`
|
|
8967
|
+
});
|
|
8968
|
+
}
|
|
8969
|
+
if (authored.timeline?.loop && (authored.timeline.duration ?? 0) > 0) {
|
|
8970
|
+
const lap = Math.round(authored.timeline.duration);
|
|
8971
|
+
const rest = periodWithoutTimeline(authored);
|
|
8972
|
+
if (rest.period !== null && rest.period > 0 && lap % rest.period !== 0) {
|
|
8973
|
+
let a = lap;
|
|
8974
|
+
let b = rest.period;
|
|
8975
|
+
while (b) [a, b] = [b, a % b];
|
|
8976
|
+
warnings.push({
|
|
8977
|
+
path: "timeline.duration",
|
|
8978
|
+
code: "loop-seam",
|
|
8979
|
+
message: `the timeline loops every ${lap} ms but the rest of the scene repeats every ${rest.period} ms (${rest.parts.join(", ")}), so the whole scene only repeats every ${lap / a * rest.period} ms \u2014 make the lap a multiple of ${rest.period} ms for one lap to loop`
|
|
8980
|
+
});
|
|
8981
|
+
}
|
|
8982
|
+
}
|
|
7892
8983
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
7893
8984
|
if (hidden?.[li]) continue;
|
|
7894
8985
|
const layer = spec.layers[li];
|
|
@@ -7956,12 +9047,16 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
7956
9047
|
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
7957
9048
|
for (const e of allEntities[li]) {
|
|
7958
9049
|
const p = positionAt(e, 0, w, h);
|
|
7959
|
-
const
|
|
9050
|
+
const unturned = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
9051
|
+
const rot = rotationAt(e, 0);
|
|
9052
|
+
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) });
|
|
7960
9053
|
const map = paintMap(spec, layer, w, h, scale);
|
|
7961
|
-
const
|
|
9054
|
+
const both = rot && map ? (q) => map(turn(q)) : rot ? turn : map;
|
|
9055
|
+
const box = both ? { ...unturned, ...mapBox(both, unturned) } : unturned;
|
|
9056
|
+
const raw = rot ? { ...unturned, ...mapBox(turn, unturned) } : unturned;
|
|
7962
9057
|
const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
7963
9058
|
const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
|
|
7964
|
-
const str = s.kind === "textBlock" ? s
|
|
9059
|
+
const str = s.kind === "textBlock" ? textBlockText(s) : s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
7965
9060
|
textBoxes.push({ li, label, alpha, str, ...box, rawW: raw.x1 - raw.x0, rawH: raw.y1 - raw.y0 });
|
|
7966
9061
|
}
|
|
7967
9062
|
}
|
|
@@ -8080,20 +9175,34 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
8080
9175
|
for (const box of boxes) {
|
|
8081
9176
|
const cy = (box.y0 + box.y1) / 2;
|
|
8082
9177
|
const cx = (box.x0 + box.x1) / 2;
|
|
8083
|
-
const
|
|
8084
|
-
const
|
|
8085
|
-
const
|
|
8086
|
-
const
|
|
8087
|
-
|
|
8088
|
-
|
|
9178
|
+
const bgAt = (x, y) => backgroundRgbAt(spec, y, h, scale, x, w, opts.backgroundSeed ?? opts.seed, opts.t ?? COHESION_T);
|
|
9179
|
+
const ix = (box.x1 - box.x0) * 0.02;
|
|
9180
|
+
const iy = (box.y1 - box.y0) * 0.02;
|
|
9181
|
+
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]];
|
|
9182
|
+
const grounds = [];
|
|
9183
|
+
let bare = false;
|
|
9184
|
+
for (const [x, y] of probes) {
|
|
9185
|
+
const under = groundUnder(spec, allEntities, box.li, hidden, { x, y }, w, h, bgAt(x, y));
|
|
9186
|
+
if (under) grounds.push({ rgb: under.rgb, onSolid: true });
|
|
9187
|
+
else bare = true;
|
|
9188
|
+
}
|
|
9189
|
+
if (bare) grounds.push({ rgb: bgAt(cx, cy), onSolid: false });
|
|
9190
|
+
for (const g of grounds) {
|
|
9191
|
+
const ground = legibilityRatio(textPlate(g.rgb, box.alpha), g.rgb);
|
|
9192
|
+
const lit = brightestAdditivePlate(spec, allEntities, g.rgb, box, w, h);
|
|
9193
|
+
const plate = lit ? legibilityRatio(textPlate(lit.rgb, box.alpha), lit.rgb) : Infinity;
|
|
9194
|
+
if (!worst || Math.min(ground, plate) < Math.min(worst.ground, worst.plate)) {
|
|
9195
|
+
worst = { ground, plate, plateLabel: lit?.label ?? null, onSolid: g.onSolid };
|
|
9196
|
+
}
|
|
8089
9197
|
}
|
|
8090
9198
|
}
|
|
8091
9199
|
if (worst && Math.min(worst.ground, worst.plate) < READ_LEGIBILITY_FLOOR) {
|
|
8092
9200
|
const under = worst.plateLabel ? ` and ${worst.plate.toFixed(1)}:1 under ${worst.plateLabel} at its brightest` : "";
|
|
9201
|
+
const against = worst.onSolid ? "against the solid layer painted under it" : "against the background at the box centre";
|
|
8093
9202
|
warnings.push({
|
|
8094
9203
|
path: `layers[${b.li}].sprite`,
|
|
8095
9204
|
code: "text-legibility",
|
|
8096
|
-
message: `${b.label} is declared role: 'read' but its colour reads at ${worst.ground.toFixed(1)}:1 against
|
|
9205
|
+
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`
|
|
8097
9206
|
});
|
|
8098
9207
|
}
|
|
8099
9208
|
const edges = [];
|
|
@@ -8116,6 +9225,21 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
8116
9225
|
}
|
|
8117
9226
|
return warnings;
|
|
8118
9227
|
}
|
|
9228
|
+
function nearLimitFlash(spec) {
|
|
9229
|
+
if (!spec.timeline) return null;
|
|
9230
|
+
const { times, lap } = lumaKeyTimes(spec);
|
|
9231
|
+
let best = null;
|
|
9232
|
+
for (let i = 0, e = 0; i < times.length; i++) {
|
|
9233
|
+
while (e < times.length && times[e] < times[i] + 1e3) e++;
|
|
9234
|
+
if (e - i >= 4 && (!best || e - i > best.n)) best = { from: i, n: e - i };
|
|
9235
|
+
}
|
|
9236
|
+
if (!best) return null;
|
|
9237
|
+
const window = times.slice(best.from, best.from + Math.min(best.n, NEAR_FLASH_MAX_BOUNDARIES));
|
|
9238
|
+
const samples = flashSampleTimes(window).map((t) => ({ t, lum: luminanceGrid(spec, { t, cols: 16, rows: 9 }).cells.flat().map((v) => srgbToLinear3(v * 255)) }));
|
|
9239
|
+
const r = analyzeFlashes2(samples, { maxFlashesPerSecond: 1 }).general;
|
|
9240
|
+
if (!r.fails) return null;
|
|
9241
|
+
return { flashesPerSecond: r.worstTileFlashesPerSecond, area: r.flashingAreaFraction, at: lap ? window[0] % lap : window[0] };
|
|
9242
|
+
}
|
|
8119
9243
|
function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
8120
9244
|
let best = null;
|
|
8121
9245
|
for (let lj = 0; lj < spec.layers.length; lj++) {
|
|
@@ -8143,19 +9267,49 @@ function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
|
8143
9267
|
}
|
|
8144
9268
|
return best ? { rgb: best.rgb, label: best.label } : null;
|
|
8145
9269
|
}
|
|
8146
|
-
function
|
|
8147
|
-
let
|
|
9270
|
+
function holeFrame(spec, allEntities, li, hidden, pt, w, h) {
|
|
9271
|
+
for (let lj = li + 1; lj < spec.layers.length; lj++) {
|
|
9272
|
+
if (hidden?.[lj]) continue;
|
|
9273
|
+
const layer = spec.layers[lj];
|
|
9274
|
+
const sp = layer.sprite;
|
|
9275
|
+
if (sp.kind !== "polygon" || !isPathPolygon(sp) || sp.soft) continue;
|
|
9276
|
+
if (layer.motion.type !== "static" || layer.blend !== void 0 || layer.transform !== void 0 || groupOf(spec, layer)?.transform !== void 0) continue;
|
|
9277
|
+
if (flattenSprite(sp).subpaths < 2) continue;
|
|
9278
|
+
const a = layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer);
|
|
9279
|
+
for (const e of allEntities[lj]) {
|
|
9280
|
+
if (e.spinSpeed !== 0 || e.rotate || e.alpha * a < 0.9) continue;
|
|
9281
|
+
const c = positionAt(e, 0, w, h);
|
|
9282
|
+
const fr = pathFrame(sp, e.size / 2, w, h);
|
|
9283
|
+
const pts = flattenSprite(sp).points.map(([u, v]) => ({ x: (u - fr.ox) * fr.sx, y: (v - fr.oy) * fr.sy }));
|
|
9284
|
+
if (pts.length < 3) continue;
|
|
9285
|
+
const dx = pt.x - c.x;
|
|
9286
|
+
const dy = pt.y - c.y;
|
|
9287
|
+
if (dx < Math.min(...pts.map((q) => q.x)) || dx > Math.max(...pts.map((q) => q.x))) continue;
|
|
9288
|
+
if (dy < Math.min(...pts.map((q) => q.y)) || dy > Math.max(...pts.map((q) => q.y))) continue;
|
|
9289
|
+
if (pointInPolygon(dx, dy, pts)) continue;
|
|
9290
|
+
const ink = spriteInks(layer, e)?.[0];
|
|
9291
|
+
if (ink === void 0) continue;
|
|
9292
|
+
const rgb = hexRgb(ink);
|
|
9293
|
+
return { rgb, luma: 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b };
|
|
9294
|
+
}
|
|
9295
|
+
}
|
|
9296
|
+
return null;
|
|
9297
|
+
}
|
|
9298
|
+
function groundUnder(spec, allEntities, li, hidden, pt, w, h, base = backgroundRgb(spec)) {
|
|
9299
|
+
let plate = base;
|
|
8148
9300
|
let covered = false;
|
|
8149
9301
|
for (let lj = 0; lj < li; lj++) {
|
|
8150
9302
|
if (hidden?.[lj]) continue;
|
|
8151
9303
|
const layer = spec.layers[lj];
|
|
8152
9304
|
const sp = layer.sprite;
|
|
8153
9305
|
if (layer.motion.type !== "static" || layer.blend !== void 0 || layer.transform !== void 0 || groupOf(spec, layer)?.transform !== void 0) continue;
|
|
9306
|
+
if (layer.life !== void 0 || layer.emit !== void 0) continue;
|
|
8154
9307
|
if ((sp.kind === "circle" || sp.kind === "polygon") && sp.soft) continue;
|
|
8155
9308
|
if (sp.kind !== "circle" && sp.kind !== "rect" && sp.kind !== "polygon") continue;
|
|
9309
|
+
if (sp.kind === "polygon" && isPathPolygon(sp)) continue;
|
|
8156
9310
|
const a = layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer);
|
|
8157
9311
|
for (const e of allEntities[lj]) {
|
|
8158
|
-
if (e.spinSpeed !== 0 || e.alpha * a <= 0) continue;
|
|
9312
|
+
if (e.spinSpeed !== 0 || e.pulseAmp > 0 || e.growAmp > 0 || e.alpha * a <= 0) continue;
|
|
8159
9313
|
const hex = spriteHex(layer, e);
|
|
8160
9314
|
if (hex === null) continue;
|
|
8161
9315
|
const c = positionAt(e, 0, w, h);
|
|
@@ -8208,7 +9362,7 @@ function isOutlineStack(a, b) {
|
|
|
8208
9362
|
if (fs <= 0 || Math.abs(a.fs - b.fs) > 0.1 * Math.max(a.fs, b.fs)) return false;
|
|
8209
9363
|
const dx = (a.x0 + a.x1) / 2 - (b.x0 + b.x1) / 2;
|
|
8210
9364
|
const dy = (a.y0 + a.y1) / 2 - (b.y0 + b.y1) / 2;
|
|
8211
|
-
return Math.
|
|
9365
|
+
return Math.max(Math.abs(dx), Math.abs(dy)) <= OUTLINE_STACK_OFFSET * fs;
|
|
8212
9366
|
}
|
|
8213
9367
|
function textBoxAt(s, e, p, spec, w, h) {
|
|
8214
9368
|
const m = s.font ? /(\d{1,5}(?:\.\d{1,4})?|\.\d{1,4})px/.exec(s.font) : null;
|
|
@@ -8228,7 +9382,7 @@ function textBlockBoxAt(s, p, w, h) {
|
|
|
8228
9382
|
const fsPx = s.fontSize * unit;
|
|
8229
9383
|
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
8230
9384
|
const maxWPx = s.maxWidth * unit;
|
|
8231
|
-
const lines = breakTextBlock(s
|
|
9385
|
+
const lines = breakTextBlock(textBlockText(s), maxWPx / fsPx, textMetricsClassFor(s.font));
|
|
8232
9386
|
const maxLineW = lines.reduce((mx, l) => Math.max(mx, l.widthEm), 0) * fsPx;
|
|
8233
9387
|
const totalH = lines.length * lh;
|
|
8234
9388
|
const align = s.align ?? "left";
|
|
@@ -8269,13 +9423,15 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
8269
9423
|
}
|
|
8270
9424
|
}
|
|
8271
9425
|
const fadeBound = seq.loop ? seq.segments.length : seq.segments.length - 1;
|
|
8272
|
-
const
|
|
9426
|
+
const live = (s) => s.transition?.type === "fade" || s.transition?.type === "wipe";
|
|
9427
|
+
const fades = seq.segments.slice(0, fadeBound).filter(live).length;
|
|
8273
9428
|
if (fades > 0) {
|
|
8274
|
-
const first = seq.segments.slice(0, fadeBound).findIndex(
|
|
9429
|
+
const first = seq.segments.slice(0, fadeBound).findIndex(live);
|
|
9430
|
+
const wipes = seq.segments.slice(0, fadeBound).some((s) => s.transition?.type === "wipe");
|
|
8275
9431
|
warnings.push({
|
|
8276
9432
|
path: `segments[${first}].transition`,
|
|
8277
9433
|
code: "fade-degrades-on-low-tier",
|
|
8278
|
-
message: `informational: ${fades} fade transition${fades === 1 ? "" : "s"} declared \u2014 viewers on the 'basic' or 'minimal' capability tier render fade as cut (two live segments for dur is over that budget); the show still plays`
|
|
9434
|
+
message: wipes ? `informational: ${fades} fade/wipe transition${fades === 1 ? "" : "s"} declared \u2014 viewers on the 'basic' or 'minimal' capability tier render them as cuts (two live segments for dur is over that budget); the show still plays` : `informational: ${fades} fade transition${fades === 1 ? "" : "s"} declared \u2014 viewers on the 'basic' or 'minimal' capability tier render fade as cut (two live segments for dur is over that budget); the show still plays`
|
|
8279
9435
|
});
|
|
8280
9436
|
}
|
|
8281
9437
|
for (let i = 0; i < seq.segments.length - 1; i++) {
|
|
@@ -8283,7 +9439,7 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
8283
9439
|
const lumaA = backgroundLuma(resolveTimelineAt(seq.segments[i].scene, seq.segments[i].duration ?? 0));
|
|
8284
9440
|
const lumaB = backgroundLuma(resolveTimelineAt(seq.segments[i + 1].scene, 0));
|
|
8285
9441
|
const delta = Math.abs(lumaA - lumaB);
|
|
8286
|
-
if (delta > 0.5 && tr?.type !== "fade") {
|
|
9442
|
+
if (delta > 0.5 && tr?.type !== "fade" && tr?.type !== "wipe") {
|
|
8287
9443
|
warnings.push({
|
|
8288
9444
|
path: `segments[${i}]`,
|
|
8289
9445
|
code: "boundary-luminance-jump",
|
|
@@ -10780,6 +11936,74 @@ var TYPED_CUE_SPEC = {
|
|
|
10780
11936
|
]
|
|
10781
11937
|
};
|
|
10782
11938
|
|
|
11939
|
+
// src/examples/ink-tide.ts
|
|
11940
|
+
var INK_TIDE_SPEC = {
|
|
11941
|
+
schemaVersion: 1,
|
|
11942
|
+
id: "ink-tide",
|
|
11943
|
+
label: "Ink Tide",
|
|
11944
|
+
seed: 31,
|
|
11945
|
+
motionIntensity: "calm",
|
|
11946
|
+
background: { type: "gradient", stops: [{ at: 0, color: "#f1e9d8" }, { at: 1, color: "#e2d6bd" }] },
|
|
11947
|
+
layers: [
|
|
11948
|
+
{
|
|
11949
|
+
key: "ink",
|
|
11950
|
+
count: 1,
|
|
11951
|
+
position: { x: 0.5, y: 0.48 },
|
|
11952
|
+
motion: { type: "static" },
|
|
11953
|
+
sprite: {
|
|
11954
|
+
kind: "polygon",
|
|
11955
|
+
radius: [0.4, 0.4],
|
|
11956
|
+
color: "#2b2622",
|
|
11957
|
+
fill: false,
|
|
11958
|
+
// One pen run: the ridge, the shoreline, then the moon.
|
|
11959
|
+
path: "M -1 0.32 L -0.64 -0.12 L -0.44 0.06 L -0.08 -0.46 L 0.2 -0.1 L 0.4 -0.26 L 1 0.32 M -0.96 0.5 C -0.5 0.4 -0.2 0.58 0.1 0.5 C 0.4 0.42 0.7 0.58 0.96 0.5 M 0.62 -0.66 A 0.12 0.12 0 1 1 0.619 -0.66",
|
|
11960
|
+
stroke: { color: "#2b2622", width: 0.01, cap: "round", join: "round", draw: { start: 0, end: 1 } }
|
|
11961
|
+
}
|
|
11962
|
+
},
|
|
11963
|
+
{
|
|
11964
|
+
key: "boat",
|
|
11965
|
+
count: 1,
|
|
11966
|
+
position: { x: 0.5, y: 0.5, dx: 0.12, dy: 0.17 },
|
|
11967
|
+
motion: { type: "static" },
|
|
11968
|
+
sprite: { kind: "polygon", radius: [0.035, 0.035], color: "#2b2622", points: [[-1, -0.15], [1, -0.15], [0.62, 0.3], [-0.62, 0.3]] },
|
|
11969
|
+
rock: { amp: 6, period: 6e3 },
|
|
11970
|
+
clock: { phase: 0 }
|
|
11971
|
+
},
|
|
11972
|
+
{
|
|
11973
|
+
key: "caption",
|
|
11974
|
+
count: 1,
|
|
11975
|
+
position: { x: 0.5, y: 0.86 },
|
|
11976
|
+
motion: { type: "static" },
|
|
11977
|
+
sprite: {
|
|
11978
|
+
kind: "textBlock",
|
|
11979
|
+
text: "the tide goes out and leaves the sky behind",
|
|
11980
|
+
maxWidth: 1.2,
|
|
11981
|
+
fontSize: 0.032,
|
|
11982
|
+
anchor: "center",
|
|
11983
|
+
align: "center",
|
|
11984
|
+
color: "#5a4f45",
|
|
11985
|
+
font: "italic Georgia, 'Times New Roman', serif",
|
|
11986
|
+
opacity: 1,
|
|
11987
|
+
reveal: { progress: 1, mode: "glyphFade", fade: 0.3, rise: 0.4 }
|
|
11988
|
+
}
|
|
11989
|
+
}
|
|
11990
|
+
],
|
|
11991
|
+
timeline: {
|
|
11992
|
+
loop: true,
|
|
11993
|
+
duration: 3e4,
|
|
11994
|
+
keys: [
|
|
11995
|
+
{ t: 0, path: "ink.sprite.stroke.draw.end", value: 1, dur: 12e3, ease: "out" },
|
|
11996
|
+
{ t: 9e3, path: "caption.sprite.reveal.progress", value: 1, dur: 4e3, ease: "out" },
|
|
11997
|
+
{ t: 2e4, path: "ink.sprite.stroke.draw.start", value: 1, dur: 7e3, ease: "in" },
|
|
11998
|
+
{ t: 21e3, path: "caption.sprite.opacity", value: 0, dur: 3e3 },
|
|
11999
|
+
{ t: 29e3, path: "ink.sprite.stroke.draw.end", value: 0, dur: 0 },
|
|
12000
|
+
{ t: 29e3, path: "ink.sprite.stroke.draw.start", value: 0, dur: 0 },
|
|
12001
|
+
{ t: 29200, path: "caption.sprite.reveal.progress", value: 0, dur: 0 },
|
|
12002
|
+
{ t: 29400, path: "caption.sprite.opacity", value: 1, dur: 0 }
|
|
12003
|
+
]
|
|
12004
|
+
}
|
|
12005
|
+
};
|
|
12006
|
+
|
|
10783
12007
|
// src/examples/index.ts
|
|
10784
12008
|
var SCHEMA_EXAMPLES = [
|
|
10785
12009
|
{ id: "aquarium", label: "Aquarium", spec: AQUARIUM_SPEC },
|
|
@@ -10809,7 +12033,8 @@ var SCHEMA_EXAMPLES = [
|
|
|
10809
12033
|
{ id: "phase-duet", label: "Phase Duet", spec: PHASE_DUET_SPEC },
|
|
10810
12034
|
{ id: "web-work", label: "Web Work", spec: WEB_WORK_SPEC },
|
|
10811
12035
|
{ id: "shard-fall", label: "Shard Fall", spec: SHARD_FALL_SPEC },
|
|
10812
|
-
{ id: "murmur", label: "Murmur", spec: MURMUR_SPEC }
|
|
12036
|
+
{ id: "murmur", label: "Murmur", spec: MURMUR_SPEC },
|
|
12037
|
+
{ id: "ink-tide", label: "Ink Tide", spec: INK_TIDE_SPEC }
|
|
10813
12038
|
];
|
|
10814
12039
|
var EXAMPLE_SEQUENCES = [
|
|
10815
12040
|
{ id: "three-movements", label: "Three Movements", sequence: THREE_MOVEMENTS_SEQUENCE }
|
|
@@ -10845,6 +12070,8 @@ export {
|
|
|
10845
12070
|
LOBBY_TALK_SPEC,
|
|
10846
12071
|
MATRIX_RAIN_SPEC,
|
|
10847
12072
|
MAX_PATH_POINTS,
|
|
12073
|
+
MAX_SCENE_PERIOD,
|
|
12074
|
+
MOTION_FIELDS,
|
|
10848
12075
|
MURMUR_SPEC,
|
|
10849
12076
|
NOSTALGHIA_CANDLE_SPEC,
|
|
10850
12077
|
ORRERY_SPEC,
|
|
@@ -10861,6 +12088,7 @@ export {
|
|
|
10861
12088
|
SIGNAL_BOARD_SPEC,
|
|
10862
12089
|
SNOWFALL_SPEC,
|
|
10863
12090
|
SPARSE_NIGHT_SPEC,
|
|
12091
|
+
STEER_EASES,
|
|
10864
12092
|
THERMAL_FIELD_SPEC,
|
|
10865
12093
|
THREE_MOVEMENTS_SEQUENCE,
|
|
10866
12094
|
TYPED_CUE_SPEC,
|
|
@@ -10871,6 +12099,7 @@ export {
|
|
|
10871
12099
|
adviseSequence,
|
|
10872
12100
|
adviseSpec,
|
|
10873
12101
|
alphaAt,
|
|
12102
|
+
applyDeltasShared,
|
|
10874
12103
|
applyDeltasToSpec,
|
|
10875
12104
|
assertValidSequence,
|
|
10876
12105
|
assertValidSpec,
|
|
@@ -10879,6 +12108,7 @@ export {
|
|
|
10879
12108
|
bedRenderSeed,
|
|
10880
12109
|
breakTextBlock,
|
|
10881
12110
|
buildEntities,
|
|
12111
|
+
buriedAreaOf,
|
|
10882
12112
|
canMorph,
|
|
10883
12113
|
canWrapMorph,
|
|
10884
12114
|
canonicalSpecPath,
|
|
@@ -10890,6 +12120,8 @@ export {
|
|
|
10890
12120
|
describeScene,
|
|
10891
12121
|
diffScenes,
|
|
10892
12122
|
dominanceRanking,
|
|
12123
|
+
drawSpan,
|
|
12124
|
+
easeGlide,
|
|
10893
12125
|
easeSmooth,
|
|
10894
12126
|
emitEnvelope,
|
|
10895
12127
|
emitWindow,
|
|
@@ -10908,13 +12140,17 @@ export {
|
|
|
10908
12140
|
headingAt,
|
|
10909
12141
|
hexToRgb255,
|
|
10910
12142
|
ignoredPropertyPaths,
|
|
12143
|
+
isCombiningKana,
|
|
12144
|
+
isFullWidth,
|
|
10911
12145
|
isPathPolygon,
|
|
10912
12146
|
latticeHash,
|
|
12147
|
+
layerCellFootprints,
|
|
10913
12148
|
layerCohesion,
|
|
10914
12149
|
lerpPathD,
|
|
10915
12150
|
lerpSpec,
|
|
10916
12151
|
lerpValue,
|
|
10917
12152
|
lifeAlphaAt,
|
|
12153
|
+
lifeStaggerAt,
|
|
10918
12154
|
linkEdges,
|
|
10919
12155
|
linkMaxDistPx,
|
|
10920
12156
|
linkPairs,
|
|
@@ -10929,6 +12165,7 @@ export {
|
|
|
10929
12165
|
pathParts,
|
|
10930
12166
|
perceiveScene,
|
|
10931
12167
|
perceiveSequenceFrame,
|
|
12168
|
+
periodWithoutTimeline,
|
|
10932
12169
|
pickScreenOp,
|
|
10933
12170
|
positionAt,
|
|
10934
12171
|
presentWithFinish,
|
|
@@ -10942,6 +12179,7 @@ export {
|
|
|
10942
12179
|
rgb255Luma,
|
|
10943
12180
|
rotationAt,
|
|
10944
12181
|
sceneColour,
|
|
12182
|
+
scenePeriod,
|
|
10945
12183
|
seamCauseOf,
|
|
10946
12184
|
seamsWorthWarning,
|
|
10947
12185
|
segmentRenderSeed,
|
|
@@ -10953,20 +12191,27 @@ export {
|
|
|
10953
12191
|
spriteIndexAt,
|
|
10954
12192
|
spriteVariants,
|
|
10955
12193
|
steerablePaths,
|
|
12194
|
+
strokeDrawnSamples,
|
|
12195
|
+
strokeLines,
|
|
10956
12196
|
structuralSignature,
|
|
10957
12197
|
textBlockAnchorOffset,
|
|
12198
|
+
textBlockText,
|
|
10958
12199
|
textMetricsClassFor,
|
|
10959
12200
|
textSprites,
|
|
10960
12201
|
textStringsDiffer,
|
|
10961
12202
|
textStringsOf,
|
|
10962
12203
|
textWidthEm,
|
|
12204
|
+
timelineFlashReport,
|
|
10963
12205
|
timelineLocalTime,
|
|
10964
12206
|
timelineSampleTimes,
|
|
10965
12207
|
timelineSampleTimesAfter,
|
|
10966
12208
|
timelineTracks,
|
|
12209
|
+
trimLines,
|
|
12210
|
+
trimPolyline,
|
|
10967
12211
|
validateSequence,
|
|
10968
12212
|
validateSpec,
|
|
10969
12213
|
validateSpecPaths,
|
|
12214
|
+
wipeMask,
|
|
10970
12215
|
withoutTimeline
|
|
10971
12216
|
};
|
|
10972
12217
|
//# sourceMappingURL=index.js.map
|