@idle-screens/schema 3.6.1 → 3.7.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 +261 -37
- package/dist/index.d.ts +274 -8
- package/dist/index.js +852 -86
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +71 -4
package/dist/index.js
CHANGED
|
@@ -46,7 +46,8 @@ var LIMITS = {
|
|
|
46
46
|
maxTextBlockLength: 2e3,
|
|
47
47
|
minTextBlockFontSize: 0.01,
|
|
48
48
|
maxTextBlockFontSize: 0.2,
|
|
49
|
-
maxTextBlockMaxWidth:
|
|
49
|
+
maxTextBlockMaxWidth: 2,
|
|
50
|
+
// of min(w,h) — wider than the frame is legal; text-off-screen reports the overflow
|
|
50
51
|
maxRevealSpeed: 120,
|
|
51
52
|
// graphemes/sec — faster than any readable typing
|
|
52
53
|
maxCaretBlinkHz: 3,
|
|
@@ -149,6 +150,58 @@ function lerpSpec(from, to, k) {
|
|
|
149
150
|
};
|
|
150
151
|
return walk(from, to);
|
|
151
152
|
}
|
|
153
|
+
function textStringsOf(sprite) {
|
|
154
|
+
if (sprite.kind === "text") return sprite.strings;
|
|
155
|
+
if (sprite.kind === "textBlock") return [sprite.text];
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
function textStringsDiffer(a, b, i) {
|
|
159
|
+
const sa = a.layers[i]?.sprite;
|
|
160
|
+
const sb = b.layers[i]?.sprite;
|
|
161
|
+
if (!sa || !sb) return false;
|
|
162
|
+
const ta = textStringsOf(sa);
|
|
163
|
+
const tb = textStringsOf(sb);
|
|
164
|
+
if (!ta || !tb) return false;
|
|
165
|
+
return ta.length !== tb.length || ta.some((s, j) => s !== tb[j]);
|
|
166
|
+
}
|
|
167
|
+
var NON_RENDERED_KEYS = /* @__PURE__ */ new Set(["id", "label", "schemaVersion", "key"]);
|
|
168
|
+
function morphNothingMorphable(a, b, opts = {}) {
|
|
169
|
+
if (opts.textCrossfade && a.layers.some((_, i) => textStringsDiffer(a, b, i))) return false;
|
|
170
|
+
let hasDiff = false;
|
|
171
|
+
let hasGlide = false;
|
|
172
|
+
const walk = (x, y, key) => {
|
|
173
|
+
if (key !== void 0 && NON_RENDERED_KEYS.has(key)) return;
|
|
174
|
+
if (x === y) return;
|
|
175
|
+
if (typeof x === "number" && typeof y === "number") {
|
|
176
|
+
hasDiff = true;
|
|
177
|
+
hasGlide = true;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (typeof x === "string" && typeof y === "string" && HEX.test(x) && HEX.test(y)) {
|
|
181
|
+
const ca = hexToRgb(x);
|
|
182
|
+
const cb = hexToRgb(y);
|
|
183
|
+
if (ca.some((v, i) => v !== cb[i])) {
|
|
184
|
+
hasDiff = true;
|
|
185
|
+
hasGlide = true;
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(x) && Array.isArray(y) && x.length === y.length) {
|
|
190
|
+
for (let i = 0; i < y.length; i++) walk(x[i], y[i]);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (x && y && typeof x === "object" && typeof y === "object" && !Array.isArray(x) && !Array.isArray(y)) {
|
|
194
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(x), ...Object.keys(y)]);
|
|
195
|
+
for (const k of keys) {
|
|
196
|
+
walk(x[k], y[k], k);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
hasDiff = true;
|
|
201
|
+
};
|
|
202
|
+
walk(a, b);
|
|
203
|
+
return hasDiff && !hasGlide;
|
|
204
|
+
}
|
|
152
205
|
function steerablePaths(spec) {
|
|
153
206
|
if (!spec || typeof spec !== "object") return [];
|
|
154
207
|
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer"]);
|
|
@@ -175,6 +228,12 @@ function steerablePaths(spec) {
|
|
|
175
228
|
walk(spec, "", "");
|
|
176
229
|
return out;
|
|
177
230
|
}
|
|
231
|
+
function sequenceSteerablePaths(seq) {
|
|
232
|
+
const out = /* @__PURE__ */ new Set(["sequence.segment"]);
|
|
233
|
+
if (seq.bed) for (const p of steerablePaths(seq.bed)) out.add(`bed.${p}`);
|
|
234
|
+
for (const seg of seq.segments) for (const p of steerablePaths(seg.scene)) out.add(p);
|
|
235
|
+
return [...out];
|
|
236
|
+
}
|
|
178
237
|
function easeSmooth(k) {
|
|
179
238
|
const c = Math.max(0, Math.min(1, k));
|
|
180
239
|
return c * c * (3 - 2 * c);
|
|
@@ -208,7 +267,7 @@ function structuralSignature(spec) {
|
|
|
208
267
|
l.sprite.kind === "rect" ? [s.width, s.aspect] : l.sprite.kind === "bar" ? [s.length, s.thickness] : void 0,
|
|
209
268
|
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
210
269
|
s.colorWeights,
|
|
211
|
-
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.fontSize] : void 0
|
|
270
|
+
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
|
|
212
271
|
];
|
|
213
272
|
})
|
|
214
273
|
]);
|
|
@@ -251,8 +310,11 @@ var KNOWN_BAR = /* @__PURE__ */ new Set(["kind", "values", "length", "thickness"
|
|
|
251
310
|
var KNOWN_POLYGON = /* @__PURE__ */ new Set(["kind", "radius", "color", "sides", "points", "soft", "colors", "colorWeights"]);
|
|
252
311
|
var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "colors", "colorWeights"]);
|
|
253
312
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
254
|
-
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
255
|
-
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal"]);
|
|
313
|
+
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle", "role"]);
|
|
314
|
+
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal", "anchor", "font", "opacity", "role"]);
|
|
315
|
+
var TEXT_ROLES = /* @__PURE__ */ new Set(["read", "atmosphere"]);
|
|
316
|
+
var TEXT_BLOCK_ANCHORS = /* @__PURE__ */ new Set(["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"]);
|
|
317
|
+
var FONT_SIZE_RE = /\d(?:px|pt|pc|em|rem|ex|ch|vw|vh|vmin|vmax|%)/i;
|
|
256
318
|
var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret", "fade"]);
|
|
257
319
|
var KNOWN_REVEAL_CARET = /* @__PURE__ */ new Set(["blink", "color"]);
|
|
258
320
|
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob", "ease"]);
|
|
@@ -674,6 +736,7 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
674
736
|
if (sprite.maxWidth !== void 0 && (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0)) {
|
|
675
737
|
err(`${path}.maxWidth`, "must be a positive number");
|
|
676
738
|
}
|
|
739
|
+
validateTextRole(sprite, path, err);
|
|
677
740
|
validateCycle(sprite, path, err);
|
|
678
741
|
} else if (sprite.kind === "circle") {
|
|
679
742
|
knownSet = KNOWN_CIRCLE;
|
|
@@ -760,6 +823,20 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
760
823
|
err(`${path}.align`, "must be 'left' | 'center' | 'right'");
|
|
761
824
|
}
|
|
762
825
|
if (sprite.color !== void 0) color(sprite.color, `${path}.color`, err);
|
|
826
|
+
if (sprite.anchor !== void 0 && !TEXT_BLOCK_ANCHORS.has(sprite.anchor)) {
|
|
827
|
+
err(`${path}.anchor`, "must be 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right'");
|
|
828
|
+
}
|
|
829
|
+
if (sprite.font !== void 0) {
|
|
830
|
+
if (!isStr(sprite.font) || sprite.font.trim() === "") {
|
|
831
|
+
err(`${path}.font`, "must be a non-empty string (family and/or weight)");
|
|
832
|
+
} else if (FONT_SIZE_RE.test(sprite.font)) {
|
|
833
|
+
err(`${path}.font`, "must name a family and/or weight only \u2014 fontSize owns the size");
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (sprite.opacity !== void 0 && (!isNum(sprite.opacity) || sprite.opacity < 0 || sprite.opacity > 1)) {
|
|
837
|
+
err(`${path}.opacity`, "must be a number between 0 and 1");
|
|
838
|
+
}
|
|
839
|
+
validateTextRole(sprite, path, err);
|
|
763
840
|
if (sprite.reveal !== void 0) {
|
|
764
841
|
const rv = sprite.reveal;
|
|
765
842
|
if (typeof rv !== "object" || rv === null || Array.isArray(rv)) {
|
|
@@ -826,6 +903,11 @@ function validatePalette(sprite, path, err) {
|
|
|
826
903
|
}
|
|
827
904
|
}
|
|
828
905
|
}
|
|
906
|
+
function validateTextRole(sprite, path, err) {
|
|
907
|
+
if (sprite.role !== void 0 && !TEXT_ROLES.has(sprite.role)) {
|
|
908
|
+
err(`${path}.role`, "must be 'read' | 'atmosphere'");
|
|
909
|
+
}
|
|
910
|
+
}
|
|
829
911
|
function validateCycle(sprite, path, err) {
|
|
830
912
|
if (sprite.cycle === void 0) return;
|
|
831
913
|
if (!isObj(sprite.cycle)) return err(`${path}.cycle`, "must be an object");
|
|
@@ -998,6 +1080,7 @@ function validateSequence(seq) {
|
|
|
998
1080
|
}
|
|
999
1081
|
}
|
|
1000
1082
|
}
|
|
1083
|
+
if (isObj(seq) && isObj(seq.bed)) normalizeColors(seq.bed);
|
|
1001
1084
|
const errors = [];
|
|
1002
1085
|
const warnings = [];
|
|
1003
1086
|
const err = (path, message) => void errors.push({ path, message });
|
|
@@ -1008,6 +1091,7 @@ function validateSequence(seq) {
|
|
|
1008
1091
|
if (!isStr(seq.label) || seq.label.trim() === "") err("label", "must be a non-empty string");
|
|
1009
1092
|
if (seq.seed !== void 0 && !isNum(seq.seed)) err("seed", "must be a number");
|
|
1010
1093
|
if (typeof seq.loop !== "boolean") err("loop", "must be a boolean");
|
|
1094
|
+
if (seq.sync !== void 0 && seq.sync !== "mount" && seq.sync !== "epoch") err("sync", "must be 'mount' | 'epoch'");
|
|
1011
1095
|
if (!Array.isArray(seq.segments) || seq.segments.length === 0) {
|
|
1012
1096
|
err("segments", "must be a non-empty array");
|
|
1013
1097
|
return { valid: false, errors, warnings };
|
|
@@ -1050,12 +1134,19 @@ function validateSequence(seq) {
|
|
|
1050
1134
|
if (s.transition !== void 0) {
|
|
1051
1135
|
if (!isObj(s.transition)) {
|
|
1052
1136
|
err(`${p}.transition`, "must be an object");
|
|
1053
|
-
} else if (s.transition.type === "morph") {
|
|
1137
|
+
} else if (s.transition.type === "morph" || s.transition.type === "fade") {
|
|
1054
1138
|
if (!isNum(s.transition.dur) || s.transition.dur < LIMITS.minTransitionDur || s.transition.dur > LIMITS.maxTransitionDur) {
|
|
1055
1139
|
err(`${p}.transition.dur`, `must be a number between ${LIMITS.minTransitionDur} and ${LIMITS.maxTransitionDur}`);
|
|
1056
1140
|
}
|
|
1057
1141
|
} else if (s.transition.type !== "cut") {
|
|
1058
|
-
err(`${p}.transition.type`, "must be 'cut' or '
|
|
1142
|
+
err(`${p}.transition.type`, "must be 'cut', 'morph' or 'fade'");
|
|
1143
|
+
}
|
|
1144
|
+
if (isObj(s.transition) && s.transition.text !== void 0) {
|
|
1145
|
+
if (s.transition.type !== "morph") {
|
|
1146
|
+
err(`${p}.transition.text`, "is a morph option (fade already cross-fades whole frames; cut has no window)");
|
|
1147
|
+
} else if (s.transition.text !== "step" && s.transition.text !== "crossfade") {
|
|
1148
|
+
err(`${p}.transition.text`, "must be 'step' | 'crossfade'");
|
|
1149
|
+
}
|
|
1059
1150
|
}
|
|
1060
1151
|
}
|
|
1061
1152
|
if (isObj(s.scene)) {
|
|
@@ -1071,6 +1162,32 @@ function validateSequence(seq) {
|
|
|
1071
1162
|
if (seq.loop === true && hasDurationless) {
|
|
1072
1163
|
err("loop", "loop: true requires all segments to have a duration");
|
|
1073
1164
|
}
|
|
1165
|
+
if (seq.bed !== void 0) {
|
|
1166
|
+
if (!isObj(seq.bed)) {
|
|
1167
|
+
err("bed", "must be a SaverSpec object");
|
|
1168
|
+
} else {
|
|
1169
|
+
const bedResult = validateSpec(seq.bed);
|
|
1170
|
+
for (const e of bedResult.errors) errors.push({ path: `bed.${e.path}`, message: e.message });
|
|
1171
|
+
for (const w of bedResult.warnings ?? []) warnings.push({ path: `bed.${w.path}`, code: w.code, message: w.message });
|
|
1172
|
+
if (bedResult.valid) {
|
|
1173
|
+
const entityTotal = (spec) => isObj(spec) && Array.isArray(spec.layers) ? spec.layers.reduce((n, l) => n + (isObj(l) && isNum(l.count) ? l.count : 0), 0) : 0;
|
|
1174
|
+
const bedTotal = entityTotal(seq.bed);
|
|
1175
|
+
const largest = seq.segments.reduce((m, s) => Math.max(m, isObj(s) ? entityTotal(s.scene) : 0), 0);
|
|
1176
|
+
if (bedTotal + largest > LIMITS.maxTotal) {
|
|
1177
|
+
err("bed", `bed entities ${bedTotal} + largest segment ${largest} = ${bedTotal + largest} exceeds cap ${LIMITS.maxTotal} (the two are live at once)`);
|
|
1178
|
+
}
|
|
1179
|
+
seq.segments.forEach((s, i) => {
|
|
1180
|
+
if (isObj(s) && isObj(s.scene) && s.scene.background !== void 0) {
|
|
1181
|
+
warnings.push({
|
|
1182
|
+
path: `segments[${i}].scene.background`,
|
|
1183
|
+
code: "bed-hides-segment-background",
|
|
1184
|
+
message: `segment ${i} declares a background but the sequence has a bed \u2014 segments render transparently over the bed, so this background is never painted`
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1074
1191
|
if (errors.length === 0 && Array.isArray(seq.segments)) {
|
|
1075
1192
|
for (let i = 0; i < seq.segments.length; i++) {
|
|
1076
1193
|
const s = seq.segments[i];
|
|
@@ -1085,6 +1202,12 @@ function validateSequence(seq) {
|
|
|
1085
1202
|
code: "morph-structural-mismatch",
|
|
1086
1203
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
1087
1204
|
});
|
|
1205
|
+
} else if (morphNothingMorphable(s.scene, next.scene, { textCrossfade: s.transition.text === "crossfade" })) {
|
|
1206
|
+
warnings.push({
|
|
1207
|
+
path: `segments[${i}].transition`,
|
|
1208
|
+
code: "morph-nothing-morphable",
|
|
1209
|
+
message: `segments ${i}\u2192${i + 1} differ only in values morph cannot interpolate (strings such as textBlock.text step on the first frame): the morph will look like a cut \u2014 fade text via colour or reveal.progress`
|
|
1210
|
+
});
|
|
1088
1211
|
}
|
|
1089
1212
|
}
|
|
1090
1213
|
}
|
|
@@ -1777,18 +1900,25 @@ function linkPairs(positions, k, maxDist, doWrap, w, h) {
|
|
|
1777
1900
|
}
|
|
1778
1901
|
var CHAR_NARROW = new Set(`iIljtf1!|.,;:'"()[]{}`);
|
|
1779
1902
|
var CHAR_WIDE = new Set("mwMWGOQD@%");
|
|
1780
|
-
|
|
1903
|
+
var MONO_FAMILY_RE = /\b(?:ui-)?mono(?:space)?\b|courier|menlo|monaco|sfmono|consolas|inconsolata|fira\s*code|source\s*code|jetbrains|ibm\s*plex\s*mono|roboto\s*mono/i;
|
|
1904
|
+
function textMetricsClassFor(font) {
|
|
1905
|
+
return font && MONO_FAMILY_RE.test(font) ? "mono" : "proportional";
|
|
1906
|
+
}
|
|
1907
|
+
var MONO_ADVANCE_EM = 0.6;
|
|
1908
|
+
function charWidthEm(ch, metrics = "proportional") {
|
|
1909
|
+
if (metrics === "mono") return MONO_ADVANCE_EM;
|
|
1781
1910
|
if (ch === " " || ch === " ") return 0.3;
|
|
1782
1911
|
if (CHAR_NARROW.has(ch)) return 0.35;
|
|
1783
1912
|
if (CHAR_WIDE.has(ch)) return 0.72;
|
|
1784
1913
|
return 0.55;
|
|
1785
1914
|
}
|
|
1786
|
-
function textWidthEm(str) {
|
|
1915
|
+
function textWidthEm(str, metrics = "proportional") {
|
|
1916
|
+
if (metrics === "mono") return graphemeClusters(str).length * MONO_ADVANCE_EM;
|
|
1787
1917
|
let w = 0;
|
|
1788
|
-
for (let i = 0; i < str.length; i++) w += charWidthEm(str[i]);
|
|
1918
|
+
for (let i = 0; i < str.length; i++) w += charWidthEm(str[i], metrics);
|
|
1789
1919
|
return w;
|
|
1790
1920
|
}
|
|
1791
|
-
function breakTextBlock(text, maxWidthEm) {
|
|
1921
|
+
function breakTextBlock(text, maxWidthEm, metrics = "proportional") {
|
|
1792
1922
|
const paragraphs = text.split("\n");
|
|
1793
1923
|
const lines = [];
|
|
1794
1924
|
for (const para of paragraphs) {
|
|
@@ -1802,11 +1932,11 @@ function breakTextBlock(text, maxWidthEm) {
|
|
|
1802
1932
|
continue;
|
|
1803
1933
|
}
|
|
1804
1934
|
let lineText = words[0];
|
|
1805
|
-
let lineW = textWidthEm(lineText);
|
|
1806
|
-
const spaceW = charWidthEm(" ");
|
|
1935
|
+
let lineW = textWidthEm(lineText, metrics);
|
|
1936
|
+
const spaceW = charWidthEm(" ", metrics);
|
|
1807
1937
|
for (let i = 1; i < words.length; i++) {
|
|
1808
1938
|
const word = words[i];
|
|
1809
|
-
const wordW = textWidthEm(word);
|
|
1939
|
+
const wordW = textWidthEm(word, metrics);
|
|
1810
1940
|
if (lineW + spaceW + wordW <= maxWidthEm) {
|
|
1811
1941
|
lineText += " " + word;
|
|
1812
1942
|
lineW += spaceW + wordW;
|
|
@@ -1820,6 +1950,13 @@ function breakTextBlock(text, maxWidthEm) {
|
|
|
1820
1950
|
}
|
|
1821
1951
|
return lines;
|
|
1822
1952
|
}
|
|
1953
|
+
function textBlockAnchorOffset(s, maxWPx, maxLineW, totalH) {
|
|
1954
|
+
if (!s.anchor) return { dx: 0, dy: 0 };
|
|
1955
|
+
const ax = s.anchor.endsWith("left") ? 0 : s.anchor.endsWith("right") ? 1 : 0.5;
|
|
1956
|
+
const ay = s.anchor.startsWith("top") ? 0 : s.anchor.startsWith("bottom") ? 1 : 0.5;
|
|
1957
|
+
const inkX0 = s.align === "center" ? (maxWPx - maxLineW) / 2 : s.align === "right" ? maxWPx - maxLineW : 0;
|
|
1958
|
+
return { dx: -(inkX0 + ax * maxLineW), dy: -(ay * totalH) };
|
|
1959
|
+
}
|
|
1823
1960
|
function isClusterExtend(cp) {
|
|
1824
1961
|
return cp >= 768 && cp <= 879 || // combining diacritics
|
|
1825
1962
|
cp >= 6832 && cp <= 6911 || cp >= 8400 && cp <= 8447 || cp >= 65024 && cp <= 65039 || // variation selectors
|
|
@@ -1967,9 +2104,45 @@ function resolveSegment(seq, T, opts = {}) {
|
|
|
1967
2104
|
const lastStart = totalTimed - (segments[last].duration ?? 0);
|
|
1968
2105
|
return { index: last, localT: t0 - lastStart, startT: lastStart };
|
|
1969
2106
|
}
|
|
2107
|
+
function canMorph(seq, from) {
|
|
2108
|
+
const seg = seq.segments[from];
|
|
2109
|
+
if (!seg || seg.transition?.type !== "morph") return false;
|
|
2110
|
+
const next = seq.segments[from + 1];
|
|
2111
|
+
if (!next) return false;
|
|
2112
|
+
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
2113
|
+
}
|
|
2114
|
+
function morphChainRoot(seq, index) {
|
|
2115
|
+
let i = index;
|
|
2116
|
+
while (i > 0 && canMorph(seq, i - 1)) i--;
|
|
2117
|
+
return i;
|
|
2118
|
+
}
|
|
2119
|
+
function normalizeSeed(seed) {
|
|
2120
|
+
return seed >>> 0 || 1;
|
|
2121
|
+
}
|
|
2122
|
+
function segmentRenderSeed(seq, index) {
|
|
2123
|
+
const root = morphChainRoot(seq, index);
|
|
2124
|
+
const scene = seq.segments[root]?.scene;
|
|
2125
|
+
if (!scene) return void 0;
|
|
2126
|
+
const raw = scene.seed ?? (seq.seed !== void 0 ? seq.seed + root : void 0);
|
|
2127
|
+
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
2128
|
+
}
|
|
2129
|
+
function bedRenderSeed(seq) {
|
|
2130
|
+
if (!seq.bed) return void 0;
|
|
2131
|
+
const raw = seq.bed.seed ?? (seq.seed !== void 0 ? seq.seed + LIMITS.maxSegments : void 0);
|
|
2132
|
+
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
2133
|
+
}
|
|
1970
2134
|
|
|
1971
2135
|
// src/compile.ts
|
|
1972
2136
|
var DEFAULT_STEER_DUR = 1e3;
|
|
2137
|
+
function applyRetainedDeltas(spec, deltas) {
|
|
2138
|
+
let out = spec;
|
|
2139
|
+
for (const d of deltas) {
|
|
2140
|
+
if (!resolveSpecPath(out, d.path)) continue;
|
|
2141
|
+
const next = applyDeltasToSpec(out, [d]);
|
|
2142
|
+
if (validateSpec(next).valid) out = next;
|
|
2143
|
+
}
|
|
2144
|
+
return out;
|
|
2145
|
+
}
|
|
1973
2146
|
function hexToRgba(hex, alpha) {
|
|
1974
2147
|
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
1975
2148
|
const n = parseInt(h.slice(1), 16);
|
|
@@ -2007,11 +2180,12 @@ function manifestFor(spec) {
|
|
|
2007
2180
|
};
|
|
2008
2181
|
}
|
|
2009
2182
|
var SpecInstance = class {
|
|
2010
|
-
constructor(spec, ctx) {
|
|
2183
|
+
constructor(spec, ctx, opts = {}) {
|
|
2011
2184
|
this.spec = spec;
|
|
2012
2185
|
this.effSpec = spec;
|
|
2013
2186
|
this.saverCtx = ctx;
|
|
2014
|
-
this.
|
|
2187
|
+
this.transparent = opts.transparent === true;
|
|
2188
|
+
this.seed = normalizeSeed(spec.seed ?? ctx.seed);
|
|
2015
2189
|
let canvas;
|
|
2016
2190
|
if (ctx.surface) {
|
|
2017
2191
|
canvas = ctx.surface;
|
|
@@ -2025,7 +2199,7 @@ var SpecInstance = class {
|
|
|
2025
2199
|
this.ownsCanvas = true;
|
|
2026
2200
|
}
|
|
2027
2201
|
this.canvas = canvas;
|
|
2028
|
-
const c2d = canvas.getContext("2d", { alpha:
|
|
2202
|
+
const c2d = canvas.getContext("2d", { alpha: this.transparent });
|
|
2029
2203
|
if (!c2d) throw new Error("schema saver: no 2d context");
|
|
2030
2204
|
this.ctx = c2d;
|
|
2031
2205
|
this.w = ctx.width;
|
|
@@ -2033,12 +2207,16 @@ var SpecInstance = class {
|
|
|
2033
2207
|
this.sizeCanvas();
|
|
2034
2208
|
this.rebuild();
|
|
2035
2209
|
this.paused = ctx.reducedMotion;
|
|
2036
|
-
if (this.paused)
|
|
2037
|
-
|
|
2210
|
+
if (this.paused) {
|
|
2211
|
+
if (!opts.skipInitialPaint) this.renderFrame(0, this.seed);
|
|
2212
|
+
} else {
|
|
2213
|
+
this.start();
|
|
2214
|
+
}
|
|
2038
2215
|
}
|
|
2039
2216
|
spec;
|
|
2040
2217
|
canvas;
|
|
2041
2218
|
ownsCanvas;
|
|
2219
|
+
transparent;
|
|
2042
2220
|
ctx;
|
|
2043
2221
|
saverCtx;
|
|
2044
2222
|
seed;
|
|
@@ -2058,6 +2236,8 @@ var SpecInstance = class {
|
|
|
2058
2236
|
/** Active glide between two resolved specs (live setParam/applyTrack). */
|
|
2059
2237
|
transition = null;
|
|
2060
2238
|
lastStructural = "";
|
|
2239
|
+
/** See `LayerPaintOverride`; null (the default, and every plain mount) draws every layer once. */
|
|
2240
|
+
paintOverrides = null;
|
|
2061
2241
|
/** Viewport factor for absolute px sizes — 1 for `units: 'px'` specs. */
|
|
2062
2242
|
fontScale() {
|
|
2063
2243
|
if (this.effSpec.units === "px") return 1;
|
|
@@ -2194,14 +2374,14 @@ var SpecInstance = class {
|
|
|
2194
2374
|
ctx.fill();
|
|
2195
2375
|
}
|
|
2196
2376
|
}
|
|
2197
|
-
drawEntity(built, e, t, lifeA, parentE) {
|
|
2377
|
+
drawEntity(built, e, t, lifeA, parentE, paint) {
|
|
2198
2378
|
const { ctx } = this;
|
|
2199
2379
|
const p = this.entityPos(e, t, parentE);
|
|
2200
|
-
const sprite = built.layer.sprite;
|
|
2380
|
+
const sprite = paint?.sprite ?? built.layer.sprite;
|
|
2201
2381
|
const sz = sizeAt(e, t);
|
|
2202
2382
|
const rot = rotationAt(e, t);
|
|
2203
2383
|
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
2204
|
-
ctx.globalAlpha = alphaAt(e, t) * lifeA;
|
|
2384
|
+
ctx.globalAlpha = alphaAt(e, t) * lifeA * (paint?.alpha ?? 1);
|
|
2205
2385
|
if (sprite.kind === "ring") {
|
|
2206
2386
|
const r = sz / 2;
|
|
2207
2387
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
@@ -2353,7 +2533,7 @@ var SpecInstance = class {
|
|
|
2353
2533
|
const lh = (sprite.lineHeight ?? 1.4) * fsPx;
|
|
2354
2534
|
const maxWPx = sprite.maxWidth * unitScale2;
|
|
2355
2535
|
const maxWEm = maxWPx / fsPx;
|
|
2356
|
-
const lines = breakTextBlock(sprite.text, maxWEm);
|
|
2536
|
+
const lines = breakTextBlock(sprite.text, maxWEm, textMetricsClassFor(sprite.font));
|
|
2357
2537
|
const align = sprite.align ?? "left";
|
|
2358
2538
|
const reveal = sprite.reveal;
|
|
2359
2539
|
const rs = reveal ? revealState(lines, reveal, t) : null;
|
|
@@ -2361,7 +2541,13 @@ var SpecInstance = class {
|
|
|
2361
2541
|
ctx.save();
|
|
2362
2542
|
ctx.translate(p.x, p.y);
|
|
2363
2543
|
if (rot) ctx.rotate(rot);
|
|
2364
|
-
|
|
2544
|
+
if (sprite.anchor) {
|
|
2545
|
+
const maxLineW = lines.reduce((m, l) => Math.max(m, l.widthEm), 0) * fsPx;
|
|
2546
|
+
const { dx, dy } = textBlockAnchorOffset(sprite, maxWPx, maxLineW, lines.length * lh);
|
|
2547
|
+
if (dx !== 0 || dy !== 0) ctx.translate(dx, dy);
|
|
2548
|
+
}
|
|
2549
|
+
if (sprite.opacity !== void 0) ctx.globalAlpha *= sprite.opacity;
|
|
2550
|
+
ctx.font = sprite.font ? composeFontShorthand(fsPx, sprite.font) : `${fsPx}px system-ui, sans-serif`;
|
|
2365
2551
|
ctx.fillStyle = sprite.color ?? "#e6e8ef";
|
|
2366
2552
|
ctx.textBaseline = "top";
|
|
2367
2553
|
ctx.textAlign = align;
|
|
@@ -2475,6 +2661,33 @@ var SpecInstance = class {
|
|
|
2475
2661
|
this.renderFrame(this.lastT, this.seed);
|
|
2476
2662
|
}
|
|
2477
2663
|
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Apply steering deltas immediately: no glide, no frame painted. Each delta
|
|
2666
|
+
* is routed by `applyRetainedDeltas` (unresolvable or invalid ones are
|
|
2667
|
+
* skipped on this spec alone). `SequenceInstance` uses this to hand a
|
|
2668
|
+
* freshly created child the track the sequence has retained, so a steer
|
|
2669
|
+
* made while another segment was active lands on the segment that owns the
|
|
2670
|
+
* path. Not painting matters: a child is created inside the parent's
|
|
2671
|
+
* renderFrame, and a stray t=0 paint here would make the real frame that
|
|
2672
|
+
* follows look "contiguous" to the ghosting warm-up — but the constructor
|
|
2673
|
+
* already painted ONE stray t=0 frame of the pre-steer spec (SpecInstance's
|
|
2674
|
+
* own mount does an immediate render when paused, which every sequence
|
|
2675
|
+
* child is), so `lastRenderT` must be reset too: otherwise, with `ghosting`
|
|
2676
|
+
* on, the real frame that follows still finds itself "contiguous" with
|
|
2677
|
+
* that stray paint and composites over it at partial alpha, briefly
|
|
2678
|
+
* showing a ghost of the un-steered scene.
|
|
2679
|
+
*/
|
|
2680
|
+
applyDeltasNow(deltas) {
|
|
2681
|
+
const next = applyRetainedDeltas(this.effSpec, deltas);
|
|
2682
|
+
if (next === this.effSpec) return;
|
|
2683
|
+
this.transition = null;
|
|
2684
|
+
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
2685
|
+
this.effSpec = next;
|
|
2686
|
+
if (structuralSignature(this.effSpec) !== this.lastStructural) this.rebuild();
|
|
2687
|
+
else this.layers.forEach((b, i) => {
|
|
2688
|
+
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2478
2691
|
drawLinks(built, t, lifeA, parentE) {
|
|
2479
2692
|
const { links } = built.layer;
|
|
2480
2693
|
if (!links) return;
|
|
@@ -2520,19 +2733,28 @@ var SpecInstance = class {
|
|
|
2520
2733
|
* showing through — the ghosting smear. */
|
|
2521
2734
|
paintFrame(t, bgAlpha) {
|
|
2522
2735
|
const { ctx } = this;
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2736
|
+
if (!this.transparent) {
|
|
2737
|
+
ctx.globalAlpha = bgAlpha;
|
|
2738
|
+
ctx.globalCompositeOperation = "source-over";
|
|
2739
|
+
this.drawBackground(t);
|
|
2740
|
+
}
|
|
2526
2741
|
ctx.globalAlpha = 1;
|
|
2527
|
-
for (
|
|
2742
|
+
for (let li = 0; li < this.layers.length; li++) {
|
|
2743
|
+
const built = this.layers[li];
|
|
2528
2744
|
const lifeA = lifeAlphaAt(built.layer.life, t);
|
|
2529
2745
|
if (lifeA <= 0) continue;
|
|
2530
2746
|
const parentE = this.parentEntityFor(built);
|
|
2531
2747
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
2532
2748
|
this.drawLinks(built, t, lifeA, parentE);
|
|
2749
|
+
const override = this.paintOverrides?.get(li) ?? null;
|
|
2533
2750
|
for (const e of built.entities) {
|
|
2534
2751
|
this.drawTrail(built, e, t, lifeA, parentE);
|
|
2535
|
-
|
|
2752
|
+
if (override === null) {
|
|
2753
|
+
this.drawEntity(built, e, t, lifeA, parentE);
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
if (override.k < 1) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: 1 - override.k });
|
|
2757
|
+
if (override.k > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: override.k });
|
|
2536
2758
|
}
|
|
2537
2759
|
}
|
|
2538
2760
|
ctx.globalAlpha = 1;
|
|
@@ -2549,7 +2771,7 @@ var SpecInstance = class {
|
|
|
2549
2771
|
*/
|
|
2550
2772
|
renderFrame(t, _seed) {
|
|
2551
2773
|
this.stepTransition(t);
|
|
2552
|
-
const g = this.effSpec.ghosting ?? 0;
|
|
2774
|
+
const g = this.transparent ? 0 : this.effSpec.ghosting ?? 0;
|
|
2553
2775
|
if (g > 0) {
|
|
2554
2776
|
const dt = 1e3 / 60;
|
|
2555
2777
|
const contiguous = this.lastRenderT !== Number.NEGATIVE_INFINITY && t > this.lastRenderT && t - this.lastRenderT <= 250;
|
|
@@ -2585,8 +2807,8 @@ var SpecInstance = class {
|
|
|
2585
2807
|
}
|
|
2586
2808
|
/**
|
|
2587
2809
|
* Replace the rendered spec without triggering a transition glide.
|
|
2588
|
-
* Checks structural signature — use `hotSwapPaint` when the caller
|
|
2589
|
-
* already knows the signature is unchanged
|
|
2810
|
+
* Checks structural signature — use `hotSwapPaint` only when the caller
|
|
2811
|
+
* already knows the signature is unchanged.
|
|
2590
2812
|
*/
|
|
2591
2813
|
hotSwapSpec(spec) {
|
|
2592
2814
|
this.effSpec = spec;
|
|
@@ -2603,6 +2825,15 @@ var SpecInstance = class {
|
|
|
2603
2825
|
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
2604
2826
|
});
|
|
2605
2827
|
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Internal (see `LayerPaintOverride`): draw the listed layers twice —
|
|
2830
|
+
* outgoing sprite at `1 − k`, own sprite at `k` — until cleared with
|
|
2831
|
+
* `null`. Keyed by layer index. A `SequenceInstance` sets it per morph
|
|
2832
|
+
* frame under `text: 'crossfade'`; nothing else does.
|
|
2833
|
+
*/
|
|
2834
|
+
setLayerPaintOverrides(overrides) {
|
|
2835
|
+
this.paintOverrides = overrides && overrides.size > 0 ? overrides : null;
|
|
2836
|
+
}
|
|
2606
2837
|
dispose() {
|
|
2607
2838
|
this.stop();
|
|
2608
2839
|
if (this.ownsCanvas && typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
|
|
@@ -2616,6 +2847,7 @@ function compileSaver(spec) {
|
|
|
2616
2847
|
spec: valid
|
|
2617
2848
|
};
|
|
2618
2849
|
}
|
|
2850
|
+
var BED_PREFIX = "bed.";
|
|
2619
2851
|
var SequenceInstance = class {
|
|
2620
2852
|
seq;
|
|
2621
2853
|
childCtx;
|
|
@@ -2646,8 +2878,55 @@ var SequenceInstance = class {
|
|
|
2646
2878
|
*/
|
|
2647
2879
|
clockOffset = 0;
|
|
2648
2880
|
releasedBelow = 0;
|
|
2881
|
+
/**
|
|
2882
|
+
* Set by a `sequence.segment` steer that targets segment 0 under `loop:
|
|
2883
|
+
* true`: the steer resets the clock to segment 0's own start (localT 0),
|
|
2884
|
+
* so `renderFrame`'s wrap check (which compares the raw, ever-increasing
|
|
2885
|
+
* clock against `timedTotal()`) can no longer recognize the jump as a
|
|
2886
|
+
* wrap. This flag lets the clicker's jump to 0 still count as one, so the
|
|
2887
|
+
* last segment's fade plays instead of a hard cut. Cleared once the wrap
|
|
2888
|
+
* window (if any) has passed.
|
|
2889
|
+
*/
|
|
2890
|
+
pendingWrapFade = false;
|
|
2891
|
+
/**
|
|
2892
|
+
* Every non-`sequence.segment` delta this instance has been handed, last
|
|
2893
|
+
* wins per path, merged across `applyTrack` calls. Children are created
|
|
2894
|
+
* lazily and disposed on every segment switch, so a steer forwarded only
|
|
2895
|
+
* to the active child would evaporate at the next boundary — and a steer
|
|
2896
|
+
* to a path only a *later* segment owns (`bars.sprite.values` while the title
|
|
2897
|
+
* slide is up) would land nowhere. Re-applied to every child on creation
|
|
2898
|
+
* and folded into the morph endpoints; each child keeps the deltas that
|
|
2899
|
+
* resolve on it (see `applyRetainedDeltas`).
|
|
2900
|
+
*/
|
|
2901
|
+
retainedDeltas = /* @__PURE__ */ new Map();
|
|
2902
|
+
/** The outgoing segment while a `fade` runs; null otherwise. */
|
|
2903
|
+
fading = null;
|
|
2904
|
+
/**
|
|
2905
|
+
* The incoming segment while a `fade` runs over a `bed`. Children over a
|
|
2906
|
+
* bed are transparent, so a cross-fade needs both of them on canvases of
|
|
2907
|
+
* their own — the incoming composited at k, the outgoing at 1 − k. Without
|
|
2908
|
+
* a bed the incoming paints its own opaque ground on the shared surface and
|
|
2909
|
+
* this stays null.
|
|
2910
|
+
*/
|
|
2911
|
+
fadingIn = null;
|
|
2912
|
+
/** False on the `basic`/`minimal` capability tiers — fade renders as cut. */
|
|
2913
|
+
fadeEnabled;
|
|
2914
|
+
/**
|
|
2915
|
+
* The sequence's `bed`: one scene on the shared surface, drawn first every
|
|
2916
|
+
* frame at the sequence clock — WITHOUT `clockOffset`, so a
|
|
2917
|
+
* `sequence.segment` steer rewinds the segment and never the bed — and never
|
|
2918
|
+
* re-created. Segments render over it transparently. Null when absent.
|
|
2919
|
+
*/
|
|
2920
|
+
bed;
|
|
2649
2921
|
constructor(seq, ctx) {
|
|
2650
2922
|
this.seq = seq;
|
|
2923
|
+
const { sequenceBaseT, capabilityTier, ...plainCtx } = ctx;
|
|
2924
|
+
this.fadeEnabled = capabilityTier !== "basic" && capabilityTier !== "minimal";
|
|
2925
|
+
if (seq.sync === "epoch" && typeof sequenceBaseT === "number" && Number.isFinite(sequenceBaseT)) {
|
|
2926
|
+
this.baseT = Math.max(0, sequenceBaseT);
|
|
2927
|
+
this.lastT = this.baseT;
|
|
2928
|
+
this.renderedT = this.baseT;
|
|
2929
|
+
}
|
|
2651
2930
|
let surface = ctx.surface ?? null;
|
|
2652
2931
|
let canvas = null;
|
|
2653
2932
|
if (!surface && typeof document !== "undefined") {
|
|
@@ -2659,15 +2938,32 @@ var SequenceInstance = class {
|
|
|
2659
2938
|
}
|
|
2660
2939
|
this.canvas = canvas;
|
|
2661
2940
|
this.seed = (seq.seed ?? ctx.seed ?? 0) >>> 0 || 1;
|
|
2662
|
-
this.childCtx = { ...
|
|
2941
|
+
this.childCtx = { ...plainCtx, surface, reducedMotion: true };
|
|
2663
2942
|
this.children = new Array(seq.segments.length).fill(null);
|
|
2943
|
+
if (seq.bed) {
|
|
2944
|
+
this.bed = new SpecInstance(this.bedScene(seq.bed), this.childCtx);
|
|
2945
|
+
this.bed.setPaused(true);
|
|
2946
|
+
} else {
|
|
2947
|
+
this.bed = null;
|
|
2948
|
+
}
|
|
2664
2949
|
this.paused = ctx.reducedMotion;
|
|
2665
|
-
if (this.paused) this.renderFrame(
|
|
2950
|
+
if (this.paused) this.renderFrame(this.baseT, this.seed);
|
|
2666
2951
|
else this.start();
|
|
2667
2952
|
}
|
|
2668
2953
|
childSeed(index, fallback) {
|
|
2669
2954
|
return this.seq.segments[index].scene.seed ?? this.seq.seed ?? fallback;
|
|
2670
2955
|
}
|
|
2956
|
+
/**
|
|
2957
|
+
* The bed's seed follows the segment rule (own seed, else the sequence seed)
|
|
2958
|
+
* but offset by `maxSegments`, past every segment's `seed + index`, so a bed
|
|
2959
|
+
* and segment 0 never share an entity stream and adding a segment does not
|
|
2960
|
+
* re-seat the bed.
|
|
2961
|
+
*/
|
|
2962
|
+
bedScene(bed) {
|
|
2963
|
+
if (bed.seed != null) return bed;
|
|
2964
|
+
if (this.seq.seed != null) return { ...bed, seed: this.seq.seed + LIMITS.maxSegments };
|
|
2965
|
+
return bed;
|
|
2966
|
+
}
|
|
2671
2967
|
childScene(index) {
|
|
2672
2968
|
const seg = this.seq.segments[index];
|
|
2673
2969
|
if (seg.scene.seed != null) return seg.scene;
|
|
@@ -2676,29 +2972,132 @@ var SequenceInstance = class {
|
|
|
2676
2972
|
}
|
|
2677
2973
|
/** Whether the boundary from `from` to `from+1` should morph. */
|
|
2678
2974
|
canMorph(from) {
|
|
2679
|
-
|
|
2680
|
-
if (!seg || seg.transition?.type !== "morph") return false;
|
|
2681
|
-
const next = this.seq.segments[from + 1];
|
|
2682
|
-
if (!next) return false;
|
|
2683
|
-
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
2975
|
+
return canMorph(this.seq, from);
|
|
2684
2976
|
}
|
|
2685
2977
|
/** Walk back through consecutive morph boundaries to find the chain origin. */
|
|
2686
2978
|
morphChainRoot(index) {
|
|
2687
|
-
|
|
2688
|
-
while (i > 0 && this.canMorph(i - 1)) i--;
|
|
2689
|
-
return i;
|
|
2979
|
+
return morphChainRoot(this.seq, index);
|
|
2690
2980
|
}
|
|
2691
2981
|
morphDur(from) {
|
|
2692
2982
|
const tr = this.seq.segments[from]?.transition;
|
|
2693
2983
|
return tr?.type === "morph" ? tr.dur : 0;
|
|
2694
2984
|
}
|
|
2695
|
-
|
|
2985
|
+
/** Whether the morph out of `from` cross-fades its text (default `step`: strings switch at k > 0). */
|
|
2986
|
+
morphTextCrossfades(from) {
|
|
2987
|
+
const tr = this.seq.segments[from]?.transition;
|
|
2988
|
+
return tr?.type === "morph" && tr.text === "crossfade";
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* The paint overrides for one crossfade frame: every text layer whose
|
|
2992
|
+
* words differ between the outgoing and incoming specs, painting the
|
|
2993
|
+
* outgoing words (in the lerped frame's paint — colour and alpha glide as
|
|
2994
|
+
* usual) at `1 − k` under the incoming at `k`. Null when nothing differs,
|
|
2995
|
+
* so a crossfade between same-worded twins is a plain morph frame.
|
|
2996
|
+
*/
|
|
2997
|
+
crossfadeOverrides(specA, specB, lerped, k) {
|
|
2998
|
+
let map = null;
|
|
2999
|
+
for (let i = 0; i < lerped.layers.length; i++) {
|
|
3000
|
+
if (!textStringsDiffer(specA, specB, i)) continue;
|
|
3001
|
+
const from = specA.layers[i].sprite;
|
|
3002
|
+
const now = lerped.layers[i].sprite;
|
|
3003
|
+
const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now, text: from.text } : now;
|
|
3004
|
+
if (outgoing === now) continue;
|
|
3005
|
+
(map ??= /* @__PURE__ */ new Map()).set(i, { outgoing, k });
|
|
3006
|
+
}
|
|
3007
|
+
return map;
|
|
3008
|
+
}
|
|
3009
|
+
/** Length of the fade out of segment `from`, or 0 (no fade, or a tier that plays it as cut). */
|
|
3010
|
+
fadeDur(from) {
|
|
3011
|
+
const tr = this.seq.segments[from]?.transition;
|
|
3012
|
+
return this.fadeEnabled && tr?.type === "fade" ? tr.dur : 0;
|
|
3013
|
+
}
|
|
3014
|
+
/** Sum of every timed segment — the lap length under `loop: true`. */
|
|
3015
|
+
timedTotal() {
|
|
3016
|
+
const last = this.seq.segments.length - 1;
|
|
3017
|
+
return segmentStart(this.seq, last) + (this.seq.segments[last].duration ?? 0);
|
|
3018
|
+
}
|
|
3019
|
+
releaseFading() {
|
|
3020
|
+
if (this.fading) {
|
|
3021
|
+
this.fading.child.dispose();
|
|
3022
|
+
this.fading = null;
|
|
3023
|
+
}
|
|
3024
|
+
if (this.fadingIn) {
|
|
3025
|
+
this.fadingIn.child.dispose();
|
|
3026
|
+
this.fadingIn = null;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* A segment child on a canvas of its own. Like a morph it mounts on its
|
|
3031
|
+
* chain root's scene and seed (so its entities are the ones the viewer was
|
|
3032
|
+
* watching) and hot-swaps to the segment's spec; the retained track is
|
|
3033
|
+
* applied last. Transparent over a bed, opaque otherwise.
|
|
3034
|
+
*/
|
|
3035
|
+
offscreenChild(index, root) {
|
|
3036
|
+
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(Math.max(1, this.childCtx.width), Math.max(1, this.childCtx.height));
|
|
3037
|
+
const child = new SpecInstance(this.childScene(root), { ...this.childCtx, surface: canvas }, { transparent: this.bed !== null });
|
|
3038
|
+
child.setPaused(true);
|
|
3039
|
+
if (root !== index) child.hotSwapSpec(this.childScene(index));
|
|
3040
|
+
if (this.retainedDeltas.size > 0) child.applyDeltasNow(this.retainedDeltas.values());
|
|
3041
|
+
return { index, child, canvas };
|
|
3042
|
+
}
|
|
3043
|
+
/** Render an offscreen child; a transparent one never clears, so its owner does here. */
|
|
3044
|
+
renderOffscreen(oc, t, seed) {
|
|
3045
|
+
if (this.bed) {
|
|
3046
|
+
const c = oc.canvas.getContext("2d");
|
|
3047
|
+
c?.clearRect(0, 0, this.childCtx.width, this.childCtx.height);
|
|
3048
|
+
}
|
|
3049
|
+
oc.child.renderFrame(t, seed);
|
|
3050
|
+
}
|
|
3051
|
+
/** Draw an offscreen canvas onto the shared surface at `alpha`. */
|
|
3052
|
+
composite(canvas, alpha) {
|
|
3053
|
+
const main = this.childCtx.surface?.getContext("2d");
|
|
3054
|
+
if (!main) return;
|
|
3055
|
+
main.save();
|
|
3056
|
+
main.globalAlpha = alpha;
|
|
3057
|
+
main.globalCompositeOperation = "source-over";
|
|
3058
|
+
main.drawImage(canvas, 0, 0, this.childCtx.width, this.childCtx.height);
|
|
3059
|
+
main.restore();
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Render the outgoing segment `from` of a live fade onto its own canvas. It
|
|
3063
|
+
* keeps animating at `duration(from) + localT` (it does not freeze — and if
|
|
3064
|
+
* it was an `advance: 'input'` hold it resumes from its duration, not from
|
|
3065
|
+
* wherever the hold had reached). Two live segments for `dur` only; the
|
|
3066
|
+
* outgoing one is disposed when the fade ends.
|
|
3067
|
+
*/
|
|
3068
|
+
renderOutgoing(from, localT, seed) {
|
|
3069
|
+
if (this.fading && this.fading.index !== from) {
|
|
3070
|
+
this.fading.child.dispose();
|
|
3071
|
+
this.fading = null;
|
|
3072
|
+
}
|
|
3073
|
+
if (!this.fading) this.fading = this.offscreenChild(from, this.morphChainRoot(from));
|
|
3074
|
+
const outDur = this.seq.segments[from].duration ?? 0;
|
|
3075
|
+
this.renderOffscreen(this.fading, outDur + localT, this.childSeed(this.morphChainRoot(from), seed));
|
|
3076
|
+
return this.fading;
|
|
3077
|
+
}
|
|
3078
|
+
/** A segment's scene with the retained track applied — `scene` itself when nothing resolves on it. */
|
|
3079
|
+
steeredScene(scene) {
|
|
3080
|
+
return this.retainedDeltas.size === 0 ? scene : applyRetainedDeltas(scene, this.retainedDeltas.values());
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* The child for segment `index`, created paused on the shared surface if
|
|
3084
|
+
* the slot is empty. `rootScene` (a morph chain's root) mounts the child
|
|
3085
|
+
* with the root's scene so its seed and entity placement are continuous
|
|
3086
|
+
* with the chain, then hot-swaps to the segment's own spec. Either way the
|
|
3087
|
+
* retained track is applied last, so a steer made while another segment
|
|
3088
|
+
* was up is already in effect on this child's first frame. `skipInitialPaint`
|
|
3089
|
+
* is for a caller (the morph branch) that is about to render a real frame,
|
|
3090
|
+
* paint overrides included, in this same call — see `SpecInstanceOptions`.
|
|
3091
|
+
*/
|
|
3092
|
+
ensureChild(index, rootScene, skipInitialPaint) {
|
|
2696
3093
|
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
2697
3094
|
let child = this.children[index];
|
|
2698
3095
|
if (!child) {
|
|
2699
|
-
child = new SpecInstance(this.childScene(index), this.childCtx);
|
|
3096
|
+
child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint });
|
|
2700
3097
|
this.children[index] = child;
|
|
2701
3098
|
child.setPaused(true);
|
|
3099
|
+
if (rootScene) child.hotSwapSpec(this.childScene(index));
|
|
3100
|
+
if (this.retainedDeltas.size > 0) child.applyDeltasNow(this.retainedDeltas.values());
|
|
2702
3101
|
}
|
|
2703
3102
|
return child;
|
|
2704
3103
|
}
|
|
@@ -2731,8 +3130,15 @@ var SequenceInstance = class {
|
|
|
2731
3130
|
this.renderedT = T;
|
|
2732
3131
|
const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
|
|
2733
3132
|
const { index, localT } = resolved;
|
|
3133
|
+
this.bed?.renderFrame(T, this.seq.bed?.seed ?? this.seq.seed ?? seed);
|
|
2734
3134
|
const prevIdx = index > 0 ? index - 1 : -1;
|
|
2735
3135
|
const morphActive = prevIdx >= 0 && this.canMorph(prevIdx) && localT < this.morphDur(prevIdx);
|
|
3136
|
+
const lastIdx = this.seq.segments.length - 1;
|
|
3137
|
+
const wrapped = this.seq.loop && index === 0 && lastIdx > 0 && (T + this.clockOffset >= this.timedTotal() || this.pendingWrapFade);
|
|
3138
|
+
const fadeFrom = prevIdx >= 0 ? prevIdx : wrapped ? lastIdx : -1;
|
|
3139
|
+
const fadeActive = fadeFrom >= 0 && localT < this.fadeDur(fadeFrom);
|
|
3140
|
+
if (!fadeActive && this.fading) this.releaseFading();
|
|
3141
|
+
if (this.pendingWrapFade && (index !== 0 || !fadeActive)) this.pendingWrapFade = false;
|
|
2736
3142
|
if (morphActive) {
|
|
2737
3143
|
const chainRoot = this.morphChainRoot(index);
|
|
2738
3144
|
this.morphFromIndex = prevIdx;
|
|
@@ -2740,12 +3146,14 @@ var SequenceInstance = class {
|
|
|
2740
3146
|
for (let i = 0; i < this.children.length; i++) {
|
|
2741
3147
|
if (i !== chainRoot) this.releaseChild(i);
|
|
2742
3148
|
}
|
|
2743
|
-
const child = this.ensureChild(chainRoot);
|
|
3149
|
+
const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(prevIdx));
|
|
2744
3150
|
const dur = this.morphDur(prevIdx);
|
|
2745
3151
|
const k = easeSmooth(localT / dur);
|
|
2746
|
-
const specA = this.childScene(prevIdx);
|
|
2747
|
-
const specB = this.childScene(index);
|
|
2748
|
-
|
|
3152
|
+
const specA = this.steeredScene(this.childScene(prevIdx));
|
|
3153
|
+
const specB = this.steeredScene(this.childScene(index));
|
|
3154
|
+
const lerped = lerpSpec(specA, specB, k);
|
|
3155
|
+
child.hotSwapSpec(lerped);
|
|
3156
|
+
child.setLayerPaintOverrides(this.morphTextCrossfades(prevIdx) ? this.crossfadeOverrides(specA, specB, lerped, k) : null);
|
|
2749
3157
|
child.renderFrame(localT, this.childSeed(chainRoot, seed));
|
|
2750
3158
|
} else {
|
|
2751
3159
|
if (this.morphFromIndex >= 0) {
|
|
@@ -2763,17 +3171,25 @@ var SequenceInstance = class {
|
|
|
2763
3171
|
const chainRoot = this.morphChainRoot(index);
|
|
2764
3172
|
const useMorphSeed = chainRoot < index;
|
|
2765
3173
|
const effSeed = useMorphSeed ? this.childSeed(chainRoot, seed) : this.childSeed(index, seed);
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
3174
|
+
if (fadeActive && this.bed) {
|
|
3175
|
+
const k = easeSmooth(localT / this.fadeDur(fadeFrom));
|
|
3176
|
+
if (this.fadingIn && this.fadingIn.index !== index) {
|
|
3177
|
+
this.fadingIn.child.dispose();
|
|
3178
|
+
this.fadingIn = null;
|
|
3179
|
+
}
|
|
3180
|
+
if (!this.fadingIn) this.fadingIn = this.offscreenChild(index, useMorphSeed ? chainRoot : index);
|
|
3181
|
+
this.renderOffscreen(this.fadingIn, localT, effSeed);
|
|
3182
|
+
const out = this.renderOutgoing(fadeFrom, localT, seed);
|
|
3183
|
+
this.composite(this.fadingIn.canvas, k);
|
|
3184
|
+
this.composite(out.canvas, 1 - k);
|
|
3185
|
+
return;
|
|
2775
3186
|
}
|
|
3187
|
+
const child = useMorphSeed ? this.ensureChild(index, this.childScene(chainRoot)) : this.ensureChild(index);
|
|
2776
3188
|
child.renderFrame(localT, effSeed);
|
|
3189
|
+
if (fadeActive) {
|
|
3190
|
+
const out = this.renderOutgoing(fadeFrom, localT, seed);
|
|
3191
|
+
this.composite(out.canvas, 1 - easeSmooth(localT / this.fadeDur(fadeFrom)));
|
|
3192
|
+
}
|
|
2777
3193
|
}
|
|
2778
3194
|
}
|
|
2779
3195
|
setPaused(paused) {
|
|
@@ -2783,6 +3199,9 @@ var SequenceInstance = class {
|
|
|
2783
3199
|
for (const child of this.children) {
|
|
2784
3200
|
child?.setPaused(true);
|
|
2785
3201
|
}
|
|
3202
|
+
this.fading?.child.setPaused(true);
|
|
3203
|
+
this.fadingIn?.child.setPaused(true);
|
|
3204
|
+
this.bed?.setPaused(true);
|
|
2786
3205
|
}
|
|
2787
3206
|
resize(width, height, dpr) {
|
|
2788
3207
|
this.childCtx.width = width;
|
|
@@ -2791,35 +3210,58 @@ var SequenceInstance = class {
|
|
|
2791
3210
|
for (const child of this.children) {
|
|
2792
3211
|
child?.resize(width, height, dpr);
|
|
2793
3212
|
}
|
|
3213
|
+
this.fading?.child.resize(width, height, dpr);
|
|
3214
|
+
this.fadingIn?.child.resize(width, height, dpr);
|
|
3215
|
+
this.bed?.resize(width, height, dpr);
|
|
2794
3216
|
}
|
|
2795
3217
|
applyTrack(track) {
|
|
2796
|
-
const
|
|
3218
|
+
const all = track?.deltas ?? [];
|
|
3219
|
+
const isBedPath = (d) => this.bed !== null && typeof d.path === "string" && d.path.startsWith(BED_PREFIX);
|
|
3220
|
+
const bedDeltas = all.filter(isBedPath).map((d) => ({ ...d, path: d.path.slice(BED_PREFIX.length) }));
|
|
3221
|
+
if (bedDeltas.length > 0) this.bed.applyTrack({ ...track, deltas: bedDeltas });
|
|
3222
|
+
const deltas = all.filter((d) => !isBedPath(d));
|
|
3223
|
+
for (const d of deltas) {
|
|
3224
|
+
if (d.path !== "sequence.segment" && typeof d.path === "string") this.retainedDeltas.set(d.path, d);
|
|
3225
|
+
}
|
|
2797
3226
|
const segDelta = deltas.find((d) => d.path === "sequence.segment");
|
|
3227
|
+
let switchedSegment = false;
|
|
2798
3228
|
if (segDelta !== void 0 && typeof segDelta.value === "number") {
|
|
2799
3229
|
const idx = Math.max(0, Math.min(this.seq.segments.length - 1, Math.round(segDelta.value)));
|
|
3230
|
+
this.pendingWrapFade = this.seq.loop && idx === 0 && this.seq.segments.length > 1;
|
|
2800
3231
|
this.clockOffset = segmentStart(this.seq, idx) - this.renderedT;
|
|
2801
3232
|
this.releasedBelow = idx;
|
|
2802
3233
|
this.renderFrame(this.renderedT, this.seed);
|
|
3234
|
+
switchedSegment = true;
|
|
2803
3235
|
}
|
|
2804
3236
|
const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
|
|
2805
|
-
if (childDeltas.length > 0
|
|
2806
|
-
|
|
2807
|
-
|
|
3237
|
+
if (childDeltas.length > 0) {
|
|
3238
|
+
if (this.morphFromIndex >= 0) {
|
|
3239
|
+
if (!switchedSegment) this.renderFrame(this.renderedT, this.seed);
|
|
3240
|
+
} else if (this.activeIndex >= 0) {
|
|
3241
|
+
const child = this.children[this.activeIndex];
|
|
3242
|
+
child?.applyTrack({ ...track, deltas: childDeltas });
|
|
3243
|
+
}
|
|
3244
|
+
this.fading?.child.applyTrack({ ...track, deltas: childDeltas });
|
|
3245
|
+
this.fadingIn?.child.applyTrack({ ...track, deltas: childDeltas });
|
|
2808
3246
|
}
|
|
2809
3247
|
}
|
|
2810
3248
|
dispose() {
|
|
2811
3249
|
this.stop();
|
|
3250
|
+
this.releaseFading();
|
|
2812
3251
|
for (let i = 0; i < this.children.length; i++) {
|
|
2813
3252
|
this.releaseChild(i);
|
|
2814
3253
|
}
|
|
3254
|
+
this.bed?.dispose();
|
|
2815
3255
|
if (this.canvas) this.canvas.remove();
|
|
2816
3256
|
}
|
|
2817
3257
|
};
|
|
2818
3258
|
function sequenceManifest(seq) {
|
|
2819
|
-
const
|
|
3259
|
+
const maxSegment = seq.segments.reduce((max, s) => {
|
|
2820
3260
|
const t = s.scene.layers.reduce((n, l) => n + l.count, 0);
|
|
2821
3261
|
return Math.max(max, t);
|
|
2822
3262
|
}, 0);
|
|
3263
|
+
const bedTotal = seq.bed ? seq.bed.layers.reduce((n, l) => n + l.count, 0) : 0;
|
|
3264
|
+
const maxTotal = bedTotal + maxSegment;
|
|
2823
3265
|
const costTier = maxTotal < 30 ? "idle" : maxTotal < 150 ? "low" : maxTotal < 400 ? "medium" : "high";
|
|
2824
3266
|
return {
|
|
2825
3267
|
id: seq.id,
|
|
@@ -2903,6 +3345,58 @@ function colourSeparation(layer, sprite, bg) {
|
|
|
2903
3345
|
const db = sprite.rgb.b - bg.rgb.b;
|
|
2904
3346
|
return Math.sqrt((dr * dr + dg * dg + db * db) / 3);
|
|
2905
3347
|
}
|
|
3348
|
+
function srgbToLinear(c) {
|
|
3349
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
3350
|
+
}
|
|
3351
|
+
function relativeLuminance(rgb) {
|
|
3352
|
+
return 0.2126 * srgbToLinear(rgb.r) + 0.7152 * srgbToLinear(rgb.g) + 0.0722 * srgbToLinear(rgb.b);
|
|
3353
|
+
}
|
|
3354
|
+
function legibilityRatio(a, b) {
|
|
3355
|
+
const la = relativeLuminance(a);
|
|
3356
|
+
const lb = relativeLuminance(b);
|
|
3357
|
+
const hi = Math.max(la, lb);
|
|
3358
|
+
const lo = Math.min(la, lb);
|
|
3359
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
3360
|
+
}
|
|
3361
|
+
function backgroundRgbAt(spec, yPx, h, unit) {
|
|
3362
|
+
const bg = spec.background;
|
|
3363
|
+
if (!bg || bg.type === "solid") return hexRgb(bg?.color ?? "#05050a");
|
|
3364
|
+
if (bg.band) {
|
|
3365
|
+
const bh = bg.band.height * unit;
|
|
3366
|
+
if (yPx >= h - bh) return hexRgb(bg.band.color);
|
|
3367
|
+
}
|
|
3368
|
+
const stops = [...bg.stops].sort((a, b) => a.at - b.at);
|
|
3369
|
+
const y = h > 0 ? Math.max(0, Math.min(1, yPx / h)) : 0;
|
|
3370
|
+
const first = stops[0];
|
|
3371
|
+
const last = stops[stops.length - 1];
|
|
3372
|
+
if (y <= first.at) return hexRgb(first.color);
|
|
3373
|
+
if (y >= last.at) return hexRgb(last.color);
|
|
3374
|
+
for (let i = 1; i < stops.length; i++) {
|
|
3375
|
+
const a = stops[i - 1];
|
|
3376
|
+
const b = stops[i];
|
|
3377
|
+
if (y > b.at) continue;
|
|
3378
|
+
const span = b.at - a.at;
|
|
3379
|
+
const k = span > 0 ? (y - a.at) / span : 1;
|
|
3380
|
+
const ca = hexRgb(a.color);
|
|
3381
|
+
const cb = hexRgb(b.color);
|
|
3382
|
+
return { r: ca.r + (cb.r - ca.r) * k, g: ca.g + (cb.g - ca.g) * k, b: ca.b + (cb.b - ca.b) * k };
|
|
3383
|
+
}
|
|
3384
|
+
return hexRgb(last.color);
|
|
3385
|
+
}
|
|
3386
|
+
function sourceOverPlate(ground, colour, alpha) {
|
|
3387
|
+
return {
|
|
3388
|
+
r: ground.r + (colour.r - ground.r) * alpha,
|
|
3389
|
+
g: ground.g + (colour.g - ground.g) * alpha,
|
|
3390
|
+
b: ground.b + (colour.b - ground.b) * alpha
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
function additivePlate(ground, colour, alpha, blend) {
|
|
3394
|
+
const mix = (g, c) => {
|
|
3395
|
+
const v = c * alpha;
|
|
3396
|
+
return blend === "lighter" ? Math.min(1, g + v) : 1 - (1 - g) * (1 - v);
|
|
3397
|
+
};
|
|
3398
|
+
return { r: mix(ground.r, colour.r), g: mix(ground.g, colour.g), b: mix(ground.b, colour.b) };
|
|
3399
|
+
}
|
|
2906
3400
|
|
|
2907
3401
|
// src/cohesion.ts
|
|
2908
3402
|
var MERGE_FLOOR = 0.35;
|
|
@@ -3114,6 +3608,8 @@ function seamsWorthWarning(layer, c, entityCount) {
|
|
|
3114
3608
|
// src/advise.ts
|
|
3115
3609
|
var LOW_CONTRAST_FLOOR = 0.05;
|
|
3116
3610
|
var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
3611
|
+
var READ_LEGIBILITY_FLOOR = 4.5;
|
|
3612
|
+
var READ_SAFE_AREA = 0.05;
|
|
3117
3613
|
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
3118
3614
|
const warnings = [];
|
|
3119
3615
|
const w = viewport.width;
|
|
@@ -3218,8 +3714,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
3218
3714
|
const fsPx = layer.sprite.fontSize * scale;
|
|
3219
3715
|
const lh = (layer.sprite.lineHeight ?? 1.4) * fsPx;
|
|
3220
3716
|
const maxWPx = layer.sprite.maxWidth * scale;
|
|
3221
|
-
const lines = breakTextBlock(layer.sprite.text, maxWPx / fsPx);
|
|
3222
|
-
pixArea = maxWPx * lines.length * lh * 0.55;
|
|
3717
|
+
const lines = breakTextBlock(layer.sprite.text, maxWPx / fsPx, textMetricsClassFor(layer.sprite.font));
|
|
3718
|
+
pixArea = maxWPx * lines.length * lh * 0.55 * (layer.sprite.opacity ?? 1);
|
|
3223
3719
|
} else {
|
|
3224
3720
|
pixArea = e.size * e.size;
|
|
3225
3721
|
}
|
|
@@ -3339,9 +3835,16 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
3339
3835
|
for (const e of allEntities[li]) {
|
|
3340
3836
|
const p = positionAt(e, 0, w, h);
|
|
3341
3837
|
const box = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
3342
|
-
|
|
3838
|
+
const alpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
3839
|
+
textBoxes.push({ li, label, alpha, ...box });
|
|
3343
3840
|
}
|
|
3344
3841
|
}
|
|
3842
|
+
const asFraction = (b) => ({
|
|
3843
|
+
x: +(b.x0 / w).toFixed(4),
|
|
3844
|
+
y: +(b.y0 / h).toFixed(4),
|
|
3845
|
+
w: +((b.x1 - b.x0) / w).toFixed(4),
|
|
3846
|
+
h: +((b.y1 - b.y0) / h).toFixed(4)
|
|
3847
|
+
});
|
|
3345
3848
|
const tolX = w * 0.01;
|
|
3346
3849
|
const tolY = h * 0.01;
|
|
3347
3850
|
const offFlagged = /* @__PURE__ */ new Set();
|
|
@@ -3380,12 +3883,109 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
3380
3883
|
warnings.push({
|
|
3381
3884
|
path: `layers[${first.li}]`,
|
|
3382
3885
|
code: "text-overlap",
|
|
3383
|
-
message: `${first.label} and ${second.label} text boxes overlap (~${Math.round(share * 100)}% of the smaller one) \u2014 they will paint over each other; move one or shrink it
|
|
3886
|
+
message: `${first.label} and ${second.label} text boxes overlap (~${Math.round(share * 100)}% of the smaller one) \u2014 they will paint over each other; move one or shrink it`,
|
|
3887
|
+
// The two boxes, so an agent can move one without re-deriving the
|
|
3888
|
+
// layout. Additive: the message is unchanged.
|
|
3889
|
+
boxes: [asFraction(first), asFraction(second)]
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
const readFlagged = /* @__PURE__ */ new Set();
|
|
3894
|
+
for (const b of textBoxes) {
|
|
3895
|
+
if (readFlagged.has(b.li)) continue;
|
|
3896
|
+
const layer = spec.layers[b.li];
|
|
3897
|
+
const s = layer.sprite;
|
|
3898
|
+
if (s.kind !== "text" && s.kind !== "textBlock" || s.role !== "read") continue;
|
|
3899
|
+
const boxes = textBoxes.filter((x) => x.li === b.li);
|
|
3900
|
+
readFlagged.add(b.li);
|
|
3901
|
+
const ink = hexRgb(s.color ?? "#e6e8ef");
|
|
3902
|
+
const textPlate = (base, alpha) => {
|
|
3903
|
+
if (layer.blend === "lighter" || layer.blend === "screen") return additivePlate(base, ink, alpha, layer.blend);
|
|
3904
|
+
if (layer.blend === "multiply") {
|
|
3905
|
+
return {
|
|
3906
|
+
r: base.r * (1 - alpha + ink.r * alpha),
|
|
3907
|
+
g: base.g * (1 - alpha + ink.g * alpha),
|
|
3908
|
+
b: base.b * (1 - alpha + ink.b * alpha)
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
return sourceOverPlate(base, ink, alpha);
|
|
3912
|
+
};
|
|
3913
|
+
let worst = null;
|
|
3914
|
+
for (const box of boxes) {
|
|
3915
|
+
const cy = (box.y0 + box.y1) / 2;
|
|
3916
|
+
const groundRgb = backgroundRgbAt(spec, cy, h, scale);
|
|
3917
|
+
const ground = legibilityRatio(textPlate(groundRgb, box.alpha), groundRgb);
|
|
3918
|
+
const lit = brightestAdditivePlate(spec, allEntities, groundRgb, box, w, h);
|
|
3919
|
+
const plate = lit ? legibilityRatio(textPlate(lit.rgb, box.alpha), lit.rgb) : Infinity;
|
|
3920
|
+
if (!worst || Math.min(ground, plate) < Math.min(worst.ground, worst.plate)) {
|
|
3921
|
+
worst = { ground, plate, plateLabel: lit?.label ?? null };
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
if (worst && Math.min(worst.ground, worst.plate) < READ_LEGIBILITY_FLOOR) {
|
|
3925
|
+
const under = worst.plateLabel ? ` and ${worst.plate.toFixed(1)}:1 under ${worst.plateLabel} at its brightest` : "";
|
|
3926
|
+
warnings.push({
|
|
3927
|
+
path: `layers[${b.li}].sprite`,
|
|
3928
|
+
code: "text-legibility",
|
|
3929
|
+
message: `${b.label} is declared role: 'read' but its colour reads at ${worst.ground.toFixed(1)}:1 against the background at the box centre${under} \u2014 below ${READ_LEGIBILITY_FLOOR}:1; brighten the text, darken the ground under it, or declare role: 'atmosphere' if it is texture`
|
|
3930
|
+
});
|
|
3931
|
+
}
|
|
3932
|
+
const edges = [];
|
|
3933
|
+
const x0 = Math.min(...boxes.map((x) => x.x0));
|
|
3934
|
+
const x1 = Math.max(...boxes.map((x) => x.x1));
|
|
3935
|
+
const y0 = Math.min(...boxes.map((x) => x.y0));
|
|
3936
|
+
const y1 = Math.max(...boxes.map((x) => x.y1));
|
|
3937
|
+
if (x0 < w * READ_SAFE_AREA) edges.push("left");
|
|
3938
|
+
if (x1 > w * (1 - READ_SAFE_AREA)) edges.push("right");
|
|
3939
|
+
if (y0 < h * READ_SAFE_AREA) edges.push("top");
|
|
3940
|
+
if (y1 > h * (1 - READ_SAFE_AREA)) edges.push("bottom");
|
|
3941
|
+
if (edges.length > 0) {
|
|
3942
|
+
warnings.push({
|
|
3943
|
+
path: `layers[${b.li}]`,
|
|
3944
|
+
code: "text-safe-area",
|
|
3945
|
+
message: `${b.label} is declared role: 'read' but its box is within ${READ_SAFE_AREA * 100}% of the ${edges.join(" and ")} edge${edges.length === 1 ? "" : "s"} at ${w}\xD7${h} \u2014 displays crop and bezels hide that zone; move it inward`,
|
|
3946
|
+
boxes: [asFraction({ x0, y0, x1, y1 })]
|
|
3384
3947
|
});
|
|
3385
3948
|
}
|
|
3386
3949
|
}
|
|
3387
3950
|
return warnings;
|
|
3388
3951
|
}
|
|
3952
|
+
function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
3953
|
+
let best = null;
|
|
3954
|
+
for (let lj = 0; lj < spec.layers.length; lj++) {
|
|
3955
|
+
const layer = spec.layers[lj];
|
|
3956
|
+
const blend = layer.blend;
|
|
3957
|
+
if (blend !== "lighter" && blend !== "screen") continue;
|
|
3958
|
+
if (lj === box.li) continue;
|
|
3959
|
+
const entities = allEntities[lj];
|
|
3960
|
+
for (const e of entities) {
|
|
3961
|
+
const hex = spriteHex(layer, e);
|
|
3962
|
+
if (hex === null) continue;
|
|
3963
|
+
const growScale = (e.motion === "warp" ? WARP_MAX_SCALE : 1) * (e.emit ? Math.max(e.emit.growFrom, e.emit.growTo) : 1);
|
|
3964
|
+
const maxDim = Math.max(e.size, e.size2 ?? 0) * (1 + e.growAmp) * growScale;
|
|
3965
|
+
if (maxDim < box.fs) continue;
|
|
3966
|
+
if (!entityReachesBox(e, maxDim / 2, box, w, h)) continue;
|
|
3967
|
+
const a = Math.min(1, e.alpha + e.pulseAmp);
|
|
3968
|
+
if (a <= 0) continue;
|
|
3969
|
+
const plate = additivePlate(ground, hexRgb(hex), a, blend);
|
|
3970
|
+
const lum = relativeLuminance(plate);
|
|
3971
|
+
if (!best || lum > best.lum) {
|
|
3972
|
+
best = { rgb: plate, label: layer.key ? `\`${layer.key}\`` : `layers[${lj}]`, lum };
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
return best ? { rgb: best.rgb, label: best.label } : null;
|
|
3977
|
+
}
|
|
3978
|
+
function entityReachesBox(e, r, box, w, h) {
|
|
3979
|
+
if (e.motion === "static") {
|
|
3980
|
+
const p = positionAt(e, 0, w, h);
|
|
3981
|
+
return p.x + r > box.x0 && p.x - r < box.x1 && p.y + r > box.y0 && p.y - r < box.y1;
|
|
3982
|
+
}
|
|
3983
|
+
if (e.motion === "orbit" && !e.orbitParent) {
|
|
3984
|
+
const span = e.orbitR + r;
|
|
3985
|
+
return e.orbitCx + span > box.x0 && e.orbitCx - span < box.x1 && e.orbitCy + span > box.y0 && e.orbitCy - span < box.y1;
|
|
3986
|
+
}
|
|
3987
|
+
return true;
|
|
3988
|
+
}
|
|
3389
3989
|
function textBoxAt(s, e, p, spec, w, h) {
|
|
3390
3990
|
const m = s.font ? /(\d{1,5}(?:\.\d{1,4})?|\.\d{1,4})px/.exec(s.font) : null;
|
|
3391
3991
|
const fontScale = spec.units === "px" ? 1 : Math.min(w, h) / (spec.referenceViewport ?? LIMITS.referenceViewport);
|
|
@@ -3397,28 +3997,39 @@ function textBoxAt(s, e, p, spec, w, h) {
|
|
|
3397
3997
|
const baseline = s.baseline ?? "middle";
|
|
3398
3998
|
const x0 = align === "left" ? p.x : align === "right" ? p.x - fw : p.x - fw / 2;
|
|
3399
3999
|
const y0 = baseline === "top" ? p.y : baseline === "bottom" ? p.y - fh : p.y - fh / 2;
|
|
3400
|
-
return { x0, y0, x1: x0 + fw, y1: y0 + fh };
|
|
4000
|
+
return { x0, y0, x1: x0 + fw, y1: y0 + fh, fs: fh };
|
|
3401
4001
|
}
|
|
3402
4002
|
function textBlockBoxAt(s, p, w, h) {
|
|
3403
4003
|
const unit = Math.min(w, h);
|
|
3404
4004
|
const fsPx = s.fontSize * unit;
|
|
3405
4005
|
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
3406
4006
|
const maxWPx = s.maxWidth * unit;
|
|
3407
|
-
const lines = breakTextBlock(s.text, maxWPx / fsPx);
|
|
4007
|
+
const lines = breakTextBlock(s.text, maxWPx / fsPx, textMetricsClassFor(s.font));
|
|
3408
4008
|
const maxLineW = lines.reduce((mx, l) => Math.max(mx, l.widthEm), 0) * fsPx;
|
|
3409
4009
|
const totalH = lines.length * lh;
|
|
3410
4010
|
const align = s.align ?? "left";
|
|
3411
4011
|
const x0 = align === "center" ? p.x + (maxWPx - maxLineW) / 2 : align === "right" ? p.x + maxWPx - maxLineW : p.x;
|
|
3412
|
-
|
|
4012
|
+
const { dx, dy } = textBlockAnchorOffset(s, maxWPx, maxLineW, totalH);
|
|
4013
|
+
return { x0: x0 + dx, y0: p.y + dy, x1: x0 + dx + maxLineW, y1: p.y + dy + totalH, fs: fsPx };
|
|
3413
4014
|
}
|
|
3414
4015
|
function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
3415
4016
|
const warnings = [];
|
|
3416
4017
|
for (let i = 0; i < seq.segments.length; i++) {
|
|
3417
4018
|
const segWarnings = adviseSpec(seq.segments[i].scene, viewport);
|
|
3418
4019
|
for (const w of segWarnings) {
|
|
3419
|
-
warnings.push({ path: `segments[${i}].scene.${w.path}`, code: w.code, message: w.message });
|
|
4020
|
+
warnings.push({ path: `segments[${i}].scene.${w.path}`, code: w.code, message: w.message, boxes: w.boxes });
|
|
3420
4021
|
}
|
|
3421
4022
|
}
|
|
4023
|
+
const fadeBound = seq.loop ? seq.segments.length : seq.segments.length - 1;
|
|
4024
|
+
const fades = seq.segments.slice(0, fadeBound).filter((s) => s.transition?.type === "fade").length;
|
|
4025
|
+
if (fades > 0) {
|
|
4026
|
+
const first = seq.segments.slice(0, fadeBound).findIndex((s) => s.transition?.type === "fade");
|
|
4027
|
+
warnings.push({
|
|
4028
|
+
path: `segments[${first}].transition`,
|
|
4029
|
+
code: "fade-degrades-on-low-tier",
|
|
4030
|
+
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`
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
3422
4033
|
for (let i = 0; i < seq.segments.length - 1; i++) {
|
|
3423
4034
|
const lumaA = backgroundLuma(seq.segments[i].scene);
|
|
3424
4035
|
const lumaB = backgroundLuma(seq.segments[i + 1].scene);
|
|
@@ -3440,6 +4051,12 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
3440
4051
|
code: "morph-structural-mismatch",
|
|
3441
4052
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
3442
4053
|
});
|
|
4054
|
+
} else if (morphNothingMorphable(seq.segments[i].scene, seq.segments[i + 1].scene, { textCrossfade: tr.text === "crossfade" })) {
|
|
4055
|
+
warnings.push({
|
|
4056
|
+
path: `segments[${i}].transition`,
|
|
4057
|
+
code: "morph-nothing-morphable",
|
|
4058
|
+
message: `segments ${i}\u2192${i + 1} differ only in values morph cannot interpolate (strings such as textBlock.text step on the first frame): the morph will look like a cut \u2014 fade text via colour or reveal.progress`
|
|
4059
|
+
});
|
|
3443
4060
|
}
|
|
3444
4061
|
}
|
|
3445
4062
|
}
|
|
@@ -3592,14 +4209,15 @@ function textBlockBox(s, _e, p, w, h) {
|
|
|
3592
4209
|
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
3593
4210
|
const maxWPx = s.maxWidth * unitScale;
|
|
3594
4211
|
const maxWEm = maxWPx / fsPx;
|
|
3595
|
-
const lines = breakTextBlock(s.text, maxWEm);
|
|
4212
|
+
const lines = breakTextBlock(s.text, maxWEm, textMetricsClassFor(s.font));
|
|
3596
4213
|
const totalH = lines.length * lh;
|
|
3597
4214
|
const maxLineW = lines.reduce((m, l) => Math.max(m, l.widthEm), 0) * fsPx;
|
|
3598
4215
|
const align = s.align ?? "left";
|
|
3599
4216
|
const cx = align === "center" ? p.x + maxWPx / 2 : align === "right" ? p.x + maxWPx - maxLineW / 2 : p.x + maxLineW / 2;
|
|
4217
|
+
const { dx, dy } = textBlockAnchorOffset(s, maxWPx, maxLineW, totalH);
|
|
3600
4218
|
return {
|
|
3601
|
-
cx,
|
|
3602
|
-
cy: p.y + totalH / 2,
|
|
4219
|
+
cx: cx + dx,
|
|
4220
|
+
cy: p.y + totalH / 2 + dy,
|
|
3603
4221
|
halfX: maxLineW / 2,
|
|
3604
4222
|
halfY: totalH / 2
|
|
3605
4223
|
};
|
|
@@ -3608,7 +4226,7 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
3608
4226
|
if (!s.reveal) return 1;
|
|
3609
4227
|
const unitScale = Math.min(w, h);
|
|
3610
4228
|
const maxWEm = s.maxWidth * unitScale / (s.fontSize * unitScale);
|
|
3611
|
-
const lines = breakTextBlock(s.text, maxWEm);
|
|
4229
|
+
const lines = breakTextBlock(s.text, maxWEm, textMetricsClassFor(s.font));
|
|
3612
4230
|
const rs = revealState(lines, s.reveal, t);
|
|
3613
4231
|
if (rs.glyphAlphas) {
|
|
3614
4232
|
let sum = 0;
|
|
@@ -3802,7 +4420,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3802
4420
|
const c1 = Math.min(cols - 1, Math.floor((centerX + reachX) / cellW));
|
|
3803
4421
|
const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
|
|
3804
4422
|
const r1 = Math.min(rows - 1, Math.floor((centerY + reachY) / cellH));
|
|
3805
|
-
const inkWeight = (s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1) * (s.kind === "textBlock" ? textBlockRevealFraction(s, w, h, t) : 1);
|
|
4423
|
+
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);
|
|
3806
4424
|
for (let r = r0; r <= r1; r++) {
|
|
3807
4425
|
for (let c = c0; c <= c1; c++) {
|
|
3808
4426
|
const dx = (c + 0.5) * cellW - centerX;
|
|
@@ -3871,6 +4489,9 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3871
4489
|
}
|
|
3872
4490
|
}
|
|
3873
4491
|
splatPass(t, 1);
|
|
4492
|
+
return finishGrid(cols, rows, cells, bgRow);
|
|
4493
|
+
}
|
|
4494
|
+
function finishGrid(cols, rows, cells, bgRow) {
|
|
3874
4495
|
let sum = 0;
|
|
3875
4496
|
let covered = 0;
|
|
3876
4497
|
let devSum = 0;
|
|
@@ -3980,6 +4601,21 @@ function textSprites(spec, opts = {}) {
|
|
|
3980
4601
|
return out;
|
|
3981
4602
|
}
|
|
3982
4603
|
function dominanceRanking(spec, opts = {}) {
|
|
4604
|
+
return rankDominance(rawDominance(spec, opts));
|
|
4605
|
+
}
|
|
4606
|
+
function rankDominance(raw) {
|
|
4607
|
+
const total = raw.reduce((s, r) => s + r.weight, 0) || 1;
|
|
4608
|
+
return raw.slice().sort((a, b) => b.weight - a.weight).map((r, i) => ({
|
|
4609
|
+
rank: i + 1,
|
|
4610
|
+
layerIndex: r.layerIndex,
|
|
4611
|
+
key: r.key,
|
|
4612
|
+
share: r.weight / total,
|
|
4613
|
+
entityCount: r.entityCount,
|
|
4614
|
+
meanLuma: r.meanLuma,
|
|
4615
|
+
factors: r.factors
|
|
4616
|
+
}));
|
|
4617
|
+
}
|
|
4618
|
+
function rawDominance(spec, opts = {}) {
|
|
3983
4619
|
const scene = buildScene(spec, opts);
|
|
3984
4620
|
const t = opts.t ?? 5e3;
|
|
3985
4621
|
const { w, h, scale } = scene;
|
|
@@ -4031,7 +4667,7 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
4031
4667
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
4032
4668
|
} else if (s.kind === "textBlock") {
|
|
4033
4669
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
4034
|
-
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t);
|
|
4670
|
+
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
|
|
4035
4671
|
} else entArea = sz * sz * 0.55;
|
|
4036
4672
|
area += entArea * a;
|
|
4037
4673
|
if (layer.trail) {
|
|
@@ -4060,16 +4696,7 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
4060
4696
|
const weight = area / (w * h) * contrast * blendBoost * motionBoost;
|
|
4061
4697
|
return { layerIndex, key: layer.key, weight, entityCount: entities.length, meanLuma, factors: { area: area / (w * h), contrast, blendBoost, motionBoost } };
|
|
4062
4698
|
});
|
|
4063
|
-
|
|
4064
|
-
return raw.slice().sort((a, b) => b.weight - a.weight).map((r, i) => ({
|
|
4065
|
-
rank: i + 1,
|
|
4066
|
-
layerIndex: r.layerIndex,
|
|
4067
|
-
key: r.key,
|
|
4068
|
-
share: r.weight / total,
|
|
4069
|
-
entityCount: r.entityCount,
|
|
4070
|
-
meanLuma: r.meanLuma,
|
|
4071
|
-
factors: r.factors
|
|
4072
|
-
}));
|
|
4699
|
+
return raw;
|
|
4073
4700
|
}
|
|
4074
4701
|
function layerCohesion(spec, opts = {}) {
|
|
4075
4702
|
const scene = buildScene(spec, opts);
|
|
@@ -4176,6 +4803,51 @@ function perceiveScene(spec, opts = {}) {
|
|
|
4176
4803
|
advisories: adviseSpec(spec, opts.viewport ?? { width: 1920, height: 1080 }, { t: opts.t, seed: opts.seed })
|
|
4177
4804
|
};
|
|
4178
4805
|
}
|
|
4806
|
+
var bedKey = (key, layerIndex) => `bed:${key ?? layerIndex}`;
|
|
4807
|
+
function perceiveSequenceFrame(seq, T, opts = {}) {
|
|
4808
|
+
const { releasedBelow, ...gridOpts } = opts;
|
|
4809
|
+
const r = resolveSegment(seq, T, { releasedBelow });
|
|
4810
|
+
const seg = seq.segments[r.index];
|
|
4811
|
+
const segment = { index: r.index, key: seg.key, localT: r.localT, ...r.held ? { held: true } : {} };
|
|
4812
|
+
const segOpts = { ...gridOpts, t: r.localT, seed: gridOpts.seed ?? segmentRenderSeed(seq, r.index) };
|
|
4813
|
+
if (!seq.bed) return { ...perceiveScene(seg.scene, segOpts), segment, bed: false };
|
|
4814
|
+
const bedOpts = { ...gridOpts, t: T, seed: gridOpts.seed ?? bedRenderSeed(seq) };
|
|
4815
|
+
const viewport = gridOpts.viewport ?? { width: 1920, height: 1080 };
|
|
4816
|
+
const ink = { ...seg.scene, background: { type: "solid", color: "#000000" } };
|
|
4817
|
+
delete ink.ghosting;
|
|
4818
|
+
const inkOverBed = { ...ink, background: seq.bed.background ?? { type: "solid", color: "#05050a" } };
|
|
4819
|
+
const bedGrid = luminanceGrid(seq.bed, bedOpts);
|
|
4820
|
+
const inkGrid = luminanceGrid(ink, segOpts);
|
|
4821
|
+
const cells = bedGrid.cells.map((v, i) => Math.min(1, v + inkGrid.cells[i]));
|
|
4822
|
+
const grid = finishGrid(bedGrid.cols, bedGrid.rows, cells, bedGrid.background);
|
|
4823
|
+
const bedLayers = seq.bed.layers.length;
|
|
4824
|
+
const dominance = rankDominance([
|
|
4825
|
+
...rawDominance(seq.bed, bedOpts).map((d) => ({ ...d, key: bedKey(d.key, d.layerIndex) })),
|
|
4826
|
+
...rawDominance(inkOverBed, segOpts).map((d) => ({ ...d, layerIndex: d.layerIndex + bedLayers }))
|
|
4827
|
+
]);
|
|
4828
|
+
const shift = (x) => ({ ...x, layerIndex: x.layerIndex + bedLayers });
|
|
4829
|
+
const prefix = (x) => ({ ...x, key: bedKey(x.key, x.layerIndex) });
|
|
4830
|
+
return {
|
|
4831
|
+
t: r.localT,
|
|
4832
|
+
braille: renderBrailleMap(grid),
|
|
4833
|
+
density: renderDensityMap(grid),
|
|
4834
|
+
coverage: grid.coverage,
|
|
4835
|
+
meanLuminance: grid.meanLuminance,
|
|
4836
|
+
centroid: grid.centroid,
|
|
4837
|
+
rowProfile: grid.rowProfile,
|
|
4838
|
+
colProfile: grid.colProfile,
|
|
4839
|
+
dominance,
|
|
4840
|
+
motion: [...motionStats(seq.bed, bedOpts).map(prefix), ...motionStats(ink, segOpts).map(shift)],
|
|
4841
|
+
form: [...layerCohesion(seq.bed, bedOpts).map(prefix), ...layerCohesion(ink, segOpts).map(shift)],
|
|
4842
|
+
text: [...textSprites(seq.bed, bedOpts).map(prefix), ...textSprites(ink, segOpts).map(shift)],
|
|
4843
|
+
advisories: [
|
|
4844
|
+
...adviseSpec(seq.bed, viewport, { t: T, seed: bedOpts.seed }).map((w) => ({ ...w, path: `bed.${w.path}` })),
|
|
4845
|
+
...adviseSpec(inkOverBed, viewport, { t: r.localT, seed: segOpts.seed })
|
|
4846
|
+
],
|
|
4847
|
+
segment,
|
|
4848
|
+
bed: true
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4179
4851
|
|
|
4180
4852
|
// src/examples/aquarium.ts
|
|
4181
4853
|
var AQUARIUM_SPEC = {
|
|
@@ -5065,6 +5737,85 @@ var LANTERNS_SPEC = {
|
|
|
5065
5737
|
]
|
|
5066
5738
|
};
|
|
5067
5739
|
|
|
5740
|
+
// src/examples/lobby-talk.ts
|
|
5741
|
+
var LOBBY_TALK_SPEC = {
|
|
5742
|
+
schemaVersion: 1,
|
|
5743
|
+
id: "lobby-talk",
|
|
5744
|
+
label: "Lobby Talk",
|
|
5745
|
+
seed: 1207,
|
|
5746
|
+
motionIntensity: "calm",
|
|
5747
|
+
background: {
|
|
5748
|
+
type: "gradient",
|
|
5749
|
+
stops: [
|
|
5750
|
+
{ at: 0, color: "#060912" },
|
|
5751
|
+
{ at: 1, color: "#0b1224" }
|
|
5752
|
+
],
|
|
5753
|
+
drift: { period: 4e4, amount: 0.1 }
|
|
5754
|
+
},
|
|
5755
|
+
layers: [
|
|
5756
|
+
{
|
|
5757
|
+
key: "haze",
|
|
5758
|
+
count: 3,
|
|
5759
|
+
sprite: { kind: "circle", radius: [0.22, 0.3], soft: true, color: "#123a4a", colors: ["#123a4a", "#2a1e4a", "#0e2a3a"] },
|
|
5760
|
+
alpha: [0.35, 0.5],
|
|
5761
|
+
blend: "lighter",
|
|
5762
|
+
region: { x: [0.2, 0.8], y: [0.3, 0.7] },
|
|
5763
|
+
motion: { type: "wander", speed: [2e-3, 4e-3], meander: 0.05 },
|
|
5764
|
+
pulse: { amp: 0.15, period: 9e3 }
|
|
5765
|
+
},
|
|
5766
|
+
{
|
|
5767
|
+
key: "dust",
|
|
5768
|
+
count: 50,
|
|
5769
|
+
sprite: { kind: "circle", radius: [8e-4, 18e-4], soft: true, color: "#7fb8c9" },
|
|
5770
|
+
alpha: [0.2, 0.5],
|
|
5771
|
+
blend: "lighter",
|
|
5772
|
+
motion: { type: "wander", speed: [1e-3, 3e-3] }
|
|
5773
|
+
},
|
|
5774
|
+
{
|
|
5775
|
+
key: "title",
|
|
5776
|
+
count: 1,
|
|
5777
|
+
sprite: { kind: "textBlock", text: "Adoption by feature, % of accounts", maxWidth: 1, fontSize: 0.034, color: "#f2f4f8", role: "read" },
|
|
5778
|
+
motion: { type: "static" },
|
|
5779
|
+
position: { x: 0.12, y: 0.16 }
|
|
5780
|
+
},
|
|
5781
|
+
{
|
|
5782
|
+
key: "labels",
|
|
5783
|
+
count: 5,
|
|
5784
|
+
sprite: { kind: "text", strings: ["Onboarding", "Search", "Billing", "Exports", "Mobile"], font: "system-ui", align: "left", baseline: "middle", color: "#a9b7cc", role: "read" },
|
|
5785
|
+
size: [0.03, 0.03],
|
|
5786
|
+
motion: { type: "static" },
|
|
5787
|
+
position: { x: 0.12, y: 0.32 },
|
|
5788
|
+
layout: { type: "list", gap: 0.085 }
|
|
5789
|
+
},
|
|
5790
|
+
{
|
|
5791
|
+
key: "bars",
|
|
5792
|
+
count: 5,
|
|
5793
|
+
sprite: {
|
|
5794
|
+
kind: "bar",
|
|
5795
|
+
values: [82, 64, 91, 37, 58],
|
|
5796
|
+
max: 100,
|
|
5797
|
+
length: 0.42,
|
|
5798
|
+
thickness: 0.026,
|
|
5799
|
+
color: "#7fe0d2",
|
|
5800
|
+
colors: ["#7fe0d2", "#7fe0d2", "#ffcf7a", "#7fe0d2", "#7fe0d2"]
|
|
5801
|
+
},
|
|
5802
|
+
alpha: [0.9, 0.9],
|
|
5803
|
+
motion: { type: "static" },
|
|
5804
|
+
position: { x: 0.33, y: 0.32 },
|
|
5805
|
+
layout: { type: "list", gap: 0.085 }
|
|
5806
|
+
},
|
|
5807
|
+
{
|
|
5808
|
+
key: "readouts",
|
|
5809
|
+
count: 5,
|
|
5810
|
+
sprite: { kind: "text", strings: ["82%", "64%", "91%", "37%", "58%"], font: "bold system-ui", align: "left", baseline: "middle", color: "#f2f4f8", role: "read" },
|
|
5811
|
+
size: [0.03, 0.03],
|
|
5812
|
+
motion: { type: "static" },
|
|
5813
|
+
position: { x: 0.78, y: 0.32 },
|
|
5814
|
+
layout: { type: "list", gap: 0.085 }
|
|
5815
|
+
}
|
|
5816
|
+
]
|
|
5817
|
+
};
|
|
5818
|
+
|
|
5068
5819
|
// src/examples/matrix-rain.ts
|
|
5069
5820
|
var MATRIX_RAIN_SPEC = {
|
|
5070
5821
|
schemaVersion: 1,
|
|
@@ -5789,7 +6540,8 @@ var SCHEMA_EXAMPLES = [
|
|
|
5789
6540
|
{ id: "haiku", label: "Haiku", spec: HAIKU_SPEC },
|
|
5790
6541
|
{ id: "pings", label: "Pings", spec: PINGS_SPEC },
|
|
5791
6542
|
{ id: "facets", label: "Facets", spec: FACETS_SPEC },
|
|
5792
|
-
{ id: "relay-board", label: "Relay Board", spec: RELAY_BOARD_SPEC }
|
|
6543
|
+
{ id: "relay-board", label: "Relay Board", spec: RELAY_BOARD_SPEC },
|
|
6544
|
+
{ id: "lobby-talk", label: "Lobby Talk", spec: LOBBY_TALK_SPEC }
|
|
5793
6545
|
];
|
|
5794
6546
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
5795
6547
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -5808,6 +6560,7 @@ export {
|
|
|
5808
6560
|
HAIKU_SPEC,
|
|
5809
6561
|
LANTERNS_SPEC,
|
|
5810
6562
|
LIMITS,
|
|
6563
|
+
LOBBY_TALK_SPEC,
|
|
5811
6564
|
MATRIX_RAIN_SPEC,
|
|
5812
6565
|
NOSTALGHIA_CANDLE_SPEC,
|
|
5813
6566
|
ORRERY_SPEC,
|
|
@@ -5820,6 +6573,7 @@ export {
|
|
|
5820
6573
|
SCHEMA_EXAMPLES,
|
|
5821
6574
|
SCHEMA_VERSION,
|
|
5822
6575
|
SNOWFALL_SPEC,
|
|
6576
|
+
WARP_MAX_SCALE,
|
|
5823
6577
|
WARP_NEAR,
|
|
5824
6578
|
WARP_TUNNEL_SPEC,
|
|
5825
6579
|
adviseSequence,
|
|
@@ -5828,8 +6582,10 @@ export {
|
|
|
5828
6582
|
applyDeltasToSpec,
|
|
5829
6583
|
assertValidSequence,
|
|
5830
6584
|
assertValidSpec,
|
|
6585
|
+
bedRenderSeed,
|
|
5831
6586
|
breakTextBlock,
|
|
5832
6587
|
buildEntities,
|
|
6588
|
+
canMorph,
|
|
5833
6589
|
cohesionOf,
|
|
5834
6590
|
compileSaver,
|
|
5835
6591
|
compileSequence,
|
|
@@ -5848,8 +6604,12 @@ export {
|
|
|
5848
6604
|
linkPairs,
|
|
5849
6605
|
luminanceGrid,
|
|
5850
6606
|
manifestFor,
|
|
6607
|
+
morphChainRoot,
|
|
6608
|
+
morphNothingMorphable,
|
|
5851
6609
|
motionStats,
|
|
6610
|
+
normalizeSeed,
|
|
5852
6611
|
perceiveScene,
|
|
6612
|
+
perceiveSequenceFrame,
|
|
5853
6613
|
positionAt,
|
|
5854
6614
|
renderBrailleMap,
|
|
5855
6615
|
renderDensityMap,
|
|
@@ -5859,13 +6619,19 @@ export {
|
|
|
5859
6619
|
rotationAt,
|
|
5860
6620
|
seamCauseOf,
|
|
5861
6621
|
seamsWorthWarning,
|
|
6622
|
+
segmentRenderSeed,
|
|
5862
6623
|
segmentStart,
|
|
6624
|
+
sequenceSteerablePaths,
|
|
5863
6625
|
sizeAt,
|
|
5864
6626
|
spriteIndexAt,
|
|
5865
6627
|
spriteVariants,
|
|
5866
6628
|
steerablePaths,
|
|
5867
6629
|
structuralSignature,
|
|
6630
|
+
textBlockAnchorOffset,
|
|
6631
|
+
textMetricsClassFor,
|
|
5868
6632
|
textSprites,
|
|
6633
|
+
textStringsDiffer,
|
|
6634
|
+
textStringsOf,
|
|
5869
6635
|
textWidthEm,
|
|
5870
6636
|
validateSequence,
|
|
5871
6637
|
validateSpec
|