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 +11 -1
- package/README.md +21 -1
- package/README.zh-CN.md +11 -1
- package/SCENE_PLUGINS.md +19 -3
- package/app.js +33 -3
- package/examples/scenes/floating-island.v2.json +152 -0
- package/index.d.ts +34 -21
- package/llms.txt +11 -0
- package/package.json +2 -2
- package/scene-plugin.js +49 -3
- package/schemas/scene-manifest.v2.schema.json +229 -0
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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`:
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
4
|
-
"description": "A draggable pixel-art day/night switch with
|
|
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
|
+
}
|