@idle-screens/schema 1.0.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.
@@ -0,0 +1,224 @@
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
+ "background": { "$ref": "#/definitions/background" },
16
+ "layers": {
17
+ "type": "array",
18
+ "minItems": 1,
19
+ "maxItems": 36,
20
+ "items": { "$ref": "#/definitions/layer" },
21
+ "description": "Rendered back-to-front. At most 36 layers; at most 800 entities total."
22
+ }
23
+ },
24
+ "definitions": {
25
+ "hexColor": {
26
+ "type": "string",
27
+ "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
28
+ "description": "Hex colour like #0a3a52."
29
+ },
30
+ "range01": {
31
+ "type": "array",
32
+ "items": { "type": "number", "minimum": 0, "maximum": 1 },
33
+ "minItems": 2,
34
+ "maxItems": 2,
35
+ "description": "[min, max] with min <= max, both 0..1."
36
+ },
37
+ "positiveRange": {
38
+ "type": "array",
39
+ "items": { "type": "number", "exclusiveMinimum": 0 },
40
+ "minItems": 2,
41
+ "maxItems": 2,
42
+ "description": "[min, max] px with 0 < min <= max."
43
+ },
44
+ "speedRange": {
45
+ "type": "array",
46
+ "items": { "type": "number", "minimum": 0, "maximum": 4000 },
47
+ "minItems": 2,
48
+ "maxItems": 2,
49
+ "description": "[min, max] px/sec, capped at 4000 so nothing teleports."
50
+ },
51
+ "background": {
52
+ "oneOf": [
53
+ {
54
+ "type": "object",
55
+ "required": ["type", "color"],
56
+ "additionalProperties": false,
57
+ "properties": {
58
+ "type": { "const": "solid" },
59
+ "color": { "$ref": "#/definitions/hexColor" }
60
+ }
61
+ },
62
+ {
63
+ "type": "object",
64
+ "required": ["type", "stops"],
65
+ "additionalProperties": false,
66
+ "properties": {
67
+ "type": { "const": "gradient" },
68
+ "stops": {
69
+ "type": "array",
70
+ "minItems": 2,
71
+ "items": {
72
+ "type": "object",
73
+ "required": ["at", "color"],
74
+ "additionalProperties": false,
75
+ "properties": {
76
+ "at": { "type": "number", "minimum": 0, "maximum": 1, "description": "0 = top, 1 = bottom." },
77
+ "color": { "$ref": "#/definitions/hexColor" }
78
+ }
79
+ },
80
+ "description": "Vertical gradient stops, top to bottom."
81
+ },
82
+ "band": {
83
+ "type": "object",
84
+ "required": ["color", "height"],
85
+ "additionalProperties": false,
86
+ "properties": {
87
+ "color": { "$ref": "#/definitions/hexColor" },
88
+ "height": { "type": "number", "exclusiveMinimum": 0, "description": "Band height in px (e.g. an aquarium seafloor)." }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ ]
94
+ },
95
+ "layer": {
96
+ "type": "object",
97
+ "required": ["count", "sprite", "motion"],
98
+ "additionalProperties": false,
99
+ "properties": {
100
+ "count": { "type": "integer", "minimum": 1, "maximum": 400, "description": "Entities in this layer (max 400 per layer, 800 total)." },
101
+ "sprite": { "$ref": "#/definitions/sprite" },
102
+ "motion": { "$ref": "#/definitions/motion" },
103
+ "size": { "$ref": "#/definitions/positiveRange", "description": "Sprite size range in px (font size for emoji/text; ignored for circle)." },
104
+ "wrap": { "type": "boolean", "description": "Wrap to the opposite edge when leaving the viewport. Default true." },
105
+ "flip": { "type": "boolean", "description": "Flip the sprite horizontally to face its heading. Default false." },
106
+ "alpha": { "$ref": "#/definitions/range01", "description": "Per-entity opacity range. Default [1, 1]." },
107
+ "blend": { "const": "lighter", "description": "Additive compositing (glow stacking). Omit for normal source-over." },
108
+ "region": {
109
+ "type": "object",
110
+ "additionalProperties": false,
111
+ "properties": {
112
+ "x": { "$ref": "#/definitions/range01" },
113
+ "y": { "$ref": "#/definitions/range01" }
114
+ },
115
+ "description": "Fractional spawn window (constrains placement, not travel). Default full viewport."
116
+ },
117
+ "pulse": {
118
+ "type": "object",
119
+ "required": ["amp", "period"],
120
+ "additionalProperties": false,
121
+ "properties": {
122
+ "amp": { "type": "number", "exclusiveMinimum": 0, "maximum": 0.5, "description": "Opacity breathing amplitude, capped at 0.5." },
123
+ "period": { "type": "number", "minimum": 500, "description": "Breathing period in ms; floor of 500 (max 2 Hz — flash-safety)." }
124
+ },
125
+ "description": "Sinusoidal opacity breathing. Per-entity seeded phase — a layer can never strobe in unison."
126
+ },
127
+ "key": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Addressable name so setParam can use key.field paths." },
128
+ "position": {
129
+ "type": "object",
130
+ "required": ["x", "y"],
131
+ "additionalProperties": false,
132
+ "properties": {
133
+ "x": { "type": "number", "minimum": 0, "maximum": 1 },
134
+ "y": { "type": "number", "minimum": 0, "maximum": 1 }
135
+ },
136
+ "description": "Exact fractional position for a single entity. Requires count: 1. Overrides region scatter."
137
+ }
138
+ },
139
+ "if": { "required": ["position"] },
140
+ "then": { "properties": { "count": { "const": 1 } } }
141
+ },
142
+ "sprite": {
143
+ "oneOf": [
144
+ {
145
+ "type": "object",
146
+ "required": ["kind", "glyphs"],
147
+ "additionalProperties": false,
148
+ "properties": {
149
+ "kind": { "const": "emoji" },
150
+ "glyphs": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Emoji picked per entity (seeded)." }
151
+ }
152
+ },
153
+ {
154
+ "type": "object",
155
+ "required": ["kind", "strings"],
156
+ "additionalProperties": false,
157
+ "properties": {
158
+ "kind": { "const": "text" },
159
+ "strings": { "type": "array", "minItems": 1, "items": { "type": "string" } },
160
+ "color": { "$ref": "#/definitions/hexColor" },
161
+ "font": { "type": "string", "description": "CSS font shorthand, e.g. 'bold 24px monospace'." },
162
+ "align": { "enum": ["left", "center", "right"] },
163
+ "baseline": { "enum": ["top", "middle", "bottom"] },
164
+ "maxWidth": { "type": "number", "exclusiveMinimum": 0 }
165
+ }
166
+ },
167
+ {
168
+ "type": "object",
169
+ "required": ["kind", "radius", "color"],
170
+ "additionalProperties": false,
171
+ "properties": {
172
+ "kind": { "const": "circle" },
173
+ "radius": { "$ref": "#/definitions/positiveRange" },
174
+ "color": { "$ref": "#/definitions/hexColor" },
175
+ "soft": { "type": "boolean", "description": "Radial falloff (glow orb) instead of a hard disc." }
176
+ }
177
+ }
178
+ ]
179
+ },
180
+ "motion": {
181
+ "oneOf": [
182
+ {
183
+ "type": "object",
184
+ "required": ["type", "speed"],
185
+ "additionalProperties": false,
186
+ "properties": {
187
+ "type": { "const": "drift" },
188
+ "speed": { "$ref": "#/definitions/speedRange" },
189
+ "angle": { "type": "number", "description": "Heading in degrees: 0 = right, 90 = down. Default 0." },
190
+ "bidirectional": { "type": "boolean", "description": "Randomly flip horizontal direction per entity (fish)." },
191
+ "bob": { "type": "number", "description": "Vertical wobble amplitude in px." }
192
+ }
193
+ },
194
+ {
195
+ "type": "object",
196
+ "required": ["type", "speed"],
197
+ "additionalProperties": false,
198
+ "properties": {
199
+ "type": { "const": "rise" },
200
+ "speed": { "$ref": "#/definitions/speedRange" },
201
+ "sway": { "type": "number", "description": "Horizontal sway amplitude in px (bubbles)." }
202
+ }
203
+ },
204
+ {
205
+ "type": "object",
206
+ "required": ["type", "speed"],
207
+ "additionalProperties": false,
208
+ "properties": {
209
+ "type": { "const": "bounce" },
210
+ "speed": { "$ref": "#/definitions/speedRange" }
211
+ }
212
+ },
213
+ {
214
+ "type": "object",
215
+ "required": ["type"],
216
+ "additionalProperties": false,
217
+ "properties": {
218
+ "type": { "const": "static", "description": "Entity stays exactly where placed. Use with position for pinned elements." }
219
+ }
220
+ }
221
+ ]
222
+ }
223
+ }
224
+ }