pixel-daylight-switch-gsvegetable 3.0.0 → 3.1.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/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
10
  }
11
-
12
- export interface SceneManifest {
13
- schemaVersion: 1;
14
- /** Unique lowercase community scene identifier (3-64 characters). */
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;
28
+ }
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;
@@ -40,8 +54,8 @@ export interface PixelDaylightSwitchAPI {
40
54
  validateScene(manifest: unknown): SceneValidationResult;
41
55
  getScene(id: string): SceneManifest | null;
42
56
  listScenes(): SceneManifest[];
57
+ createGlass: typeof createGlass;
43
58
  }
44
-
45
59
  declare const PixelDaylightSwitch: PixelDaylightSwitchAPI;
46
60
  export declare const mount: PixelDaylightSwitchAPI['mount'];
47
61
  export declare const unmount: PixelDaylightSwitchAPI['unmount'];
@@ -50,3 +64,22 @@ export declare const validateScene: PixelDaylightSwitchAPI['validateScene'];
50
64
  export declare const getScene: PixelDaylightSwitchAPI['getScene'];
51
65
  export declare const listScenes: PixelDaylightSwitchAPI['listScenes'];
52
66
  export default PixelDaylightSwitch;
67
+
68
+ export type GlassSource = HTMLCanvasElement | OffscreenCanvas | HTMLImageElement | HTMLVideoElement | ImageBitmap | (() => GlassSource);
69
+ export interface GlassEngineOptions {
70
+ source?: GlassSource;
71
+ width?: number | string; height?: number | string;
72
+ left?: number | string; top?: number | string;
73
+ radius?: number | string; refraction?: number; dispersion?: number;
74
+ bezel?: number; shine?: number; opacity?: number; edgeDark?: number;
75
+ ink?: [number, number, number]; light?: [number, number, number];\n shellDay?: [number, number, number]; shellNight?: [number, number, number];\n hiNight?: [number, number, number]; loNight?: [number, number, number]; night?: number;
76
+ dpr?: number; continuous?: boolean;
77
+ }
78
+ export interface GlassEngineInstance {
79
+ canvas: HTMLCanvasElement;
80
+ setSource(source: GlassSource): this;
81
+ setOptions(options: Partial<GlassEngineOptions>): this;
82
+ render(): this;
83
+ destroy(): void;
84
+ }
85
+ export declare function createGlass(container: HTMLElement, options?: GlassEngineOptions): GlassEngineInstance;
package/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import api from './index.cjs';
2
+ import { createGlass } from './glass-engine.mjs';
2
3
 
3
4
  export const mount = api.mount;
4
5
  export const unmount = api.unmount;
@@ -6,4 +7,5 @@ export const registerScene = api.registerScene;
6
7
  export const validateScene = api.validateScene;
7
8
  export const getScene = api.getScene;
8
9
  export const listScenes = api.listScenes;
10
+ export { createGlass };
9
11
  export default api;
package/llms.txt CHANGED
@@ -42,11 +42,17 @@ CommonJS: `const PixelDaylightSwitch = require('pixel-daylight-switch-gsvegetabl
42
42
 
43
43
  The package is safe to import during SSR; mount only after the target DOM element exists. `mount(element)` is idempotent and returns a cleanup function on success (or null on failure). `unmount(element)` stops rendering and removes observers/listeners; it does not remove the DOM root or forcibly invalidate the canvas context.
44
44
 
45
- Framework lifecycle examples are in [INTEGRATIONS.md](INTEGRATIONS.md). Scene plugin authoring, manifest constraints and API examples are in [SCENE_PLUGINS.md](SCENE_PLUGINS.md).
45
+ Framework lifecycle examples are in [INTEGRATIONS.md](docs/INTEGRATIONS.md). Scene plugin authoring, manifest constraints and API examples are in [SCENE_PLUGINS.md](docs/SCENE_PLUGINS.md).
46
46
 
47
- ## Community scene plugins (v3.0.0)
47
+ ## Standalone refractive glass engine (v3.1.1)
48
48
 
49
- The optional `scene-plugin.js` registry exposes `registerScene(manifest)`, `validateScene(manifest)`, `getScene(id)` and `listScenes()`. Load the registry before `app.js` in a browser, or import `registerScene` from the npm package. Register a scene before mounting its root. Manifest v1 uses a 1–192 by 1–112 printable-ASCII pixel map, up to 8 named palettes, and a 24-hour timeline that starts at 0 and ends at 24. Every used pixel symbol must have a valid `#RGB` or `#RRGGBB` color in every palette. Data is copied and deeply frozen on registration. Arbitrary plugin JavaScript is never executed. External image/sprite assets, layered tile maps and custom animation tracks are not supported in v1; they are reserved for a future schema extension. See [SCENE_PLUGINS.md](SCENE_PLUGINS.md), the [v1 JSON Schema](schemas/scene-manifest.v1.schema.json), and the [Sakura example](examples/scenes/sakura-evening.json).
49
+ Import `createGlass` from `pixel-daylight-switch-gsvegetable/glass-engine`, or load `glass-engine.js` directly and use `PixelGlassEngine.createGlass(container, options)`. The independent WebGL2 overlay has configurable width, height, position, radius, refraction, dispersion, bezel, shine, opacity, source and lifecycle methods. Sources are origin-clean canvas, image, video, ImageBitmap or a callback returning one. WebGL cannot sample arbitrary DOM pixels; host applications must supply a renderable background source. See [GLASS_ENGINE.md](docs/GLASS_ENGINE.md) and [the demo](https://gsvegetable.github.io/pixel-daylight-switch/demos/glass-engine.html).
50
+
51
+ ## Community scene plugins (current: Manifest v2)
52
+
53
+ The optional `scene-plugin.js` registry exposes `registerScene(manifest)`, `validateScene(manifest)`, `getScene(id)` and `listScenes()`. Register a data-only JSON manifest before mounting the scene. Manifest v2 supports up to 24 reusable pixel-matrix assets, up to 16 ordered layers, transparent overlay pixels (.), and bounded deterministic drift/bob motion tracks. Manifest v1 remains supported and its schema is unchanged. Every symbol in the base matrix and assets must be defined in each palette. Registration validates, copies and freezes the data; arbitrary JavaScript, remote image downloads and executable callbacks are not run.
54
+
55
+ For npm, import `registerScene` from the package. In a browser, load `scene-plugin.js` before `app.js` and register the manifest before mounting. See [the scene authoring guide](docs/SCENE_PLUGINS.md), [Manifest v2 Schema](schemas/scene-manifest.v2.schema.json), [v1 compatibility schema](schemas/scene-manifest.v1.schema.json) and [Floating Island v2 example](examples/scenes/floating-island.v2.json).
50
56
 
51
57
  ## Scenes and weather
52
58
 
@@ -93,19 +99,19 @@ Invalid values fall back to defaults; documented numeric ranges are clamped.
93
99
  ## Links
94
100
 
95
101
  - [Live demo](https://gsvegetable.github.io/pixel-daylight-switch/)
96
- - [Interactive Playground](https://gsvegetable.github.io/pixel-daylight-switch/playground.html)
97
- - [Preview](https://gsvegetable.github.io/pixel-daylight-switch/preview.html)
98
- - [Integration recipes](INTEGRATIONS.md)
102
+ - [Interactive Playground](https://gsvegetable.github.io/pixel-daylight-switch/demos/playground.html)
103
+ - [Preview](https://gsvegetable.github.io/pixel-daylight-switch/demos/preview.html)
104
+ - [Integration recipes](docs/INTEGRATIONS.md)
99
105
  - [English README](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md)
100
- - [Changelog](CHANGELOG.md)
106
+ - [Changelog](docs/CHANGELOG.md)
101
107
 
102
108
 
103
- ## Scene plugins (v3.0.0)
109
+ ## Scene plugins — current Manifest v2
104
110
 
105
- - Public API: registerScene(manifest), validateScene(manifest), getScene(id), listScenes().
106
- - Manifest schema: schemas/scene-manifest.v1.schema.json.
107
- - Example: examples/scenes/sakura-evening.json.
108
- - Manifest v1: printable-ASCII pixel matrix (up to 192x112), named palettes, 24-hour palette interpolation.
109
- - Scene registration is data-only; arbitrary plugin JavaScript is not executed.
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
- - Existing built-in scenes and their custom live animation code remain unchanged.
111
+ - Current schema: `schemas/scene-manifest.v2.schema.json`.
112
+ - Example: `examples/scenes/floating-island.v2.json`.
113
+ - Reusable assets: up to 24 pixel matrices; ordered layers: up to 16.
114
+ - Overlay dot (.) is transparent; later layers appear above earlier layers.
115
+ - Motion tracks: bounded deterministic `drift` and `bob`, on x or y.
116
+ - Manifest v1 remains backward compatible; its schema is retained.
117
+ - 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.1.1",
4
+ "description": "A pixel-art day/night switch plus a standalone reusable WebGL2 refractive glass engine.",
5
5
  "keywords": [
6
6
  "pixel-art",
7
7
  "day-night",
@@ -17,7 +17,11 @@
17
17
  "scene-plugin",
18
18
  "pixel-world",
19
19
  "scene-manifest",
20
- "pixel-art-scenes"
20
+ "pixel-art-scenes",
21
+ "refractive-glass",
22
+ "glass-engine",
23
+ "webgl2-glass",
24
+ "lens-shader"
21
25
  ],
22
26
  "author": "GSvegetable",
23
27
  "license": "Unlicense",
@@ -44,6 +48,11 @@
44
48
  "./scene-plugin": {
45
49
  "import": "./scene-plugin.js",
46
50
  "require": "./scene-plugin.js"
51
+ },
52
+ "./glass-engine": {
53
+ "types": "./index.d.ts",
54
+ "import": "./glass-engine.mjs",
55
+ "require": "./glass-engine.js"
47
56
  }
48
57
  },
49
58
  "files": [
@@ -53,13 +62,17 @@
53
62
  "README.md",
54
63
  "LICENSE",
55
64
  "llms.txt",
56
- "INTEGRATIONS.md",
57
- "SCENE_PLUGINS.md",
65
+ "docs/INTEGRATIONS.md",
66
+ "docs/SCENE_PLUGINS.md",
58
67
  "scene-plugin.js",
59
68
  "schemas",
60
69
  "examples",
61
70
  "browser.mjs",
62
- "index.d.ts"
71
+ "index.d.ts",
72
+ "docs/CHANGELOG.md",
73
+ "glass-engine.js",
74
+ "docs/GLASS_ENGINE.md",
75
+ "glass-engine.mjs"
63
76
  ],
64
77
  "sideEffects": [
65
78
  "./app.js"
@@ -71,8 +84,8 @@
71
84
  "access": "public"
72
85
  },
73
86
  "scripts": {
74
- "test": "node tests/package-smoke.cjs && node tests/scene-plugin-smoke.cjs && node tests/mobile-smoke.cjs && node tests/embed-smoke.cjs",
75
- "test:browser": "playwright test",
87
+ "test": "node tests/package-smoke.cjs && node tests/scene-plugin-smoke.cjs && node tests/mobile-smoke.cjs && node tests/embed-smoke.cjs && node tests/glass-engine-smoke.cjs",
88
+ "test:browser": "playwright test --config config/playwright.config.cjs",
76
89
  "mobile:prepare": "node scripts/prepare-mobile.cjs",
77
90
  "mobile:android": "npm run mobile:prepare && cap sync android && node scripts/set-android-version.cjs android/app/build.gradle",
78
91
  "mobile:ios": "npm run mobile:prepare && cap sync ios",
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
+ }