@idle-screens/schema 0.1.0 → 1.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,11 @@ function positionAt(e, t, w, h) {
216
292
  import {
217
293
  createRng
218
294
  } from "@idle-screens/core";
295
+ function hexToRgba(hex, alpha) {
296
+ const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
297
+ const n = parseInt(h.slice(1), 16);
298
+ return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${alpha})`;
299
+ }
219
300
  function manifestFor(spec) {
220
301
  const total = spec.layers.reduce((n, l) => n + l.count, 0);
221
302
  const costTier = total < 30 ? "idle" : total < 150 ? "low" : total < 400 ? "medium" : "high";
@@ -235,11 +316,18 @@ function manifestFor(spec) {
235
316
  var SpecInstance = class {
236
317
  constructor(spec, ctx) {
237
318
  this.spec = spec;
319
+ this.saverCtx = ctx;
238
320
  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);
321
+ let canvas;
322
+ if (ctx.surface) {
323
+ canvas = ctx.surface;
324
+ } else {
325
+ const el = document.createElement("canvas");
326
+ el.style.cssText = "display:block;width:100%;height:100%";
327
+ el.setAttribute("aria-hidden", "true");
328
+ ctx.host.appendChild(el);
329
+ canvas = el;
330
+ }
243
331
  this.canvas = canvas;
244
332
  const c2d = canvas.getContext("2d", { alpha: false });
245
333
  if (!c2d) throw new Error("schema saver: no 2d context");
@@ -255,6 +343,7 @@ var SpecInstance = class {
255
343
  spec;
256
344
  canvas;
257
345
  ctx;
346
+ saverCtx;
258
347
  seed;
259
348
  w;
260
349
  h;
@@ -262,8 +351,11 @@ var SpecInstance = class {
262
351
  frameId = null;
263
352
  paused = false;
264
353
  startT = 0;
354
+ baseT = 0;
355
+ // elapsed logical time carried across pause/resume
356
+ lastT = 0;
265
357
  sizeCanvas() {
266
- const dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 2);
358
+ const dpr = Math.min(this.saverCtx.dpr, 2);
267
359
  this.canvas.width = Math.max(1, Math.round(this.w * dpr));
268
360
  this.canvas.height = Math.max(1, Math.round(this.h * dpr));
269
361
  this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
@@ -283,11 +375,13 @@ var SpecInstance = class {
283
375
  cancelAnimationFrame(this.frameId);
284
376
  this.frameId = null;
285
377
  }
378
+ this.baseT = this.lastT;
286
379
  }
287
380
  loop(now) {
288
381
  this.frameId = requestAnimationFrame((n) => this.loop(n));
289
382
  if (this.startT === 0) this.startT = now;
290
- this.renderFrame(now - this.startT, this.seed);
383
+ this.lastT = now - this.startT + this.baseT;
384
+ this.renderFrame(this.lastT, this.seed);
291
385
  }
292
386
  drawBackground() {
293
387
  const { ctx, w, h } = this;
@@ -310,10 +404,20 @@ var SpecInstance = class {
310
404
  const { ctx } = this;
311
405
  const p = positionAt(e, t, this.w, this.h);
312
406
  const sprite = built.layer.sprite;
407
+ ctx.globalAlpha = alphaAt(e, t);
313
408
  if (sprite.kind === "circle") {
314
- ctx.fillStyle = sprite.color;
409
+ const r = e.size / 2;
410
+ if (sprite.soft) {
411
+ const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r);
412
+ g.addColorStop(0, sprite.color);
413
+ g.addColorStop(0.35, hexToRgba(sprite.color, 0.75));
414
+ g.addColorStop(1, hexToRgba(sprite.color, 0));
415
+ ctx.fillStyle = g;
416
+ } else {
417
+ ctx.fillStyle = sprite.color;
418
+ }
315
419
  ctx.beginPath();
316
- ctx.arc(p.x, p.y, e.size / 2, 0, Math.PI * 2);
420
+ ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
317
421
  ctx.fill();
318
422
  return;
319
423
  }
@@ -326,18 +430,28 @@ var SpecInstance = class {
326
430
  ctx.font = `${e.size}px serif`;
327
431
  ctx.fillText(sprite.glyphs[e.spriteIndex] ?? sprite.glyphs[0], 0, 0);
328
432
  } else {
433
+ ctx.textAlign = sprite.align ?? "center";
434
+ ctx.textBaseline = sprite.baseline ?? "middle";
329
435
  ctx.font = sprite.font ?? `${e.size}px system-ui, sans-serif`;
330
436
  ctx.fillStyle = sprite.color ?? "#e6e8ef";
331
- ctx.fillText(sprite.strings[e.spriteIndex] ?? sprite.strings[0], 0, 0);
437
+ const text = sprite.strings[e.spriteIndex] ?? sprite.strings[0];
438
+ if (sprite.maxWidth) ctx.fillText(text, 0, 0, sprite.maxWidth);
439
+ else ctx.fillText(text, 0, 0);
332
440
  }
333
441
  ctx.restore();
334
442
  }
335
443
  /** Deterministic, frame-addressable render (shared by the rAF loop). */
336
444
  renderFrame(t, _seed) {
445
+ const { ctx } = this;
446
+ ctx.globalAlpha = 1;
447
+ ctx.globalCompositeOperation = "source-over";
337
448
  this.drawBackground();
338
449
  for (const built of this.layers) {
450
+ ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
339
451
  for (const e of built.entities) this.drawEntity(built, e, t);
340
452
  }
453
+ ctx.globalAlpha = 1;
454
+ ctx.globalCompositeOperation = "source-over";
341
455
  }
342
456
  setPaused(paused) {
343
457
  this.paused = paused;
@@ -347,23 +461,25 @@ var SpecInstance = class {
347
461
  this.start();
348
462
  }
349
463
  }
350
- resize(width, height) {
464
+ resize(width, height, dpr) {
351
465
  this.w = width;
352
466
  this.h = height;
467
+ if (dpr !== void 0) this.saverCtx.dpr = dpr;
353
468
  this.sizeCanvas();
354
469
  this.rebuild();
355
470
  if (this.paused) this.renderFrame(0, this.seed);
356
471
  }
357
472
  dispose() {
358
473
  this.stop();
359
- this.canvas.remove();
474
+ if (typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
360
475
  }
361
476
  };
362
477
  function compileSaver(spec) {
363
478
  const valid = assertValidSpec(spec);
364
479
  return {
365
480
  manifest: manifestFor(valid),
366
- mount: (ctx) => new SpecInstance(valid, ctx)
481
+ mount: (ctx) => new SpecInstance(valid, ctx),
482
+ spec: valid
367
483
  };
368
484
  }
369
485
 
@@ -371,7 +487,7 @@ function compileSaver(spec) {
371
487
  var AQUARIUM_SPEC = {
372
488
  schemaVersion: 1,
373
489
  id: "aquarium",
374
- label: "Aquarium (declarative)",
490
+ label: "Aquarium",
375
491
  seed: 42,
376
492
  motionIntensity: "calm",
377
493
  background: {
@@ -401,7 +517,7 @@ var AQUARIUM_SPEC = {
401
517
  var RAIN_SPEC = {
402
518
  schemaVersion: 1,
403
519
  id: "rain",
404
- label: "Rain (declarative)",
520
+ label: "Rain",
405
521
  seed: 7,
406
522
  motionIntensity: "moderate",
407
523
  background: { type: "solid", color: "#05070a" },
@@ -414,13 +530,675 @@ var RAIN_SPEC = {
414
530
  }
415
531
  ]
416
532
  };
417
- var EXAMPLE_SPECS = [AQUARIUM_SPEC, RAIN_SPEC];
533
+ var SNOWFALL_SPEC = {
534
+ schemaVersion: 1,
535
+ id: "snowfall",
536
+ label: "Snowfall",
537
+ seed: 12,
538
+ motionIntensity: "calm",
539
+ background: {
540
+ type: "gradient",
541
+ stops: [
542
+ { at: 0, color: "#0a1628" },
543
+ { at: 0.7, color: "#0d1f3c" },
544
+ { at: 1, color: "#121212" }
545
+ ]
546
+ },
547
+ layers: [
548
+ {
549
+ count: 50,
550
+ sprite: { kind: "circle", radius: [1, 2.5], color: "#8899aa" },
551
+ motion: { type: "drift", angle: 90, speed: [15, 40], bob: 3 }
552
+ },
553
+ {
554
+ count: 35,
555
+ sprite: { kind: "circle", radius: [2, 4.5], color: "#c0cdd8" },
556
+ motion: { type: "drift", angle: 85, speed: [35, 80], bob: 6 }
557
+ },
558
+ {
559
+ count: 15,
560
+ sprite: { kind: "circle", radius: [3.5, 7], color: "#e8eff5" },
561
+ motion: { type: "drift", angle: 82, speed: [55, 120], bob: 10 }
562
+ }
563
+ ]
564
+ };
565
+ var LANTERNS_SPEC = {
566
+ schemaVersion: 1,
567
+ id: "lanterns",
568
+ label: "Night Lanterns",
569
+ seed: 88,
570
+ motionIntensity: "calm",
571
+ background: {
572
+ type: "gradient",
573
+ stops: [
574
+ { at: 0, color: "#04060f" },
575
+ { at: 0.55, color: "#0b1026" },
576
+ { at: 0.85, color: "#251731" },
577
+ { at: 1, color: "#472518" }
578
+ ]
579
+ },
580
+ layers: [
581
+ {
582
+ count: 60,
583
+ sprite: { kind: "circle", radius: [0.5, 1.4], color: "#8fa0c8" },
584
+ alpha: [0.35, 1],
585
+ region: { y: [0, 0.62] },
586
+ motion: { type: "drift", speed: [0.5, 2], bob: 1 }
587
+ },
588
+ {
589
+ count: 36,
590
+ sprite: {
591
+ kind: "circle",
592
+ radius: [1.5, 3],
593
+ color: "#b06a2a",
594
+ soft: true
595
+ },
596
+ alpha: [0.5, 0.9],
597
+ blend: "lighter",
598
+ pulse: { amp: 0.18, period: 2800 },
599
+ motion: { type: "rise", speed: [6, 14], sway: 4 }
600
+ },
601
+ {
602
+ count: 14,
603
+ sprite: {
604
+ kind: "circle",
605
+ radius: [3.5, 6.5],
606
+ color: "#e08a34",
607
+ soft: true
608
+ },
609
+ alpha: [0.6, 1],
610
+ blend: "lighter",
611
+ pulse: { amp: 0.22, period: 3400 },
612
+ motion: { type: "rise", speed: [16, 28], sway: 7 }
613
+ },
614
+ {
615
+ count: 8,
616
+ sprite: { kind: "emoji", glyphs: ["\u{1F3EE}"] },
617
+ size: [26, 44],
618
+ alpha: [0.85, 1],
619
+ motion: { type: "rise", speed: [30, 48], sway: 10 }
620
+ },
621
+ {
622
+ count: 4,
623
+ sprite: { kind: "emoji", glyphs: ["\u{1F3EE}"] },
624
+ size: [58, 84],
625
+ motion: { type: "rise", speed: [52, 74], sway: 14 }
626
+ },
627
+ {
628
+ count: 12,
629
+ sprite: { kind: "circle", radius: [2, 5], color: "#e08a34", soft: true },
630
+ alpha: [0.12, 0.35],
631
+ blend: "lighter",
632
+ region: { y: [0.93, 1] },
633
+ pulse: { amp: 0.1, period: 2200 },
634
+ motion: { type: "drift", speed: [2, 6], bob: 2 }
635
+ }
636
+ ]
637
+ };
638
+ var SAKURA_SPEC = {
639
+ schemaVersion: 1,
640
+ id: "sakura",
641
+ label: "Sakura Drift",
642
+ seed: 314,
643
+ motionIntensity: "calm",
644
+ background: {
645
+ type: "gradient",
646
+ stops: [
647
+ { at: 0, color: "#1b1d38" },
648
+ { at: 0.45, color: "#3a2645" },
649
+ { at: 0.8, color: "#7d3d4e" },
650
+ { at: 1, color: "#0f140d" }
651
+ ],
652
+ band: { color: "#0c140d", height: 44 }
653
+ },
654
+ layers: [
655
+ {
656
+ count: 60,
657
+ sprite: { kind: "circle", radius: [0.6, 1.6], color: "#ffe3ec", soft: true },
658
+ alpha: [0.3, 0.9],
659
+ blend: "lighter",
660
+ region: { y: [0, 0.72] },
661
+ pulse: { amp: 0.22, period: 3e3 },
662
+ motion: { type: "drift", speed: [0.3, 1.2], bob: 1 }
663
+ },
664
+ {
665
+ count: 28,
666
+ sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
667
+ size: [13, 20],
668
+ alpha: [0.5, 0.8],
669
+ motion: { type: "drift", speed: [12, 26], angle: 100, bob: 3 }
670
+ },
671
+ {
672
+ count: 18,
673
+ sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
674
+ size: [20, 32],
675
+ alpha: [0.7, 0.95],
676
+ motion: { type: "drift", speed: [24, 44], angle: 100, bob: 6 }
677
+ },
678
+ {
679
+ count: 10,
680
+ sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
681
+ size: [34, 54],
682
+ alpha: [0.85, 1],
683
+ motion: { type: "drift", speed: [42, 68], angle: 100, bob: 11 }
684
+ },
685
+ {
686
+ count: 16,
687
+ sprite: { kind: "circle", radius: [1.5, 4], color: "#ff9bb5", soft: true },
688
+ alpha: [0.1, 0.3],
689
+ blend: "lighter",
690
+ region: { y: [0.9, 1] },
691
+ pulse: { amp: 0.15, period: 2600 },
692
+ motion: { type: "drift", speed: [0.3, 1.5], angle: 180, bob: 1 }
693
+ }
694
+ ]
695
+ };
696
+ var DASHBOARD_SPEC = {
697
+ schemaVersion: 1,
698
+ id: "dev-dashboard",
699
+ label: "Dev Dashboard",
700
+ seed: 256,
701
+ motionIntensity: "calm",
702
+ background: {
703
+ type: "gradient",
704
+ stops: [
705
+ { at: 0, color: "#06060c" },
706
+ { at: 0.5, color: "#0a0a14" },
707
+ { at: 1, color: "#0c0c18" }
708
+ ]
709
+ },
710
+ layers: [
711
+ // --- ambient depth ---
712
+ {
713
+ count: 50,
714
+ sprite: { kind: "circle", radius: [0.2, 0.8], color: "#1a2538" },
715
+ alpha: [0.1, 0.25],
716
+ motion: { type: "drift", speed: [0.3, 1.2], bob: 0.3 }
717
+ },
718
+ {
719
+ count: 12,
720
+ sprite: { kind: "circle", radius: [1, 2.5], color: "#1a3050", soft: true },
721
+ alpha: [0.08, 0.18],
722
+ blend: "lighter",
723
+ pulse: { amp: 0.08, period: 4e3 },
724
+ motion: { type: "drift", speed: [0.5, 1.5], bob: 0.5 }
725
+ },
726
+ // --- header ---
727
+ {
728
+ key: "title",
729
+ count: 1,
730
+ position: { x: 0.5, y: 0.04 },
731
+ sprite: {
732
+ kind: "text",
733
+ strings: ["DEV PULSE"],
734
+ color: "#556677",
735
+ font: "600 13px 'SF Mono', monospace",
736
+ align: "center"
737
+ },
738
+ motion: { type: "static" }
739
+ },
740
+ {
741
+ key: "status",
742
+ count: 1,
743
+ position: { x: 0.92, y: 0.04 },
744
+ sprite: {
745
+ kind: "text",
746
+ strings: ["\u25CF ACTIVE"],
747
+ color: "#44cc66",
748
+ font: "bold 10px monospace",
749
+ align: "right"
750
+ },
751
+ motion: { type: "static" }
752
+ },
753
+ {
754
+ key: "uptime",
755
+ count: 1,
756
+ position: { x: 0.08, y: 0.04 },
757
+ sprite: {
758
+ kind: "text",
759
+ strings: ["up 2h 14m"],
760
+ color: "#445566",
761
+ font: "10px monospace",
762
+ align: "left"
763
+ },
764
+ motion: { type: "static" }
765
+ },
766
+ // --- left column: metrics ---
767
+ {
768
+ key: "tools-label",
769
+ count: 1,
770
+ position: { x: 0.06, y: 0.12 },
771
+ sprite: {
772
+ kind: "text",
773
+ strings: ["TOOL CALLS"],
774
+ color: "#445566",
775
+ font: "9px monospace",
776
+ align: "left",
777
+ baseline: "top"
778
+ },
779
+ motion: { type: "static" }
780
+ },
781
+ {
782
+ key: "tools-value",
783
+ count: 1,
784
+ position: { x: 0.06, y: 0.16 },
785
+ sprite: {
786
+ kind: "text",
787
+ strings: ["147"],
788
+ color: "#44aaff",
789
+ font: "bold 28px monospace",
790
+ align: "left",
791
+ baseline: "top"
792
+ },
793
+ motion: { type: "static" }
794
+ },
795
+ {
796
+ key: "reads-label",
797
+ count: 1,
798
+ position: { x: 0.06, y: 0.26 },
799
+ sprite: {
800
+ kind: "text",
801
+ strings: ["READS"],
802
+ color: "#445566",
803
+ font: "9px monospace",
804
+ align: "left",
805
+ baseline: "top"
806
+ },
807
+ motion: { type: "static" }
808
+ },
809
+ {
810
+ key: "reads-value",
811
+ count: 1,
812
+ position: { x: 0.06, y: 0.3 },
813
+ sprite: {
814
+ kind: "text",
815
+ strings: ["89"],
816
+ color: "#44ddcc",
817
+ font: "bold 28px monospace",
818
+ align: "left",
819
+ baseline: "top"
820
+ },
821
+ motion: { type: "static" }
822
+ },
823
+ {
824
+ key: "edits-label",
825
+ count: 1,
826
+ position: { x: 0.06, y: 0.4 },
827
+ sprite: {
828
+ kind: "text",
829
+ strings: ["EDITS"],
830
+ color: "#445566",
831
+ font: "9px monospace",
832
+ align: "left",
833
+ baseline: "top"
834
+ },
835
+ motion: { type: "static" }
836
+ },
837
+ {
838
+ key: "edits-value",
839
+ count: 1,
840
+ position: { x: 0.06, y: 0.44 },
841
+ sprite: {
842
+ kind: "text",
843
+ strings: ["34"],
844
+ color: "#ff8844",
845
+ font: "bold 28px monospace",
846
+ align: "left",
847
+ baseline: "top"
848
+ },
849
+ motion: { type: "static" }
850
+ },
851
+ {
852
+ key: "errors-label",
853
+ count: 1,
854
+ position: { x: 0.06, y: 0.54 },
855
+ sprite: {
856
+ kind: "text",
857
+ strings: ["ERRORS"],
858
+ color: "#445566",
859
+ font: "9px monospace",
860
+ align: "left",
861
+ baseline: "top"
862
+ },
863
+ motion: { type: "static" }
864
+ },
865
+ {
866
+ key: "errors-value",
867
+ count: 1,
868
+ position: { x: 0.06, y: 0.58 },
869
+ sprite: {
870
+ kind: "text",
871
+ strings: ["2"],
872
+ color: "#ff4455",
873
+ font: "bold 28px monospace",
874
+ align: "left",
875
+ baseline: "top"
876
+ },
877
+ motion: { type: "static" }
878
+ },
879
+ // --- center column: system gauges ---
880
+ {
881
+ key: "cpu-label",
882
+ count: 1,
883
+ position: { x: 0.38, y: 0.12 },
884
+ sprite: {
885
+ kind: "text",
886
+ strings: ["CPU"],
887
+ color: "#445566",
888
+ font: "9px monospace",
889
+ align: "center"
890
+ },
891
+ motion: { type: "static" }
892
+ },
893
+ {
894
+ key: "cpu-value",
895
+ count: 1,
896
+ position: { x: 0.38, y: 0.22 },
897
+ sprite: {
898
+ kind: "text",
899
+ strings: ["73%"],
900
+ color: "#44ff88",
901
+ font: "bold 36px monospace",
902
+ align: "center"
903
+ },
904
+ motion: { type: "static" }
905
+ },
906
+ {
907
+ key: "mem-label",
908
+ count: 1,
909
+ position: { x: 0.38, y: 0.36 },
910
+ sprite: {
911
+ kind: "text",
912
+ strings: ["MEMORY"],
913
+ color: "#445566",
914
+ font: "9px monospace",
915
+ align: "center"
916
+ },
917
+ motion: { type: "static" }
918
+ },
919
+ {
920
+ key: "mem-value",
921
+ count: 1,
922
+ position: { x: 0.38, y: 0.46 },
923
+ sprite: {
924
+ kind: "text",
925
+ strings: ["4.2 GB"],
926
+ color: "#ddcc44",
927
+ font: "bold 36px monospace",
928
+ align: "center"
929
+ },
930
+ motion: { type: "static" }
931
+ },
932
+ {
933
+ key: "tokens-label",
934
+ count: 1,
935
+ position: { x: 0.38, y: 0.58 },
936
+ sprite: {
937
+ kind: "text",
938
+ strings: ["TOKENS"],
939
+ color: "#445566",
940
+ font: "9px monospace",
941
+ align: "center"
942
+ },
943
+ motion: { type: "static" }
944
+ },
945
+ {
946
+ key: "tokens-value",
947
+ count: 1,
948
+ position: { x: 0.38, y: 0.66 },
949
+ sprite: {
950
+ kind: "text",
951
+ strings: ["128k"],
952
+ color: "#aa88ff",
953
+ font: "bold 28px monospace",
954
+ align: "center"
955
+ },
956
+ motion: { type: "static" }
957
+ },
958
+ // --- right column: recent files ---
959
+ {
960
+ key: "files-label",
961
+ count: 1,
962
+ position: { x: 0.62, y: 0.12 },
963
+ sprite: {
964
+ kind: "text",
965
+ strings: ["RECENT FILES"],
966
+ color: "#445566",
967
+ font: "9px monospace",
968
+ align: "left",
969
+ baseline: "top"
970
+ },
971
+ motion: { type: "static" }
972
+ },
973
+ {
974
+ key: "file-1",
975
+ count: 1,
976
+ position: { x: 0.62, y: 0.17 },
977
+ sprite: {
978
+ kind: "text",
979
+ strings: ["src/worker.ts"],
980
+ color: "#8899aa",
981
+ font: "11px monospace",
982
+ align: "left",
983
+ baseline: "top",
984
+ maxWidth: 280
985
+ },
986
+ motion: { type: "static" }
987
+ },
988
+ {
989
+ key: "file-2",
990
+ count: 1,
991
+ position: { x: 0.62, y: 0.21 },
992
+ sprite: {
993
+ kind: "text",
994
+ strings: ["site/src/App.tsx"],
995
+ color: "#778899",
996
+ font: "11px monospace",
997
+ align: "left",
998
+ baseline: "top",
999
+ maxWidth: 280
1000
+ },
1001
+ motion: { type: "static" }
1002
+ },
1003
+ {
1004
+ key: "file-3",
1005
+ count: 1,
1006
+ position: { x: 0.62, y: 0.25 },
1007
+ sprite: {
1008
+ kind: "text",
1009
+ strings: ["packages/schema/compile.ts"],
1010
+ color: "#667788",
1011
+ font: "11px monospace",
1012
+ align: "left",
1013
+ baseline: "top",
1014
+ maxWidth: 280
1015
+ },
1016
+ motion: { type: "static" }
1017
+ },
1018
+ {
1019
+ key: "file-4",
1020
+ count: 1,
1021
+ position: { x: 0.62, y: 0.29 },
1022
+ sprite: {
1023
+ kind: "text",
1024
+ strings: ["docs/dashboard-spec.md"],
1025
+ color: "#556677",
1026
+ font: "11px monospace",
1027
+ align: "left",
1028
+ baseline: "top",
1029
+ maxWidth: 280
1030
+ },
1031
+ motion: { type: "static" }
1032
+ },
1033
+ {
1034
+ key: "file-5",
1035
+ count: 1,
1036
+ position: { x: 0.62, y: 0.33 },
1037
+ sprite: {
1038
+ kind: "text",
1039
+ strings: ["packages/core/types.ts"],
1040
+ color: "#445566",
1041
+ font: "11px monospace",
1042
+ align: "left",
1043
+ baseline: "top",
1044
+ maxWidth: 280
1045
+ },
1046
+ motion: { type: "static" }
1047
+ },
1048
+ // --- right column: session info ---
1049
+ {
1050
+ key: "session-label",
1051
+ count: 1,
1052
+ position: { x: 0.62, y: 0.42 },
1053
+ sprite: {
1054
+ kind: "text",
1055
+ strings: ["SESSION"],
1056
+ color: "#445566",
1057
+ font: "9px monospace",
1058
+ align: "left",
1059
+ baseline: "top"
1060
+ },
1061
+ motion: { type: "static" }
1062
+ },
1063
+ {
1064
+ key: "session-model",
1065
+ count: 1,
1066
+ position: { x: 0.62, y: 0.47 },
1067
+ sprite: {
1068
+ kind: "text",
1069
+ strings: ["claude-opus-4-6"],
1070
+ color: "#8888aa",
1071
+ font: "11px monospace",
1072
+ align: "left",
1073
+ baseline: "top"
1074
+ },
1075
+ motion: { type: "static" }
1076
+ },
1077
+ {
1078
+ key: "session-cost",
1079
+ count: 1,
1080
+ position: { x: 0.62, y: 0.51 },
1081
+ sprite: {
1082
+ kind: "text",
1083
+ strings: ["$2.47 this session"],
1084
+ color: "#667788",
1085
+ font: "11px monospace",
1086
+ align: "left",
1087
+ baseline: "top"
1088
+ },
1089
+ motion: { type: "static" }
1090
+ },
1091
+ // --- bottom: event log ---
1092
+ {
1093
+ key: "log-label",
1094
+ count: 1,
1095
+ position: { x: 0.06, y: 0.76 },
1096
+ sprite: {
1097
+ kind: "text",
1098
+ strings: ["EVENT LOG"],
1099
+ color: "#445566",
1100
+ font: "9px monospace",
1101
+ align: "left",
1102
+ baseline: "top"
1103
+ },
1104
+ motion: { type: "static" }
1105
+ },
1106
+ {
1107
+ key: "log-1",
1108
+ count: 1,
1109
+ position: { x: 0.06, y: 0.8 },
1110
+ sprite: {
1111
+ kind: "text",
1112
+ strings: ["14:07 Edit compile.ts +18 -3"],
1113
+ color: "#667766",
1114
+ font: "10px monospace",
1115
+ align: "left",
1116
+ baseline: "top",
1117
+ maxWidth: 600
1118
+ },
1119
+ motion: { type: "static" }
1120
+ },
1121
+ {
1122
+ key: "log-2",
1123
+ count: 1,
1124
+ position: { x: 0.06, y: 0.84 },
1125
+ sprite: {
1126
+ kind: "text",
1127
+ strings: ["14:06 Bash pnpm test \u2713 125 passed"],
1128
+ color: "#556655",
1129
+ font: "10px monospace",
1130
+ align: "left",
1131
+ baseline: "top",
1132
+ maxWidth: 600
1133
+ },
1134
+ motion: { type: "static" }
1135
+ },
1136
+ {
1137
+ key: "log-3",
1138
+ count: 1,
1139
+ position: { x: 0.06, y: 0.88 },
1140
+ sprite: {
1141
+ kind: "text",
1142
+ strings: ["14:05 Read types.ts (121 lines)"],
1143
+ color: "#4d5e4d",
1144
+ font: "10px monospace",
1145
+ align: "left",
1146
+ baseline: "top",
1147
+ maxWidth: 600
1148
+ },
1149
+ motion: { type: "static" }
1150
+ },
1151
+ {
1152
+ key: "log-4",
1153
+ count: 1,
1154
+ position: { x: 0.06, y: 0.92 },
1155
+ sprite: {
1156
+ kind: "text",
1157
+ strings: ["14:03 Edit examples.ts +84 -1"],
1158
+ color: "#445544",
1159
+ font: "10px monospace",
1160
+ align: "left",
1161
+ baseline: "top",
1162
+ maxWidth: 600
1163
+ },
1164
+ motion: { type: "static" }
1165
+ },
1166
+ {
1167
+ key: "log-5",
1168
+ count: 1,
1169
+ position: { x: 0.06, y: 0.96 },
1170
+ sprite: {
1171
+ kind: "text",
1172
+ strings: ["14:01 Bash pnpm build \u2713 6 packages"],
1173
+ color: "#3d4e3d",
1174
+ font: "10px monospace",
1175
+ align: "left",
1176
+ baseline: "top",
1177
+ maxWidth: 600
1178
+ },
1179
+ motion: { type: "static" }
1180
+ }
1181
+ ]
1182
+ };
1183
+ var EXAMPLE_SPECS = [
1184
+ AQUARIUM_SPEC,
1185
+ RAIN_SPEC,
1186
+ SNOWFALL_SPEC,
1187
+ LANTERNS_SPEC,
1188
+ SAKURA_SPEC,
1189
+ DASHBOARD_SPEC
1190
+ ];
418
1191
  export {
419
1192
  AQUARIUM_SPEC,
1193
+ DASHBOARD_SPEC,
420
1194
  EXAMPLE_SPECS,
1195
+ LANTERNS_SPEC,
421
1196
  LIMITS,
422
1197
  RAIN_SPEC,
1198
+ SAKURA_SPEC,
423
1199
  SCHEMA_VERSION,
1200
+ SNOWFALL_SPEC,
1201
+ alphaAt,
424
1202
  assertValidSpec,
425
1203
  buildEntities,
426
1204
  compileSaver,