@idle-screens/schema 3.10.0 → 3.11.1
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 +38 -4
- package/dist/index.d.ts +28 -1
- package/dist/index.js +302 -94
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +33 -1
package/dist/index.js
CHANGED
|
@@ -85,6 +85,7 @@ var LIMITS = {
|
|
|
85
85
|
maxLayerTransformScale: 8,
|
|
86
86
|
maxLayerTransformRotate: 3600,
|
|
87
87
|
// degrees
|
|
88
|
+
maxGroups: 16,
|
|
88
89
|
maxEmitEvery: 6e5,
|
|
89
90
|
// ms — ten minutes; longer silences than that are `life.enter`
|
|
90
91
|
minEmitLife: 500,
|
|
@@ -110,7 +111,7 @@ var LIMITS = {
|
|
|
110
111
|
|
|
111
112
|
// src/steer.ts
|
|
112
113
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
113
|
-
var STEERABLE_ROOT_KEYS = /* @__PURE__ */ new Set(["ghosting", "referenceViewport", "finish"]);
|
|
114
|
+
var STEERABLE_ROOT_KEYS = /* @__PURE__ */ new Set(["ghosting", "referenceViewport", "finish", "groups"]);
|
|
114
115
|
function resolveSpecPath(spec, path) {
|
|
115
116
|
if (!spec || typeof spec !== "object" || !path) return null;
|
|
116
117
|
const parts = path.split(".");
|
|
@@ -124,6 +125,8 @@ function resolveSpecPath(spec, path) {
|
|
|
124
125
|
return null;
|
|
125
126
|
}
|
|
126
127
|
}
|
|
128
|
+
if (parts[0] === "layers" && parts.length === 3 && parts[2] === "group") return null;
|
|
129
|
+
if (parts.length >= 2 && parts[parts.length - 1] === "origin" && parts[parts.length - 2] === "transform") return null;
|
|
127
130
|
let node = spec;
|
|
128
131
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
129
132
|
if (node === null || typeof node !== "object") return null;
|
|
@@ -176,6 +179,10 @@ function alignPaintFields(a, b) {
|
|
|
176
179
|
if (!x || !y || typeof x !== "object" || typeof y !== "object") continue;
|
|
177
180
|
let nx = x;
|
|
178
181
|
let ny = y;
|
|
182
|
+
if (x.group === void 0 !== (y.group === void 0)) {
|
|
183
|
+
if (x.group === void 0) nx = { ...nx, group: y.group };
|
|
184
|
+
else ny = { ...ny, group: x.group };
|
|
185
|
+
}
|
|
179
186
|
if (x.opacity === void 0 !== (y.opacity === void 0)) {
|
|
180
187
|
if (x.opacity === void 0) nx = { ...nx, opacity: 1 };
|
|
181
188
|
else ny = { ...ny, opacity: 1 };
|
|
@@ -206,7 +213,39 @@ function alignPaintFields(a, b) {
|
|
|
206
213
|
if (nx !== x) (outA ??= la.slice())[i] = nx;
|
|
207
214
|
if (ny !== y) (outB ??= lb.slice())[i] = ny;
|
|
208
215
|
}
|
|
209
|
-
|
|
216
|
+
let ra = outA ? { ...a, layers: outA } : a;
|
|
217
|
+
let rb = outB ? { ...b, layers: outB } : b;
|
|
218
|
+
const ga = a.groups;
|
|
219
|
+
const gb = b.groups;
|
|
220
|
+
if (ga || gb) {
|
|
221
|
+
const na = { ...ga ?? {} };
|
|
222
|
+
const nb = { ...gb ?? {} };
|
|
223
|
+
let changed = !ga || !gb;
|
|
224
|
+
for (const name of /* @__PURE__ */ new Set([...Object.keys(na), ...Object.keys(nb)])) {
|
|
225
|
+
const x = na[name] ?? {};
|
|
226
|
+
const y = nb[name] ?? {};
|
|
227
|
+
if (!na[name] || !nb[name]) changed = true;
|
|
228
|
+
let nx = x;
|
|
229
|
+
let ny = y;
|
|
230
|
+
if (x.opacity === void 0 !== (y.opacity === void 0)) {
|
|
231
|
+
if (x.opacity === void 0) nx = { ...nx, opacity: 1 };
|
|
232
|
+
else ny = { ...ny, opacity: 1 };
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
if (!x.transform !== !y.transform) {
|
|
236
|
+
if (!x.transform) nx = { ...nx, transform: {} };
|
|
237
|
+
else ny = { ...ny, transform: {} };
|
|
238
|
+
changed = true;
|
|
239
|
+
}
|
|
240
|
+
na[name] = nx;
|
|
241
|
+
nb[name] = ny;
|
|
242
|
+
}
|
|
243
|
+
if (changed) {
|
|
244
|
+
ra = { ...ra, groups: na };
|
|
245
|
+
rb = { ...rb, groups: nb };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return [ra, rb];
|
|
210
249
|
}
|
|
211
250
|
function applyDeltasToSpec(spec, deltas) {
|
|
212
251
|
const copy = JSON.parse(JSON.stringify(spec));
|
|
@@ -315,7 +354,8 @@ function morphNothingMorphable(a, b, opts = {}) {
|
|
|
315
354
|
}
|
|
316
355
|
function steerablePaths(spec) {
|
|
317
356
|
if (!spec || typeof spec !== "object") return [];
|
|
318
|
-
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer"]);
|
|
357
|
+
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer", "group", "origin"]);
|
|
358
|
+
const SKIP_ROOT = /* @__PURE__ */ new Set(["timeline"]);
|
|
319
359
|
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops", "bands"]);
|
|
320
360
|
const layers = spec.layers;
|
|
321
361
|
const shadowedRootKeys = new Set(
|
|
@@ -333,8 +373,8 @@ function steerablePaths(spec) {
|
|
|
333
373
|
}
|
|
334
374
|
if (node && typeof node === "object") {
|
|
335
375
|
for (const [k, v] of Object.entries(node)) {
|
|
336
|
-
if (SKIP.has(k)) continue;
|
|
337
|
-
if (!prefix && shadowedRootKeys.has(k)) continue;
|
|
376
|
+
if (SKIP.has(k) && prefix !== "groups") continue;
|
|
377
|
+
if (!prefix && (shadowedRootKeys.has(k) || SKIP_ROOT.has(k))) continue;
|
|
338
378
|
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
339
379
|
}
|
|
340
380
|
return;
|
|
@@ -634,7 +674,7 @@ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
634
674
|
var isStr = (v) => typeof v === "string";
|
|
635
675
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
636
676
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
637
|
-
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting", "finish", "timeline"]);
|
|
677
|
+
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting", "finish", "timeline", "groups"]);
|
|
638
678
|
var KNOWN_FINISH = /* @__PURE__ */ new Set(["grain", "dither", "animate"]);
|
|
639
679
|
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
640
680
|
"count",
|
|
@@ -659,9 +699,12 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
659
699
|
"clock",
|
|
660
700
|
"rotate",
|
|
661
701
|
"opacity",
|
|
662
|
-
"transform"
|
|
702
|
+
"transform",
|
|
703
|
+
"group"
|
|
663
704
|
]);
|
|
664
|
-
var KNOWN_TRANSFORM = /* @__PURE__ */ new Set(["x", "y", "scale", "scaleX", "rotate"]);
|
|
705
|
+
var KNOWN_TRANSFORM = /* @__PURE__ */ new Set(["x", "y", "scale", "scaleX", "rotate", "origin"]);
|
|
706
|
+
var KNOWN_GROUP = /* @__PURE__ */ new Set(["transform", "opacity"]);
|
|
707
|
+
var GROUP_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
665
708
|
var KNOWN_POSITION = /* @__PURE__ */ new Set(["x", "y", "dx", "dy"]);
|
|
666
709
|
var KNOWN_TIMELINE = /* @__PURE__ */ new Set(["loop", "duration", "keys"]);
|
|
667
710
|
var KNOWN_TIMELINE_KEY = /* @__PURE__ */ new Set(["t", "path", "value", "ease", "dur"]);
|
|
@@ -733,12 +776,15 @@ function validateSpecPaths(spec, paths) {
|
|
|
733
776
|
const layers = /* @__PURE__ */ new Set();
|
|
734
777
|
let background = false;
|
|
735
778
|
let finish = false;
|
|
779
|
+
let groups = false;
|
|
736
780
|
for (const p of paths) {
|
|
737
781
|
if (/^layers\.\d+(\.(key|count|motion)(\.|$)|$)/.test(p)) return validateSpec(spec).valid;
|
|
782
|
+
if (p === "groups") return validateSpec(spec).valid;
|
|
738
783
|
const [head, idx] = p.split(".");
|
|
739
784
|
if (head === "layers" && idx !== void 0 && /^\d+$/.test(idx)) layers.add(Number(idx));
|
|
740
785
|
else if (head === "background") background = true;
|
|
741
786
|
else if (head === "finish") finish = true;
|
|
787
|
+
else if (head === "groups") groups = true;
|
|
742
788
|
else return validateSpec(spec).valid;
|
|
743
789
|
}
|
|
744
790
|
let ok = true;
|
|
@@ -755,6 +801,7 @@ function validateSpecPaths(spec, paths) {
|
|
|
755
801
|
}
|
|
756
802
|
if (background && spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
757
803
|
if (finish && spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
|
|
804
|
+
if (groups && spec.groups !== void 0) validateGroups(spec.groups, err, warn, spec);
|
|
758
805
|
if (layers.size > 0) {
|
|
759
806
|
const total = spec.layers.reduce((n, l) => n + (isObj(l) && isNum(l.count) ? l.count : 0), 0);
|
|
760
807
|
if (total > LIMITS.maxTotal) ok = false;
|
|
@@ -801,6 +848,7 @@ function validateSpecCore(spec) {
|
|
|
801
848
|
}
|
|
802
849
|
if (spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
803
850
|
if (spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
|
|
851
|
+
if (spec.groups !== void 0) validateGroups(spec.groups, err, warn, spec);
|
|
804
852
|
if (!Array.isArray(spec.layers) || spec.layers.length === 0) {
|
|
805
853
|
err("layers", "must be a non-empty array");
|
|
806
854
|
} else {
|
|
@@ -815,6 +863,41 @@ function validateSpecCore(spec) {
|
|
|
815
863
|
if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn);
|
|
816
864
|
return { valid: errors.length === 0, errors, warnings };
|
|
817
865
|
}
|
|
866
|
+
function validateTransform(tf, path, err, warn, spec, anchorOk) {
|
|
867
|
+
if (!isObj(tf)) return err(path, "must be an object {x?, y?, scale?, scaleX?, rotate?, origin?}");
|
|
868
|
+
for (const k of unknownKeys(tf, KNOWN_TRANSFORM)) warn(`${path}.${k}`, "unknown-property", `unknown transform property '${k}' \u2014 will be ignored`);
|
|
869
|
+
const [off, unit] = offsetBound(spec);
|
|
870
|
+
const sc = LIMITS.maxLayerTransformScale;
|
|
871
|
+
for (const axis of ["x", "y"]) {
|
|
872
|
+
if (tf[axis] !== void 0 && (!isNum(tf[axis]) || Math.abs(tf[axis]) > off)) err(`${path}.${axis}`, `must be a number within \xB1${off} (${unit})`);
|
|
873
|
+
}
|
|
874
|
+
if (tf.scale !== void 0 && (!isNum(tf.scale) || tf.scale < 0 || tf.scale > sc)) err(`${path}.scale`, `must be a number 0..${sc}`);
|
|
875
|
+
if (tf.scaleX !== void 0 && (!isNum(tf.scaleX) || Math.abs(tf.scaleX) > sc)) err(`${path}.scaleX`, `must be a number within \xB1${sc} (negative mirrors)`);
|
|
876
|
+
if (tf.rotate !== void 0 && (!isNum(tf.rotate) || Math.abs(tf.rotate) > LIMITS.maxLayerTransformRotate)) {
|
|
877
|
+
err(`${path}.rotate`, `must be a number of degrees within \xB1${LIMITS.maxLayerTransformRotate}`);
|
|
878
|
+
}
|
|
879
|
+
if (tf.origin !== void 0) {
|
|
880
|
+
if (tf.origin !== "viewport" && tf.origin !== "anchor") err(`${path}.origin`, "must be 'viewport' | 'anchor'");
|
|
881
|
+
else if (tf.origin === "anchor" && !anchorOk) err(`${path}.origin`, "a group transform always turns about the viewport centre \u2014 'anchor' is for a layer's own transform");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function validateGroups(groups, err, warn, spec) {
|
|
885
|
+
if (!isObj(groups)) return err("groups", "must be an object of named groups {name: {transform?, opacity?}}");
|
|
886
|
+
const names = Object.keys(groups);
|
|
887
|
+
if (names.length > LIMITS.maxGroups) err("groups", `at most ${LIMITS.maxGroups} groups`);
|
|
888
|
+
for (const name of names) {
|
|
889
|
+
const p = `groups.${name}`;
|
|
890
|
+
if (!GROUP_NAME.test(name)) err(p, "group names are 1\u201332 letters, digits, - or _, starting with a letter");
|
|
891
|
+
const g = groups[name];
|
|
892
|
+
if (!isObj(g)) {
|
|
893
|
+
err(p, "must be an object {transform?, opacity?}");
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
for (const k of unknownKeys(g, KNOWN_GROUP)) warn(`${p}.${k}`, "unknown-property", `unknown group property '${k}' \u2014 will be ignored`);
|
|
897
|
+
if (g.opacity !== void 0 && (!isNum(g.opacity) || g.opacity < 0 || g.opacity > 1)) err(`${p}.opacity`, "must be a number 0..1");
|
|
898
|
+
if (g.transform !== void 0) validateTransform(g.transform, `${p}.transform`, err, warn, spec, false);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
818
901
|
function validateTimeline(spec, errOuter, warn) {
|
|
819
902
|
let failed = false;
|
|
820
903
|
const err = (p, m) => {
|
|
@@ -856,10 +939,13 @@ function validateTimeline(spec, errOuter, warn) {
|
|
|
856
939
|
if (!isStr(key.path) || key.path.trim() === "") {
|
|
857
940
|
err(`${p}.path`, "must be a non-empty dot-path");
|
|
858
941
|
ok = false;
|
|
942
|
+
} else if (/\.group$/.test(key.path) || /\.transform\.origin$/.test(key.path)) {
|
|
943
|
+
err(`${p}.path`, `'${key.path}' is an enum that can only step \u2014 group membership and a transform's origin are fixed per scene`);
|
|
944
|
+
ok = false;
|
|
859
945
|
} else if (!cp) {
|
|
860
946
|
err(`${p}.path`, `'${key.path}' does not resolve on the spec \u2014 a key can only animate a field the spec already has`);
|
|
861
947
|
ok = false;
|
|
862
|
-
} else if (cp
|
|
948
|
+
} else if (cp === "timeline" || cp.startsWith("timeline.") || cp === "schemaVersion" || cp === "id" || cp === "label" || cp === "seed") {
|
|
863
949
|
err(`${p}.path`, `'${key.path}' is not animatable`);
|
|
864
950
|
ok = false;
|
|
865
951
|
}
|
|
@@ -872,6 +958,12 @@ function validateTimeline(spec, errOuter, warn) {
|
|
|
872
958
|
for (const e of r.errors) err(`${p}.value`, `applied at '${key.path}' \u2192 ${e.path || "<root>"}: ${e.message}`);
|
|
873
959
|
return;
|
|
874
960
|
}
|
|
961
|
+
if (isObj(key.value) && cp.endsWith("transform")) {
|
|
962
|
+
const before = readSpecPath(base, cp);
|
|
963
|
+
const o0 = isObj(before) ? before.origin ?? "viewport" : "viewport";
|
|
964
|
+
const o1 = key.value.origin ?? "viewport";
|
|
965
|
+
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`);
|
|
966
|
+
}
|
|
875
967
|
if (structuralSignature(applied) !== baseSig) {
|
|
876
968
|
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`);
|
|
877
969
|
}
|
|
@@ -1039,22 +1131,11 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
1039
1131
|
if (layer.opacity !== void 0 && (!isNum(layer.opacity) || layer.opacity < 0 || layer.opacity > 1)) {
|
|
1040
1132
|
err(`${path}.opacity`, "must be a number 0..1");
|
|
1041
1133
|
}
|
|
1042
|
-
if (layer.transform !== void 0) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
const [off, unit] = offsetBound(spec);
|
|
1048
|
-
const sc = LIMITS.maxLayerTransformScale;
|
|
1049
|
-
for (const axis of ["x", "y"]) {
|
|
1050
|
-
if (tf[axis] !== void 0 && (!isNum(tf[axis]) || Math.abs(tf[axis]) > off)) err(`${path}.transform.${axis}`, `must be a number within \xB1${off} (${unit})`);
|
|
1051
|
-
}
|
|
1052
|
-
if (tf.scale !== void 0 && (!isNum(tf.scale) || tf.scale < 0 || tf.scale > sc)) err(`${path}.transform.scale`, `must be a number 0..${sc}`);
|
|
1053
|
-
if (tf.scaleX !== void 0 && (!isNum(tf.scaleX) || Math.abs(tf.scaleX) > sc)) err(`${path}.transform.scaleX`, `must be a number within \xB1${sc} (negative mirrors)`);
|
|
1054
|
-
if (tf.rotate !== void 0 && (!isNum(tf.rotate) || Math.abs(tf.rotate) > LIMITS.maxLayerTransformRotate)) {
|
|
1055
|
-
err(`${path}.transform.rotate`, `must be a number of degrees within \xB1${LIMITS.maxLayerTransformRotate}`);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1134
|
+
if (layer.transform !== void 0) validateTransform(layer.transform, `${path}.transform`, err, warn, spec, true);
|
|
1135
|
+
if (layer.group !== void 0) {
|
|
1136
|
+
const groups = isObj(spec) && isObj(spec.groups) ? spec.groups : void 0;
|
|
1137
|
+
if (!isStr(layer.group)) err(`${path}.group`, "must be the name of a `groups` entry");
|
|
1138
|
+
else if (!groups || !Object.prototype.hasOwnProperty.call(groups, layer.group) || !isObj(groups[layer.group])) err(`${path}.group`, `'${layer.group}' is not in \`groups\` \u2014 declare it there first`);
|
|
1058
1139
|
}
|
|
1059
1140
|
if (layer.position !== void 0) {
|
|
1060
1141
|
if (!isObj(layer.position) || !isNum(layer.position.x) || !isNum(layer.position.y)) {
|
|
@@ -2688,6 +2769,67 @@ import {
|
|
|
2688
2769
|
createRng
|
|
2689
2770
|
} from "@idle-screens/core";
|
|
2690
2771
|
|
|
2772
|
+
// src/paint.ts
|
|
2773
|
+
function layerAnchor(layer, w, h, unit) {
|
|
2774
|
+
const p = layer.position;
|
|
2775
|
+
if (p) return { x: p.x * w + (p.dx ?? 0) * unit, y: p.y * h + (p.dy ?? 0) * unit };
|
|
2776
|
+
const rx = layer.region?.x ?? [0, 1];
|
|
2777
|
+
const ry = layer.region?.y ?? [0, 1];
|
|
2778
|
+
return { x: (rx[0] + rx[1]) / 2 * w, y: (ry[0] + ry[1]) / 2 * h };
|
|
2779
|
+
}
|
|
2780
|
+
function transformOrigin(tf, layer, w, h, unit) {
|
|
2781
|
+
return tf.origin === "anchor" && layer ? layerAnchor(layer, w, h, unit) : { x: w / 2, y: h / 2 };
|
|
2782
|
+
}
|
|
2783
|
+
function transformPoint(tf, p, w, h, unit, origin = { x: w / 2, y: h / 2 }) {
|
|
2784
|
+
const s = tf.scale ?? 1;
|
|
2785
|
+
const sx = s * (tf.scaleX ?? 1);
|
|
2786
|
+
let x = (p.x - origin.x) * sx;
|
|
2787
|
+
let y = (p.y - origin.y) * s;
|
|
2788
|
+
if (tf.rotate) {
|
|
2789
|
+
const r = tf.rotate * Math.PI / 180;
|
|
2790
|
+
const c = Math.cos(r);
|
|
2791
|
+
const n = Math.sin(r);
|
|
2792
|
+
[x, y] = [x * c - y * n, x * n + y * c];
|
|
2793
|
+
}
|
|
2794
|
+
return { x: x + origin.x + (tf.x ?? 0) * unit, y: y + origin.y + (tf.y ?? 0) * unit };
|
|
2795
|
+
}
|
|
2796
|
+
function transformSize(tf) {
|
|
2797
|
+
if (!tf) return 1;
|
|
2798
|
+
const s = tf.scale ?? 1;
|
|
2799
|
+
return s * Math.sqrt(Math.abs(tf.scaleX ?? 1));
|
|
2800
|
+
}
|
|
2801
|
+
function groupOf(spec, layer) {
|
|
2802
|
+
return layer.group === void 0 ? void 0 : spec.groups?.[layer.group];
|
|
2803
|
+
}
|
|
2804
|
+
var clamp01 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
|
|
2805
|
+
function paintOpacity(spec, layer) {
|
|
2806
|
+
const own = layer.opacity === void 0 ? 1 : clamp01(layer.opacity);
|
|
2807
|
+
const g = groupOf(spec, layer);
|
|
2808
|
+
return g?.opacity === void 0 ? own : own * clamp01(g.opacity);
|
|
2809
|
+
}
|
|
2810
|
+
function paintSize(spec, layer) {
|
|
2811
|
+
return transformSize(layer.transform) * transformSize(groupOf(spec, layer)?.transform);
|
|
2812
|
+
}
|
|
2813
|
+
function paintMap(spec, layer, w, h, unit) {
|
|
2814
|
+
const tf = layer.transform;
|
|
2815
|
+
const gtf = groupOf(spec, layer)?.transform;
|
|
2816
|
+
if (!tf && !gtf) return null;
|
|
2817
|
+
const o = tf ? transformOrigin(tf, layer, w, h, unit) : null;
|
|
2818
|
+
return (p) => {
|
|
2819
|
+
const q = tf ? transformPoint(tf, p, w, h, unit, o) : p;
|
|
2820
|
+
return gtf ? transformPoint(gtf, q, w, h, unit) : q;
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
function mapBox(map, box) {
|
|
2824
|
+
const pts = [[box.x0, box.y0], [box.x1, box.y0], [box.x0, box.y1], [box.x1, box.y1]].map(([x, y]) => map({ x, y }));
|
|
2825
|
+
return {
|
|
2826
|
+
x0: Math.min(...pts.map((p) => p.x)),
|
|
2827
|
+
y0: Math.min(...pts.map((p) => p.y)),
|
|
2828
|
+
x1: Math.max(...pts.map((p) => p.x)),
|
|
2829
|
+
y1: Math.max(...pts.map((p) => p.y))
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2691
2833
|
// src/field.ts
|
|
2692
2834
|
var TWO_PI = Math.PI * 2;
|
|
2693
2835
|
var FIELD_BUCKET_MS = 100;
|
|
@@ -2697,7 +2839,7 @@ var WARP_GAIN = 2;
|
|
|
2697
2839
|
var DRIFT_GAIN = 1;
|
|
2698
2840
|
var FIELD_DRIFT_DEFAULT_AMOUNT = 0.3;
|
|
2699
2841
|
var CONTRAST = [1, 1.45, 1.8, 1.95, 2];
|
|
2700
|
-
var
|
|
2842
|
+
var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
2701
2843
|
function latticeHash(ix, iy, seed) {
|
|
2702
2844
|
let h = Math.imul(ix | 0, 668265261) ^ Math.imul(iy | 0, 374761393) ^ Math.imul(seed | 0, 2654435761);
|
|
2703
2845
|
h = Math.imul(h ^ h >>> 15, 2246822507);
|
|
@@ -2760,7 +2902,7 @@ function fieldAt(u, v, t, cfg, seed) {
|
|
|
2760
2902
|
y += warp * WARP_GAIN * qy;
|
|
2761
2903
|
}
|
|
2762
2904
|
const raw = fbm(x, y, s, octaves);
|
|
2763
|
-
return
|
|
2905
|
+
return clamp012(0.5 + (raw - 0.5) * CONTRAST[octaves]);
|
|
2764
2906
|
}
|
|
2765
2907
|
function hexToRgb255(hex) {
|
|
2766
2908
|
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
@@ -2776,10 +2918,10 @@ function fieldRgb(value, bands, quantize) {
|
|
|
2776
2918
|
const q = Math.round(quantize ?? n);
|
|
2777
2919
|
let f;
|
|
2778
2920
|
if (q >= 2) {
|
|
2779
|
-
const level = Math.min(q - 1, Math.floor(
|
|
2921
|
+
const level = Math.min(q - 1, Math.floor(clamp012(value) * q));
|
|
2780
2922
|
f = level * (n - 1) / (q - 1);
|
|
2781
2923
|
} else {
|
|
2782
|
-
f =
|
|
2924
|
+
f = clamp012(value) * (n - 1);
|
|
2783
2925
|
}
|
|
2784
2926
|
const i = Math.min(n - 2, Math.floor(f));
|
|
2785
2927
|
const k = f - i;
|
|
@@ -2802,9 +2944,9 @@ var DITHER_TILE = 8;
|
|
|
2802
2944
|
var GRAIN_ALPHA = 0.35;
|
|
2803
2945
|
var DITHER_ALPHA = 0.25;
|
|
2804
2946
|
var FINISH_ANIMATE_HZ = 12;
|
|
2805
|
-
var
|
|
2947
|
+
var clamp013 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
2806
2948
|
function finishStrength(f) {
|
|
2807
|
-
return { grain:
|
|
2949
|
+
return { grain: clamp013(f?.grain ?? 0), dither: clamp013(f?.dither ?? 0) };
|
|
2808
2950
|
}
|
|
2809
2951
|
function grainTilePixels(seed) {
|
|
2810
2952
|
const px = new Uint8ClampedArray(GRAIN_TILE * GRAIN_TILE * 4);
|
|
@@ -2983,10 +3125,10 @@ function manifestFor(spec) {
|
|
|
2983
3125
|
workerReady: true
|
|
2984
3126
|
};
|
|
2985
3127
|
}
|
|
2986
|
-
var
|
|
3128
|
+
var clamp014 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
|
|
2987
3129
|
var EXPIRED = /* @__PURE__ */ Symbol("expired");
|
|
2988
3130
|
function overrideHeldAt(o, t) {
|
|
2989
|
-
const k = o.dur > 0 ? easeSmooth(
|
|
3131
|
+
const k = o.dur > 0 ? easeSmooth(clamp014((t - o.t0) / o.dur)) : 1;
|
|
2990
3132
|
return k >= 1 ? o.to : lerpValue(o.from, o.to, k, false, o.leaf);
|
|
2991
3133
|
}
|
|
2992
3134
|
function overrideValueAt(o, t, timelineValue) {
|
|
@@ -3015,10 +3157,10 @@ function applyOverridesAt(resolved, overrides, t, expired) {
|
|
|
3015
3157
|
function overridesStayValid(authored, overrides, now) {
|
|
3016
3158
|
const paths = [...overrides.keys()];
|
|
3017
3159
|
if (paths.length === 0) return true;
|
|
3018
|
-
const
|
|
3019
|
-
const groups = [...new Set(paths.map(
|
|
3020
|
-
const full = paths.some((p) => /^layers\.\d+(\.(key|count|motion)(\.|$)|$)/.test(p) || !/^(layers|background|finish)(\.|$)/.test(p));
|
|
3021
|
-
const only = full ? void 0 : (p) => groups.includes(
|
|
3160
|
+
const groupOf2 = (p) => p.split(".").slice(0, p.startsWith("layers.") ? 2 : 1).join(".");
|
|
3161
|
+
const groups = [...new Set(paths.map(groupOf2))];
|
|
3162
|
+
const full = paths.some((p) => /^layers\.\d+(\.(key|count|motion)(\.|$)|$)/.test(p) || p === "groups" || !/^(layers|background|finish|groups)(\.|$)/.test(p));
|
|
3163
|
+
const only = full ? void 0 : (p) => groups.includes(groupOf2(p));
|
|
3022
3164
|
const seen = /* @__PURE__ */ new Set();
|
|
3023
3165
|
for (const t of [now, ...timelineSampleTimesAfter(authored, now, false, only)]) {
|
|
3024
3166
|
const held = /* @__PURE__ */ new Map();
|
|
@@ -3727,13 +3869,17 @@ var SpecInstance = class {
|
|
|
3727
3869
|
for (let li = 0; li < this.layers.length; li++) {
|
|
3728
3870
|
const built = this.layers[li];
|
|
3729
3871
|
const opacity = built.layer.opacity;
|
|
3730
|
-
const
|
|
3872
|
+
const group = built.layer.group === void 0 ? void 0 : this.effSpec.groups?.[built.layer.group];
|
|
3873
|
+
let lifeA = opacity === void 0 ? lifeAlphaAt(built.layer.life, t) : lifeAlphaAt(built.layer.life, t) * clamp014(opacity);
|
|
3874
|
+
if (group?.opacity !== void 0) lifeA *= clamp014(group.opacity);
|
|
3731
3875
|
if (lifeA <= 0) continue;
|
|
3732
3876
|
const parentE = this.parentEntityFor(built);
|
|
3733
3877
|
const tf = built.layer.transform;
|
|
3734
|
-
|
|
3878
|
+
const gtf = group?.transform;
|
|
3879
|
+
if (tf || gtf) {
|
|
3735
3880
|
ctx.save();
|
|
3736
|
-
this.applyLayerTransform(
|
|
3881
|
+
if (gtf) this.applyLayerTransform(gtf, null);
|
|
3882
|
+
if (tf) this.applyLayerTransform(tf, built.layer);
|
|
3737
3883
|
}
|
|
3738
3884
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
3739
3885
|
this.drawLinks(built, t, lifeA, parentE);
|
|
@@ -3748,17 +3894,20 @@ var SpecInstance = class {
|
|
|
3748
3894
|
if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
|
|
3749
3895
|
if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
|
|
3750
3896
|
}
|
|
3751
|
-
if (tf) ctx.restore();
|
|
3897
|
+
if (tf || gtf) ctx.restore();
|
|
3752
3898
|
}
|
|
3753
3899
|
ctx.globalAlpha = 1;
|
|
3754
3900
|
ctx.globalCompositeOperation = "source-over";
|
|
3755
3901
|
}
|
|
3756
|
-
/**
|
|
3757
|
-
|
|
3902
|
+
/**
|
|
3903
|
+
* A `transform`, in min(w,h) units (px under `units: 'px'`), about the
|
|
3904
|
+
* viewport centre — or, with `origin: 'anchor'`, about `layer`'s own anchor
|
|
3905
|
+
* (a group's transform passes no layer: always the viewport centre).
|
|
3906
|
+
*/
|
|
3907
|
+
applyLayerTransform(tf, layer) {
|
|
3758
3908
|
const { ctx, w, h } = this;
|
|
3759
3909
|
const unit = this.effSpec.units === "px" ? 1 : Math.min(w, h);
|
|
3760
|
-
const cx = w
|
|
3761
|
-
const cy = h / 2;
|
|
3910
|
+
const { x: cx, y: cy } = transformOrigin(tf, layer, w, h, unit);
|
|
3762
3911
|
ctx.translate(cx + (tf.x ?? 0) * unit, cy + (tf.y ?? 0) * unit);
|
|
3763
3912
|
if (tf.rotate) ctx.rotate(tf.rotate * Math.PI / 180);
|
|
3764
3913
|
const s = tf.scale ?? 1;
|
|
@@ -4943,42 +5092,13 @@ function seamsWorthWarning(layer, c, entityCount) {
|
|
|
4943
5092
|
return layer.blend !== "lighter" && layer.blend !== "screen";
|
|
4944
5093
|
}
|
|
4945
5094
|
|
|
4946
|
-
// src/paint.ts
|
|
4947
|
-
function transformPoint(tf, p, w, h, unit) {
|
|
4948
|
-
const s = tf.scale ?? 1;
|
|
4949
|
-
const sx = s * (tf.scaleX ?? 1);
|
|
4950
|
-
let x = (p.x - w / 2) * sx;
|
|
4951
|
-
let y = (p.y - h / 2) * s;
|
|
4952
|
-
if (tf.rotate) {
|
|
4953
|
-
const r = tf.rotate * Math.PI / 180;
|
|
4954
|
-
const c = Math.cos(r);
|
|
4955
|
-
const n = Math.sin(r);
|
|
4956
|
-
[x, y] = [x * c - y * n, x * n + y * c];
|
|
4957
|
-
}
|
|
4958
|
-
return { x: x + w / 2 + (tf.x ?? 0) * unit, y: y + h / 2 + (tf.y ?? 0) * unit };
|
|
4959
|
-
}
|
|
4960
|
-
function transformSize(tf) {
|
|
4961
|
-
if (!tf) return 1;
|
|
4962
|
-
const s = tf.scale ?? 1;
|
|
4963
|
-
return s * Math.sqrt(Math.abs(tf.scaleX ?? 1));
|
|
4964
|
-
}
|
|
4965
|
-
var layerOpacity = (layer) => layer.opacity === void 0 ? 1 : Math.max(0, Math.min(1, layer.opacity));
|
|
4966
|
-
function transformBox(tf, box, w, h, unit) {
|
|
4967
|
-
const pts = [[box.x0, box.y0], [box.x1, box.y0], [box.x0, box.y1], [box.x1, box.y1]].map(([x, y]) => transformPoint(tf, { x, y }, w, h, unit));
|
|
4968
|
-
return {
|
|
4969
|
-
x0: Math.min(...pts.map((p) => p.x)),
|
|
4970
|
-
y0: Math.min(...pts.map((p) => p.y)),
|
|
4971
|
-
x1: Math.max(...pts.map((p) => p.x)),
|
|
4972
|
-
y1: Math.max(...pts.map((p) => p.y))
|
|
4973
|
-
};
|
|
4974
|
-
}
|
|
4975
|
-
|
|
4976
5095
|
// src/advise.ts
|
|
4977
5096
|
var LOW_CONTRAST_FLOOR = 0.05;
|
|
4978
5097
|
var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
4979
5098
|
var READ_LEGIBILITY_FLOOR = 4.5;
|
|
4980
5099
|
var READ_SAFE_AREA = 0.05;
|
|
4981
5100
|
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
5101
|
+
const hidden = hiddenLayers(spec);
|
|
4982
5102
|
spec = resolveTimelineAt(spec, opts.t ?? COHESION_T);
|
|
4983
5103
|
const warnings = [];
|
|
4984
5104
|
const w = viewport.width;
|
|
@@ -5004,6 +5124,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
5004
5124
|
let textLayerCount = 0;
|
|
5005
5125
|
let motionLayerCount = 0;
|
|
5006
5126
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5127
|
+
if (hidden?.[li]) continue;
|
|
5007
5128
|
const layer = spec.layers[li];
|
|
5008
5129
|
const entities = allEntities[li];
|
|
5009
5130
|
totalEntities += entities.length;
|
|
@@ -5070,8 +5191,10 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
5070
5191
|
}
|
|
5071
5192
|
let totalCoverage = 0;
|
|
5072
5193
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5194
|
+
if (hidden?.[li]) continue;
|
|
5073
5195
|
const layer = spec.layers[li];
|
|
5074
5196
|
const entities = allEntities[li];
|
|
5197
|
+
const paintA = layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer);
|
|
5075
5198
|
for (const e of entities) {
|
|
5076
5199
|
const r = e.size / 2;
|
|
5077
5200
|
let pixArea;
|
|
@@ -5096,7 +5219,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
5096
5219
|
const g2 = (ga * ga + ga * gb + gb * gb) / 3;
|
|
5097
5220
|
duty = Math.min(layer.emit.life, layer.emit.every) / layer.emit.every * 0.5 * g2;
|
|
5098
5221
|
}
|
|
5099
|
-
totalCoverage += pixArea * e.alpha * duty / (w * h);
|
|
5222
|
+
totalCoverage += paintA === 1 ? pixArea * e.alpha * duty / (w * h) : pixArea * e.alpha * duty * paintA / (w * h);
|
|
5100
5223
|
}
|
|
5101
5224
|
if (layer.links) {
|
|
5102
5225
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
@@ -5105,7 +5228,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
5105
5228
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
5106
5229
|
const lwPx = (layer.links.width ?? defaultWidth) * scale;
|
|
5107
5230
|
const la = layer.links.alpha ?? 1;
|
|
5108
|
-
for (const edge of edges) totalCoverage += edge.dist * lwPx * la / (w * h);
|
|
5231
|
+
for (const edge of edges) totalCoverage += paintA === 1 ? edge.dist * lwPx * la / (w * h) : edge.dist * lwPx * la * paintA / (w * h);
|
|
5109
5232
|
}
|
|
5110
5233
|
}
|
|
5111
5234
|
const density = spec.density ?? "normal";
|
|
@@ -5148,6 +5271,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
5148
5271
|
}
|
|
5149
5272
|
}
|
|
5150
5273
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5274
|
+
if (hidden?.[li]) continue;
|
|
5151
5275
|
const layer = spec.layers[li];
|
|
5152
5276
|
if (!layer.links || layer.links.mode === "chain") continue;
|
|
5153
5277
|
const entities = allEntities[li];
|
|
@@ -5164,6 +5288,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
5164
5288
|
}
|
|
5165
5289
|
}
|
|
5166
5290
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5291
|
+
if (hidden?.[li]) continue;
|
|
5167
5292
|
const layer = spec.layers[li];
|
|
5168
5293
|
if (layer.motion.type === "static") continue;
|
|
5169
5294
|
const entities = allEntities[li];
|
|
@@ -5180,7 +5305,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
5180
5305
|
});
|
|
5181
5306
|
}
|
|
5182
5307
|
}
|
|
5183
|
-
const composed = spec.layers.map((l, li) => ({ l, ents: allEntities[li] })).filter(({ l }) => !["bounce", "warp", "path"].includes(l.motion.type)).filter(({ l }) => !(l.motion.type === "orbit" && l.motion.center && "layer" in l.motion.center));
|
|
5308
|
+
const composed = spec.layers.map((l, li) => ({ l, li, ents: allEntities[li] })).filter(({ li }) => !hidden?.[li]).filter(({ l }) => !["bounce", "warp", "path"].includes(l.motion.type)).filter(({ l }) => !(l.motion.type === "orbit" && l.motion.center && "layer" in l.motion.center));
|
|
5184
5309
|
const composedCount = composed.reduce((s, c) => s + c.ents.length, 0);
|
|
5185
5310
|
if (composedCount >= 8) {
|
|
5186
5311
|
const positions = composed.flatMap(({ ents }) => ents.map((e) => positionAt(e, 0, w, h)));
|
|
@@ -5198,6 +5323,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
5198
5323
|
}
|
|
5199
5324
|
const textBoxes = [];
|
|
5200
5325
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5326
|
+
if (hidden?.[li]) continue;
|
|
5201
5327
|
const layer = spec.layers[li];
|
|
5202
5328
|
const s = layer.sprite;
|
|
5203
5329
|
if (layer.motion.type !== "static") continue;
|
|
@@ -5206,9 +5332,10 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
5206
5332
|
for (const e of allEntities[li]) {
|
|
5207
5333
|
const p = positionAt(e, 0, w, h);
|
|
5208
5334
|
const raw = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
5209
|
-
const
|
|
5335
|
+
const map = paintMap(spec, layer, w, h, scale);
|
|
5336
|
+
const box = map ? { ...raw, ...mapBox(map, raw) } : raw;
|
|
5210
5337
|
const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
5211
|
-
const alpha = layer.opacity === void 0 ? baseAlpha : baseAlpha *
|
|
5338
|
+
const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
|
|
5212
5339
|
textBoxes.push({ li, label, alpha, ...box });
|
|
5213
5340
|
}
|
|
5214
5341
|
}
|
|
@@ -5331,14 +5458,15 @@ function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
|
5331
5458
|
if (blend !== "lighter" && blend !== "screen") continue;
|
|
5332
5459
|
if (lj === box.li) continue;
|
|
5333
5460
|
const entities = allEntities[lj];
|
|
5461
|
+
const moved = layer.transform !== void 0 || groupOf(spec, layer)?.transform !== void 0;
|
|
5334
5462
|
for (const e of entities) {
|
|
5335
5463
|
const hex = spriteHex(layer, e);
|
|
5336
5464
|
if (hex === null) continue;
|
|
5337
5465
|
const growScale = (e.motion === "warp" ? WARP_MAX_SCALE : 1) * (e.emit ? Math.max(e.emit.growFrom, e.emit.growTo) : 1);
|
|
5338
5466
|
const maxDim = Math.max(e.size, e.size2 ?? 0) * (1 + e.growAmp) * growScale;
|
|
5339
5467
|
if (maxDim < box.fs) continue;
|
|
5340
|
-
if (!
|
|
5341
|
-
const a = layer.opacity === void 0 ? Math.min(1, e.alpha + e.pulseAmp) : Math.min(1, e.alpha + e.pulseAmp) *
|
|
5468
|
+
if (!moved && !entityReachesBox(e, maxDim / 2, box, w, h)) continue;
|
|
5469
|
+
const a = layer.opacity === void 0 && layer.group === void 0 ? Math.min(1, e.alpha + e.pulseAmp) : Math.min(1, e.alpha + e.pulseAmp) * paintOpacity(spec, layer);
|
|
5342
5470
|
if (a <= 0) continue;
|
|
5343
5471
|
const plate = additivePlate(ground, hexRgb(hex), a, blend);
|
|
5344
5472
|
const lum = relativeLuminance(plate);
|
|
@@ -5386,6 +5514,26 @@ function textBlockBoxAt(s, p, w, h) {
|
|
|
5386
5514
|
const { dx, dy } = textBlockAnchorOffset(s, maxWPx, maxLineW, totalH);
|
|
5387
5515
|
return { x0: x0 + dx, y0: p.y + dy, x1: x0 + dx + maxLineW, y1: p.y + dy + totalH, fs: fsPx };
|
|
5388
5516
|
}
|
|
5517
|
+
function hiddenLayers(spec) {
|
|
5518
|
+
if (!spec.groups && !spec.layers.some((l) => l.opacity !== void 0)) return null;
|
|
5519
|
+
const tracks = spec.timeline ? timelineTracks(spec) : null;
|
|
5520
|
+
const pinnedZero = (path, base, ancestors) => {
|
|
5521
|
+
if (base !== 0) return false;
|
|
5522
|
+
if (!tracks) return true;
|
|
5523
|
+
for (const [p, keys] of tracks) {
|
|
5524
|
+
if (p === path && keys.some((k) => k.value !== 0)) return false;
|
|
5525
|
+
if (ancestors.includes(p)) return false;
|
|
5526
|
+
}
|
|
5527
|
+
return true;
|
|
5528
|
+
};
|
|
5529
|
+
const out = spec.layers.map((l, i) => {
|
|
5530
|
+
const lp = `layers.${i}`;
|
|
5531
|
+
if (pinnedZero(`${lp}.opacity`, l.opacity, ["layers", lp])) return true;
|
|
5532
|
+
const g = l.group !== void 0 && spec.groups && Object.prototype.hasOwnProperty.call(spec.groups, l.group) ? spec.groups[l.group] : void 0;
|
|
5533
|
+
return !!g && pinnedZero(`groups.${l.group}.opacity`, g.opacity, ["groups", `groups.${l.group}`]);
|
|
5534
|
+
});
|
|
5535
|
+
return out.some(Boolean) ? out : null;
|
|
5536
|
+
}
|
|
5389
5537
|
function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
5390
5538
|
const warnings = [];
|
|
5391
5539
|
for (let i = 0; i < seq.segments.length; i++) {
|
|
@@ -5405,17 +5553,17 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
5405
5553
|
});
|
|
5406
5554
|
}
|
|
5407
5555
|
for (let i = 0; i < seq.segments.length - 1; i++) {
|
|
5556
|
+
const tr = seq.segments[i].transition;
|
|
5408
5557
|
const lumaA = backgroundLuma(resolveTimelineAt(seq.segments[i].scene, seq.segments[i].duration ?? 0));
|
|
5409
5558
|
const lumaB = backgroundLuma(resolveTimelineAt(seq.segments[i + 1].scene, 0));
|
|
5410
5559
|
const delta = Math.abs(lumaA - lumaB);
|
|
5411
|
-
if (delta > 0.5) {
|
|
5560
|
+
if (delta > 0.5 && tr?.type !== "fade") {
|
|
5412
5561
|
warnings.push({
|
|
5413
5562
|
path: `segments[${i}]`,
|
|
5414
5563
|
code: "boundary-luminance-jump",
|
|
5415
|
-
message: `background luminance jumps ${delta.toFixed(2)} at boundary ${i}\u2192${i + 1} \u2014 may flash on cut`
|
|
5564
|
+
message: `background luminance jumps ${delta.toFixed(2)} at boundary ${i}\u2192${i + 1} \u2014 may flash on cut${tr === void 0 || tr.type === "cut" ? `; declare transition: { type: 'fade', dur } on segments[${i}] to ramp it` : ""}`
|
|
5416
5565
|
});
|
|
5417
5566
|
}
|
|
5418
|
-
const tr = seq.segments[i].transition;
|
|
5419
5567
|
if (tr?.type === "morph") {
|
|
5420
5568
|
const sigA = structuralSignature(seq.segments[i].scene);
|
|
5421
5569
|
const sigB = structuralSignature(seq.segments[i + 1].scene);
|
|
@@ -5553,9 +5701,10 @@ function buildScene(spec, opts) {
|
|
|
5553
5701
|
for (const { layer, entities } of layers) if (layer.key) byKey.set(layer.key, entities);
|
|
5554
5702
|
let transforms = null;
|
|
5555
5703
|
for (const { layer, entities } of layers) {
|
|
5556
|
-
|
|
5704
|
+
const map = paintMap(spec, layer, w, h, scale);
|
|
5705
|
+
if (!map) continue;
|
|
5557
5706
|
transforms ??= /* @__PURE__ */ new Map();
|
|
5558
|
-
for (const e of entities) transforms.set(e,
|
|
5707
|
+
for (const e of entities) transforms.set(e, map);
|
|
5559
5708
|
}
|
|
5560
5709
|
return { w, h, scale, seed, layers, byKey, transforms };
|
|
5561
5710
|
}
|
|
@@ -5569,8 +5718,8 @@ function posOf(scene, e, t) {
|
|
|
5569
5718
|
p.y += pp.y;
|
|
5570
5719
|
}
|
|
5571
5720
|
}
|
|
5572
|
-
const
|
|
5573
|
-
return
|
|
5721
|
+
const map = scene.transforms?.get(e);
|
|
5722
|
+
return map ? map(p) : p;
|
|
5574
5723
|
}
|
|
5575
5724
|
function textBox(s, e, p) {
|
|
5576
5725
|
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
@@ -5680,6 +5829,37 @@ function distanceToOutline(pts, x, y) {
|
|
|
5680
5829
|
}
|
|
5681
5830
|
return Math.sqrt(best);
|
|
5682
5831
|
}
|
|
5832
|
+
function rotatedBoxCoverage(cx, cy, hx, hy, rot, x0, y0, x1, y1) {
|
|
5833
|
+
const cr = Math.cos(rot), sr = Math.sin(rot);
|
|
5834
|
+
let poly = [[-hx, -hy], [hx, -hy], [hx, hy], [-hx, hy]].map(([x, y]) => [cx + x * cr - y * sr, cy + x * sr + y * cr]);
|
|
5835
|
+
const clip = (inside, cut) => {
|
|
5836
|
+
const out = [];
|
|
5837
|
+
for (let i = 0; i < poly.length; i++) {
|
|
5838
|
+
const a = poly[i], b = poly[(i + 1) % poly.length];
|
|
5839
|
+
if (inside(a)) {
|
|
5840
|
+
out.push(a);
|
|
5841
|
+
if (!inside(b)) out.push(cut(a, b));
|
|
5842
|
+
} else if (inside(b)) out.push(cut(a, b));
|
|
5843
|
+
}
|
|
5844
|
+
poly = out;
|
|
5845
|
+
};
|
|
5846
|
+
const atX = (x) => (a, b) => [x, a[1] + (x - a[0]) / (b[0] - a[0]) * (b[1] - a[1])];
|
|
5847
|
+
const atY = (y) => (a, b) => [a[0] + (y - a[1]) / (b[1] - a[1]) * (b[0] - a[0]), y];
|
|
5848
|
+
clip((p) => p[0] >= x0, atX(x0));
|
|
5849
|
+
if (!poly.length) return 0;
|
|
5850
|
+
clip((p) => p[0] <= x1, atX(x1));
|
|
5851
|
+
if (!poly.length) return 0;
|
|
5852
|
+
clip((p) => p[1] >= y0, atY(y0));
|
|
5853
|
+
if (!poly.length) return 0;
|
|
5854
|
+
clip((p) => p[1] <= y1, atY(y1));
|
|
5855
|
+
if (!poly.length) return 0;
|
|
5856
|
+
let area = 0;
|
|
5857
|
+
for (let i = 0; i < poly.length; i++) {
|
|
5858
|
+
const a = poly[i], b = poly[(i + 1) % poly.length];
|
|
5859
|
+
area += a[0] * b[1] - b[0] * a[1];
|
|
5860
|
+
}
|
|
5861
|
+
return Math.abs(area) / 2 / ((x1 - x0) * (y1 - y0));
|
|
5862
|
+
}
|
|
5683
5863
|
function luminanceGrid(spec, opts = {}) {
|
|
5684
5864
|
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5685
5865
|
const scene = buildScene(spec, opts);
|
|
@@ -5839,7 +6019,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5839
6019
|
};
|
|
5840
6020
|
const splatPass = (tPass, alphaScale) => {
|
|
5841
6021
|
for (const { layer, entities } of scene.layers) {
|
|
5842
|
-
const lifeA = layer.opacity === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) *
|
|
6022
|
+
const lifeA = layer.opacity === void 0 && layer.group === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * paintOpacity(spec, layer);
|
|
5843
6023
|
if (lifeA <= 0) continue;
|
|
5844
6024
|
for (const e of entities) {
|
|
5845
6025
|
splatTrail(layer, e, tPass, lifeA, alphaScale);
|
|
@@ -5847,7 +6027,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5847
6027
|
if (a <= 4e-3) continue;
|
|
5848
6028
|
const lum = spriteLuma(layer, e);
|
|
5849
6029
|
const p = posOf(scene, e, tPass);
|
|
5850
|
-
const sz = layer.transform ? sizeAt(e, tPass) *
|
|
6030
|
+
const sz = layer.transform || layer.group !== void 0 ? sizeAt(e, tPass) * paintSize(spec, layer) : sizeAt(e, tPass);
|
|
5851
6031
|
const s = layer.sprite;
|
|
5852
6032
|
if (s.kind === "polygon" && sz >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH)) {
|
|
5853
6033
|
splatPolygonOutline(layer, s, e, p, sz, a, lum, tPass);
|
|
@@ -5902,6 +6082,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5902
6082
|
let centerY = p.y;
|
|
5903
6083
|
let halfX = sz / 2;
|
|
5904
6084
|
let halfY = s.kind === "rect" ? (e.size2 ?? sz) * (e.size > 0 ? sz / e.size : 1) / 2 : sz / 2;
|
|
6085
|
+
let boxRot = 0, boxHx = 0, boxHy = 0;
|
|
5905
6086
|
if (s.kind === "text") {
|
|
5906
6087
|
const box = textBox(s, e, p);
|
|
5907
6088
|
centerX = box.cx;
|
|
@@ -5924,8 +6105,23 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5924
6105
|
const sr = Math.sin(rot);
|
|
5925
6106
|
centerX = p.x + box.cx * cr - box.cy * sr;
|
|
5926
6107
|
centerY = p.y + box.cx * sr + box.cy * cr;
|
|
6108
|
+
boxRot = rot;
|
|
6109
|
+
boxHx = box.halfX;
|
|
6110
|
+
boxHy = box.halfY;
|
|
5927
6111
|
halfX = Math.abs(box.halfX * cr) + Math.abs(box.halfY * sr);
|
|
5928
6112
|
halfY = Math.abs(box.halfX * sr) + Math.abs(box.halfY * cr);
|
|
6113
|
+
} else if (s.kind === "rect") {
|
|
6114
|
+
const rot = rotationAt(e, tPass);
|
|
6115
|
+
if (rot) {
|
|
6116
|
+
boxRot = rot;
|
|
6117
|
+
boxHx = halfX;
|
|
6118
|
+
boxHy = halfY;
|
|
6119
|
+
const cr = Math.abs(Math.cos(rot));
|
|
6120
|
+
const sr = Math.abs(Math.sin(rot));
|
|
6121
|
+
const hx = halfX * cr + halfY * sr;
|
|
6122
|
+
halfY = halfX * sr + halfY * cr;
|
|
6123
|
+
halfX = hx;
|
|
6124
|
+
}
|
|
5929
6125
|
}
|
|
5930
6126
|
const circular = s.kind === "circle" || s.kind === "ring" || s.kind === "polygon";
|
|
5931
6127
|
const soft = (s.kind === "circle" || s.kind === "polygon") && !!s.soft;
|
|
@@ -5939,7 +6135,10 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5939
6135
|
const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
|
|
5940
6136
|
const r1 = Math.min(rows - 1, Math.floor((centerY + reachY) / cellH));
|
|
5941
6137
|
const inkWeight = (s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1) * (s.kind === "textBlock" ? textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1) : 1);
|
|
6138
|
+
const byArea = s.kind === "rect" || s.kind === "bar";
|
|
5942
6139
|
for (let r = r0; r <= r1; r++) {
|
|
6140
|
+
const fy = byArea ? Math.max(0, Math.min((r + 1) * cellH, centerY + halfY) - Math.max(r * cellH, centerY - halfY)) / cellH : 1;
|
|
6141
|
+
if (fy <= 0) continue;
|
|
5943
6142
|
for (let c = c0; c <= c1; c++) {
|
|
5944
6143
|
const dx = (c + 0.5) * cellW - centerX;
|
|
5945
6144
|
const dy = (r + 0.5) * cellH - centerY;
|
|
@@ -5963,6 +6162,14 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5963
6162
|
wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
|
|
5964
6163
|
}
|
|
5965
6164
|
compose(r * cols + c, lum, a * wgt * shapeWeight, layer.blend);
|
|
6165
|
+
} else if (byArea) {
|
|
6166
|
+
if (boxRot) {
|
|
6167
|
+
const cov = rotatedBoxCoverage(centerX, centerY, boxHx, boxHy, boxRot, c * cellW, r * cellH, (c + 1) * cellW, (r + 1) * cellH);
|
|
6168
|
+
if (cov > 0) compose(r * cols + c, lum, a * shapeWeight * cov, layer.blend);
|
|
6169
|
+
} else {
|
|
6170
|
+
const fx = Math.max(0, Math.min((c + 1) * cellW, centerX + halfX) - Math.max(c * cellW, centerX - halfX)) / cellW;
|
|
6171
|
+
if (fx > 0) compose(r * cols + c, lum, a * shapeWeight * fx * fy, layer.blend);
|
|
6172
|
+
}
|
|
5966
6173
|
} else {
|
|
5967
6174
|
compose(r * cols + c, lum, a * inkWeight * shapeWeight, layer.blend);
|
|
5968
6175
|
}
|
|
@@ -6161,8 +6368,9 @@ function rawDominance(spec, opts = {}) {
|
|
|
6161
6368
|
return Math.sqrt(dx * dx + dy * dy);
|
|
6162
6369
|
};
|
|
6163
6370
|
const raw = scene.layers.map(({ layer, entities }, layerIndex) => {
|
|
6164
|
-
const lifeA = layer.opacity === void 0 ? lifeAlphaAt(layer.life, t) : lifeAlphaAt(layer.life, t) *
|
|
6165
|
-
const
|
|
6371
|
+
const lifeA = layer.opacity === void 0 && layer.group === void 0 ? lifeAlphaAt(layer.life, t) : lifeAlphaAt(layer.life, t) * paintOpacity(spec, layer);
|
|
6372
|
+
const painted = layer.transform !== void 0 || layer.group !== void 0;
|
|
6373
|
+
const areaScale = painted ? paintSize(spec, layer) ** 2 : 1;
|
|
6166
6374
|
let area = 0;
|
|
6167
6375
|
let lumAcc = 0;
|
|
6168
6376
|
for (const e of entities) {
|
|
@@ -6191,7 +6399,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
6191
6399
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
6192
6400
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
|
|
6193
6401
|
} else entArea = sz * sz * 0.55;
|
|
6194
|
-
area +=
|
|
6402
|
+
area += painted ? entArea * areaScale * a : entArea * a;
|
|
6195
6403
|
if (layer.trail) {
|
|
6196
6404
|
const fade = layer.trail.fade ?? 1;
|
|
6197
6405
|
const span = sweptDistance(e, layer.trail.length);
|