@idle-screens/schema 2.1.0 → 2.3.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 +157 -36
- package/dist/index.d.ts +549 -28
- package/dist/index.js +2211 -371
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +205 -7
package/dist/index.js
CHANGED
|
@@ -20,8 +20,29 @@ var LIMITS = {
|
|
|
20
20
|
maxLinkLayerCount: 200,
|
|
21
21
|
minCyclePeriod: 500,
|
|
22
22
|
// ms — same flash-safety floor as pulse
|
|
23
|
-
referenceViewport: 1080
|
|
23
|
+
referenceViewport: 1080,
|
|
24
24
|
// for validating viewport-unit dimensional caps
|
|
25
|
+
maxTrailLength: 5e3,
|
|
26
|
+
// ms — cap trail duration
|
|
27
|
+
maxTrailSamples: 24,
|
|
28
|
+
// dots per trail
|
|
29
|
+
minDriftPeriod: 1e4,
|
|
30
|
+
// ms — background drift floor (10 s)
|
|
31
|
+
maxDriftAmount: 0.3,
|
|
32
|
+
// fraction of gradient stop shift
|
|
33
|
+
maxGhosting: 0.95,
|
|
34
|
+
// frame persistence cap — bounds the seek warm-up replay
|
|
35
|
+
maxGhostReplayFrames: 120,
|
|
36
|
+
// fixed-step frames replayed on a non-contiguous seek
|
|
37
|
+
maxMeander: 500,
|
|
38
|
+
// px — wander harmonic amplitude cap (viewport cap: /referenceViewport)
|
|
39
|
+
maxWarpSpeed: 1.5,
|
|
40
|
+
// depth-units/sec — full near-to-far span in ~0.7 s at max
|
|
41
|
+
minPathPoints: 2,
|
|
42
|
+
maxPathPoints: 24,
|
|
43
|
+
minPathDuration: 2e3,
|
|
44
|
+
// ms — a path lap can't be faster than this
|
|
45
|
+
maxGridColumns: 100
|
|
25
46
|
};
|
|
26
47
|
|
|
27
48
|
// src/validate.ts
|
|
@@ -30,10 +51,53 @@ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
30
51
|
var isStr = (v) => typeof v === "string";
|
|
31
52
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
32
53
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
54
|
+
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "units", "referenceViewport", "background", "layers", "ghosting"]);
|
|
55
|
+
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
56
|
+
"count",
|
|
57
|
+
"sprite",
|
|
58
|
+
"motion",
|
|
59
|
+
"size",
|
|
60
|
+
"wrap",
|
|
61
|
+
"flip",
|
|
62
|
+
"alpha",
|
|
63
|
+
"blend",
|
|
64
|
+
"region",
|
|
65
|
+
"pulse",
|
|
66
|
+
"spin",
|
|
67
|
+
"grow",
|
|
68
|
+
"key",
|
|
69
|
+
"position",
|
|
70
|
+
"trail",
|
|
71
|
+
"links",
|
|
72
|
+
"layout",
|
|
73
|
+
"life"
|
|
74
|
+
]);
|
|
75
|
+
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
|
|
76
|
+
var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
|
|
77
|
+
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
78
|
+
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "colors", "colorWeights"]);
|
|
79
|
+
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
80
|
+
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
81
|
+
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob"]);
|
|
82
|
+
var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway"]);
|
|
83
|
+
var KNOWN_BOUNCE = /* @__PURE__ */ new Set(["type", "speed"]);
|
|
84
|
+
var KNOWN_STATIC = /* @__PURE__ */ new Set(["type"]);
|
|
85
|
+
var KNOWN_ORBIT = /* @__PURE__ */ new Set(["type", "speed", "radius", "center"]);
|
|
86
|
+
var KNOWN_WANDER = /* @__PURE__ */ new Set(["type", "speed", "angle", "meander", "coherence"]);
|
|
87
|
+
var KNOWN_WARP = /* @__PURE__ */ new Set(["type", "speed", "center"]);
|
|
88
|
+
var KNOWN_PATH = /* @__PURE__ */ new Set(["type", "points", "duration", "curve", "closed", "scatter"]);
|
|
89
|
+
var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
90
|
+
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
91
|
+
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key"]);
|
|
92
|
+
function unknownKeys(obj, known) {
|
|
93
|
+
return Object.keys(obj).filter((k) => !known.has(k));
|
|
94
|
+
}
|
|
33
95
|
function validateSpec(spec) {
|
|
34
96
|
const errors = [];
|
|
97
|
+
const warnings = [];
|
|
35
98
|
const err = (path, message) => void errors.push({ path, message });
|
|
36
|
-
|
|
99
|
+
const warn = (path, code, message) => void warnings.push({ path, code, message });
|
|
100
|
+
if (!isObj(spec)) return { valid: false, errors: [{ path: "", message: "spec must be an object" }], warnings: [] };
|
|
37
101
|
if (spec.schemaVersion !== SCHEMA_VERSION) err("schemaVersion", `must be ${SCHEMA_VERSION}`);
|
|
38
102
|
if (!isStr(spec.id) || spec.id.trim() === "") err("id", "must be a non-empty string");
|
|
39
103
|
if (!isStr(spec.label) || spec.label.trim() === "") err("label", "must be a non-empty string");
|
|
@@ -44,7 +108,18 @@ function validateSpec(spec) {
|
|
|
44
108
|
if (spec.units !== void 0 && spec.units !== "px" && spec.units !== "viewport") {
|
|
45
109
|
err("units", "must be 'px' | 'viewport'");
|
|
46
110
|
}
|
|
47
|
-
if (spec.
|
|
111
|
+
if (spec.referenceViewport !== void 0) {
|
|
112
|
+
if (!isNum(spec.referenceViewport) || spec.referenceViewport < 100 || spec.referenceViewport > 8640) {
|
|
113
|
+
err("referenceViewport", "must be a number between 100 and 8640");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (spec.ghosting !== void 0 && (!isNum(spec.ghosting) || spec.ghosting < 0 || spec.ghosting > LIMITS.maxGhosting)) {
|
|
117
|
+
err("ghosting", `must be a number 0..${LIMITS.maxGhosting}`);
|
|
118
|
+
}
|
|
119
|
+
for (const k of unknownKeys(spec, KNOWN_TOP)) {
|
|
120
|
+
warn(k, "unknown-property", `unknown top-level property '${k}' \u2014 will be ignored`);
|
|
121
|
+
}
|
|
122
|
+
if (spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
48
123
|
if (!Array.isArray(spec.layers) || spec.layers.length === 0) {
|
|
49
124
|
err("layers", "must be a non-empty array");
|
|
50
125
|
} else {
|
|
@@ -52,19 +127,22 @@ function validateSpec(spec) {
|
|
|
52
127
|
let total = 0;
|
|
53
128
|
spec.layers.forEach((layer, i) => {
|
|
54
129
|
total += isObj(layer) && isNum(layer.count) ? layer.count : 0;
|
|
55
|
-
validateLayer(layer, `layers[${i}]`, err, spec);
|
|
130
|
+
validateLayer(layer, `layers[${i}]`, err, warn, spec);
|
|
56
131
|
});
|
|
57
132
|
if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
|
|
58
133
|
}
|
|
59
|
-
return { valid: errors.length === 0, errors };
|
|
134
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
60
135
|
}
|
|
61
136
|
function color(v, path, err) {
|
|
62
137
|
if (!isStr(v) || !HEX.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
63
138
|
}
|
|
64
|
-
function validateBackground(bg, err) {
|
|
139
|
+
function validateBackground(bg, err, warn) {
|
|
65
140
|
if (!isObj(bg)) return err("background", "must be an object");
|
|
66
141
|
if (bg.type === "solid") {
|
|
67
142
|
color(bg.color, "background.color", err);
|
|
143
|
+
for (const k of unknownKeys(bg, KNOWN_BG_SOLID)) {
|
|
144
|
+
warn(`background.${k}`, "unknown-property", `unknown background property '${k}' \u2014 will be ignored`);
|
|
145
|
+
}
|
|
68
146
|
} else if (bg.type === "gradient") {
|
|
69
147
|
if (!Array.isArray(bg.stops) || bg.stops.length < 2) {
|
|
70
148
|
err("background.stops", "gradient needs at least 2 stops");
|
|
@@ -74,6 +152,17 @@ function validateBackground(bg, err) {
|
|
|
74
152
|
color(isObj(s) ? s.color : void 0, `background.stops[${i}].color`, err);
|
|
75
153
|
});
|
|
76
154
|
}
|
|
155
|
+
if (bg.drift !== void 0) {
|
|
156
|
+
if (!isObj(bg.drift)) err("background.drift", "must be an object");
|
|
157
|
+
else {
|
|
158
|
+
if (!isNum(bg.drift.period) || bg.drift.period < LIMITS.minDriftPeriod) {
|
|
159
|
+
err("background.drift.period", `must be >= ${LIMITS.minDriftPeriod} ms`);
|
|
160
|
+
}
|
|
161
|
+
if (bg.drift.amount !== void 0 && (!isNum(bg.drift.amount) || bg.drift.amount <= 0 || bg.drift.amount > LIMITS.maxDriftAmount)) {
|
|
162
|
+
err("background.drift.amount", `must be > 0 and <= ${LIMITS.maxDriftAmount}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
77
166
|
if (bg.band !== void 0) {
|
|
78
167
|
if (!isObj(bg.band)) err("background.band", "must be an object");
|
|
79
168
|
else {
|
|
@@ -81,12 +170,24 @@ function validateBackground(bg, err) {
|
|
|
81
170
|
if (!isNum(bg.band.height) || bg.band.height <= 0) err("background.band.height", "must be > 0");
|
|
82
171
|
}
|
|
83
172
|
}
|
|
173
|
+
for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
|
|
174
|
+
warn(`background.${k}`, "unknown-property", `unknown background property '${k}' \u2014 will be ignored`);
|
|
175
|
+
}
|
|
84
176
|
} else {
|
|
85
177
|
err("background.type", "must be solid | gradient");
|
|
86
178
|
}
|
|
87
179
|
}
|
|
88
|
-
function validateLayer(layer, path, err, spec) {
|
|
180
|
+
function validateLayer(layer, path, err, warn, spec) {
|
|
89
181
|
if (!isObj(layer)) return err(path, "must be an object");
|
|
182
|
+
for (const k of unknownKeys(layer, KNOWN_LAYER)) {
|
|
183
|
+
if (k === "id") {
|
|
184
|
+
warn(`${path}.id`, "misplaced-property", `'id' is not a layer property \u2014 did you mean 'key'? Use 'key' to name a layer`);
|
|
185
|
+
} else if (k === "depth") {
|
|
186
|
+
warn(`${path}.depth`, "unknown-property", `'depth' is not a layer property \u2014 use motion type 'warp' for true perspective depth, or fake parallax with size/speed/alpha across layers`);
|
|
187
|
+
} else {
|
|
188
|
+
warn(`${path}.${k}`, "unknown-property", `unknown layer property '${k}' \u2014 will be ignored`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
90
191
|
if (!isNum(layer.count) || !Number.isInteger(layer.count) || layer.count < 1) {
|
|
91
192
|
err(`${path}.count`, "must be a positive integer");
|
|
92
193
|
} else if (layer.count > LIMITS.maxPerLayer) {
|
|
@@ -100,7 +201,9 @@ function validateLayer(layer, path, err, spec) {
|
|
|
100
201
|
if (layer.alpha !== void 0 && (!isRange(layer.alpha) || layer.alpha[0] < 0 || layer.alpha[1] > 1)) {
|
|
101
202
|
err(`${path}.alpha`, "must be a [min,max] range within 0..1");
|
|
102
203
|
}
|
|
103
|
-
if (layer.blend !== void 0 &&
|
|
204
|
+
if (layer.blend !== void 0 && !["lighter", "screen", "multiply"].includes(layer.blend)) {
|
|
205
|
+
err(`${path}.blend`, "must be 'lighter' | 'screen' | 'multiply' when set");
|
|
206
|
+
}
|
|
104
207
|
if (layer.key !== void 0 && (!isStr(layer.key) || layer.key.trim() === "")) {
|
|
105
208
|
err(`${path}.key`, "must be a non-empty string");
|
|
106
209
|
}
|
|
@@ -133,6 +236,17 @@ function validateLayer(layer, path, err, spec) {
|
|
|
133
236
|
if (!isNum(layer.pulse.period) || layer.pulse.period < LIMITS.minPulsePeriod) {
|
|
134
237
|
err(`${path}.pulse.period`, `must be >= ${LIMITS.minPulsePeriod} ms (flash-safety floor)`);
|
|
135
238
|
}
|
|
239
|
+
if (layer.pulse.wave !== void 0) {
|
|
240
|
+
if (!isObj(layer.pulse.wave)) err(`${path}.pulse.wave`, "must be an object");
|
|
241
|
+
else {
|
|
242
|
+
if (!isNum(layer.pulse.wave.wavelength) || layer.pulse.wave.wavelength <= 0) {
|
|
243
|
+
err(`${path}.pulse.wave.wavelength`, "must be > 0 (px or viewport-units)");
|
|
244
|
+
}
|
|
245
|
+
if (layer.pulse.wave.angle !== void 0 && !isNum(layer.pulse.wave.angle)) {
|
|
246
|
+
err(`${path}.pulse.wave.angle`, "must be a number (degrees)");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
136
250
|
}
|
|
137
251
|
}
|
|
138
252
|
if (layer.spin !== void 0) {
|
|
@@ -166,22 +280,86 @@ function validateLayer(layer, path, err, spec) {
|
|
|
166
280
|
if (layer.links.width !== void 0 && (!isNum(layer.links.width) || layer.links.width <= 0)) {
|
|
167
281
|
err(`${path}.links.width`, "must be > 0");
|
|
168
282
|
}
|
|
283
|
+
if (layer.links.mode !== void 0 && !["nearest", "chain", "random"].includes(layer.links.mode)) {
|
|
284
|
+
err(`${path}.links.mode`, "must be 'nearest' | 'chain' | 'random'");
|
|
285
|
+
}
|
|
286
|
+
if (layer.links.falloff !== void 0 && typeof layer.links.falloff !== "boolean") {
|
|
287
|
+
err(`${path}.links.falloff`, "must be a boolean");
|
|
288
|
+
}
|
|
289
|
+
if (layer.links.closed !== void 0 && typeof layer.links.closed !== "boolean") {
|
|
290
|
+
err(`${path}.links.closed`, "must be a boolean");
|
|
291
|
+
}
|
|
169
292
|
if (isNum(layer.count) && layer.count > LIMITS.maxLinkLayerCount) {
|
|
170
293
|
err(`${path}.links`, `layer count must be <= ${LIMITS.maxLinkLayerCount} when links is set`);
|
|
171
294
|
}
|
|
172
295
|
}
|
|
173
296
|
}
|
|
174
|
-
|
|
175
|
-
|
|
297
|
+
if (layer.layout !== void 0) {
|
|
298
|
+
if (!isObj(layer.layout) || layer.layout.type !== "grid") {
|
|
299
|
+
err(`${path}.layout`, "must be an object with type: 'grid'");
|
|
300
|
+
} else {
|
|
301
|
+
if (layer.layout.columns !== void 0 && (!isNum(layer.layout.columns) || !Number.isInteger(layer.layout.columns) || layer.layout.columns < 1 || layer.layout.columns > LIMITS.maxGridColumns)) {
|
|
302
|
+
err(`${path}.layout.columns`, `must be an integer 1..${LIMITS.maxGridColumns}`);
|
|
303
|
+
}
|
|
304
|
+
const jit = layer.layout.jitter;
|
|
305
|
+
if (jit !== void 0) {
|
|
306
|
+
const jitterOk = (v) => isNum(v) && v >= 0 && v <= 1;
|
|
307
|
+
if (isNum(jit)) {
|
|
308
|
+
if (!jitterOk(jit)) err(`${path}.layout.jitter`, "must be 0..1");
|
|
309
|
+
} else if (isObj(jit)) {
|
|
310
|
+
if (jit.x !== void 0 && !jitterOk(jit.x)) err(`${path}.layout.jitter.x`, "must be 0..1");
|
|
311
|
+
if (jit.y !== void 0 && !jitterOk(jit.y)) err(`${path}.layout.jitter.y`, "must be 0..1");
|
|
312
|
+
} else {
|
|
313
|
+
err(`${path}.layout.jitter`, "must be a number 0..1 or { x?, y? }");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (layer.position !== void 0) {
|
|
317
|
+
warn(`${path}.layout`, "layout-with-position", "position overrides layout for the single entity \u2014 layout has no effect");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (layer.life !== void 0) {
|
|
322
|
+
if (!isObj(layer.life)) err(`${path}.life`, "must be an object");
|
|
323
|
+
else {
|
|
324
|
+
if (layer.life.enter !== void 0 && (!isNum(layer.life.enter) || layer.life.enter < 0)) {
|
|
325
|
+
err(`${path}.life.enter`, "must be >= 0 (ms)");
|
|
326
|
+
}
|
|
327
|
+
if (layer.life.exit !== void 0 && (!isNum(layer.life.exit) || layer.life.exit < 0)) {
|
|
328
|
+
err(`${path}.life.exit`, "must be >= 0 (ms)");
|
|
329
|
+
}
|
|
330
|
+
if (isNum(layer.life.enter) && isNum(layer.life.exit) && layer.life.exit <= layer.life.enter) {
|
|
331
|
+
err(`${path}.life.exit`, "must be greater than life.enter");
|
|
332
|
+
}
|
|
333
|
+
if (layer.life.fade !== void 0 && (!isNum(layer.life.fade) || layer.life.fade < 0)) {
|
|
334
|
+
err(`${path}.life.fade`, "must be >= 0 (ms)");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (layer.trail !== void 0) {
|
|
339
|
+
if (!isObj(layer.trail)) err(`${path}.trail`, "must be an object");
|
|
340
|
+
else {
|
|
341
|
+
if (!isNum(layer.trail.length) || layer.trail.length <= 0 || layer.trail.length > LIMITS.maxTrailLength) {
|
|
342
|
+
err(`${path}.trail.length`, `must be > 0 and <= ${LIMITS.maxTrailLength} ms`);
|
|
343
|
+
}
|
|
344
|
+
if (layer.trail.fade !== void 0 && (!isNum(layer.trail.fade) || layer.trail.fade < 0 || layer.trail.fade > 1)) {
|
|
345
|
+
err(`${path}.trail.fade`, "must be 0..1");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
validateSprite(layer.sprite, `${path}.sprite`, err, warn);
|
|
350
|
+
validateMotion(layer.motion, `${path}.motion`, err, warn, spec);
|
|
176
351
|
}
|
|
177
|
-
function validateSprite(sprite, path, err) {
|
|
352
|
+
function validateSprite(sprite, path, err, warn) {
|
|
178
353
|
if (!isObj(sprite)) return err(path, "must be an object");
|
|
354
|
+
let knownSet;
|
|
179
355
|
if (sprite.kind === "emoji") {
|
|
356
|
+
knownSet = KNOWN_EMOJI;
|
|
180
357
|
if (!Array.isArray(sprite.glyphs) || sprite.glyphs.length === 0 || !sprite.glyphs.every(isStr)) {
|
|
181
358
|
err(`${path}.glyphs`, "must be a non-empty array of strings");
|
|
182
359
|
}
|
|
183
360
|
validateCycle(sprite, path, err);
|
|
184
361
|
} else if (sprite.kind === "text") {
|
|
362
|
+
knownSet = KNOWN_TEXT;
|
|
185
363
|
if (!Array.isArray(sprite.strings) || sprite.strings.length === 0 || !sprite.strings.every(isStr)) {
|
|
186
364
|
err(`${path}.strings`, "must be a non-empty array of strings");
|
|
187
365
|
}
|
|
@@ -197,18 +375,59 @@ function validateSprite(sprite, path, err) {
|
|
|
197
375
|
}
|
|
198
376
|
validateCycle(sprite, path, err);
|
|
199
377
|
} else if (sprite.kind === "circle") {
|
|
378
|
+
knownSet = KNOWN_CIRCLE;
|
|
200
379
|
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
|
|
201
380
|
color(sprite.color, `${path}.color`, err);
|
|
202
381
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
382
|
+
validatePalette(sprite, path, err);
|
|
383
|
+
} else if (sprite.kind === "ring") {
|
|
384
|
+
knownSet = KNOWN_RING;
|
|
385
|
+
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
|
|
386
|
+
color(sprite.color, `${path}.color`, err);
|
|
387
|
+
if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
|
|
388
|
+
validatePalette(sprite, path, err);
|
|
389
|
+
} else if (sprite.kind === "streak") {
|
|
390
|
+
knownSet = KNOWN_STREAK;
|
|
391
|
+
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, "must be a [min,max] range of positive px");
|
|
392
|
+
color(sprite.color, `${path}.color`, err);
|
|
393
|
+
if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
|
|
394
|
+
validatePalette(sprite, path, err);
|
|
395
|
+
} else if (sprite.kind === "rect") {
|
|
396
|
+
knownSet = KNOWN_RECT;
|
|
397
|
+
if (!isRange(sprite.width) || sprite.width[0] <= 0) err(`${path}.width`, "must be a [min,max] range of positive px");
|
|
398
|
+
if (sprite.aspect !== void 0 && (!isRange(sprite.aspect) || sprite.aspect[0] <= 0)) {
|
|
399
|
+
err(`${path}.aspect`, "must be a [min,max] range of positive height/width ratios");
|
|
209
400
|
}
|
|
401
|
+
color(sprite.color, `${path}.color`, err);
|
|
402
|
+
validatePalette(sprite, path, err);
|
|
210
403
|
} else {
|
|
211
|
-
err(`${path}.kind`, "must be emoji | text | circle");
|
|
404
|
+
err(`${path}.kind`, "must be emoji | text | circle | ring | streak | rect");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
for (const k of unknownKeys(sprite, knownSet)) {
|
|
408
|
+
if (LAYER_PROPS_ON_SPRITE.has(k)) {
|
|
409
|
+
warn(`${path}.${k}`, "misplaced-property", `'${k}' belongs on the layer, not inside sprite \u2014 move it up one level`);
|
|
410
|
+
} else {
|
|
411
|
+
warn(`${path}.${k}`, "unknown-property", `unknown sprite property '${k}' \u2014 will be ignored`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function validatePalette(sprite, path, err) {
|
|
416
|
+
if (sprite.colors !== void 0) {
|
|
417
|
+
if (!Array.isArray(sprite.colors) || sprite.colors.length === 0) {
|
|
418
|
+
err(`${path}.colors`, "must be a non-empty array of hex colours");
|
|
419
|
+
} else {
|
|
420
|
+
sprite.colors.forEach((c, ci) => color(c, `${path}.colors[${ci}]`, err));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (sprite.colorWeights !== void 0) {
|
|
424
|
+
if (!Array.isArray(sprite.colors)) {
|
|
425
|
+
err(`${path}.colorWeights`, "requires colors to be set");
|
|
426
|
+
} else if (!Array.isArray(sprite.colorWeights) || sprite.colorWeights.length !== sprite.colors.length) {
|
|
427
|
+
err(`${path}.colorWeights`, "must be an array the same length as colors");
|
|
428
|
+
} else if (!sprite.colorWeights.every((v) => isNum(v) && v >= 0) || !sprite.colorWeights.some((v) => isNum(v) && v > 0)) {
|
|
429
|
+
err(`${path}.colorWeights`, "weights must be >= 0 with at least one > 0");
|
|
430
|
+
}
|
|
212
431
|
}
|
|
213
432
|
}
|
|
214
433
|
function validateCycle(sprite, path, err) {
|
|
@@ -218,29 +437,92 @@ function validateCycle(sprite, path, err) {
|
|
|
218
437
|
err(`${path}.cycle.period`, `must be >= ${LIMITS.minCyclePeriod} ms (flash-safety floor)`);
|
|
219
438
|
}
|
|
220
439
|
}
|
|
221
|
-
function validateMotion(motion, path, err, spec) {
|
|
440
|
+
function validateMotion(motion, path, err, warn, spec) {
|
|
222
441
|
if (!isObj(motion)) return err(path, "must be an object");
|
|
223
|
-
const isViewport = isObj(spec)
|
|
224
|
-
const
|
|
442
|
+
const isViewport = !isObj(spec) || spec.units !== "px";
|
|
443
|
+
const refVp = isObj(spec) && isNum(spec.referenceViewport) ? spec.referenceViewport : LIMITS.referenceViewport;
|
|
444
|
+
const speedCap = isViewport ? LIMITS.maxSpeed / refVp : LIMITS.maxSpeed;
|
|
225
445
|
const speedOk = (v, p) => {
|
|
226
446
|
if (!isRange(v)) err(p, "must be a [min,max] range");
|
|
447
|
+
else if (v[0] < 0) err(p, "speed lower bound must be >= 0");
|
|
227
448
|
else if (v[1] > speedCap) err(p, `speed exceeds cap ${isViewport ? speedCap.toFixed(2) + " viewport-units/sec" : LIMITS.maxSpeed + " px/sec"}`);
|
|
228
449
|
};
|
|
450
|
+
let knownSet;
|
|
229
451
|
if (motion.type === "drift") {
|
|
452
|
+
knownSet = KNOWN_DRIFT;
|
|
230
453
|
speedOk(motion.speed, `${path}.speed`);
|
|
231
454
|
if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
|
|
232
455
|
if (motion.bidirectional !== void 0 && typeof motion.bidirectional !== "boolean") err(`${path}.bidirectional`, "must be a boolean");
|
|
233
456
|
if (motion.bob !== void 0 && !isNum(motion.bob)) err(`${path}.bob`, "must be a number");
|
|
457
|
+
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
458
|
+
warn(`${path}.speed`, "near-zero-speed", `max speed is ${motion.speed[1]} px/sec \u2014 entities will appear frozen. Typical range: 10\u2013200 px/sec`);
|
|
459
|
+
}
|
|
234
460
|
} else if (motion.type === "rise") {
|
|
461
|
+
knownSet = KNOWN_RISE;
|
|
235
462
|
speedOk(motion.speed, `${path}.speed`);
|
|
236
463
|
if (motion.sway !== void 0 && !isNum(motion.sway)) err(`${path}.sway`, "must be a number");
|
|
464
|
+
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
465
|
+
warn(`${path}.speed`, "near-zero-speed", `max speed is ${motion.speed[1]} px/sec \u2014 entities will appear frozen. Typical range: 5\u201380 px/sec`);
|
|
466
|
+
}
|
|
237
467
|
} else if (motion.type === "bounce") {
|
|
468
|
+
knownSet = KNOWN_BOUNCE;
|
|
238
469
|
speedOk(motion.speed, `${path}.speed`);
|
|
470
|
+
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
471
|
+
warn(`${path}.speed`, "near-zero-speed", `max speed is ${motion.speed[1]} px/sec \u2014 entities will appear frozen. Typical range: 20\u2013150 px/sec`);
|
|
472
|
+
}
|
|
239
473
|
} else if (motion.type === "static") {
|
|
474
|
+
knownSet = KNOWN_STATIC;
|
|
240
475
|
} else if (motion.type === "orbit") {
|
|
476
|
+
knownSet = KNOWN_ORBIT;
|
|
241
477
|
if (!isRange(motion.speed)) err(`${path}.speed`, "must be a [min,max] range");
|
|
242
|
-
else
|
|
478
|
+
else {
|
|
479
|
+
if (Math.abs(motion.speed[0]) > LIMITS.maxOrbitSpeed || Math.abs(motion.speed[1]) > LIMITS.maxOrbitSpeed) err(`${path}.speed`, `orbit speed exceeds cap ${LIMITS.maxOrbitSpeed} deg/sec`);
|
|
480
|
+
if (Math.max(Math.abs(motion.speed[0]), Math.abs(motion.speed[1])) < 1) {
|
|
481
|
+
warn(`${path}.speed`, "near-zero-speed", `orbit speed is near zero \u2014 entities will appear frozen. Typical range: 5\u201360 deg/sec`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
243
484
|
if (!isRange(motion.radius) || motion.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
|
|
485
|
+
if (motion.center !== void 0) {
|
|
486
|
+
if (!isObj(motion.center)) {
|
|
487
|
+
err(`${path}.center`, "must be {x, y} (0..1) or { layer: key }");
|
|
488
|
+
} else if ("layer" in motion.center) {
|
|
489
|
+
if (!isStr(motion.center.layer) || motion.center.layer.trim() === "") {
|
|
490
|
+
err(`${path}.center.layer`, "must be a non-empty layer key");
|
|
491
|
+
} else if (isObj(spec) && Array.isArray(spec.layers)) {
|
|
492
|
+
const layers = spec.layers;
|
|
493
|
+
const parent = layers.find((l) => isObj(l) && l.key === motion.center.layer);
|
|
494
|
+
if (!parent) {
|
|
495
|
+
err(`${path}.center.layer`, `no layer with key '${motion.center.layer}'`);
|
|
496
|
+
} else {
|
|
497
|
+
if (parent.count !== 1) err(`${path}.center.layer`, "parent layer must have count: 1");
|
|
498
|
+
const pm = parent.motion;
|
|
499
|
+
if (isObj(pm) && pm.type === "orbit" && isObj(pm.center) && "layer" in pm.center) {
|
|
500
|
+
err(`${path}.center.layer`, "parent may not itself orbit a layer (one level deep only)");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} else if (!isNum(motion.center.x) || !isNum(motion.center.y)) {
|
|
505
|
+
err(`${path}.center`, "must be {x, y} with numbers 0..1");
|
|
506
|
+
} else {
|
|
507
|
+
if (motion.center.x < 0 || motion.center.x > 1) err(`${path}.center.x`, "must be 0..1");
|
|
508
|
+
if (motion.center.y < 0 || motion.center.y > 1) err(`${path}.center.y`, "must be 0..1");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} else if (motion.type === "wander") {
|
|
512
|
+
knownSet = KNOWN_WANDER;
|
|
513
|
+
speedOk(motion.speed, `${path}.speed`);
|
|
514
|
+
if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
|
|
515
|
+
const meanderCap = isViewport ? LIMITS.maxMeander / refVp : LIMITS.maxMeander;
|
|
516
|
+
if (motion.meander !== void 0 && (!isNum(motion.meander) || motion.meander <= 0 || motion.meander > meanderCap)) {
|
|
517
|
+
err(`${path}.meander`, `must be > 0 and <= ${isViewport ? meanderCap.toFixed(3) + " viewport-units" : LIMITS.maxMeander + " px"}`);
|
|
518
|
+
}
|
|
519
|
+
if (motion.coherence !== void 0 && (!isNum(motion.coherence) || motion.coherence < 0 || motion.coherence > 1)) {
|
|
520
|
+
err(`${path}.coherence`, "must be 0..1");
|
|
521
|
+
}
|
|
522
|
+
} else if (motion.type === "warp") {
|
|
523
|
+
knownSet = KNOWN_WARP;
|
|
524
|
+
if (!isRange(motion.speed) || motion.speed[0] < 0) err(`${path}.speed`, "must be a [min,max] range >= 0 (depth-units/sec)");
|
|
525
|
+
else if (motion.speed[1] > LIMITS.maxWarpSpeed) err(`${path}.speed`, `warp speed exceeds cap ${LIMITS.maxWarpSpeed} depth-units/sec`);
|
|
244
526
|
if (motion.center !== void 0) {
|
|
245
527
|
if (!isObj(motion.center) || !isNum(motion.center.x) || !isNum(motion.center.y)) {
|
|
246
528
|
err(`${path}.center`, "must be {x, y} with numbers 0..1");
|
|
@@ -249,8 +531,34 @@ function validateMotion(motion, path, err, spec) {
|
|
|
249
531
|
if (motion.center.y < 0 || motion.center.y > 1) err(`${path}.center.y`, "must be 0..1");
|
|
250
532
|
}
|
|
251
533
|
}
|
|
534
|
+
} else if (motion.type === "path") {
|
|
535
|
+
knownSet = KNOWN_PATH;
|
|
536
|
+
if (!Array.isArray(motion.points) || motion.points.length < LIMITS.minPathPoints || motion.points.length > LIMITS.maxPathPoints) {
|
|
537
|
+
err(`${path}.points`, `must be an array of ${LIMITS.minPathPoints}..${LIMITS.maxPathPoints} points`);
|
|
538
|
+
} else {
|
|
539
|
+
motion.points.forEach((p, pi) => {
|
|
540
|
+
if (!isObj(p) || !isNum(p.x) || !isNum(p.y) || p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1) {
|
|
541
|
+
err(`${path}.points[${pi}]`, "must be {x, y} with numbers 0..1");
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (!isNum(motion.duration) || motion.duration < LIMITS.minPathDuration) {
|
|
546
|
+
err(`${path}.duration`, `must be >= ${LIMITS.minPathDuration} ms`);
|
|
547
|
+
}
|
|
548
|
+
if (motion.curve !== void 0 && motion.curve !== "linear" && motion.curve !== "smooth") {
|
|
549
|
+
err(`${path}.curve`, "must be 'linear' | 'smooth'");
|
|
550
|
+
}
|
|
551
|
+
if (motion.closed !== void 0 && typeof motion.closed !== "boolean") err(`${path}.closed`, "must be a boolean");
|
|
552
|
+
const scatterCap = isViewport ? LIMITS.maxMeander / refVp : LIMITS.maxMeander;
|
|
553
|
+
if (motion.scatter !== void 0 && (!isNum(motion.scatter) || motion.scatter <= 0 || motion.scatter > scatterCap)) {
|
|
554
|
+
err(`${path}.scatter`, `must be > 0 and <= ${isViewport ? scatterCap.toFixed(3) + " viewport-units" : LIMITS.maxMeander + " px"}`);
|
|
555
|
+
}
|
|
252
556
|
} else {
|
|
253
|
-
err(`${path}.type`, "must be drift | rise | bounce | static | orbit");
|
|
557
|
+
err(`${path}.type`, "must be drift | rise | bounce | static | orbit | wander | warp | path");
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
for (const k of unknownKeys(motion, knownSet)) {
|
|
561
|
+
warn(`${path}.${k}`, "unknown-property", `unknown motion property '${k}' \u2014 will be ignored`);
|
|
254
562
|
}
|
|
255
563
|
}
|
|
256
564
|
function assertValidSpec(spec) {
|
|
@@ -263,23 +571,98 @@ ${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
|
263
571
|
}
|
|
264
572
|
|
|
265
573
|
// src/simulate.ts
|
|
574
|
+
var WARP_NEAR = 0.08;
|
|
575
|
+
var WARP_MAX_SCALE = 8;
|
|
266
576
|
function spriteVariants(sprite) {
|
|
267
577
|
if (sprite.kind === "emoji") return sprite.glyphs.length;
|
|
268
578
|
if (sprite.kind === "text") return sprite.strings.length;
|
|
269
579
|
return 1;
|
|
270
580
|
}
|
|
271
|
-
|
|
581
|
+
var OCT_AMP = [1, 0.5, 0.25];
|
|
582
|
+
var OCT_PERIOD = [
|
|
583
|
+
[6e3, 14e3],
|
|
584
|
+
[3e3, 7e3],
|
|
585
|
+
[1500, 4e3]
|
|
586
|
+
];
|
|
587
|
+
function drawOsc(rng, amp) {
|
|
588
|
+
const osc = { ax: [], fx: [], phx: [], ay: [], fy: [], phy: [] };
|
|
589
|
+
for (let i = 0; i < 3; i++) {
|
|
590
|
+
osc.ax.push(amp * OCT_AMP[i] * rng.range(0.6, 1.4));
|
|
591
|
+
osc.fx.push(2 * Math.PI / rng.range(OCT_PERIOD[i][0], OCT_PERIOD[i][1]));
|
|
592
|
+
osc.phx.push(rng.range(0, Math.PI * 2));
|
|
593
|
+
}
|
|
594
|
+
for (let i = 0; i < 3; i++) {
|
|
595
|
+
osc.ay.push(amp * OCT_AMP[i] * rng.range(0.6, 1.4));
|
|
596
|
+
osc.fy.push(2 * Math.PI / rng.range(OCT_PERIOD[i][0], OCT_PERIOD[i][1]));
|
|
597
|
+
osc.phy.push(rng.range(0, Math.PI * 2));
|
|
598
|
+
}
|
|
599
|
+
return osc;
|
|
600
|
+
}
|
|
601
|
+
function weightedIndex(u, weights) {
|
|
602
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
603
|
+
let acc = 0;
|
|
604
|
+
for (let i = 0; i < weights.length; i++) {
|
|
605
|
+
acc += weights[i] / total;
|
|
606
|
+
if (u < acc) return i;
|
|
607
|
+
}
|
|
608
|
+
return weights.length - 1;
|
|
609
|
+
}
|
|
610
|
+
function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
611
|
+
const effectiveCount = countScale === 1 ? layer.count : Math.max(1, Math.min(
|
|
612
|
+
Math.round(layer.count * countScale),
|
|
613
|
+
layer.links ? LIMITS.maxLinkLayerCount : LIMITS.maxPerLayer
|
|
614
|
+
));
|
|
272
615
|
const [smin, smax] = layer.size ?? [20, 40];
|
|
273
616
|
const variants = spriteVariants(layer.sprite);
|
|
274
|
-
const
|
|
275
|
-
const
|
|
617
|
+
const sprite = layer.sprite;
|
|
618
|
+
const spriteColors = sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect" ? sprite.colors : void 0;
|
|
619
|
+
const colorWeights = sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect" ? sprite.colorWeights : void 0;
|
|
620
|
+
const colorsLen = spriteColors?.length ?? 0;
|
|
621
|
+
const cycle = sprite.kind === "emoji" || sprite.kind === "text" ? sprite.cycle : void 0;
|
|
622
|
+
const mSpec = layer.motion;
|
|
623
|
+
let sharedOsc = null;
|
|
624
|
+
let meanderPx = 0;
|
|
625
|
+
if (mSpec.type === "wander") {
|
|
626
|
+
meanderPx = (mSpec.meander ?? (scale === 1 ? 60 : 0.05)) * scale;
|
|
627
|
+
sharedOsc = drawOsc(rng, meanderPx);
|
|
628
|
+
}
|
|
629
|
+
let pathPts = null;
|
|
630
|
+
if (mSpec.type === "path") {
|
|
631
|
+
pathPts = mSpec.points.map((p) => ({ x: p.x * w, y: p.y * h }));
|
|
632
|
+
}
|
|
633
|
+
const layout = layer.layout;
|
|
634
|
+
let gridCols = 0;
|
|
635
|
+
let cellW = 0;
|
|
636
|
+
let cellH = 0;
|
|
637
|
+
let gridX0 = 0;
|
|
638
|
+
let gridY0 = 0;
|
|
639
|
+
if (layout?.type === "grid") {
|
|
640
|
+
const [rx0, rx1] = layer.region?.x ?? [0, 1];
|
|
641
|
+
const [ry0, ry1] = layer.region?.y ?? [0, 1];
|
|
642
|
+
const gw = Math.max(1, (rx1 - rx0) * w);
|
|
643
|
+
const gh = Math.max(1, (ry1 - ry0) * h);
|
|
644
|
+
gridCols = Math.min(
|
|
645
|
+
layout.columns ?? Math.max(1, Math.round(Math.sqrt(effectiveCount * (gw / gh)))),
|
|
646
|
+
LIMITS.maxGridColumns
|
|
647
|
+
);
|
|
648
|
+
const gridRows = Math.max(1, Math.ceil(effectiveCount / gridCols));
|
|
649
|
+
cellW = gw / gridCols;
|
|
650
|
+
cellH = gh / gridRows;
|
|
651
|
+
gridX0 = rx0 * w;
|
|
652
|
+
gridY0 = ry0 * h;
|
|
653
|
+
}
|
|
276
654
|
const out = [];
|
|
277
|
-
for (let i = 0; i <
|
|
278
|
-
const size =
|
|
655
|
+
for (let i = 0; i < effectiveCount; i++) {
|
|
656
|
+
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;
|
|
657
|
+
const size2 = sprite.kind === "rect" ? size * (sprite.aspect ? rng.range(sprite.aspect[0], sprite.aspect[1]) : 1) : void 0;
|
|
279
658
|
let vx = 0;
|
|
280
659
|
let vy = 0;
|
|
281
660
|
let bob = 0;
|
|
282
661
|
let motion = "drift";
|
|
662
|
+
let warpParams;
|
|
663
|
+
let pathPhase = 0;
|
|
664
|
+
let pathOffX = 0;
|
|
665
|
+
let pathOffY = 0;
|
|
283
666
|
const m = layer.motion;
|
|
284
667
|
if (m.type === "static") {
|
|
285
668
|
motion = "static";
|
|
@@ -303,18 +686,53 @@ function buildEntities(layer, rng, w, h, scale = 1) {
|
|
|
303
686
|
} else if (m.type === "orbit") {
|
|
304
687
|
motion = "orbit";
|
|
305
688
|
vx = rng.range(m.speed[0], m.speed[1]);
|
|
689
|
+
} else if (m.type === "wander") {
|
|
690
|
+
motion = "wander";
|
|
691
|
+
const s = rng.range(m.speed[0], m.speed[1]) * scale;
|
|
692
|
+
const heading = m.angle !== void 0 ? m.angle * Math.PI / 180 : rng.range(0, Math.PI * 2);
|
|
693
|
+
vx = s * Math.cos(heading);
|
|
694
|
+
vy = s * Math.sin(heading);
|
|
695
|
+
} else if (m.type === "warp") {
|
|
696
|
+
motion = "warp";
|
|
697
|
+
const vz = rng.range(m.speed[0], m.speed[1]);
|
|
698
|
+
const theta = rng.range(0, Math.PI * 2);
|
|
699
|
+
const r = rng.range(0.15, 1);
|
|
700
|
+
warpParams = {
|
|
701
|
+
ux: Math.cos(theta) * r,
|
|
702
|
+
uy: Math.sin(theta) * r,
|
|
703
|
+
z0: rng.range(WARP_NEAR, 1),
|
|
704
|
+
vz,
|
|
705
|
+
cx: (m.center?.x ?? 0.5) * w,
|
|
706
|
+
cy: (m.center?.y ?? 0.5) * h
|
|
707
|
+
};
|
|
708
|
+
} else if (m.type === "path") {
|
|
709
|
+
motion = "path";
|
|
710
|
+
pathPhase = rng.next();
|
|
711
|
+
if (m.scatter) {
|
|
712
|
+
pathOffX = rng.range(-1, 1) * m.scatter * scale;
|
|
713
|
+
pathOffY = rng.range(-1, 1) * m.scatter * scale;
|
|
714
|
+
}
|
|
306
715
|
}
|
|
307
716
|
let x0;
|
|
308
717
|
let y0;
|
|
309
718
|
if (layer.position && layer.count === 1) {
|
|
310
719
|
x0 = layer.position.x * w;
|
|
311
720
|
y0 = layer.position.y * h;
|
|
721
|
+
} else if (layout?.type === "grid") {
|
|
722
|
+
const col = i % gridCols;
|
|
723
|
+
const row = Math.floor(i / gridCols);
|
|
724
|
+
const j = layout.jitter ?? 0;
|
|
725
|
+
const jx = typeof j === "number" ? j : j.x ?? 0;
|
|
726
|
+
const jy = typeof j === "number" ? j : j.y ?? 0;
|
|
727
|
+
x0 = gridX0 + (col + 0.5) * cellW + rng.range(-0.5, 0.5) * jx * cellW;
|
|
728
|
+
y0 = gridY0 + (row + 0.5) * cellH + rng.range(-0.5, 0.5) * jy * cellH;
|
|
312
729
|
} else {
|
|
313
730
|
const [rx0, rx1] = layer.region?.x ?? [0, 1];
|
|
314
731
|
const [ry0, ry1] = layer.region?.y ?? [0, 1];
|
|
315
732
|
x0 = rng.range(rx0 * w, rx1 * w);
|
|
316
733
|
y0 = rng.range(ry0 * h, ry1 * h);
|
|
317
734
|
}
|
|
735
|
+
const orbitParent = m.type === "orbit" && m.center && "layer" in m.center ? m.center.layer : void 0;
|
|
318
736
|
out.push({
|
|
319
737
|
x0,
|
|
320
738
|
y0,
|
|
@@ -338,29 +756,79 @@ function buildEntities(layer, rng, w, h, scale = 1) {
|
|
|
338
756
|
growPeriod: layer.grow?.period ?? 1e3,
|
|
339
757
|
growPhase: layer.grow ? rng.range(0, Math.PI * 2) : 0,
|
|
340
758
|
orbitR: m.type === "orbit" ? rng.range(m.radius[0], m.radius[1]) * scale : 0,
|
|
341
|
-
orbitCx: m.type === "orbit" ? (m.center?.x ?? 0.5) * w : 0,
|
|
342
|
-
orbitCy: m.type === "orbit" ? (m.center?.y ?? 0.5) * h : 0,
|
|
759
|
+
orbitCx: m.type === "orbit" && !orbitParent ? (m.center?.x ?? 0.5) * w : 0,
|
|
760
|
+
orbitCy: m.type === "orbit" && !orbitParent ? (m.center?.y ?? 0.5) * h : 0,
|
|
343
761
|
// New draws AFTER orbit — guarded so existing specs keep identical streams.
|
|
344
|
-
colorIndex: colorsLen > 0 ? rng.int(0, colorsLen - 1) : -1,
|
|
345
|
-
cyclePeriod: cycle ? cycle.period : 0
|
|
762
|
+
colorIndex: colorsLen > 0 ? colorWeights && colorWeights.length === colorsLen ? weightedIndex(rng.next(), colorWeights) : rng.int(0, colorsLen - 1) : -1,
|
|
763
|
+
cyclePeriod: cycle ? cycle.period : 0,
|
|
764
|
+
// Optional feature fields last (guarded draws inside — wander draws 18 here).
|
|
765
|
+
...size2 !== void 0 ? { size2 } : {},
|
|
766
|
+
...orbitParent ? { orbitParent } : {},
|
|
767
|
+
...m.type === "wander" ? {
|
|
768
|
+
wander: {
|
|
769
|
+
own: drawOsc(rng, meanderPx),
|
|
770
|
+
shared: sharedOsc,
|
|
771
|
+
coherence: Math.max(0, Math.min(1, m.coherence ?? 0)),
|
|
772
|
+
margin: meanderPx * (OCT_AMP[0] + OCT_AMP[1] + OCT_AMP[2]) * 1.4
|
|
773
|
+
}
|
|
774
|
+
} : {},
|
|
775
|
+
...warpParams ? { warp: warpParams } : {},
|
|
776
|
+
...m.type === "path" && pathPts ? {
|
|
777
|
+
path: {
|
|
778
|
+
pts: pathPts,
|
|
779
|
+
duration: m.duration,
|
|
780
|
+
closed: m.closed !== false,
|
|
781
|
+
smooth: m.curve !== "linear",
|
|
782
|
+
phase: pathPhase,
|
|
783
|
+
offX: pathOffX,
|
|
784
|
+
offY: pathOffY
|
|
785
|
+
}
|
|
786
|
+
} : {}
|
|
346
787
|
});
|
|
788
|
+
if (layer.pulse?.wave) {
|
|
789
|
+
const wl = Math.max(1e-6, layer.pulse.wave.wavelength * scale);
|
|
790
|
+
const ang = (layer.pulse.wave.angle ?? 0) * Math.PI / 180;
|
|
791
|
+
out[out.length - 1].pulsePhase = -((x0 * Math.cos(ang) + y0 * Math.sin(ang)) / wl) * Math.PI * 2;
|
|
792
|
+
}
|
|
347
793
|
}
|
|
348
794
|
return out;
|
|
349
795
|
}
|
|
796
|
+
function warpZ(e, t) {
|
|
797
|
+
const wp = e.warp;
|
|
798
|
+
return wrap(wp.z0 - wp.vz * (t / 1e3), WARP_NEAR, 1);
|
|
799
|
+
}
|
|
350
800
|
function alphaAt(e, t) {
|
|
351
|
-
|
|
352
|
-
|
|
801
|
+
let a = e.alpha;
|
|
802
|
+
if (e.pulseAmp) a = e.alpha + e.pulseAmp * Math.sin(t * 2 * Math.PI / e.pulsePeriod + e.pulsePhase);
|
|
803
|
+
if (e.motion === "warp") {
|
|
804
|
+
const z = warpZ(e, t);
|
|
805
|
+
a *= Math.max(0, Math.min(1, (1 - z) / 0.2));
|
|
806
|
+
}
|
|
353
807
|
return a < 0 ? 0 : a > 1 ? 1 : a;
|
|
354
808
|
}
|
|
355
809
|
function sizeAt(e, t) {
|
|
356
|
-
|
|
357
|
-
|
|
810
|
+
let s = e.size;
|
|
811
|
+
if (e.growAmp) s = e.size * (1 + e.growAmp * Math.sin(t * 2 * Math.PI / e.growPeriod + e.growPhase));
|
|
812
|
+
if (e.motion === "warp") s *= Math.min(1 / warpZ(e, t), WARP_MAX_SCALE);
|
|
358
813
|
return s > 0 ? s : 0.1;
|
|
359
814
|
}
|
|
360
815
|
function rotationAt(e, t) {
|
|
361
816
|
if (!e.spinSpeed) return 0;
|
|
362
817
|
return e.spinPhase + e.spinSpeed * (t / 1e3);
|
|
363
818
|
}
|
|
819
|
+
function lifeAlphaAt(life, t) {
|
|
820
|
+
if (!life) return 1;
|
|
821
|
+
const fade = life.fade ?? 1e3;
|
|
822
|
+
let a = 1;
|
|
823
|
+
if (life.enter !== void 0) {
|
|
824
|
+
if (t < life.enter) return 0;
|
|
825
|
+
a = fade > 0 ? Math.min(1, (t - life.enter) / fade) : 1;
|
|
826
|
+
}
|
|
827
|
+
if (life.exit !== void 0 && t >= life.exit) {
|
|
828
|
+
a *= fade > 0 ? Math.max(0, 1 - (t - life.exit) / fade) : 0;
|
|
829
|
+
}
|
|
830
|
+
return a;
|
|
831
|
+
}
|
|
364
832
|
function wrap(v, min, max) {
|
|
365
833
|
const range = max - min;
|
|
366
834
|
return ((v - min) % range + range) % range + min;
|
|
@@ -372,6 +840,40 @@ function reflect(v, min, max) {
|
|
|
372
840
|
const p = ((v - min) % period + period) % period;
|
|
373
841
|
return min + (p < range ? p : period - p);
|
|
374
842
|
}
|
|
843
|
+
function catmullRom(p0, p1, p2, p3, u) {
|
|
844
|
+
const u2 = u * u;
|
|
845
|
+
const u3 = u2 * u;
|
|
846
|
+
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
847
|
+
}
|
|
848
|
+
function pathPosition(e, t) {
|
|
849
|
+
const p = e.path;
|
|
850
|
+
const n = p.pts.length;
|
|
851
|
+
let s = (t / p.duration + p.phase) % 1;
|
|
852
|
+
if (s < 0) s += 1;
|
|
853
|
+
if (!p.closed) {
|
|
854
|
+
const pp = s * 2;
|
|
855
|
+
s = pp < 1 ? pp : 2 - pp;
|
|
856
|
+
}
|
|
857
|
+
const segs = p.closed ? n : n - 1;
|
|
858
|
+
const u = Math.min(s * segs, segs - 1e-9);
|
|
859
|
+
const i = Math.floor(u);
|
|
860
|
+
const local = u - i;
|
|
861
|
+
const at = (k) => p.closed ? p.pts[(k % n + n) % n] : p.pts[Math.max(0, Math.min(n - 1, k))];
|
|
862
|
+
const p1 = at(i);
|
|
863
|
+
const p2 = at(i + 1);
|
|
864
|
+
if (!p.smooth) {
|
|
865
|
+
return {
|
|
866
|
+
x: p1.x + (p2.x - p1.x) * local + p.offX,
|
|
867
|
+
y: p1.y + (p2.y - p1.y) * local + p.offY
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
const p0 = at(i - 1);
|
|
871
|
+
const p3 = at(i + 2);
|
|
872
|
+
return {
|
|
873
|
+
x: catmullRom(p0.x, p1.x, p2.x, p3.x, local) + p.offX,
|
|
874
|
+
y: catmullRom(p0.y, p1.y, p2.y, p3.y, local) + p.offY
|
|
875
|
+
};
|
|
876
|
+
}
|
|
375
877
|
function positionAt(e, t, w, h) {
|
|
376
878
|
if (e.motion === "static") return { x: e.x0, y: e.y0, flip: false };
|
|
377
879
|
if (e.motion === "orbit") {
|
|
@@ -382,6 +884,20 @@ function positionAt(e, t, w, h) {
|
|
|
382
884
|
flip: false
|
|
383
885
|
};
|
|
384
886
|
}
|
|
887
|
+
if (e.motion === "warp") {
|
|
888
|
+
const wp = e.warp;
|
|
889
|
+
const persp = 1 / warpZ(e, t);
|
|
890
|
+
const halfMin = Math.min(w, h) / 2;
|
|
891
|
+
return {
|
|
892
|
+
x: wp.cx + wp.ux * halfMin * persp,
|
|
893
|
+
y: wp.cy + wp.uy * halfMin * persp,
|
|
894
|
+
flip: false
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (e.motion === "path") {
|
|
898
|
+
const pos = pathPosition(e, t);
|
|
899
|
+
return { x: pos.x, y: pos.y, flip: false };
|
|
900
|
+
}
|
|
385
901
|
const dt = t / 1e3;
|
|
386
902
|
const m = e.size;
|
|
387
903
|
if (e.motion === "bounce") {
|
|
@@ -398,15 +914,94 @@ function positionAt(e, t, w, h) {
|
|
|
398
914
|
flip: false
|
|
399
915
|
};
|
|
400
916
|
}
|
|
917
|
+
if (e.motion === "wander") {
|
|
918
|
+
const wp = e.wander;
|
|
919
|
+
const c = wp.coherence;
|
|
920
|
+
let hx = 0;
|
|
921
|
+
let hy = 0;
|
|
922
|
+
for (let i = 0; i < 3; i++) {
|
|
923
|
+
if (c < 1) {
|
|
924
|
+
hx += (1 - c) * wp.own.ax[i] * Math.sin(wp.own.fx[i] * t + wp.own.phx[i]);
|
|
925
|
+
hy += (1 - c) * wp.own.ay[i] * Math.sin(wp.own.fy[i] * t + wp.own.phy[i]);
|
|
926
|
+
}
|
|
927
|
+
if (c > 0) {
|
|
928
|
+
hx += c * wp.shared.ax[i] * Math.sin(wp.shared.fx[i] * t + wp.shared.phx[i]);
|
|
929
|
+
hy += c * wp.shared.ay[i] * Math.sin(wp.shared.fy[i] * t + wp.shared.phy[i]);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const margin = m + wp.margin;
|
|
933
|
+
return {
|
|
934
|
+
x: wrap(e.x0 + e.vx * dt + hx, -margin, w + margin),
|
|
935
|
+
y: wrap(e.y0 + e.vy * dt + hy, -margin, h + margin),
|
|
936
|
+
flip: e.headingLeft
|
|
937
|
+
};
|
|
938
|
+
}
|
|
401
939
|
const x = wrap(e.x0 + e.vx * dt, -m, w + m);
|
|
402
940
|
let y = e.vy !== 0 ? wrap(e.y0 + e.vy * dt, -m, h + m) : e.y0;
|
|
403
941
|
if (e.bob) y += e.bob * Math.sin(t / 500 + e.phase);
|
|
404
942
|
return { x, y, flip: e.headingLeft };
|
|
405
943
|
}
|
|
944
|
+
function headingAt(e, t, w, h) {
|
|
945
|
+
const dtMs = 32;
|
|
946
|
+
const t1 = t < dtMs ? t + dtMs : t;
|
|
947
|
+
const t0 = t1 - dtMs;
|
|
948
|
+
const a = positionAt(e, t0, w, h);
|
|
949
|
+
const b = positionAt(e, t1, w, h);
|
|
950
|
+
const dx = b.x - a.x;
|
|
951
|
+
const dy = b.y - a.y;
|
|
952
|
+
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) return null;
|
|
953
|
+
if (dx === 0 && dy === 0) return null;
|
|
954
|
+
return Math.atan2(dy, dx);
|
|
955
|
+
}
|
|
406
956
|
function spriteIndexAt(e, t, variants) {
|
|
407
957
|
if (!e.cyclePeriod || variants <= 1) return e.spriteIndex;
|
|
408
958
|
return Math.floor(t / e.cyclePeriod + e.phase / (2 * Math.PI)) % variants;
|
|
409
959
|
}
|
|
960
|
+
function wrapDist(pi, pj, doWrap, w, h) {
|
|
961
|
+
let dx = pj.x - pi.x;
|
|
962
|
+
let dy = pj.y - pi.y;
|
|
963
|
+
if (doWrap) {
|
|
964
|
+
if (Math.abs(dx) > w / 2) dx = dx > 0 ? dx - w : dx + w;
|
|
965
|
+
if (Math.abs(dy) > h / 2) dy = dy > 0 ? dy - h : dy + h;
|
|
966
|
+
}
|
|
967
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
968
|
+
}
|
|
969
|
+
function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
970
|
+
const n = positions.length;
|
|
971
|
+
const mode = links.mode ?? "nearest";
|
|
972
|
+
if (mode === "chain") {
|
|
973
|
+
const out = [];
|
|
974
|
+
for (let i = 0; i < n - 1; i++) {
|
|
975
|
+
out.push({ i, j: i + 1, dist: wrapDist(positions[i], positions[i + 1], doWrap, w, h) });
|
|
976
|
+
}
|
|
977
|
+
if (links.closed && n > 2) out.push({ i: n - 1, j: 0, dist: wrapDist(positions[n - 1], positions[0], doWrap, w, h) });
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
980
|
+
if (mode === "random") {
|
|
981
|
+
const stride = Math.max(1, Math.round(n * 0.381966));
|
|
982
|
+
const edges = /* @__PURE__ */ new Set();
|
|
983
|
+
const out = [];
|
|
984
|
+
for (let i = 0; i < n; i++) {
|
|
985
|
+
for (let mIdx = 1; mIdx <= links.k; mIdx++) {
|
|
986
|
+
const j = (i + mIdx * stride) % n;
|
|
987
|
+
if (j === i) continue;
|
|
988
|
+
const dist = wrapDist(positions[i], positions[j], doWrap, w, h);
|
|
989
|
+
if (dist > maxDistPx) continue;
|
|
990
|
+
const key = `${Math.min(i, j)}:${Math.max(i, j)}`;
|
|
991
|
+
if (!edges.has(key)) {
|
|
992
|
+
edges.add(key);
|
|
993
|
+
out.push({ i: Math.min(i, j), j: Math.max(i, j), dist });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
return linkPairs(positions, links.k, maxDistPx, doWrap, w, h).map(([i, j]) => ({
|
|
1000
|
+
i,
|
|
1001
|
+
j,
|
|
1002
|
+
dist: wrapDist(positions[i], positions[j], doWrap, w, h)
|
|
1003
|
+
}));
|
|
1004
|
+
}
|
|
410
1005
|
function linkPairs(positions, k, maxDist, doWrap, w, h) {
|
|
411
1006
|
const n = positions.length;
|
|
412
1007
|
const edges = /* @__PURE__ */ new Set();
|
|
@@ -513,6 +1108,32 @@ function lerpSpec(from, to, k) {
|
|
|
513
1108
|
};
|
|
514
1109
|
return walk(from, to);
|
|
515
1110
|
}
|
|
1111
|
+
function steerablePaths(spec) {
|
|
1112
|
+
if (!spec || typeof spec !== "object") return [];
|
|
1113
|
+
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "mode", "curve", "layer"]);
|
|
1114
|
+
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops"]);
|
|
1115
|
+
const out = [];
|
|
1116
|
+
const walk = (node, prefix, key) => {
|
|
1117
|
+
if (Array.isArray(node) && INDEXED.has(key)) {
|
|
1118
|
+
node.forEach((child, i) => walk(child, `${prefix}.${i}`, ""));
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (Array.isArray(node)) {
|
|
1122
|
+
if (prefix) out.push(prefix);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (node && typeof node === "object") {
|
|
1126
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1127
|
+
if (SKIP.has(k)) continue;
|
|
1128
|
+
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
1129
|
+
}
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (prefix) out.push(prefix);
|
|
1133
|
+
};
|
|
1134
|
+
walk(spec, "", "");
|
|
1135
|
+
return out;
|
|
1136
|
+
}
|
|
516
1137
|
function easeSmooth(k) {
|
|
517
1138
|
const c = Math.max(0, Math.min(1, k));
|
|
518
1139
|
return c * c * (3 - 2 * c);
|
|
@@ -520,23 +1141,32 @@ function easeSmooth(k) {
|
|
|
520
1141
|
function structuralSignature(spec) {
|
|
521
1142
|
return JSON.stringify([
|
|
522
1143
|
spec.units,
|
|
523
|
-
spec.layers.map((l) =>
|
|
524
|
-
l.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1144
|
+
spec.layers.map((l) => {
|
|
1145
|
+
const s = l.sprite;
|
|
1146
|
+
return [
|
|
1147
|
+
l.count,
|
|
1148
|
+
l.size,
|
|
1149
|
+
l.region,
|
|
1150
|
+
l.position,
|
|
1151
|
+
l.motion,
|
|
1152
|
+
l.wrap,
|
|
1153
|
+
l.flip,
|
|
1154
|
+
l.alpha,
|
|
1155
|
+
l.pulse,
|
|
1156
|
+
l.spin,
|
|
1157
|
+
l.grow,
|
|
1158
|
+
l.layout,
|
|
1159
|
+
l.sprite.kind,
|
|
1160
|
+
// Dimensional draws baked into entities: radius (circle/ring), length
|
|
1161
|
+
// (streak), width+aspect (rect), palette pick (all shaped sprites).
|
|
1162
|
+
s.radius,
|
|
1163
|
+
s.length,
|
|
1164
|
+
l.sprite.kind === "rect" ? [s.width, s.aspect] : void 0,
|
|
1165
|
+
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
1166
|
+
s.colorWeights,
|
|
1167
|
+
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
|
|
1168
|
+
];
|
|
1169
|
+
})
|
|
540
1170
|
]);
|
|
541
1171
|
}
|
|
542
1172
|
|
|
@@ -547,6 +1177,12 @@ function hexToRgba(hex, alpha) {
|
|
|
547
1177
|
const n = parseInt(h.slice(1), 16);
|
|
548
1178
|
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${alpha})`;
|
|
549
1179
|
}
|
|
1180
|
+
var FONT_PREFIX_RE = /^((?:(?:italic|oblique|bold|bolder|lighter|normal|\d{3})\s+)+)/;
|
|
1181
|
+
function composeFontShorthand(sz, font) {
|
|
1182
|
+
const m = FONT_PREFIX_RE.exec(font);
|
|
1183
|
+
if (m) return `${m[1].trim()} ${sz}px ${font.slice(m[0].length)}`;
|
|
1184
|
+
return `${sz}px ${font}`;
|
|
1185
|
+
}
|
|
550
1186
|
function manifestFor(spec) {
|
|
551
1187
|
const total = spec.layers.reduce((n, l) => n + l.count, 0);
|
|
552
1188
|
const costTier = total < 30 ? "idle" : total < 150 ? "low" : total < 400 ? "medium" : "high";
|
|
@@ -606,6 +1242,8 @@ var SpecInstance = class {
|
|
|
606
1242
|
baseT = 0;
|
|
607
1243
|
// elapsed logical time carried across pause/resume
|
|
608
1244
|
lastT = 0;
|
|
1245
|
+
/** Last time painted — detects non-contiguous seeks for the ghosting warm-up replay. */
|
|
1246
|
+
lastRenderT = Number.NEGATIVE_INFINITY;
|
|
609
1247
|
/** The spec currently being rendered (base spec + any applied steering). */
|
|
610
1248
|
effSpec;
|
|
611
1249
|
/** Active glide between two resolved specs (live setParam/applyTrack). */
|
|
@@ -616,12 +1254,19 @@ var SpecInstance = class {
|
|
|
616
1254
|
this.canvas.width = Math.max(1, Math.round(this.w * dpr));
|
|
617
1255
|
this.canvas.height = Math.max(1, Math.round(this.h * dpr));
|
|
618
1256
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1257
|
+
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
619
1258
|
}
|
|
620
|
-
/** (Re)seed and place all entities — deterministic for the seed +
|
|
1259
|
+
/** (Re)seed and place all entities — deterministic for the seed + viewport. */
|
|
621
1260
|
rebuild() {
|
|
622
1261
|
const rng = createRng(this.seed);
|
|
623
|
-
const scale = this.effSpec.units === "
|
|
624
|
-
|
|
1262
|
+
const scale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
1263
|
+
const refVp = this.effSpec.referenceViewport ?? LIMITS.referenceViewport;
|
|
1264
|
+
let countScale = scale > 1 ? Math.min(this.w, this.h) / refVp : 1;
|
|
1265
|
+
if (countScale > 1) {
|
|
1266
|
+
const rawTotal = this.effSpec.layers.reduce((s, l) => s + Math.round(l.count * countScale), 0);
|
|
1267
|
+
if (rawTotal > LIMITS.maxTotal) countScale *= LIMITS.maxTotal / rawTotal;
|
|
1268
|
+
}
|
|
1269
|
+
this.layers = this.effSpec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, this.w, this.h, scale, countScale) }));
|
|
625
1270
|
this.lastStructural = structuralSignature(this.effSpec);
|
|
626
1271
|
}
|
|
627
1272
|
start() {
|
|
@@ -642,7 +1287,7 @@ var SpecInstance = class {
|
|
|
642
1287
|
this.lastT = now - this.startT + this.baseT;
|
|
643
1288
|
this.renderFrame(this.lastT, this.seed);
|
|
644
1289
|
}
|
|
645
|
-
drawBackground() {
|
|
1290
|
+
drawBackground(t) {
|
|
646
1291
|
const { ctx, w, h } = this;
|
|
647
1292
|
const bg = this.effSpec.background;
|
|
648
1293
|
if (!bg || bg.type === "solid") {
|
|
@@ -651,22 +1296,136 @@ var SpecInstance = class {
|
|
|
651
1296
|
return;
|
|
652
1297
|
}
|
|
653
1298
|
const g = ctx.createLinearGradient(0, 0, 0, h);
|
|
654
|
-
|
|
1299
|
+
const drift = bg.drift;
|
|
1300
|
+
for (let i = 0; i < bg.stops.length; i++) {
|
|
1301
|
+
const s = bg.stops[i];
|
|
1302
|
+
let at = s.at;
|
|
1303
|
+
if (drift) {
|
|
1304
|
+
const amount = drift.amount ?? 0.15;
|
|
1305
|
+
const phase = i / bg.stops.length * Math.PI * 2;
|
|
1306
|
+
at = Math.max(0, Math.min(1, at + amount * Math.sin(t * 2 * Math.PI / drift.period + phase)));
|
|
1307
|
+
}
|
|
1308
|
+
g.addColorStop(at, s.color);
|
|
1309
|
+
}
|
|
655
1310
|
ctx.fillStyle = g;
|
|
656
1311
|
ctx.fillRect(0, 0, w, h);
|
|
657
1312
|
if (bg.band) {
|
|
658
1313
|
ctx.fillStyle = bg.band.color;
|
|
659
|
-
const bh = bg.band.height * (this.effSpec.units === "
|
|
1314
|
+
const bh = bg.band.height * (this.effSpec.units === "px" ? 1 : Math.min(w, h));
|
|
660
1315
|
ctx.fillRect(0, h - bh, w, bh);
|
|
661
1316
|
}
|
|
662
1317
|
}
|
|
663
|
-
|
|
664
|
-
|
|
1318
|
+
/** Position with parent-orbit resolution: a layer-parented orbit entity's
|
|
1319
|
+
* positionAt is an offset around (0,0); add the parent's own analytic position. */
|
|
1320
|
+
entityPos(e, t, parentE) {
|
|
665
1321
|
const p = positionAt(e, t, this.w, this.h);
|
|
1322
|
+
if (parentE && e.orbitParent) {
|
|
1323
|
+
const pp = positionAt(parentE, t, this.w, this.h);
|
|
1324
|
+
p.x += pp.x;
|
|
1325
|
+
p.y += pp.y;
|
|
1326
|
+
}
|
|
1327
|
+
return p;
|
|
1328
|
+
}
|
|
1329
|
+
/** Resolve the single parent entity for a layer-parented orbit layer (or null). */
|
|
1330
|
+
parentEntityFor(built) {
|
|
1331
|
+
const m = built.layer.motion;
|
|
1332
|
+
if (m.type !== "orbit" || !m.center || !("layer" in m.center)) return null;
|
|
1333
|
+
const key = m.center.layer;
|
|
1334
|
+
const parent = this.layers.find((b) => b.layer.key === key);
|
|
1335
|
+
return parent?.entities[0] ?? null;
|
|
1336
|
+
}
|
|
1337
|
+
drawTrail(built, e, t, lifeA, parentE) {
|
|
1338
|
+
const trail = built.layer.trail;
|
|
1339
|
+
if (!trail) return;
|
|
1340
|
+
const { ctx, w, h } = this;
|
|
1341
|
+
const fade = trail.fade ?? 1;
|
|
1342
|
+
const n = Math.min(Math.ceil(trail.length / 50), LIMITS.maxTrailSamples);
|
|
1343
|
+
const headAlpha = alphaAt(e, t) * lifeA;
|
|
1344
|
+
const headSize = sizeAt(e, t);
|
|
1345
|
+
const sprite = built.layer.sprite;
|
|
1346
|
+
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";
|
|
1347
|
+
const isSoft = sprite.kind === "circle" && sprite.soft;
|
|
1348
|
+
const wrap2 = built.layer.wrap !== false;
|
|
1349
|
+
const head = this.entityPos(e, t, parentE);
|
|
1350
|
+
let prevX = head.x;
|
|
1351
|
+
let prevY = head.y;
|
|
1352
|
+
for (let s = 1; s <= n; s++) {
|
|
1353
|
+
const k = s / n;
|
|
1354
|
+
const pastT = t - k * trail.length;
|
|
1355
|
+
if (pastT < 0) break;
|
|
1356
|
+
const pos = this.entityPos(e, pastT, parentE);
|
|
1357
|
+
if (wrap2) {
|
|
1358
|
+
const dx = pos.x - prevX;
|
|
1359
|
+
const dy = pos.y - prevY;
|
|
1360
|
+
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) break;
|
|
1361
|
+
}
|
|
1362
|
+
prevX = pos.x;
|
|
1363
|
+
prevY = pos.y;
|
|
1364
|
+
const a = headAlpha * (1 - k * fade);
|
|
1365
|
+
if (a <= 0) break;
|
|
1366
|
+
const r = headSize / 2 * (1 - k * 0.7);
|
|
1367
|
+
if (r < 0.2) break;
|
|
1368
|
+
ctx.globalAlpha = a;
|
|
1369
|
+
if (isSoft) {
|
|
1370
|
+
const g = ctx.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, r);
|
|
1371
|
+
g.addColorStop(0, resolvedColor);
|
|
1372
|
+
g.addColorStop(0.35, hexToRgba(resolvedColor, 0.75));
|
|
1373
|
+
g.addColorStop(1, hexToRgba(resolvedColor, 0));
|
|
1374
|
+
ctx.fillStyle = g;
|
|
1375
|
+
} else {
|
|
1376
|
+
ctx.fillStyle = resolvedColor;
|
|
1377
|
+
}
|
|
1378
|
+
ctx.beginPath();
|
|
1379
|
+
ctx.arc(pos.x, pos.y, r, 0, Math.PI * 2);
|
|
1380
|
+
ctx.fill();
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
drawEntity(built, e, t, lifeA, parentE) {
|
|
1384
|
+
const { ctx } = this;
|
|
1385
|
+
const p = this.entityPos(e, t, parentE);
|
|
666
1386
|
const sprite = built.layer.sprite;
|
|
667
1387
|
const sz = sizeAt(e, t);
|
|
668
1388
|
const rot = rotationAt(e, t);
|
|
669
|
-
|
|
1389
|
+
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
1390
|
+
ctx.globalAlpha = alphaAt(e, t) * lifeA;
|
|
1391
|
+
if (sprite.kind === "ring") {
|
|
1392
|
+
const r = sz / 2;
|
|
1393
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
1394
|
+
ctx.strokeStyle = resolvedColor;
|
|
1395
|
+
ctx.lineWidth = Math.max(0.5, (sprite.width ?? (unitScale === 1 ? 2 : 2e-3)) * unitScale);
|
|
1396
|
+
ctx.beginPath();
|
|
1397
|
+
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
|
|
1398
|
+
ctx.stroke();
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (sprite.kind === "streak") {
|
|
1402
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
1403
|
+
const heading = headingAt(e, t, this.w, this.h) ?? 0;
|
|
1404
|
+
const tailX = p.x - Math.cos(heading) * sz;
|
|
1405
|
+
const tailY = p.y - Math.sin(heading) * sz;
|
|
1406
|
+
const g = ctx.createLinearGradient(tailX, tailY, p.x, p.y);
|
|
1407
|
+
g.addColorStop(0, hexToRgba(resolvedColor, 0));
|
|
1408
|
+
g.addColorStop(1, resolvedColor);
|
|
1409
|
+
ctx.strokeStyle = g;
|
|
1410
|
+
ctx.lineWidth = Math.max(0.5, (sprite.width ?? (unitScale === 1 ? 2 : 2e-3)) * unitScale);
|
|
1411
|
+
ctx.lineCap = "round";
|
|
1412
|
+
ctx.beginPath();
|
|
1413
|
+
ctx.moveTo(tailX, tailY);
|
|
1414
|
+
ctx.lineTo(p.x, p.y);
|
|
1415
|
+
ctx.stroke();
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (sprite.kind === "rect") {
|
|
1419
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
1420
|
+
const rh = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
1421
|
+
ctx.save();
|
|
1422
|
+
ctx.translate(p.x, p.y);
|
|
1423
|
+
if (rot) ctx.rotate(rot);
|
|
1424
|
+
ctx.fillStyle = resolvedColor;
|
|
1425
|
+
ctx.fillRect(-sz / 2, -rh / 2, sz, rh);
|
|
1426
|
+
ctx.restore();
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
670
1429
|
if (sprite.kind === "circle") {
|
|
671
1430
|
const r = sz / 2;
|
|
672
1431
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
@@ -704,11 +1463,11 @@ var SpecInstance = class {
|
|
|
704
1463
|
} else {
|
|
705
1464
|
ctx.textAlign = sprite.align ?? "center";
|
|
706
1465
|
ctx.textBaseline = sprite.baseline ?? "middle";
|
|
707
|
-
ctx.font = sprite.font
|
|
1466
|
+
ctx.font = sprite.font ? /\dpx/.test(sprite.font) ? sprite.font : composeFontShorthand(sz, sprite.font) : `${sz}px system-ui, sans-serif`;
|
|
708
1467
|
ctx.fillStyle = sprite.color ?? "#e6e8ef";
|
|
709
1468
|
const idx = spriteIndexAt(e, t, sprite.strings.length);
|
|
710
1469
|
const text = sprite.strings[idx] ?? sprite.strings[0];
|
|
711
|
-
const mw = sprite.maxWidth ? sprite.maxWidth * (this.effSpec.units === "
|
|
1470
|
+
const mw = sprite.maxWidth ? sprite.maxWidth * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h)) : void 0;
|
|
712
1471
|
if (mw) ctx.fillText(text, 0, 0, mw);
|
|
713
1472
|
else ctx.fillText(text, 0, 0);
|
|
714
1473
|
}
|
|
@@ -752,26 +1511,32 @@ var SpecInstance = class {
|
|
|
752
1511
|
this.renderFrame(this.lastT, this.seed);
|
|
753
1512
|
}
|
|
754
1513
|
}
|
|
755
|
-
drawLinks(built, t) {
|
|
1514
|
+
drawLinks(built, t, lifeA, parentE) {
|
|
756
1515
|
const { links } = built.layer;
|
|
757
1516
|
if (!links) return;
|
|
758
1517
|
const { ctx } = this;
|
|
759
|
-
const
|
|
760
|
-
const
|
|
761
|
-
const
|
|
762
|
-
const
|
|
1518
|
+
const motionWraps = ["drift", "rise", "wander"].includes(built.layer.motion.type);
|
|
1519
|
+
const wrap2 = built.layer.wrap !== false && motionWraps;
|
|
1520
|
+
const positions = built.entities.map((e) => this.entityPos(e, t, parentE));
|
|
1521
|
+
const maxDistPx = links.maxDist * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h));
|
|
1522
|
+
const edges = linkEdges(links, positions, maxDistPx, wrap2, this.w, this.h);
|
|
1523
|
+
const lw = (links.width ?? 1) * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h));
|
|
763
1524
|
ctx.lineWidth = lw;
|
|
764
|
-
|
|
1525
|
+
ctx.lineCap = "butt";
|
|
1526
|
+
for (const { i, j, dist } of edges) {
|
|
765
1527
|
const pi = positions[i];
|
|
766
1528
|
const pj = positions[j];
|
|
767
1529
|
const ei = built.entities[i];
|
|
768
1530
|
let resolvedColor = links.color;
|
|
769
1531
|
if (!resolvedColor) {
|
|
770
1532
|
const sprite = built.layer.sprite;
|
|
771
|
-
if (sprite.kind === "circle"
|
|
772
|
-
|
|
1533
|
+
if (sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect") {
|
|
1534
|
+
resolvedColor = sprite.colors?.[ei.colorIndex] ?? sprite.color;
|
|
1535
|
+
} else resolvedColor = "#e6e8ef";
|
|
773
1536
|
}
|
|
774
|
-
|
|
1537
|
+
let a = links.alpha ?? alphaAt(ei, t);
|
|
1538
|
+
if (links.falloff) a *= Math.max(0, 1 - dist / maxDistPx);
|
|
1539
|
+
ctx.globalAlpha = a * lifeA;
|
|
775
1540
|
ctx.strokeStyle = resolvedColor;
|
|
776
1541
|
let dx = pj.x - pi.x;
|
|
777
1542
|
let dy = pj.y - pi.y;
|
|
@@ -785,21 +1550,57 @@ var SpecInstance = class {
|
|
|
785
1550
|
ctx.stroke();
|
|
786
1551
|
}
|
|
787
1552
|
}
|
|
788
|
-
/**
|
|
789
|
-
|
|
790
|
-
|
|
1553
|
+
/** Paint one composite pass at time t. `bgAlpha` < 1 leaves the previous frame
|
|
1554
|
+
* showing through — the ghosting smear. */
|
|
1555
|
+
paintFrame(t, bgAlpha) {
|
|
791
1556
|
const { ctx } = this;
|
|
792
|
-
ctx.globalAlpha =
|
|
1557
|
+
ctx.globalAlpha = bgAlpha;
|
|
793
1558
|
ctx.globalCompositeOperation = "source-over";
|
|
794
|
-
this.drawBackground();
|
|
1559
|
+
this.drawBackground(t);
|
|
1560
|
+
ctx.globalAlpha = 1;
|
|
795
1561
|
for (const built of this.layers) {
|
|
1562
|
+
const lifeA = lifeAlphaAt(built.layer.life, t);
|
|
1563
|
+
if (lifeA <= 0) continue;
|
|
1564
|
+
const parentE = this.parentEntityFor(built);
|
|
796
1565
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
797
|
-
this.drawLinks(built, t);
|
|
798
|
-
for (const e of built.entities)
|
|
1566
|
+
this.drawLinks(built, t, lifeA, parentE);
|
|
1567
|
+
for (const e of built.entities) {
|
|
1568
|
+
this.drawTrail(built, e, t, lifeA, parentE);
|
|
1569
|
+
this.drawEntity(built, e, t, lifeA, parentE);
|
|
1570
|
+
}
|
|
799
1571
|
}
|
|
800
1572
|
ctx.globalAlpha = 1;
|
|
801
1573
|
ctx.globalCompositeOperation = "source-over";
|
|
802
1574
|
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Deterministic, frame-addressable render (shared by the rAF loop).
|
|
1577
|
+
*
|
|
1578
|
+
* With `ghosting` set, contiguous frames composite over the previous frame with a
|
|
1579
|
+
* refresh-rate-normalized fade. A non-contiguous seek (pause render, e2e frame
|
|
1580
|
+
* addressing) replays a bounded fixed-step warm-up from a full clear, so the
|
|
1581
|
+
* accumulated smear at time t is identical on every run — the determinism proof
|
|
1582
|
+
* survives statefulness because the "state" is reconstructed from pure history.
|
|
1583
|
+
*/
|
|
1584
|
+
renderFrame(t, _seed) {
|
|
1585
|
+
this.stepTransition(t);
|
|
1586
|
+
const g = this.effSpec.ghosting ?? 0;
|
|
1587
|
+
if (g > 0) {
|
|
1588
|
+
const dt = 1e3 / 60;
|
|
1589
|
+
const contiguous = this.lastRenderT !== Number.NEGATIVE_INFINITY && t > this.lastRenderT && t - this.lastRenderT <= 250;
|
|
1590
|
+
if (contiguous) {
|
|
1591
|
+
this.paintFrame(t, 1 - Math.pow(g, (t - this.lastRenderT) / dt));
|
|
1592
|
+
} else {
|
|
1593
|
+
const k = Math.min(Math.ceil(Math.log(1 / 255) / Math.log(g)), LIMITS.maxGhostReplayFrames);
|
|
1594
|
+
const t0 = Math.max(0, t - k * dt);
|
|
1595
|
+
this.paintFrame(t0, 1);
|
|
1596
|
+
for (let ft = t0 + dt; ft < t - dt / 2; ft += dt) this.paintFrame(ft, 1 - g);
|
|
1597
|
+
if (t > t0) this.paintFrame(t, 1 - g);
|
|
1598
|
+
}
|
|
1599
|
+
} else {
|
|
1600
|
+
this.paintFrame(t, 1);
|
|
1601
|
+
}
|
|
1602
|
+
this.lastRenderT = t;
|
|
1603
|
+
}
|
|
803
1604
|
setPaused(paused) {
|
|
804
1605
|
this.paused = paused;
|
|
805
1606
|
if (paused) {
|
|
@@ -836,9 +1637,11 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
836
1637
|
const warnings = [];
|
|
837
1638
|
const w = viewport.width;
|
|
838
1639
|
const h = viewport.height;
|
|
839
|
-
const scale = spec.units === "
|
|
1640
|
+
const scale = spec.units === "px" ? 1 : Math.min(w, h);
|
|
1641
|
+
const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
|
|
1642
|
+
const countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
|
|
840
1643
|
const rng = createRng2(spec.seed ?? 42);
|
|
841
|
-
const allEntities = spec.layers.map((l) => buildEntities(l, rng, w, h, scale));
|
|
1644
|
+
const allEntities = spec.layers.map((l) => buildEntities(l, rng, w, h, scale, countScale));
|
|
842
1645
|
let totalEntities = 0;
|
|
843
1646
|
let textLayerCount = 0;
|
|
844
1647
|
let motionLayerCount = 0;
|
|
@@ -847,10 +1650,30 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
847
1650
|
const entities = allEntities[li];
|
|
848
1651
|
totalEntities += entities.length;
|
|
849
1652
|
const isText = layer.sprite.kind === "text";
|
|
850
|
-
const isEmoji = layer.sprite.kind === "emoji";
|
|
851
1653
|
const isStaticText = isText && layer.motion.type === "static";
|
|
852
1654
|
if (isStaticText) textLayerCount++;
|
|
853
1655
|
if (layer.motion.type !== "static") motionLayerCount++;
|
|
1656
|
+
if (layer.trail && layer.motion.type === "static") {
|
|
1657
|
+
warnings.push({
|
|
1658
|
+
path: `layers[${li}].trail`,
|
|
1659
|
+
code: "trail-on-static",
|
|
1660
|
+
message: "trail has no effect on static entities \u2014 they have no past positions to draw"
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
if (layer.sprite.kind === "streak" && layer.motion.type === "static") {
|
|
1664
|
+
warnings.push({
|
|
1665
|
+
path: `layers[${li}].sprite`,
|
|
1666
|
+
code: "streak-on-static",
|
|
1667
|
+
message: "streak sprites orient along the motion heading \u2014 static entities have none and will render at angle 0"
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
if (layer.motion.type === "wander" && layer.motion.coherence === 1 && entities.length >= 20) {
|
|
1671
|
+
warnings.push({
|
|
1672
|
+
path: `layers[${li}].motion.coherence`,
|
|
1673
|
+
code: "full-coherence",
|
|
1674
|
+
message: "coherence 1 makes every entity share identical harmonics \u2014 the layer moves as a rigid sheet. 0.5\u20130.8 reads as schooling"
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
854
1677
|
if (layer.sprite.kind === "circle") {
|
|
855
1678
|
const maxR = entities.reduce((m, e) => Math.max(m, e.size / 2), 0);
|
|
856
1679
|
const maxAlpha = entities.reduce((m, e) => Math.max(m, e.alpha), 0);
|
|
@@ -872,6 +1695,14 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
872
1695
|
const pixArea = layer.sprite.kind === "circle" ? Math.PI * r * r : e.size * e.size;
|
|
873
1696
|
totalCoverage += pixArea * e.alpha / (w * h);
|
|
874
1697
|
}
|
|
1698
|
+
if (layer.links) {
|
|
1699
|
+
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
1700
|
+
const maxDistPx = layer.links.maxDist * scale;
|
|
1701
|
+
const edges = linkEdges(layer.links, positions, maxDistPx, layer.wrap !== false, w, h);
|
|
1702
|
+
const lwPx = (layer.links.width ?? 1) * scale;
|
|
1703
|
+
const la = layer.links.alpha ?? 1;
|
|
1704
|
+
for (const edge of edges) totalCoverage += edge.dist * lwPx * la / (w * h);
|
|
1705
|
+
}
|
|
875
1706
|
}
|
|
876
1707
|
if (totalCoverage < 5e-4 && spec.layers.length > 0) {
|
|
877
1708
|
warnings.push({
|
|
@@ -897,40 +1728,716 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
897
1728
|
});
|
|
898
1729
|
}
|
|
899
1730
|
}
|
|
1731
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
1732
|
+
const layer = spec.layers[li];
|
|
1733
|
+
if (!layer.links || layer.links.mode === "chain") continue;
|
|
1734
|
+
const entities = allEntities[li];
|
|
1735
|
+
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
1736
|
+
const maxDist = layer.links.maxDist * scale;
|
|
1737
|
+
const pairs = linkPairs(positions, layer.links.k, maxDist, layer.wrap !== false, w, h);
|
|
1738
|
+
const maxPossible = Math.min(entities.length * layer.links.k, entities.length * (entities.length - 1) / 2);
|
|
1739
|
+
if (maxPossible > 0 && pairs.length / maxPossible < 0.1) {
|
|
1740
|
+
warnings.push({
|
|
1741
|
+
path: `layers[${li}].links`,
|
|
1742
|
+
code: "link-starvation",
|
|
1743
|
+
message: `only ${pairs.length}/${maxPossible} possible edges formed \u2014 raise count or widen links.maxDist`
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
1748
|
+
const layer = spec.layers[li];
|
|
1749
|
+
if (layer.motion.type === "static") continue;
|
|
1750
|
+
const entities = allEntities[li];
|
|
1751
|
+
if (entities.length < 3) continue;
|
|
1752
|
+
const speeds = entities.map((e) => Math.sqrt(e.vx * e.vx + e.vy * e.vy));
|
|
1753
|
+
const mean = speeds.reduce((a, b) => a + b, 0) / speeds.length;
|
|
1754
|
+
if (mean === 0) continue;
|
|
1755
|
+
const maxDev = Math.max(...speeds.map((s) => Math.abs(s - mean)));
|
|
1756
|
+
if (maxDev / mean < 0.05) {
|
|
1757
|
+
warnings.push({
|
|
1758
|
+
path: `layers[${li}].motion`,
|
|
1759
|
+
code: "uniform-motion",
|
|
1760
|
+
message: "all entities move at nearly identical speed \u2014 widen the speed range for visual variety"
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const composed = spec.layers.map((l, li) => ({ l, ents: allEntities[li] })).filter(({ l }) => !["bounce", "warp", "path"].includes(l.motion.type)).filter(({ l }) => !(l.motion.type === "orbit" && l.motion.center && "layer" in l.motion.center));
|
|
1765
|
+
const composedCount = composed.reduce((s, c) => s + c.ents.length, 0);
|
|
1766
|
+
if (composedCount >= 8) {
|
|
1767
|
+
const positions = composed.flatMap(({ ents }) => ents.map((e) => positionAt(e, 0, w, h)));
|
|
1768
|
+
const cx = positions.reduce((s, p) => s + p.x, 0) / positions.length;
|
|
1769
|
+
const cy = positions.reduce((s, p) => s + p.y, 0) / positions.length;
|
|
1770
|
+
const offX = Math.abs(cx / w - 0.5);
|
|
1771
|
+
const offY = Math.abs(cy / h - 0.5);
|
|
1772
|
+
if (offX > 0.2 || offY > 0.2) {
|
|
1773
|
+
warnings.push({
|
|
1774
|
+
path: "layers",
|
|
1775
|
+
code: "off-center",
|
|
1776
|
+
message: `centroid at (${(cx / w).toFixed(2)}, ${(cy / h).toFixed(2)}) \u2014 composition is heavily offset from center`
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
900
1780
|
return warnings;
|
|
901
1781
|
}
|
|
902
1782
|
|
|
903
|
-
// src/
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1783
|
+
// src/describe.ts
|
|
1784
|
+
import { createRng as createRng3 } from "@idle-screens/core";
|
|
1785
|
+
function describeScene(spec, options = {}) {
|
|
1786
|
+
const { width: w, height: h } = options.viewport ?? { width: 1920, height: 1080 };
|
|
1787
|
+
const seed = options.seed ?? spec.seed ?? 42;
|
|
1788
|
+
const times = options.times ?? [0, 5e3, 15e3];
|
|
1789
|
+
const scale = spec.units === "viewport" ? Math.min(w, h) : 1;
|
|
1790
|
+
const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
|
|
1791
|
+
const countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
|
|
1792
|
+
const rng = createRng3(seed);
|
|
1793
|
+
const builtLayers = spec.layers.map((layer) => ({
|
|
1794
|
+
layer,
|
|
1795
|
+
entities: buildEntities(layer, rng, w, h, scale, countScale)
|
|
1796
|
+
}));
|
|
1797
|
+
const snapshots = times.map((t) => {
|
|
1798
|
+
const layerSnapshots = builtLayers.map(({ layer, entities }) => {
|
|
1799
|
+
const positions = entities.map((e) => positionAt(e, t, w, h));
|
|
1800
|
+
const cx = positions.length > 0 ? positions.reduce((s, p) => s + p.x, 0) / positions.length : 0;
|
|
1801
|
+
const cy = positions.length > 0 ? positions.reduce((s, p) => s + p.y, 0) / positions.length : 0;
|
|
1802
|
+
let pixArea = 0;
|
|
1803
|
+
for (const e of entities) {
|
|
1804
|
+
const r = e.size / 2;
|
|
1805
|
+
pixArea += layer.sprite.kind === "circle" ? Math.PI * r * r : e.size * e.size;
|
|
1806
|
+
}
|
|
1807
|
+
const coverage = pixArea / (w * h);
|
|
1808
|
+
const meanAlpha = entities.length > 0 ? entities.reduce((s, e) => s + e.alpha, 0) / entities.length : 0;
|
|
1809
|
+
let linksDrawn = null;
|
|
1810
|
+
let linksExpected = null;
|
|
1811
|
+
let connectedComponents = null;
|
|
1812
|
+
let isolatedNodes = null;
|
|
1813
|
+
if (layer.links) {
|
|
1814
|
+
const maxDist = layer.links.maxDist * scale;
|
|
1815
|
+
const pairs = linkPairs(positions, layer.links.k, maxDist, layer.wrap !== false, w, h);
|
|
1816
|
+
linksDrawn = pairs.length;
|
|
1817
|
+
linksExpected = Math.min(entities.length * layer.links.k, entities.length * (entities.length - 1) / 2);
|
|
1818
|
+
const parent = Array.from({ length: entities.length }, (_, i) => i);
|
|
1819
|
+
const find = (x) => {
|
|
1820
|
+
while (parent[x] !== x) {
|
|
1821
|
+
parent[x] = parent[parent[x]];
|
|
1822
|
+
x = parent[x];
|
|
1823
|
+
}
|
|
1824
|
+
return x;
|
|
1825
|
+
};
|
|
1826
|
+
for (const [a, b] of pairs) {
|
|
1827
|
+
const ra = find(a);
|
|
1828
|
+
const rb = find(b);
|
|
1829
|
+
if (ra !== rb) parent[ra] = rb;
|
|
1830
|
+
}
|
|
1831
|
+
const roots = new Set(entities.map((_, i) => find(i)));
|
|
1832
|
+
connectedComponents = roots.size;
|
|
1833
|
+
const degree = new Uint16Array(entities.length);
|
|
1834
|
+
for (const [a, b] of pairs) {
|
|
1835
|
+
degree[a]++;
|
|
1836
|
+
degree[b]++;
|
|
1837
|
+
}
|
|
1838
|
+
isolatedNodes = degree.filter((d) => d === 0).length;
|
|
1839
|
+
}
|
|
1840
|
+
return {
|
|
1841
|
+
key: layer.key,
|
|
1842
|
+
count: entities.length,
|
|
1843
|
+
centroid: { x: +(cx / w).toFixed(3), y: +(cy / h).toFixed(3) },
|
|
1844
|
+
coverage: +coverage.toFixed(4),
|
|
1845
|
+
meanAlpha: +meanAlpha.toFixed(3),
|
|
1846
|
+
linksDrawn,
|
|
1847
|
+
linksExpected,
|
|
1848
|
+
connectedComponents,
|
|
1849
|
+
isolatedNodes
|
|
1850
|
+
};
|
|
1851
|
+
});
|
|
1852
|
+
const warnings = [];
|
|
1853
|
+
for (const ls of layerSnapshots) {
|
|
1854
|
+
const label = ls.key ?? `layer`;
|
|
1855
|
+
if (ls.coverage * ls.meanAlpha < 5e-4) {
|
|
1856
|
+
warnings.push(`${label}: effectively invisible (coverage\xD7alpha=${(ls.coverage * ls.meanAlpha).toFixed(5)})`);
|
|
1857
|
+
}
|
|
1858
|
+
if (ls.linksDrawn !== null && ls.linksExpected !== null && ls.linksExpected > 0) {
|
|
1859
|
+
const ratio = ls.linksDrawn / ls.linksExpected;
|
|
1860
|
+
if (ratio < 0.1) {
|
|
1861
|
+
warnings.push(`${label}: link starvation \u2014 ${ls.linksDrawn}/${ls.linksExpected} edges formed`);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if (ls.isolatedNodes !== null && ls.count > 0 && ls.isolatedNodes / ls.count > 0.5) {
|
|
1865
|
+
warnings.push(`${label}: ${ls.isolatedNodes}/${ls.count} nodes isolated \u2014 graph is disconnected`);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return { t, viewport: { w, h }, layers: layerSnapshots, warnings };
|
|
1869
|
+
});
|
|
1870
|
+
return {
|
|
1871
|
+
spec: { id: spec.id, label: spec.label, units: spec.units ?? "px" },
|
|
1872
|
+
snapshots
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/perceive.ts
|
|
1877
|
+
import { createRng as createRng4 } from "@idle-screens/core";
|
|
1878
|
+
function buildScene(spec, opts) {
|
|
1879
|
+
const { width: w, height: h } = opts.viewport ?? { width: 1920, height: 1080 };
|
|
1880
|
+
const seed = opts.seed ?? spec.seed ?? 42;
|
|
1881
|
+
const scale = spec.units === "px" ? 1 : Math.min(w, h);
|
|
1882
|
+
const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
|
|
1883
|
+
let countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
|
|
1884
|
+
if (countScale > 1) {
|
|
1885
|
+
const rawTotal = spec.layers.reduce((s, l) => s + Math.round(l.count * countScale), 0);
|
|
1886
|
+
if (rawTotal > LIMITS.maxTotal) countScale *= LIMITS.maxTotal / rawTotal;
|
|
1887
|
+
}
|
|
1888
|
+
const rng = createRng4(seed);
|
|
1889
|
+
const layers = spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, w, h, scale, countScale) }));
|
|
1890
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1891
|
+
for (const { layer, entities } of layers) if (layer.key) byKey.set(layer.key, entities);
|
|
1892
|
+
return { w, h, scale, layers, byKey };
|
|
1893
|
+
}
|
|
1894
|
+
function posOf(scene, e, t) {
|
|
1895
|
+
const p = positionAt(e, t, scene.w, scene.h);
|
|
1896
|
+
if (e.orbitParent) {
|
|
1897
|
+
const parent = scene.byKey.get(e.orbitParent)?.[0];
|
|
1898
|
+
if (parent) {
|
|
1899
|
+
const pp = positionAt(parent, t, scene.w, scene.h);
|
|
1900
|
+
p.x += pp.x;
|
|
1901
|
+
p.y += pp.y;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return p;
|
|
1905
|
+
}
|
|
1906
|
+
function hexLuma(hex) {
|
|
1907
|
+
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
1908
|
+
const n = parseInt(h.slice(1), 16);
|
|
1909
|
+
if (Number.isNaN(n)) return 0.7;
|
|
1910
|
+
const r = (n >> 16 & 255) / 255;
|
|
1911
|
+
const g = (n >> 8 & 255) / 255;
|
|
1912
|
+
const b = (n & 255) / 255;
|
|
1913
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1914
|
+
}
|
|
1915
|
+
function spriteLuma(layer, e) {
|
|
1916
|
+
const s = layer.sprite;
|
|
1917
|
+
if (s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect") {
|
|
1918
|
+
return hexLuma(s.colors?.[e.colorIndex] ?? s.color);
|
|
1919
|
+
}
|
|
1920
|
+
if (s.kind === "text") return hexLuma(s.color ?? "#e6e8ef");
|
|
1921
|
+
return 0.75;
|
|
1922
|
+
}
|
|
1923
|
+
function textBox(s, e, p) {
|
|
1924
|
+
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
1925
|
+
const fh = m ? Number(m[1]) : e.size;
|
|
1926
|
+
const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
1927
|
+
const fw = Math.max(fh, 0.62 * fh * Math.min(str.length, 48));
|
|
1928
|
+
const align = s.align ?? "center";
|
|
1929
|
+
const baseline = s.baseline ?? "middle";
|
|
1930
|
+
return {
|
|
1931
|
+
cx: align === "left" ? p.x + fw / 2 : align === "right" ? p.x - fw / 2 : p.x,
|
|
1932
|
+
cy: baseline === "top" ? p.y + fh * 0.6 : baseline === "bottom" ? p.y - fh * 0.6 : p.y,
|
|
1933
|
+
halfX: fw / 2,
|
|
1934
|
+
halfY: fh * 0.6
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function luminanceGrid(spec, opts = {}) {
|
|
1938
|
+
const scene = buildScene(spec, opts);
|
|
1939
|
+
const t = opts.t ?? 5e3;
|
|
1940
|
+
const cols = Math.max(8, Math.min(200, opts.cols ?? 80));
|
|
1941
|
+
const rows = Math.max(8, Math.min(200, opts.rows ?? 48));
|
|
1942
|
+
const { w, h, scale } = scene;
|
|
1943
|
+
const cellW = w / cols;
|
|
1944
|
+
const cellH = h / rows;
|
|
1945
|
+
const bg = spec.background;
|
|
1946
|
+
const bgRow = new Array(rows).fill(0);
|
|
1947
|
+
if (!bg || bg.type === "solid") {
|
|
1948
|
+
bgRow.fill(hexLuma(bg?.color ?? "#05050a"));
|
|
1949
|
+
} else {
|
|
1950
|
+
const stops = [...bg.stops].sort((a, b) => a.at - b.at);
|
|
1951
|
+
for (let r = 0; r < rows; r++) {
|
|
1952
|
+
const y = (r + 0.5) / rows;
|
|
1953
|
+
let lum;
|
|
1954
|
+
if (y <= stops[0].at) lum = hexLuma(stops[0].color);
|
|
1955
|
+
else if (y >= stops[stops.length - 1].at) lum = hexLuma(stops[stops.length - 1].color);
|
|
1956
|
+
else {
|
|
1957
|
+
let i = 0;
|
|
1958
|
+
while (i < stops.length - 1 && stops[i + 1].at < y) i++;
|
|
1959
|
+
const a = stops[i];
|
|
1960
|
+
const b = stops[i + 1];
|
|
1961
|
+
const k = b.at === a.at ? 0 : (y - a.at) / (b.at - a.at);
|
|
1962
|
+
lum = hexLuma(a.color) + (hexLuma(b.color) - hexLuma(a.color)) * k;
|
|
1963
|
+
}
|
|
1964
|
+
bgRow[r] = lum;
|
|
1965
|
+
}
|
|
1966
|
+
if (bg.band) {
|
|
1967
|
+
const bandPx = bg.band.height * (spec.units === "px" ? 1 : Math.min(w, h));
|
|
1968
|
+
const fromRow = Math.max(0, Math.floor((h - bandPx) / cellH));
|
|
1969
|
+
for (let r = fromRow; r < rows; r++) bgRow[r] = hexLuma(bg.band.color);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
const cells = new Array(cols * rows);
|
|
1973
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) cells[r * cols + c] = bgRow[r];
|
|
1974
|
+
const compose = (idx, lum, a, blend) => {
|
|
1975
|
+
const cur = cells[idx];
|
|
1976
|
+
if (blend === "lighter" || blend === "screen") cells[idx] = Math.min(1, cur + lum * a);
|
|
1977
|
+
else if (blend === "multiply") cells[idx] = cur * (1 - a * (1 - lum));
|
|
1978
|
+
else cells[idx] = cur * (1 - a) + lum * a;
|
|
1979
|
+
};
|
|
1980
|
+
for (const { layer, entities } of scene.layers) {
|
|
1981
|
+
const lifeA = lifeAlphaAt(layer.life, t);
|
|
1982
|
+
if (lifeA <= 0) continue;
|
|
1983
|
+
for (const e of entities) {
|
|
1984
|
+
const a = Math.min(1, alphaAt(e, t) * lifeA);
|
|
1985
|
+
if (a <= 4e-3) continue;
|
|
1986
|
+
const lum = spriteLuma(layer, e);
|
|
1987
|
+
const p = posOf(scene, e, t);
|
|
1988
|
+
const sz = sizeAt(e, t);
|
|
1989
|
+
const s = layer.sprite;
|
|
1990
|
+
if (s.kind === "streak") {
|
|
1991
|
+
const heading = headingAt(e, t, w, h) ?? 0;
|
|
1992
|
+
const steps = Math.max(2, Math.ceil(sz / Math.min(cellW, cellH)));
|
|
1993
|
+
const wgt = Math.min(1, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale / cellH);
|
|
1994
|
+
for (let i = 0; i <= steps; i++) {
|
|
1995
|
+
const k = i / steps;
|
|
1996
|
+
const x = p.x - Math.cos(heading) * sz * (1 - k);
|
|
1997
|
+
const y = p.y - Math.sin(heading) * sz * (1 - k);
|
|
1998
|
+
const c = Math.floor(x / cellW);
|
|
1999
|
+
const r = Math.floor(y / cellH);
|
|
2000
|
+
if (c < 0 || c >= cols || r < 0 || r >= rows) continue;
|
|
2001
|
+
compose(r * cols + c, lum, a * k * wgt, layer.blend);
|
|
2002
|
+
}
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
let centerX = p.x;
|
|
2006
|
+
let centerY = p.y;
|
|
2007
|
+
let halfX = sz / 2;
|
|
2008
|
+
let halfY = s.kind === "rect" ? (e.size2 ?? sz) * (e.size > 0 ? sz / e.size : 1) / 2 : sz / 2;
|
|
2009
|
+
if (s.kind === "text") {
|
|
2010
|
+
const box = textBox(s, e, p);
|
|
2011
|
+
centerX = box.cx;
|
|
2012
|
+
centerY = box.cy;
|
|
2013
|
+
halfX = box.halfX;
|
|
2014
|
+
halfY = box.halfY;
|
|
2015
|
+
}
|
|
2016
|
+
const c0 = Math.max(0, Math.floor((centerX - halfX) / cellW));
|
|
2017
|
+
const c1 = Math.min(cols - 1, Math.floor((centerX + halfX) / cellW));
|
|
2018
|
+
const r0 = Math.max(0, Math.floor((centerY - halfY) / cellH));
|
|
2019
|
+
const r1 = Math.min(rows - 1, Math.floor((centerY + halfY) / cellH));
|
|
2020
|
+
const circular = s.kind === "circle" || s.kind === "ring";
|
|
2021
|
+
const soft = s.kind === "circle" && s.soft;
|
|
2022
|
+
const inkWeight = s.kind === "text" || s.kind === "emoji" ? 0.55 : 1;
|
|
2023
|
+
for (let r = r0; r <= r1; r++) {
|
|
2024
|
+
for (let c = c0; c <= c1; c++) {
|
|
2025
|
+
const dx = (c + 0.5) * cellW - centerX;
|
|
2026
|
+
const dy = (r + 0.5) * cellH - centerY;
|
|
2027
|
+
if (circular) {
|
|
2028
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
2029
|
+
if (d > halfX + Math.min(cellW, cellH) / 2) continue;
|
|
2030
|
+
if (s.kind === "ring") {
|
|
2031
|
+
const strokeW = (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale;
|
|
2032
|
+
if (d < halfX - strokeW) continue;
|
|
2033
|
+
}
|
|
2034
|
+
const wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
|
|
2035
|
+
compose(r * cols + c, lum, a * wgt, layer.blend);
|
|
2036
|
+
} else {
|
|
2037
|
+
compose(r * cols + c, lum, a * inkWeight, layer.blend);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
if (layer.links) {
|
|
2043
|
+
const positions = entities.map((e) => posOf(scene, e, t));
|
|
2044
|
+
const maxDistPx = layer.links.maxDist * scale;
|
|
2045
|
+
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
2046
|
+
const edges = linkEdges(layer.links, positions, maxDistPx, layer.wrap !== false && motionWraps, w, h);
|
|
2047
|
+
const la = Math.min(1, (layer.links.alpha ?? 0.6) * lifeA);
|
|
2048
|
+
const lum = layer.links.color ? hexLuma(layer.links.color) : 0.7;
|
|
2049
|
+
const wgt = Math.min(1, (layer.links.width ?? 1) * scale / cellH) * la;
|
|
2050
|
+
for (const edge of edges) {
|
|
2051
|
+
const pi = positions[edge.i];
|
|
2052
|
+
const pj = positions[edge.j];
|
|
2053
|
+
const steps = Math.max(1, Math.ceil(edge.dist / Math.min(cellW, cellH)));
|
|
2054
|
+
for (let i = 0; i <= steps; i++) {
|
|
2055
|
+
const k = i / steps;
|
|
2056
|
+
const c = Math.floor((pi.x + (pj.x - pi.x) * k) / cellW);
|
|
2057
|
+
const r = Math.floor((pi.y + (pj.y - pi.y) * k) / cellH);
|
|
2058
|
+
if (c < 0 || c >= cols || r < 0 || r >= rows) continue;
|
|
2059
|
+
compose(r * cols + c, lum, wgt, layer.blend);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
let sum = 0;
|
|
2065
|
+
let covered = 0;
|
|
2066
|
+
let devSum = 0;
|
|
2067
|
+
let cxAcc = 0;
|
|
2068
|
+
let cyAcc = 0;
|
|
2069
|
+
const rowProfile = new Array(rows).fill(0);
|
|
2070
|
+
const colProfile = new Array(cols).fill(0);
|
|
2071
|
+
for (let r = 0; r < rows; r++) {
|
|
2072
|
+
for (let c = 0; c < cols; c++) {
|
|
2073
|
+
const v = cells[r * cols + c];
|
|
2074
|
+
sum += v;
|
|
2075
|
+
const dev = Math.abs(v - bgRow[r]);
|
|
2076
|
+
if (dev > 0.03) covered++;
|
|
2077
|
+
devSum += dev;
|
|
2078
|
+
cxAcc += dev * (c + 0.5);
|
|
2079
|
+
cyAcc += dev * (r + 0.5);
|
|
2080
|
+
rowProfile[r] += dev;
|
|
2081
|
+
colProfile[c] += dev;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
for (let r = 0; r < rows; r++) rowProfile[r] = rowProfile[r] / cols;
|
|
2085
|
+
for (let c = 0; c < cols; c++) colProfile[c] = colProfile[c] / rows;
|
|
2086
|
+
return {
|
|
2087
|
+
cols,
|
|
2088
|
+
rows,
|
|
2089
|
+
cells,
|
|
2090
|
+
background: bgRow,
|
|
2091
|
+
meanLuminance: sum / (cols * rows),
|
|
2092
|
+
coverage: covered / (cols * rows),
|
|
2093
|
+
centroid: devSum > 1e-6 ? { x: cxAcc / devSum / cols, y: cyAcc / devSum / rows } : null,
|
|
2094
|
+
rowProfile,
|
|
2095
|
+
colProfile
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
var DITHER = [
|
|
2099
|
+
[0.5 / 8, 4.5 / 8],
|
|
2100
|
+
[2.5 / 8, 6.5 / 8],
|
|
2101
|
+
[1.5 / 8, 5.5 / 8],
|
|
2102
|
+
[3.5 / 8, 7.5 / 8]
|
|
2103
|
+
];
|
|
2104
|
+
var DOT_BIT = [
|
|
2105
|
+
[1, 8],
|
|
2106
|
+
[2, 16],
|
|
2107
|
+
[4, 32],
|
|
2108
|
+
[64, 128]
|
|
2109
|
+
];
|
|
2110
|
+
function renderBrailleMap(grid) {
|
|
2111
|
+
const sorted = [...grid.cells].sort((a, b) => a - b);
|
|
2112
|
+
const lo = sorted[0];
|
|
2113
|
+
const hi = Math.max(sorted[Math.floor(sorted.length * 0.98)], lo + 0.08);
|
|
2114
|
+
const expose = (v) => Math.pow(Math.max(0, Math.min(1, (v - lo) / (hi - lo))), 0.8);
|
|
2115
|
+
const outRows = [];
|
|
2116
|
+
for (let br = 0; br < Math.floor(grid.rows / 4); br++) {
|
|
2117
|
+
let line = "";
|
|
2118
|
+
for (let bc = 0; bc < Math.floor(grid.cols / 2); bc++) {
|
|
2119
|
+
let bits = 0;
|
|
2120
|
+
for (let dr = 0; dr < 4; dr++) {
|
|
2121
|
+
for (let dc = 0; dc < 2; dc++) {
|
|
2122
|
+
const v = expose(grid.cells[(br * 4 + dr) * grid.cols + (bc * 2 + dc)]);
|
|
2123
|
+
if (v > DITHER[dr][dc]) bits |= DOT_BIT[dr][dc];
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
line += String.fromCharCode(10240 + bits);
|
|
2127
|
+
}
|
|
2128
|
+
outRows.push(line);
|
|
2129
|
+
}
|
|
2130
|
+
return outRows.join("\n");
|
|
2131
|
+
}
|
|
2132
|
+
function dominanceRanking(spec, opts = {}) {
|
|
2133
|
+
const scene = buildScene(spec, opts);
|
|
2134
|
+
const t = opts.t ?? 5e3;
|
|
2135
|
+
const { w, h, scale } = scene;
|
|
2136
|
+
const bgMean = (() => {
|
|
2137
|
+
const bg = spec.background;
|
|
2138
|
+
if (!bg || bg.type === "solid") return hexLuma(bg?.color ?? "#05050a");
|
|
2139
|
+
return bg.stops.reduce((s, st) => s + hexLuma(st.color), 0) / bg.stops.length;
|
|
2140
|
+
})();
|
|
2141
|
+
const raw = scene.layers.map(({ layer, entities }, layerIndex) => {
|
|
2142
|
+
const lifeA = lifeAlphaAt(layer.life, t);
|
|
2143
|
+
let area = 0;
|
|
2144
|
+
let lumAcc = 0;
|
|
2145
|
+
for (const e of entities) {
|
|
2146
|
+
const a = alphaAt(e, t) * lifeA;
|
|
2147
|
+
const sz = sizeAt(e, t);
|
|
2148
|
+
const s = layer.sprite;
|
|
2149
|
+
let entArea;
|
|
2150
|
+
if (s.kind === "circle") entArea = Math.PI * (sz / 2) * (sz / 2);
|
|
2151
|
+
else if (s.kind === "ring") entArea = Math.PI * sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
2152
|
+
else if (s.kind === "streak") entArea = sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
2153
|
+
else if (s.kind === "rect") {
|
|
2154
|
+
const h2 = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
2155
|
+
entArea = sz * h2;
|
|
2156
|
+
} else if (s.kind === "text") {
|
|
2157
|
+
const box = textBox(s, e, { x: 0, y: 0 });
|
|
2158
|
+
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
2159
|
+
} else entArea = sz * sz * 0.55;
|
|
2160
|
+
area += entArea * a;
|
|
2161
|
+
lumAcc += spriteLuma(layer, e);
|
|
2162
|
+
}
|
|
2163
|
+
if (layer.links && entities.length > 1) {
|
|
2164
|
+
const positions = entities.map((e) => posOf(scene, e, t));
|
|
2165
|
+
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
2166
|
+
const edges = linkEdges(layer.links, positions, layer.links.maxDist * scale, layer.wrap !== false && motionWraps, w, h);
|
|
2167
|
+
const la = (layer.links.alpha ?? 0.6) * lifeA;
|
|
2168
|
+
const lw = (layer.links.width ?? 1) * scale;
|
|
2169
|
+
for (const edge of edges) area += edge.dist * lw * la;
|
|
2170
|
+
}
|
|
2171
|
+
const meanLuma = entities.length ? lumAcc / entities.length : 0;
|
|
2172
|
+
const contrast = Math.abs(meanLuma - bgMean) + 0.1;
|
|
2173
|
+
const blendBoost = layer.blend === "lighter" || layer.blend === "screen" ? 1.3 : layer.blend === "multiply" ? 1.1 : 1;
|
|
2174
|
+
const motionBoost = layer.motion.type === "static" ? 1 : 1.2;
|
|
2175
|
+
const weight = area / (w * h) * contrast * blendBoost * motionBoost;
|
|
2176
|
+
return { layerIndex, key: layer.key, weight, entityCount: entities.length, meanLuma, factors: { area: area / (w * h), contrast, blendBoost, motionBoost } };
|
|
2177
|
+
});
|
|
2178
|
+
const total = raw.reduce((s, r) => s + r.weight, 0) || 1;
|
|
2179
|
+
return raw.slice().sort((a, b) => b.weight - a.weight).map((r, i) => ({
|
|
2180
|
+
rank: i + 1,
|
|
2181
|
+
layerIndex: r.layerIndex,
|
|
2182
|
+
key: r.key,
|
|
2183
|
+
share: r.weight / total,
|
|
2184
|
+
entityCount: r.entityCount,
|
|
2185
|
+
meanLuma: r.meanLuma,
|
|
2186
|
+
factors: r.factors
|
|
2187
|
+
}));
|
|
2188
|
+
}
|
|
2189
|
+
function motionStats(spec, opts = {}) {
|
|
2190
|
+
const scene = buildScene(spec, opts);
|
|
2191
|
+
const t = opts.t ?? 5e3;
|
|
2192
|
+
const dt = opts.dt ?? 500;
|
|
2193
|
+
const { w, h } = scene;
|
|
2194
|
+
return scene.layers.map(({ layer, entities }, layerIndex) => {
|
|
2195
|
+
let acc = 0;
|
|
2196
|
+
let max = 0;
|
|
2197
|
+
let n = 0;
|
|
2198
|
+
for (const e of entities) {
|
|
2199
|
+
const p0 = posOf(scene, e, t);
|
|
2200
|
+
const p1 = posOf(scene, e, t + dt);
|
|
2201
|
+
const dx = p1.x - p0.x;
|
|
2202
|
+
const dy = p1.y - p0.y;
|
|
2203
|
+
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) continue;
|
|
2204
|
+
const speed = Math.sqrt(dx * dx + dy * dy) / dt * 1e3;
|
|
2205
|
+
acc += speed;
|
|
2206
|
+
max = Math.max(max, speed);
|
|
2207
|
+
n++;
|
|
2208
|
+
}
|
|
2209
|
+
const meanSpeed = n ? acc / n : 0;
|
|
2210
|
+
return { layerIndex, key: layer.key, meanSpeed, maxSpeed: max, moving: meanSpeed > 0.5 };
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
var REGION_NAMES = [
|
|
2214
|
+
"top-left",
|
|
2215
|
+
"top-center",
|
|
2216
|
+
"top-right",
|
|
2217
|
+
"mid-left",
|
|
2218
|
+
"center",
|
|
2219
|
+
"mid-right",
|
|
2220
|
+
"bottom-left",
|
|
2221
|
+
"bottom-center",
|
|
2222
|
+
"bottom-right"
|
|
2223
|
+
];
|
|
2224
|
+
function regionMeans(grid) {
|
|
2225
|
+
const out = new Array(9).fill(0);
|
|
2226
|
+
const cnt = new Array(9).fill(0);
|
|
2227
|
+
for (let r = 0; r < grid.rows; r++) {
|
|
2228
|
+
for (let c = 0; c < grid.cols; c++) {
|
|
2229
|
+
const region = Math.min(2, Math.floor(r / grid.rows * 3)) * 3 + Math.min(2, Math.floor(c / grid.cols * 3));
|
|
2230
|
+
out[region] += Math.abs(grid.cells[r * grid.cols + c] - grid.background[r]);
|
|
2231
|
+
cnt[region]++;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return out.map((v, i) => cnt[i] ? v / cnt[i] : 0);
|
|
2235
|
+
}
|
|
2236
|
+
function diffScenes(a, b, opts = {}) {
|
|
2237
|
+
const t = opts.t ?? 5e3;
|
|
2238
|
+
const gridA = luminanceGrid(a, opts);
|
|
2239
|
+
const gridB = luminanceGrid(b, opts);
|
|
2240
|
+
const regsA = regionMeans(gridA);
|
|
2241
|
+
const regsB = regionMeans(gridB);
|
|
2242
|
+
const domA = dominanceRanking(a, opts);
|
|
2243
|
+
const domB = dominanceRanking(b, opts);
|
|
2244
|
+
const nameOf = (d) => d.key ?? `layer-${d.layerIndex}`;
|
|
2245
|
+
const names = /* @__PURE__ */ new Set([...domA.map(nameOf), ...domB.map(nameOf)]);
|
|
2246
|
+
const dominance = [...names].map((key) => {
|
|
2247
|
+
const inA = domA.find((d) => nameOf(d) === key);
|
|
2248
|
+
const inB = domB.find((d) => nameOf(d) === key);
|
|
2249
|
+
return {
|
|
2250
|
+
key,
|
|
2251
|
+
rankA: inA?.rank ?? null,
|
|
2252
|
+
rankB: inB?.rank ?? null,
|
|
2253
|
+
shareA: inA?.share ?? 0,
|
|
2254
|
+
shareB: inB?.share ?? 0
|
|
2255
|
+
};
|
|
2256
|
+
}).sort((x, y) => Math.abs(y.shareB - y.shareA) - Math.abs(x.shareB - x.shareA));
|
|
2257
|
+
const codesA = new Set(adviseSpec(a).map((w) => w.code));
|
|
2258
|
+
const codesB = new Set(adviseSpec(b).map((w) => w.code));
|
|
2259
|
+
return {
|
|
2260
|
+
t,
|
|
2261
|
+
coverage: { a: gridA.coverage, b: gridB.coverage, delta: gridB.coverage - gridA.coverage },
|
|
2262
|
+
meanLuminance: { a: gridA.meanLuminance, b: gridB.meanLuminance, delta: gridB.meanLuminance - gridA.meanLuminance },
|
|
2263
|
+
balanceShift: gridA.centroid && gridB.centroid ? { dx: gridB.centroid.x - gridA.centroid.x, dy: gridB.centroid.y - gridA.centroid.y } : null,
|
|
2264
|
+
regions: REGION_NAMES.map((region, i) => ({ region, a: regsA[i], b: regsB[i], delta: regsB[i] - regsA[i] })),
|
|
2265
|
+
dominance,
|
|
2266
|
+
advisories: {
|
|
2267
|
+
added: [...codesB].filter((c) => !codesA.has(c)),
|
|
2268
|
+
removed: [...codesA].filter((c) => !codesB.has(c))
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
function perceiveScene(spec, opts = {}) {
|
|
2273
|
+
const grid = luminanceGrid(spec, opts);
|
|
2274
|
+
return {
|
|
2275
|
+
t: opts.t ?? 5e3,
|
|
2276
|
+
braille: renderBrailleMap(grid),
|
|
2277
|
+
coverage: grid.coverage,
|
|
2278
|
+
meanLuminance: grid.meanLuminance,
|
|
2279
|
+
centroid: grid.centroid,
|
|
2280
|
+
rowProfile: grid.rowProfile,
|
|
2281
|
+
colProfile: grid.colProfile,
|
|
2282
|
+
dominance: dominanceRanking(spec, opts),
|
|
2283
|
+
motion: motionStats(spec, opts),
|
|
2284
|
+
advisories: adviseSpec(spec, opts.viewport ?? { width: 1920, height: 1080 })
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/examples/aquarium.ts
|
|
2289
|
+
var AQUARIUM_SPEC = {
|
|
2290
|
+
schemaVersion: 1,
|
|
2291
|
+
id: "aquarium",
|
|
2292
|
+
label: "Aquarium",
|
|
2293
|
+
seed: 42,
|
|
2294
|
+
motionIntensity: "calm",
|
|
2295
|
+
background: {
|
|
2296
|
+
type: "gradient",
|
|
2297
|
+
stops: [
|
|
2298
|
+
{ at: 0, color: "#0a3a52" },
|
|
2299
|
+
{ at: 0.6, color: "#062534" },
|
|
2300
|
+
{ at: 1, color: "#02141d" }
|
|
2301
|
+
],
|
|
2302
|
+
band: { color: "#0b1a12", height: 0.02222 }
|
|
2303
|
+
},
|
|
2304
|
+
layers: [
|
|
2305
|
+
{
|
|
2306
|
+
count: 14,
|
|
2307
|
+
sprite: { kind: "emoji", glyphs: ["\u{1F41F}", "\u{1F420}", "\u{1F421}", "\u{1F988}", "\u{1F419}"] },
|
|
2308
|
+
size: [0.03148, 0.06667],
|
|
2309
|
+
motion: { type: "drift", speed: [0.02778, 0.1019], bidirectional: true, bob: 5556e-6 },
|
|
2310
|
+
flip: true
|
|
2311
|
+
},
|
|
2312
|
+
{
|
|
2313
|
+
count: 22,
|
|
2314
|
+
sprite: { kind: "circle", radius: [1852e-6, 6481e-6], color: "#c8ebff" },
|
|
2315
|
+
motion: { type: "rise", speed: [0.02222, 0.07222], sway: 7407e-6 }
|
|
2316
|
+
}
|
|
2317
|
+
]
|
|
2318
|
+
};
|
|
2319
|
+
|
|
2320
|
+
// src/examples/aurora.ts
|
|
2321
|
+
var AURORA_SPEC = {
|
|
2322
|
+
schemaVersion: 1,
|
|
2323
|
+
id: "aurora",
|
|
2324
|
+
label: "Aurora",
|
|
2325
|
+
seed: 66,
|
|
2326
|
+
motionIntensity: "calm",
|
|
2327
|
+
ghosting: 0.85,
|
|
2328
|
+
background: {
|
|
2329
|
+
type: "gradient",
|
|
2330
|
+
stops: [
|
|
2331
|
+
{ at: 0, color: "#02030d" },
|
|
2332
|
+
{ at: 0.65, color: "#050a18" },
|
|
2333
|
+
{ at: 1, color: "#0a1220" }
|
|
2334
|
+
],
|
|
2335
|
+
drift: { period: 4e4, amount: 0.1 }
|
|
2336
|
+
},
|
|
2337
|
+
layers: [
|
|
2338
|
+
{
|
|
2339
|
+
key: "stars",
|
|
2340
|
+
count: 110,
|
|
2341
|
+
sprite: { kind: "circle", radius: [5e-4, 15e-4], color: "#e8ecff", soft: true },
|
|
2342
|
+
motion: { type: "static" },
|
|
2343
|
+
region: { y: [0, 0.75] },
|
|
2344
|
+
alpha: [0.2, 0.6],
|
|
2345
|
+
// A slow shimmer travels across the sky instead of stars twinkling independently.
|
|
2346
|
+
pulse: { amp: 0.25, period: 6e3, wave: { wavelength: 0.5, angle: 15 } }
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
key: "curtain-green",
|
|
2350
|
+
count: 100,
|
|
2351
|
+
sprite: {
|
|
2352
|
+
kind: "circle",
|
|
2353
|
+
radius: [0.055, 0.11],
|
|
2354
|
+
color: "#1fd68a",
|
|
2355
|
+
soft: true,
|
|
2356
|
+
colors: ["#1fd68a", "#2ee6c8", "#4bd1ff"],
|
|
2357
|
+
colorWeights: [5, 2, 1]
|
|
2358
|
+
},
|
|
2359
|
+
motion: { type: "wander", speed: [4e-3, 0.012], angle: 0, meander: 0.13, coherence: 0.7 },
|
|
2360
|
+
region: { y: [0.1, 0.42] },
|
|
2361
|
+
alpha: [0.04, 0.1],
|
|
2362
|
+
blend: "lighter",
|
|
2363
|
+
grow: { amp: 0.25, period: 9e3 }
|
|
2364
|
+
},
|
|
2365
|
+
{
|
|
2366
|
+
key: "curtain-violet",
|
|
2367
|
+
count: 60,
|
|
2368
|
+
sprite: {
|
|
2369
|
+
kind: "circle",
|
|
2370
|
+
radius: [0.045, 0.09],
|
|
2371
|
+
color: "#8d5bff",
|
|
2372
|
+
soft: true,
|
|
2373
|
+
colors: ["#8d5bff", "#d14bff", "#ff4bd8"],
|
|
2374
|
+
colorWeights: [4, 1.5, 0.5]
|
|
2375
|
+
},
|
|
2376
|
+
motion: { type: "wander", speed: [3e-3, 0.01], angle: 180, meander: 0.1, coherence: 0.55 },
|
|
2377
|
+
region: { y: [0.04, 0.28] },
|
|
2378
|
+
alpha: [0.03, 0.08],
|
|
2379
|
+
blend: "lighter",
|
|
2380
|
+
grow: { amp: 0.3, period: 12e3 }
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
key: "fireflies",
|
|
2384
|
+
count: 14,
|
|
2385
|
+
sprite: { kind: "circle", radius: [1e-3, 3e-3], color: "#b8ffe1", soft: true },
|
|
2386
|
+
motion: { type: "wander", speed: [6e-3, 0.02], meander: 0.06 },
|
|
2387
|
+
region: { y: [0.55, 0.95] },
|
|
2388
|
+
alpha: [0.2, 0.55],
|
|
2389
|
+
pulse: { amp: 0.3, period: 3500 },
|
|
2390
|
+
blend: "lighter"
|
|
2391
|
+
}
|
|
2392
|
+
]
|
|
2393
|
+
};
|
|
2394
|
+
|
|
2395
|
+
// src/examples/comets.ts
|
|
2396
|
+
var COMETS_SPEC = {
|
|
2397
|
+
schemaVersion: 1,
|
|
2398
|
+
id: "comets",
|
|
2399
|
+
label: "Comet Shower",
|
|
2400
|
+
seed: 42,
|
|
2401
|
+
motionIntensity: "moderate",
|
|
2402
|
+
background: {
|
|
2403
|
+
type: "gradient",
|
|
2404
|
+
stops: [
|
|
2405
|
+
{ at: 0, color: "#0a0015" },
|
|
2406
|
+
{ at: 0.5, color: "#0d1b2a" },
|
|
2407
|
+
{ at: 1, color: "#1b0a2e" }
|
|
2408
|
+
],
|
|
2409
|
+
drift: { period: 25e3, amount: 0.12 }
|
|
2410
|
+
},
|
|
2411
|
+
layers: [
|
|
2412
|
+
{
|
|
2413
|
+
key: "stars",
|
|
2414
|
+
count: 120,
|
|
2415
|
+
sprite: { kind: "circle", radius: [463e-6, 1852e-6], color: "#ffffff", soft: true },
|
|
2416
|
+
motion: { type: "static" },
|
|
2417
|
+
alpha: [0.3, 0.7],
|
|
2418
|
+
pulse: { amp: 0.15, period: 3e3 }
|
|
2419
|
+
},
|
|
2420
|
+
{
|
|
2421
|
+
key: "comets",
|
|
2422
|
+
count: 8,
|
|
2423
|
+
sprite: { kind: "circle", radius: [2778e-6, 7407e-6], color: "#88ccff", soft: true },
|
|
2424
|
+
motion: { type: "drift", speed: [0.07407, 0.1852], angle: 225 },
|
|
2425
|
+
alpha: [0.7, 1],
|
|
2426
|
+
blend: "lighter",
|
|
2427
|
+
trail: { length: 1500, fade: 1 }
|
|
2428
|
+
},
|
|
2429
|
+
{
|
|
2430
|
+
key: "fireflies",
|
|
2431
|
+
count: 20,
|
|
2432
|
+
sprite: { kind: "circle", radius: [1852e-6, 463e-5], color: "#ffdd44", soft: true },
|
|
2433
|
+
motion: { type: "drift", speed: [9259e-6, 0.02778], bidirectional: true, bob: 0.01852 },
|
|
2434
|
+
alpha: [0.4, 0.8],
|
|
2435
|
+
pulse: { amp: 0.3, period: 2e3 },
|
|
2436
|
+
blend: "lighter",
|
|
2437
|
+
trail: { length: 800 }
|
|
2438
|
+
}
|
|
2439
|
+
]
|
|
2440
|
+
};
|
|
934
2441
|
|
|
935
2442
|
// src/examples/constellation.ts
|
|
936
2443
|
var CONSTELLATION_SPEC = {
|
|
@@ -951,7 +2458,7 @@ var CONSTELLATION_SPEC = {
|
|
|
951
2458
|
// Distant static stars (no links, just atmosphere)
|
|
952
2459
|
{
|
|
953
2460
|
count: 100,
|
|
954
|
-
sprite: { kind: "circle", radius: [
|
|
2461
|
+
sprite: { kind: "circle", radius: [9259e-7, 2778e-6], color: "#667799", colors: ["#8899cc", "#99aadd", "#aabbee", "#7788aa"] },
|
|
955
2462
|
alpha: [0.2, 0.7],
|
|
956
2463
|
pulse: { amp: 0.2, period: 3e3 },
|
|
957
2464
|
motion: { type: "static" }
|
|
@@ -960,19 +2467,19 @@ var CONSTELLATION_SPEC = {
|
|
|
960
2467
|
{
|
|
961
2468
|
key: "nodes",
|
|
962
2469
|
count: 40,
|
|
963
|
-
sprite: { kind: "circle", radius: [
|
|
2470
|
+
sprite: { kind: "circle", radius: [1852e-6, 463e-5], color: "#88aaff", colors: ["#88aaff", "#aaccff", "#ffcc88", "#ff8866"], soft: true },
|
|
964
2471
|
alpha: [0.5, 0.9],
|
|
965
2472
|
blend: "lighter",
|
|
966
|
-
motion: { type: "drift", speed: [
|
|
967
|
-
links: { k: 3, maxDist:
|
|
2473
|
+
motion: { type: "drift", speed: [1852e-6, 7407e-6], bidirectional: true, bob: 2778e-6 },
|
|
2474
|
+
links: { k: 3, maxDist: 0.1852, alpha: 0.15, width: 463e-6 }
|
|
968
2475
|
},
|
|
969
2476
|
// Cycling asterism markers
|
|
970
2477
|
{
|
|
971
2478
|
count: 6,
|
|
972
2479
|
sprite: { kind: "emoji", glyphs: ["\u2726", "\u2727", "\u22C6", "\u2217"], cycle: { period: 2e3 } },
|
|
973
|
-
size: [
|
|
2480
|
+
size: [0.01296, 0.02037],
|
|
974
2481
|
alpha: [0.4, 0.7],
|
|
975
|
-
motion: { type: "drift", speed: [
|
|
2482
|
+
motion: { type: "drift", speed: [9259e-7, 3704e-6], bidirectional: true }
|
|
976
2483
|
}
|
|
977
2484
|
]
|
|
978
2485
|
};
|
|
@@ -981,7 +2488,7 @@ var CONSTELLATION_SPEC = {
|
|
|
981
2488
|
var DASHBOARD_SPEC = {
|
|
982
2489
|
schemaVersion: 1,
|
|
983
2490
|
id: "dev-dashboard",
|
|
984
|
-
label: "
|
|
2491
|
+
label: "Control Center",
|
|
985
2492
|
seed: 256,
|
|
986
2493
|
motionIntensity: "calm",
|
|
987
2494
|
background: {
|
|
@@ -995,473 +2502,490 @@ var DASHBOARD_SPEC = {
|
|
|
995
2502
|
layers: [
|
|
996
2503
|
// --- ambient depth ---
|
|
997
2504
|
{
|
|
998
|
-
|
|
999
|
-
|
|
2505
|
+
key: "dust",
|
|
2506
|
+
count: 40,
|
|
2507
|
+
sprite: { kind: "circle", radius: [1852e-7, 7407e-7], color: "#1a2538" },
|
|
1000
2508
|
alpha: [0.1, 0.25],
|
|
1001
|
-
motion: { type: "drift", speed: [
|
|
2509
|
+
motion: { type: "drift", speed: [2778e-7, 1111e-6], bob: 2778e-7 }
|
|
1002
2510
|
},
|
|
1003
2511
|
{
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
2512
|
+
key: "led-wall",
|
|
2513
|
+
count: 140,
|
|
2514
|
+
sprite: { kind: "circle", radius: [8e-4, 12e-4], color: "#274058" },
|
|
2515
|
+
alpha: [0.05, 0.11],
|
|
2516
|
+
layout: { type: "grid", columns: 20, jitter: 0.15 },
|
|
2517
|
+
// A slow breath travels diagonally across the wall of dots.
|
|
2518
|
+
pulse: { amp: 0.3, period: 5e3, wave: { wavelength: 0.35, angle: 20 } },
|
|
2519
|
+
motion: { type: "static" }
|
|
1010
2520
|
},
|
|
1011
|
-
// --- header ---
|
|
2521
|
+
// --- header (boots first) ---
|
|
1012
2522
|
{
|
|
1013
2523
|
key: "title",
|
|
1014
2524
|
count: 1,
|
|
1015
|
-
position: { x: 0.5, y: 0.
|
|
2525
|
+
position: { x: 0.5, y: 0.045 },
|
|
1016
2526
|
sprite: {
|
|
1017
2527
|
kind: "text",
|
|
1018
|
-
strings: ["
|
|
2528
|
+
strings: ["CONTROL CENTER"],
|
|
1019
2529
|
color: "#556677",
|
|
1020
2530
|
font: "600 13px 'SF Mono', monospace",
|
|
1021
2531
|
align: "center"
|
|
1022
2532
|
},
|
|
1023
|
-
motion: { type: "static" }
|
|
2533
|
+
motion: { type: "static" },
|
|
2534
|
+
life: { enter: 200, fade: 600 }
|
|
1024
2535
|
},
|
|
1025
2536
|
{
|
|
1026
2537
|
key: "status",
|
|
1027
2538
|
count: 1,
|
|
1028
|
-
position: { x: 0.
|
|
2539
|
+
position: { x: 0.94, y: 0.045 },
|
|
1029
2540
|
sprite: {
|
|
1030
2541
|
kind: "text",
|
|
1031
|
-
strings: ["\u25CF
|
|
2542
|
+
strings: ["\u25CF ALL SYSTEMS GO"],
|
|
1032
2543
|
color: "#44cc66",
|
|
1033
2544
|
font: "bold 10px monospace",
|
|
1034
2545
|
align: "right"
|
|
1035
2546
|
},
|
|
1036
|
-
|
|
2547
|
+
pulse: { amp: 0.2, period: 2400 },
|
|
2548
|
+
motion: { type: "static" },
|
|
2549
|
+
life: { enter: 200, fade: 600 }
|
|
1037
2550
|
},
|
|
1038
2551
|
{
|
|
1039
|
-
key: "
|
|
2552
|
+
key: "clock",
|
|
1040
2553
|
count: 1,
|
|
1041
|
-
position: { x: 0.
|
|
2554
|
+
position: { x: 0.06, y: 0.045 },
|
|
1042
2555
|
sprite: {
|
|
1043
2556
|
kind: "text",
|
|
1044
|
-
strings: ["
|
|
1045
|
-
color: "#
|
|
1046
|
-
font: "
|
|
2557
|
+
strings: ["21:47"],
|
|
2558
|
+
color: "#8899aa",
|
|
2559
|
+
font: "bold 12px monospace",
|
|
1047
2560
|
align: "left"
|
|
1048
2561
|
},
|
|
1049
|
-
motion: { type: "static" }
|
|
2562
|
+
motion: { type: "static" },
|
|
2563
|
+
life: { enter: 200, fade: 600 }
|
|
1050
2564
|
},
|
|
1051
|
-
// --- left column:
|
|
2565
|
+
// --- left column: readings (boots second) ---
|
|
1052
2566
|
{
|
|
1053
|
-
key: "
|
|
2567
|
+
key: "power-label",
|
|
1054
2568
|
count: 1,
|
|
1055
|
-
position: { x: 0.06, y: 0.
|
|
2569
|
+
position: { x: 0.06, y: 0.14 },
|
|
1056
2570
|
sprite: {
|
|
1057
2571
|
kind: "text",
|
|
1058
|
-
strings: ["
|
|
2572
|
+
strings: ["POWER"],
|
|
1059
2573
|
color: "#445566",
|
|
1060
2574
|
font: "9px monospace",
|
|
1061
2575
|
align: "left",
|
|
1062
2576
|
baseline: "top"
|
|
1063
2577
|
},
|
|
1064
|
-
motion: { type: "static" }
|
|
2578
|
+
motion: { type: "static" },
|
|
2579
|
+
life: { enter: 700, fade: 600 }
|
|
1065
2580
|
},
|
|
1066
2581
|
{
|
|
1067
|
-
key: "
|
|
2582
|
+
key: "power-value",
|
|
1068
2583
|
count: 1,
|
|
1069
|
-
position: { x: 0.06, y: 0.
|
|
2584
|
+
position: { x: 0.06, y: 0.18 },
|
|
1070
2585
|
sprite: {
|
|
1071
2586
|
kind: "text",
|
|
1072
|
-
strings: ["
|
|
1073
|
-
color: "#
|
|
1074
|
-
font: "bold
|
|
2587
|
+
strings: ["3.2 kW"],
|
|
2588
|
+
color: "#ffd166",
|
|
2589
|
+
font: "bold 26px monospace",
|
|
1075
2590
|
align: "left",
|
|
1076
2591
|
baseline: "top"
|
|
1077
2592
|
},
|
|
1078
|
-
motion: { type: "static" }
|
|
2593
|
+
motion: { type: "static" },
|
|
2594
|
+
life: { enter: 700, fade: 600 }
|
|
1079
2595
|
},
|
|
1080
2596
|
{
|
|
1081
|
-
key: "
|
|
2597
|
+
key: "network-label",
|
|
1082
2598
|
count: 1,
|
|
1083
|
-
position: { x: 0.06, y: 0.
|
|
2599
|
+
position: { x: 0.06, y: 0.28 },
|
|
1084
2600
|
sprite: {
|
|
1085
2601
|
kind: "text",
|
|
1086
|
-
strings: ["
|
|
2602
|
+
strings: ["NETWORK"],
|
|
1087
2603
|
color: "#445566",
|
|
1088
2604
|
font: "9px monospace",
|
|
1089
2605
|
align: "left",
|
|
1090
2606
|
baseline: "top"
|
|
1091
2607
|
},
|
|
1092
|
-
motion: { type: "static" }
|
|
2608
|
+
motion: { type: "static" },
|
|
2609
|
+
life: { enter: 700, fade: 600 }
|
|
1093
2610
|
},
|
|
1094
2611
|
{
|
|
1095
|
-
key: "
|
|
2612
|
+
key: "network-value",
|
|
1096
2613
|
count: 1,
|
|
1097
|
-
position: { x: 0.06, y: 0.
|
|
2614
|
+
position: { x: 0.06, y: 0.32 },
|
|
1098
2615
|
sprite: {
|
|
1099
2616
|
kind: "text",
|
|
1100
|
-
strings: ["
|
|
1101
|
-
color: "#
|
|
1102
|
-
font: "bold
|
|
2617
|
+
strings: ["940 Mb/s"],
|
|
2618
|
+
color: "#44aaff",
|
|
2619
|
+
font: "bold 26px monospace",
|
|
1103
2620
|
align: "left",
|
|
1104
2621
|
baseline: "top"
|
|
1105
2622
|
},
|
|
1106
|
-
motion: { type: "static" }
|
|
2623
|
+
motion: { type: "static" },
|
|
2624
|
+
life: { enter: 700, fade: 600 }
|
|
1107
2625
|
},
|
|
1108
2626
|
{
|
|
1109
|
-
key: "
|
|
2627
|
+
key: "climate-label",
|
|
1110
2628
|
count: 1,
|
|
1111
|
-
position: { x: 0.06, y: 0.
|
|
2629
|
+
position: { x: 0.06, y: 0.42 },
|
|
1112
2630
|
sprite: {
|
|
1113
2631
|
kind: "text",
|
|
1114
|
-
strings: ["
|
|
2632
|
+
strings: ["CLIMATE"],
|
|
1115
2633
|
color: "#445566",
|
|
1116
2634
|
font: "9px monospace",
|
|
1117
2635
|
align: "left",
|
|
1118
2636
|
baseline: "top"
|
|
1119
2637
|
},
|
|
1120
|
-
motion: { type: "static" }
|
|
2638
|
+
motion: { type: "static" },
|
|
2639
|
+
life: { enter: 700, fade: 600 }
|
|
1121
2640
|
},
|
|
1122
2641
|
{
|
|
1123
|
-
key: "
|
|
2642
|
+
key: "climate-value",
|
|
1124
2643
|
count: 1,
|
|
1125
|
-
position: { x: 0.06, y: 0.
|
|
2644
|
+
position: { x: 0.06, y: 0.46 },
|
|
1126
2645
|
sprite: {
|
|
1127
2646
|
kind: "text",
|
|
1128
|
-
strings: ["
|
|
1129
|
-
color: "#
|
|
1130
|
-
font: "bold
|
|
2647
|
+
strings: ["21.5\xB0"],
|
|
2648
|
+
color: "#44ddcc",
|
|
2649
|
+
font: "bold 26px monospace",
|
|
1131
2650
|
align: "left",
|
|
1132
2651
|
baseline: "top"
|
|
1133
2652
|
},
|
|
1134
|
-
motion: { type: "static" }
|
|
2653
|
+
motion: { type: "static" },
|
|
2654
|
+
life: { enter: 700, fade: 600 }
|
|
1135
2655
|
},
|
|
1136
2656
|
{
|
|
1137
|
-
key: "
|
|
2657
|
+
key: "alerts-label",
|
|
1138
2658
|
count: 1,
|
|
1139
|
-
position: { x: 0.06, y: 0.
|
|
2659
|
+
position: { x: 0.06, y: 0.56 },
|
|
1140
2660
|
sprite: {
|
|
1141
2661
|
kind: "text",
|
|
1142
|
-
strings: ["
|
|
2662
|
+
strings: ["ALERTS"],
|
|
1143
2663
|
color: "#445566",
|
|
1144
2664
|
font: "9px monospace",
|
|
1145
2665
|
align: "left",
|
|
1146
2666
|
baseline: "top"
|
|
1147
2667
|
},
|
|
1148
|
-
motion: { type: "static" }
|
|
2668
|
+
motion: { type: "static" },
|
|
2669
|
+
life: { enter: 700, fade: 600 }
|
|
1149
2670
|
},
|
|
1150
2671
|
{
|
|
1151
|
-
key: "
|
|
2672
|
+
key: "alerts-value",
|
|
1152
2673
|
count: 1,
|
|
1153
|
-
position: { x: 0.06, y: 0.
|
|
2674
|
+
position: { x: 0.06, y: 0.6 },
|
|
1154
2675
|
sprite: {
|
|
1155
2676
|
kind: "text",
|
|
1156
|
-
strings: ["
|
|
1157
|
-
color: "#
|
|
1158
|
-
font: "bold
|
|
2677
|
+
strings: ["0"],
|
|
2678
|
+
color: "#44cc66",
|
|
2679
|
+
font: "bold 26px monospace",
|
|
1159
2680
|
align: "left",
|
|
1160
2681
|
baseline: "top"
|
|
1161
2682
|
},
|
|
1162
|
-
motion: { type: "static" }
|
|
2683
|
+
motion: { type: "static" },
|
|
2684
|
+
life: { enter: 700, fade: 600 }
|
|
1163
2685
|
},
|
|
1164
|
-
// --- center
|
|
2686
|
+
// --- center: energy gauge (boots third) ---
|
|
1165
2687
|
{
|
|
1166
|
-
key: "
|
|
2688
|
+
key: "gauge-ring",
|
|
1167
2689
|
count: 1,
|
|
1168
|
-
position: { x: 0.
|
|
1169
|
-
sprite: {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
font: "9px monospace",
|
|
1174
|
-
align: "center"
|
|
1175
|
-
},
|
|
1176
|
-
motion: { type: "static" }
|
|
2690
|
+
position: { x: 0.4, y: 0.36 },
|
|
2691
|
+
sprite: { kind: "ring", radius: [0.105, 0.105], color: "#1e3a52", width: 28e-4 },
|
|
2692
|
+
alpha: [0.85, 0.85],
|
|
2693
|
+
motion: { type: "static" },
|
|
2694
|
+
life: { enter: 1200, fade: 700 }
|
|
1177
2695
|
},
|
|
1178
2696
|
{
|
|
1179
|
-
key: "
|
|
2697
|
+
key: "gauge-glow",
|
|
1180
2698
|
count: 1,
|
|
1181
|
-
position: { x: 0.
|
|
1182
|
-
sprite: {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
motion: { type: "static" }
|
|
2699
|
+
position: { x: 0.4, y: 0.36 },
|
|
2700
|
+
sprite: { kind: "ring", radius: [0.088, 0.088], color: "#2ee6c8", width: 16e-4 },
|
|
2701
|
+
alpha: [0.55, 0.55],
|
|
2702
|
+
blend: "lighter",
|
|
2703
|
+
pulse: { amp: 0.25, period: 4e3 },
|
|
2704
|
+
grow: { amp: 0.05, period: 4e3 },
|
|
2705
|
+
motion: { type: "static" },
|
|
2706
|
+
life: { enter: 1200, fade: 700 }
|
|
1190
2707
|
},
|
|
1191
2708
|
{
|
|
1192
|
-
key: "
|
|
2709
|
+
key: "gauge-label",
|
|
1193
2710
|
count: 1,
|
|
1194
|
-
position: { x: 0.
|
|
2711
|
+
position: { x: 0.4, y: 0.29 },
|
|
1195
2712
|
sprite: {
|
|
1196
2713
|
kind: "text",
|
|
1197
|
-
strings: ["
|
|
2714
|
+
strings: ["ENERGY"],
|
|
1198
2715
|
color: "#445566",
|
|
1199
2716
|
font: "9px monospace",
|
|
1200
2717
|
align: "center"
|
|
1201
2718
|
},
|
|
1202
|
-
motion: { type: "static" }
|
|
1203
|
-
|
|
1204
|
-
{
|
|
1205
|
-
key: "mem-value",
|
|
1206
|
-
count: 1,
|
|
1207
|
-
position: { x: 0.38, y: 0.46 },
|
|
1208
|
-
sprite: {
|
|
1209
|
-
kind: "text",
|
|
1210
|
-
strings: ["4.2 GB"],
|
|
1211
|
-
color: "#ddcc44",
|
|
1212
|
-
font: "bold 36px monospace",
|
|
1213
|
-
align: "center"
|
|
1214
|
-
},
|
|
1215
|
-
motion: { type: "static" }
|
|
2719
|
+
motion: { type: "static" },
|
|
2720
|
+
life: { enter: 1200, fade: 700 }
|
|
1216
2721
|
},
|
|
1217
2722
|
{
|
|
1218
|
-
key: "
|
|
2723
|
+
key: "gauge-value",
|
|
1219
2724
|
count: 1,
|
|
1220
|
-
position: { x: 0.
|
|
2725
|
+
position: { x: 0.4, y: 0.375 },
|
|
1221
2726
|
sprite: {
|
|
1222
2727
|
kind: "text",
|
|
1223
|
-
strings: ["
|
|
1224
|
-
color: "#
|
|
1225
|
-
font: "
|
|
2728
|
+
strings: ["73%"],
|
|
2729
|
+
color: "#2ee6c8",
|
|
2730
|
+
font: "bold 34px monospace",
|
|
1226
2731
|
align: "center"
|
|
1227
2732
|
},
|
|
1228
|
-
motion: { type: "static" }
|
|
2733
|
+
motion: { type: "static" },
|
|
2734
|
+
life: { enter: 1200, fade: 700 }
|
|
1229
2735
|
},
|
|
1230
2736
|
{
|
|
1231
|
-
key: "
|
|
1232
|
-
count:
|
|
1233
|
-
position: { x: 0.38, y: 0.66 },
|
|
2737
|
+
key: "equalizer",
|
|
2738
|
+
count: 22,
|
|
1234
2739
|
sprite: {
|
|
1235
|
-
kind: "
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
2740
|
+
kind: "rect",
|
|
2741
|
+
width: [5e-3, 5e-3],
|
|
2742
|
+
aspect: [0.6, 3.4],
|
|
2743
|
+
color: "#2ee6c8",
|
|
2744
|
+
colors: ["#2ee6c8", "#44aaff", "#aa88ff"],
|
|
2745
|
+
colorWeights: [3, 2, 1]
|
|
1240
2746
|
},
|
|
1241
|
-
|
|
2747
|
+
alpha: [0.35, 0.7],
|
|
2748
|
+
layout: { type: "grid", columns: 22 },
|
|
2749
|
+
region: { x: [0.28, 0.52], y: [0.585, 0.615] },
|
|
2750
|
+
// Wave-phased pulse sweeps along the bars — reads as a live meter.
|
|
2751
|
+
pulse: { amp: 0.4, period: 1600, wave: { wavelength: 0.1, angle: 0 } },
|
|
2752
|
+
motion: { type: "static" },
|
|
2753
|
+
life: { enter: 1200, fade: 700 }
|
|
1242
2754
|
},
|
|
1243
|
-
// --- right column:
|
|
2755
|
+
// --- right column: agenda + devices (boots fourth) ---
|
|
1244
2756
|
{
|
|
1245
|
-
key: "
|
|
2757
|
+
key: "agenda-label",
|
|
1246
2758
|
count: 1,
|
|
1247
|
-
position: { x: 0.
|
|
2759
|
+
position: { x: 0.64, y: 0.14 },
|
|
1248
2760
|
sprite: {
|
|
1249
2761
|
kind: "text",
|
|
1250
|
-
strings: ["
|
|
2762
|
+
strings: ["AGENDA"],
|
|
1251
2763
|
color: "#445566",
|
|
1252
2764
|
font: "9px monospace",
|
|
1253
2765
|
align: "left",
|
|
1254
2766
|
baseline: "top"
|
|
1255
2767
|
},
|
|
1256
|
-
motion: { type: "static" }
|
|
2768
|
+
motion: { type: "static" },
|
|
2769
|
+
life: { enter: 1700, fade: 600 }
|
|
1257
2770
|
},
|
|
1258
2771
|
{
|
|
1259
|
-
key: "
|
|
2772
|
+
key: "agenda-1",
|
|
1260
2773
|
count: 1,
|
|
1261
|
-
position: { x: 0.
|
|
2774
|
+
position: { x: 0.64, y: 0.19 },
|
|
1262
2775
|
sprite: {
|
|
1263
2776
|
kind: "text",
|
|
1264
|
-
strings: ["
|
|
2777
|
+
strings: ["09:00 Standup"],
|
|
1265
2778
|
color: "#8899aa",
|
|
1266
2779
|
font: "11px monospace",
|
|
1267
2780
|
align: "left",
|
|
1268
2781
|
baseline: "top",
|
|
1269
|
-
maxWidth:
|
|
2782
|
+
maxWidth: 0.2593
|
|
1270
2783
|
},
|
|
1271
|
-
motion: { type: "static" }
|
|
2784
|
+
motion: { type: "static" },
|
|
2785
|
+
life: { enter: 1700, fade: 600 }
|
|
1272
2786
|
},
|
|
1273
2787
|
{
|
|
1274
|
-
key: "
|
|
2788
|
+
key: "agenda-2",
|
|
1275
2789
|
count: 1,
|
|
1276
|
-
position: { x: 0.
|
|
2790
|
+
position: { x: 0.64, y: 0.23 },
|
|
1277
2791
|
sprite: {
|
|
1278
2792
|
kind: "text",
|
|
1279
|
-
strings: ["
|
|
2793
|
+
strings: ["12:30 Lunch w/ Kai"],
|
|
1280
2794
|
color: "#778899",
|
|
1281
2795
|
font: "11px monospace",
|
|
1282
2796
|
align: "left",
|
|
1283
2797
|
baseline: "top",
|
|
1284
|
-
maxWidth:
|
|
2798
|
+
maxWidth: 0.2593
|
|
1285
2799
|
},
|
|
1286
|
-
motion: { type: "static" }
|
|
2800
|
+
motion: { type: "static" },
|
|
2801
|
+
life: { enter: 1700, fade: 600 }
|
|
1287
2802
|
},
|
|
1288
2803
|
{
|
|
1289
|
-
key: "
|
|
2804
|
+
key: "agenda-3",
|
|
1290
2805
|
count: 1,
|
|
1291
|
-
position: { x: 0.
|
|
2806
|
+
position: { x: 0.64, y: 0.27 },
|
|
1292
2807
|
sprite: {
|
|
1293
2808
|
kind: "text",
|
|
1294
|
-
strings: ["
|
|
2809
|
+
strings: ["18:00 Gym"],
|
|
1295
2810
|
color: "#667788",
|
|
1296
2811
|
font: "11px monospace",
|
|
1297
2812
|
align: "left",
|
|
1298
2813
|
baseline: "top",
|
|
1299
|
-
maxWidth:
|
|
2814
|
+
maxWidth: 0.2593
|
|
1300
2815
|
},
|
|
1301
|
-
motion: { type: "static" }
|
|
2816
|
+
motion: { type: "static" },
|
|
2817
|
+
life: { enter: 1700, fade: 600 }
|
|
1302
2818
|
},
|
|
1303
2819
|
{
|
|
1304
|
-
key: "
|
|
2820
|
+
key: "devices-label",
|
|
1305
2821
|
count: 1,
|
|
1306
|
-
position: { x: 0.
|
|
2822
|
+
position: { x: 0.64, y: 0.36 },
|
|
1307
2823
|
sprite: {
|
|
1308
2824
|
kind: "text",
|
|
1309
|
-
strings: ["
|
|
1310
|
-
color: "#
|
|
1311
|
-
font: "
|
|
2825
|
+
strings: ["DEVICES"],
|
|
2826
|
+
color: "#445566",
|
|
2827
|
+
font: "9px monospace",
|
|
1312
2828
|
align: "left",
|
|
1313
|
-
baseline: "top"
|
|
1314
|
-
maxWidth: 280
|
|
2829
|
+
baseline: "top"
|
|
1315
2830
|
},
|
|
1316
|
-
motion: { type: "static" }
|
|
2831
|
+
motion: { type: "static" },
|
|
2832
|
+
life: { enter: 1700, fade: 600 }
|
|
1317
2833
|
},
|
|
1318
2834
|
{
|
|
1319
|
-
key: "
|
|
2835
|
+
key: "devices-1",
|
|
1320
2836
|
count: 1,
|
|
1321
|
-
position: { x: 0.
|
|
2837
|
+
position: { x: 0.64, y: 0.41 },
|
|
1322
2838
|
sprite: {
|
|
1323
2839
|
kind: "text",
|
|
1324
|
-
strings: ["
|
|
1325
|
-
color: "#
|
|
2840
|
+
strings: ["Lights 6 on"],
|
|
2841
|
+
color: "#8899aa",
|
|
1326
2842
|
font: "11px monospace",
|
|
1327
2843
|
align: "left",
|
|
1328
|
-
baseline: "top"
|
|
1329
|
-
maxWidth: 280
|
|
2844
|
+
baseline: "top"
|
|
1330
2845
|
},
|
|
1331
|
-
motion: { type: "static" }
|
|
2846
|
+
motion: { type: "static" },
|
|
2847
|
+
life: { enter: 1700, fade: 600 }
|
|
1332
2848
|
},
|
|
1333
|
-
// --- right column: session info ---
|
|
1334
2849
|
{
|
|
1335
|
-
key: "
|
|
2850
|
+
key: "devices-2",
|
|
1336
2851
|
count: 1,
|
|
1337
|
-
position: { x: 0.
|
|
2852
|
+
position: { x: 0.64, y: 0.45 },
|
|
1338
2853
|
sprite: {
|
|
1339
2854
|
kind: "text",
|
|
1340
|
-
strings: ["
|
|
1341
|
-
color: "#
|
|
1342
|
-
font: "
|
|
2855
|
+
strings: ["Thermostat eco"],
|
|
2856
|
+
color: "#778899",
|
|
2857
|
+
font: "11px monospace",
|
|
1343
2858
|
align: "left",
|
|
1344
2859
|
baseline: "top"
|
|
1345
2860
|
},
|
|
1346
|
-
motion: { type: "static" }
|
|
2861
|
+
motion: { type: "static" },
|
|
2862
|
+
life: { enter: 1700, fade: 600 }
|
|
1347
2863
|
},
|
|
1348
2864
|
{
|
|
1349
|
-
key: "
|
|
2865
|
+
key: "devices-3",
|
|
1350
2866
|
count: 1,
|
|
1351
|
-
position: { x: 0.
|
|
2867
|
+
position: { x: 0.64, y: 0.49 },
|
|
1352
2868
|
sprite: {
|
|
1353
2869
|
kind: "text",
|
|
1354
|
-
strings: ["
|
|
1355
|
-
color: "#
|
|
2870
|
+
strings: ["Locks secured"],
|
|
2871
|
+
color: "#667788",
|
|
1356
2872
|
font: "11px monospace",
|
|
1357
2873
|
align: "left",
|
|
1358
2874
|
baseline: "top"
|
|
1359
2875
|
},
|
|
1360
|
-
motion: { type: "static" }
|
|
2876
|
+
motion: { type: "static" },
|
|
2877
|
+
life: { enter: 1700, fade: 600 }
|
|
1361
2878
|
},
|
|
1362
2879
|
{
|
|
1363
|
-
key: "
|
|
2880
|
+
key: "devices-4",
|
|
1364
2881
|
count: 1,
|
|
1365
|
-
position: { x: 0.
|
|
2882
|
+
position: { x: 0.64, y: 0.53 },
|
|
1366
2883
|
sprite: {
|
|
1367
2884
|
kind: "text",
|
|
1368
|
-
strings: ["
|
|
1369
|
-
color: "#
|
|
2885
|
+
strings: ["Media paused"],
|
|
2886
|
+
color: "#556677",
|
|
1370
2887
|
font: "11px monospace",
|
|
1371
2888
|
align: "left",
|
|
1372
2889
|
baseline: "top"
|
|
1373
2890
|
},
|
|
1374
|
-
motion: { type: "static" }
|
|
2891
|
+
motion: { type: "static" },
|
|
2892
|
+
life: { enter: 1700, fade: 600 }
|
|
1375
2893
|
},
|
|
1376
|
-
// --- bottom:
|
|
2894
|
+
// --- bottom: activity feed (boots last) ---
|
|
1377
2895
|
{
|
|
1378
|
-
key: "
|
|
2896
|
+
key: "feed-label",
|
|
1379
2897
|
count: 1,
|
|
1380
|
-
position: { x: 0.06, y: 0.
|
|
2898
|
+
position: { x: 0.06, y: 0.74 },
|
|
1381
2899
|
sprite: {
|
|
1382
2900
|
kind: "text",
|
|
1383
|
-
strings: ["
|
|
2901
|
+
strings: ["ACTIVITY"],
|
|
1384
2902
|
color: "#445566",
|
|
1385
2903
|
font: "9px monospace",
|
|
1386
2904
|
align: "left",
|
|
1387
2905
|
baseline: "top"
|
|
1388
2906
|
},
|
|
1389
|
-
motion: { type: "static" }
|
|
2907
|
+
motion: { type: "static" },
|
|
2908
|
+
life: { enter: 2200, fade: 600 }
|
|
1390
2909
|
},
|
|
1391
2910
|
{
|
|
1392
|
-
key: "
|
|
2911
|
+
key: "feed-1",
|
|
1393
2912
|
count: 1,
|
|
1394
|
-
position: { x: 0.06, y: 0.
|
|
2913
|
+
position: { x: 0.06, y: 0.78 },
|
|
1395
2914
|
sprite: {
|
|
1396
2915
|
kind: "text",
|
|
1397
|
-
strings: ["
|
|
1398
|
-
color: "#
|
|
2916
|
+
strings: ["21:42 Front door locked"],
|
|
2917
|
+
color: "#7a8a7a",
|
|
1399
2918
|
font: "10px monospace",
|
|
1400
2919
|
align: "left",
|
|
1401
2920
|
baseline: "top",
|
|
1402
|
-
maxWidth:
|
|
2921
|
+
maxWidth: 0.5556
|
|
1403
2922
|
},
|
|
1404
|
-
motion: { type: "static" }
|
|
2923
|
+
motion: { type: "static" },
|
|
2924
|
+
life: { enter: 2200, fade: 600 }
|
|
1405
2925
|
},
|
|
1406
2926
|
{
|
|
1407
|
-
key: "
|
|
2927
|
+
key: "feed-2",
|
|
1408
2928
|
count: 1,
|
|
1409
|
-
position: { x: 0.06, y: 0.
|
|
2929
|
+
position: { x: 0.06, y: 0.82 },
|
|
1410
2930
|
sprite: {
|
|
1411
2931
|
kind: "text",
|
|
1412
|
-
strings: ["
|
|
1413
|
-
color: "#
|
|
2932
|
+
strings: ["21:36 Thermostat \u2192 eco"],
|
|
2933
|
+
color: "#6a7a6a",
|
|
1414
2934
|
font: "10px monospace",
|
|
1415
2935
|
align: "left",
|
|
1416
2936
|
baseline: "top",
|
|
1417
|
-
maxWidth:
|
|
2937
|
+
maxWidth: 0.5556
|
|
1418
2938
|
},
|
|
1419
|
-
motion: { type: "static" }
|
|
2939
|
+
motion: { type: "static" },
|
|
2940
|
+
life: { enter: 2200, fade: 600 }
|
|
1420
2941
|
},
|
|
1421
2942
|
{
|
|
1422
|
-
key: "
|
|
2943
|
+
key: "feed-3",
|
|
1423
2944
|
count: 1,
|
|
1424
|
-
position: { x: 0.06, y: 0.
|
|
2945
|
+
position: { x: 0.06, y: 0.86 },
|
|
1425
2946
|
sprite: {
|
|
1426
2947
|
kind: "text",
|
|
1427
|
-
strings: ["14
|
|
1428
|
-
color: "#
|
|
2948
|
+
strings: ["21:14 Backup completed"],
|
|
2949
|
+
color: "#5a6a5a",
|
|
1429
2950
|
font: "10px monospace",
|
|
1430
2951
|
align: "left",
|
|
1431
2952
|
baseline: "top",
|
|
1432
|
-
maxWidth:
|
|
2953
|
+
maxWidth: 0.5556
|
|
1433
2954
|
},
|
|
1434
|
-
motion: { type: "static" }
|
|
2955
|
+
motion: { type: "static" },
|
|
2956
|
+
life: { enter: 2200, fade: 600 }
|
|
1435
2957
|
},
|
|
1436
2958
|
{
|
|
1437
|
-
key: "
|
|
2959
|
+
key: "feed-4",
|
|
1438
2960
|
count: 1,
|
|
1439
|
-
position: { x: 0.06, y: 0.
|
|
2961
|
+
position: { x: 0.06, y: 0.9 },
|
|
1440
2962
|
sprite: {
|
|
1441
2963
|
kind: "text",
|
|
1442
|
-
strings: ["
|
|
1443
|
-
color: "#
|
|
2964
|
+
strings: ["20:58 Motion \xB7 driveway"],
|
|
2965
|
+
color: "#4d5e4d",
|
|
1444
2966
|
font: "10px monospace",
|
|
1445
2967
|
align: "left",
|
|
1446
2968
|
baseline: "top",
|
|
1447
|
-
maxWidth:
|
|
2969
|
+
maxWidth: 0.5556
|
|
1448
2970
|
},
|
|
1449
|
-
motion: { type: "static" }
|
|
2971
|
+
motion: { type: "static" },
|
|
2972
|
+
life: { enter: 2200, fade: 600 }
|
|
1450
2973
|
},
|
|
1451
2974
|
{
|
|
1452
|
-
key: "
|
|
2975
|
+
key: "feed-5",
|
|
1453
2976
|
count: 1,
|
|
1454
|
-
position: { x: 0.06, y: 0.
|
|
2977
|
+
position: { x: 0.06, y: 0.94 },
|
|
1455
2978
|
sprite: {
|
|
1456
2979
|
kind: "text",
|
|
1457
|
-
strings: ["
|
|
2980
|
+
strings: ["20:41 Sunset scene on"],
|
|
1458
2981
|
color: "#3d4e3d",
|
|
1459
2982
|
font: "10px monospace",
|
|
1460
2983
|
align: "left",
|
|
1461
2984
|
baseline: "top",
|
|
1462
|
-
maxWidth:
|
|
2985
|
+
maxWidth: 0.5556
|
|
1463
2986
|
},
|
|
1464
|
-
motion: { type: "static" }
|
|
2987
|
+
motion: { type: "static" },
|
|
2988
|
+
life: { enter: 2200, fade: 600 }
|
|
1465
2989
|
}
|
|
1466
2990
|
]
|
|
1467
2991
|
};
|
|
@@ -1485,58 +3009,147 @@ var LANTERNS_SPEC = {
|
|
|
1485
3009
|
layers: [
|
|
1486
3010
|
{
|
|
1487
3011
|
count: 60,
|
|
1488
|
-
sprite: { kind: "circle", radius: [
|
|
3012
|
+
sprite: { kind: "circle", radius: [463e-6, 1296e-6], color: "#8fa0c8" },
|
|
1489
3013
|
alpha: [0.35, 1],
|
|
1490
3014
|
region: { y: [0, 0.62] },
|
|
1491
|
-
motion: { type: "drift", speed: [
|
|
3015
|
+
motion: { type: "drift", speed: [463e-6, 1852e-6], bob: 9259e-7 }
|
|
1492
3016
|
},
|
|
1493
3017
|
{
|
|
1494
3018
|
count: 36,
|
|
1495
3019
|
sprite: {
|
|
1496
3020
|
kind: "circle",
|
|
1497
|
-
radius: [
|
|
3021
|
+
radius: [1389e-6, 2778e-6],
|
|
1498
3022
|
color: "#b06a2a",
|
|
1499
3023
|
soft: true
|
|
1500
3024
|
},
|
|
1501
3025
|
alpha: [0.5, 0.9],
|
|
1502
3026
|
blend: "lighter",
|
|
1503
3027
|
pulse: { amp: 0.18, period: 2800 },
|
|
1504
|
-
motion: { type: "rise", speed: [6,
|
|
3028
|
+
motion: { type: "rise", speed: [5556e-6, 0.01296], sway: 3704e-6 }
|
|
1505
3029
|
},
|
|
1506
3030
|
{
|
|
1507
3031
|
count: 14,
|
|
1508
3032
|
sprite: {
|
|
1509
3033
|
kind: "circle",
|
|
1510
|
-
radius: [
|
|
3034
|
+
radius: [3241e-6, 6019e-6],
|
|
1511
3035
|
color: "#e08a34",
|
|
1512
3036
|
soft: true
|
|
1513
3037
|
},
|
|
1514
3038
|
alpha: [0.6, 1],
|
|
1515
3039
|
blend: "lighter",
|
|
1516
3040
|
pulse: { amp: 0.22, period: 3400 },
|
|
1517
|
-
motion: { type: "rise", speed: [
|
|
3041
|
+
motion: { type: "rise", speed: [0.01481, 0.02593], sway: 6481e-6 }
|
|
1518
3042
|
},
|
|
1519
3043
|
{
|
|
1520
3044
|
count: 8,
|
|
1521
3045
|
sprite: { kind: "emoji", glyphs: ["\u{1F3EE}"] },
|
|
1522
|
-
size: [
|
|
3046
|
+
size: [0.02407, 0.04074],
|
|
1523
3047
|
alpha: [0.85, 1],
|
|
1524
|
-
motion: { type: "rise", speed: [
|
|
3048
|
+
motion: { type: "rise", speed: [0.02778, 0.04444], sway: 9259e-6 }
|
|
1525
3049
|
},
|
|
1526
3050
|
{
|
|
1527
3051
|
count: 4,
|
|
1528
3052
|
sprite: { kind: "emoji", glyphs: ["\u{1F3EE}"] },
|
|
1529
|
-
size: [
|
|
1530
|
-
motion: { type: "rise", speed: [
|
|
3053
|
+
size: [0.0537, 0.07778],
|
|
3054
|
+
motion: { type: "rise", speed: [0.04815, 0.06852], sway: 0.01296 }
|
|
1531
3055
|
},
|
|
1532
3056
|
{
|
|
1533
3057
|
count: 12,
|
|
1534
|
-
sprite: { kind: "circle", radius: [
|
|
3058
|
+
sprite: { kind: "circle", radius: [1852e-6, 463e-5], color: "#e08a34", soft: true },
|
|
1535
3059
|
alpha: [0.12, 0.35],
|
|
1536
3060
|
blend: "lighter",
|
|
1537
3061
|
region: { y: [0.93, 1] },
|
|
1538
3062
|
pulse: { amp: 0.1, period: 2200 },
|
|
1539
|
-
motion: { type: "drift", speed: [
|
|
3063
|
+
motion: { type: "drift", speed: [1852e-6, 5556e-6], bob: 1852e-6 }
|
|
3064
|
+
}
|
|
3065
|
+
]
|
|
3066
|
+
};
|
|
3067
|
+
|
|
3068
|
+
// src/examples/matrix-rain.ts
|
|
3069
|
+
var MATRIX_RAIN_SPEC = {
|
|
3070
|
+
schemaVersion: 1,
|
|
3071
|
+
id: "matrix-rain",
|
|
3072
|
+
label: "Matrix Rain",
|
|
3073
|
+
seed: 1999,
|
|
3074
|
+
motionIntensity: "moderate",
|
|
3075
|
+
ghosting: 0.88,
|
|
3076
|
+
background: { type: "solid", color: "#010401" },
|
|
3077
|
+
layers: [
|
|
3078
|
+
{
|
|
3079
|
+
key: "rain-far",
|
|
3080
|
+
count: 64,
|
|
3081
|
+
sprite: {
|
|
3082
|
+
kind: "text",
|
|
3083
|
+
strings: ["\u30A2", "\u30AB", "\u30B5", "\u30BF", "\u30CA", "\u30CF", "\u30DE", "\u30E4", "\u30E9", "\u30EF", "0", "1", "7", "Z", "X", "K"],
|
|
3084
|
+
color: "#1c7a2e",
|
|
3085
|
+
font: "bold monospace",
|
|
3086
|
+
cycle: { period: 900 }
|
|
3087
|
+
},
|
|
3088
|
+
size: [0.014, 0.02],
|
|
3089
|
+
layout: { type: "grid", columns: 32, jitter: { y: 1 } },
|
|
3090
|
+
motion: { type: "drift", speed: [0.06, 0.14], angle: 90 },
|
|
3091
|
+
alpha: [0.35, 0.6]
|
|
3092
|
+
},
|
|
3093
|
+
{
|
|
3094
|
+
key: "rain-near",
|
|
3095
|
+
count: 40,
|
|
3096
|
+
sprite: {
|
|
3097
|
+
kind: "text",
|
|
3098
|
+
strings: ["\u30DF", "\u30CD", "\u30EA", "\u30BD", "\u30C4", "\u30CC", "\u30DB", "\u30E6", "3", "8", "9", "V", "N"],
|
|
3099
|
+
color: "#2ecc40",
|
|
3100
|
+
font: "bold monospace",
|
|
3101
|
+
cycle: { period: 700 }
|
|
3102
|
+
},
|
|
3103
|
+
size: [0.022, 0.03],
|
|
3104
|
+
layout: { type: "grid", columns: 20, jitter: { y: 1 } },
|
|
3105
|
+
motion: { type: "drift", speed: [0.16, 0.3], angle: 90 },
|
|
3106
|
+
alpha: [0.6, 0.9]
|
|
3107
|
+
},
|
|
3108
|
+
{
|
|
3109
|
+
key: "heads",
|
|
3110
|
+
count: 12,
|
|
3111
|
+
sprite: {
|
|
3112
|
+
kind: "text",
|
|
3113
|
+
strings: ["\u30B1", "\u30E1", "\u30A8", "\u30E0", "\u30F2", "5", "2"],
|
|
3114
|
+
color: "#c8ffd4",
|
|
3115
|
+
font: "bold monospace",
|
|
3116
|
+
cycle: { period: 500 }
|
|
3117
|
+
},
|
|
3118
|
+
size: [0.024, 0.032],
|
|
3119
|
+
layout: { type: "grid", columns: 12, jitter: { y: 1 } },
|
|
3120
|
+
motion: { type: "drift", speed: [0.2, 0.34], angle: 90 },
|
|
3121
|
+
alpha: [0.9, 1]
|
|
3122
|
+
}
|
|
3123
|
+
]
|
|
3124
|
+
};
|
|
3125
|
+
|
|
3126
|
+
// src/examples/polygons.ts
|
|
3127
|
+
var POLYGONS_SPEC = {
|
|
3128
|
+
schemaVersion: 1,
|
|
3129
|
+
id: "polygons",
|
|
3130
|
+
label: "Polygons",
|
|
3131
|
+
seed: 1995,
|
|
3132
|
+
motionIntensity: "moderate",
|
|
3133
|
+
ghosting: 0.95,
|
|
3134
|
+
background: { type: "solid", color: "#04040a" },
|
|
3135
|
+
layers: [
|
|
3136
|
+
{
|
|
3137
|
+
key: "poly-cyan",
|
|
3138
|
+
count: 4,
|
|
3139
|
+
sprite: { kind: "circle", radius: [25e-4, 25e-4], color: "#37e5d7" },
|
|
3140
|
+
motion: { type: "bounce", speed: [0.14, 0.24] },
|
|
3141
|
+
wrap: false,
|
|
3142
|
+
alpha: [0.9, 0.9],
|
|
3143
|
+
links: { k: 1, maxDist: 3, mode: "chain", closed: true, color: "#37e5d7", alpha: 0.85, width: 18e-4 }
|
|
3144
|
+
},
|
|
3145
|
+
{
|
|
3146
|
+
key: "poly-magenta",
|
|
3147
|
+
count: 4,
|
|
3148
|
+
sprite: { kind: "circle", radius: [25e-4, 25e-4], color: "#e83e9c" },
|
|
3149
|
+
motion: { type: "bounce", speed: [0.1, 0.2] },
|
|
3150
|
+
wrap: false,
|
|
3151
|
+
alpha: [0.9, 0.9],
|
|
3152
|
+
links: { k: 1, maxDist: 3, mode: "chain", closed: true, color: "#e83e9c", alpha: 0.85, width: 18e-4 }
|
|
1540
3153
|
}
|
|
1541
3154
|
]
|
|
1542
3155
|
};
|
|
@@ -1560,7 +3173,7 @@ var ORRERY_SPEC = {
|
|
|
1560
3173
|
// Distant stars
|
|
1561
3174
|
{
|
|
1562
3175
|
count: 80,
|
|
1563
|
-
sprite: { kind: "circle", radius: [
|
|
3176
|
+
sprite: { kind: "circle", radius: [2778e-7, 1111e-6], color: "#8899cc" },
|
|
1564
3177
|
alpha: [0.2, 0.8],
|
|
1565
3178
|
pulse: { amp: 0.3, period: 3e3 },
|
|
1566
3179
|
motion: { type: "static" }
|
|
@@ -1569,7 +3182,7 @@ var ORRERY_SPEC = {
|
|
|
1569
3182
|
{
|
|
1570
3183
|
key: "corona",
|
|
1571
3184
|
count: 1,
|
|
1572
|
-
sprite: { kind: "circle", radius: [
|
|
3185
|
+
sprite: { kind: "circle", radius: [0.05556, 0.05556], color: "#ffaa22", soft: true },
|
|
1573
3186
|
blend: "lighter",
|
|
1574
3187
|
alpha: [0.6, 0.6],
|
|
1575
3188
|
grow: { amp: 0.15, period: 4e3 },
|
|
@@ -1581,7 +3194,7 @@ var ORRERY_SPEC = {
|
|
|
1581
3194
|
key: "sun",
|
|
1582
3195
|
count: 1,
|
|
1583
3196
|
sprite: { kind: "emoji", glyphs: ["\u2600\uFE0F"] },
|
|
1584
|
-
size: [
|
|
3197
|
+
size: [0.04444, 0.04444],
|
|
1585
3198
|
grow: { amp: 0.08, period: 4e3 },
|
|
1586
3199
|
motion: { type: "static" },
|
|
1587
3200
|
position: { x: 0.5, y: 0.5 }
|
|
@@ -1591,36 +3204,114 @@ var ORRERY_SPEC = {
|
|
|
1591
3204
|
key: "inner",
|
|
1592
3205
|
count: 3,
|
|
1593
3206
|
sprite: { kind: "emoji", glyphs: ["\u{1FAA8}", "\u{1F534}", "\u{1F7E0}"] },
|
|
1594
|
-
size: [
|
|
3207
|
+
size: [0.01481, 0.02037],
|
|
1595
3208
|
spin: 30,
|
|
1596
|
-
motion: { type: "orbit", speed: [28, 42], radius: [
|
|
3209
|
+
motion: { type: "orbit", speed: [28, 42], radius: [0.07407, 0.1111] }
|
|
1597
3210
|
},
|
|
1598
3211
|
// Middle ring — gas giants
|
|
1599
3212
|
{
|
|
1600
3213
|
key: "middle",
|
|
1601
3214
|
count: 2,
|
|
1602
3215
|
sprite: { kind: "emoji", glyphs: ["\u{1F7E4}", "\u{1FA90}"] },
|
|
1603
|
-
size: [
|
|
3216
|
+
size: [0.02593, 0.03519],
|
|
1604
3217
|
spin: -15,
|
|
1605
|
-
motion: { type: "orbit", speed: [12, 20], radius: [
|
|
3218
|
+
motion: { type: "orbit", speed: [12, 20], radius: [0.1481, 0.1852] }
|
|
1606
3219
|
},
|
|
1607
3220
|
// Outer ring — ice giants
|
|
1608
3221
|
{
|
|
1609
3222
|
key: "outer",
|
|
1610
3223
|
count: 2,
|
|
1611
3224
|
sprite: { kind: "emoji", glyphs: ["\u{1F535}", "\u{1F48E}"] },
|
|
1612
|
-
size: [
|
|
3225
|
+
size: [0.02037, 0.02778],
|
|
1613
3226
|
spin: 10,
|
|
1614
|
-
motion: { type: "orbit", speed: [5, 10], radius: [
|
|
3227
|
+
motion: { type: "orbit", speed: [5, 10], radius: [0.2222, 0.2778] }
|
|
1615
3228
|
},
|
|
1616
3229
|
// Orbital dust ring (soft glow particles orbiting slowly)
|
|
1617
3230
|
{
|
|
1618
3231
|
count: 24,
|
|
1619
|
-
sprite: { kind: "circle", radius: [
|
|
3232
|
+
sprite: { kind: "circle", radius: [9259e-7, 2778e-6], color: "#ccaa66", soft: true },
|
|
1620
3233
|
alpha: [0.15, 0.4],
|
|
1621
3234
|
blend: "lighter",
|
|
1622
3235
|
pulse: { amp: 0.2, period: 2500 },
|
|
1623
|
-
motion: { type: "orbit", speed: [2, 8], radius: [
|
|
3236
|
+
motion: { type: "orbit", speed: [2, 8], radius: [0.09259, 0.2963] }
|
|
3237
|
+
}
|
|
3238
|
+
]
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3241
|
+
// src/examples/procession.ts
|
|
3242
|
+
var PROCESSION_SPEC = {
|
|
3243
|
+
schemaVersion: 1,
|
|
3244
|
+
id: "procession",
|
|
3245
|
+
label: "Night Procession",
|
|
3246
|
+
seed: 8,
|
|
3247
|
+
motionIntensity: "calm",
|
|
3248
|
+
background: {
|
|
3249
|
+
type: "gradient",
|
|
3250
|
+
stops: [
|
|
3251
|
+
{ at: 0, color: "#060612" },
|
|
3252
|
+
{ at: 0.8, color: "#101024" },
|
|
3253
|
+
{ at: 1, color: "#1a1428" }
|
|
3254
|
+
],
|
|
3255
|
+
drift: { period: 3e4, amount: 0.08 }
|
|
3256
|
+
},
|
|
3257
|
+
layers: [
|
|
3258
|
+
{
|
|
3259
|
+
key: "stars",
|
|
3260
|
+
count: 90,
|
|
3261
|
+
sprite: { kind: "circle", radius: [6e-4, 16e-4], color: "#f0ead8", soft: true },
|
|
3262
|
+
motion: { type: "static" },
|
|
3263
|
+
region: { y: [0, 0.6] },
|
|
3264
|
+
alpha: [0.25, 0.6],
|
|
3265
|
+
pulse: { amp: 0.15, period: 5e3 }
|
|
3266
|
+
},
|
|
3267
|
+
{
|
|
3268
|
+
key: "lanterns",
|
|
3269
|
+
count: 9,
|
|
3270
|
+
sprite: {
|
|
3271
|
+
kind: "rect",
|
|
3272
|
+
width: [0.012, 0.02],
|
|
3273
|
+
aspect: [1.3, 1.7],
|
|
3274
|
+
color: "#ffb347",
|
|
3275
|
+
colors: ["#ffb347", "#ff8c69", "#ffd97a"],
|
|
3276
|
+
colorWeights: [3, 1, 2]
|
|
3277
|
+
},
|
|
3278
|
+
motion: {
|
|
3279
|
+
type: "path",
|
|
3280
|
+
points: [
|
|
3281
|
+
{ x: 0.12, y: 0.72 },
|
|
3282
|
+
{ x: 0.38, y: 0.58 },
|
|
3283
|
+
{ x: 0.62, y: 0.78 },
|
|
3284
|
+
{ x: 0.88, y: 0.62 },
|
|
3285
|
+
{ x: 0.62, y: 0.5 },
|
|
3286
|
+
{ x: 0.38, y: 0.72 }
|
|
3287
|
+
],
|
|
3288
|
+
duration: 36e3,
|
|
3289
|
+
curve: "smooth",
|
|
3290
|
+
closed: true,
|
|
3291
|
+
scatter: 0.02
|
|
3292
|
+
},
|
|
3293
|
+
alpha: [0.65, 0.9],
|
|
3294
|
+
pulse: { amp: 0.15, period: 2600 },
|
|
3295
|
+
life: { enter: 4e3, fade: 2500 },
|
|
3296
|
+
trail: { length: 1200, fade: 1 }
|
|
3297
|
+
},
|
|
3298
|
+
{
|
|
3299
|
+
key: "bearer",
|
|
3300
|
+
count: 1,
|
|
3301
|
+
sprite: { kind: "circle", radius: [0.014, 0.014], color: "#ffe9b0", soft: true },
|
|
3302
|
+
motion: { type: "wander", speed: [8e-3, 0.012], meander: 0.08 },
|
|
3303
|
+
alpha: [0.8, 0.8],
|
|
3304
|
+
blend: "lighter",
|
|
3305
|
+
life: { enter: 4e3, fade: 2500 }
|
|
3306
|
+
},
|
|
3307
|
+
{
|
|
3308
|
+
key: "moths",
|
|
3309
|
+
count: 7,
|
|
3310
|
+
sprite: { kind: "ring", radius: [2e-3, 5e-3], color: "#d8f6ff", width: 1e-3 },
|
|
3311
|
+
motion: { type: "orbit", speed: [20, 55], radius: [0.03, 0.09], center: { layer: "bearer" } },
|
|
3312
|
+
alpha: [0.3, 0.7],
|
|
3313
|
+
life: { enter: 7e3, fade: 2e3 },
|
|
3314
|
+
trail: { length: 600 }
|
|
1624
3315
|
}
|
|
1625
3316
|
]
|
|
1626
3317
|
};
|
|
@@ -1637,8 +3328,8 @@ var RAIN_SPEC = {
|
|
|
1637
3328
|
{
|
|
1638
3329
|
count: 140,
|
|
1639
3330
|
sprite: { kind: "text", strings: ["\u2502"], color: "#9fd0ff" },
|
|
1640
|
-
size: [
|
|
1641
|
-
motion: { type: "drift", angle: 90, speed: [
|
|
3331
|
+
size: [0.01111, 0.02407],
|
|
3332
|
+
motion: { type: "drift", angle: 90, speed: [0.4815, 0.8333] }
|
|
1642
3333
|
}
|
|
1643
3334
|
]
|
|
1644
3335
|
};
|
|
@@ -1655,50 +3346,122 @@ var SAKURA_SPEC = {
|
|
|
1655
3346
|
stops: [
|
|
1656
3347
|
{ at: 0, color: "#1b1d38" },
|
|
1657
3348
|
{ at: 0.45, color: "#3a2645" },
|
|
1658
|
-
{ at: 0.
|
|
1659
|
-
{ at:
|
|
3349
|
+
{ at: 0.78, color: "#7d3d4e" },
|
|
3350
|
+
{ at: 0.92, color: "#4a2438" },
|
|
3351
|
+
{ at: 1, color: "#1c1018" }
|
|
1660
3352
|
],
|
|
1661
|
-
|
|
3353
|
+
drift: { period: 35e3, amount: 0.08 }
|
|
1662
3354
|
},
|
|
1663
3355
|
layers: [
|
|
1664
3356
|
{
|
|
3357
|
+
key: "blossom-glow",
|
|
1665
3358
|
count: 60,
|
|
1666
|
-
sprite: {
|
|
3359
|
+
sprite: {
|
|
3360
|
+
kind: "circle",
|
|
3361
|
+
radius: [5556e-7, 1481e-6],
|
|
3362
|
+
color: "#ffe3ec",
|
|
3363
|
+
soft: true
|
|
3364
|
+
},
|
|
1667
3365
|
alpha: [0.3, 0.9],
|
|
1668
3366
|
blend: "lighter",
|
|
1669
3367
|
region: { y: [0, 0.72] },
|
|
1670
|
-
|
|
1671
|
-
|
|
3368
|
+
// Shimmer travels across the sky instead of orbs twinkling independently.
|
|
3369
|
+
pulse: { amp: 0.22, period: 4e3, wave: { wavelength: 0.45, angle: 10 } },
|
|
3370
|
+
motion: { type: "drift", speed: [2778e-7, 1111e-6], bob: 9259e-7 }
|
|
1672
3371
|
},
|
|
1673
3372
|
{
|
|
3373
|
+
key: "petals-far",
|
|
1674
3374
|
count: 28,
|
|
1675
3375
|
sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
|
|
1676
|
-
size: [
|
|
3376
|
+
size: [0.01204, 0.01852],
|
|
1677
3377
|
alpha: [0.5, 0.8],
|
|
1678
|
-
|
|
3378
|
+
spin: 16,
|
|
3379
|
+
motion: {
|
|
3380
|
+
type: "wander",
|
|
3381
|
+
speed: [0.011, 0.024],
|
|
3382
|
+
angle: 100,
|
|
3383
|
+
meander: 0.025,
|
|
3384
|
+
coherence: 0.35
|
|
3385
|
+
}
|
|
1679
3386
|
},
|
|
1680
3387
|
{
|
|
3388
|
+
key: "petals-mid",
|
|
1681
3389
|
count: 18,
|
|
1682
3390
|
sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
|
|
1683
|
-
size: [
|
|
3391
|
+
size: [0.01852, 0.02963],
|
|
1684
3392
|
alpha: [0.7, 0.95],
|
|
1685
|
-
|
|
3393
|
+
spin: 24,
|
|
3394
|
+
motion: {
|
|
3395
|
+
type: "wander",
|
|
3396
|
+
speed: [0.022, 0.041],
|
|
3397
|
+
angle: 100,
|
|
3398
|
+
meander: 0.045,
|
|
3399
|
+
coherence: 0.35
|
|
3400
|
+
}
|
|
1686
3401
|
},
|
|
1687
3402
|
{
|
|
3403
|
+
key: "petals-near",
|
|
1688
3404
|
count: 10,
|
|
1689
3405
|
sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
|
|
1690
|
-
size: [
|
|
3406
|
+
size: [0.03148, 0.05],
|
|
1691
3407
|
alpha: [0.85, 1],
|
|
1692
|
-
|
|
3408
|
+
spin: 32,
|
|
3409
|
+
motion: {
|
|
3410
|
+
type: "wander",
|
|
3411
|
+
speed: [0.039, 0.063],
|
|
3412
|
+
angle: 100,
|
|
3413
|
+
meander: 0.07,
|
|
3414
|
+
coherence: 0.35
|
|
3415
|
+
}
|
|
3416
|
+
},
|
|
3417
|
+
{
|
|
3418
|
+
key: "gusts",
|
|
3419
|
+
count: 7,
|
|
3420
|
+
sprite: {
|
|
3421
|
+
kind: "streak",
|
|
3422
|
+
length: [0.02, 0.045],
|
|
3423
|
+
width: 12e-4,
|
|
3424
|
+
color: "#ffd9e6"
|
|
3425
|
+
},
|
|
3426
|
+
alpha: [0.1, 0.22],
|
|
3427
|
+
blend: "lighter",
|
|
3428
|
+
motion: { type: "drift", speed: [0.09, 0.14], angle: 100 }
|
|
3429
|
+
},
|
|
3430
|
+
{
|
|
3431
|
+
key: "fireflies",
|
|
3432
|
+
count: 6,
|
|
3433
|
+
sprite: {
|
|
3434
|
+
kind: "circle",
|
|
3435
|
+
radius: [9e-4, 2e-3],
|
|
3436
|
+
color: "#ffe9a8",
|
|
3437
|
+
soft: true
|
|
3438
|
+
},
|
|
3439
|
+
alpha: [0.15, 0.5],
|
|
3440
|
+
blend: "lighter",
|
|
3441
|
+
region: { y: [0.6, 0.9] },
|
|
3442
|
+
pulse: { amp: 0.3, period: 2800 },
|
|
3443
|
+
motion: { type: "wander", speed: [4e-3, 0.01], meander: 0.05 }
|
|
1693
3444
|
},
|
|
1694
3445
|
{
|
|
3446
|
+
key: "fallen-petals",
|
|
1695
3447
|
count: 16,
|
|
1696
|
-
sprite: {
|
|
3448
|
+
sprite: {
|
|
3449
|
+
kind: "circle",
|
|
3450
|
+
radius: [1389e-6, 3704e-6],
|
|
3451
|
+
color: "#ff9bb5",
|
|
3452
|
+
soft: true
|
|
3453
|
+
},
|
|
1697
3454
|
alpha: [0.1, 0.3],
|
|
1698
3455
|
blend: "lighter",
|
|
1699
3456
|
region: { y: [0.9, 1] },
|
|
1700
|
-
|
|
1701
|
-
|
|
3457
|
+
// Settled petals catch the last light in a slow wave along the ground.
|
|
3458
|
+
pulse: { amp: 0.15, period: 3200, wave: { wavelength: 0.3, angle: 0 } },
|
|
3459
|
+
motion: {
|
|
3460
|
+
type: "drift",
|
|
3461
|
+
speed: [2778e-7, 1389e-6],
|
|
3462
|
+
angle: 180,
|
|
3463
|
+
bob: 9259e-7
|
|
3464
|
+
}
|
|
1702
3465
|
}
|
|
1703
3466
|
]
|
|
1704
3467
|
};
|
|
@@ -1721,18 +3484,71 @@ var SNOWFALL_SPEC = {
|
|
|
1721
3484
|
layers: [
|
|
1722
3485
|
{
|
|
1723
3486
|
count: 50,
|
|
1724
|
-
sprite: { kind: "circle", radius: [
|
|
1725
|
-
motion: { type: "drift", angle: 90, speed: [
|
|
3487
|
+
sprite: { kind: "circle", radius: [9259e-7, 2315e-6], color: "#8899aa" },
|
|
3488
|
+
motion: { type: "drift", angle: 90, speed: [0.01389, 0.03704], bob: 2778e-6 }
|
|
1726
3489
|
},
|
|
1727
3490
|
{
|
|
1728
3491
|
count: 35,
|
|
1729
|
-
sprite: { kind: "circle", radius: [
|
|
1730
|
-
motion: { type: "drift", angle: 85, speed: [
|
|
3492
|
+
sprite: { kind: "circle", radius: [1852e-6, 4167e-6], color: "#c0cdd8" },
|
|
3493
|
+
motion: { type: "drift", angle: 85, speed: [0.03241, 0.07407], bob: 5556e-6 }
|
|
1731
3494
|
},
|
|
1732
3495
|
{
|
|
1733
3496
|
count: 15,
|
|
1734
|
-
sprite: { kind: "circle", radius: [
|
|
1735
|
-
motion: { type: "drift", angle: 82, speed: [
|
|
3497
|
+
sprite: { kind: "circle", radius: [3241e-6, 6481e-6], color: "#e8eff5" },
|
|
3498
|
+
motion: { type: "drift", angle: 82, speed: [0.05093, 0.1111], bob: 9259e-6 }
|
|
3499
|
+
}
|
|
3500
|
+
]
|
|
3501
|
+
};
|
|
3502
|
+
|
|
3503
|
+
// src/examples/warp-tunnel.ts
|
|
3504
|
+
var WARP_TUNNEL_SPEC = {
|
|
3505
|
+
schemaVersion: 1,
|
|
3506
|
+
id: "warp-tunnel",
|
|
3507
|
+
label: "Warp Tunnel",
|
|
3508
|
+
seed: 1701,
|
|
3509
|
+
motionIntensity: "energetic",
|
|
3510
|
+
ghosting: 0.55,
|
|
3511
|
+
background: { type: "solid", color: "#020208" },
|
|
3512
|
+
layers: [
|
|
3513
|
+
{
|
|
3514
|
+
key: "far-stars",
|
|
3515
|
+
count: 150,
|
|
3516
|
+
sprite: {
|
|
3517
|
+
kind: "streak",
|
|
3518
|
+
length: [8e-3, 0.02],
|
|
3519
|
+
width: 15e-4,
|
|
3520
|
+
color: "#cfd8ff",
|
|
3521
|
+
colors: ["#cfd8ff", "#ffffff", "#8ab4ff", "#ffd9a0"],
|
|
3522
|
+
colorWeights: [5, 3, 1.5, 0.5]
|
|
3523
|
+
},
|
|
3524
|
+
motion: { type: "warp", speed: [0.06, 0.16] },
|
|
3525
|
+
alpha: [0.5, 0.9],
|
|
3526
|
+
blend: "lighter"
|
|
3527
|
+
},
|
|
3528
|
+
{
|
|
3529
|
+
key: "near-stars",
|
|
3530
|
+
count: 50,
|
|
3531
|
+
sprite: {
|
|
3532
|
+
kind: "streak",
|
|
3533
|
+
length: [0.025, 0.06],
|
|
3534
|
+
width: 25e-4,
|
|
3535
|
+
color: "#ffffff",
|
|
3536
|
+
colors: ["#ffffff", "#a8c8ff"],
|
|
3537
|
+
colorWeights: [3, 1]
|
|
3538
|
+
},
|
|
3539
|
+
motion: { type: "warp", speed: [0.3, 0.55] },
|
|
3540
|
+
alpha: [0.7, 1],
|
|
3541
|
+
blend: "lighter"
|
|
3542
|
+
},
|
|
3543
|
+
{
|
|
3544
|
+
key: "core-glow",
|
|
3545
|
+
count: 1,
|
|
3546
|
+
sprite: { kind: "circle", radius: [0.06, 0.06], color: "#3a4a8f", soft: true },
|
|
3547
|
+
motion: { type: "static" },
|
|
3548
|
+
position: { x: 0.5, y: 0.5 },
|
|
3549
|
+
alpha: [0.35, 0.35],
|
|
3550
|
+
pulse: { amp: 0.15, period: 5e3 },
|
|
3551
|
+
blend: "lighter"
|
|
1736
3552
|
}
|
|
1737
3553
|
]
|
|
1738
3554
|
};
|
|
@@ -1744,9 +3560,15 @@ var SCHEMA_EXAMPLES = [
|
|
|
1744
3560
|
{ id: "snowfall", label: "Snowfall", spec: SNOWFALL_SPEC },
|
|
1745
3561
|
{ id: "lanterns", label: "Night Lanterns", spec: LANTERNS_SPEC },
|
|
1746
3562
|
{ id: "sakura", label: "Sakura Drift", spec: SAKURA_SPEC },
|
|
1747
|
-
{ id: "dev-dashboard", label: "
|
|
3563
|
+
{ id: "dev-dashboard", label: "Control Center", spec: DASHBOARD_SPEC },
|
|
1748
3564
|
{ id: "orrery", label: "Orrery", spec: ORRERY_SPEC },
|
|
1749
|
-
{ id: "constellation", label: "Constellation", spec: CONSTELLATION_SPEC }
|
|
3565
|
+
{ id: "constellation", label: "Constellation", spec: CONSTELLATION_SPEC },
|
|
3566
|
+
{ id: "comets", label: "Comet Shower", spec: COMETS_SPEC },
|
|
3567
|
+
{ id: "aurora", label: "Aurora", spec: AURORA_SPEC },
|
|
3568
|
+
{ id: "warp-tunnel", label: "Warp Tunnel", spec: WARP_TUNNEL_SPEC },
|
|
3569
|
+
{ id: "polygons", label: "Polygons", spec: POLYGONS_SPEC },
|
|
3570
|
+
{ id: "matrix-rain", label: "Matrix Rain", spec: MATRIX_RAIN_SPEC },
|
|
3571
|
+
{ id: "procession", label: "Night Procession", spec: PROCESSION_SPEC }
|
|
1750
3572
|
];
|
|
1751
3573
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
1752
3574
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -1754,34 +3576,52 @@ var EXAMPLE_BY_ID = Object.fromEntries(
|
|
|
1754
3576
|
);
|
|
1755
3577
|
export {
|
|
1756
3578
|
AQUARIUM_SPEC,
|
|
3579
|
+
AURORA_SPEC,
|
|
3580
|
+
COMETS_SPEC,
|
|
1757
3581
|
CONSTELLATION_SPEC,
|
|
1758
3582
|
DASHBOARD_SPEC,
|
|
1759
3583
|
EXAMPLE_BY_ID,
|
|
1760
3584
|
EXAMPLE_SPECS,
|
|
1761
3585
|
LANTERNS_SPEC,
|
|
1762
3586
|
LIMITS,
|
|
3587
|
+
MATRIX_RAIN_SPEC,
|
|
1763
3588
|
ORRERY_SPEC,
|
|
3589
|
+
POLYGONS_SPEC,
|
|
3590
|
+
PROCESSION_SPEC,
|
|
1764
3591
|
RAIN_SPEC,
|
|
1765
3592
|
SAKURA_SPEC,
|
|
1766
3593
|
SCHEMA_EXAMPLES,
|
|
1767
3594
|
SCHEMA_VERSION,
|
|
1768
3595
|
SNOWFALL_SPEC,
|
|
3596
|
+
WARP_NEAR,
|
|
3597
|
+
WARP_TUNNEL_SPEC,
|
|
1769
3598
|
adviseSpec,
|
|
1770
3599
|
alphaAt,
|
|
1771
3600
|
applyDeltasToSpec,
|
|
1772
3601
|
assertValidSpec,
|
|
1773
3602
|
buildEntities,
|
|
1774
3603
|
compileSaver,
|
|
3604
|
+
describeScene,
|
|
3605
|
+
diffScenes,
|
|
3606
|
+
dominanceRanking,
|
|
1775
3607
|
easeSmooth,
|
|
3608
|
+
headingAt,
|
|
1776
3609
|
lerpSpec,
|
|
3610
|
+
lifeAlphaAt,
|
|
3611
|
+
linkEdges,
|
|
1777
3612
|
linkPairs,
|
|
3613
|
+
luminanceGrid,
|
|
1778
3614
|
manifestFor,
|
|
3615
|
+
motionStats,
|
|
3616
|
+
perceiveScene,
|
|
1779
3617
|
positionAt,
|
|
3618
|
+
renderBrailleMap,
|
|
1780
3619
|
resolveSpecPath,
|
|
1781
3620
|
rotationAt,
|
|
1782
3621
|
sizeAt,
|
|
1783
3622
|
spriteIndexAt,
|
|
1784
3623
|
spriteVariants,
|
|
3624
|
+
steerablePaths,
|
|
1785
3625
|
structuralSignature,
|
|
1786
3626
|
validateSpec
|
|
1787
3627
|
};
|