@idle-screens/schema 1.0.0 → 2.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.
@@ -0,0 +1,287 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://idlescreens.com/schemas/saver-spec-1.schema.json",
4
+ "title": "SaverSpec (format version 1)",
5
+ "description": "Declarative, agent-authorable screensaver: a static background plus layers of moving sprites. No code, no network, no strobe primitive — safety by construction. This schema is stricter than the runtime validator (unknown properties are rejected here to catch authoring typos; the runtime ignores them). Cross-field rules the schema cannot express: total entities across all layers must be <= 800; range pairs must satisfy min <= max.",
6
+ "type": "object",
7
+ "required": ["schemaVersion", "id", "label", "layers"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "schemaVersion": { "const": 1, "description": "Format version. Independent of the npm package version." },
11
+ "id": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Unique kebab-case saver id." },
12
+ "label": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Human-readable name." },
13
+ "seed": { "type": "number", "description": "Deterministic RNG seed. Same spec + same seed = identical frames. Falls back to the host's seed." },
14
+ "motionIntensity": { "enum": ["calm", "moderate", "energetic"], "description": "Advisory intensity for capability/reduced-motion gating." },
15
+ "units": { "enum": ["px", "viewport"], "description": "Dimensional unit system. 'viewport' = all sizes/speeds/distances are fractions of min(w,h)." },
16
+ "background": { "$ref": "#/definitions/background" },
17
+ "layers": {
18
+ "type": "array",
19
+ "minItems": 1,
20
+ "maxItems": 36,
21
+ "items": { "$ref": "#/definitions/layer" },
22
+ "description": "Rendered back-to-front. At most 36 layers; at most 800 entities total."
23
+ }
24
+ },
25
+ "definitions": {
26
+ "hexColor": {
27
+ "type": "string",
28
+ "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
29
+ "description": "Hex colour like #0a3a52."
30
+ },
31
+ "range01": {
32
+ "type": "array",
33
+ "items": { "type": "number", "minimum": 0, "maximum": 1 },
34
+ "minItems": 2,
35
+ "maxItems": 2,
36
+ "description": "[min, max] with min <= max, both 0..1."
37
+ },
38
+ "positiveRange": {
39
+ "type": "array",
40
+ "items": { "type": "number", "exclusiveMinimum": 0 },
41
+ "minItems": 2,
42
+ "maxItems": 2,
43
+ "description": "[min, max] px with 0 < min <= max."
44
+ },
45
+ "speedRange": {
46
+ "type": "array",
47
+ "items": { "type": "number", "minimum": 0, "maximum": 4000 },
48
+ "minItems": 2,
49
+ "maxItems": 2,
50
+ "description": "[min, max] px/sec, capped at 4000 so nothing teleports."
51
+ },
52
+ "background": {
53
+ "oneOf": [
54
+ {
55
+ "type": "object",
56
+ "required": ["type", "color"],
57
+ "additionalProperties": false,
58
+ "properties": {
59
+ "type": { "const": "solid" },
60
+ "color": { "$ref": "#/definitions/hexColor" }
61
+ }
62
+ },
63
+ {
64
+ "type": "object",
65
+ "required": ["type", "stops"],
66
+ "additionalProperties": false,
67
+ "properties": {
68
+ "type": { "const": "gradient" },
69
+ "stops": {
70
+ "type": "array",
71
+ "minItems": 2,
72
+ "items": {
73
+ "type": "object",
74
+ "required": ["at", "color"],
75
+ "additionalProperties": false,
76
+ "properties": {
77
+ "at": { "type": "number", "minimum": 0, "maximum": 1, "description": "0 = top, 1 = bottom." },
78
+ "color": { "$ref": "#/definitions/hexColor" }
79
+ }
80
+ },
81
+ "description": "Vertical gradient stops, top to bottom."
82
+ },
83
+ "band": {
84
+ "type": "object",
85
+ "required": ["color", "height"],
86
+ "additionalProperties": false,
87
+ "properties": {
88
+ "color": { "$ref": "#/definitions/hexColor" },
89
+ "height": { "type": "number", "exclusiveMinimum": 0, "description": "Band height in px (e.g. an aquarium seafloor)." }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ]
95
+ },
96
+ "layer": {
97
+ "type": "object",
98
+ "required": ["count", "sprite", "motion"],
99
+ "additionalProperties": false,
100
+ "properties": {
101
+ "count": { "type": "integer", "minimum": 1, "maximum": 400, "description": "Entities in this layer (max 400 per layer, 800 total)." },
102
+ "sprite": { "$ref": "#/definitions/sprite" },
103
+ "motion": { "$ref": "#/definitions/motion" },
104
+ "size": { "$ref": "#/definitions/positiveRange", "description": "Sprite size range in px (font size for emoji/text; ignored for circle)." },
105
+ "wrap": { "type": "boolean", "description": "Wrap to the opposite edge when leaving the viewport. Default true." },
106
+ "flip": { "type": "boolean", "description": "Flip the sprite horizontally to face its heading. Default false." },
107
+ "alpha": { "$ref": "#/definitions/range01", "description": "Per-entity opacity range. Default [1, 1]." },
108
+ "blend": { "const": "lighter", "description": "Additive compositing (glow stacking). Omit for normal source-over." },
109
+ "region": {
110
+ "type": "object",
111
+ "additionalProperties": false,
112
+ "properties": {
113
+ "x": { "$ref": "#/definitions/range01" },
114
+ "y": { "$ref": "#/definitions/range01" }
115
+ },
116
+ "description": "Fractional spawn window (constrains placement, not travel). Default full viewport."
117
+ },
118
+ "pulse": {
119
+ "type": "object",
120
+ "required": ["amp", "period"],
121
+ "additionalProperties": false,
122
+ "properties": {
123
+ "amp": { "type": "number", "exclusiveMinimum": 0, "maximum": 0.5, "description": "Opacity breathing amplitude, capped at 0.5." },
124
+ "period": { "type": "number", "minimum": 500, "description": "Breathing period in ms; floor of 500 (max 2 Hz — flash-safety)." }
125
+ },
126
+ "description": "Sinusoidal opacity breathing. Per-entity seeded phase — a layer can never strobe in unison."
127
+ },
128
+ "spin": { "type": "number", "minimum": -360, "maximum": 360, "description": "Per-entity rotation speed in degrees/sec (positive = clockwise). Each entity gets a seeded starting angle." },
129
+ "grow": {
130
+ "type": "object",
131
+ "required": ["amp", "period"],
132
+ "additionalProperties": false,
133
+ "properties": {
134
+ "amp": { "type": "number", "exclusiveMinimum": 0, "maximum": 0.8, "description": "Size breathing amplitude as fraction of base size (0.3 = ±30%)." },
135
+ "period": { "type": "number", "minimum": 500, "description": "Breathing period in ms; same flash-safety floor as pulse." }
136
+ },
137
+ "description": "Sinusoidal size breathing. Per-entity seeded phase. Composes with any sprite kind."
138
+ },
139
+ "key": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Addressable name so setParam can use key.field paths." },
140
+ "position": {
141
+ "type": "object",
142
+ "required": ["x", "y"],
143
+ "additionalProperties": false,
144
+ "properties": {
145
+ "x": { "type": "number", "minimum": 0, "maximum": 1 },
146
+ "y": { "type": "number", "minimum": 0, "maximum": 1 }
147
+ },
148
+ "description": "Exact fractional position for a single entity. Requires count: 1. Overrides region scatter."
149
+ },
150
+ "links": {
151
+ "type": "object",
152
+ "required": ["k", "maxDist"],
153
+ "additionalProperties": false,
154
+ "properties": {
155
+ "k": { "type": "integer", "minimum": 1, "maximum": 8, "description": "Connect to the k nearest neighbors." },
156
+ "maxDist": { "type": "number", "exclusiveMinimum": 0, "description": "Maximum link distance (px or viewport-units)." },
157
+ "color": { "$ref": "#/definitions/hexColor", "description": "Link color. Defaults to per-entity color." },
158
+ "alpha": { "type": "number", "minimum": 0, "maximum": 1, "description": "Link opacity. Defaults to entity alpha." },
159
+ "width": { "type": "number", "exclusiveMinimum": 0, "description": "Stroke width (px or viewport-units). Default 1." }
160
+ },
161
+ "description": "Draw lines to each entity's k nearest neighbors within maxDist. The layer's entity count (count field) must be <= 200 for performance."
162
+ }
163
+ },
164
+ "if": { "required": ["position"] },
165
+ "then": { "properties": { "count": { "const": 1 } } }
166
+ },
167
+ "sprite": {
168
+ "oneOf": [
169
+ {
170
+ "type": "object",
171
+ "required": ["kind", "glyphs"],
172
+ "additionalProperties": false,
173
+ "properties": {
174
+ "kind": { "const": "emoji" },
175
+ "glyphs": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Emoji picked per entity (seeded)." },
176
+ "cycle": { "$ref": "#/definitions/cycle" }
177
+ }
178
+ },
179
+ {
180
+ "type": "object",
181
+ "required": ["kind", "strings"],
182
+ "additionalProperties": false,
183
+ "properties": {
184
+ "kind": { "const": "text" },
185
+ "strings": { "type": "array", "minItems": 1, "items": { "type": "string" } },
186
+ "color": { "$ref": "#/definitions/hexColor" },
187
+ "font": { "type": "string", "description": "CSS font shorthand, e.g. 'bold 24px monospace'." },
188
+ "align": { "enum": ["left", "center", "right"] },
189
+ "baseline": { "enum": ["top", "middle", "bottom"] },
190
+ "maxWidth": { "type": "number", "exclusiveMinimum": 0 },
191
+ "cycle": { "$ref": "#/definitions/cycle" }
192
+ }
193
+ },
194
+ {
195
+ "type": "object",
196
+ "required": ["kind", "radius", "color"],
197
+ "additionalProperties": false,
198
+ "properties": {
199
+ "kind": { "const": "circle" },
200
+ "radius": { "$ref": "#/definitions/positiveRange" },
201
+ "color": { "$ref": "#/definitions/hexColor" },
202
+ "soft": { "type": "boolean", "description": "Radial falloff (glow orb) instead of a hard disc." },
203
+ "colors": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/hexColor" }, "description": "Per-entity color palette (seeded pick). Falls back to color." }
204
+ }
205
+ }
206
+ ]
207
+ },
208
+ "cycle": {
209
+ "type": "object",
210
+ "required": ["period"],
211
+ "additionalProperties": false,
212
+ "properties": {
213
+ "period": { "type": "number", "minimum": 500, "description": "Cycling period in ms; floor of 500 (flash-safety)." }
214
+ },
215
+ "description": "Rotate through sprite variants over time. Each entity offsets by its seeded phase."
216
+ },
217
+ "motion": {
218
+ "oneOf": [
219
+ {
220
+ "type": "object",
221
+ "required": ["type", "speed"],
222
+ "additionalProperties": false,
223
+ "properties": {
224
+ "type": { "const": "drift" },
225
+ "speed": { "$ref": "#/definitions/speedRange" },
226
+ "angle": { "type": "number", "description": "Heading in degrees: 0 = right, 90 = down. Default 0." },
227
+ "bidirectional": { "type": "boolean", "description": "Randomly flip horizontal direction per entity (fish)." },
228
+ "bob": { "type": "number", "description": "Vertical wobble amplitude in px." }
229
+ }
230
+ },
231
+ {
232
+ "type": "object",
233
+ "required": ["type", "speed"],
234
+ "additionalProperties": false,
235
+ "properties": {
236
+ "type": { "const": "rise" },
237
+ "speed": { "$ref": "#/definitions/speedRange" },
238
+ "sway": { "type": "number", "description": "Horizontal sway amplitude in px (bubbles)." }
239
+ }
240
+ },
241
+ {
242
+ "type": "object",
243
+ "required": ["type", "speed"],
244
+ "additionalProperties": false,
245
+ "properties": {
246
+ "type": { "const": "bounce" },
247
+ "speed": { "$ref": "#/definitions/speedRange" }
248
+ }
249
+ },
250
+ {
251
+ "type": "object",
252
+ "required": ["type"],
253
+ "additionalProperties": false,
254
+ "properties": {
255
+ "type": { "const": "static", "description": "Entity stays exactly where placed. Use with position for pinned elements." }
256
+ }
257
+ },
258
+ {
259
+ "type": "object",
260
+ "required": ["type", "speed", "radius"],
261
+ "additionalProperties": false,
262
+ "properties": {
263
+ "type": { "const": "orbit" },
264
+ "speed": {
265
+ "type": "array",
266
+ "items": { "type": "number", "minimum": 0, "maximum": 180 },
267
+ "minItems": 2,
268
+ "maxItems": 2,
269
+ "description": "[min, max] angular velocity in degrees/sec, capped at 180."
270
+ },
271
+ "radius": { "$ref": "#/definitions/positiveRange", "description": "[min, max] orbit radius in px." },
272
+ "center": {
273
+ "type": "object",
274
+ "required": ["x", "y"],
275
+ "additionalProperties": false,
276
+ "properties": {
277
+ "x": { "type": "number", "minimum": 0, "maximum": 1 },
278
+ "y": { "type": "number", "minimum": 0, "maximum": 1 }
279
+ },
280
+ "description": "Orbit center as fractional viewport position. Default {0.5, 0.5}."
281
+ }
282
+ }
283
+ }
284
+ ]
285
+ }
286
+ }
287
+ }