@idle-screens/schema 3.6.1 → 3.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idle-screens/schema",
3
- "version": "3.6.1",
3
+ "version": "3.8.0",
4
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
5
  "license": "MIT",
6
6
  "author": "Shawn Partridge",
@@ -100,6 +100,26 @@
100
100
  },
101
101
  "description": "Animated reveal controls for a text block."
102
102
  },
103
+ "finish": {
104
+ "type": "object",
105
+ "additionalProperties": false,
106
+ "description": "Print finish over the finished frame, applied last. grain (0..1): a seeded zero-mean noise tile screened at up to 35% alpha; dither (0..1): an 8x8 ordered Bayer tile at up to 25% - a stylistic screen, not per-pixel dithering. Both composite with a symmetric blend (overlay), so mean luminance is unchanged: flash-safe, and ignored by luminanceGrid. The grain tile is generated once per mount from the spec seed; animate: true steps its offset from the frame's time bucket (12 Hz, seeded), ignored on the basic/minimal tiers. Never fed back into ghosting. grain/dither are steerable paint paths (finish.grain). Native clients ignore finish.",
107
+ "properties": {
108
+ "grain": {
109
+ "type": "number",
110
+ "minimum": 0,
111
+ "maximum": 1
112
+ },
113
+ "dither": {
114
+ "type": "number",
115
+ "minimum": 0,
116
+ "maximum": 1
117
+ },
118
+ "animate": {
119
+ "type": "boolean"
120
+ }
121
+ }
122
+ },
103
123
  "background": {
104
124
  "oneOf": [
105
125
  {
@@ -193,6 +213,80 @@
193
213
  "description": "Slow oscillation of gradient stop positions \u2014 makes the background feel alive."
194
214
  }
195
215
  }
216
+ },
217
+ {
218
+ "type": "object",
219
+ "required": [
220
+ "type",
221
+ "scale",
222
+ "bands"
223
+ ],
224
+ "additionalProperties": false,
225
+ "description": "A scalar-field background: seeded, warped value noise quantised into bands (thermal maps, contour terrain, riso washes). Pure in (x, y, t, seed): rendered as a low-res raster (short side 96 px, 48 on basic/minimal tiers) scaled to the canvas, recomputed once per 100 ms while drifting; the perception grid samples the same function. Never touches the entity RNG. Native clients that ignore the field paint a vertical gradient through bands in order.",
226
+ "properties": {
227
+ "type": {
228
+ "const": "field"
229
+ },
230
+ "scale": {
231
+ "type": "number",
232
+ "minimum": 0.5,
233
+ "maximum": 8,
234
+ "description": "Noise features across the short side of the viewport."
235
+ },
236
+ "octaves": {
237
+ "type": "integer",
238
+ "minimum": 1,
239
+ "maximum": 4,
240
+ "description": "Octaves of detail. Default 2."
241
+ },
242
+ "warp": {
243
+ "type": "number",
244
+ "minimum": 0,
245
+ "maximum": 1,
246
+ "description": "Domain-warp amount: 0 (default) round blobs, 1 smeared, folded contours."
247
+ },
248
+ "quantize": {
249
+ "type": "integer",
250
+ "minimum": 0,
251
+ "maximum": 8,
252
+ "not": { "const": 1 },
253
+ "description": "Levels to posterise into (2..8; hard bands), or 0 for smooth interpolation between bands. Default bands.length."
254
+ },
255
+ "bands": {
256
+ "type": "array",
257
+ "minItems": 2,
258
+ "maxItems": 8,
259
+ "items": {
260
+ "$ref": "#/definitions/hexColor"
261
+ },
262
+ "description": "2..8 colours, read as a ramp from the field's low end to its high end."
263
+ },
264
+ "drift": {
265
+ "type": "object",
266
+ "required": [
267
+ "period"
268
+ ],
269
+ "additionalProperties": false,
270
+ "properties": {
271
+ "period": {
272
+ "type": "number",
273
+ "minimum": 10000,
274
+ "description": "Full loop period in ms. Floor of 10 s (flash guard: drift is the only way a field changes over time)."
275
+ },
276
+ "amount": {
277
+ "type": "number",
278
+ "minimum": 0,
279
+ "maximum": 1,
280
+ "description": "Radius of the sample domain's slow circle, in feature units. Default 0.3."
281
+ }
282
+ },
283
+ "description": "Slow travel of the sample domain \u2014 the field's animate. The field loops exactly every period."
284
+ },
285
+ "seed": {
286
+ "type": "number",
287
+ "description": "Field seed. Defaults to the spec seed; independent of the entity stream either way."
288
+ }
289
+ }
196
290
  }
197
291
  ]
198
292
  },
@@ -547,6 +641,26 @@
547
641
  },
548
642
  "description": "Sparse events: each entity appears, expands and fades in place for `life` ms every `every` ms, dark in between. The one-ping primitive; a pure function of t."
549
643
  },
644
+ "rotate": {
645
+ "description": "Static per-entity rotation in degrees (positive = clockwise). A scalar turns every entity the same way; a [min,max] range gives each entity a seeded angle. Composes with spin (added to its seeded start angle). spin: [0, 0] does NOT hold a seeded angle — a zero spin speed renders at 0. Native clients that read spin only render it as 0.",
646
+ "oneOf": [
647
+ {
648
+ "type": "number",
649
+ "minimum": -360,
650
+ "maximum": 360
651
+ },
652
+ {
653
+ "type": "array",
654
+ "items": {
655
+ "type": "number",
656
+ "minimum": -360,
657
+ "maximum": 360
658
+ },
659
+ "minItems": 2,
660
+ "maxItems": 2
661
+ }
662
+ ]
663
+ },
550
664
  "clock": {
551
665
  "type": "object",
552
666
  "additionalProperties": false,
@@ -682,6 +796,9 @@
682
796
  },
683
797
  "cycle": {
684
798
  "$ref": "#/definitions/cycle"
799
+ },
800
+ "role": {
801
+ "$ref": "#/definitions/textRole"
685
802
  }
686
803
  }
687
804
  },
@@ -924,8 +1041,8 @@
924
1041
  "maxWidth": {
925
1042
  "type": "number",
926
1043
  "exclusiveMinimum": 0,
927
- "maximum": 1.0,
928
- "description": "Block width as a viewport fraction of min(w,h)."
1044
+ "maximum": 2.0,
1045
+ "description": "Wrap width as a viewport fraction of min(w,h), up to 2.0 (wider than the frame is legal; the text-off-screen advisory reports overflow)."
929
1046
  },
930
1047
  "fontSize": {
931
1048
  "type": "number",
@@ -952,12 +1069,48 @@
952
1069
  },
953
1070
  "reveal": {
954
1071
  "$ref": "#/definitions/textReveal"
1072
+ },
1073
+ "anchor": {
1074
+ "enum": [
1075
+ "top-left",
1076
+ "top",
1077
+ "top-right",
1078
+ "left",
1079
+ "center",
1080
+ "right",
1081
+ "bottom-left",
1082
+ "bottom",
1083
+ "bottom-right"
1084
+ ],
1085
+ "description": "Which point of the rendered text (widest line × lines·lineHeight, as align lays it out) position names. Absent: position is the top-left of the maxWidth layout box (today's behaviour). 'center' centres the block at position on every aspect ratio. Placement (structural). Native clients without anchor render as absent."
1086
+ },
1087
+ "font": {
1088
+ "type": "string",
1089
+ "minLength": 1,
1090
+ "pattern": "^(?!.*\\d(?:px|pt|pc|em|rem|ex|ch|vw|vh|vmin|vmax|%)).*$",
1091
+ "description": "CSS font family and/or weight/style only, e.g. 'bold monospace' or \"300 'Inter', sans-serif\" — a size inside it is rejected (fontSize owns size). A monospace family switches the line-breaker to a uniform advance. Default 'system-ui, sans-serif'; native clients fall back to the system face."
1092
+ },
1093
+ "opacity": {
1094
+ "type": "number",
1095
+ "minimum": 0,
1096
+ "maximum": 1,
1097
+ "description": "Paint-level alpha multiplier for the block (default 1). Not structural — steer it with dur to fade the block in or out without a rebuild. Declare it to make the path steerable. Native clients without it render at 1."
1098
+ },
1099
+ "role": {
1100
+ "$ref": "#/definitions/textRole"
955
1101
  }
956
1102
  },
957
- "description": "Multi-line text block with deterministic line-breaking. All dimensions are viewport fractions. Use with count: 1, static motion, and position."
1103
+ "description": "Multi-line text block with deterministic line-breaking. All dimensions are viewport fractions. Use with count: 1, static motion, and position; add anchor: 'center' to centre the block at position."
958
1104
  }
959
1105
  ]
960
1106
  },
1107
+ "textRole": {
1108
+ "enum": [
1109
+ "read",
1110
+ "atmosphere"
1111
+ ],
1112
+ "description": "What the text is for. 'read' opts the layer into the legibility advisories (text-legibility: WCAG ratio below 4.5:1 against the background at the box centre or against the brightest additive layer over it; text-safe-area: box within 5% of an edge). 'atmosphere' (or absent) keeps it silent. A declaration only — changes no pixel on any client."
1113
+ },
961
1114
  "colorWeights": {
962
1115
  "type": "array",
963
1116
  "minItems": 1,
@@ -1370,6 +1523,9 @@
1370
1523
  "background": {
1371
1524
  "$ref": "#/definitions/background"
1372
1525
  },
1526
+ "finish": {
1527
+ "$ref": "#/definitions/finish"
1528
+ },
1373
1529
  "layers": {
1374
1530
  "type": "array",
1375
1531
  "minItems": 1,
@@ -1423,6 +1579,21 @@
1423
1579
  "type": "boolean",
1424
1580
  "description": "When true, global time wraps over the sum of segment durations. Requires every segment to have a duration."
1425
1581
  },
1582
+ "sync": {
1583
+ "enum": [
1584
+ "mount",
1585
+ "epoch"
1586
+ ],
1587
+ "description": "What the sequence clock is anchored to. 'mount' (default): every viewer starts at T = 0 when it mounts — a joiner sees segment 0 (pre-roll semantics). 'epoch': a viewer handed sequenceBaseT on its mount context (the host's Date.now() − epoch) starts its clock there, so every screen shows the same segment; a late joiner lands mid-loop. advance: 'input' holds stay armed under either. Native clients that anchor at their own mount behave as 'mount'."
1588
+ },
1589
+ "bed": {
1590
+ "$ref": "#/definitions/saverSpec",
1591
+ "description": "A scene drawn under every segment on the sequence's global clock — the ground that does not reset at a boundary or under a sequence.segment steer. Segments render over it transparently (their background and ghosting are ignored; validateSequence warns bed-hides-segment-background). Its entities plus the largest segment's must stay within the 800 cap (enforced by validateSequence). Steer it with bed.<path>. Native clients that ignore it render segments with their own backgrounds."
1592
+ },
1593
+ "finish": {
1594
+ "$ref": "#/definitions/finish",
1595
+ "description": "A print finish applied once per composed frame (bed + segment + fade) on the sequence's presentation canvas. Overrides every segment's own finish while set; absent, the active segment's finish applies. A bed's finish is ignored. Native clients ignore it."
1596
+ },
1426
1597
  "segments": {
1427
1598
  "type": "array",
1428
1599
  "minItems": 1,
@@ -1463,7 +1634,7 @@
1463
1634
  "input",
1464
1635
  "either"
1465
1636
  ],
1466
- "description": "How the segment advances. Validated; runtime drivers planned for a follow-up."
1637
+ "description": "How the segment advances. 'auto' (default) and 'either' advance on the timer; 'input' holds a timed segment at the end of its duration (the scene keeps animating) until a sequence.segment steer releases it — the clicker. A durationless final segment holds regardless."
1467
1638
  },
1468
1639
  "transition": {
1469
1640
  "type": "object",
@@ -1485,6 +1656,23 @@
1485
1656
  "minimum": 200,
1486
1657
  "maximum": 5000,
1487
1658
  "description": "Morph glide duration in ms. Requires structurally identical adjacent segments."
1659
+ },
1660
+ "text": {
1661
+ "enum": ["step", "crossfade"],
1662
+ "description": "How text / textBlock strings change during the morph. 'step' (default, today's behaviour): the incoming string shows from the first frame. 'crossfade': each text layer whose string(s) differ is drawn twice for dur — the outgoing words at 1 − k, the incoming at k (k = the eased progress). Native clients step (tvOS already cross-fades whole frames on a segment change)."
1663
+ }
1664
+ },
1665
+ "required": ["type", "dur"],
1666
+ "additionalProperties": false
1667
+ },
1668
+ {
1669
+ "properties": {
1670
+ "type": { "const": "fade" },
1671
+ "dur": {
1672
+ "type": "number",
1673
+ "minimum": 200,
1674
+ "maximum": 5000,
1675
+ "description": "Cross-fade duration in ms: the outgoing segment keeps animating on its own canvas and is composited over the incoming one at 1 − easeSmooth(localT / dur). No structural requirement. Hosts on the 'basic' or 'minimal' capability tier render it as cut."
1488
1676
  }
1489
1677
  },
1490
1678
  "required": ["type", "dur"],