@idle-screens/schema 0.1.0 → 2.0.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.js CHANGED
@@ -3,9 +3,13 @@ var SCHEMA_VERSION = 1;
3
3
  var LIMITS = {
4
4
  maxPerLayer: 400,
5
5
  maxTotal: 800,
6
- maxLayers: 8,
7
- maxSpeed: 4e3
6
+ maxLayers: 36,
7
+ maxSpeed: 4e3,
8
8
  // px/sec — bounds motion so nothing teleports
9
+ maxPulseAmp: 0.5,
10
+ // opacity breathing amplitude cap
11
+ minPulsePeriod: 500
12
+ // ms — caps pulse at 2 Hz (WCAG flash threshold is 3 Hz)
9
13
  };
10
14
 
11
15
  // src/validate.ts
@@ -78,6 +82,44 @@ function validateLayer(layer, path, err) {
78
82
  }
79
83
  if (layer.wrap !== void 0 && typeof layer.wrap !== "boolean") err(`${path}.wrap`, "must be a boolean");
80
84
  if (layer.flip !== void 0 && typeof layer.flip !== "boolean") err(`${path}.flip`, "must be a boolean");
85
+ if (layer.alpha !== void 0 && (!isRange(layer.alpha) || layer.alpha[0] < 0 || layer.alpha[1] > 1)) {
86
+ err(`${path}.alpha`, "must be a [min,max] range within 0..1");
87
+ }
88
+ if (layer.blend !== void 0 && layer.blend !== "lighter") err(`${path}.blend`, "must be 'lighter' when set");
89
+ if (layer.key !== void 0 && (!isStr(layer.key) || layer.key.trim() === "")) {
90
+ err(`${path}.key`, "must be a non-empty string");
91
+ }
92
+ if (layer.position !== void 0) {
93
+ if (!isObj(layer.position) || !isNum(layer.position.x) || !isNum(layer.position.y)) {
94
+ err(`${path}.position`, "must be {x, y} with numbers 0..1");
95
+ } else {
96
+ if (layer.position.x < 0 || layer.position.x > 1) err(`${path}.position.x`, "must be 0..1");
97
+ if (layer.position.y < 0 || layer.position.y > 1) err(`${path}.position.y`, "must be 0..1");
98
+ if (isNum(layer.count) && layer.count !== 1) err(`${path}.position`, "position requires count: 1");
99
+ }
100
+ }
101
+ if (layer.region !== void 0) {
102
+ if (!isObj(layer.region)) err(`${path}.region`, "must be an object");
103
+ else {
104
+ for (const axis of ["x", "y"]) {
105
+ const r = layer.region[axis];
106
+ if (r !== void 0 && (!isRange(r) || r[0] < 0 || r[1] > 1)) {
107
+ err(`${path}.region.${axis}`, "must be a [min,max] range within 0..1");
108
+ }
109
+ }
110
+ }
111
+ }
112
+ if (layer.pulse !== void 0) {
113
+ if (!isObj(layer.pulse)) err(`${path}.pulse`, "must be an object");
114
+ else {
115
+ if (!isNum(layer.pulse.amp) || layer.pulse.amp <= 0 || layer.pulse.amp > LIMITS.maxPulseAmp) {
116
+ err(`${path}.pulse.amp`, `must be within 0..${LIMITS.maxPulseAmp}`);
117
+ }
118
+ if (!isNum(layer.pulse.period) || layer.pulse.period < LIMITS.minPulsePeriod) {
119
+ err(`${path}.pulse.period`, `must be >= ${LIMITS.minPulsePeriod} ms (flash-safety floor)`);
120
+ }
121
+ }
122
+ }
81
123
  validateSprite(layer.sprite, `${path}.sprite`, err);
82
124
  validateMotion(layer.motion, `${path}.motion`, err);
83
125
  }
@@ -92,9 +134,19 @@ function validateSprite(sprite, path, err) {
92
134
  err(`${path}.strings`, "must be a non-empty array of strings");
93
135
  }
94
136
  if (sprite.color !== void 0) color(sprite.color, `${path}.color`, err);
137
+ if (sprite.align !== void 0 && !["left", "center", "right"].includes(sprite.align)) {
138
+ err(`${path}.align`, "must be left | center | right");
139
+ }
140
+ if (sprite.baseline !== void 0 && !["top", "middle", "bottom"].includes(sprite.baseline)) {
141
+ err(`${path}.baseline`, "must be top | middle | bottom");
142
+ }
143
+ if (sprite.maxWidth !== void 0 && (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0)) {
144
+ err(`${path}.maxWidth`, "must be a positive number");
145
+ }
95
146
  } else if (sprite.kind === "circle") {
96
147
  if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
97
148
  color(sprite.color, `${path}.color`, err);
149
+ if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
98
150
  } else {
99
151
  err(`${path}.kind`, "must be emoji | text | circle");
100
152
  }
@@ -115,8 +167,9 @@ function validateMotion(motion, path, err) {
115
167
  if (motion.sway !== void 0 && !isNum(motion.sway)) err(`${path}.sway`, "must be a number");
116
168
  } else if (motion.type === "bounce") {
117
169
  speedOk(motion.speed, `${path}.speed`);
170
+ } else if (motion.type === "static") {
118
171
  } else {
119
- err(`${path}.type`, "must be drift | rise | bounce");
172
+ err(`${path}.type`, "must be drift | rise | bounce | static");
120
173
  }
121
174
  }
122
175
  function assertValidSpec(spec) {
@@ -145,7 +198,9 @@ function buildEntities(layer, rng, w, h) {
145
198
  let bob = 0;
146
199
  let motion = "drift";
147
200
  const m = layer.motion;
148
- if (m.type === "drift") {
201
+ if (m.type === "static") {
202
+ motion = "static";
203
+ } else if (m.type === "drift") {
149
204
  const s = rng.range(m.speed[0], m.speed[1]);
150
205
  let angle = (m.angle ?? 0) * Math.PI / 180;
151
206
  if (m.bidirectional && rng.next() < 0.5) angle = Math.PI - angle;
@@ -163,9 +218,20 @@ function buildEntities(layer, rng, w, h) {
163
218
  vx = s * Math.cos(a);
164
219
  vy = s * Math.sin(a);
165
220
  }
221
+ let x0;
222
+ let y0;
223
+ if (layer.position && layer.count === 1) {
224
+ x0 = layer.position.x * w;
225
+ y0 = layer.position.y * h;
226
+ } else {
227
+ const [rx0, rx1] = layer.region?.x ?? [0, 1];
228
+ const [ry0, ry1] = layer.region?.y ?? [0, 1];
229
+ x0 = rng.range(rx0 * w, rx1 * w);
230
+ y0 = rng.range(ry0 * h, ry1 * h);
231
+ }
166
232
  out.push({
167
- x0: rng.range(0, w),
168
- y0: rng.range(0, h),
233
+ x0,
234
+ y0,
169
235
  size,
170
236
  spriteIndex: variants > 1 ? rng.int(0, variants - 1) : 0,
171
237
  phase: rng.range(0, Math.PI * 2),
@@ -173,11 +239,20 @@ function buildEntities(layer, rng, w, h) {
173
239
  vy,
174
240
  bob,
175
241
  motion,
176
- headingLeft: vx < 0
242
+ headingLeft: vx < 0,
243
+ alpha: layer.alpha ? rng.range(layer.alpha[0], layer.alpha[1]) : 1,
244
+ pulseAmp: layer.pulse?.amp ?? 0,
245
+ pulsePeriod: layer.pulse?.period ?? 1e3,
246
+ pulsePhase: layer.pulse ? rng.range(0, Math.PI * 2) : 0
177
247
  });
178
248
  }
179
249
  return out;
180
250
  }
251
+ function alphaAt(e, t) {
252
+ if (!e.pulseAmp) return e.alpha;
253
+ const a = e.alpha + e.pulseAmp * Math.sin(t * 2 * Math.PI / e.pulsePeriod + e.pulsePhase);
254
+ return a < 0 ? 0 : a > 1 ? 1 : a;
255
+ }
181
256
  function wrap(v, min, max) {
182
257
  const range = max - min;
183
258
  return ((v - min) % range + range) % range + min;
@@ -190,6 +265,7 @@ function reflect(v, min, max) {
190
265
  return min + (p < range ? p : period - p);
191
266
  }
192
267
  function positionAt(e, t, w, h) {
268
+ if (e.motion === "static") return { x: e.x0, y: e.y0, flip: false };
193
269
  const dt = t / 1e3;
194
270
  const m = e.size;
195
271
  if (e.motion === "bounce") {
@@ -216,6 +292,104 @@ function positionAt(e, t, w, h) {
216
292
  import {
217
293
  createRng
218
294
  } from "@idle-screens/core";
295
+
296
+ // src/steer.ts
297
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
298
+ function resolveSpecPath(spec, path) {
299
+ if (!spec || typeof spec !== "object" || !path) return null;
300
+ const parts = path.split(".");
301
+ if (parts.some((p) => UNSAFE_KEYS.has(p))) return null;
302
+ const s = spec;
303
+ if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
304
+ const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
305
+ if (idx === -1) return null;
306
+ parts.splice(0, 1, "layers", String(idx));
307
+ }
308
+ let node = spec;
309
+ for (let i = 0; i < parts.length - 1; i++) {
310
+ if (node === null || typeof node !== "object") return null;
311
+ node = node[parts[i]];
312
+ }
313
+ if (node === null || typeof node !== "object") return null;
314
+ const last = parts[parts.length - 1];
315
+ const key = Array.isArray(node) ? Number(last) : last;
316
+ const exists = Array.isArray(node) ? Number.isInteger(key) && key >= 0 && key < node.length : last in node;
317
+ return exists ? { parent: node, key } : null;
318
+ }
319
+ function applyDeltasToSpec(spec, deltas) {
320
+ const copy = JSON.parse(JSON.stringify(spec));
321
+ for (const d of deltas) {
322
+ const loc = resolveSpecPath(copy, d.path);
323
+ if (loc) loc.parent[loc.key] = d.value;
324
+ }
325
+ return copy;
326
+ }
327
+ var HEX2 = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
328
+ function hexToRgb(hex) {
329
+ const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
330
+ const n = parseInt(h.slice(1), 16);
331
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
332
+ }
333
+ function lerpHex(a, b, k) {
334
+ const ca = hexToRgb(a);
335
+ const cb = hexToRgb(b);
336
+ const mix = ca.map((v, i) => Math.round(v + (cb[i] - v) * k));
337
+ return `#${mix.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
338
+ }
339
+ function lerpSpec(from, to, k) {
340
+ const kk = Math.max(0, Math.min(1, k));
341
+ const walk = (a, b, key) => {
342
+ if (typeof a === "number" && typeof b === "number") {
343
+ const v = a + (b - a) * kk;
344
+ return key === "count" ? Math.max(1, Math.round(v)) : v;
345
+ }
346
+ if (typeof a === "string" && typeof b === "string" && HEX2.test(a) && HEX2.test(b)) {
347
+ return lerpHex(a, b, kk);
348
+ }
349
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
350
+ return b.map((bv, i) => walk(a[i], bv, i));
351
+ }
352
+ if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
353
+ const out = {};
354
+ for (const kName of Object.keys(b)) {
355
+ out[kName] = walk(a[kName], b[kName], kName);
356
+ }
357
+ return out;
358
+ }
359
+ return kk > 0 ? b : a;
360
+ };
361
+ return walk(from, to);
362
+ }
363
+ function easeSmooth(k) {
364
+ const c = Math.max(0, Math.min(1, k));
365
+ return c * c * (3 - 2 * c);
366
+ }
367
+ function structuralSignature(spec) {
368
+ return JSON.stringify(
369
+ spec.layers.map((l) => [
370
+ l.count,
371
+ l.size,
372
+ l.region,
373
+ l.position,
374
+ l.motion,
375
+ l.wrap,
376
+ l.flip,
377
+ l.alpha,
378
+ l.pulse,
379
+ l.sprite.kind,
380
+ l.sprite.kind === "circle" ? l.sprite.radius : void 0,
381
+ l.sprite.kind === "emoji" ? l.sprite.glyphs.length : l.sprite.kind === "text" ? l.sprite.strings.length : void 0
382
+ ])
383
+ );
384
+ }
385
+
386
+ // src/compile.ts
387
+ var DEFAULT_STEER_DUR = 1e3;
388
+ function hexToRgba(hex, alpha) {
389
+ const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
390
+ const n = parseInt(h.slice(1), 16);
391
+ return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${alpha})`;
392
+ }
219
393
  function manifestFor(spec) {
220
394
  const total = spec.layers.reduce((n, l) => n + l.count, 0);
221
395
  const costTier = total < 30 ? "idle" : total < 150 ? "low" : total < 400 ? "medium" : "high";
@@ -229,17 +403,26 @@ function manifestFor(spec) {
229
403
  reducedMotionFallback: "static",
230
404
  // Flash-safe by construction: static background + bounded sprites, no strobe
231
405
  // primitive. Proven by sampling a compiled spec through @idle-screens/validator.
232
- a11y: { flashSafe: true }
406
+ a11y: { flashSafe: true },
407
+ workerReady: true
233
408
  };
234
409
  }
235
410
  var SpecInstance = class {
236
411
  constructor(spec, ctx) {
237
412
  this.spec = spec;
413
+ this.effSpec = spec;
414
+ this.saverCtx = ctx;
238
415
  this.seed = (spec.seed ?? ctx.seed) >>> 0 || 1;
239
- const canvas = document.createElement("canvas");
240
- canvas.style.cssText = "display:block;width:100%;height:100%";
241
- canvas.setAttribute("aria-hidden", "true");
242
- ctx.host.appendChild(canvas);
416
+ let canvas;
417
+ if (ctx.surface) {
418
+ canvas = ctx.surface;
419
+ } else {
420
+ const el = document.createElement("canvas");
421
+ el.style.cssText = "display:block;width:100%;height:100%";
422
+ el.setAttribute("aria-hidden", "true");
423
+ ctx.host.appendChild(el);
424
+ canvas = el;
425
+ }
243
426
  this.canvas = canvas;
244
427
  const c2d = canvas.getContext("2d", { alpha: false });
245
428
  if (!c2d) throw new Error("schema saver: no 2d context");
@@ -255,6 +438,7 @@ var SpecInstance = class {
255
438
  spec;
256
439
  canvas;
257
440
  ctx;
441
+ saverCtx;
258
442
  seed;
259
443
  w;
260
444
  h;
@@ -262,8 +446,16 @@ var SpecInstance = class {
262
446
  frameId = null;
263
447
  paused = false;
264
448
  startT = 0;
449
+ baseT = 0;
450
+ // elapsed logical time carried across pause/resume
451
+ lastT = 0;
452
+ /** The spec currently being rendered (base spec + any applied steering). */
453
+ effSpec;
454
+ /** Active glide between two resolved specs (live setParam/applyTrack). */
455
+ transition = null;
456
+ lastStructural = "";
265
457
  sizeCanvas() {
266
- const dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 2);
458
+ const dpr = Math.min(this.saverCtx.dpr, 2);
267
459
  this.canvas.width = Math.max(1, Math.round(this.w * dpr));
268
460
  this.canvas.height = Math.max(1, Math.round(this.h * dpr));
269
461
  this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
@@ -271,7 +463,8 @@ var SpecInstance = class {
271
463
  /** (Re)seed and place all entities — deterministic for the seed + size. */
272
464
  rebuild() {
273
465
  const rng = createRng(this.seed);
274
- this.layers = this.spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, this.w, this.h) }));
466
+ this.layers = this.effSpec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, this.w, this.h) }));
467
+ this.lastStructural = structuralSignature(this.effSpec);
275
468
  }
276
469
  start() {
277
470
  if (this.frameId !== null || typeof requestAnimationFrame === "undefined") return;
@@ -283,15 +476,17 @@ var SpecInstance = class {
283
476
  cancelAnimationFrame(this.frameId);
284
477
  this.frameId = null;
285
478
  }
479
+ this.baseT = this.lastT;
286
480
  }
287
481
  loop(now) {
288
482
  this.frameId = requestAnimationFrame((n) => this.loop(n));
289
483
  if (this.startT === 0) this.startT = now;
290
- this.renderFrame(now - this.startT, this.seed);
484
+ this.lastT = now - this.startT + this.baseT;
485
+ this.renderFrame(this.lastT, this.seed);
291
486
  }
292
487
  drawBackground() {
293
488
  const { ctx, w, h } = this;
294
- const bg = this.spec.background;
489
+ const bg = this.effSpec.background;
295
490
  if (!bg || bg.type === "solid") {
296
491
  ctx.fillStyle = bg?.color ?? "#05050a";
297
492
  ctx.fillRect(0, 0, w, h);
@@ -310,10 +505,20 @@ var SpecInstance = class {
310
505
  const { ctx } = this;
311
506
  const p = positionAt(e, t, this.w, this.h);
312
507
  const sprite = built.layer.sprite;
508
+ ctx.globalAlpha = alphaAt(e, t);
313
509
  if (sprite.kind === "circle") {
314
- ctx.fillStyle = sprite.color;
510
+ const r = e.size / 2;
511
+ if (sprite.soft) {
512
+ const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r);
513
+ g.addColorStop(0, sprite.color);
514
+ g.addColorStop(0.35, hexToRgba(sprite.color, 0.75));
515
+ g.addColorStop(1, hexToRgba(sprite.color, 0));
516
+ ctx.fillStyle = g;
517
+ } else {
518
+ ctx.fillStyle = sprite.color;
519
+ }
315
520
  ctx.beginPath();
316
- ctx.arc(p.x, p.y, e.size / 2, 0, Math.PI * 2);
521
+ ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
317
522
  ctx.fill();
318
523
  return;
319
524
  }
@@ -326,18 +531,67 @@ var SpecInstance = class {
326
531
  ctx.font = `${e.size}px serif`;
327
532
  ctx.fillText(sprite.glyphs[e.spriteIndex] ?? sprite.glyphs[0], 0, 0);
328
533
  } else {
534
+ ctx.textAlign = sprite.align ?? "center";
535
+ ctx.textBaseline = sprite.baseline ?? "middle";
329
536
  ctx.font = sprite.font ?? `${e.size}px system-ui, sans-serif`;
330
537
  ctx.fillStyle = sprite.color ?? "#e6e8ef";
331
- ctx.fillText(sprite.strings[e.spriteIndex] ?? sprite.strings[0], 0, 0);
538
+ const text = sprite.strings[e.spriteIndex] ?? sprite.strings[0];
539
+ if (sprite.maxWidth) ctx.fillText(text, 0, 0, sprite.maxWidth);
540
+ else ctx.fillText(text, 0, 0);
332
541
  }
333
542
  ctx.restore();
334
543
  }
544
+ /** Advance any live steering glide; rebuild entities on structural change. */
545
+ stepTransition(t) {
546
+ const tr = this.transition;
547
+ if (!tr) return;
548
+ const k = tr.dur <= 0 ? 1 : (t - tr.startT) / tr.dur;
549
+ this.effSpec = k >= 1 ? tr.to : lerpSpec(tr.from, tr.to, easeSmooth(k));
550
+ if (k >= 1) this.transition = null;
551
+ const sig = structuralSignature(this.effSpec);
552
+ if (sig !== this.lastStructural) this.rebuild();
553
+ else this.layers.forEach((b, i) => {
554
+ b.layer = this.effSpec.layers[i] ?? b.layer;
555
+ });
556
+ }
557
+ /**
558
+ * Live steering: glide from what's currently rendered to the track's target
559
+ * state (deltas applied last-wins). Duration = the longest delta `dur`.
560
+ */
561
+ applyTrack(track) {
562
+ const deltas = track?.deltas ?? [];
563
+ const target = applyDeltasToSpec(this.effSpec, deltas);
564
+ if (!validateSpec(target).valid) return;
565
+ const dur = deltas.length ? deltas.reduce((m, d) => Math.max(m, d.dur ?? DEFAULT_STEER_DUR), 0) : DEFAULT_STEER_DUR;
566
+ this.transition = {
567
+ from: JSON.parse(JSON.stringify(this.effSpec)),
568
+ to: target,
569
+ startT: this.lastT,
570
+ dur
571
+ };
572
+ if (this.paused) {
573
+ this.transition = null;
574
+ this.effSpec = target;
575
+ if (structuralSignature(this.effSpec) !== this.lastStructural) this.rebuild();
576
+ else this.layers.forEach((b, i) => {
577
+ b.layer = this.effSpec.layers[i] ?? b.layer;
578
+ });
579
+ this.renderFrame(this.lastT, this.seed);
580
+ }
581
+ }
335
582
  /** Deterministic, frame-addressable render (shared by the rAF loop). */
336
583
  renderFrame(t, _seed) {
584
+ this.stepTransition(t);
585
+ const { ctx } = this;
586
+ ctx.globalAlpha = 1;
587
+ ctx.globalCompositeOperation = "source-over";
337
588
  this.drawBackground();
338
589
  for (const built of this.layers) {
590
+ ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
339
591
  for (const e of built.entities) this.drawEntity(built, e, t);
340
592
  }
593
+ ctx.globalAlpha = 1;
594
+ ctx.globalCompositeOperation = "source-over";
341
595
  }
342
596
  setPaused(paused) {
343
597
  this.paused = paused;
@@ -347,31 +601,33 @@ var SpecInstance = class {
347
601
  this.start();
348
602
  }
349
603
  }
350
- resize(width, height) {
604
+ resize(width, height, dpr) {
351
605
  this.w = width;
352
606
  this.h = height;
607
+ if (dpr !== void 0) this.saverCtx.dpr = dpr;
353
608
  this.sizeCanvas();
354
609
  this.rebuild();
355
610
  if (this.paused) this.renderFrame(0, this.seed);
356
611
  }
357
612
  dispose() {
358
613
  this.stop();
359
- this.canvas.remove();
614
+ if (typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
360
615
  }
361
616
  };
362
617
  function compileSaver(spec) {
363
618
  const valid = assertValidSpec(spec);
364
619
  return {
365
620
  manifest: manifestFor(valid),
366
- mount: (ctx) => new SpecInstance(valid, ctx)
621
+ mount: (ctx) => new SpecInstance(valid, ctx),
622
+ spec: valid
367
623
  };
368
624
  }
369
625
 
370
- // src/examples.ts
626
+ // src/examples/aquarium.ts
371
627
  var AQUARIUM_SPEC = {
372
628
  schemaVersion: 1,
373
629
  id: "aquarium",
374
- label: "Aquarium (declarative)",
630
+ label: "Aquarium",
375
631
  seed: 42,
376
632
  motionIntensity: "calm",
377
633
  background: {
@@ -398,10 +654,576 @@ var AQUARIUM_SPEC = {
398
654
  }
399
655
  ]
400
656
  };
657
+
658
+ // src/examples/dev-dashboard.ts
659
+ var DASHBOARD_SPEC = {
660
+ schemaVersion: 1,
661
+ id: "dev-dashboard",
662
+ label: "Dev Dashboard",
663
+ seed: 256,
664
+ motionIntensity: "calm",
665
+ background: {
666
+ type: "gradient",
667
+ stops: [
668
+ { at: 0, color: "#06060c" },
669
+ { at: 0.5, color: "#0a0a14" },
670
+ { at: 1, color: "#0c0c18" }
671
+ ]
672
+ },
673
+ layers: [
674
+ // --- ambient depth ---
675
+ {
676
+ count: 50,
677
+ sprite: { kind: "circle", radius: [0.2, 0.8], color: "#1a2538" },
678
+ alpha: [0.1, 0.25],
679
+ motion: { type: "drift", speed: [0.3, 1.2], bob: 0.3 }
680
+ },
681
+ {
682
+ count: 12,
683
+ sprite: { kind: "circle", radius: [1, 2.5], color: "#1a3050", soft: true },
684
+ alpha: [0.08, 0.18],
685
+ blend: "lighter",
686
+ pulse: { amp: 0.08, period: 4e3 },
687
+ motion: { type: "drift", speed: [0.5, 1.5], bob: 0.5 }
688
+ },
689
+ // --- header ---
690
+ {
691
+ key: "title",
692
+ count: 1,
693
+ position: { x: 0.5, y: 0.04 },
694
+ sprite: {
695
+ kind: "text",
696
+ strings: ["DEV PULSE"],
697
+ color: "#556677",
698
+ font: "600 13px 'SF Mono', monospace",
699
+ align: "center"
700
+ },
701
+ motion: { type: "static" }
702
+ },
703
+ {
704
+ key: "status",
705
+ count: 1,
706
+ position: { x: 0.92, y: 0.04 },
707
+ sprite: {
708
+ kind: "text",
709
+ strings: ["\u25CF ACTIVE"],
710
+ color: "#44cc66",
711
+ font: "bold 10px monospace",
712
+ align: "right"
713
+ },
714
+ motion: { type: "static" }
715
+ },
716
+ {
717
+ key: "uptime",
718
+ count: 1,
719
+ position: { x: 0.08, y: 0.04 },
720
+ sprite: {
721
+ kind: "text",
722
+ strings: ["up 2h 14m"],
723
+ color: "#445566",
724
+ font: "10px monospace",
725
+ align: "left"
726
+ },
727
+ motion: { type: "static" }
728
+ },
729
+ // --- left column: metrics ---
730
+ {
731
+ key: "tools-label",
732
+ count: 1,
733
+ position: { x: 0.06, y: 0.12 },
734
+ sprite: {
735
+ kind: "text",
736
+ strings: ["TOOL CALLS"],
737
+ color: "#445566",
738
+ font: "9px monospace",
739
+ align: "left",
740
+ baseline: "top"
741
+ },
742
+ motion: { type: "static" }
743
+ },
744
+ {
745
+ key: "tools-value",
746
+ count: 1,
747
+ position: { x: 0.06, y: 0.16 },
748
+ sprite: {
749
+ kind: "text",
750
+ strings: ["147"],
751
+ color: "#44aaff",
752
+ font: "bold 28px monospace",
753
+ align: "left",
754
+ baseline: "top"
755
+ },
756
+ motion: { type: "static" }
757
+ },
758
+ {
759
+ key: "reads-label",
760
+ count: 1,
761
+ position: { x: 0.06, y: 0.26 },
762
+ sprite: {
763
+ kind: "text",
764
+ strings: ["READS"],
765
+ color: "#445566",
766
+ font: "9px monospace",
767
+ align: "left",
768
+ baseline: "top"
769
+ },
770
+ motion: { type: "static" }
771
+ },
772
+ {
773
+ key: "reads-value",
774
+ count: 1,
775
+ position: { x: 0.06, y: 0.3 },
776
+ sprite: {
777
+ kind: "text",
778
+ strings: ["89"],
779
+ color: "#44ddcc",
780
+ font: "bold 28px monospace",
781
+ align: "left",
782
+ baseline: "top"
783
+ },
784
+ motion: { type: "static" }
785
+ },
786
+ {
787
+ key: "edits-label",
788
+ count: 1,
789
+ position: { x: 0.06, y: 0.4 },
790
+ sprite: {
791
+ kind: "text",
792
+ strings: ["EDITS"],
793
+ color: "#445566",
794
+ font: "9px monospace",
795
+ align: "left",
796
+ baseline: "top"
797
+ },
798
+ motion: { type: "static" }
799
+ },
800
+ {
801
+ key: "edits-value",
802
+ count: 1,
803
+ position: { x: 0.06, y: 0.44 },
804
+ sprite: {
805
+ kind: "text",
806
+ strings: ["34"],
807
+ color: "#ff8844",
808
+ font: "bold 28px monospace",
809
+ align: "left",
810
+ baseline: "top"
811
+ },
812
+ motion: { type: "static" }
813
+ },
814
+ {
815
+ key: "errors-label",
816
+ count: 1,
817
+ position: { x: 0.06, y: 0.54 },
818
+ sprite: {
819
+ kind: "text",
820
+ strings: ["ERRORS"],
821
+ color: "#445566",
822
+ font: "9px monospace",
823
+ align: "left",
824
+ baseline: "top"
825
+ },
826
+ motion: { type: "static" }
827
+ },
828
+ {
829
+ key: "errors-value",
830
+ count: 1,
831
+ position: { x: 0.06, y: 0.58 },
832
+ sprite: {
833
+ kind: "text",
834
+ strings: ["2"],
835
+ color: "#ff4455",
836
+ font: "bold 28px monospace",
837
+ align: "left",
838
+ baseline: "top"
839
+ },
840
+ motion: { type: "static" }
841
+ },
842
+ // --- center column: system gauges ---
843
+ {
844
+ key: "cpu-label",
845
+ count: 1,
846
+ position: { x: 0.38, y: 0.12 },
847
+ sprite: {
848
+ kind: "text",
849
+ strings: ["CPU"],
850
+ color: "#445566",
851
+ font: "9px monospace",
852
+ align: "center"
853
+ },
854
+ motion: { type: "static" }
855
+ },
856
+ {
857
+ key: "cpu-value",
858
+ count: 1,
859
+ position: { x: 0.38, y: 0.22 },
860
+ sprite: {
861
+ kind: "text",
862
+ strings: ["73%"],
863
+ color: "#44ff88",
864
+ font: "bold 36px monospace",
865
+ align: "center"
866
+ },
867
+ motion: { type: "static" }
868
+ },
869
+ {
870
+ key: "mem-label",
871
+ count: 1,
872
+ position: { x: 0.38, y: 0.36 },
873
+ sprite: {
874
+ kind: "text",
875
+ strings: ["MEMORY"],
876
+ color: "#445566",
877
+ font: "9px monospace",
878
+ align: "center"
879
+ },
880
+ motion: { type: "static" }
881
+ },
882
+ {
883
+ key: "mem-value",
884
+ count: 1,
885
+ position: { x: 0.38, y: 0.46 },
886
+ sprite: {
887
+ kind: "text",
888
+ strings: ["4.2 GB"],
889
+ color: "#ddcc44",
890
+ font: "bold 36px monospace",
891
+ align: "center"
892
+ },
893
+ motion: { type: "static" }
894
+ },
895
+ {
896
+ key: "tokens-label",
897
+ count: 1,
898
+ position: { x: 0.38, y: 0.58 },
899
+ sprite: {
900
+ kind: "text",
901
+ strings: ["TOKENS"],
902
+ color: "#445566",
903
+ font: "9px monospace",
904
+ align: "center"
905
+ },
906
+ motion: { type: "static" }
907
+ },
908
+ {
909
+ key: "tokens-value",
910
+ count: 1,
911
+ position: { x: 0.38, y: 0.66 },
912
+ sprite: {
913
+ kind: "text",
914
+ strings: ["128k"],
915
+ color: "#aa88ff",
916
+ font: "bold 28px monospace",
917
+ align: "center"
918
+ },
919
+ motion: { type: "static" }
920
+ },
921
+ // --- right column: recent files ---
922
+ {
923
+ key: "files-label",
924
+ count: 1,
925
+ position: { x: 0.62, y: 0.12 },
926
+ sprite: {
927
+ kind: "text",
928
+ strings: ["RECENT FILES"],
929
+ color: "#445566",
930
+ font: "9px monospace",
931
+ align: "left",
932
+ baseline: "top"
933
+ },
934
+ motion: { type: "static" }
935
+ },
936
+ {
937
+ key: "file-1",
938
+ count: 1,
939
+ position: { x: 0.62, y: 0.17 },
940
+ sprite: {
941
+ kind: "text",
942
+ strings: ["src/worker.ts"],
943
+ color: "#8899aa",
944
+ font: "11px monospace",
945
+ align: "left",
946
+ baseline: "top",
947
+ maxWidth: 280
948
+ },
949
+ motion: { type: "static" }
950
+ },
951
+ {
952
+ key: "file-2",
953
+ count: 1,
954
+ position: { x: 0.62, y: 0.21 },
955
+ sprite: {
956
+ kind: "text",
957
+ strings: ["site/src/App.tsx"],
958
+ color: "#778899",
959
+ font: "11px monospace",
960
+ align: "left",
961
+ baseline: "top",
962
+ maxWidth: 280
963
+ },
964
+ motion: { type: "static" }
965
+ },
966
+ {
967
+ key: "file-3",
968
+ count: 1,
969
+ position: { x: 0.62, y: 0.25 },
970
+ sprite: {
971
+ kind: "text",
972
+ strings: ["packages/schema/compile.ts"],
973
+ color: "#667788",
974
+ font: "11px monospace",
975
+ align: "left",
976
+ baseline: "top",
977
+ maxWidth: 280
978
+ },
979
+ motion: { type: "static" }
980
+ },
981
+ {
982
+ key: "file-4",
983
+ count: 1,
984
+ position: { x: 0.62, y: 0.29 },
985
+ sprite: {
986
+ kind: "text",
987
+ strings: ["docs/dashboard-spec.md"],
988
+ color: "#556677",
989
+ font: "11px monospace",
990
+ align: "left",
991
+ baseline: "top",
992
+ maxWidth: 280
993
+ },
994
+ motion: { type: "static" }
995
+ },
996
+ {
997
+ key: "file-5",
998
+ count: 1,
999
+ position: { x: 0.62, y: 0.33 },
1000
+ sprite: {
1001
+ kind: "text",
1002
+ strings: ["packages/core/types.ts"],
1003
+ color: "#445566",
1004
+ font: "11px monospace",
1005
+ align: "left",
1006
+ baseline: "top",
1007
+ maxWidth: 280
1008
+ },
1009
+ motion: { type: "static" }
1010
+ },
1011
+ // --- right column: session info ---
1012
+ {
1013
+ key: "session-label",
1014
+ count: 1,
1015
+ position: { x: 0.62, y: 0.42 },
1016
+ sprite: {
1017
+ kind: "text",
1018
+ strings: ["SESSION"],
1019
+ color: "#445566",
1020
+ font: "9px monospace",
1021
+ align: "left",
1022
+ baseline: "top"
1023
+ },
1024
+ motion: { type: "static" }
1025
+ },
1026
+ {
1027
+ key: "session-model",
1028
+ count: 1,
1029
+ position: { x: 0.62, y: 0.47 },
1030
+ sprite: {
1031
+ kind: "text",
1032
+ strings: ["claude-opus-4-6"],
1033
+ color: "#8888aa",
1034
+ font: "11px monospace",
1035
+ align: "left",
1036
+ baseline: "top"
1037
+ },
1038
+ motion: { type: "static" }
1039
+ },
1040
+ {
1041
+ key: "session-cost",
1042
+ count: 1,
1043
+ position: { x: 0.62, y: 0.51 },
1044
+ sprite: {
1045
+ kind: "text",
1046
+ strings: ["$2.47 this session"],
1047
+ color: "#667788",
1048
+ font: "11px monospace",
1049
+ align: "left",
1050
+ baseline: "top"
1051
+ },
1052
+ motion: { type: "static" }
1053
+ },
1054
+ // --- bottom: event log ---
1055
+ {
1056
+ key: "log-label",
1057
+ count: 1,
1058
+ position: { x: 0.06, y: 0.76 },
1059
+ sprite: {
1060
+ kind: "text",
1061
+ strings: ["EVENT LOG"],
1062
+ color: "#445566",
1063
+ font: "9px monospace",
1064
+ align: "left",
1065
+ baseline: "top"
1066
+ },
1067
+ motion: { type: "static" }
1068
+ },
1069
+ {
1070
+ key: "log-1",
1071
+ count: 1,
1072
+ position: { x: 0.06, y: 0.8 },
1073
+ sprite: {
1074
+ kind: "text",
1075
+ strings: ["14:07 Edit compile.ts +18 -3"],
1076
+ color: "#667766",
1077
+ font: "10px monospace",
1078
+ align: "left",
1079
+ baseline: "top",
1080
+ maxWidth: 600
1081
+ },
1082
+ motion: { type: "static" }
1083
+ },
1084
+ {
1085
+ key: "log-2",
1086
+ count: 1,
1087
+ position: { x: 0.06, y: 0.84 },
1088
+ sprite: {
1089
+ kind: "text",
1090
+ strings: ["14:06 Bash pnpm test \u2713 125 passed"],
1091
+ color: "#556655",
1092
+ font: "10px monospace",
1093
+ align: "left",
1094
+ baseline: "top",
1095
+ maxWidth: 600
1096
+ },
1097
+ motion: { type: "static" }
1098
+ },
1099
+ {
1100
+ key: "log-3",
1101
+ count: 1,
1102
+ position: { x: 0.06, y: 0.88 },
1103
+ sprite: {
1104
+ kind: "text",
1105
+ strings: ["14:05 Read types.ts (121 lines)"],
1106
+ color: "#4d5e4d",
1107
+ font: "10px monospace",
1108
+ align: "left",
1109
+ baseline: "top",
1110
+ maxWidth: 600
1111
+ },
1112
+ motion: { type: "static" }
1113
+ },
1114
+ {
1115
+ key: "log-4",
1116
+ count: 1,
1117
+ position: { x: 0.06, y: 0.92 },
1118
+ sprite: {
1119
+ kind: "text",
1120
+ strings: ["14:03 Edit examples/dev-dashboard.ts +84 -1"],
1121
+ color: "#445544",
1122
+ font: "10px monospace",
1123
+ align: "left",
1124
+ baseline: "top",
1125
+ maxWidth: 600
1126
+ },
1127
+ motion: { type: "static" }
1128
+ },
1129
+ {
1130
+ key: "log-5",
1131
+ count: 1,
1132
+ position: { x: 0.06, y: 0.96 },
1133
+ sprite: {
1134
+ kind: "text",
1135
+ strings: ["14:01 Bash pnpm build \u2713 6 packages"],
1136
+ color: "#3d4e3d",
1137
+ font: "10px monospace",
1138
+ align: "left",
1139
+ baseline: "top",
1140
+ maxWidth: 600
1141
+ },
1142
+ motion: { type: "static" }
1143
+ }
1144
+ ]
1145
+ };
1146
+
1147
+ // src/examples/lanterns.ts
1148
+ var LANTERNS_SPEC = {
1149
+ schemaVersion: 1,
1150
+ id: "lanterns",
1151
+ label: "Night Lanterns",
1152
+ seed: 88,
1153
+ motionIntensity: "calm",
1154
+ background: {
1155
+ type: "gradient",
1156
+ stops: [
1157
+ { at: 0, color: "#04060f" },
1158
+ { at: 0.55, color: "#0b1026" },
1159
+ { at: 0.85, color: "#251731" },
1160
+ { at: 1, color: "#472518" }
1161
+ ]
1162
+ },
1163
+ layers: [
1164
+ {
1165
+ count: 60,
1166
+ sprite: { kind: "circle", radius: [0.5, 1.4], color: "#8fa0c8" },
1167
+ alpha: [0.35, 1],
1168
+ region: { y: [0, 0.62] },
1169
+ motion: { type: "drift", speed: [0.5, 2], bob: 1 }
1170
+ },
1171
+ {
1172
+ count: 36,
1173
+ sprite: {
1174
+ kind: "circle",
1175
+ radius: [1.5, 3],
1176
+ color: "#b06a2a",
1177
+ soft: true
1178
+ },
1179
+ alpha: [0.5, 0.9],
1180
+ blend: "lighter",
1181
+ pulse: { amp: 0.18, period: 2800 },
1182
+ motion: { type: "rise", speed: [6, 14], sway: 4 }
1183
+ },
1184
+ {
1185
+ count: 14,
1186
+ sprite: {
1187
+ kind: "circle",
1188
+ radius: [3.5, 6.5],
1189
+ color: "#e08a34",
1190
+ soft: true
1191
+ },
1192
+ alpha: [0.6, 1],
1193
+ blend: "lighter",
1194
+ pulse: { amp: 0.22, period: 3400 },
1195
+ motion: { type: "rise", speed: [16, 28], sway: 7 }
1196
+ },
1197
+ {
1198
+ count: 8,
1199
+ sprite: { kind: "emoji", glyphs: ["\u{1F3EE}"] },
1200
+ size: [26, 44],
1201
+ alpha: [0.85, 1],
1202
+ motion: { type: "rise", speed: [30, 48], sway: 10 }
1203
+ },
1204
+ {
1205
+ count: 4,
1206
+ sprite: { kind: "emoji", glyphs: ["\u{1F3EE}"] },
1207
+ size: [58, 84],
1208
+ motion: { type: "rise", speed: [52, 74], sway: 14 }
1209
+ },
1210
+ {
1211
+ count: 12,
1212
+ sprite: { kind: "circle", radius: [2, 5], color: "#e08a34", soft: true },
1213
+ alpha: [0.12, 0.35],
1214
+ blend: "lighter",
1215
+ region: { y: [0.93, 1] },
1216
+ pulse: { amp: 0.1, period: 2200 },
1217
+ motion: { type: "drift", speed: [2, 6], bob: 2 }
1218
+ }
1219
+ ]
1220
+ };
1221
+
1222
+ // src/examples/rain.ts
401
1223
  var RAIN_SPEC = {
402
1224
  schemaVersion: 1,
403
1225
  id: "rain",
404
- label: "Rain (declarative)",
1226
+ label: "Rain",
405
1227
  seed: 7,
406
1228
  motionIntensity: "moderate",
407
1229
  background: { type: "solid", color: "#05070a" },
@@ -414,19 +1236,138 @@ var RAIN_SPEC = {
414
1236
  }
415
1237
  ]
416
1238
  };
417
- var EXAMPLE_SPECS = [AQUARIUM_SPEC, RAIN_SPEC];
1239
+
1240
+ // src/examples/sakura.ts
1241
+ var SAKURA_SPEC = {
1242
+ schemaVersion: 1,
1243
+ id: "sakura",
1244
+ label: "Sakura Drift",
1245
+ seed: 314,
1246
+ motionIntensity: "calm",
1247
+ background: {
1248
+ type: "gradient",
1249
+ stops: [
1250
+ { at: 0, color: "#1b1d38" },
1251
+ { at: 0.45, color: "#3a2645" },
1252
+ { at: 0.8, color: "#7d3d4e" },
1253
+ { at: 1, color: "#0f140d" }
1254
+ ],
1255
+ band: { color: "#0c140d", height: 44 }
1256
+ },
1257
+ layers: [
1258
+ {
1259
+ count: 60,
1260
+ sprite: { kind: "circle", radius: [0.6, 1.6], color: "#ffe3ec", soft: true },
1261
+ alpha: [0.3, 0.9],
1262
+ blend: "lighter",
1263
+ region: { y: [0, 0.72] },
1264
+ pulse: { amp: 0.22, period: 3e3 },
1265
+ motion: { type: "drift", speed: [0.3, 1.2], bob: 1 }
1266
+ },
1267
+ {
1268
+ count: 28,
1269
+ sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
1270
+ size: [13, 20],
1271
+ alpha: [0.5, 0.8],
1272
+ motion: { type: "drift", speed: [12, 26], angle: 100, bob: 3 }
1273
+ },
1274
+ {
1275
+ count: 18,
1276
+ sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
1277
+ size: [20, 32],
1278
+ alpha: [0.7, 0.95],
1279
+ motion: { type: "drift", speed: [24, 44], angle: 100, bob: 6 }
1280
+ },
1281
+ {
1282
+ count: 10,
1283
+ sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
1284
+ size: [34, 54],
1285
+ alpha: [0.85, 1],
1286
+ motion: { type: "drift", speed: [42, 68], angle: 100, bob: 11 }
1287
+ },
1288
+ {
1289
+ count: 16,
1290
+ sprite: { kind: "circle", radius: [1.5, 4], color: "#ff9bb5", soft: true },
1291
+ alpha: [0.1, 0.3],
1292
+ blend: "lighter",
1293
+ region: { y: [0.9, 1] },
1294
+ pulse: { amp: 0.15, period: 2600 },
1295
+ motion: { type: "drift", speed: [0.3, 1.5], angle: 180, bob: 1 }
1296
+ }
1297
+ ]
1298
+ };
1299
+
1300
+ // src/examples/snowfall.ts
1301
+ var SNOWFALL_SPEC = {
1302
+ schemaVersion: 1,
1303
+ id: "snowfall",
1304
+ label: "Snowfall",
1305
+ seed: 12,
1306
+ motionIntensity: "calm",
1307
+ background: {
1308
+ type: "gradient",
1309
+ stops: [
1310
+ { at: 0, color: "#0a1628" },
1311
+ { at: 0.7, color: "#0d1f3c" },
1312
+ { at: 1, color: "#121212" }
1313
+ ]
1314
+ },
1315
+ layers: [
1316
+ {
1317
+ count: 50,
1318
+ sprite: { kind: "circle", radius: [1, 2.5], color: "#8899aa" },
1319
+ motion: { type: "drift", angle: 90, speed: [15, 40], bob: 3 }
1320
+ },
1321
+ {
1322
+ count: 35,
1323
+ sprite: { kind: "circle", radius: [2, 4.5], color: "#c0cdd8" },
1324
+ motion: { type: "drift", angle: 85, speed: [35, 80], bob: 6 }
1325
+ },
1326
+ {
1327
+ count: 15,
1328
+ sprite: { kind: "circle", radius: [3.5, 7], color: "#e8eff5" },
1329
+ motion: { type: "drift", angle: 82, speed: [55, 120], bob: 10 }
1330
+ }
1331
+ ]
1332
+ };
1333
+
1334
+ // src/examples/index.ts
1335
+ var SCHEMA_EXAMPLES = [
1336
+ { id: "aquarium", label: "Aquarium", spec: AQUARIUM_SPEC },
1337
+ { id: "rain", label: "Rain", spec: RAIN_SPEC },
1338
+ { id: "snowfall", label: "Snowfall", spec: SNOWFALL_SPEC },
1339
+ { id: "lanterns", label: "Night Lanterns", spec: LANTERNS_SPEC },
1340
+ { id: "sakura", label: "Sakura Drift", spec: SAKURA_SPEC },
1341
+ { id: "dev-dashboard", label: "Dev Dashboard", spec: DASHBOARD_SPEC }
1342
+ ];
1343
+ var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
1344
+ var EXAMPLE_BY_ID = Object.fromEntries(
1345
+ SCHEMA_EXAMPLES.map((e) => [e.id, e.spec])
1346
+ );
418
1347
  export {
419
1348
  AQUARIUM_SPEC,
1349
+ DASHBOARD_SPEC,
1350
+ EXAMPLE_BY_ID,
420
1351
  EXAMPLE_SPECS,
1352
+ LANTERNS_SPEC,
421
1353
  LIMITS,
422
1354
  RAIN_SPEC,
1355
+ SAKURA_SPEC,
1356
+ SCHEMA_EXAMPLES,
423
1357
  SCHEMA_VERSION,
1358
+ SNOWFALL_SPEC,
1359
+ alphaAt,
1360
+ applyDeltasToSpec,
424
1361
  assertValidSpec,
425
1362
  buildEntities,
426
1363
  compileSaver,
1364
+ easeSmooth,
1365
+ lerpSpec,
427
1366
  manifestFor,
428
1367
  positionAt,
1368
+ resolveSpecPath,
429
1369
  spriteVariants,
1370
+ structuralSignature,
430
1371
  validateSpec
431
1372
  };
432
1373
  //# sourceMappingURL=index.js.map