@idle-screens/schema 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shawn Partridge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @idle-screens/schema
2
+
3
+ Declarative, agent-authorable saver format for idle-screens. Validate a data spec, then compile it into a runnable, seeded, flash-safe SaverPlugin.
4
+
5
+ See the [idle-screens repository](https://github.com/shaaaaawn/idle-screens) for full documentation.
@@ -0,0 +1,164 @@
1
+ import { Rng, SaverPlugin, SaverManifest } from '@idle-screens/core';
2
+
3
+ /**
4
+ * Declarative, agent-authorable saver format. A `SaverSpec` describes a saver as DATA
5
+ * (no code), which `compileSaver` turns into a seeded, deterministic, flash-safe
6
+ * SaverPlugin. It models the "drifting sprite field" family that covers most classic
7
+ * savers (fish, toasters, DVD, rain, messages): a static background plus one or more
8
+ * layers of entities that move.
9
+ *
10
+ * SAFETY INVARIANT: there is deliberately NO full-field strobe/flash primitive — the
11
+ * background is static and entities are bounded sprites — so a compiled spec cannot
12
+ * produce photosensitive flashing by construction (proven by sampling a compiled spec
13
+ * through @idle-screens/validator).
14
+ */
15
+ declare const SCHEMA_VERSION: 1;
16
+ interface SaverSpec {
17
+ schemaVersion: 1;
18
+ id: string;
19
+ label: string;
20
+ /** Deterministic seed; falls back to the host's SaverContext.seed. */
21
+ seed?: number;
22
+ background?: BackgroundSpec;
23
+ layers: LayerSpec[];
24
+ motionIntensity?: 'calm' | 'moderate' | 'energetic';
25
+ }
26
+ type BackgroundSpec = {
27
+ type: 'solid';
28
+ color: string;
29
+ } | {
30
+ type: 'gradient';
31
+ stops: GradientStop[];
32
+ band?: BandSpec;
33
+ };
34
+ /** A vertical gradient stop (`at` 0 = top, 1 = bottom). */
35
+ interface GradientStop {
36
+ at: number;
37
+ color: string;
38
+ }
39
+ /** An optional solid band at the bottom (e.g. an aquarium seafloor). */
40
+ interface BandSpec {
41
+ color: string;
42
+ /** Band height in px. */
43
+ height: number;
44
+ }
45
+ interface LayerSpec {
46
+ count: number;
47
+ sprite: SpriteSpec;
48
+ /** Sprite size range in px (font size for emoji/text; ignored for circle). */
49
+ size?: [number, number];
50
+ motion: MotionSpec;
51
+ /** Wrap around the opposite edge when leaving the viewport. Default true. */
52
+ wrap?: boolean;
53
+ /** Flip the sprite horizontally to face its heading. Default false. */
54
+ flip?: boolean;
55
+ }
56
+ type SpriteSpec = {
57
+ kind: 'emoji';
58
+ glyphs: string[];
59
+ } | {
60
+ kind: 'text';
61
+ strings: string[];
62
+ color?: string;
63
+ font?: string;
64
+ } | {
65
+ kind: 'circle';
66
+ radius: [number, number];
67
+ color: string;
68
+ };
69
+ type MotionSpec =
70
+ /**
71
+ * Drift at a per-entity speed picked from `speed` (px/sec) along `angle` degrees
72
+ * (0 = right, 90 = down; default 0). `bidirectional` randomly flips horizontal
73
+ * direction per entity (e.g. fish swimming both ways); `bob` adds a small vertical
74
+ * wobble amplitude (px). Covers horizontal fields, diagonals (toasters) and rain
75
+ * (angle 90).
76
+ */
77
+ {
78
+ type: 'drift';
79
+ speed: [number, number];
80
+ angle?: number;
81
+ bidirectional?: boolean;
82
+ bob?: number;
83
+ }
84
+ /** Rise upward (px/sec) with an optional horizontal sway amplitude (px) — bubbles. */
85
+ | {
86
+ type: 'rise';
87
+ speed: [number, number];
88
+ sway?: number;
89
+ }
90
+ /** Bounce diagonally at a per-entity speed, reflecting off the edges (px/sec). */
91
+ | {
92
+ type: 'bounce';
93
+ speed: [number, number];
94
+ };
95
+ /** A validation problem, pointing at a JSON path within the spec. */
96
+ interface SpecError {
97
+ path: string;
98
+ message: string;
99
+ }
100
+ interface ValidationResult {
101
+ valid: boolean;
102
+ errors: SpecError[];
103
+ }
104
+ /** Perf/safety caps enforced by `validateSpec`. */
105
+ declare const LIMITS: {
106
+ readonly maxPerLayer: 400;
107
+ readonly maxTotal: 800;
108
+ readonly maxLayers: 8;
109
+ readonly maxSpeed: 4000;
110
+ };
111
+
112
+ /**
113
+ * Validate an untrusted (agent- or JSON-authored) spec structurally + semantically.
114
+ * Returns typed errors; never throws. `compileSaver` refuses to run an invalid spec.
115
+ */
116
+ declare function validateSpec(spec: unknown): ValidationResult;
117
+ /** Narrowing helper: validate + cast. Throws with a joined message when invalid. */
118
+ declare function assertValidSpec(spec: unknown): SaverSpec;
119
+
120
+ /** A seeded entity: initial state + resolved velocity. `positionAt` is a pure function
121
+ * of this + time, so the rAF loop and renderFrame(t) produce identical results. */
122
+ interface Entity {
123
+ x0: number;
124
+ y0: number;
125
+ size: number;
126
+ spriteIndex: number;
127
+ phase: number;
128
+ vx: number;
129
+ vy: number;
130
+ bob: number;
131
+ motion: 'drift' | 'rise' | 'bounce';
132
+ headingLeft: boolean;
133
+ }
134
+ interface Placed {
135
+ x: number;
136
+ y: number;
137
+ flip: boolean;
138
+ }
139
+ declare function spriteVariants(sprite: SpriteSpec): number;
140
+ /** Deterministically place a layer's entities using the seeded RNG (never Math.random). */
141
+ declare function buildEntities(layer: LayerSpec, rng: Rng, w: number, h: number): Entity[];
142
+ /** Analytic position of an entity at logical time `t` (ms). Pure & deterministic. */
143
+ declare function positionAt(e: Entity, t: number, w: number, h: number): Placed;
144
+
145
+ /** Derive a manifest so a compiled spec composes with @idle-screens/capabilities. */
146
+ declare function manifestFor(spec: SaverSpec): SaverManifest;
147
+ /**
148
+ * Compile a declarative spec into a runnable SaverPlugin. Throws if the spec is invalid
149
+ * (so an agent-authored spec is validated before it can run). The result is seeded,
150
+ * deterministic, `renderFrame(t,seed)`-addressable, and flash-safe by construction.
151
+ */
152
+ declare function compileSaver(spec: unknown): SaverPlugin;
153
+
154
+ /**
155
+ * Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test
156
+ * that the schema is actually expressive: two layers (fish drifting both ways at varied
157
+ * speeds with a bob + flip, over a gradient with a seafloor band; bubbles rising with sway).
158
+ */
159
+ declare const AQUARIUM_SPEC: SaverSpec;
160
+ /** Rain: dense text streaks falling straight down over a near-black sky. */
161
+ declare const RAIN_SPEC: SaverSpec;
162
+ declare const EXAMPLE_SPECS: SaverSpec[];
163
+
164
+ export { AQUARIUM_SPEC, type BackgroundSpec, type BandSpec, EXAMPLE_SPECS, type Entity, type GradientStop, LIMITS, type LayerSpec, type MotionSpec, type Placed, RAIN_SPEC, SCHEMA_VERSION, type SaverSpec, type SpecError, type SpriteSpec, type ValidationResult, assertValidSpec, buildEntities, compileSaver, manifestFor, positionAt, spriteVariants, validateSpec };
package/dist/index.js ADDED
@@ -0,0 +1,432 @@
1
+ // src/types.ts
2
+ var SCHEMA_VERSION = 1;
3
+ var LIMITS = {
4
+ maxPerLayer: 400,
5
+ maxTotal: 800,
6
+ maxLayers: 8,
7
+ maxSpeed: 4e3
8
+ // px/sec — bounds motion so nothing teleports
9
+ };
10
+
11
+ // src/validate.ts
12
+ var HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
13
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
14
+ var isStr = (v) => typeof v === "string";
15
+ var isNum = (v) => typeof v === "number" && Number.isFinite(v);
16
+ var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
17
+ function validateSpec(spec) {
18
+ const errors = [];
19
+ const err = (path, message) => void errors.push({ path, message });
20
+ if (!isObj(spec)) return { valid: false, errors: [{ path: "", message: "spec must be an object" }] };
21
+ if (spec.schemaVersion !== SCHEMA_VERSION) err("schemaVersion", `must be ${SCHEMA_VERSION}`);
22
+ if (!isStr(spec.id) || spec.id.trim() === "") err("id", "must be a non-empty string");
23
+ if (!isStr(spec.label) || spec.label.trim() === "") err("label", "must be a non-empty string");
24
+ if (spec.seed !== void 0 && !isNum(spec.seed)) err("seed", "must be a number");
25
+ if (spec.motionIntensity !== void 0 && !["calm", "moderate", "energetic"].includes(spec.motionIntensity)) {
26
+ err("motionIntensity", "must be calm | moderate | energetic");
27
+ }
28
+ if (spec.background !== void 0) validateBackground(spec.background, err);
29
+ if (!Array.isArray(spec.layers) || spec.layers.length === 0) {
30
+ err("layers", "must be a non-empty array");
31
+ } else {
32
+ if (spec.layers.length > LIMITS.maxLayers) err("layers", `at most ${LIMITS.maxLayers} layers`);
33
+ let total = 0;
34
+ spec.layers.forEach((layer, i) => {
35
+ total += isObj(layer) && isNum(layer.count) ? layer.count : 0;
36
+ validateLayer(layer, `layers[${i}]`, err);
37
+ });
38
+ if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
39
+ }
40
+ return { valid: errors.length === 0, errors };
41
+ }
42
+ function color(v, path, err) {
43
+ if (!isStr(v) || !HEX.test(v)) err(path, "must be a hex colour like #0a3a52");
44
+ }
45
+ function validateBackground(bg, err) {
46
+ if (!isObj(bg)) return err("background", "must be an object");
47
+ if (bg.type === "solid") {
48
+ color(bg.color, "background.color", err);
49
+ } else if (bg.type === "gradient") {
50
+ if (!Array.isArray(bg.stops) || bg.stops.length < 2) {
51
+ err("background.stops", "gradient needs at least 2 stops");
52
+ } else {
53
+ bg.stops.forEach((s, i) => {
54
+ if (!isObj(s) || !isNum(s.at) || s.at < 0 || s.at > 1) err(`background.stops[${i}].at`, "must be 0..1");
55
+ color(isObj(s) ? s.color : void 0, `background.stops[${i}].color`, err);
56
+ });
57
+ }
58
+ if (bg.band !== void 0) {
59
+ if (!isObj(bg.band)) err("background.band", "must be an object");
60
+ else {
61
+ color(bg.band.color, "background.band.color", err);
62
+ if (!isNum(bg.band.height) || bg.band.height <= 0) err("background.band.height", "must be > 0");
63
+ }
64
+ }
65
+ } else {
66
+ err("background.type", "must be solid | gradient");
67
+ }
68
+ }
69
+ function validateLayer(layer, path, err) {
70
+ if (!isObj(layer)) return err(path, "must be an object");
71
+ if (!isNum(layer.count) || !Number.isInteger(layer.count) || layer.count < 1) {
72
+ err(`${path}.count`, "must be a positive integer");
73
+ } else if (layer.count > LIMITS.maxPerLayer) {
74
+ err(`${path}.count`, `at most ${LIMITS.maxPerLayer} per layer`);
75
+ }
76
+ if (layer.size !== void 0 && (!isRange(layer.size) || layer.size[0] <= 0)) {
77
+ err(`${path}.size`, "must be a [min,max] range of positive px");
78
+ }
79
+ if (layer.wrap !== void 0 && typeof layer.wrap !== "boolean") err(`${path}.wrap`, "must be a boolean");
80
+ if (layer.flip !== void 0 && typeof layer.flip !== "boolean") err(`${path}.flip`, "must be a boolean");
81
+ validateSprite(layer.sprite, `${path}.sprite`, err);
82
+ validateMotion(layer.motion, `${path}.motion`, err);
83
+ }
84
+ function validateSprite(sprite, path, err) {
85
+ if (!isObj(sprite)) return err(path, "must be an object");
86
+ if (sprite.kind === "emoji") {
87
+ if (!Array.isArray(sprite.glyphs) || sprite.glyphs.length === 0 || !sprite.glyphs.every(isStr)) {
88
+ err(`${path}.glyphs`, "must be a non-empty array of strings");
89
+ }
90
+ } else if (sprite.kind === "text") {
91
+ if (!Array.isArray(sprite.strings) || sprite.strings.length === 0 || !sprite.strings.every(isStr)) {
92
+ err(`${path}.strings`, "must be a non-empty array of strings");
93
+ }
94
+ if (sprite.color !== void 0) color(sprite.color, `${path}.color`, err);
95
+ } else if (sprite.kind === "circle") {
96
+ if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
97
+ color(sprite.color, `${path}.color`, err);
98
+ } else {
99
+ err(`${path}.kind`, "must be emoji | text | circle");
100
+ }
101
+ }
102
+ function validateMotion(motion, path, err) {
103
+ if (!isObj(motion)) return err(path, "must be an object");
104
+ const speedOk = (v, p) => {
105
+ if (!isRange(v)) err(p, "must be a [min,max] range");
106
+ else if (v[1] > LIMITS.maxSpeed) err(p, `speed exceeds cap ${LIMITS.maxSpeed} px/sec`);
107
+ };
108
+ if (motion.type === "drift") {
109
+ speedOk(motion.speed, `${path}.speed`);
110
+ if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
111
+ if (motion.bidirectional !== void 0 && typeof motion.bidirectional !== "boolean") err(`${path}.bidirectional`, "must be a boolean");
112
+ if (motion.bob !== void 0 && !isNum(motion.bob)) err(`${path}.bob`, "must be a number");
113
+ } else if (motion.type === "rise") {
114
+ speedOk(motion.speed, `${path}.speed`);
115
+ if (motion.sway !== void 0 && !isNum(motion.sway)) err(`${path}.sway`, "must be a number");
116
+ } else if (motion.type === "bounce") {
117
+ speedOk(motion.speed, `${path}.speed`);
118
+ } else {
119
+ err(`${path}.type`, "must be drift | rise | bounce");
120
+ }
121
+ }
122
+ function assertValidSpec(spec) {
123
+ const r = validateSpec(spec);
124
+ if (!r.valid) {
125
+ throw new Error(`invalid saver spec:
126
+ ${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
127
+ }
128
+ return spec;
129
+ }
130
+
131
+ // src/simulate.ts
132
+ function spriteVariants(sprite) {
133
+ if (sprite.kind === "emoji") return sprite.glyphs.length;
134
+ if (sprite.kind === "text") return sprite.strings.length;
135
+ return 1;
136
+ }
137
+ function buildEntities(layer, rng, w, h) {
138
+ const [smin, smax] = layer.size ?? [20, 40];
139
+ const variants = spriteVariants(layer.sprite);
140
+ const out = [];
141
+ for (let i = 0; i < layer.count; i++) {
142
+ const size = layer.sprite.kind === "circle" ? rng.range(layer.sprite.radius[0], layer.sprite.radius[1]) * 2 : rng.range(smin, smax);
143
+ let vx = 0;
144
+ let vy = 0;
145
+ let bob = 0;
146
+ let motion = "drift";
147
+ const m = layer.motion;
148
+ if (m.type === "drift") {
149
+ const s = rng.range(m.speed[0], m.speed[1]);
150
+ let angle = (m.angle ?? 0) * Math.PI / 180;
151
+ if (m.bidirectional && rng.next() < 0.5) angle = Math.PI - angle;
152
+ vx = s * Math.cos(angle);
153
+ vy = s * Math.sin(angle);
154
+ bob = m.bob ?? 0;
155
+ } else if (m.type === "rise") {
156
+ motion = "rise";
157
+ vy = -rng.range(m.speed[0], m.speed[1]);
158
+ bob = m.sway ?? 0;
159
+ } else {
160
+ motion = "bounce";
161
+ const s = rng.range(m.speed[0], m.speed[1]);
162
+ const a = rng.range(0, Math.PI * 2);
163
+ vx = s * Math.cos(a);
164
+ vy = s * Math.sin(a);
165
+ }
166
+ out.push({
167
+ x0: rng.range(0, w),
168
+ y0: rng.range(0, h),
169
+ size,
170
+ spriteIndex: variants > 1 ? rng.int(0, variants - 1) : 0,
171
+ phase: rng.range(0, Math.PI * 2),
172
+ vx,
173
+ vy,
174
+ bob,
175
+ motion,
176
+ headingLeft: vx < 0
177
+ });
178
+ }
179
+ return out;
180
+ }
181
+ function wrap(v, min, max) {
182
+ const range = max - min;
183
+ return ((v - min) % range + range) % range + min;
184
+ }
185
+ function reflect(v, min, max) {
186
+ const range = max - min;
187
+ if (range <= 0) return min;
188
+ const period = 2 * range;
189
+ const p = ((v - min) % period + period) % period;
190
+ return min + (p < range ? p : period - p);
191
+ }
192
+ function positionAt(e, t, w, h) {
193
+ const dt = t / 1e3;
194
+ const m = e.size;
195
+ if (e.motion === "bounce") {
196
+ return {
197
+ x: reflect(e.x0 + e.vx * dt, m / 2, w - m / 2),
198
+ y: reflect(e.y0 + e.vy * dt, m / 2, h - m / 2),
199
+ flip: false
200
+ };
201
+ }
202
+ if (e.motion === "rise") {
203
+ return {
204
+ x: e.x0 + (e.bob ? e.bob * Math.sin(t / 700 + e.phase) : 0),
205
+ y: wrap(e.y0 + e.vy * dt, -m, h + m),
206
+ flip: false
207
+ };
208
+ }
209
+ const x = wrap(e.x0 + e.vx * dt, -m, w + m);
210
+ let y = e.vy !== 0 ? wrap(e.y0 + e.vy * dt, -m, h + m) : e.y0;
211
+ if (e.bob) y += e.bob * Math.sin(t / 500 + e.phase);
212
+ return { x, y, flip: e.headingLeft };
213
+ }
214
+
215
+ // src/compile.ts
216
+ import {
217
+ createRng
218
+ } from "@idle-screens/core";
219
+ function manifestFor(spec) {
220
+ const total = spec.layers.reduce((n, l) => n + l.count, 0);
221
+ const costTier = total < 30 ? "idle" : total < 150 ? "low" : total < 400 ? "medium" : "high";
222
+ return {
223
+ id: spec.id,
224
+ label: spec.label,
225
+ passthrough: false,
226
+ minBackend: "canvas2d",
227
+ costTier,
228
+ motionIntensity: spec.motionIntensity ?? "moderate",
229
+ reducedMotionFallback: "static",
230
+ // Flash-safe by construction: static background + bounded sprites, no strobe
231
+ // primitive. Proven by sampling a compiled spec through @idle-screens/validator.
232
+ a11y: { flashSafe: true }
233
+ };
234
+ }
235
+ var SpecInstance = class {
236
+ constructor(spec, ctx) {
237
+ this.spec = spec;
238
+ 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);
243
+ this.canvas = canvas;
244
+ const c2d = canvas.getContext("2d", { alpha: false });
245
+ if (!c2d) throw new Error("schema saver: no 2d context");
246
+ this.ctx = c2d;
247
+ this.w = ctx.width;
248
+ this.h = ctx.height;
249
+ this.sizeCanvas();
250
+ this.rebuild();
251
+ this.paused = ctx.reducedMotion;
252
+ if (this.paused) this.renderFrame(0, this.seed);
253
+ else this.start();
254
+ }
255
+ spec;
256
+ canvas;
257
+ ctx;
258
+ seed;
259
+ w;
260
+ h;
261
+ layers = [];
262
+ frameId = null;
263
+ paused = false;
264
+ startT = 0;
265
+ sizeCanvas() {
266
+ const dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 2);
267
+ this.canvas.width = Math.max(1, Math.round(this.w * dpr));
268
+ this.canvas.height = Math.max(1, Math.round(this.h * dpr));
269
+ this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
270
+ }
271
+ /** (Re)seed and place all entities — deterministic for the seed + size. */
272
+ rebuild() {
273
+ const rng = createRng(this.seed);
274
+ this.layers = this.spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, this.w, this.h) }));
275
+ }
276
+ start() {
277
+ if (this.frameId !== null || typeof requestAnimationFrame === "undefined") return;
278
+ this.startT = 0;
279
+ this.frameId = requestAnimationFrame((now) => this.loop(now));
280
+ }
281
+ stop() {
282
+ if (this.frameId !== null) {
283
+ cancelAnimationFrame(this.frameId);
284
+ this.frameId = null;
285
+ }
286
+ }
287
+ loop(now) {
288
+ this.frameId = requestAnimationFrame((n) => this.loop(n));
289
+ if (this.startT === 0) this.startT = now;
290
+ this.renderFrame(now - this.startT, this.seed);
291
+ }
292
+ drawBackground() {
293
+ const { ctx, w, h } = this;
294
+ const bg = this.spec.background;
295
+ if (!bg || bg.type === "solid") {
296
+ ctx.fillStyle = bg?.color ?? "#05050a";
297
+ ctx.fillRect(0, 0, w, h);
298
+ return;
299
+ }
300
+ const g = ctx.createLinearGradient(0, 0, 0, h);
301
+ for (const s of bg.stops) g.addColorStop(Math.max(0, Math.min(1, s.at)), s.color);
302
+ ctx.fillStyle = g;
303
+ ctx.fillRect(0, 0, w, h);
304
+ if (bg.band) {
305
+ ctx.fillStyle = bg.band.color;
306
+ ctx.fillRect(0, h - bg.band.height, w, bg.band.height);
307
+ }
308
+ }
309
+ drawEntity(built, e, t) {
310
+ const { ctx } = this;
311
+ const p = positionAt(e, t, this.w, this.h);
312
+ const sprite = built.layer.sprite;
313
+ if (sprite.kind === "circle") {
314
+ ctx.fillStyle = sprite.color;
315
+ ctx.beginPath();
316
+ ctx.arc(p.x, p.y, e.size / 2, 0, Math.PI * 2);
317
+ ctx.fill();
318
+ return;
319
+ }
320
+ ctx.save();
321
+ ctx.translate(p.x, p.y);
322
+ if (p.flip && built.layer.flip) ctx.scale(-1, 1);
323
+ ctx.textAlign = "center";
324
+ ctx.textBaseline = "middle";
325
+ if (sprite.kind === "emoji") {
326
+ ctx.font = `${e.size}px serif`;
327
+ ctx.fillText(sprite.glyphs[e.spriteIndex] ?? sprite.glyphs[0], 0, 0);
328
+ } else {
329
+ ctx.font = sprite.font ?? `${e.size}px system-ui, sans-serif`;
330
+ ctx.fillStyle = sprite.color ?? "#e6e8ef";
331
+ ctx.fillText(sprite.strings[e.spriteIndex] ?? sprite.strings[0], 0, 0);
332
+ }
333
+ ctx.restore();
334
+ }
335
+ /** Deterministic, frame-addressable render (shared by the rAF loop). */
336
+ renderFrame(t, _seed) {
337
+ this.drawBackground();
338
+ for (const built of this.layers) {
339
+ for (const e of built.entities) this.drawEntity(built, e, t);
340
+ }
341
+ }
342
+ setPaused(paused) {
343
+ this.paused = paused;
344
+ if (paused) {
345
+ this.stop();
346
+ } else {
347
+ this.start();
348
+ }
349
+ }
350
+ resize(width, height) {
351
+ this.w = width;
352
+ this.h = height;
353
+ this.sizeCanvas();
354
+ this.rebuild();
355
+ if (this.paused) this.renderFrame(0, this.seed);
356
+ }
357
+ dispose() {
358
+ this.stop();
359
+ this.canvas.remove();
360
+ }
361
+ };
362
+ function compileSaver(spec) {
363
+ const valid = assertValidSpec(spec);
364
+ return {
365
+ manifest: manifestFor(valid),
366
+ mount: (ctx) => new SpecInstance(valid, ctx)
367
+ };
368
+ }
369
+
370
+ // src/examples.ts
371
+ var AQUARIUM_SPEC = {
372
+ schemaVersion: 1,
373
+ id: "aquarium",
374
+ label: "Aquarium (declarative)",
375
+ seed: 42,
376
+ motionIntensity: "calm",
377
+ background: {
378
+ type: "gradient",
379
+ stops: [
380
+ { at: 0, color: "#0a3a52" },
381
+ { at: 0.6, color: "#062534" },
382
+ { at: 1, color: "#02141d" }
383
+ ],
384
+ band: { color: "#0b1a12", height: 24 }
385
+ },
386
+ layers: [
387
+ {
388
+ count: 14,
389
+ sprite: { kind: "emoji", glyphs: ["\u{1F41F}", "\u{1F420}", "\u{1F421}", "\u{1F988}", "\u{1F419}"] },
390
+ size: [34, 72],
391
+ motion: { type: "drift", speed: [30, 110], bidirectional: true, bob: 6 },
392
+ flip: true
393
+ },
394
+ {
395
+ count: 22,
396
+ sprite: { kind: "circle", radius: [2, 7], color: "#c8ebff" },
397
+ motion: { type: "rise", speed: [24, 78], sway: 8 }
398
+ }
399
+ ]
400
+ };
401
+ var RAIN_SPEC = {
402
+ schemaVersion: 1,
403
+ id: "rain",
404
+ label: "Rain (declarative)",
405
+ seed: 7,
406
+ motionIntensity: "moderate",
407
+ background: { type: "solid", color: "#05070a" },
408
+ layers: [
409
+ {
410
+ count: 140,
411
+ sprite: { kind: "text", strings: ["\u2502"], color: "#9fd0ff" },
412
+ size: [12, 26],
413
+ motion: { type: "drift", angle: 90, speed: [520, 900] }
414
+ }
415
+ ]
416
+ };
417
+ var EXAMPLE_SPECS = [AQUARIUM_SPEC, RAIN_SPEC];
418
+ export {
419
+ AQUARIUM_SPEC,
420
+ EXAMPLE_SPECS,
421
+ LIMITS,
422
+ RAIN_SPEC,
423
+ SCHEMA_VERSION,
424
+ assertValidSpec,
425
+ buildEntities,
426
+ compileSaver,
427
+ manifestFor,
428
+ positionAt,
429
+ spriteVariants,
430
+ validateSpec
431
+ };
432
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/validate.ts","../src/simulate.ts","../src/compile.ts","../src/examples.ts"],"sourcesContent":["/**\n * Declarative, agent-authorable saver format. A `SaverSpec` describes a saver as DATA\n * (no code), which `compileSaver` turns into a seeded, deterministic, flash-safe\n * SaverPlugin. It models the \"drifting sprite field\" family that covers most classic\n * savers (fish, toasters, DVD, rain, messages): a static background plus one or more\n * layers of entities that move.\n *\n * SAFETY INVARIANT: there is deliberately NO full-field strobe/flash primitive — the\n * background is static and entities are bounded sprites — so a compiled spec cannot\n * produce photosensitive flashing by construction (proven by sampling a compiled spec\n * through @idle-screens/validator).\n */\nexport const SCHEMA_VERSION = 1 as const;\n\nexport interface SaverSpec {\n schemaVersion: 1;\n id: string;\n label: string;\n /** Deterministic seed; falls back to the host's SaverContext.seed. */\n seed?: number;\n background?: BackgroundSpec;\n layers: LayerSpec[];\n motionIntensity?: 'calm' | 'moderate' | 'energetic';\n}\n\nexport type BackgroundSpec =\n | { type: 'solid'; color: string }\n | { type: 'gradient'; stops: GradientStop[]; band?: BandSpec };\n\n/** A vertical gradient stop (`at` 0 = top, 1 = bottom). */\nexport interface GradientStop {\n at: number;\n color: string;\n}\n\n/** An optional solid band at the bottom (e.g. an aquarium seafloor). */\nexport interface BandSpec {\n color: string;\n /** Band height in px. */\n height: number;\n}\n\nexport interface LayerSpec {\n count: number;\n sprite: SpriteSpec;\n /** Sprite size range in px (font size for emoji/text; ignored for circle). */\n size?: [number, number];\n motion: MotionSpec;\n /** Wrap around the opposite edge when leaving the viewport. Default true. */\n wrap?: boolean;\n /** Flip the sprite horizontally to face its heading. Default false. */\n flip?: boolean;\n}\n\nexport type SpriteSpec =\n | { kind: 'emoji'; glyphs: string[] }\n | { kind: 'text'; strings: string[]; color?: string; font?: string }\n | { kind: 'circle'; radius: [number, number]; color: string };\n\nexport type MotionSpec =\n /**\n * Drift at a per-entity speed picked from `speed` (px/sec) along `angle` degrees\n * (0 = right, 90 = down; default 0). `bidirectional` randomly flips horizontal\n * direction per entity (e.g. fish swimming both ways); `bob` adds a small vertical\n * wobble amplitude (px). Covers horizontal fields, diagonals (toasters) and rain\n * (angle 90).\n */\n | { type: 'drift'; speed: [number, number]; angle?: number; bidirectional?: boolean; bob?: number }\n /** Rise upward (px/sec) with an optional horizontal sway amplitude (px) — bubbles. */\n | { type: 'rise'; speed: [number, number]; sway?: number }\n /** Bounce diagonally at a per-entity speed, reflecting off the edges (px/sec). */\n | { type: 'bounce'; speed: [number, number] };\n\n/** A validation problem, pointing at a JSON path within the spec. */\nexport interface SpecError {\n path: string;\n message: string;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n errors: SpecError[];\n}\n\n/** Perf/safety caps enforced by `validateSpec`. */\nexport const LIMITS = {\n maxPerLayer: 400,\n maxTotal: 800,\n maxLayers: 8,\n maxSpeed: 4000, // px/sec — bounds motion so nothing teleports\n} as const;\n","import { LIMITS, SCHEMA_VERSION, type SaverSpec, type SpecError, type ValidationResult } from './types';\n\nconst HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;\nconst isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);\nconst isStr = (v: unknown): v is string => typeof v === 'string';\nconst isNum = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);\nconst isRange = (v: unknown): v is [number, number] =>\n Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];\n\n/**\n * Validate an untrusted (agent- or JSON-authored) spec structurally + semantically.\n * Returns typed errors; never throws. `compileSaver` refuses to run an invalid spec.\n */\nexport function validateSpec(spec: unknown): ValidationResult {\n const errors: SpecError[] = [];\n const err = (path: string, message: string): void => void errors.push({ path, message });\n\n if (!isObj(spec)) return { valid: false, errors: [{ path: '', message: 'spec must be an object' }] };\n\n if (spec.schemaVersion !== SCHEMA_VERSION) err('schemaVersion', `must be ${SCHEMA_VERSION}`);\n if (!isStr(spec.id) || spec.id.trim() === '') err('id', 'must be a non-empty string');\n if (!isStr(spec.label) || spec.label.trim() === '') err('label', 'must be a non-empty string');\n if (spec.seed !== undefined && !isNum(spec.seed)) err('seed', 'must be a number');\n if (spec.motionIntensity !== undefined && !['calm', 'moderate', 'energetic'].includes(spec.motionIntensity as string)) {\n err('motionIntensity', 'must be calm | moderate | energetic');\n }\n\n if (spec.background !== undefined) validateBackground(spec.background, err);\n\n if (!Array.isArray(spec.layers) || spec.layers.length === 0) {\n err('layers', 'must be a non-empty array');\n } else {\n if (spec.layers.length > LIMITS.maxLayers) err('layers', `at most ${LIMITS.maxLayers} layers`);\n let total = 0;\n spec.layers.forEach((layer, i) => {\n total += isObj(layer) && isNum(layer.count) ? layer.count : 0;\n validateLayer(layer, `layers[${i}]`, err);\n });\n if (total > LIMITS.maxTotal) err('layers', `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);\n }\n\n return { valid: errors.length === 0, errors };\n}\n\nfunction color(v: unknown, path: string, err: (p: string, m: string) => void): void {\n if (!isStr(v) || !HEX.test(v)) err(path, 'must be a hex colour like #0a3a52');\n}\n\nfunction validateBackground(bg: unknown, err: (p: string, m: string) => void): void {\n if (!isObj(bg)) return err('background', 'must be an object');\n if (bg.type === 'solid') {\n color(bg.color, 'background.color', err);\n } else if (bg.type === 'gradient') {\n if (!Array.isArray(bg.stops) || bg.stops.length < 2) {\n err('background.stops', 'gradient needs at least 2 stops');\n } else {\n bg.stops.forEach((s, i) => {\n if (!isObj(s) || !isNum(s.at) || s.at < 0 || s.at > 1) err(`background.stops[${i}].at`, 'must be 0..1');\n color(isObj(s) ? s.color : undefined, `background.stops[${i}].color`, err);\n });\n }\n if (bg.band !== undefined) {\n if (!isObj(bg.band)) err('background.band', 'must be an object');\n else {\n color(bg.band.color, 'background.band.color', err);\n if (!isNum(bg.band.height) || bg.band.height <= 0) err('background.band.height', 'must be > 0');\n }\n }\n } else {\n err('background.type', 'must be solid | gradient');\n }\n}\n\nfunction validateLayer(layer: unknown, path: string, err: (p: string, m: string) => void): void {\n if (!isObj(layer)) return err(path, 'must be an object');\n if (!isNum(layer.count) || !Number.isInteger(layer.count) || layer.count < 1) {\n err(`${path}.count`, 'must be a positive integer');\n } else if (layer.count > LIMITS.maxPerLayer) {\n err(`${path}.count`, `at most ${LIMITS.maxPerLayer} per layer`);\n }\n if (layer.size !== undefined && (!isRange(layer.size) || layer.size[0] <= 0)) {\n err(`${path}.size`, 'must be a [min,max] range of positive px');\n }\n if (layer.wrap !== undefined && typeof layer.wrap !== 'boolean') err(`${path}.wrap`, 'must be a boolean');\n if (layer.flip !== undefined && typeof layer.flip !== 'boolean') err(`${path}.flip`, 'must be a boolean');\n\n validateSprite(layer.sprite, `${path}.sprite`, err);\n validateMotion(layer.motion, `${path}.motion`, err);\n}\n\nfunction validateSprite(sprite: unknown, path: string, err: (p: string, m: string) => void): void {\n if (!isObj(sprite)) return err(path, 'must be an object');\n if (sprite.kind === 'emoji') {\n if (!Array.isArray(sprite.glyphs) || sprite.glyphs.length === 0 || !sprite.glyphs.every(isStr)) {\n err(`${path}.glyphs`, 'must be a non-empty array of strings');\n }\n } else if (sprite.kind === 'text') {\n if (!Array.isArray(sprite.strings) || sprite.strings.length === 0 || !sprite.strings.every(isStr)) {\n err(`${path}.strings`, 'must be a non-empty array of strings');\n }\n if (sprite.color !== undefined) color(sprite.color, `${path}.color`, err);\n } else if (sprite.kind === 'circle') {\n if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, 'must be a [min,max] range of positive px');\n color(sprite.color, `${path}.color`, err);\n } else {\n err(`${path}.kind`, 'must be emoji | text | circle');\n }\n}\n\nfunction validateMotion(motion: unknown, path: string, err: (p: string, m: string) => void): void {\n if (!isObj(motion)) return err(path, 'must be an object');\n const speedOk = (v: unknown, p: string): void => {\n if (!isRange(v)) err(p, 'must be a [min,max] range');\n else if (v[1] > LIMITS.maxSpeed) err(p, `speed exceeds cap ${LIMITS.maxSpeed} px/sec`);\n };\n if (motion.type === 'drift') {\n speedOk(motion.speed, `${path}.speed`);\n if (motion.angle !== undefined && !isNum(motion.angle)) err(`${path}.angle`, 'must be a number (degrees)');\n if (motion.bidirectional !== undefined && typeof motion.bidirectional !== 'boolean') err(`${path}.bidirectional`, 'must be a boolean');\n if (motion.bob !== undefined && !isNum(motion.bob)) err(`${path}.bob`, 'must be a number');\n } else if (motion.type === 'rise') {\n speedOk(motion.speed, `${path}.speed`);\n if (motion.sway !== undefined && !isNum(motion.sway)) err(`${path}.sway`, 'must be a number');\n } else if (motion.type === 'bounce') {\n speedOk(motion.speed, `${path}.speed`);\n } else {\n err(`${path}.type`, 'must be drift | rise | bounce');\n }\n}\n\n/** Narrowing helper: validate + cast. Throws with a joined message when invalid. */\nexport function assertValidSpec(spec: unknown): SaverSpec {\n const r = validateSpec(spec);\n if (!r.valid) {\n throw new Error(`invalid saver spec:\\n${r.errors.map((e) => ` ${e.path || '<root>'}: ${e.message}`).join('\\n')}`);\n }\n return spec as SaverSpec;\n}\n","import type { Rng } from '@idle-screens/core';\nimport type { LayerSpec, SpriteSpec } from './types';\n\n/** A seeded entity: initial state + resolved velocity. `positionAt` is a pure function\n * of this + time, so the rAF loop and renderFrame(t) produce identical results. */\nexport interface Entity {\n x0: number;\n y0: number;\n size: number;\n spriteIndex: number;\n phase: number;\n vx: number; // px/sec\n vy: number; // px/sec\n bob: number; // vertical bob / horizontal sway amplitude (px)\n motion: 'drift' | 'rise' | 'bounce';\n headingLeft: boolean;\n}\n\nexport interface Placed {\n x: number;\n y: number;\n flip: boolean;\n}\n\nexport function spriteVariants(sprite: SpriteSpec): number {\n if (sprite.kind === 'emoji') return sprite.glyphs.length;\n if (sprite.kind === 'text') return sprite.strings.length;\n return 1;\n}\n\n/** Deterministically place a layer's entities using the seeded RNG (never Math.random). */\nexport function buildEntities(layer: LayerSpec, rng: Rng, w: number, h: number): Entity[] {\n const [smin, smax] = layer.size ?? [20, 40];\n const variants = spriteVariants(layer.sprite);\n const out: Entity[] = [];\n for (let i = 0; i < layer.count; i++) {\n const size =\n layer.sprite.kind === 'circle'\n ? rng.range(layer.sprite.radius[0], layer.sprite.radius[1]) * 2\n : rng.range(smin, smax);\n\n let vx = 0;\n let vy = 0;\n let bob = 0;\n let motion: Entity['motion'] = 'drift';\n const m = layer.motion;\n if (m.type === 'drift') {\n const s = rng.range(m.speed[0], m.speed[1]);\n let angle = ((m.angle ?? 0) * Math.PI) / 180;\n if (m.bidirectional && rng.next() < 0.5) angle = Math.PI - angle;\n vx = s * Math.cos(angle);\n vy = s * Math.sin(angle);\n bob = m.bob ?? 0;\n } else if (m.type === 'rise') {\n motion = 'rise';\n vy = -rng.range(m.speed[0], m.speed[1]); // upward\n bob = m.sway ?? 0;\n } else {\n motion = 'bounce';\n const s = rng.range(m.speed[0], m.speed[1]);\n const a = rng.range(0, Math.PI * 2);\n vx = s * Math.cos(a);\n vy = s * Math.sin(a);\n }\n\n out.push({\n x0: rng.range(0, w),\n y0: rng.range(0, h),\n size,\n spriteIndex: variants > 1 ? rng.int(0, variants - 1) : 0,\n phase: rng.range(0, Math.PI * 2),\n vx,\n vy,\n bob,\n motion,\n headingLeft: vx < 0,\n });\n }\n return out;\n}\n\nfunction wrap(v: number, min: number, max: number): number {\n const range = max - min;\n return ((((v - min) % range) + range) % range) + min;\n}\n\nfunction reflect(v: number, min: number, max: number): number {\n const range = max - min;\n if (range <= 0) return min;\n const period = 2 * range;\n const p = (((v - min) % period) + period) % period;\n return min + (p < range ? p : period - p);\n}\n\n/** Analytic position of an entity at logical time `t` (ms). Pure & deterministic. */\nexport function positionAt(e: Entity, t: number, w: number, h: number): Placed {\n const dt = t / 1000;\n const m = e.size;\n if (e.motion === 'bounce') {\n return {\n x: reflect(e.x0 + e.vx * dt, m / 2, w - m / 2),\n y: reflect(e.y0 + e.vy * dt, m / 2, h - m / 2),\n flip: false,\n };\n }\n if (e.motion === 'rise') {\n return {\n x: e.x0 + (e.bob ? e.bob * Math.sin(t / 700 + e.phase) : 0),\n y: wrap(e.y0 + e.vy * dt, -m, h + m),\n flip: false,\n };\n }\n // drift\n const x = wrap(e.x0 + e.vx * dt, -m, w + m);\n let y = e.vy !== 0 ? wrap(e.y0 + e.vy * dt, -m, h + m) : e.y0;\n if (e.bob) y += e.bob * Math.sin(t / 500 + e.phase);\n return { x, y, flip: e.headingLeft };\n}\n","import {\n createRng,\n type SaverContext,\n type SaverInstance,\n type SaverManifest,\n type SaverPlugin,\n} from '@idle-screens/core';\nimport { assertValidSpec } from './validate';\nimport { buildEntities, positionAt, type Entity } from './simulate';\nimport type { LayerSpec, SaverSpec } from './types';\n\n/** Derive a manifest so a compiled spec composes with @idle-screens/capabilities. */\nexport function manifestFor(spec: SaverSpec): SaverManifest {\n const total = spec.layers.reduce((n, l) => n + l.count, 0);\n const costTier = total < 30 ? 'idle' : total < 150 ? 'low' : total < 400 ? 'medium' : 'high';\n return {\n id: spec.id,\n label: spec.label,\n passthrough: false,\n minBackend: 'canvas2d',\n costTier,\n motionIntensity: spec.motionIntensity ?? 'moderate',\n reducedMotionFallback: 'static',\n // Flash-safe by construction: static background + bounded sprites, no strobe\n // primitive. Proven by sampling a compiled spec through @idle-screens/validator.\n a11y: { flashSafe: true },\n };\n}\n\ninterface Built {\n layer: LayerSpec;\n entities: Entity[];\n}\n\nclass SpecInstance implements SaverInstance {\n private readonly canvas: HTMLCanvasElement;\n private readonly ctx: CanvasRenderingContext2D;\n private readonly seed: number;\n private w: number;\n private h: number;\n private layers: Built[] = [];\n private frameId: number | null = null;\n private paused = false;\n private startT = 0;\n\n constructor(\n private readonly spec: SaverSpec,\n ctx: SaverContext,\n ) {\n this.seed = ((spec.seed ?? ctx.seed) >>> 0) || 1;\n const canvas = document.createElement('canvas');\n canvas.style.cssText = 'display:block;width:100%;height:100%';\n canvas.setAttribute('aria-hidden', 'true');\n ctx.host.appendChild(canvas);\n this.canvas = canvas;\n const c2d = canvas.getContext('2d', { alpha: false });\n if (!c2d) throw new Error('schema saver: no 2d context');\n this.ctx = c2d;\n\n this.w = ctx.width;\n this.h = ctx.height;\n this.sizeCanvas();\n this.rebuild();\n\n this.paused = ctx.reducedMotion;\n if (this.paused) this.renderFrame(0, this.seed);\n else this.start();\n }\n\n private sizeCanvas(): void {\n const dpr = Math.min(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, 2);\n this.canvas.width = Math.max(1, Math.round(this.w * dpr));\n this.canvas.height = Math.max(1, Math.round(this.h * dpr));\n this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n }\n\n /** (Re)seed and place all entities — deterministic for the seed + size. */\n private rebuild(): void {\n const rng = createRng(this.seed);\n this.layers = this.spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, this.w, this.h) }));\n }\n\n private start(): void {\n if (this.frameId !== null || typeof requestAnimationFrame === 'undefined') return;\n this.startT = 0;\n this.frameId = requestAnimationFrame((now) => this.loop(now));\n }\n\n private stop(): void {\n if (this.frameId !== null) {\n cancelAnimationFrame(this.frameId);\n this.frameId = null;\n }\n }\n\n private loop(now: number): void {\n this.frameId = requestAnimationFrame((n) => this.loop(n));\n if (this.startT === 0) this.startT = now;\n this.renderFrame(now - this.startT, this.seed);\n }\n\n private drawBackground(): void {\n const { ctx, w, h } = this;\n const bg = this.spec.background;\n if (!bg || bg.type === 'solid') {\n ctx.fillStyle = bg?.color ?? '#05050a';\n ctx.fillRect(0, 0, w, h);\n return;\n }\n const g = ctx.createLinearGradient(0, 0, 0, h);\n for (const s of bg.stops) g.addColorStop(Math.max(0, Math.min(1, s.at)), s.color);\n ctx.fillStyle = g;\n ctx.fillRect(0, 0, w, h);\n if (bg.band) {\n ctx.fillStyle = bg.band.color;\n ctx.fillRect(0, h - bg.band.height, w, bg.band.height);\n }\n }\n\n private drawEntity(built: Built, e: Entity, t: number): void {\n const { ctx } = this;\n const p = positionAt(e, t, this.w, this.h);\n const sprite = built.layer.sprite;\n if (sprite.kind === 'circle') {\n ctx.fillStyle = sprite.color;\n ctx.beginPath();\n ctx.arc(p.x, p.y, e.size / 2, 0, Math.PI * 2);\n ctx.fill();\n return;\n }\n ctx.save();\n ctx.translate(p.x, p.y);\n if (p.flip && built.layer.flip) ctx.scale(-1, 1);\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n if (sprite.kind === 'emoji') {\n ctx.font = `${e.size}px serif`;\n ctx.fillText(sprite.glyphs[e.spriteIndex] ?? sprite.glyphs[0]!, 0, 0);\n } else {\n ctx.font = sprite.font ?? `${e.size}px system-ui, sans-serif`;\n ctx.fillStyle = sprite.color ?? '#e6e8ef';\n ctx.fillText(sprite.strings[e.spriteIndex] ?? sprite.strings[0]!, 0, 0);\n }\n ctx.restore();\n }\n\n /** Deterministic, frame-addressable render (shared by the rAF loop). */\n renderFrame(t: number, _seed: number): void {\n this.drawBackground();\n for (const built of this.layers) {\n for (const e of built.entities) this.drawEntity(built, e, t);\n }\n }\n\n setPaused(paused: boolean): void {\n this.paused = paused;\n if (paused) {\n this.stop();\n } else {\n this.start();\n }\n }\n\n resize(width: number, height: number): void {\n this.w = width;\n this.h = height;\n this.sizeCanvas();\n this.rebuild();\n if (this.paused) this.renderFrame(0, this.seed);\n }\n\n dispose(): void {\n this.stop();\n this.canvas.remove();\n }\n}\n\n/**\n * Compile a declarative spec into a runnable SaverPlugin. Throws if the spec is invalid\n * (so an agent-authored spec is validated before it can run). The result is seeded,\n * deterministic, `renderFrame(t,seed)`-addressable, and flash-safe by construction.\n */\nexport function compileSaver(spec: unknown): SaverPlugin {\n const valid = assertValidSpec(spec);\n return {\n manifest: manifestFor(valid),\n mount: (ctx: SaverContext) => new SpecInstance(valid, ctx),\n };\n}\n","import type { SaverSpec } from './types';\n\n/**\n * Reproduces the hand-coded Fish Aquarium saver as pure DATA — the discriminating test\n * that the schema is actually expressive: two layers (fish drifting both ways at varied\n * speeds with a bob + flip, over a gradient with a seafloor band; bubbles rising with sway).\n */\nexport const AQUARIUM_SPEC: SaverSpec = {\n schemaVersion: 1,\n id: 'aquarium',\n label: 'Aquarium (declarative)',\n seed: 42,\n motionIntensity: 'calm',\n background: {\n type: 'gradient',\n stops: [\n { at: 0, color: '#0a3a52' },\n { at: 0.6, color: '#062534' },\n { at: 1, color: '#02141d' },\n ],\n band: { color: '#0b1a12', height: 24 },\n },\n layers: [\n {\n count: 14,\n sprite: { kind: 'emoji', glyphs: ['🐟', '🐠', '🐡', '🦈', '🐙'] },\n size: [34, 72],\n motion: { type: 'drift', speed: [30, 110], bidirectional: true, bob: 6 },\n flip: true,\n },\n {\n count: 22,\n sprite: { kind: 'circle', radius: [2, 7], color: '#c8ebff' },\n motion: { type: 'rise', speed: [24, 78], sway: 8 },\n },\n ],\n};\n\n/** Rain: dense text streaks falling straight down over a near-black sky. */\nexport const RAIN_SPEC: SaverSpec = {\n schemaVersion: 1,\n id: 'rain',\n label: 'Rain (declarative)',\n seed: 7,\n motionIntensity: 'moderate',\n background: { type: 'solid', color: '#05070a' },\n layers: [\n {\n count: 140,\n sprite: { kind: 'text', strings: ['│'], color: '#9fd0ff' },\n size: [12, 26],\n motion: { type: 'drift', angle: 90, speed: [520, 900] },\n },\n ],\n};\n\nexport const EXAMPLE_SPECS: SaverSpec[] = [AQUARIUM_SPEC, RAIN_SPEC];\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@idle-screens/schema",
3
+ "version": "0.1.0",
4
+ "description": "Declarative, agent-authorable saver format for idle-screens: validate a data spec, then compile it into a runnable, seeded, flash-safe SaverPlugin.",
5
+ "license": "MIT",
6
+ "author": "Shawn Partridge",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/shaaaaawn/idle-screens",
10
+ "directory": "packages/schema"
11
+ },
12
+ "homepage": "https://github.com/shaaaaawn/idle-screens#readme",
13
+ "type": "module",
14
+ "sideEffects": false,
15
+ "main": "./dist/index.js",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "keywords": [
28
+ "screensaver",
29
+ "idle",
30
+ "ambient",
31
+ "declarative",
32
+ "schema",
33
+ "canvas"
34
+ ],
35
+ "bugs": {
36
+ "url": "https://github.com/shaaaaawn/idle-screens/issues"
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "peerDependencies": {
42
+ "@idle-screens/core": "0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@idle-screens/core": "0.1.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "dev": "tsup --watch",
50
+ "typecheck": "tsc --noEmit",
51
+ "lint": "eslint src"
52
+ }
53
+ }