@idle-screens/schema 3.1.1 → 3.2.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 +65 -2
- package/dist/index.d.ts +86 -1
- package/dist/index.js +434 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +933 -189
package/dist/index.js
CHANGED
|
@@ -42,7 +42,16 @@ var LIMITS = {
|
|
|
42
42
|
maxPathPoints: 24,
|
|
43
43
|
minPathDuration: 2e3,
|
|
44
44
|
// ms — a path lap can't be faster than this
|
|
45
|
-
maxGridColumns: 100
|
|
45
|
+
maxGridColumns: 100,
|
|
46
|
+
maxTextBlockLength: 2e3,
|
|
47
|
+
minTextBlockFontSize: 0.01,
|
|
48
|
+
maxTextBlockFontSize: 0.2,
|
|
49
|
+
maxTextBlockMaxWidth: 1,
|
|
50
|
+
maxSegments: 24,
|
|
51
|
+
minSegmentDuration: 1e3,
|
|
52
|
+
// ms — flash-safety floor: segment cuts are luminance transitions
|
|
53
|
+
maxTransitionDur: 5e3,
|
|
54
|
+
minTransitionDur: 200
|
|
46
55
|
};
|
|
47
56
|
|
|
48
57
|
// src/validate.ts
|
|
@@ -78,6 +87,7 @@ var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width",
|
|
|
78
87
|
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "colors", "colorWeights"]);
|
|
79
88
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
80
89
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
90
|
+
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color"]);
|
|
81
91
|
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob"]);
|
|
82
92
|
var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway"]);
|
|
83
93
|
var KNOWN_BOUNCE = /* @__PURE__ */ new Set(["type", "speed"]);
|
|
@@ -108,6 +118,14 @@ function validateSpec(spec) {
|
|
|
108
118
|
if (spec.units !== void 0 && spec.units !== "px" && spec.units !== "viewport") {
|
|
109
119
|
err("units", "must be 'px' | 'viewport'");
|
|
110
120
|
}
|
|
121
|
+
if (spec.units === "px" && Array.isArray(spec.layers)) {
|
|
122
|
+
for (const l of spec.layers) {
|
|
123
|
+
if (isObj(l) && isObj(l.sprite) && l.sprite.kind === "textBlock") {
|
|
124
|
+
err("units", "textBlock dimensions are viewport fractions \u2014 units: 'px' is not supported with textBlock sprites");
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
111
129
|
if (spec.referenceViewport !== void 0) {
|
|
112
130
|
if (!isNum(spec.referenceViewport) || spec.referenceViewport < 100 || spec.referenceViewport > 8640) {
|
|
113
131
|
err("referenceViewport", "must be a number between 100 and 8640");
|
|
@@ -407,8 +425,28 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
407
425
|
}
|
|
408
426
|
color(sprite.color, `${path}.color`, err);
|
|
409
427
|
validatePalette(sprite, path, err);
|
|
428
|
+
} else if (sprite.kind === "textBlock") {
|
|
429
|
+
knownSet = KNOWN_TEXT_BLOCK;
|
|
430
|
+
if (typeof sprite.text !== "string" || sprite.text.length === 0) {
|
|
431
|
+
err(`${path}.text`, "must be a non-empty string");
|
|
432
|
+
} else if (sprite.text.length > LIMITS.maxTextBlockLength) {
|
|
433
|
+
err(`${path}.text`, `must be at most ${LIMITS.maxTextBlockLength} characters`);
|
|
434
|
+
}
|
|
435
|
+
if (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0 || sprite.maxWidth > LIMITS.maxTextBlockMaxWidth) {
|
|
436
|
+
err(`${path}.maxWidth`, `must be a positive number up to ${LIMITS.maxTextBlockMaxWidth} (viewport fraction)`);
|
|
437
|
+
}
|
|
438
|
+
if (!isNum(sprite.fontSize) || sprite.fontSize < LIMITS.minTextBlockFontSize || sprite.fontSize > LIMITS.maxTextBlockFontSize) {
|
|
439
|
+
err(`${path}.fontSize`, `must be between ${LIMITS.minTextBlockFontSize} and ${LIMITS.maxTextBlockFontSize} (viewport fraction)`);
|
|
440
|
+
}
|
|
441
|
+
if (sprite.lineHeight !== void 0 && (!isNum(sprite.lineHeight) || sprite.lineHeight < 0.5 || sprite.lineHeight > 4)) {
|
|
442
|
+
err(`${path}.lineHeight`, "must be between 0.5 and 4 (multiplier)");
|
|
443
|
+
}
|
|
444
|
+
if (sprite.align !== void 0 && !["left", "center", "right"].includes(sprite.align)) {
|
|
445
|
+
err(`${path}.align`, "must be 'left' | 'center' | 'right'");
|
|
446
|
+
}
|
|
447
|
+
if (sprite.color !== void 0) color(sprite.color, `${path}.color`, err);
|
|
410
448
|
} else {
|
|
411
|
-
err(`${path}.kind`, "must be emoji | text | circle | ring | streak | rect");
|
|
449
|
+
err(`${path}.kind`, "must be emoji | text | circle | ring | streak | rect | textBlock");
|
|
412
450
|
return;
|
|
413
451
|
}
|
|
414
452
|
for (const k of unknownKeys(sprite, knownSet)) {
|
|
@@ -576,6 +614,86 @@ ${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
|
576
614
|
}
|
|
577
615
|
return spec;
|
|
578
616
|
}
|
|
617
|
+
function validateSequence(seq) {
|
|
618
|
+
const errors = [];
|
|
619
|
+
const warnings = [];
|
|
620
|
+
const err = (path, message) => void errors.push({ path, message });
|
|
621
|
+
if (!isObj(seq)) return { valid: false, errors: [{ path: "", message: "sequence must be an object" }], warnings: [] };
|
|
622
|
+
if (seq.format !== "idle-sequence") err("format", "must be 'idle-sequence'");
|
|
623
|
+
if (seq.schemaVersion !== SCHEMA_VERSION) err("schemaVersion", `must be ${SCHEMA_VERSION}`);
|
|
624
|
+
if (!isStr(seq.id) || seq.id.trim() === "") err("id", "must be a non-empty string");
|
|
625
|
+
if (!isStr(seq.label) || seq.label.trim() === "") err("label", "must be a non-empty string");
|
|
626
|
+
if (seq.seed !== void 0 && !isNum(seq.seed)) err("seed", "must be a number");
|
|
627
|
+
if (typeof seq.loop !== "boolean") err("loop", "must be a boolean");
|
|
628
|
+
if (!Array.isArray(seq.segments) || seq.segments.length === 0) {
|
|
629
|
+
err("segments", "must be a non-empty array");
|
|
630
|
+
return { valid: false, errors, warnings };
|
|
631
|
+
}
|
|
632
|
+
if (seq.segments.length > LIMITS.maxSegments) {
|
|
633
|
+
err("segments", `at most ${LIMITS.maxSegments} segments`);
|
|
634
|
+
}
|
|
635
|
+
const keys = /* @__PURE__ */ new Set();
|
|
636
|
+
let hasDurationless = false;
|
|
637
|
+
for (let i = 0; i < seq.segments.length; i++) {
|
|
638
|
+
const s = seq.segments[i];
|
|
639
|
+
const p = `segments[${i}]`;
|
|
640
|
+
if (!isObj(s)) {
|
|
641
|
+
err(p, "must be an object");
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (!isStr(s.key) || s.key.trim() === "") {
|
|
645
|
+
err(`${p}.key`, "must be a non-empty string");
|
|
646
|
+
} else if (keys.has(s.key)) {
|
|
647
|
+
err(`${p}.key`, `duplicate key '${s.key}'`);
|
|
648
|
+
} else {
|
|
649
|
+
keys.add(s.key);
|
|
650
|
+
}
|
|
651
|
+
if (s.duration !== void 0) {
|
|
652
|
+
if (hasDurationless) {
|
|
653
|
+
err(`${p}.duration`, "a segment after a durationless segment may not have a duration");
|
|
654
|
+
}
|
|
655
|
+
if (!isNum(s.duration) || s.duration < LIMITS.minSegmentDuration) {
|
|
656
|
+
err(`${p}.duration`, `must be >= ${LIMITS.minSegmentDuration} ms`);
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
if (i < seq.segments.length - 1) {
|
|
660
|
+
err(`${p}.duration`, "only the final segment may omit duration");
|
|
661
|
+
}
|
|
662
|
+
hasDurationless = true;
|
|
663
|
+
}
|
|
664
|
+
if (s.advance !== void 0 && !["auto", "input", "either"].includes(s.advance)) {
|
|
665
|
+
err(`${p}.advance`, "must be 'auto' | 'input' | 'either'");
|
|
666
|
+
}
|
|
667
|
+
if (s.transition !== void 0) {
|
|
668
|
+
if (!isObj(s.transition)) {
|
|
669
|
+
err(`${p}.transition`, "must be an object");
|
|
670
|
+
} else if (s.transition.type !== "cut") {
|
|
671
|
+
err(`${p}.transition.type`, "must be 'cut' (fade not yet supported)");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (isObj(s.scene)) {
|
|
675
|
+
const sceneResult = validateSpec(s.scene);
|
|
676
|
+
for (const e of sceneResult.errors) errors.push({ path: `${p}.scene.${e.path}`, message: e.message });
|
|
677
|
+
if (sceneResult.warnings) {
|
|
678
|
+
for (const w of sceneResult.warnings) warnings.push({ path: `${p}.scene.${w.path}`, code: w.code, message: w.message });
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
err(`${p}.scene`, "must be a valid SaverSpec object");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (seq.loop === true && hasDurationless) {
|
|
685
|
+
err("loop", "loop: true requires all segments to have a duration");
|
|
686
|
+
}
|
|
687
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
688
|
+
}
|
|
689
|
+
function assertValidSequence(seq) {
|
|
690
|
+
const r = validateSequence(seq);
|
|
691
|
+
if (!r.valid) {
|
|
692
|
+
throw new Error(`invalid sequence:
|
|
693
|
+
${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
694
|
+
}
|
|
695
|
+
return seq;
|
|
696
|
+
}
|
|
579
697
|
|
|
580
698
|
// src/simulate.ts
|
|
581
699
|
var WARP_NEAR = 0.08;
|
|
@@ -660,7 +778,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
660
778
|
}
|
|
661
779
|
const out = [];
|
|
662
780
|
for (let i = 0; i < effectiveCount; i++) {
|
|
663
|
-
const size = sprite.kind === "circle" || sprite.kind === "ring" ? rng.range(sprite.radius[0], sprite.radius[1]) * 2 * scale : sprite.kind === "streak" ? rng.range(sprite.length[0], sprite.length[1]) * scale : sprite.kind === "rect" ? rng.range(sprite.width[0], sprite.width[1]) * scale : rng.range(smin, smax) * scale;
|
|
781
|
+
const size = sprite.kind === "textBlock" ? sprite.fontSize * scale : sprite.kind === "circle" || sprite.kind === "ring" ? rng.range(sprite.radius[0], sprite.radius[1]) * 2 * scale : sprite.kind === "streak" ? rng.range(sprite.length[0], sprite.length[1]) * scale : sprite.kind === "rect" ? rng.range(sprite.width[0], sprite.width[1]) * scale : rng.range(smin, smax) * scale;
|
|
664
782
|
const size2 = sprite.kind === "rect" ? size * (sprite.aspect ? rng.range(sprite.aspect[0], sprite.aspect[1]) : 1) : void 0;
|
|
665
783
|
let vx = 0;
|
|
666
784
|
let vy = 0;
|
|
@@ -1044,6 +1162,51 @@ function linkPairs(positions, k, maxDist, doWrap, w, h) {
|
|
|
1044
1162
|
}
|
|
1045
1163
|
return out;
|
|
1046
1164
|
}
|
|
1165
|
+
var CHAR_NARROW = new Set(`iIljtf1!|.,;:'"()[]{}`);
|
|
1166
|
+
var CHAR_WIDE = new Set("mwMWGOQD@%");
|
|
1167
|
+
function charWidthEm(ch) {
|
|
1168
|
+
if (ch === " " || ch === " ") return 0.3;
|
|
1169
|
+
if (CHAR_NARROW.has(ch)) return 0.35;
|
|
1170
|
+
if (CHAR_WIDE.has(ch)) return 0.72;
|
|
1171
|
+
return 0.55;
|
|
1172
|
+
}
|
|
1173
|
+
function textWidthEm(str) {
|
|
1174
|
+
let w = 0;
|
|
1175
|
+
for (let i = 0; i < str.length; i++) w += charWidthEm(str[i]);
|
|
1176
|
+
return w;
|
|
1177
|
+
}
|
|
1178
|
+
function breakTextBlock(text, maxWidthEm) {
|
|
1179
|
+
const paragraphs = text.split("\n");
|
|
1180
|
+
const lines = [];
|
|
1181
|
+
for (const para of paragraphs) {
|
|
1182
|
+
if (para.length === 0) {
|
|
1183
|
+
lines.push({ text: "", widthEm: 0 });
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
const words = para.split(/\s+/).filter((w) => w.length > 0);
|
|
1187
|
+
if (words.length === 0) {
|
|
1188
|
+
lines.push({ text: "", widthEm: 0 });
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
let lineText = words[0];
|
|
1192
|
+
let lineW = textWidthEm(lineText);
|
|
1193
|
+
const spaceW = charWidthEm(" ");
|
|
1194
|
+
for (let i = 1; i < words.length; i++) {
|
|
1195
|
+
const word = words[i];
|
|
1196
|
+
const wordW = textWidthEm(word);
|
|
1197
|
+
if (lineW + spaceW + wordW <= maxWidthEm) {
|
|
1198
|
+
lineText += " " + word;
|
|
1199
|
+
lineW += spaceW + wordW;
|
|
1200
|
+
} else {
|
|
1201
|
+
lines.push({ text: lineText, widthEm: lineW });
|
|
1202
|
+
lineText = word;
|
|
1203
|
+
lineW = wordW;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
lines.push({ text: lineText, widthEm: lineW });
|
|
1207
|
+
}
|
|
1208
|
+
return lines;
|
|
1209
|
+
}
|
|
1047
1210
|
|
|
1048
1211
|
// src/compile.ts
|
|
1049
1212
|
import {
|
|
@@ -1173,12 +1336,41 @@ function structuralSignature(spec) {
|
|
|
1173
1336
|
l.sprite.kind === "rect" ? [s.width, s.aspect] : void 0,
|
|
1174
1337
|
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
1175
1338
|
s.colorWeights,
|
|
1176
|
-
l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : void 0
|
|
1339
|
+
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
|
|
1177
1340
|
];
|
|
1178
1341
|
})
|
|
1179
1342
|
]);
|
|
1180
1343
|
}
|
|
1181
1344
|
|
|
1345
|
+
// src/sequence.ts
|
|
1346
|
+
function resolveSegment(seq, T) {
|
|
1347
|
+
const { segments, loop } = seq;
|
|
1348
|
+
if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
|
|
1349
|
+
let totalTimed = 0;
|
|
1350
|
+
for (const seg of segments) {
|
|
1351
|
+
if (seg.duration != null) totalTimed += seg.duration;
|
|
1352
|
+
else break;
|
|
1353
|
+
}
|
|
1354
|
+
let t = Math.max(0, T);
|
|
1355
|
+
if (loop && totalTimed > 0) {
|
|
1356
|
+
t = t % totalTimed;
|
|
1357
|
+
}
|
|
1358
|
+
let cumulative = 0;
|
|
1359
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1360
|
+
const seg = segments[i];
|
|
1361
|
+
if (seg.duration == null) {
|
|
1362
|
+
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
1363
|
+
}
|
|
1364
|
+
if (t < cumulative + seg.duration) {
|
|
1365
|
+
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
1366
|
+
}
|
|
1367
|
+
cumulative += seg.duration;
|
|
1368
|
+
}
|
|
1369
|
+
const last = segments.length - 1;
|
|
1370
|
+
const lastStart = cumulative - (segments[last].duration ?? 0);
|
|
1371
|
+
return { index: last, localT: t - lastStart, startT: lastStart };
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1182
1374
|
// src/compile.ts
|
|
1183
1375
|
var DEFAULT_STEER_DUR = 1e3;
|
|
1184
1376
|
function hexToRgba(hex, alpha) {
|
|
@@ -1226,12 +1418,14 @@ var SpecInstance = class {
|
|
|
1226
1418
|
let canvas;
|
|
1227
1419
|
if (ctx.surface) {
|
|
1228
1420
|
canvas = ctx.surface;
|
|
1421
|
+
this.ownsCanvas = false;
|
|
1229
1422
|
} else {
|
|
1230
1423
|
const el = document.createElement("canvas");
|
|
1231
1424
|
el.style.cssText = "display:block;width:100%;height:100%";
|
|
1232
1425
|
el.setAttribute("aria-hidden", "true");
|
|
1233
1426
|
ctx.host.appendChild(el);
|
|
1234
1427
|
canvas = el;
|
|
1428
|
+
this.ownsCanvas = true;
|
|
1235
1429
|
}
|
|
1236
1430
|
this.canvas = canvas;
|
|
1237
1431
|
const c2d = canvas.getContext("2d", { alpha: false });
|
|
@@ -1247,6 +1441,7 @@ var SpecInstance = class {
|
|
|
1247
1441
|
}
|
|
1248
1442
|
spec;
|
|
1249
1443
|
canvas;
|
|
1444
|
+
ownsCanvas;
|
|
1250
1445
|
ctx;
|
|
1251
1446
|
saverCtx;
|
|
1252
1447
|
seed;
|
|
@@ -1365,7 +1560,7 @@ var SpecInstance = class {
|
|
|
1365
1560
|
const headAlpha = alphaAt(e, t) * lifeA;
|
|
1366
1561
|
const headSize = sizeAt(e, t);
|
|
1367
1562
|
const sprite = built.layer.sprite;
|
|
1368
|
-
const resolvedColor = sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect" ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
1563
|
+
const resolvedColor = sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect" ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" || sprite.kind === "textBlock" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
1369
1564
|
const isSoft = sprite.kind === "circle" && sprite.soft;
|
|
1370
1565
|
const wrap2 = built.layer.wrap !== false;
|
|
1371
1566
|
const head = this.entityPos(e, t, parentE);
|
|
@@ -1472,6 +1667,28 @@ var SpecInstance = class {
|
|
|
1472
1667
|
ctx.restore();
|
|
1473
1668
|
return;
|
|
1474
1669
|
}
|
|
1670
|
+
if (sprite.kind === "textBlock") {
|
|
1671
|
+
const unitScale2 = Math.min(this.w, this.h);
|
|
1672
|
+
const fsPx = sprite.fontSize * unitScale2;
|
|
1673
|
+
const lh = (sprite.lineHeight ?? 1.4) * fsPx;
|
|
1674
|
+
const maxWPx = sprite.maxWidth * unitScale2;
|
|
1675
|
+
const maxWEm = maxWPx / fsPx;
|
|
1676
|
+
const lines = breakTextBlock(sprite.text, maxWEm);
|
|
1677
|
+
const align = sprite.align ?? "left";
|
|
1678
|
+
ctx.save();
|
|
1679
|
+
ctx.translate(p.x, p.y);
|
|
1680
|
+
if (rot) ctx.rotate(rot);
|
|
1681
|
+
ctx.font = `${fsPx}px system-ui, sans-serif`;
|
|
1682
|
+
ctx.fillStyle = sprite.color ?? "#e6e8ef";
|
|
1683
|
+
ctx.textBaseline = "top";
|
|
1684
|
+
ctx.textAlign = align;
|
|
1685
|
+
const xOff = align === "center" ? maxWPx / 2 : align === "right" ? maxWPx : 0;
|
|
1686
|
+
for (let li = 0; li < lines.length; li++) {
|
|
1687
|
+
ctx.fillText(lines[li].text, xOff, li * lh);
|
|
1688
|
+
}
|
|
1689
|
+
ctx.restore();
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1475
1692
|
ctx.save();
|
|
1476
1693
|
ctx.translate(p.x, p.y);
|
|
1477
1694
|
if (rot) ctx.rotate(rot);
|
|
@@ -1641,7 +1858,7 @@ var SpecInstance = class {
|
|
|
1641
1858
|
}
|
|
1642
1859
|
dispose() {
|
|
1643
1860
|
this.stop();
|
|
1644
|
-
if (typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
|
|
1861
|
+
if (this.ownsCanvas && typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
|
|
1645
1862
|
}
|
|
1646
1863
|
};
|
|
1647
1864
|
function compileSaver(spec) {
|
|
@@ -1652,6 +1869,129 @@ function compileSaver(spec) {
|
|
|
1652
1869
|
spec: valid
|
|
1653
1870
|
};
|
|
1654
1871
|
}
|
|
1872
|
+
var SequenceInstance = class {
|
|
1873
|
+
seq;
|
|
1874
|
+
childCtx;
|
|
1875
|
+
canvas;
|
|
1876
|
+
children;
|
|
1877
|
+
activeIndex = -1;
|
|
1878
|
+
paused = false;
|
|
1879
|
+
constructor(seq, ctx) {
|
|
1880
|
+
this.seq = seq;
|
|
1881
|
+
let surface = ctx.surface ?? null;
|
|
1882
|
+
let canvas = null;
|
|
1883
|
+
if (!surface && typeof document !== "undefined") {
|
|
1884
|
+
canvas = document.createElement("canvas");
|
|
1885
|
+
canvas.style.cssText = "display:block;width:100%;height:100%";
|
|
1886
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
1887
|
+
ctx.host.appendChild(canvas);
|
|
1888
|
+
surface = canvas;
|
|
1889
|
+
}
|
|
1890
|
+
this.canvas = canvas;
|
|
1891
|
+
this.childCtx = { ...ctx, surface, reducedMotion: true };
|
|
1892
|
+
this.children = new Array(seq.segments.length).fill(null);
|
|
1893
|
+
}
|
|
1894
|
+
childScene(index) {
|
|
1895
|
+
const seg = this.seq.segments[index];
|
|
1896
|
+
if (seg.scene.seed != null) return seg.scene;
|
|
1897
|
+
if (this.seq.seed != null) return { ...seg.scene, seed: this.seq.seed + index };
|
|
1898
|
+
return seg.scene;
|
|
1899
|
+
}
|
|
1900
|
+
ensureChild(index) {
|
|
1901
|
+
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
1902
|
+
let child = this.children[index];
|
|
1903
|
+
if (!child) {
|
|
1904
|
+
child = new SpecInstance(this.childScene(index), this.childCtx);
|
|
1905
|
+
this.children[index] = child;
|
|
1906
|
+
if (this.paused) child.setPaused(true);
|
|
1907
|
+
}
|
|
1908
|
+
return child;
|
|
1909
|
+
}
|
|
1910
|
+
releaseChild(index) {
|
|
1911
|
+
const child = this.children[index];
|
|
1912
|
+
if (child) {
|
|
1913
|
+
child.dispose();
|
|
1914
|
+
this.children[index] = null;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
renderFrame(T, seed) {
|
|
1918
|
+
const resolved = resolveSegment(this.seq, T);
|
|
1919
|
+
const { index, localT } = resolved;
|
|
1920
|
+
if (index !== this.activeIndex) {
|
|
1921
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
1922
|
+
if (i !== index) this.releaseChild(i);
|
|
1923
|
+
}
|
|
1924
|
+
this.activeIndex = index;
|
|
1925
|
+
}
|
|
1926
|
+
const child = this.ensureChild(index);
|
|
1927
|
+
const childSeed = this.seq.segments[index].scene.seed ?? this.seq.seed ?? seed;
|
|
1928
|
+
child.renderFrame(localT, childSeed);
|
|
1929
|
+
}
|
|
1930
|
+
setPaused(paused) {
|
|
1931
|
+
this.paused = paused;
|
|
1932
|
+
for (const child of this.children) {
|
|
1933
|
+
child?.setPaused(paused);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
resize(width, height, dpr) {
|
|
1937
|
+
for (const child of this.children) {
|
|
1938
|
+
child?.resize(width, height, dpr);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
applyTrack(track) {
|
|
1942
|
+
const deltas = track?.deltas ?? [];
|
|
1943
|
+
const segDelta = deltas.find((d) => d.path === "sequence.segment");
|
|
1944
|
+
if (segDelta !== void 0 && typeof segDelta.value === "number") {
|
|
1945
|
+
const idx = Math.max(0, Math.min(this.seq.segments.length - 1, Math.round(segDelta.value)));
|
|
1946
|
+
if (idx !== this.activeIndex) {
|
|
1947
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
1948
|
+
if (i !== idx) this.releaseChild(i);
|
|
1949
|
+
}
|
|
1950
|
+
this.activeIndex = idx;
|
|
1951
|
+
const child = this.ensureChild(idx);
|
|
1952
|
+
child.renderFrame(0, this.seq.segments[idx].scene.seed ?? this.seq.seed ?? 0);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
|
|
1956
|
+
if (childDeltas.length > 0 && this.activeIndex >= 0) {
|
|
1957
|
+
const child = this.children[this.activeIndex];
|
|
1958
|
+
child?.applyTrack({ ...track, deltas: childDeltas });
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
dispose() {
|
|
1962
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
1963
|
+
this.releaseChild(i);
|
|
1964
|
+
}
|
|
1965
|
+
if (this.canvas) this.canvas.remove();
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
function sequenceManifest(seq) {
|
|
1969
|
+
const maxTotal = seq.segments.reduce((max, s) => {
|
|
1970
|
+
const t = s.scene.layers.reduce((n, l) => n + l.count, 0);
|
|
1971
|
+
return Math.max(max, t);
|
|
1972
|
+
}, 0);
|
|
1973
|
+
const costTier = maxTotal < 30 ? "idle" : maxTotal < 150 ? "low" : maxTotal < 400 ? "medium" : "high";
|
|
1974
|
+
return {
|
|
1975
|
+
id: seq.id,
|
|
1976
|
+
label: seq.label,
|
|
1977
|
+
timeModel: "closed-form",
|
|
1978
|
+
passthrough: false,
|
|
1979
|
+
minBackend: "canvas2d",
|
|
1980
|
+
costTier,
|
|
1981
|
+
motionIntensity: "moderate",
|
|
1982
|
+
reducedMotionFallback: "static",
|
|
1983
|
+
a11y: { flashSafe: true },
|
|
1984
|
+
workerReady: false
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function compileSequence(spec) {
|
|
1988
|
+
const valid = assertValidSequence(spec);
|
|
1989
|
+
return {
|
|
1990
|
+
manifest: sequenceManifest(valid),
|
|
1991
|
+
mount: (ctx) => new SequenceInstance(valid, ctx),
|
|
1992
|
+
spec: valid
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1655
1995
|
|
|
1656
1996
|
// src/advise.ts
|
|
1657
1997
|
import { createRng as createRng2 } from "@idle-screens/core";
|
|
@@ -1671,7 +2011,7 @@ function spriteLuma(layer, e) {
|
|
|
1671
2011
|
if (s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect") {
|
|
1672
2012
|
return hexLuma(s.colors?.[e.colorIndex] ?? s.color);
|
|
1673
2013
|
}
|
|
1674
|
-
if (s.kind === "text") return hexLuma(s.color ?? "#e6e8ef");
|
|
2014
|
+
if (s.kind === "text" || s.kind === "textBlock") return hexLuma(s.color ?? "#e6e8ef");
|
|
1675
2015
|
return 0.75;
|
|
1676
2016
|
}
|
|
1677
2017
|
function spriteHex(layer, e) {
|
|
@@ -1679,7 +2019,7 @@ function spriteHex(layer, e) {
|
|
|
1679
2019
|
if (s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect") {
|
|
1680
2020
|
return s.colors?.[e.colorIndex] ?? s.color;
|
|
1681
2021
|
}
|
|
1682
|
-
if (s.kind === "text") return s.color ?? "#e6e8ef";
|
|
2022
|
+
if (s.kind === "text" || s.kind === "textBlock") return s.color ?? "#e6e8ef";
|
|
1683
2023
|
return null;
|
|
1684
2024
|
}
|
|
1685
2025
|
function backgroundLuma(spec) {
|
|
@@ -1734,7 +2074,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1734
2074
|
const layer = spec.layers[li];
|
|
1735
2075
|
const entities = allEntities[li];
|
|
1736
2076
|
totalEntities += entities.length;
|
|
1737
|
-
const isText = layer.sprite.kind === "text";
|
|
2077
|
+
const isText = layer.sprite.kind === "text" || layer.sprite.kind === "textBlock";
|
|
1738
2078
|
const isStaticText = isText && layer.motion.type === "static";
|
|
1739
2079
|
if (isStaticText) textLayerCount++;
|
|
1740
2080
|
if (layer.motion.type !== "static") motionLayerCount++;
|
|
@@ -1793,7 +2133,18 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1793
2133
|
const entities = allEntities[li];
|
|
1794
2134
|
for (const e of entities) {
|
|
1795
2135
|
const r = e.size / 2;
|
|
1796
|
-
|
|
2136
|
+
let pixArea;
|
|
2137
|
+
if (layer.sprite.kind === "circle") {
|
|
2138
|
+
pixArea = Math.PI * r * r;
|
|
2139
|
+
} else if (layer.sprite.kind === "textBlock") {
|
|
2140
|
+
const fsPx = layer.sprite.fontSize * scale;
|
|
2141
|
+
const lh = (layer.sprite.lineHeight ?? 1.4) * fsPx;
|
|
2142
|
+
const maxWPx = layer.sprite.maxWidth * scale;
|
|
2143
|
+
const lines = breakTextBlock(layer.sprite.text, maxWPx / fsPx);
|
|
2144
|
+
pixArea = maxWPx * lines.length * lh * 0.55;
|
|
2145
|
+
} else {
|
|
2146
|
+
pixArea = e.size * e.size;
|
|
2147
|
+
}
|
|
1797
2148
|
totalCoverage += pixArea * e.alpha / (w * h);
|
|
1798
2149
|
}
|
|
1799
2150
|
if (layer.links) {
|
|
@@ -2021,6 +2372,24 @@ function textBox(s, e, p) {
|
|
|
2021
2372
|
halfY: fh * 0.6
|
|
2022
2373
|
};
|
|
2023
2374
|
}
|
|
2375
|
+
function textBlockBox(s, _e, p, w, h) {
|
|
2376
|
+
const unitScale = Math.min(w, h);
|
|
2377
|
+
const fsPx = s.fontSize * unitScale;
|
|
2378
|
+
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
2379
|
+
const maxWPx = s.maxWidth * unitScale;
|
|
2380
|
+
const maxWEm = maxWPx / fsPx;
|
|
2381
|
+
const lines = breakTextBlock(s.text, maxWEm);
|
|
2382
|
+
const totalH = lines.length * lh;
|
|
2383
|
+
const maxLineW = lines.reduce((m, l) => Math.max(m, l.widthEm), 0) * fsPx;
|
|
2384
|
+
const align = s.align ?? "left";
|
|
2385
|
+
const cx = align === "center" ? p.x + maxWPx / 2 : align === "right" ? p.x + maxWPx - maxLineW / 2 : p.x + maxLineW / 2;
|
|
2386
|
+
return {
|
|
2387
|
+
cx,
|
|
2388
|
+
cy: p.y + totalH / 2,
|
|
2389
|
+
halfX: maxLineW / 2,
|
|
2390
|
+
halfY: totalH / 2
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2024
2393
|
function luminanceGrid(spec, opts = {}) {
|
|
2025
2394
|
const scene = buildScene(spec, opts);
|
|
2026
2395
|
const t = opts.t ?? 5e3;
|
|
@@ -2142,6 +2511,12 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
2142
2511
|
centerY = box.cy;
|
|
2143
2512
|
halfX = box.halfX;
|
|
2144
2513
|
halfY = box.halfY;
|
|
2514
|
+
} else if (s.kind === "textBlock") {
|
|
2515
|
+
const box = textBlockBox(s, e, p, w, h);
|
|
2516
|
+
centerX = box.cx;
|
|
2517
|
+
centerY = box.cy;
|
|
2518
|
+
halfX = box.halfX;
|
|
2519
|
+
halfY = box.halfY;
|
|
2145
2520
|
}
|
|
2146
2521
|
const circular = s.kind === "circle" || s.kind === "ring";
|
|
2147
2522
|
const soft = s.kind === "circle" && !!s.soft;
|
|
@@ -2153,7 +2528,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
2153
2528
|
const c1 = Math.min(cols - 1, Math.floor((centerX + reachX) / cellW));
|
|
2154
2529
|
const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
|
|
2155
2530
|
const r1 = Math.min(rows - 1, Math.floor((centerY + reachY) / cellH));
|
|
2156
|
-
const inkWeight = s.kind === "text" || s.kind === "emoji" ? 0.55 : 1;
|
|
2531
|
+
const inkWeight = s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1;
|
|
2157
2532
|
for (let r = r0; r <= r1; r++) {
|
|
2158
2533
|
for (let c = c0; c <= c1; c++) {
|
|
2159
2534
|
const dx = (c + 0.5) * cellW - centerX;
|
|
@@ -2312,6 +2687,13 @@ function textSprites(spec, opts = {}) {
|
|
|
2312
2687
|
const t = opts.t ?? 5e3;
|
|
2313
2688
|
const out = [];
|
|
2314
2689
|
scene.layers.forEach(({ layer, entities }, layerIndex) => {
|
|
2690
|
+
if (layer.sprite.kind === "textBlock") {
|
|
2691
|
+
const s2 = layer.sprite;
|
|
2692
|
+
const unitScale = Math.min(scene.w, scene.h);
|
|
2693
|
+
const sizePx2 = s2.fontSize * unitScale;
|
|
2694
|
+
out.push({ layerIndex, key: layer.key, strings: [s2.text], count: entities.length, sizePx: Math.round(sizePx2) });
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2315
2697
|
if (layer.sprite.kind !== "text") return;
|
|
2316
2698
|
const s = layer.sprite;
|
|
2317
2699
|
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
@@ -2362,6 +2744,9 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
2362
2744
|
} else if (s.kind === "text") {
|
|
2363
2745
|
const box = textBox(s, e, { x: 0, y: 0 });
|
|
2364
2746
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
2747
|
+
} else if (s.kind === "textBlock") {
|
|
2748
|
+
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
2749
|
+
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
2365
2750
|
} else entArea = sz * sz * 0.55;
|
|
2366
2751
|
area += entArea * a;
|
|
2367
2752
|
if (layer.trail) {
|
|
@@ -3213,6 +3598,36 @@ var DASHBOARD_SPEC = {
|
|
|
3213
3598
|
]
|
|
3214
3599
|
};
|
|
3215
3600
|
|
|
3601
|
+
// src/examples/haiku.ts
|
|
3602
|
+
var HAIKU_SPEC = {
|
|
3603
|
+
schemaVersion: 1,
|
|
3604
|
+
id: "haiku",
|
|
3605
|
+
label: "Haiku",
|
|
3606
|
+
seed: 42,
|
|
3607
|
+
background: { type: "solid", color: "#080812" },
|
|
3608
|
+
layers: [
|
|
3609
|
+
{
|
|
3610
|
+
count: 1,
|
|
3611
|
+
sprite: {
|
|
3612
|
+
kind: "textBlock",
|
|
3613
|
+
text: "An old silent pond\nA frog jumps into the pond \u2014\nSplash! Silence again.",
|
|
3614
|
+
maxWidth: 0.8,
|
|
3615
|
+
fontSize: 0.05,
|
|
3616
|
+
lineHeight: 1.8,
|
|
3617
|
+
align: "center",
|
|
3618
|
+
color: "#c8cad4"
|
|
3619
|
+
},
|
|
3620
|
+
motion: { type: "static" },
|
|
3621
|
+
position: { x: 0.1, y: 0.35 }
|
|
3622
|
+
},
|
|
3623
|
+
{
|
|
3624
|
+
count: 30,
|
|
3625
|
+
sprite: { kind: "circle", radius: [5e-4, 15e-4], color: "#3a3a5c" },
|
|
3626
|
+
motion: { type: "drift", speed: [2e-3, 6e-3], angle: 270 }
|
|
3627
|
+
}
|
|
3628
|
+
]
|
|
3629
|
+
};
|
|
3630
|
+
|
|
3216
3631
|
// src/examples/lanterns.ts
|
|
3217
3632
|
var LANTERNS_SPEC = {
|
|
3218
3633
|
schemaVersion: 1,
|
|
@@ -3906,7 +4321,8 @@ var SCHEMA_EXAMPLES = [
|
|
|
3906
4321
|
{ id: "polygons", label: "Polygons", spec: POLYGONS_SPEC },
|
|
3907
4322
|
{ id: "matrix-rain", label: "Matrix Rain", spec: MATRIX_RAIN_SPEC },
|
|
3908
4323
|
{ id: "procession", label: "Night Procession", spec: PROCESSION_SPEC },
|
|
3909
|
-
{ id: "nostalghia-candle", label: "Nostalghia's Candle", spec: NOSTALGHIA_CANDLE_SPEC }
|
|
4324
|
+
{ id: "nostalghia-candle", label: "Nostalghia's Candle", spec: NOSTALGHIA_CANDLE_SPEC },
|
|
4325
|
+
{ id: "haiku", label: "Haiku", spec: HAIKU_SPEC }
|
|
3910
4326
|
];
|
|
3911
4327
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
3912
4328
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -3920,6 +4336,7 @@ export {
|
|
|
3920
4336
|
DASHBOARD_SPEC,
|
|
3921
4337
|
EXAMPLE_BY_ID,
|
|
3922
4338
|
EXAMPLE_SPECS,
|
|
4339
|
+
HAIKU_SPEC,
|
|
3923
4340
|
LANTERNS_SPEC,
|
|
3924
4341
|
LIMITS,
|
|
3925
4342
|
MATRIX_RAIN_SPEC,
|
|
@@ -3937,9 +4354,12 @@ export {
|
|
|
3937
4354
|
adviseSpec,
|
|
3938
4355
|
alphaAt,
|
|
3939
4356
|
applyDeltasToSpec,
|
|
4357
|
+
assertValidSequence,
|
|
3940
4358
|
assertValidSpec,
|
|
4359
|
+
breakTextBlock,
|
|
3941
4360
|
buildEntities,
|
|
3942
4361
|
compileSaver,
|
|
4362
|
+
compileSequence,
|
|
3943
4363
|
describeScene,
|
|
3944
4364
|
diffScenes,
|
|
3945
4365
|
dominanceRanking,
|
|
@@ -3956,6 +4376,7 @@ export {
|
|
|
3956
4376
|
positionAt,
|
|
3957
4377
|
renderBrailleMap,
|
|
3958
4378
|
renderDensityMap,
|
|
4379
|
+
resolveSegment,
|
|
3959
4380
|
resolveSpecPath,
|
|
3960
4381
|
rotationAt,
|
|
3961
4382
|
sizeAt,
|
|
@@ -3964,6 +4385,7 @@ export {
|
|
|
3964
4385
|
steerablePaths,
|
|
3965
4386
|
structuralSignature,
|
|
3966
4387
|
textSprites,
|
|
4388
|
+
validateSequence,
|
|
3967
4389
|
validateSpec
|
|
3968
4390
|
};
|
|
3969
4391
|
//# sourceMappingURL=index.js.map
|