pixel-daylight-switch-gsvegetable 3.0.0 → 3.0.1

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/README.ja.md CHANGED
@@ -129,7 +129,17 @@ Manifest v1 はシーン ID、作者・ライセンス、ピクセルキャン
129
129
  - [Manifest v1 JSON Schema](schemas/scene-manifest.v1.schema.json)
130
130
  - [Sakura Evening サンプル](examples/scenes/sakura-evening.json)
131
131
 
132
- v1 は外部画像/Sprite、レイヤー式 Tile Map、独自アニメーショントラックにはまだ対応していません。これらは任意コードの実行を許可せず、将来の独立した Schema 拡張として追加します。5 つの組み込みシーンは従来の描画・アニメーション実装を維持します。
132
+ ### Manifest v2:レイヤー付きピクセルワールド(v3.0.1)
133
+
134
+ 3.0.1 では Manifest v1 を変更せずに v2 を追加しました。v2 は最大 24 個の再利用可能なピクセルアセットと、最大 16 個の順序付きレイヤーに対応します。レイヤーはシーン座標に配置でき、drift または bob の宣言型モーショントラックで横・縦方向に動かせます。重ねるアセット内の . は透明として扱い、それ以外の文字は昼夜パレットで着色されます。
135
+
136
+ - [Manifest v2 JSON Schema](schemas/scene-manifest.v2.schema.json)
137
+ - [Floating Island レイヤーサンプル](examples/scenes/floating-island.v2.json)
138
+ - [シーンプラグイン開発ガイド](SCENE_PLUGINS.md)
139
+
140
+ v2 のアセットは文字ベースのピクセルマップです。PNG/JPEG の外部ダウンロードには対応しません。これによりピクセル表現と昼夜の色変化を一貫させ、外部画像の CORS や任意コード実行を避けます。
141
+
142
+ 5 つの組み込みシーンは従来の描画・アニメーション実装を維持します。
133
143
 
134
144
  ## 設定属性
135
145
 
package/README.md CHANGED
@@ -128,7 +128,27 @@ registerScene(sakura);
128
128
  PixelDaylightSwitch.mount(document.querySelector('[data-ld-part="toggle-daylight"]'));
129
129
  ```
130
130
 
131
- ### Manifest v1
131
+ ### Manifest v2 — layered pixel worlds (v3.0.1)
132
+
133
+ Version 3.0.1 extends the community scene format without changing Manifest v1. V2 scenes can define up to 24 reusable pixel-matrix assets and up to 16 ordered layers. A layer places an asset at integer scene coordinates and can optionally animate with a deterministic drift or bob track along either axis. The dot (.) is transparent in overlay assets; all other symbols are color-mapped by the scene palettes.
134
+
135
+ ```js
136
+ import PixelDaylightSwitch, { registerScene } from 'pixel-daylight-switch-gsvegetable';
137
+ import island from './floating-island.v2.json';
138
+
139
+ registerScene(island);
140
+ const root = document.querySelector('[data-ld-part="toggle-daylight"]');
141
+ root.dataset.scene = island.id;
142
+ PixelDaylightSwitch.mount(root);
143
+ ```
144
+
145
+ - [Manifest v2 JSON Schema](schemas/scene-manifest.v2.schema.json)
146
+ - [Floating Island layered example](examples/scenes/floating-island.v2.json)
147
+ - [Full scene authoring guide](SCENE_PLUGINS.md)
148
+
149
+ V2 assets are native pixel matrices, not downloaded PNG/JPEG files. This keeps the scene pipeline deterministic and avoids remote asset/CORS dependencies or executable plugin code.
150
+
151
+ ## Manifest v1
132
152
 
133
153
  A manifest contains a fixed-size character pixel map, named color palettes and a 24-hour palette timeline. The renderer interpolates colors between timeline stops while keeping the same scene geometry.
134
154
 
package/README.zh-CN.md CHANGED
@@ -129,7 +129,17 @@ Manifest v1 包含场景 ID、作者与许可证、像素画布、字符像素
129
129
  - [Manifest v1 JSON Schema](schemas/scene-manifest.v1.schema.json)
130
130
  - [樱花黄昏示例](examples/scenes/sakura-evening.json)
131
131
 
132
- v1 暂不支持外部图片/Sprite、分层 Tile Map 和自定义动画轨道;这些能力会通过后续独立版本的 Schema 扩展,不会开放任意代码执行。五个内置场景仍保留原有绘制与动态逻辑。
132
+ ### Manifest v2:分层像素世界(v3.0.1)
133
+
134
+ 3.0.1 在不改变 Manifest v1 的前提下增加了 v2。v2 支持最多 24 个可复用像素素材、最多 16 个按顺序合成的图层。图层可以指定场景坐标,并使用 drift 或 bob 声明式运动轨道沿水平或垂直方向移动。叠加素材中的点号 . 表示透明,其余字符由昼夜色板统一着色。
135
+
136
+ - [Manifest v2 JSON Schema](schemas/scene-manifest.v2.schema.json)
137
+ - [Floating Island 分层场景示例](examples/scenes/floating-island.v2.json)
138
+ - [完整场景插件开发指南](SCENE_PLUGINS.md)
139
+
140
+ 当前 v2 素材采用原生字符像素矩阵,而不是远程 PNG/JPEG 图片;这样可以保持像素边缘清晰、昼夜调色一致,也不需要额外的跨域图片加载或执行第三方代码。
141
+
142
+ 五个内置场景仍保留原有绘制与动态逻辑。
133
143
 
134
144
  ## 配置参数
135
145
 
package/SCENE_PLUGINS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Scene Plugin Guide
2
2
 
3
- Pixel Daylight Switch 3.0.0 introduces a declarative community-scene format. A scene is JSON data, not an executable JavaScript plugin. The renderer validates the manifest and draws the resulting pixel world through the existing refractive-glass pipeline.
3
+ Pixel Daylight Switch 3.0.0 introduced a declarative community-scene format. A scene is JSON data, not an executable JavaScript plugin. The renderer validates the manifest and draws the resulting pixel world through the existing refractive-glass pipeline.
4
4
 
5
5
  ## Two ways to create a scene
6
6
 
@@ -50,7 +50,7 @@ The one-line `embed.js` loader loads the registry before the renderer, but it do
50
50
 
51
51
  Required fields:
52
52
 
53
- - `schemaVersion`: currently `1`.
53
+ - `schemaVersion`: `1` for the original single-matrix format or `2` for layered pixel assets and motion.
54
54
  - `id`: unique lowercase ID, 3–64 characters. Built-in scene IDs are reserved.
55
55
  - `name`, `author`, `license`: scene metadata.
56
56
  - `canvas`: width 1–192 and height 1–112.
@@ -62,6 +62,22 @@ Every symbol used in `pixels` must have a color in every palette. The renderer i
62
62
 
63
63
  The authoritative machine-readable contract is [schemas/scene-manifest.v1.schema.json](schemas/scene-manifest.v1.schema.json). A complete sample is [examples/scenes/sakura-evening.json](examples/scenes/sakura-evening.json).
64
64
 
65
+ ## Layered scenes and pixel assets (Manifest v2)
66
+
67
+ Version 3.0.1 adds reusable pixel-art assets, compositing layers and deterministic motion tracks. Manifest v1 remains supported without changes; use `schemaVersion: 2` to opt in.
68
+
69
+ A v2 manifest keeps the base `canvas`, `pixels`, `palettes` and `timeCycle`, and adds:
70
+
71
+ - `assets`: up to 24 named pixel matrices (each up to 192 × 112).
72
+ - `layers`: 1–16 ordered placements that reference an asset and specify integer `x` / `y` coordinates.
73
+ - `animation`: optional `drift` or `bob` motion on either axis, with bounded speed, distance and phase.
74
+
75
+ The dot character (`.`) is transparent when an asset is composited as a layer. All other characters are palette symbols, so every palette must define colors for symbols used by the base matrix and every asset. Layers are drawn in array order; later layers appear above earlier ones. Motion is deterministic and uses the scene's animation clock; it does not execute plugin code.
76
+
77
+ Example: [Floating Island v2](examples/scenes/floating-island.v2.json). The v2 contract is [schemas/scene-manifest.v2.schema.json](schemas/scene-manifest.v2.schema.json); the original v1 contract remains at [schemas/scene-manifest.v1.schema.json](schemas/scene-manifest.v1.schema.json).
78
+
79
+ The current asset format is deliberately pixel-matrix based. Arbitrary PNG/JPEG downloads and executable animation callbacks are not part of v3.0.1; this keeps rendering crisp, portable, CSP-friendly and safe for community contributions.
80
+
65
81
  ## API
66
82
 
67
83
  - `registerScene(manifest)`: validate, snapshot and register a scene; returns an unregister function.
@@ -73,6 +89,6 @@ Registration rejects duplicate IDs, built-in IDs, malformed dimensions, invalid
73
89
 
74
90
  ## Scope and safety
75
91
 
76
- Manifest v1 supports palette-driven pixel maps. It intentionally does not execute plugin JavaScript, evaluate expressions, or load arbitrary code. External image/sprite assets, layered tile maps and custom animation tracks are not part of v1; they should be introduced through a separately versioned schema extension with explicit asset limits and lifecycle rules.
92
+ Manifest v1 supports palette-driven pixel maps; Manifest v2 adds bounded pixel-matrix assets, ordered layers and declarative drift/bob tracks. Neither version executes plugin JavaScript, evaluates expressions, loads arbitrary code or downloads external images. Raster image imports and tile-atlas tooling are outside the v3.0.1 scope.
77
93
 
78
94
  All five built-in scenes continue to use their original hand-authored painters and scene-specific live behavior. Community scenes use the data-driven renderer and do not replace or mutate built-in scene definitions.
package/app.js CHANGED
@@ -931,6 +931,7 @@ function mount(root) {
931
931
  Object.keys(palette).forEach((symbol) => { colors[symbol.charCodeAt(0)] = parsePluginColor(palette[symbol]); });
932
932
  return [name, colors];
933
933
  })) : null;
934
+ let pluginAnimClock = 0;
934
935
  const renderPluginWorld = (hour) => {
935
936
  const stops = pluginScene.timeCycle;
936
937
  let left = stops[0]; let right = stops[stops.length - 1]; let sampleHour = hour;
@@ -945,14 +946,43 @@ function mount(root) {
945
946
  const aPalette = pluginPaletteRgb[left.palette];
946
947
  const bPalette = pluginPaletteRgb[right.palette];
947
948
  const width = pluginScene.canvas.width; const height = pluginScene.canvas.height;
949
+ let scenePixels = pluginScene.pixels;
950
+ if (pluginScene.schemaVersion === 2 && Array.isArray(pluginScene.layers)) {
951
+ const composed = pluginScene.pixels.map(row => row.split(''));
952
+ for (const layer of pluginScene.layers) {
953
+ const asset = pluginScene.assets[layer.asset];
954
+ const motion = layer.animation;
955
+ let dx = 0, dy = 0;
956
+ if (motion) {
957
+ const phase = pluginAnimClock * motion.speed + (motion.phase || 0);
958
+ const wave = motion.type === 'drift' ? (2 / Math.PI) * Math.asin(Math.sin(phase)) : Math.sin(phase);
959
+ const offset = wave * motion.distance;
960
+ if (motion.axis === 'x') dx = Math.round(offset);
961
+ else dy = Math.round(offset);
962
+ }
963
+ const originX = layer.x + dx, originY = layer.y + dy;
964
+ for (let sy = 0; sy < asset.height; sy++) {
965
+ const targetY = originY + sy;
966
+ if (targetY < 0 || targetY >= height) continue;
967
+ const row = asset.pixels[sy];
968
+ for (let sx = 0; sx < asset.width; sx++) {
969
+ const targetX = originX + sx;
970
+ if (targetX < 0 || targetX >= width) continue;
971
+ const symbol = row[sx];
972
+ if (symbol !== '.') composed[targetY][targetX] = symbol;
973
+ }
974
+ }
975
+ }
976
+ scenePixels = composed.map(row => row.join(''));
977
+ }
948
978
  const scale = Math.min(bw / width, bh / height);
949
979
  const drawW = Math.max(1, Math.round(width * scale)); const drawH = Math.max(1, Math.round(height * scale));
950
980
  const offsetX = Math.floor((bw - drawW) / 2); const offsetY = Math.floor((bh - drawH) / 2);
951
- const background = pluginScene.pixels[0].charCodeAt(0);
981
+ const background = scenePixels[0].charCodeAt(0);
952
982
  for (let py = 0; py < bh; py++) {
953
983
  const insideY = py >= offsetY && py < offsetY + drawH;
954
984
  const sy = insideY ? clamp(Math.floor(((py - offsetY) / drawH) * height), 0, height - 1) : 0;
955
- const row = pluginScene.pixels[sy];
985
+ const row = scenePixels[sy];
956
986
  for (let px = 0; px < bw; px++) {
957
987
  const inside = insideY && px >= offsetX && px < offsetX + drawW;
958
988
  const sx = inside ? clamp(Math.floor(((px - offsetX) / drawW) * width), 0, width - 1) : 0;
@@ -1083,7 +1113,7 @@ function mount(root) {
1083
1113
  const pickKeys = (h) => { for (let i = 0; i < KEYS.length - 1; i++) { if (h >= KEYS[i][0] && h < KEYS[i + 1][0]) { keyA = i; keyB = i + 1; keyP = clamp(((h - KEYS[i][0]) / (KEYS[i + 1][0] - KEYS[i][0]) - 0.32) / 0.36); return; } } keyA = 0; keyB = 1; keyP = 0; };
1084
1114
  const renderWorld = (clock, dt, hurry) => {
1085
1115
  const h = hourOf(clock);
1086
- if (pluginScene) { renderPluginWorld(h); return; }
1116
+ if (pluginScene) { if (!calm) pluginAnimClock += dt; renderPluginWorld(h); return; }
1087
1117
  if (!calm) anim += dt; matBuf.set(baseBuf); varBuf.fill(0); emitList.length = 0;
1088
1118
  for (const c of sources) { c.w = 0; c.x = 0; c.y = 0; }
1089
1119
  lampFlick = calm ? 1 : 0.95 + 0.05 * Math.sin(anim * 13) * Math.sin(anim * 5.3);
@@ -0,0 +1,152 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "id": "floating-island",
4
+ "name": "Floating Island",
5
+ "author": "Pixel Daylight Switch",
6
+ "license": "CC0-1.0",
7
+ "canvas": {
8
+ "width": 24,
9
+ "height": 12
10
+ },
11
+ "pixels": [
12
+ "........................",
13
+ "........................",
14
+ "........................",
15
+ "........................",
16
+ "........................",
17
+ "........................",
18
+ "........................",
19
+ "........................",
20
+ "........................",
21
+ "........................",
22
+ "........................",
23
+ "........................"
24
+ ],
25
+ "palettes": {
26
+ "day": {
27
+ ".": "#8ed5f4",
28
+ "g": "#74b66d",
29
+ "t": "#79543f",
30
+ "l": "#f8d77a",
31
+ "c": "#ffffff"
32
+ },
33
+ "dusk": {
34
+ ".": "#e99a91",
35
+ "g": "#627e69",
36
+ "t": "#5b4653",
37
+ "l": "#f6bb86",
38
+ "c": "#f4c6d0"
39
+ },
40
+ "night": {
41
+ ".": "#171d3b",
42
+ "g": "#304b4a",
43
+ "t": "#302c43",
44
+ "l": "#f1d28a",
45
+ "c": "#a8b7e8"
46
+ }
47
+ },
48
+ "timeCycle": [
49
+ {
50
+ "at": 0,
51
+ "palette": "night"
52
+ },
53
+ {
54
+ "at": 6,
55
+ "palette": "night"
56
+ },
57
+ {
58
+ "at": 8,
59
+ "palette": "day"
60
+ },
61
+ {
62
+ "at": 17,
63
+ "palette": "day"
64
+ },
65
+ {
66
+ "at": 19,
67
+ "palette": "dusk"
68
+ },
69
+ {
70
+ "at": 21,
71
+ "palette": "night"
72
+ },
73
+ {
74
+ "at": 24,
75
+ "palette": "night"
76
+ }
77
+ ],
78
+ "assets": {
79
+ "island": {
80
+ "width": 14,
81
+ "height": 4,
82
+ "pixels": [
83
+ "......gg......",
84
+ "....gggggg....",
85
+ "..gggggggggg..",
86
+ "gggggggggggggg"
87
+ ]
88
+ },
89
+ "tree": {
90
+ "width": 5,
91
+ "height": 7,
92
+ "pixels": [
93
+ "..g..",
94
+ ".ggg.",
95
+ "ggggg",
96
+ "..t..",
97
+ "..t..",
98
+ "..t..",
99
+ "..t.."
100
+ ]
101
+ },
102
+ "cloud": {
103
+ "width": 8,
104
+ "height": 3,
105
+ "pixels": [
106
+ "..cccc..",
107
+ ".cccccc.",
108
+ "cccccccc"
109
+ ]
110
+ }
111
+ },
112
+ "layers": [
113
+ {
114
+ "id": "island",
115
+ "asset": "island",
116
+ "x": 5,
117
+ "y": 7
118
+ },
119
+ {
120
+ "id": "tree",
121
+ "asset": "tree",
122
+ "x": 8,
123
+ "y": 0
124
+ },
125
+ {
126
+ "id": "cloud-left",
127
+ "asset": "cloud",
128
+ "x": -3,
129
+ "y": 2,
130
+ "animation": {
131
+ "type": "drift",
132
+ "axis": "x",
133
+ "speed": 0.12,
134
+ "distance": 4,
135
+ "phase": 0
136
+ }
137
+ },
138
+ {
139
+ "id": "cloud-right",
140
+ "asset": "cloud",
141
+ "x": 17,
142
+ "y": 4,
143
+ "animation": {
144
+ "type": "bob",
145
+ "axis": "y",
146
+ "speed": 0.18,
147
+ "distance": 2,
148
+ "phase": 1.4
149
+ }
150
+ }
151
+ ]
152
+ }
package/index.d.ts CHANGED
@@ -1,38 +1,52 @@
1
1
  export interface ScenePalette {
2
2
  [symbol: string]: `#${string}`;
3
3
  }
4
-
5
- export interface SceneTimeStop {
6
- /** Local scene time in hours, from 0 through 24. */
7
- at: number;
8
- /** Key in SceneManifest.palettes. */
9
- palette: string;
4
+ export interface SceneTimeStop { at: number; palette: string; }
5
+ export interface ScenePixelAsset {
6
+ width: number;
7
+ height: number;
8
+ /** One printable-ASCII character per pixel. A dot is transparent when used as a layer. */
9
+ pixels: string[];
10
+ }
11
+ export interface SceneLayerAnimation {
12
+ /** drift and bob are deterministic sinusoidal motion tracks. */
13
+ type: 'drift' | 'bob';
14
+ axis: 'x' | 'y';
15
+ /** Angular speed in radians per second, from 0 to 4. */
16
+ speed: number;
17
+ /** Motion amplitude in scene pixels, from 1 to 64. */
18
+ distance: number;
19
+ /** Optional phase in radians, from 0 to 2π. */
20
+ phase?: number;
21
+ }
22
+ export interface SceneLayer {
23
+ id: string;
24
+ asset: string;
25
+ x: number;
26
+ y: number;
27
+ animation?: SceneLayerAnimation;
10
28
  }
11
-
12
- export interface SceneManifest {
13
- schemaVersion: 1;
14
- /** Unique lowercase community scene identifier (3-64 characters). */
29
+ export interface SceneManifestBase {
15
30
  id: string;
16
31
  name: string;
17
32
  author: string;
18
- /** SPDX identifier or a clearly stated license label. */
19
33
  license: string;
20
34
  canvas: { width: number; height: number };
21
- /** One printable-ASCII character per pixel; rows match canvas dimensions. */
22
35
  pixels: string[];
23
- /** Each palette maps every used pixel symbol to a #RGB or #RRGGBB color. */
24
36
  palettes: Record<string, ScenePalette>;
25
- /** Strictly increasing palette stops; must begin at 0 and end at 24. */
26
37
  timeCycle: SceneTimeStop[];
27
38
  }
28
-
29
- export interface SceneValidationResult {
30
- valid: boolean;
31
- errors: string[];
39
+ /** Manifest v1 preserves the original single pixel-matrix format. */
40
+ export interface SceneManifestV1 extends SceneManifestBase { schemaVersion: 1; }
41
+ /** Manifest v2 adds reusable pixel assets, compositing layers and declarative motion. */
42
+ export interface SceneManifestV2 extends SceneManifestBase {
43
+ schemaVersion: 2;
44
+ assets: Record<string, ScenePixelAsset>;
45
+ layers: SceneLayer[];
32
46
  }
33
-
47
+ export type SceneManifest = SceneManifestV1 | SceneManifestV2;
48
+ export interface SceneValidationResult { valid: boolean; errors: string[]; }
34
49
  export type UnregisterScene = () => boolean;
35
-
36
50
  export interface PixelDaylightSwitchAPI {
37
51
  mount(root: Element | null): (() => void) | null;
38
52
  unmount(root: Element | null): void;
@@ -41,7 +55,6 @@ export interface PixelDaylightSwitchAPI {
41
55
  getScene(id: string): SceneManifest | null;
42
56
  listScenes(): SceneManifest[];
43
57
  }
44
-
45
58
  declare const PixelDaylightSwitch: PixelDaylightSwitchAPI;
46
59
  export declare const mount: PixelDaylightSwitchAPI['mount'];
47
60
  export declare const unmount: PixelDaylightSwitchAPI['unmount'];
package/llms.txt CHANGED
@@ -109,3 +109,14 @@ Invalid values fall back to defaults; documented numeric ranges are clamped.
109
109
  - Scene registration is data-only; arbitrary plugin JavaScript is not executed.
110
110
  - Browser script order: scene-plugin.js, register the manifest, then app.js. The one-line embed loader loads the registry before the renderer.
111
111
  - Existing built-in scenes and their custom live animation code remain unchanged.
112
+
113
+
114
+ ## Scene Manifest v2 (v3.0.1)
115
+
116
+ - Manifest v1 remains supported and its schema is unchanged.
117
+ - Manifest v2 adds up to 24 reusable pixel-matrix assets and up to 16 ordered layers.
118
+ - Layers reference named assets and integer x/y positions; '.' is transparent in overlay assets.
119
+ - Optional deterministic motion tracks: drift or bob, on x or y, bounded speed/distance/phase.
120
+ - Every symbol across the base matrix and assets must be defined in every palette.
121
+ - Schema: schemas/scene-manifest.v2.schema.json. Example: examples/scenes/floating-island.v2.json.
122
+ - No arbitrary remote image loading, eval, or executable plugin callbacks.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixel-daylight-switch-gsvegetable",
3
- "version": "3.0.0",
4
- "description": "A draggable pixel-art day/night switch with a community-extensible scene plugin system.",
3
+ "version": "3.0.1",
4
+ "description": "A draggable pixel-art day/night switch with layered, animated community scenes.",
5
5
  "keywords": [
6
6
  "pixel-art",
7
7
  "day-night",
package/scene-plugin.js CHANGED
@@ -8,7 +8,7 @@
8
8
  })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
9
9
  'use strict';
10
10
  const registry = new Map();
11
- const MAX_WIDTH = 192, MAX_HEIGHT = 112, MAX_PALETTES = 8, MAX_SYMBOLS = 64, MAX_TIME_STOPS = 48;
11
+ const MAX_WIDTH = 192, MAX_HEIGHT = 112, MAX_PALETTES = 8, MAX_SYMBOLS = 64, MAX_TIME_STOPS = 48, MAX_ASSETS = 24, MAX_LAYERS = 16, MAX_TOTAL_CELLS = 430080;
12
12
  const ID_RE = /^[a-z0-9][a-z0-9._-]{1,62}[a-z0-9]$/;
13
13
  const COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
14
14
  const SYMBOL_RE = /^[\x20-\x7E]$/;
@@ -24,8 +24,10 @@
24
24
  function validateScene(input) {
25
25
  const errors = [];
26
26
  if (!isPlainObject(input)) return { valid:false, errors:['Scene manifest must be a plain object.'] };
27
- unknownKeys(input, new Set(['schemaVersion','id','name','author','license','canvas','pixels','palettes','timeCycle']), 'manifest', errors);
28
- if (input.schemaVersion !== 1) errors.push('schemaVersion must be 1.');
27
+ unknownKeys(input, new Set(['schemaVersion','id','name','author','license','canvas','pixels','palettes','timeCycle','assets','layers']), 'manifest', errors);
28
+ if (input.schemaVersion !== 1 && input.schemaVersion !== 2) errors.push('schemaVersion must be 1 or 2.');
29
+ if (input.schemaVersion === 1 && (input.assets !== undefined || input.layers !== undefined)) errors.push('assets and layers require schemaVersion 2.');
30
+ if (input.schemaVersion !== 1 && input.schemaVersion !== 2) errors.push('schemaVersion must be 1 or 2.');
29
31
  if (typeof input.id !== 'string' || !ID_RE.test(input.id)) errors.push('id must be 3-64 lowercase letters, numbers, dots, underscores or hyphens.');
30
32
  if (BUILT_INS.has(input.id)) errors.push('Built-in scene IDs are reserved.');
31
33
  if (typeof input.name !== 'string' || !input.name.trim() || input.name.length > 80) errors.push('name must be a non-empty string of at most 80 characters.');
@@ -65,6 +67,49 @@
65
67
  }
66
68
  });
67
69
  }
70
+ const assets = input.assets;
71
+ if (input.schemaVersion === 2) {
72
+ if (!isPlainObject(assets) || Object.keys(assets).length < 1) errors.push('schemaVersion 2 requires at least one named pixel asset.');
73
+ else {
74
+ const names = Object.keys(assets);
75
+ if (names.length > MAX_ASSETS) errors.push('assets may contain at most 24 pixel assets.');
76
+ names.forEach(name => {
77
+ const asset = assets[name];
78
+ if (!/^[a-z0-9][a-z0-9._-]{0,47}$/.test(name)) errors.push('Asset names must be lowercase identifiers.');
79
+ if (!isPlainObject(asset)) { errors.push('Asset "' + name + '" must be an object.'); return; }
80
+ unknownKeys(asset, new Set(['width','height','pixels']), 'assets.' + name, errors);
81
+ if (!Number.isInteger(asset.width) || asset.width < 1 || asset.width > MAX_WIDTH) errors.push('Asset "' + name + '" width must be 1-192.');
82
+ if (!Number.isInteger(asset.height) || asset.height < 1 || asset.height > MAX_HEIGHT) errors.push('Asset "' + name + '" height must be 1-112.');
83
+ if (!Array.isArray(asset.pixels) || asset.pixels.length !== asset.height) errors.push('Asset "' + name + '" pixels must match its height.');
84
+ else asset.pixels.forEach((row,index) => {
85
+ if (typeof row !== 'string' || row.length !== asset.width || !Array.from(row).every(symbol => SYMBOL_RE.test(symbol))) errors.push('Asset "' + name + '" row ' + index + ' must be printable ASCII and match its width.');
86
+ });
87
+ });
88
+ }
89
+ if (!Array.isArray(input.layers) || input.layers.length < 1 || input.layers.length > MAX_LAYERS) errors.push('layers must contain 1-16 layer definitions.');
90
+ else input.layers.forEach((layer,index) => {
91
+ if (!isPlainObject(layer)) { errors.push('layers[' + index + '] must be an object.'); return; }
92
+ unknownKeys(layer, new Set(['id','asset','x','y','animation']), 'layers[' + index + ']', errors);
93
+ if (typeof layer.id !== 'string' || !/^[a-z0-9][a-z0-9._-]{0,47}$/.test(layer.id)) errors.push('layers[' + index + '].id must be a lowercase identifier.');
94
+ if (!isPlainObject(assets) || typeof layer.asset !== 'string' || !Object.prototype.hasOwnProperty.call(assets,layer.asset)) errors.push('layers[' + index + '] references an unknown asset.');
95
+ if (!Number.isInteger(layer.x) || layer.x < -MAX_WIDTH || layer.x > MAX_WIDTH) errors.push('layers[' + index + '].x must be an integer from -192 to 192.');
96
+ if (!Number.isInteger(layer.y) || layer.y < -MAX_HEIGHT || layer.y > MAX_HEIGHT) errors.push('layers[' + index + '].y must be an integer from -112 to 112.');
97
+ if (layer.animation !== undefined) {
98
+ const animation = layer.animation;
99
+ if (!isPlainObject(animation)) errors.push('layers[' + index + '].animation must be an object.');
100
+ else {
101
+ unknownKeys(animation,new Set(['type','axis','speed','distance','phase']),'layers[' + index + '].animation',errors);
102
+ if (animation.type !== 'drift' && animation.type !== 'bob') errors.push('Animation type must be drift or bob.');
103
+ if (animation.axis !== 'x' && animation.axis !== 'y') errors.push('Animation axis must be x or y.');
104
+ if (!Number.isFinite(animation.speed) || animation.speed < 0 || animation.speed > 4) errors.push('Animation speed must be between 0 and 4.');
105
+ if (!Number.isInteger(animation.distance) || animation.distance < 1 || animation.distance > 64) errors.push('Animation distance must be an integer from 1 to 64.');
106
+ if (animation.phase !== undefined && (!Number.isFinite(animation.phase) || animation.phase < 0 || animation.phase > 6.2832)) errors.push('Animation phase must be between 0 and 2π.');
107
+ }
108
+ }
109
+ });
110
+ const totalCells = Object.values(assets || {}).reduce((sum,asset) => sum + (asset && Number.isInteger(asset.width) && Number.isInteger(asset.height) ? asset.width * asset.height : 0),0);
111
+ if (totalCells > MAX_TOTAL_CELLS) errors.push('Total pixel asset area exceeds the scene budget.');
112
+ }
68
113
  const timeCycle = input.timeCycle;
69
114
  if (!Array.isArray(timeCycle) || timeCycle.length < 2) errors.push('timeCycle must contain at least two time stops.');
70
115
  else {
@@ -83,6 +128,7 @@
83
128
  if (Array.isArray(input.pixels) && isPlainObject(palettes)) {
84
129
  const symbols = new Set();
85
130
  input.pixels.forEach(row => { if (typeof row === 'string') for (const symbol of row) symbols.add(symbol); });
131
+ if (input.schemaVersion === 2 && isPlainObject(assets)) Object.values(assets).forEach(asset => { if (asset && Array.isArray(asset.pixels)) asset.pixels.forEach(row => { if (typeof row === 'string') for (const symbol of row) symbols.add(symbol); }); });
86
132
  if (symbols.size > MAX_SYMBOLS) errors.push('pixels cannot use more than 64 unique symbols.');
87
133
  Object.keys(palettes).forEach(name => {
88
134
  const palette = palettes[name];
@@ -0,0 +1,229 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://gsvegetable.github.io/pixel-daylight-switch/schemas/scene-manifest.v2.schema.json",
4
+ "title": "Pixel Daylight Switch Scene Manifest v2",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schemaVersion",
9
+ "id",
10
+ "name",
11
+ "author",
12
+ "license",
13
+ "canvas",
14
+ "pixels",
15
+ "palettes",
16
+ "timeCycle",
17
+ "assets",
18
+ "layers"
19
+ ],
20
+ "properties": {
21
+ "schemaVersion": {
22
+ "const": 2
23
+ },
24
+ "id": {
25
+ "type": "string",
26
+ "minLength": 3,
27
+ "maxLength": 64,
28
+ "pattern": "^[a-z0-9][a-z0-9._-]{1,62}[a-z0-9]$"
29
+ },
30
+ "name": {
31
+ "type": "string",
32
+ "minLength": 1,
33
+ "maxLength": 80
34
+ },
35
+ "author": {
36
+ "type": "string",
37
+ "minLength": 1,
38
+ "maxLength": 80
39
+ },
40
+ "license": {
41
+ "type": "string",
42
+ "minLength": 1,
43
+ "maxLength": 64
44
+ },
45
+ "canvas": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "required": [
49
+ "width",
50
+ "height"
51
+ ],
52
+ "properties": {
53
+ "width": {
54
+ "type": "integer",
55
+ "minimum": 1,
56
+ "maximum": 192
57
+ },
58
+ "height": {
59
+ "type": "integer",
60
+ "minimum": 1,
61
+ "maximum": 112
62
+ }
63
+ }
64
+ },
65
+ "pixels": {
66
+ "type": "array",
67
+ "minItems": 1,
68
+ "maxItems": 112,
69
+ "items": {
70
+ "type": "string",
71
+ "minLength": 1,
72
+ "maxLength": 192,
73
+ "pattern": "^[\\x20-\\x7E]+$"
74
+ }
75
+ },
76
+ "palettes": {
77
+ "type": "object",
78
+ "minProperties": 1,
79
+ "maxProperties": 8,
80
+ "additionalProperties": {
81
+ "type": "object",
82
+ "maxProperties": 64,
83
+ "propertyNames": {
84
+ "type": "string",
85
+ "minLength": 1,
86
+ "maxLength": 1,
87
+ "pattern": "^[\\u0020-\\u007E]$"
88
+ },
89
+ "additionalProperties": {
90
+ "type": "string",
91
+ "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
92
+ }
93
+ }
94
+ },
95
+ "timeCycle": {
96
+ "type": "array",
97
+ "minItems": 2,
98
+ "items": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "required": [
102
+ "at",
103
+ "palette"
104
+ ],
105
+ "properties": {
106
+ "at": {
107
+ "type": "number",
108
+ "minimum": 0,
109
+ "maximum": 24
110
+ },
111
+ "palette": {
112
+ "type": "string"
113
+ }
114
+ }
115
+ },
116
+ "maxItems": 48
117
+ },
118
+ "assets": {
119
+ "type": "object",
120
+ "minProperties": 1,
121
+ "maxProperties": 24,
122
+ "additionalProperties": {
123
+ "type": "object",
124
+ "additionalProperties": false,
125
+ "required": [
126
+ "width",
127
+ "height",
128
+ "pixels"
129
+ ],
130
+ "properties": {
131
+ "width": {
132
+ "type": "integer",
133
+ "minimum": 1,
134
+ "maximum": 192
135
+ },
136
+ "height": {
137
+ "type": "integer",
138
+ "minimum": 1,
139
+ "maximum": 112
140
+ },
141
+ "pixels": {
142
+ "type": "array",
143
+ "minItems": 1,
144
+ "maxItems": 112,
145
+ "items": {
146
+ "type": "string",
147
+ "minLength": 1,
148
+ "maxLength": 192,
149
+ "pattern": "^[\\x20-\\x7E]+$"
150
+ }
151
+ }
152
+ }
153
+ }
154
+ },
155
+ "layers": {
156
+ "type": "array",
157
+ "minItems": 1,
158
+ "maxItems": 16,
159
+ "items": {
160
+ "type": "object",
161
+ "additionalProperties": false,
162
+ "required": [
163
+ "id",
164
+ "asset",
165
+ "x",
166
+ "y"
167
+ ],
168
+ "properties": {
169
+ "id": {
170
+ "type": "string",
171
+ "pattern": "^[a-z0-9][a-z0-9._-]{0,47}$"
172
+ },
173
+ "asset": {
174
+ "type": "string"
175
+ },
176
+ "x": {
177
+ "type": "integer",
178
+ "minimum": -192,
179
+ "maximum": 192
180
+ },
181
+ "y": {
182
+ "type": "integer",
183
+ "minimum": -112,
184
+ "maximum": 112
185
+ },
186
+ "animation": {
187
+ "type": "object",
188
+ "additionalProperties": false,
189
+ "required": [
190
+ "type",
191
+ "axis",
192
+ "speed",
193
+ "distance"
194
+ ],
195
+ "properties": {
196
+ "type": {
197
+ "enum": [
198
+ "drift",
199
+ "bob"
200
+ ]
201
+ },
202
+ "axis": {
203
+ "enum": [
204
+ "x",
205
+ "y"
206
+ ]
207
+ },
208
+ "speed": {
209
+ "type": "number",
210
+ "minimum": 0,
211
+ "maximum": 4
212
+ },
213
+ "distance": {
214
+ "type": "integer",
215
+ "minimum": 1,
216
+ "maximum": 64
217
+ },
218
+ "phase": {
219
+ "type": "number",
220
+ "minimum": 0,
221
+ "maximum": 6.2832
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }