@idle-screens/schema 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +201 -5
- package/dist/index.js +869 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +86 -3
package/dist/index.js
CHANGED
|
@@ -8,8 +8,28 @@ var LIMITS = {
|
|
|
8
8
|
// px/sec — bounds motion so nothing teleports
|
|
9
9
|
maxPulseAmp: 0.5,
|
|
10
10
|
// opacity breathing amplitude cap
|
|
11
|
-
minPulsePeriod: 500
|
|
11
|
+
minPulsePeriod: 500,
|
|
12
12
|
// ms — caps pulse at 2 Hz (WCAG flash threshold is 3 Hz)
|
|
13
|
+
maxSpin: 360,
|
|
14
|
+
// degrees/sec — one full revolution per second
|
|
15
|
+
maxGrowAmp: 0.8,
|
|
16
|
+
// size breathing amplitude cap (fraction of base size)
|
|
17
|
+
maxOrbitSpeed: 180,
|
|
18
|
+
// degrees/sec — half a revolution per second
|
|
19
|
+
maxLinksK: 8,
|
|
20
|
+
maxLinkLayerCount: 200,
|
|
21
|
+
minCyclePeriod: 500,
|
|
22
|
+
// ms — same flash-safety floor as pulse
|
|
23
|
+
referenceViewport: 1080,
|
|
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
|
|
13
33
|
};
|
|
14
34
|
|
|
15
35
|
// src/validate.ts
|
|
@@ -18,10 +38,45 @@ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
18
38
|
var isStr = (v) => typeof v === "string";
|
|
19
39
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
20
40
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
41
|
+
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "units", "background", "layers"]);
|
|
42
|
+
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
43
|
+
"count",
|
|
44
|
+
"sprite",
|
|
45
|
+
"motion",
|
|
46
|
+
"size",
|
|
47
|
+
"wrap",
|
|
48
|
+
"flip",
|
|
49
|
+
"alpha",
|
|
50
|
+
"blend",
|
|
51
|
+
"region",
|
|
52
|
+
"pulse",
|
|
53
|
+
"spin",
|
|
54
|
+
"grow",
|
|
55
|
+
"key",
|
|
56
|
+
"position",
|
|
57
|
+
"trail",
|
|
58
|
+
"links"
|
|
59
|
+
]);
|
|
60
|
+
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors"]);
|
|
61
|
+
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
62
|
+
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
63
|
+
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob"]);
|
|
64
|
+
var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway"]);
|
|
65
|
+
var KNOWN_BOUNCE = /* @__PURE__ */ new Set(["type", "speed"]);
|
|
66
|
+
var KNOWN_STATIC = /* @__PURE__ */ new Set(["type"]);
|
|
67
|
+
var KNOWN_ORBIT = /* @__PURE__ */ new Set(["type", "speed", "radius", "center"]);
|
|
68
|
+
var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
69
|
+
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
70
|
+
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key"]);
|
|
71
|
+
function unknownKeys(obj, known) {
|
|
72
|
+
return Object.keys(obj).filter((k) => !known.has(k));
|
|
73
|
+
}
|
|
21
74
|
function validateSpec(spec) {
|
|
22
75
|
const errors = [];
|
|
76
|
+
const warnings = [];
|
|
23
77
|
const err = (path, message) => void errors.push({ path, message });
|
|
24
|
-
|
|
78
|
+
const warn = (path, code, message) => void warnings.push({ path, code, message });
|
|
79
|
+
if (!isObj(spec)) return { valid: false, errors: [{ path: "", message: "spec must be an object" }], warnings: [] };
|
|
25
80
|
if (spec.schemaVersion !== SCHEMA_VERSION) err("schemaVersion", `must be ${SCHEMA_VERSION}`);
|
|
26
81
|
if (!isStr(spec.id) || spec.id.trim() === "") err("id", "must be a non-empty string");
|
|
27
82
|
if (!isStr(spec.label) || spec.label.trim() === "") err("label", "must be a non-empty string");
|
|
@@ -29,7 +84,13 @@ function validateSpec(spec) {
|
|
|
29
84
|
if (spec.motionIntensity !== void 0 && !["calm", "moderate", "energetic"].includes(spec.motionIntensity)) {
|
|
30
85
|
err("motionIntensity", "must be calm | moderate | energetic");
|
|
31
86
|
}
|
|
32
|
-
if (spec.
|
|
87
|
+
if (spec.units !== void 0 && spec.units !== "px" && spec.units !== "viewport") {
|
|
88
|
+
err("units", "must be 'px' | 'viewport'");
|
|
89
|
+
}
|
|
90
|
+
for (const k of unknownKeys(spec, KNOWN_TOP)) {
|
|
91
|
+
warn(k, "unknown-property", `unknown top-level property '${k}' \u2014 will be ignored`);
|
|
92
|
+
}
|
|
93
|
+
if (spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
33
94
|
if (!Array.isArray(spec.layers) || spec.layers.length === 0) {
|
|
34
95
|
err("layers", "must be a non-empty array");
|
|
35
96
|
} else {
|
|
@@ -37,19 +98,22 @@ function validateSpec(spec) {
|
|
|
37
98
|
let total = 0;
|
|
38
99
|
spec.layers.forEach((layer, i) => {
|
|
39
100
|
total += isObj(layer) && isNum(layer.count) ? layer.count : 0;
|
|
40
|
-
validateLayer(layer, `layers[${i}]`, err);
|
|
101
|
+
validateLayer(layer, `layers[${i}]`, err, warn, spec);
|
|
41
102
|
});
|
|
42
103
|
if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
|
|
43
104
|
}
|
|
44
|
-
return { valid: errors.length === 0, errors };
|
|
105
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
45
106
|
}
|
|
46
107
|
function color(v, path, err) {
|
|
47
108
|
if (!isStr(v) || !HEX.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
48
109
|
}
|
|
49
|
-
function validateBackground(bg, err) {
|
|
110
|
+
function validateBackground(bg, err, warn) {
|
|
50
111
|
if (!isObj(bg)) return err("background", "must be an object");
|
|
51
112
|
if (bg.type === "solid") {
|
|
52
113
|
color(bg.color, "background.color", err);
|
|
114
|
+
for (const k of unknownKeys(bg, KNOWN_BG_SOLID)) {
|
|
115
|
+
warn(`background.${k}`, "unknown-property", `unknown background property '${k}' \u2014 will be ignored`);
|
|
116
|
+
}
|
|
53
117
|
} else if (bg.type === "gradient") {
|
|
54
118
|
if (!Array.isArray(bg.stops) || bg.stops.length < 2) {
|
|
55
119
|
err("background.stops", "gradient needs at least 2 stops");
|
|
@@ -59,6 +123,17 @@ function validateBackground(bg, err) {
|
|
|
59
123
|
color(isObj(s) ? s.color : void 0, `background.stops[${i}].color`, err);
|
|
60
124
|
});
|
|
61
125
|
}
|
|
126
|
+
if (bg.drift !== void 0) {
|
|
127
|
+
if (!isObj(bg.drift)) err("background.drift", "must be an object");
|
|
128
|
+
else {
|
|
129
|
+
if (!isNum(bg.drift.period) || bg.drift.period < LIMITS.minDriftPeriod) {
|
|
130
|
+
err("background.drift.period", `must be >= ${LIMITS.minDriftPeriod} ms`);
|
|
131
|
+
}
|
|
132
|
+
if (bg.drift.amount !== void 0 && (!isNum(bg.drift.amount) || bg.drift.amount <= 0 || bg.drift.amount > LIMITS.maxDriftAmount)) {
|
|
133
|
+
err("background.drift.amount", `must be > 0 and <= ${LIMITS.maxDriftAmount}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
62
137
|
if (bg.band !== void 0) {
|
|
63
138
|
if (!isObj(bg.band)) err("background.band", "must be an object");
|
|
64
139
|
else {
|
|
@@ -66,12 +141,24 @@ function validateBackground(bg, err) {
|
|
|
66
141
|
if (!isNum(bg.band.height) || bg.band.height <= 0) err("background.band.height", "must be > 0");
|
|
67
142
|
}
|
|
68
143
|
}
|
|
144
|
+
for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
|
|
145
|
+
warn(`background.${k}`, "unknown-property", `unknown background property '${k}' \u2014 will be ignored`);
|
|
146
|
+
}
|
|
69
147
|
} else {
|
|
70
148
|
err("background.type", "must be solid | gradient");
|
|
71
149
|
}
|
|
72
150
|
}
|
|
73
|
-
function validateLayer(layer, path, err) {
|
|
151
|
+
function validateLayer(layer, path, err, warn, spec) {
|
|
74
152
|
if (!isObj(layer)) return err(path, "must be an object");
|
|
153
|
+
for (const k of unknownKeys(layer, KNOWN_LAYER)) {
|
|
154
|
+
if (k === "id") {
|
|
155
|
+
warn(`${path}.id`, "misplaced-property", `'id' is not a layer property \u2014 did you mean 'key'? Use 'key' to name a layer`);
|
|
156
|
+
} else if (k === "depth") {
|
|
157
|
+
warn(`${path}.depth`, "unknown-property", `'depth' is not yet supported \u2014 fake parallax with size/speed/alpha across layers instead`);
|
|
158
|
+
} else {
|
|
159
|
+
warn(`${path}.${k}`, "unknown-property", `unknown layer property '${k}' \u2014 will be ignored`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
75
162
|
if (!isNum(layer.count) || !Number.isInteger(layer.count) || layer.count < 1) {
|
|
76
163
|
err(`${path}.count`, "must be a positive integer");
|
|
77
164
|
} else if (layer.count > LIMITS.maxPerLayer) {
|
|
@@ -120,16 +207,67 @@ function validateLayer(layer, path, err) {
|
|
|
120
207
|
}
|
|
121
208
|
}
|
|
122
209
|
}
|
|
123
|
-
|
|
124
|
-
|
|
210
|
+
if (layer.spin !== void 0) {
|
|
211
|
+
if (!isNum(layer.spin)) err(`${path}.spin`, "must be a number (degrees/sec)");
|
|
212
|
+
else if (Math.abs(layer.spin) > LIMITS.maxSpin) err(`${path}.spin`, `must be within \xB1${LIMITS.maxSpin} deg/sec`);
|
|
213
|
+
}
|
|
214
|
+
if (layer.grow !== void 0) {
|
|
215
|
+
if (!isObj(layer.grow)) err(`${path}.grow`, "must be an object");
|
|
216
|
+
else {
|
|
217
|
+
if (!isNum(layer.grow.amp) || layer.grow.amp <= 0 || layer.grow.amp > LIMITS.maxGrowAmp) {
|
|
218
|
+
err(`${path}.grow.amp`, `must be within 0..${LIMITS.maxGrowAmp}`);
|
|
219
|
+
}
|
|
220
|
+
if (!isNum(layer.grow.period) || layer.grow.period < LIMITS.minPulsePeriod) {
|
|
221
|
+
err(`${path}.grow.period`, `must be >= ${LIMITS.minPulsePeriod} ms (flash-safety floor)`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (layer.links !== void 0) {
|
|
226
|
+
if (!isObj(layer.links)) err(`${path}.links`, "must be an object");
|
|
227
|
+
else {
|
|
228
|
+
if (!isNum(layer.links.k) || !Number.isInteger(layer.links.k) || layer.links.k < 1 || layer.links.k > LIMITS.maxLinksK) {
|
|
229
|
+
err(`${path}.links.k`, `must be an integer 1..${LIMITS.maxLinksK}`);
|
|
230
|
+
}
|
|
231
|
+
if (!isNum(layer.links.maxDist) || layer.links.maxDist <= 0) {
|
|
232
|
+
err(`${path}.links.maxDist`, "must be > 0");
|
|
233
|
+
}
|
|
234
|
+
if (layer.links.color !== void 0) color(layer.links.color, `${path}.links.color`, err);
|
|
235
|
+
if (layer.links.alpha !== void 0 && (!isNum(layer.links.alpha) || layer.links.alpha < 0 || layer.links.alpha > 1)) {
|
|
236
|
+
err(`${path}.links.alpha`, "must be 0..1");
|
|
237
|
+
}
|
|
238
|
+
if (layer.links.width !== void 0 && (!isNum(layer.links.width) || layer.links.width <= 0)) {
|
|
239
|
+
err(`${path}.links.width`, "must be > 0");
|
|
240
|
+
}
|
|
241
|
+
if (isNum(layer.count) && layer.count > LIMITS.maxLinkLayerCount) {
|
|
242
|
+
err(`${path}.links`, `layer count must be <= ${LIMITS.maxLinkLayerCount} when links is set`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (layer.trail !== void 0) {
|
|
247
|
+
if (!isObj(layer.trail)) err(`${path}.trail`, "must be an object");
|
|
248
|
+
else {
|
|
249
|
+
if (!isNum(layer.trail.length) || layer.trail.length <= 0 || layer.trail.length > LIMITS.maxTrailLength) {
|
|
250
|
+
err(`${path}.trail.length`, `must be > 0 and <= ${LIMITS.maxTrailLength} ms`);
|
|
251
|
+
}
|
|
252
|
+
if (layer.trail.fade !== void 0 && (!isNum(layer.trail.fade) || layer.trail.fade < 0 || layer.trail.fade > 1)) {
|
|
253
|
+
err(`${path}.trail.fade`, "must be 0..1");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
validateSprite(layer.sprite, `${path}.sprite`, err, warn);
|
|
258
|
+
validateMotion(layer.motion, `${path}.motion`, err, warn, spec);
|
|
125
259
|
}
|
|
126
|
-
function validateSprite(sprite, path, err) {
|
|
260
|
+
function validateSprite(sprite, path, err, warn) {
|
|
127
261
|
if (!isObj(sprite)) return err(path, "must be an object");
|
|
262
|
+
let knownSet;
|
|
128
263
|
if (sprite.kind === "emoji") {
|
|
264
|
+
knownSet = KNOWN_EMOJI;
|
|
129
265
|
if (!Array.isArray(sprite.glyphs) || sprite.glyphs.length === 0 || !sprite.glyphs.every(isStr)) {
|
|
130
266
|
err(`${path}.glyphs`, "must be a non-empty array of strings");
|
|
131
267
|
}
|
|
268
|
+
validateCycle(sprite, path, err);
|
|
132
269
|
} else if (sprite.kind === "text") {
|
|
270
|
+
knownSet = KNOWN_TEXT;
|
|
133
271
|
if (!Array.isArray(sprite.strings) || sprite.strings.length === 0 || !sprite.strings.every(isStr)) {
|
|
134
272
|
err(`${path}.strings`, "must be a non-empty array of strings");
|
|
135
273
|
}
|
|
@@ -143,33 +281,95 @@ function validateSprite(sprite, path, err) {
|
|
|
143
281
|
if (sprite.maxWidth !== void 0 && (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0)) {
|
|
144
282
|
err(`${path}.maxWidth`, "must be a positive number");
|
|
145
283
|
}
|
|
284
|
+
validateCycle(sprite, path, err);
|
|
146
285
|
} else if (sprite.kind === "circle") {
|
|
286
|
+
knownSet = KNOWN_CIRCLE;
|
|
147
287
|
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
|
|
148
288
|
color(sprite.color, `${path}.color`, err);
|
|
149
289
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
290
|
+
if (sprite.colors !== void 0) {
|
|
291
|
+
if (!Array.isArray(sprite.colors) || sprite.colors.length === 0) {
|
|
292
|
+
err(`${path}.colors`, "must be a non-empty array of hex colours");
|
|
293
|
+
} else {
|
|
294
|
+
sprite.colors.forEach((c, ci) => color(c, `${path}.colors[${ci}]`, err));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
150
297
|
} else {
|
|
151
298
|
err(`${path}.kind`, "must be emoji | text | circle");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
for (const k of unknownKeys(sprite, knownSet)) {
|
|
302
|
+
if (LAYER_PROPS_ON_SPRITE.has(k)) {
|
|
303
|
+
warn(`${path}.${k}`, "misplaced-property", `'${k}' belongs on the layer, not inside sprite \u2014 move it up one level`);
|
|
304
|
+
} else {
|
|
305
|
+
warn(`${path}.${k}`, "unknown-property", `unknown sprite property '${k}' \u2014 will be ignored`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function validateCycle(sprite, path, err) {
|
|
310
|
+
if (sprite.cycle === void 0) return;
|
|
311
|
+
if (!isObj(sprite.cycle)) return err(`${path}.cycle`, "must be an object");
|
|
312
|
+
if (!isNum(sprite.cycle.period) || sprite.cycle.period < LIMITS.minCyclePeriod) {
|
|
313
|
+
err(`${path}.cycle.period`, `must be >= ${LIMITS.minCyclePeriod} ms (flash-safety floor)`);
|
|
152
314
|
}
|
|
153
315
|
}
|
|
154
|
-
function validateMotion(motion, path, err) {
|
|
316
|
+
function validateMotion(motion, path, err, warn, spec) {
|
|
155
317
|
if (!isObj(motion)) return err(path, "must be an object");
|
|
318
|
+
const isViewport = isObj(spec) && spec.units === "viewport";
|
|
319
|
+
const speedCap = isViewport ? LIMITS.maxSpeed / LIMITS.referenceViewport : LIMITS.maxSpeed;
|
|
156
320
|
const speedOk = (v, p) => {
|
|
157
321
|
if (!isRange(v)) err(p, "must be a [min,max] range");
|
|
158
|
-
else if (v[1] >
|
|
322
|
+
else if (v[1] > speedCap) err(p, `speed exceeds cap ${isViewport ? speedCap.toFixed(2) + " viewport-units/sec" : LIMITS.maxSpeed + " px/sec"}`);
|
|
159
323
|
};
|
|
324
|
+
let knownSet;
|
|
160
325
|
if (motion.type === "drift") {
|
|
326
|
+
knownSet = KNOWN_DRIFT;
|
|
161
327
|
speedOk(motion.speed, `${path}.speed`);
|
|
162
328
|
if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
|
|
163
329
|
if (motion.bidirectional !== void 0 && typeof motion.bidirectional !== "boolean") err(`${path}.bidirectional`, "must be a boolean");
|
|
164
330
|
if (motion.bob !== void 0 && !isNum(motion.bob)) err(`${path}.bob`, "must be a number");
|
|
331
|
+
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
332
|
+
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`);
|
|
333
|
+
}
|
|
165
334
|
} else if (motion.type === "rise") {
|
|
335
|
+
knownSet = KNOWN_RISE;
|
|
166
336
|
speedOk(motion.speed, `${path}.speed`);
|
|
167
337
|
if (motion.sway !== void 0 && !isNum(motion.sway)) err(`${path}.sway`, "must be a number");
|
|
338
|
+
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
339
|
+
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`);
|
|
340
|
+
}
|
|
168
341
|
} else if (motion.type === "bounce") {
|
|
342
|
+
knownSet = KNOWN_BOUNCE;
|
|
169
343
|
speedOk(motion.speed, `${path}.speed`);
|
|
344
|
+
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
345
|
+
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`);
|
|
346
|
+
}
|
|
170
347
|
} else if (motion.type === "static") {
|
|
348
|
+
knownSet = KNOWN_STATIC;
|
|
349
|
+
} else if (motion.type === "orbit") {
|
|
350
|
+
knownSet = KNOWN_ORBIT;
|
|
351
|
+
if (!isRange(motion.speed)) err(`${path}.speed`, "must be a [min,max] range");
|
|
352
|
+
else {
|
|
353
|
+
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`);
|
|
354
|
+
if (Math.max(Math.abs(motion.speed[0]), Math.abs(motion.speed[1])) < 1) {
|
|
355
|
+
warn(`${path}.speed`, "near-zero-speed", `orbit speed is near zero \u2014 entities will appear frozen. Typical range: 5\u201360 deg/sec`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (!isRange(motion.radius) || motion.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
|
|
359
|
+
if (motion.center !== void 0) {
|
|
360
|
+
if (!isObj(motion.center) || !isNum(motion.center.x) || !isNum(motion.center.y)) {
|
|
361
|
+
err(`${path}.center`, "must be {x, y} with numbers 0..1");
|
|
362
|
+
} else {
|
|
363
|
+
if (motion.center.x < 0 || motion.center.x > 1) err(`${path}.center.x`, "must be 0..1");
|
|
364
|
+
if (motion.center.y < 0 || motion.center.y > 1) err(`${path}.center.y`, "must be 0..1");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
171
367
|
} else {
|
|
172
|
-
err(`${path}.type`, "must be drift | rise | bounce | static");
|
|
368
|
+
err(`${path}.type`, "must be drift | rise | bounce | static | orbit");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
for (const k of unknownKeys(motion, knownSet)) {
|
|
372
|
+
warn(`${path}.${k}`, "unknown-property", `unknown motion property '${k}' \u2014 will be ignored`);
|
|
173
373
|
}
|
|
174
374
|
}
|
|
175
375
|
function assertValidSpec(spec) {
|
|
@@ -187,12 +387,18 @@ function spriteVariants(sprite) {
|
|
|
187
387
|
if (sprite.kind === "text") return sprite.strings.length;
|
|
188
388
|
return 1;
|
|
189
389
|
}
|
|
190
|
-
function buildEntities(layer, rng, w, h) {
|
|
390
|
+
function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
391
|
+
const effectiveCount = countScale === 1 ? layer.count : Math.max(1, Math.min(
|
|
392
|
+
Math.round(layer.count * countScale),
|
|
393
|
+
layer.links ? LIMITS.maxLinkLayerCount : LIMITS.maxPerLayer
|
|
394
|
+
));
|
|
191
395
|
const [smin, smax] = layer.size ?? [20, 40];
|
|
192
396
|
const variants = spriteVariants(layer.sprite);
|
|
397
|
+
const colorsLen = layer.sprite.kind === "circle" && layer.sprite.colors ? layer.sprite.colors.length : 0;
|
|
398
|
+
const cycle = layer.sprite.kind === "emoji" || layer.sprite.kind === "text" ? layer.sprite.cycle : void 0;
|
|
193
399
|
const out = [];
|
|
194
|
-
for (let i = 0; i <
|
|
195
|
-
const size = layer.sprite.kind === "circle" ? rng.range(layer.sprite.radius[0], layer.sprite.radius[1]) * 2 : rng.range(smin, smax);
|
|
400
|
+
for (let i = 0; i < effectiveCount; i++) {
|
|
401
|
+
const size = layer.sprite.kind === "circle" ? rng.range(layer.sprite.radius[0], layer.sprite.radius[1]) * 2 * scale : rng.range(smin, smax) * scale;
|
|
196
402
|
let vx = 0;
|
|
197
403
|
let vy = 0;
|
|
198
404
|
let bob = 0;
|
|
@@ -201,22 +407,25 @@ function buildEntities(layer, rng, w, h) {
|
|
|
201
407
|
if (m.type === "static") {
|
|
202
408
|
motion = "static";
|
|
203
409
|
} else if (m.type === "drift") {
|
|
204
|
-
const s = rng.range(m.speed[0], m.speed[1]);
|
|
410
|
+
const s = rng.range(m.speed[0], m.speed[1]) * scale;
|
|
205
411
|
let angle = (m.angle ?? 0) * Math.PI / 180;
|
|
206
412
|
if (m.bidirectional && rng.next() < 0.5) angle = Math.PI - angle;
|
|
207
413
|
vx = s * Math.cos(angle);
|
|
208
414
|
vy = s * Math.sin(angle);
|
|
209
|
-
bob = m.bob ?? 0;
|
|
415
|
+
bob = (m.bob ?? 0) * scale;
|
|
210
416
|
} else if (m.type === "rise") {
|
|
211
417
|
motion = "rise";
|
|
212
|
-
vy = -rng.range(m.speed[0], m.speed[1]);
|
|
213
|
-
bob = m.sway ?? 0;
|
|
214
|
-
} else {
|
|
418
|
+
vy = -rng.range(m.speed[0], m.speed[1]) * scale;
|
|
419
|
+
bob = (m.sway ?? 0) * scale;
|
|
420
|
+
} else if (m.type === "bounce") {
|
|
215
421
|
motion = "bounce";
|
|
216
|
-
const s = rng.range(m.speed[0], m.speed[1]);
|
|
422
|
+
const s = rng.range(m.speed[0], m.speed[1]) * scale;
|
|
217
423
|
const a = rng.range(0, Math.PI * 2);
|
|
218
424
|
vx = s * Math.cos(a);
|
|
219
425
|
vy = s * Math.sin(a);
|
|
426
|
+
} else if (m.type === "orbit") {
|
|
427
|
+
motion = "orbit";
|
|
428
|
+
vx = rng.range(m.speed[0], m.speed[1]);
|
|
220
429
|
}
|
|
221
430
|
let x0;
|
|
222
431
|
let y0;
|
|
@@ -243,7 +452,20 @@ function buildEntities(layer, rng, w, h) {
|
|
|
243
452
|
alpha: layer.alpha ? rng.range(layer.alpha[0], layer.alpha[1]) : 1,
|
|
244
453
|
pulseAmp: layer.pulse?.amp ?? 0,
|
|
245
454
|
pulsePeriod: layer.pulse?.period ?? 1e3,
|
|
246
|
-
pulsePhase: layer.pulse ? rng.range(0, Math.PI * 2) : 0
|
|
455
|
+
pulsePhase: layer.pulse ? rng.range(0, Math.PI * 2) : 0,
|
|
456
|
+
// New draws AFTER pulsePhase — guarded so existing specs keep identical streams.
|
|
457
|
+
spinSpeed: layer.spin ? layer.spin * Math.PI / 180 : 0,
|
|
458
|
+
// deg/sec → rad/sec
|
|
459
|
+
spinPhase: layer.spin ? rng.range(0, Math.PI * 2) : 0,
|
|
460
|
+
growAmp: layer.grow?.amp ?? 0,
|
|
461
|
+
growPeriod: layer.grow?.period ?? 1e3,
|
|
462
|
+
growPhase: layer.grow ? rng.range(0, Math.PI * 2) : 0,
|
|
463
|
+
orbitR: m.type === "orbit" ? rng.range(m.radius[0], m.radius[1]) * scale : 0,
|
|
464
|
+
orbitCx: m.type === "orbit" ? (m.center?.x ?? 0.5) * w : 0,
|
|
465
|
+
orbitCy: m.type === "orbit" ? (m.center?.y ?? 0.5) * h : 0,
|
|
466
|
+
// New draws AFTER orbit — guarded so existing specs keep identical streams.
|
|
467
|
+
colorIndex: colorsLen > 0 ? rng.int(0, colorsLen - 1) : -1,
|
|
468
|
+
cyclePeriod: cycle ? cycle.period : 0
|
|
247
469
|
});
|
|
248
470
|
}
|
|
249
471
|
return out;
|
|
@@ -253,6 +475,15 @@ function alphaAt(e, t) {
|
|
|
253
475
|
const a = e.alpha + e.pulseAmp * Math.sin(t * 2 * Math.PI / e.pulsePeriod + e.pulsePhase);
|
|
254
476
|
return a < 0 ? 0 : a > 1 ? 1 : a;
|
|
255
477
|
}
|
|
478
|
+
function sizeAt(e, t) {
|
|
479
|
+
if (!e.growAmp) return e.size;
|
|
480
|
+
const s = e.size * (1 + e.growAmp * Math.sin(t * 2 * Math.PI / e.growPeriod + e.growPhase));
|
|
481
|
+
return s > 0 ? s : 0.1;
|
|
482
|
+
}
|
|
483
|
+
function rotationAt(e, t) {
|
|
484
|
+
if (!e.spinSpeed) return 0;
|
|
485
|
+
return e.spinPhase + e.spinSpeed * (t / 1e3);
|
|
486
|
+
}
|
|
256
487
|
function wrap(v, min, max) {
|
|
257
488
|
const range = max - min;
|
|
258
489
|
return ((v - min) % range + range) % range + min;
|
|
@@ -266,6 +497,14 @@ function reflect(v, min, max) {
|
|
|
266
497
|
}
|
|
267
498
|
function positionAt(e, t, w, h) {
|
|
268
499
|
if (e.motion === "static") return { x: e.x0, y: e.y0, flip: false };
|
|
500
|
+
if (e.motion === "orbit") {
|
|
501
|
+
const angle = e.phase + e.vx * Math.PI / 180 * (t / 1e3);
|
|
502
|
+
return {
|
|
503
|
+
x: e.orbitCx + e.orbitR * Math.cos(angle),
|
|
504
|
+
y: e.orbitCy + e.orbitR * Math.sin(angle),
|
|
505
|
+
flip: false
|
|
506
|
+
};
|
|
507
|
+
}
|
|
269
508
|
const dt = t / 1e3;
|
|
270
509
|
const m = e.size;
|
|
271
510
|
if (e.motion === "bounce") {
|
|
@@ -287,6 +526,43 @@ function positionAt(e, t, w, h) {
|
|
|
287
526
|
if (e.bob) y += e.bob * Math.sin(t / 500 + e.phase);
|
|
288
527
|
return { x, y, flip: e.headingLeft };
|
|
289
528
|
}
|
|
529
|
+
function spriteIndexAt(e, t, variants) {
|
|
530
|
+
if (!e.cyclePeriod || variants <= 1) return e.spriteIndex;
|
|
531
|
+
return Math.floor(t / e.cyclePeriod + e.phase / (2 * Math.PI)) % variants;
|
|
532
|
+
}
|
|
533
|
+
function linkPairs(positions, k, maxDist, doWrap, w, h) {
|
|
534
|
+
const n = positions.length;
|
|
535
|
+
const edges = /* @__PURE__ */ new Set();
|
|
536
|
+
const out = [];
|
|
537
|
+
for (let i = 0; i < n; i++) {
|
|
538
|
+
const pi = positions[i];
|
|
539
|
+
const neighbors = [];
|
|
540
|
+
for (let j = 0; j < n; j++) {
|
|
541
|
+
if (i === j) continue;
|
|
542
|
+
const pj = positions[j];
|
|
543
|
+
let dx = pj.x - pi.x;
|
|
544
|
+
let dy = pj.y - pi.y;
|
|
545
|
+
if (doWrap) {
|
|
546
|
+
if (Math.abs(dx) > w / 2) dx = dx > 0 ? dx - w : dx + w;
|
|
547
|
+
if (Math.abs(dy) > h / 2) dy = dy > 0 ? dy - h : dy + h;
|
|
548
|
+
}
|
|
549
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
550
|
+
if (dist <= maxDist) neighbors.push({ dist, j });
|
|
551
|
+
}
|
|
552
|
+
neighbors.sort((a, b) => a.dist - b.dist || a.j - b.j);
|
|
553
|
+
for (let ni = 0; ni < Math.min(k, neighbors.length); ni++) {
|
|
554
|
+
const j = neighbors[ni].j;
|
|
555
|
+
const lo = Math.min(i, j);
|
|
556
|
+
const hi = Math.max(i, j);
|
|
557
|
+
const key = `${lo}:${hi}`;
|
|
558
|
+
if (!edges.has(key)) {
|
|
559
|
+
edges.add(key);
|
|
560
|
+
out.push([lo, hi]);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
290
566
|
|
|
291
567
|
// src/compile.ts
|
|
292
568
|
import {
|
|
@@ -360,12 +636,39 @@ function lerpSpec(from, to, k) {
|
|
|
360
636
|
};
|
|
361
637
|
return walk(from, to);
|
|
362
638
|
}
|
|
639
|
+
function steerablePaths(spec) {
|
|
640
|
+
if (!spec || typeof spec !== "object") return [];
|
|
641
|
+
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units"]);
|
|
642
|
+
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops"]);
|
|
643
|
+
const out = [];
|
|
644
|
+
const walk = (node, prefix, key) => {
|
|
645
|
+
if (Array.isArray(node) && INDEXED.has(key)) {
|
|
646
|
+
node.forEach((child, i) => walk(child, `${prefix}.${i}`, ""));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (Array.isArray(node)) {
|
|
650
|
+
if (prefix) out.push(prefix);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (node && typeof node === "object") {
|
|
654
|
+
for (const [k, v] of Object.entries(node)) {
|
|
655
|
+
if (SKIP.has(k)) continue;
|
|
656
|
+
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (prefix) out.push(prefix);
|
|
661
|
+
};
|
|
662
|
+
walk(spec, "", "");
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
363
665
|
function easeSmooth(k) {
|
|
364
666
|
const c = Math.max(0, Math.min(1, k));
|
|
365
667
|
return c * c * (3 - 2 * c);
|
|
366
668
|
}
|
|
367
669
|
function structuralSignature(spec) {
|
|
368
|
-
return JSON.stringify(
|
|
670
|
+
return JSON.stringify([
|
|
671
|
+
spec.units,
|
|
369
672
|
spec.layers.map((l) => [
|
|
370
673
|
l.count,
|
|
371
674
|
l.size,
|
|
@@ -376,11 +679,14 @@ function structuralSignature(spec) {
|
|
|
376
679
|
l.flip,
|
|
377
680
|
l.alpha,
|
|
378
681
|
l.pulse,
|
|
682
|
+
l.spin,
|
|
683
|
+
l.grow,
|
|
379
684
|
l.sprite.kind,
|
|
380
685
|
l.sprite.kind === "circle" ? l.sprite.radius : void 0,
|
|
381
|
-
l.sprite.kind === "
|
|
686
|
+
l.sprite.kind === "circle" ? l.sprite.colors?.length : void 0,
|
|
687
|
+
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
|
|
382
688
|
])
|
|
383
|
-
);
|
|
689
|
+
]);
|
|
384
690
|
}
|
|
385
691
|
|
|
386
692
|
// src/compile.ts
|
|
@@ -460,10 +766,16 @@ var SpecInstance = class {
|
|
|
460
766
|
this.canvas.height = Math.max(1, Math.round(this.h * dpr));
|
|
461
767
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
462
768
|
}
|
|
463
|
-
/** (Re)seed and place all entities — deterministic for the seed +
|
|
769
|
+
/** (Re)seed and place all entities — deterministic for the seed + viewport. */
|
|
464
770
|
rebuild() {
|
|
465
771
|
const rng = createRng(this.seed);
|
|
466
|
-
|
|
772
|
+
const scale = this.effSpec.units === "viewport" ? Math.min(this.w, this.h) : 1;
|
|
773
|
+
let countScale = scale > 1 ? Math.min(this.w, this.h) / LIMITS.referenceViewport : 1;
|
|
774
|
+
if (countScale > 1) {
|
|
775
|
+
const rawTotal = this.effSpec.layers.reduce((s, l) => s + Math.round(l.count * countScale), 0);
|
|
776
|
+
if (rawTotal > LIMITS.maxTotal) countScale *= LIMITS.maxTotal / rawTotal;
|
|
777
|
+
}
|
|
778
|
+
this.layers = this.effSpec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, this.w, this.h, scale, countScale) }));
|
|
467
779
|
this.lastStructural = structuralSignature(this.effSpec);
|
|
468
780
|
}
|
|
469
781
|
start() {
|
|
@@ -484,7 +796,7 @@ var SpecInstance = class {
|
|
|
484
796
|
this.lastT = now - this.startT + this.baseT;
|
|
485
797
|
this.renderFrame(this.lastT, this.seed);
|
|
486
798
|
}
|
|
487
|
-
drawBackground() {
|
|
799
|
+
drawBackground(t) {
|
|
488
800
|
const { ctx, w, h } = this;
|
|
489
801
|
const bg = this.effSpec.background;
|
|
490
802
|
if (!bg || bg.type === "solid") {
|
|
@@ -493,50 +805,121 @@ var SpecInstance = class {
|
|
|
493
805
|
return;
|
|
494
806
|
}
|
|
495
807
|
const g = ctx.createLinearGradient(0, 0, 0, h);
|
|
496
|
-
|
|
808
|
+
const drift = bg.drift;
|
|
809
|
+
for (let i = 0; i < bg.stops.length; i++) {
|
|
810
|
+
const s = bg.stops[i];
|
|
811
|
+
let at = s.at;
|
|
812
|
+
if (drift) {
|
|
813
|
+
const amount = drift.amount ?? 0.15;
|
|
814
|
+
const phase = i / bg.stops.length * Math.PI * 2;
|
|
815
|
+
at = Math.max(0, Math.min(1, at + amount * Math.sin(t * 2 * Math.PI / drift.period + phase)));
|
|
816
|
+
}
|
|
817
|
+
g.addColorStop(at, s.color);
|
|
818
|
+
}
|
|
497
819
|
ctx.fillStyle = g;
|
|
498
820
|
ctx.fillRect(0, 0, w, h);
|
|
499
821
|
if (bg.band) {
|
|
500
822
|
ctx.fillStyle = bg.band.color;
|
|
501
|
-
|
|
823
|
+
const bh = bg.band.height * (this.effSpec.units === "viewport" ? Math.min(w, h) : 1);
|
|
824
|
+
ctx.fillRect(0, h - bh, w, bh);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
drawTrail(built, e, t) {
|
|
828
|
+
const trail = built.layer.trail;
|
|
829
|
+
if (!trail) return;
|
|
830
|
+
const { ctx, w, h } = this;
|
|
831
|
+
const fade = trail.fade ?? 1;
|
|
832
|
+
const n = Math.min(Math.ceil(trail.length / 50), LIMITS.maxTrailSamples);
|
|
833
|
+
const headAlpha = alphaAt(e, t);
|
|
834
|
+
const headSize = sizeAt(e, t);
|
|
835
|
+
const sprite = built.layer.sprite;
|
|
836
|
+
const resolvedColor = sprite.kind === "circle" ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
837
|
+
const isSoft = sprite.kind === "circle" && sprite.soft;
|
|
838
|
+
const wrap2 = built.layer.wrap !== false;
|
|
839
|
+
const head = positionAt(e, t, w, h);
|
|
840
|
+
let prevX = head.x;
|
|
841
|
+
let prevY = head.y;
|
|
842
|
+
for (let s = 1; s <= n; s++) {
|
|
843
|
+
const k = s / n;
|
|
844
|
+
const pastT = t - k * trail.length;
|
|
845
|
+
if (pastT < 0) break;
|
|
846
|
+
const pos = positionAt(e, pastT, w, h);
|
|
847
|
+
if (wrap2) {
|
|
848
|
+
const dx = pos.x - prevX;
|
|
849
|
+
const dy = pos.y - prevY;
|
|
850
|
+
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) break;
|
|
851
|
+
}
|
|
852
|
+
prevX = pos.x;
|
|
853
|
+
prevY = pos.y;
|
|
854
|
+
const a = headAlpha * (1 - k * fade);
|
|
855
|
+
if (a <= 0) break;
|
|
856
|
+
const r = headSize / 2 * (1 - k * 0.7);
|
|
857
|
+
if (r < 0.2) break;
|
|
858
|
+
ctx.globalAlpha = a;
|
|
859
|
+
if (isSoft) {
|
|
860
|
+
const g = ctx.createRadialGradient(pos.x, pos.y, 0, pos.x, pos.y, r);
|
|
861
|
+
g.addColorStop(0, resolvedColor);
|
|
862
|
+
g.addColorStop(0.35, hexToRgba(resolvedColor, 0.75));
|
|
863
|
+
g.addColorStop(1, hexToRgba(resolvedColor, 0));
|
|
864
|
+
ctx.fillStyle = g;
|
|
865
|
+
} else {
|
|
866
|
+
ctx.fillStyle = resolvedColor;
|
|
867
|
+
}
|
|
868
|
+
ctx.beginPath();
|
|
869
|
+
ctx.arc(pos.x, pos.y, r, 0, Math.PI * 2);
|
|
870
|
+
ctx.fill();
|
|
502
871
|
}
|
|
503
872
|
}
|
|
504
873
|
drawEntity(built, e, t) {
|
|
505
874
|
const { ctx } = this;
|
|
506
875
|
const p = positionAt(e, t, this.w, this.h);
|
|
507
876
|
const sprite = built.layer.sprite;
|
|
877
|
+
const sz = sizeAt(e, t);
|
|
878
|
+
const rot = rotationAt(e, t);
|
|
508
879
|
ctx.globalAlpha = alphaAt(e, t);
|
|
509
880
|
if (sprite.kind === "circle") {
|
|
510
|
-
const r =
|
|
881
|
+
const r = sz / 2;
|
|
882
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
883
|
+
ctx.save();
|
|
884
|
+
if (rot) {
|
|
885
|
+
ctx.translate(p.x, p.y);
|
|
886
|
+
ctx.rotate(rot);
|
|
887
|
+
ctx.translate(-p.x, -p.y);
|
|
888
|
+
}
|
|
511
889
|
if (sprite.soft) {
|
|
512
890
|
const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r);
|
|
513
|
-
g.addColorStop(0,
|
|
514
|
-
g.addColorStop(0.35, hexToRgba(
|
|
515
|
-
g.addColorStop(1, hexToRgba(
|
|
891
|
+
g.addColorStop(0, resolvedColor);
|
|
892
|
+
g.addColorStop(0.35, hexToRgba(resolvedColor, 0.75));
|
|
893
|
+
g.addColorStop(1, hexToRgba(resolvedColor, 0));
|
|
516
894
|
ctx.fillStyle = g;
|
|
517
895
|
} else {
|
|
518
|
-
ctx.fillStyle =
|
|
896
|
+
ctx.fillStyle = resolvedColor;
|
|
519
897
|
}
|
|
520
898
|
ctx.beginPath();
|
|
521
899
|
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
|
|
522
900
|
ctx.fill();
|
|
901
|
+
ctx.restore();
|
|
523
902
|
return;
|
|
524
903
|
}
|
|
525
904
|
ctx.save();
|
|
526
905
|
ctx.translate(p.x, p.y);
|
|
906
|
+
if (rot) ctx.rotate(rot);
|
|
527
907
|
if (p.flip && built.layer.flip) ctx.scale(-1, 1);
|
|
528
908
|
ctx.textAlign = "center";
|
|
529
909
|
ctx.textBaseline = "middle";
|
|
530
910
|
if (sprite.kind === "emoji") {
|
|
531
|
-
|
|
532
|
-
ctx.
|
|
911
|
+
const idx = spriteIndexAt(e, t, sprite.glyphs.length);
|
|
912
|
+
ctx.font = `${sz}px serif`;
|
|
913
|
+
ctx.fillText(sprite.glyphs[idx] ?? sprite.glyphs[0], 0, 0);
|
|
533
914
|
} else {
|
|
534
915
|
ctx.textAlign = sprite.align ?? "center";
|
|
535
916
|
ctx.textBaseline = sprite.baseline ?? "middle";
|
|
536
|
-
ctx.font = sprite.font ?? `${
|
|
917
|
+
ctx.font = sprite.font ?? `${sz}px system-ui, sans-serif`;
|
|
537
918
|
ctx.fillStyle = sprite.color ?? "#e6e8ef";
|
|
538
|
-
const
|
|
539
|
-
|
|
919
|
+
const idx = spriteIndexAt(e, t, sprite.strings.length);
|
|
920
|
+
const text = sprite.strings[idx] ?? sprite.strings[0];
|
|
921
|
+
const mw = sprite.maxWidth ? sprite.maxWidth * (this.effSpec.units === "viewport" ? Math.min(this.w, this.h) : 1) : void 0;
|
|
922
|
+
if (mw) ctx.fillText(text, 0, 0, mw);
|
|
540
923
|
else ctx.fillText(text, 0, 0);
|
|
541
924
|
}
|
|
542
925
|
ctx.restore();
|
|
@@ -579,16 +962,53 @@ var SpecInstance = class {
|
|
|
579
962
|
this.renderFrame(this.lastT, this.seed);
|
|
580
963
|
}
|
|
581
964
|
}
|
|
965
|
+
drawLinks(built, t) {
|
|
966
|
+
const { links } = built.layer;
|
|
967
|
+
if (!links) return;
|
|
968
|
+
const { ctx } = this;
|
|
969
|
+
const wrap2 = built.layer.wrap !== false;
|
|
970
|
+
const positions = built.entities.map((e) => positionAt(e, t, this.w, this.h));
|
|
971
|
+
const pairs = linkPairs(positions, links.k, links.maxDist * (this.effSpec.units === "viewport" ? Math.min(this.w, this.h) : 1), wrap2, this.w, this.h);
|
|
972
|
+
const lw = (links.width ?? 1) * (this.effSpec.units === "viewport" ? Math.min(this.w, this.h) : 1);
|
|
973
|
+
ctx.lineWidth = lw;
|
|
974
|
+
for (const [i, j] of pairs) {
|
|
975
|
+
const pi = positions[i];
|
|
976
|
+
const pj = positions[j];
|
|
977
|
+
const ei = built.entities[i];
|
|
978
|
+
let resolvedColor = links.color;
|
|
979
|
+
if (!resolvedColor) {
|
|
980
|
+
const sprite = built.layer.sprite;
|
|
981
|
+
if (sprite.kind === "circle") resolvedColor = sprite.colors?.[ei.colorIndex] ?? sprite.color;
|
|
982
|
+
else resolvedColor = "#e6e8ef";
|
|
983
|
+
}
|
|
984
|
+
ctx.globalAlpha = links.alpha ?? alphaAt(ei, t);
|
|
985
|
+
ctx.strokeStyle = resolvedColor;
|
|
986
|
+
let dx = pj.x - pi.x;
|
|
987
|
+
let dy = pj.y - pi.y;
|
|
988
|
+
if (wrap2) {
|
|
989
|
+
if (Math.abs(dx) > this.w / 2) dx = dx > 0 ? dx - this.w : dx + this.w;
|
|
990
|
+
if (Math.abs(dy) > this.h / 2) dy = dy > 0 ? dy - this.h : dy + this.h;
|
|
991
|
+
}
|
|
992
|
+
ctx.beginPath();
|
|
993
|
+
ctx.moveTo(pi.x, pi.y);
|
|
994
|
+
ctx.lineTo(pi.x + dx, pi.y + dy);
|
|
995
|
+
ctx.stroke();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
582
998
|
/** Deterministic, frame-addressable render (shared by the rAF loop). */
|
|
583
999
|
renderFrame(t, _seed) {
|
|
584
1000
|
this.stepTransition(t);
|
|
585
1001
|
const { ctx } = this;
|
|
586
1002
|
ctx.globalAlpha = 1;
|
|
587
1003
|
ctx.globalCompositeOperation = "source-over";
|
|
588
|
-
this.drawBackground();
|
|
1004
|
+
this.drawBackground(t);
|
|
589
1005
|
for (const built of this.layers) {
|
|
590
1006
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
591
|
-
|
|
1007
|
+
this.drawLinks(built, t);
|
|
1008
|
+
for (const e of built.entities) {
|
|
1009
|
+
this.drawTrail(built, e, t);
|
|
1010
|
+
this.drawEntity(built, e, t);
|
|
1011
|
+
}
|
|
592
1012
|
}
|
|
593
1013
|
ctx.globalAlpha = 1;
|
|
594
1014
|
ctx.globalCompositeOperation = "source-over";
|
|
@@ -623,6 +1043,222 @@ function compileSaver(spec) {
|
|
|
623
1043
|
};
|
|
624
1044
|
}
|
|
625
1045
|
|
|
1046
|
+
// src/advise.ts
|
|
1047
|
+
import { createRng as createRng2 } from "@idle-screens/core";
|
|
1048
|
+
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
1049
|
+
const warnings = [];
|
|
1050
|
+
const w = viewport.width;
|
|
1051
|
+
const h = viewport.height;
|
|
1052
|
+
const scale = spec.units === "viewport" ? Math.min(w, h) : 1;
|
|
1053
|
+
const countScale = scale > 1 ? Math.min(w, h) / LIMITS.referenceViewport : 1;
|
|
1054
|
+
const rng = createRng2(spec.seed ?? 42);
|
|
1055
|
+
const allEntities = spec.layers.map((l) => buildEntities(l, rng, w, h, scale, countScale));
|
|
1056
|
+
let totalEntities = 0;
|
|
1057
|
+
let textLayerCount = 0;
|
|
1058
|
+
let motionLayerCount = 0;
|
|
1059
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
1060
|
+
const layer = spec.layers[li];
|
|
1061
|
+
const entities = allEntities[li];
|
|
1062
|
+
totalEntities += entities.length;
|
|
1063
|
+
const isText = layer.sprite.kind === "text";
|
|
1064
|
+
const isStaticText = isText && layer.motion.type === "static";
|
|
1065
|
+
if (isStaticText) textLayerCount++;
|
|
1066
|
+
if (layer.motion.type !== "static") motionLayerCount++;
|
|
1067
|
+
if (layer.trail && layer.motion.type === "static") {
|
|
1068
|
+
warnings.push({
|
|
1069
|
+
path: `layers[${li}].trail`,
|
|
1070
|
+
code: "trail-on-static",
|
|
1071
|
+
message: "trail has no effect on static entities \u2014 they have no past positions to draw"
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if (layer.sprite.kind === "circle") {
|
|
1075
|
+
const maxR = entities.reduce((m, e) => Math.max(m, e.size / 2), 0);
|
|
1076
|
+
const maxAlpha = entities.reduce((m, e) => Math.max(m, e.alpha), 0);
|
|
1077
|
+
if (maxR * maxAlpha < 0.05) {
|
|
1078
|
+
warnings.push({
|
|
1079
|
+
path: `layers[${li}]`,
|
|
1080
|
+
code: "invisible-layer",
|
|
1081
|
+
message: `layer's max visible radius (${maxR.toFixed(1)}px \xD7 ${maxAlpha.toFixed(2)} alpha) is below perceptual floor`
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
let totalCoverage = 0;
|
|
1087
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
1088
|
+
const layer = spec.layers[li];
|
|
1089
|
+
const entities = allEntities[li];
|
|
1090
|
+
for (const e of entities) {
|
|
1091
|
+
const r = e.size / 2;
|
|
1092
|
+
const pixArea = layer.sprite.kind === "circle" ? Math.PI * r * r : e.size * e.size;
|
|
1093
|
+
totalCoverage += pixArea * e.alpha / (w * h);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (totalCoverage < 5e-4 && spec.layers.length > 0) {
|
|
1097
|
+
warnings.push({
|
|
1098
|
+
path: "layers",
|
|
1099
|
+
code: "sparse-scene",
|
|
1100
|
+
message: `alpha-weighted coverage is ${(totalCoverage * 100).toFixed(4)}% \u2014 scene will look empty on most displays`
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
if (totalEntities > 500) {
|
|
1104
|
+
warnings.push({
|
|
1105
|
+
path: "layers",
|
|
1106
|
+
code: "dense-scene",
|
|
1107
|
+
message: `${totalEntities} entities \u2014 scene may feel crowded and hurt performance on low-end devices`
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
if (spec.layers.length > 0 && textLayerCount > 0 && motionLayerCount === 0) {
|
|
1111
|
+
const textRatio = textLayerCount / spec.layers.length;
|
|
1112
|
+
if (textRatio >= 0.8) {
|
|
1113
|
+
warnings.push({
|
|
1114
|
+
path: "layers",
|
|
1115
|
+
code: "text-heavy",
|
|
1116
|
+
message: "most layers are static text \u2014 reads as a document, not a screensaver"
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
1121
|
+
const layer = spec.layers[li];
|
|
1122
|
+
if (!layer.links) continue;
|
|
1123
|
+
const entities = allEntities[li];
|
|
1124
|
+
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
1125
|
+
const maxDist = layer.links.maxDist * scale;
|
|
1126
|
+
const pairs = linkPairs(positions, layer.links.k, maxDist, layer.wrap !== false, w, h);
|
|
1127
|
+
const maxPossible = Math.min(entities.length * layer.links.k, entities.length * (entities.length - 1) / 2);
|
|
1128
|
+
if (maxPossible > 0 && pairs.length / maxPossible < 0.1) {
|
|
1129
|
+
warnings.push({
|
|
1130
|
+
path: `layers[${li}].links`,
|
|
1131
|
+
code: "link-starvation",
|
|
1132
|
+
message: `only ${pairs.length}/${maxPossible} possible edges formed \u2014 raise count or widen links.maxDist`
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
1137
|
+
const layer = spec.layers[li];
|
|
1138
|
+
if (layer.motion.type === "static") continue;
|
|
1139
|
+
const entities = allEntities[li];
|
|
1140
|
+
if (entities.length < 3) continue;
|
|
1141
|
+
const speeds = entities.map((e) => Math.sqrt(e.vx * e.vx + e.vy * e.vy));
|
|
1142
|
+
const mean = speeds.reduce((a, b) => a + b, 0) / speeds.length;
|
|
1143
|
+
if (mean === 0) continue;
|
|
1144
|
+
const maxDev = Math.max(...speeds.map((s) => Math.abs(s - mean)));
|
|
1145
|
+
if (maxDev / mean < 0.05) {
|
|
1146
|
+
warnings.push({
|
|
1147
|
+
path: `layers[${li}].motion`,
|
|
1148
|
+
code: "uniform-motion",
|
|
1149
|
+
message: "all entities move at nearly identical speed \u2014 widen the speed range for visual variety"
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (totalEntities >= 8) {
|
|
1154
|
+
const positions = allEntities.flatMap((ents) => ents.map((e) => positionAt(e, 0, w, h)));
|
|
1155
|
+
const cx = positions.reduce((s, p) => s + p.x, 0) / positions.length;
|
|
1156
|
+
const cy = positions.reduce((s, p) => s + p.y, 0) / positions.length;
|
|
1157
|
+
const offX = Math.abs(cx / w - 0.5);
|
|
1158
|
+
const offY = Math.abs(cy / h - 0.5);
|
|
1159
|
+
if (offX > 0.2 || offY > 0.2) {
|
|
1160
|
+
warnings.push({
|
|
1161
|
+
path: "layers",
|
|
1162
|
+
code: "off-center",
|
|
1163
|
+
message: `centroid at (${(cx / w).toFixed(2)}, ${(cy / h).toFixed(2)}) \u2014 composition is heavily offset from center`
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return warnings;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/describe.ts
|
|
1171
|
+
import { createRng as createRng3 } from "@idle-screens/core";
|
|
1172
|
+
function describeScene(spec, options = {}) {
|
|
1173
|
+
const { width: w, height: h } = options.viewport ?? { width: 1920, height: 1080 };
|
|
1174
|
+
const seed = options.seed ?? spec.seed ?? 42;
|
|
1175
|
+
const times = options.times ?? [0, 5e3, 15e3];
|
|
1176
|
+
const scale = spec.units === "viewport" ? Math.min(w, h) : 1;
|
|
1177
|
+
const countScale = scale > 1 ? Math.min(w, h) / LIMITS.referenceViewport : 1;
|
|
1178
|
+
const rng = createRng3(seed);
|
|
1179
|
+
const builtLayers = spec.layers.map((layer) => ({
|
|
1180
|
+
layer,
|
|
1181
|
+
entities: buildEntities(layer, rng, w, h, scale, countScale)
|
|
1182
|
+
}));
|
|
1183
|
+
const snapshots = times.map((t) => {
|
|
1184
|
+
const layerSnapshots = builtLayers.map(({ layer, entities }) => {
|
|
1185
|
+
const positions = entities.map((e) => positionAt(e, t, w, h));
|
|
1186
|
+
const cx = positions.length > 0 ? positions.reduce((s, p) => s + p.x, 0) / positions.length : 0;
|
|
1187
|
+
const cy = positions.length > 0 ? positions.reduce((s, p) => s + p.y, 0) / positions.length : 0;
|
|
1188
|
+
let pixArea = 0;
|
|
1189
|
+
for (const e of entities) {
|
|
1190
|
+
const r = e.size / 2;
|
|
1191
|
+
pixArea += layer.sprite.kind === "circle" ? Math.PI * r * r : e.size * e.size;
|
|
1192
|
+
}
|
|
1193
|
+
const coverage = pixArea / (w * h);
|
|
1194
|
+
const meanAlpha = entities.length > 0 ? entities.reduce((s, e) => s + e.alpha, 0) / entities.length : 0;
|
|
1195
|
+
let linksDrawn = null;
|
|
1196
|
+
let linksExpected = null;
|
|
1197
|
+
let connectedComponents = null;
|
|
1198
|
+
let isolatedNodes = null;
|
|
1199
|
+
if (layer.links) {
|
|
1200
|
+
const maxDist = layer.links.maxDist * scale;
|
|
1201
|
+
const pairs = linkPairs(positions, layer.links.k, maxDist, layer.wrap !== false, w, h);
|
|
1202
|
+
linksDrawn = pairs.length;
|
|
1203
|
+
linksExpected = Math.min(entities.length * layer.links.k, entities.length * (entities.length - 1) / 2);
|
|
1204
|
+
const parent = Array.from({ length: entities.length }, (_, i) => i);
|
|
1205
|
+
const find = (x) => {
|
|
1206
|
+
while (parent[x] !== x) {
|
|
1207
|
+
parent[x] = parent[parent[x]];
|
|
1208
|
+
x = parent[x];
|
|
1209
|
+
}
|
|
1210
|
+
return x;
|
|
1211
|
+
};
|
|
1212
|
+
for (const [a, b] of pairs) {
|
|
1213
|
+
const ra = find(a);
|
|
1214
|
+
const rb = find(b);
|
|
1215
|
+
if (ra !== rb) parent[ra] = rb;
|
|
1216
|
+
}
|
|
1217
|
+
const roots = new Set(entities.map((_, i) => find(i)));
|
|
1218
|
+
connectedComponents = roots.size;
|
|
1219
|
+
const degree = new Uint16Array(entities.length);
|
|
1220
|
+
for (const [a, b] of pairs) {
|
|
1221
|
+
degree[a]++;
|
|
1222
|
+
degree[b]++;
|
|
1223
|
+
}
|
|
1224
|
+
isolatedNodes = degree.filter((d) => d === 0).length;
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
key: layer.key,
|
|
1228
|
+
count: entities.length,
|
|
1229
|
+
centroid: { x: +(cx / w).toFixed(3), y: +(cy / h).toFixed(3) },
|
|
1230
|
+
coverage: +coverage.toFixed(4),
|
|
1231
|
+
meanAlpha: +meanAlpha.toFixed(3),
|
|
1232
|
+
linksDrawn,
|
|
1233
|
+
linksExpected,
|
|
1234
|
+
connectedComponents,
|
|
1235
|
+
isolatedNodes
|
|
1236
|
+
};
|
|
1237
|
+
});
|
|
1238
|
+
const warnings = [];
|
|
1239
|
+
for (const ls of layerSnapshots) {
|
|
1240
|
+
const label = ls.key ?? `layer`;
|
|
1241
|
+
if (ls.coverage * ls.meanAlpha < 5e-4) {
|
|
1242
|
+
warnings.push(`${label}: effectively invisible (coverage\xD7alpha=${(ls.coverage * ls.meanAlpha).toFixed(5)})`);
|
|
1243
|
+
}
|
|
1244
|
+
if (ls.linksDrawn !== null && ls.linksExpected !== null && ls.linksExpected > 0) {
|
|
1245
|
+
const ratio = ls.linksDrawn / ls.linksExpected;
|
|
1246
|
+
if (ratio < 0.1) {
|
|
1247
|
+
warnings.push(`${label}: link starvation \u2014 ${ls.linksDrawn}/${ls.linksExpected} edges formed`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (ls.isolatedNodes !== null && ls.count > 0 && ls.isolatedNodes / ls.count > 0.5) {
|
|
1251
|
+
warnings.push(`${label}: ${ls.isolatedNodes}/${ls.count} nodes isolated \u2014 graph is disconnected`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return { t, viewport: { w, h }, layers: layerSnapshots, warnings };
|
|
1255
|
+
});
|
|
1256
|
+
return {
|
|
1257
|
+
spec: { id: spec.id, label: spec.label, units: spec.units ?? "px" },
|
|
1258
|
+
snapshots
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
|
|
626
1262
|
// src/examples/aquarium.ts
|
|
627
1263
|
var AQUARIUM_SPEC = {
|
|
628
1264
|
schemaVersion: 1,
|
|
@@ -655,6 +1291,98 @@ var AQUARIUM_SPEC = {
|
|
|
655
1291
|
]
|
|
656
1292
|
};
|
|
657
1293
|
|
|
1294
|
+
// src/examples/comets.ts
|
|
1295
|
+
var COMETS_SPEC = {
|
|
1296
|
+
schemaVersion: 1,
|
|
1297
|
+
id: "comets",
|
|
1298
|
+
label: "Comet Shower",
|
|
1299
|
+
seed: 42,
|
|
1300
|
+
motionIntensity: "moderate",
|
|
1301
|
+
background: {
|
|
1302
|
+
type: "gradient",
|
|
1303
|
+
stops: [
|
|
1304
|
+
{ at: 0, color: "#0a0015" },
|
|
1305
|
+
{ at: 0.5, color: "#0d1b2a" },
|
|
1306
|
+
{ at: 1, color: "#1b0a2e" }
|
|
1307
|
+
],
|
|
1308
|
+
drift: { period: 25e3, amount: 0.12 }
|
|
1309
|
+
},
|
|
1310
|
+
layers: [
|
|
1311
|
+
{
|
|
1312
|
+
key: "stars",
|
|
1313
|
+
count: 120,
|
|
1314
|
+
sprite: { kind: "circle", radius: [0.5, 2], color: "#ffffff", soft: true },
|
|
1315
|
+
motion: { type: "static" },
|
|
1316
|
+
alpha: [0.3, 0.7],
|
|
1317
|
+
pulse: { amp: 0.15, period: 3e3 }
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
key: "comets",
|
|
1321
|
+
count: 8,
|
|
1322
|
+
sprite: { kind: "circle", radius: [3, 8], color: "#88ccff", soft: true },
|
|
1323
|
+
motion: { type: "drift", speed: [80, 200], angle: 225 },
|
|
1324
|
+
alpha: [0.7, 1],
|
|
1325
|
+
blend: "lighter",
|
|
1326
|
+
trail: { length: 1500, fade: 1 }
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
key: "fireflies",
|
|
1330
|
+
count: 20,
|
|
1331
|
+
sprite: { kind: "circle", radius: [2, 5], color: "#ffdd44", soft: true },
|
|
1332
|
+
motion: { type: "drift", speed: [10, 30], bidirectional: true, bob: 20 },
|
|
1333
|
+
alpha: [0.4, 0.8],
|
|
1334
|
+
pulse: { amp: 0.3, period: 2e3 },
|
|
1335
|
+
blend: "lighter",
|
|
1336
|
+
trail: { length: 800 }
|
|
1337
|
+
}
|
|
1338
|
+
]
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
// src/examples/constellation.ts
|
|
1342
|
+
var CONSTELLATION_SPEC = {
|
|
1343
|
+
schemaVersion: 1,
|
|
1344
|
+
id: "constellation",
|
|
1345
|
+
label: "Constellation",
|
|
1346
|
+
seed: 73,
|
|
1347
|
+
motionIntensity: "calm",
|
|
1348
|
+
background: {
|
|
1349
|
+
type: "gradient",
|
|
1350
|
+
stops: [
|
|
1351
|
+
{ at: 0, color: "#020212" },
|
|
1352
|
+
{ at: 0.5, color: "#050520" },
|
|
1353
|
+
{ at: 1, color: "#0a0830" }
|
|
1354
|
+
]
|
|
1355
|
+
},
|
|
1356
|
+
layers: [
|
|
1357
|
+
// Distant static stars (no links, just atmosphere)
|
|
1358
|
+
{
|
|
1359
|
+
count: 100,
|
|
1360
|
+
sprite: { kind: "circle", radius: [1, 3], color: "#667799", colors: ["#8899cc", "#99aadd", "#aabbee", "#7788aa"] },
|
|
1361
|
+
alpha: [0.2, 0.7],
|
|
1362
|
+
pulse: { amp: 0.2, period: 3e3 },
|
|
1363
|
+
motion: { type: "static" }
|
|
1364
|
+
},
|
|
1365
|
+
// Connected constellation nodes — slow drift, linked
|
|
1366
|
+
{
|
|
1367
|
+
key: "nodes",
|
|
1368
|
+
count: 40,
|
|
1369
|
+
sprite: { kind: "circle", radius: [2, 5], color: "#88aaff", colors: ["#88aaff", "#aaccff", "#ffcc88", "#ff8866"], soft: true },
|
|
1370
|
+
alpha: [0.5, 0.9],
|
|
1371
|
+
blend: "lighter",
|
|
1372
|
+
motion: { type: "drift", speed: [2, 8], bidirectional: true, bob: 3 },
|
|
1373
|
+
links: { k: 3, maxDist: 200, alpha: 0.15, width: 0.5 }
|
|
1374
|
+
},
|
|
1375
|
+
// Cycling asterism markers
|
|
1376
|
+
{
|
|
1377
|
+
count: 6,
|
|
1378
|
+
sprite: { kind: "emoji", glyphs: ["\u2726", "\u2727", "\u22C6", "\u2217"], cycle: { period: 2e3 } },
|
|
1379
|
+
size: [14, 22],
|
|
1380
|
+
alpha: [0.4, 0.7],
|
|
1381
|
+
motion: { type: "drift", speed: [1, 4], bidirectional: true }
|
|
1382
|
+
}
|
|
1383
|
+
]
|
|
1384
|
+
};
|
|
1385
|
+
|
|
658
1386
|
// src/examples/dev-dashboard.ts
|
|
659
1387
|
var DASHBOARD_SPEC = {
|
|
660
1388
|
schemaVersion: 1,
|
|
@@ -1219,6 +1947,90 @@ var LANTERNS_SPEC = {
|
|
|
1219
1947
|
]
|
|
1220
1948
|
};
|
|
1221
1949
|
|
|
1950
|
+
// src/examples/orrery.ts
|
|
1951
|
+
var ORRERY_SPEC = {
|
|
1952
|
+
schemaVersion: 1,
|
|
1953
|
+
id: "orrery",
|
|
1954
|
+
label: "Orrery",
|
|
1955
|
+
seed: 7,
|
|
1956
|
+
motionIntensity: "calm",
|
|
1957
|
+
background: {
|
|
1958
|
+
type: "gradient",
|
|
1959
|
+
stops: [
|
|
1960
|
+
{ at: 0, color: "#020210" },
|
|
1961
|
+
{ at: 0.5, color: "#06051a" },
|
|
1962
|
+
{ at: 1, color: "#0a0820" }
|
|
1963
|
+
]
|
|
1964
|
+
},
|
|
1965
|
+
layers: [
|
|
1966
|
+
// Distant stars
|
|
1967
|
+
{
|
|
1968
|
+
count: 80,
|
|
1969
|
+
sprite: { kind: "circle", radius: [0.3, 1.2], color: "#8899cc" },
|
|
1970
|
+
alpha: [0.2, 0.8],
|
|
1971
|
+
pulse: { amp: 0.3, period: 3e3 },
|
|
1972
|
+
motion: { type: "static" }
|
|
1973
|
+
},
|
|
1974
|
+
// Sun corona (soft glow)
|
|
1975
|
+
{
|
|
1976
|
+
key: "corona",
|
|
1977
|
+
count: 1,
|
|
1978
|
+
sprite: { kind: "circle", radius: [60, 60], color: "#ffaa22", soft: true },
|
|
1979
|
+
blend: "lighter",
|
|
1980
|
+
alpha: [0.6, 0.6],
|
|
1981
|
+
grow: { amp: 0.15, period: 4e3 },
|
|
1982
|
+
motion: { type: "static" },
|
|
1983
|
+
position: { x: 0.5, y: 0.5 }
|
|
1984
|
+
},
|
|
1985
|
+
// Sun body
|
|
1986
|
+
{
|
|
1987
|
+
key: "sun",
|
|
1988
|
+
count: 1,
|
|
1989
|
+
sprite: { kind: "emoji", glyphs: ["\u2600\uFE0F"] },
|
|
1990
|
+
size: [48, 48],
|
|
1991
|
+
grow: { amp: 0.08, period: 4e3 },
|
|
1992
|
+
motion: { type: "static" },
|
|
1993
|
+
position: { x: 0.5, y: 0.5 }
|
|
1994
|
+
},
|
|
1995
|
+
// Inner ring — small fast planets
|
|
1996
|
+
{
|
|
1997
|
+
key: "inner",
|
|
1998
|
+
count: 3,
|
|
1999
|
+
sprite: { kind: "emoji", glyphs: ["\u{1FAA8}", "\u{1F534}", "\u{1F7E0}"] },
|
|
2000
|
+
size: [16, 22],
|
|
2001
|
+
spin: 30,
|
|
2002
|
+
motion: { type: "orbit", speed: [28, 42], radius: [80, 120] }
|
|
2003
|
+
},
|
|
2004
|
+
// Middle ring — gas giants
|
|
2005
|
+
{
|
|
2006
|
+
key: "middle",
|
|
2007
|
+
count: 2,
|
|
2008
|
+
sprite: { kind: "emoji", glyphs: ["\u{1F7E4}", "\u{1FA90}"] },
|
|
2009
|
+
size: [28, 38],
|
|
2010
|
+
spin: -15,
|
|
2011
|
+
motion: { type: "orbit", speed: [12, 20], radius: [160, 200] }
|
|
2012
|
+
},
|
|
2013
|
+
// Outer ring — ice giants
|
|
2014
|
+
{
|
|
2015
|
+
key: "outer",
|
|
2016
|
+
count: 2,
|
|
2017
|
+
sprite: { kind: "emoji", glyphs: ["\u{1F535}", "\u{1F48E}"] },
|
|
2018
|
+
size: [22, 30],
|
|
2019
|
+
spin: 10,
|
|
2020
|
+
motion: { type: "orbit", speed: [5, 10], radius: [240, 300] }
|
|
2021
|
+
},
|
|
2022
|
+
// Orbital dust ring (soft glow particles orbiting slowly)
|
|
2023
|
+
{
|
|
2024
|
+
count: 24,
|
|
2025
|
+
sprite: { kind: "circle", radius: [1, 3], color: "#ccaa66", soft: true },
|
|
2026
|
+
alpha: [0.15, 0.4],
|
|
2027
|
+
blend: "lighter",
|
|
2028
|
+
pulse: { amp: 0.2, period: 2500 },
|
|
2029
|
+
motion: { type: "orbit", speed: [2, 8], radius: [100, 320] }
|
|
2030
|
+
}
|
|
2031
|
+
]
|
|
2032
|
+
};
|
|
2033
|
+
|
|
1222
2034
|
// src/examples/rain.ts
|
|
1223
2035
|
var RAIN_SPEC = {
|
|
1224
2036
|
schemaVersion: 1,
|
|
@@ -1338,7 +2150,10 @@ var SCHEMA_EXAMPLES = [
|
|
|
1338
2150
|
{ id: "snowfall", label: "Snowfall", spec: SNOWFALL_SPEC },
|
|
1339
2151
|
{ id: "lanterns", label: "Night Lanterns", spec: LANTERNS_SPEC },
|
|
1340
2152
|
{ id: "sakura", label: "Sakura Drift", spec: SAKURA_SPEC },
|
|
1341
|
-
{ id: "dev-dashboard", label: "Dev Dashboard", spec: DASHBOARD_SPEC }
|
|
2153
|
+
{ id: "dev-dashboard", label: "Dev Dashboard", spec: DASHBOARD_SPEC },
|
|
2154
|
+
{ id: "orrery", label: "Orrery", spec: ORRERY_SPEC },
|
|
2155
|
+
{ id: "constellation", label: "Constellation", spec: CONSTELLATION_SPEC },
|
|
2156
|
+
{ id: "comets", label: "Comet Shower", spec: COMETS_SPEC }
|
|
1342
2157
|
];
|
|
1343
2158
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
1344
2159
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -1346,27 +2161,37 @@ var EXAMPLE_BY_ID = Object.fromEntries(
|
|
|
1346
2161
|
);
|
|
1347
2162
|
export {
|
|
1348
2163
|
AQUARIUM_SPEC,
|
|
2164
|
+
COMETS_SPEC,
|
|
2165
|
+
CONSTELLATION_SPEC,
|
|
1349
2166
|
DASHBOARD_SPEC,
|
|
1350
2167
|
EXAMPLE_BY_ID,
|
|
1351
2168
|
EXAMPLE_SPECS,
|
|
1352
2169
|
LANTERNS_SPEC,
|
|
1353
2170
|
LIMITS,
|
|
2171
|
+
ORRERY_SPEC,
|
|
1354
2172
|
RAIN_SPEC,
|
|
1355
2173
|
SAKURA_SPEC,
|
|
1356
2174
|
SCHEMA_EXAMPLES,
|
|
1357
2175
|
SCHEMA_VERSION,
|
|
1358
2176
|
SNOWFALL_SPEC,
|
|
2177
|
+
adviseSpec,
|
|
1359
2178
|
alphaAt,
|
|
1360
2179
|
applyDeltasToSpec,
|
|
1361
2180
|
assertValidSpec,
|
|
1362
2181
|
buildEntities,
|
|
1363
2182
|
compileSaver,
|
|
2183
|
+
describeScene,
|
|
1364
2184
|
easeSmooth,
|
|
1365
2185
|
lerpSpec,
|
|
2186
|
+
linkPairs,
|
|
1366
2187
|
manifestFor,
|
|
1367
2188
|
positionAt,
|
|
1368
2189
|
resolveSpecPath,
|
|
2190
|
+
rotationAt,
|
|
2191
|
+
sizeAt,
|
|
2192
|
+
spriteIndexAt,
|
|
1369
2193
|
spriteVariants,
|
|
2194
|
+
steerablePaths,
|
|
1370
2195
|
structuralSignature,
|
|
1371
2196
|
validateSpec
|
|
1372
2197
|
};
|