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/README.ja.md +65 -52
- package/README.md +64 -63
- package/README.zh-CN.md +65 -52
- package/app.js +33 -3
- package/browser.mjs +4 -0
- package/docs/CHANGELOG.md +127 -0
- package/docs/GLASS_ENGINE.md +65 -0
- package/{INTEGRATIONS.md → docs/INTEGRATIONS.md} +22 -1
- package/docs/README.md +5 -0
- package/{SCENE_PLUGINS.md → docs/SCENE_PLUGINS.md} +22 -6
- package/examples/scenes/floating-island.v2.json +152 -0
- package/glass-engine.js +162 -0
- package/glass-engine.mjs +4 -0
- package/index.cjs +1 -0
- package/index.d.ts +54 -21
- package/index.mjs +2 -0
- package/llms.txt +21 -15
- package/package.json +21 -8
- package/scene-plugin.js +49 -3
- package/schemas/scene-manifest.v2.schema.json +229 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Changelog\n\n## [3.1.1] — Standalone refractive glass engine\n\n- Extract the refractive lens into a separately mountable WebGL2 overlay with no fixed aspect ratio or switch dependency.\n- Expose configurable geometry, refraction, dispersion, bezel, shine, opacity and lifecycle methods.\n- Accept origin-clean canvas, image, video and bitmap sources; document browser security boundaries for DOM backgrounds.\n- Add a dedicated glass-engine demo, integration guide, TypeScript API and smoke checks.\n- Keep the existing day/night switch renderer and all five scenes unchanged.\n\n [3.0.1] — Layered community scenes
|
|
2
|
+
|
|
3
|
+
- Add Manifest v2 with reusable pixel-matrix assets and ordered scene layers.
|
|
4
|
+
- Add bounded, deterministic drift and bob motion tracks without executing third-party code.
|
|
5
|
+
- Preserve the original Manifest v1 schema and registration behavior.
|
|
6
|
+
- Add a Floating Island example, v2 JSON Schema, TypeScript declarations and validation tests.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## [3.0.0] — Community scene plugins
|
|
10
|
+
|
|
11
|
+
- Add a validated, data-only Scene Manifest v1 and public scene registry API.
|
|
12
|
+
- Render registered pixel-matrix scenes through the existing refractive glass renderer with interpolated 24-hour palettes.
|
|
13
|
+
- Expose scene registration from CommonJS and ESM package entry points.
|
|
14
|
+
- Add CDN registry loading, a Sakura Evening example, JSON Schema, documentation and automated tests.
|
|
15
|
+
- Preserve all five built-in scene implementations and their dedicated live animation systems.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
All notable changes to Pixel Daylight Switch are documented here.
|
|
19
|
+
|
|
20
|
+
## [Unreleased] — Developer integration
|
|
21
|
+
|
|
22
|
+
- Make the one-line embed responsive and prevent demo-page global CSS from leaking into host websites.
|
|
23
|
+
- Generate responsive, copy-ready integration snippets from the Playground.
|
|
24
|
+
- Add React and Vue 3 lifecycle integration recipes.
|
|
25
|
+
- Refresh `llms.txt` and add smoke/browser regression coverage for embed isolation.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## [2.2.0] — Mobile scene controls
|
|
29
|
+
|
|
30
|
+
### Mobile app experience
|
|
31
|
+
- Add a dedicated mobile app shell with five selectable scenes: countryside, Japanese street, apartment, seaside and mountain cabin.
|
|
32
|
+
- Add an optional snowfall control.
|
|
33
|
+
- Add live day/night status, device-language UI (English, Simplified Chinese and Japanese), accessible pressed states and compact portrait/landscape layouts.
|
|
34
|
+
- Reuse the shared renderer and preserve the glass shader, refraction, highlights, dispersion and drag interaction.
|
|
35
|
+
|
|
36
|
+
### Documentation and verification
|
|
37
|
+
- Update English, Simplified Chinese and Japanese README files with the mobile controls.
|
|
38
|
+
- Extend mobile smoke checks for all five scene choices, weather control and renderer remount behavior.
|
|
39
|
+
|
|
40
|
+
## [2.1.0] — Android visual and versioning fix
|
|
41
|
+
|
|
42
|
+
### Android app
|
|
43
|
+
- Replace the warm yellow-beige native page background with a neutral cool gray.
|
|
44
|
+
- Match the Capacitor native window background and mobile theme color to the page background.
|
|
45
|
+
- Set Android versionName and versionCode from the package version so new APKs no longer appear as version 1.0.
|
|
46
|
+
|
|
47
|
+
### Build reliability
|
|
48
|
+
- Apply package-driven Android version metadata after each generated Capacitor project.
|
|
49
|
+
- Keep the shared WebGL renderer and scene implementation unchanged.
|
|
50
|
+
|
|
51
|
+
## [2.0.0] — Released
|
|
52
|
+
|
|
53
|
+
### Android 与 iOS
|
|
54
|
+
- 引入 Capacitor 8 原生应用构建路径,支持 Android 与 iOS 共用现有 Web 渲染器。
|
|
55
|
+
- 新增独立移动端入口与全屏布局,处理 viewport、安全区域、横竖屏和触控拖动。
|
|
56
|
+
- 新增移动资源准备脚本与 smoke test,确保原生包使用与 Web 完全相同的 `app.js`。
|
|
57
|
+
- 新增 Android Debug APK 与 iOS Simulator 自动构建工作流。
|
|
58
|
+
|
|
59
|
+
### 架构与兼容性
|
|
60
|
+
- Web 继续保持独立运行;不重写、不复制 WebGL shader、场景数据或交互物理逻辑。
|
|
61
|
+
- 原生 Android / iOS 工程按需由 Capacitor 生成,不把 IDE 派生文件提交到仓库。
|
|
62
|
+
- 本版本不包含已签名商店包;正式发布仍需开发者签名与商店配置。
|
|
63
|
+
|
|
64
|
+
## [1.3.1]
|
|
65
|
+
|
|
66
|
+
### 稳定性与生命周期
|
|
67
|
+
- 修复组件卸载时主动销毁 WebGL context,导致同一 canvas/root 后续重新挂载可能异常的问题。
|
|
68
|
+
- 保留 GPU 资源释放与事件监听器、观察器清理,同时允许 canvas 在卸载后重新挂载。
|
|
69
|
+
|
|
70
|
+
### 自动化验证
|
|
71
|
+
- 增加 CommonJS、ESM 与 SSR 导入检查。
|
|
72
|
+
- 增加 Node.js 18、20、22 的 CI 兼容性检查。
|
|
73
|
+
- 增加 Chromium 浏览器回归:自动挂载、重复挂载/卸载、多实例、指针交互、事件监听器清理。
|
|
74
|
+
- 增加移动端触控、Playground 小屏布局与参数应用检查。
|
|
75
|
+
- 将浏览器回归纳入 npm 发布前置检查。
|
|
76
|
+
|
|
77
|
+
## [1.3.0]
|
|
78
|
+
|
|
79
|
+
<details>
|
|
80
|
+
<summary>查看 v1.3.0 详细更新</summary>
|
|
81
|
+
|
|
82
|
+
### 场景与世界
|
|
83
|
+
- 新增日本街道:店铺、暖帘、路灯、自动售货机、自行车与道路细节。
|
|
84
|
+
- 新增现代公寓:城市窗景、沙发、茶几、落地灯、室内植物与地毯。
|
|
85
|
+
- 新增海边:海面、沙滩、灯塔、帆船与海岸植被。
|
|
86
|
+
- 新增山间木屋:层叠山脉、针叶林、雪顶、木屋与烟囱。
|
|
87
|
+
- 保留乡村小屋作为默认场景。
|
|
88
|
+
- 新增 `data-scene` 场景属性,并在 Playground 中加入场景选择。
|
|
89
|
+
|
|
90
|
+
### Playground 与参数组织
|
|
91
|
+
- 参数按用途拆分:滑动响应速度、惯性与回弹归入运动反馈;折射、边缘厚度、色散与高光归入玻璃光学;时间倍率单独归入世界节奏;场景、天气和初始昼夜归入场景环境。
|
|
92
|
+
|
|
93
|
+
### 兼容性
|
|
94
|
+
- 默认仍为 `countryside`,既有接入方式不需要修改。
|
|
95
|
+
- 场景切换不改变开关物理、玻璃 shader 与昼夜时间逻辑。
|
|
96
|
+
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
## [1.2.0]
|
|
100
|
+
|
|
101
|
+
<details>
|
|
102
|
+
<summary>查看 v1.2.0 详细更新</summary>
|
|
103
|
+
|
|
104
|
+
### 参数与世界
|
|
105
|
+
- 新增 `data-friction`、`data-bounce`,分别调节惯性阻尼与边缘回弹。
|
|
106
|
+
- 新增 `data-dispersion`、`data-shine`,调节玻璃边缘色散与表面高光。
|
|
107
|
+
- 新增 `data-time-speed` 时间倍率,以及 `data-weather="snow"` 飘雪天气。
|
|
108
|
+
|
|
109
|
+
### Playground
|
|
110
|
+
- 新增独立参数实验室,支持分组调节、预设和 HTML 代码复制。
|
|
111
|
+
- 重构实验室视觉,采用更克制的编辑式布局。
|
|
112
|
+
- 通过独立预览与双缓冲切换,减少快速调参时的 WebGL 重建与白屏。
|
|
113
|
+
|
|
114
|
+
### 文档
|
|
115
|
+
- README 改为中文优先,并整理参数说明与英文文档。
|
|
116
|
+
|
|
117
|
+
### 兼容性
|
|
118
|
+
- 新增参数均为可选项;未设置时保留原有默认行为。
|
|
119
|
+
|
|
120
|
+
</details>
|
|
121
|
+
|
|
122
|
+
## [1.1.0]
|
|
123
|
+
- 新增动画速度、镜片折射、外框厚度和初始夜晚状态的 HTML 参数配置。
|
|
124
|
+
|
|
125
|
+
## [1.0.0]
|
|
126
|
+
- 首个公开版本。
|
|
127
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Standalone Glass Engine
|
|
2
|
+
|
|
3
|
+
PixelGlassEngine extracts the project's refractive lens into a reusable WebGL2 overlay. It is independent of the day/night switch: it does not create a switch, impose an aspect ratio, or set a fixed component size.
|
|
4
|
+
|
|
5
|
+
## Browser
|
|
6
|
+
|
|
7
|
+
Load `glass-engine.js`, then pass a source that the browser can upload to WebGL:
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div id="card" style="position:relative;width:100%;height:260px;overflow:hidden">
|
|
11
|
+
<canvas id="background" width="1200" height="520"></canvas>
|
|
12
|
+
</div>
|
|
13
|
+
<script src="https://gsvegetable.github.io/pixel-daylight-switch/glass-engine.js"></script>
|
|
14
|
+
<script>
|
|
15
|
+
const card = document.querySelector('#card');
|
|
16
|
+
const background = document.querySelector('#background');
|
|
17
|
+
const glass = PixelGlassEngine.createGlass(card, {
|
|
18
|
+
source: background,
|
|
19
|
+
width: '72%',
|
|
20
|
+
height: '68%',
|
|
21
|
+
left: '14%',
|
|
22
|
+
top: '16%',
|
|
23
|
+
radius: 32,
|
|
24
|
+
refraction: 1.24,
|
|
25
|
+
dispersion: 0.05,
|
|
26
|
+
bezel: 0.046,
|
|
27
|
+
shine: 0.44
|
|
28
|
+
});
|
|
29
|
+
// Later: glass.setOptions({ refraction: 1.32 });
|
|
30
|
+
// Before removing the card: glass.destroy();
|
|
31
|
+
</script>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## npm
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { createGlass } from 'pixel-daylight-switch-gsvegetable/glass-engine';
|
|
38
|
+
|
|
39
|
+
const glass = createGlass(container, { source: backgroundCanvas, width: '70%', height: 160, radius: 28 });
|
|
40
|
+
glass.setOptions({ dispersion: 0.08 });
|
|
41
|
+
glass.setSource(nextCanvasOrImage);
|
|
42
|
+
glass.destroy();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Source contract and browser security
|
|
46
|
+
|
|
47
|
+
The source may be an HTMLCanvasElement, OffscreenCanvas, HTMLImageElement, HTMLVideoElement, ImageBitmap, or a function returning one of those objects. The source must be ready and origin-clean. Cross-origin images require appropriate CORS headers and `crossOrigin = "anonymous"` before assigning `src`.
|
|
48
|
+
|
|
49
|
+
A browser does not expose arbitrary DOM pixels as a WebGL texture. Therefore this engine does not claim to refract any arbitrary page behind an overlay automatically. For a live webpage, provide a canvas snapshot/render target from the host application (for example, the same canvas used by a chart, game, or custom renderer). A DOM-to-canvas adapter is application-specific and may be blocked by cross-origin content, video, fonts, or browser security.
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
- `source`: canvas/image/video/bitmap or a callback returning one.
|
|
54
|
+
- `width`, `height`, `left`, `top`: numbers in CSS pixels or CSS length strings; no fixed aspect ratio.
|
|
55
|
+
- `radius`: number in CSS pixels or CSS length string.
|
|
56
|
+
- `refraction`: magnification factor; default `1.24`.
|
|
57
|
+
- `dispersion`: RGB edge separation, clamped to `0–0.25`.
|
|
58
|
+
- `bezel`: relative edge thickness, clamped to `0–0.3`.
|
|
59
|
+
- `shine`: highlight intensity.
|
|
60
|
+
- `opacity`: glass opacity from `0–1`.
|
|
61
|
+
- `ink`, `light`: RGB arrays in normalized `0–1`.\n- `shellDay`, `shellNight`, `hiNight`, `loNight`, `night`: day/night environment colors and blend amount, preserving the original renderer's lighting controls.
|
|
62
|
+
- `dpr`: render pixel-ratio ceiling, default `2`.
|
|
63
|
+
- `continuous`: redraw continuously for animated sources; set `false` for static sources and call `render()` when the source changes.
|
|
64
|
+
|
|
65
|
+
The renderer uses WebGL2. It throws a clear error if WebGL2 is unavailable rather than silently substituting a visually unrelated fake-refraction effect.
|
|
@@ -123,7 +123,7 @@ unregister();
|
|
|
123
123
|
|
|
124
124
|
For CDN usage, load `scene-plugin.js` before `app.js`, then call `PixelDaylightSwitchScenePlugins.registerScene(manifest)` before mounting. The embed loader loads both scripts in the correct order for built-in scenes; custom manifests still need to be registered by the host page.
|
|
125
125
|
|
|
126
|
-
Manifest v1 is data-only: a bounded pixel matrix, named palettes and a 24-hour palette timeline. See [the schema](schemas/scene-manifest.v1.schema.json) and [the example](examples/scenes/sakura-evening.json). Do not place executable code, remote script URLs or untrusted HTML in a manifest.
|
|
126
|
+
Manifest v1 is data-only: a bounded pixel matrix, named palettes and a 24-hour palette timeline. See [the schema](../schemas/scene-manifest.v1.schema.json) and [the example](../examples/scenes/sakura-evening.json). Do not place executable code, remote script URLs or untrusted HTML in a manifest.
|
|
127
127
|
|
|
128
128
|
## Lifecycle and events
|
|
129
129
|
|
|
@@ -137,3 +137,24 @@ Manifest v1 is data-only: a bounded pixel matrix, named palettes and a 24-hour p
|
|
|
137
137
|
## Configuration
|
|
138
138
|
|
|
139
139
|
See [README.md](README.md) for the complete list of `data-*` attributes, CSS custom properties, rendering behavior and supported scene identifiers.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## Standalone refractive glass
|
|
143
|
+
|
|
144
|
+
The independent glass layer is exported as `createGlass` and does not mount or depend on the day/night switch:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
import { createGlass } from 'pixel-daylight-switch-gsvegetable/glass-engine';
|
|
148
|
+
const glass = createGlass(container, {
|
|
149
|
+
source: backgroundCanvas,
|
|
150
|
+
width: '70%', height: 180, left: '15%', top: 24,
|
|
151
|
+
radius: 28, refraction: 1.24, dispersion: 0.05,
|
|
152
|
+
bezel: 0.046, shine: 0.44
|
|
153
|
+
});
|
|
154
|
+
// Update, swap source, and clean up independently.
|
|
155
|
+
glass.setOptions({ refraction: 1.32 });
|
|
156
|
+
glass.setSource(nextCanvas);
|
|
157
|
+
glass.destroy();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
See [the complete glass engine guide](GLASS_ENGINE.md) and [live parameter demo](https://gsvegetable.github.io/pixel-daylight-switch/demos/glass-engine.html). The source must be an origin-clean canvas, image, video, bitmap or callback returning one. Arbitrary DOM cannot be sampled directly by WebGL; host applications must provide a renderable source.
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Project documentation
|
|
2
|
+
|
|
3
|
+
- [Framework integration recipes](INTEGRATIONS.md) — React, Vue and plain HTML integration.
|
|
4
|
+
- [Scene plugin authoring guide](SCENE_PLUGINS.md) — Manifest v2 assets, layers, motion and v1 compatibility.
|
|
5
|
+
- [Changelog](CHANGELOG.md) — release history.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Scene Plugin Guide
|
|
2
2
|
|
|
3
|
-
Pixel Daylight Switch
|
|
3
|
+
The Pixel Daylight Switch community scene system supports 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
|
|
|
7
|
-
**Palette creator:** Start with
|
|
7
|
+
**Palette creator:** Start with `../examples/scenes/sakura-evening.json`, keep its pixel rows, and change the colors under `palettes`. This is the quickest way to create a distinct atmosphere.
|
|
8
8
|
|
|
9
9
|
**Pixel-world creator:** Edit `canvas` and `pixels` to design your own geometry, then define a color for every symbol in each palette. Each character represents one pixel; rows must all have the declared width.
|
|
10
10
|
|
|
@@ -38,7 +38,7 @@ Load the registry before the renderer:
|
|
|
38
38
|
<script src="https://gsvegetable.github.io/pixel-daylight-switch/app.js"></script>
|
|
39
39
|
<div data-ld-part="toggle-daylight" data-scene="sakura-evening"></div>
|
|
40
40
|
<script>
|
|
41
|
-
|
|
41
|
+
PixelDaylightSwitchScenePlugins.registerScene(sceneManifest);
|
|
42
42
|
</script>
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -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.
|
|
@@ -60,7 +60,23 @@ Required fields:
|
|
|
60
60
|
|
|
61
61
|
Every symbol used in `pixels` must have a color in every palette. The renderer interpolates the RGB values between adjacent time stops, including across midnight. Keep the first and last palette compatible to avoid a visible midnight jump.
|
|
62
62
|
|
|
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).
|
|
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
|
+
|
|
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.
|
|
64
80
|
|
|
65
81
|
## API
|
|
66
82
|
|
|
@@ -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.
|
|
@@ -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/glass-engine.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Pixel Daylight Switch — standalone refractive glass engine
|
|
3
|
+
* Unlicense. The optical lens equations are adapted from the original app.js shader.
|
|
4
|
+
*/
|
|
5
|
+
(function (root, factory) {
|
|
6
|
+
'use strict';
|
|
7
|
+
const api = factory();
|
|
8
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
9
|
+
if (root) root.PixelGlassEngine = api;
|
|
10
|
+
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
11
|
+
'use strict';
|
|
12
|
+
const VERT = `#version 300 es
|
|
13
|
+
in vec2 aPos;
|
|
14
|
+
void main(){ gl_Position=vec4(aPos,0.0,1.0); }`;
|
|
15
|
+
const FRAG = `#version 300 es
|
|
16
|
+
precision highp float;
|
|
17
|
+
uniform sampler2D uSource;
|
|
18
|
+
uniform vec2 uResolution, uCenter, uHalf, uSourceSize;
|
|
19
|
+
uniform float uRadius, uRefraction, uDispersion, uBezel, uShine, uOpacity, uEdgeDark;
|
|
20
|
+
uniform vec3 uInk, uLight, uShellDay, uShellNight, uHiNight, uLoNight;\n uniform float uNight;\n vec3 envLow, envHigh;
|
|
21
|
+
out vec4 outColor;
|
|
22
|
+
float sdRoundBox(vec2 p, vec2 b, float r) {
|
|
23
|
+
vec2 q=abs(p)-b+vec2(r);
|
|
24
|
+
return length(max(q,0.0))+min(max(q.x,q.y),0.0)-r;
|
|
25
|
+
}
|
|
26
|
+
float softBox(vec2 p, vec2 c, vec2 h, float s) {
|
|
27
|
+
vec2 q=abs(p-c)-h;
|
|
28
|
+
float d=length(max(q,0.0))+min(max(q.x,q.y),0.0);
|
|
29
|
+
return 1.0-smoothstep(-s,s,d);
|
|
30
|
+
}
|
|
31
|
+
vec3 smoothTex(vec2 uv) {
|
|
32
|
+
vec2 st=uv*uSourceSize-0.5, i=floor(st), f=st-i, f2=f*f, f3=f2*f;
|
|
33
|
+
vec2 w0=(1.0-3.0*f+3.0*f2-f3)/6.0;
|
|
34
|
+
vec2 w1=(4.0-6.0*f2+3.0*f3)/6.0;
|
|
35
|
+
vec2 w2=(1.0+3.0*f+3.0*f2-3.0*f3)/6.0;
|
|
36
|
+
vec2 w3=f3/6.0, g0=w0+w1, g1=w2+w3;
|
|
37
|
+
vec2 h0=(i-0.5+w1/g0+0.5)/uSourceSize;
|
|
38
|
+
vec2 h1=(i+1.5+w3/g1+0.5)/uSourceSize;
|
|
39
|
+
return g0.y*(g0.x*texture(uSource,vec2(h0.x,h0.y)).rgb+g1.x*texture(uSource,vec2(h1.x,h0.y)).rgb)
|
|
40
|
+
+g1.y*(g0.x*texture(uSource,vec2(h0.x,h1.y)).rgb+g1.x*texture(uSource,vec2(h1.x,h1.y)).rgb);
|
|
41
|
+
}
|
|
42
|
+
vec3 studio(vec3 R) {
|
|
43
|
+
vec3 c=mix(envLow,envHigh,smoothstep(-0.55,0.85,R.y));
|
|
44
|
+
vec2 s=R.xy/(1.0+max(R.z,0.0));
|
|
45
|
+
float key=softBox(s,vec2(-0.1,0.56),vec2(0.5,0.11),0.17);
|
|
46
|
+
float fill=softBox(s,vec2(0.34,-0.6),vec2(0.3,0.05),0.2);
|
|
47
|
+
c+=envHigh*key*1.15;
|
|
48
|
+
c+=envHigh*fill*0.14;
|
|
49
|
+
return c;
|
|
50
|
+
}
|
|
51
|
+
void main(){
|
|
52
|
+
envLow=mix(uShellDay*0.74,uLoNight,uNight);
|
|
53
|
+
envHigh=mix(uLight,uHiNight,uNight);
|
|
54
|
+
vec2 frag=gl_FragCoord.xy;
|
|
55
|
+
vec2 p=frag-uCenter;
|
|
56
|
+
float d=sdRoundBox(p,uHalf,uRadius);
|
|
57
|
+
if(d>1.0){outColor=vec4(0.0);return;}
|
|
58
|
+
vec2 q=p;
|
|
59
|
+
vec2 inner=max(uHalf-uRadius,vec2(0.0));
|
|
60
|
+
q.x=sign(p.x)*max(abs(p.x)-inner.x,0.0);
|
|
61
|
+
q.y=sign(p.y)*max(abs(p.y)-inner.y,0.0);
|
|
62
|
+
float rad=length(q);
|
|
63
|
+
vec2 gr=q/max(rad,1.0e-3);
|
|
64
|
+
float rho=clamp(rad/max(uRadius,1.0),0.0,1.0);
|
|
65
|
+
float mag=max(uRefraction,1.001);
|
|
66
|
+
float bt=clamp((rho-0.7)/0.3,0.0,1.0);
|
|
67
|
+
float band=bt*bt*(3.0-2.0*bt);
|
|
68
|
+
float strength=1.0-1.0/mag+0.2;
|
|
69
|
+
float bend=pow(band,2.2)*uRadius*strength;
|
|
70
|
+
vec2 base=uCenter+p/mag;
|
|
71
|
+
vec2 shiftDir=gr*bend;
|
|
72
|
+
float ca=uDispersion*band;
|
|
73
|
+
vec2 uvR=(base+shiftDir*(1.0-ca))/uResolution;
|
|
74
|
+
vec2 uvG=(base+shiftDir)/uResolution;
|
|
75
|
+
vec2 uvB=(base+shiftDir*(1.0+ca))/uResolution;
|
|
76
|
+
vec3 seen;
|
|
77
|
+
if(band>0.02) seen=vec3(smoothTex(uvR).r,smoothTex(uvG).g,smoothTex(uvB).b);
|
|
78
|
+
else seen=smoothTex(uvG);
|
|
79
|
+
vec2 tilt=gr*(0.22*rho+0.78*pow(rho,7.0));
|
|
80
|
+
vec3 N=normalize(vec3(tilt,sqrt(max(1.0-dot(tilt,tilt),0.02))));
|
|
81
|
+
vec3 R=vec3(2.0*N.z*N.xy,2.0*N.z*N.z-1.0);
|
|
82
|
+
vec2 lp=q/max(uRadius,1.0);
|
|
83
|
+
vec3 lens=seen*1.035;
|
|
84
|
+
float edge=pow(rho,16.0);
|
|
85
|
+
lens=mix(lens,uInk,edge*0.42);
|
|
86
|
+
float pool=smoothstep(0.05,0.95,-lp.y*0.9+lp.x*0.2)*smoothstep(1.0,0.8,rho)*smoothstep(0.42,0.86,rho);
|
|
87
|
+
lens+=envHigh*pool*mix(0.3,0.1,uNight);
|
|
88
|
+
float broad=softBox(lp,vec2(-0.1,0.62),vec2(0.5,0.17),0.3)*smoothstep(0.98,0.72,rho);
|
|
89
|
+
lens=mix(lens,envHigh,broad*mix(uShine,0.1,uNight));
|
|
90
|
+
float fresnel=pow(1.0-N.z,3.0);
|
|
91
|
+
float dir=0.2+0.8*smoothstep(-0.6,0.85,dot(gr,normalize(vec2(-0.42,0.9))));
|
|
92
|
+
float mirror=clamp(fresnel*dir*dir*0.6,0.0,0.42);
|
|
93
|
+
if(mirror>1.0e-3) lens=mix(lens,studio(R),mirror);
|
|
94
|
+
float alpha=uOpacity*(1.0-smoothstep(0.0,1.0,d));
|
|
95
|
+
outColor=vec4(clamp(lens,0.0,1.0)*alpha,alpha);
|
|
96
|
+
}`;
|
|
97
|
+
const defaults={width:'100%',height:'100%',left:'0px',top:'0px',radius:'28px',refraction:1.24,dispersion:0.05,bezel:0.046,shine:0.44,opacity:1,edgeDark:0.42,ink:[0.08,0.1,0.16],light:[0.94,0.97,1],shellDay:[0.42,0.52,0.62],shellNight:[0.08,0.1,0.16],hiNight:[0.42,0.5,0.62],loNight:[0.06,0.08,0.13],night:0,dpr:2,continuous:true};
|
|
98
|
+
function createGlass(container, options) {
|
|
99
|
+
if(!container || !container.ownerDocument) throw new TypeError('createGlass(container): a DOM element is required');
|
|
100
|
+
const opts={...defaults,...(options||{})};
|
|
101
|
+
if(!opts.source) throw new TypeError('createGlass requires a renderable source (canvas, image, video, bitmap, or callback).');
|
|
102
|
+
const doc=container.ownerDocument;
|
|
103
|
+
const canvas=doc.createElement('canvas');
|
|
104
|
+
canvas.setAttribute('aria-hidden','true');
|
|
105
|
+
canvas.className='pixel-glass-engine';
|
|
106
|
+
canvas.style.cssText='position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:1;display:block;max-width:none;max-height:none;';
|
|
107
|
+
const computed=doc.defaultView.getComputedStyle(container);
|
|
108
|
+
const previousPosition=container.style.position;
|
|
109
|
+
if(computed.position==='static') container.style.position='relative';
|
|
110
|
+
container.appendChild(canvas);
|
|
111
|
+
const gl=canvas.getContext('webgl2',{alpha:true,premultipliedAlpha:true,antialias:false,depth:false,stencil:false,powerPreference:'high-performance'});
|
|
112
|
+
if(!gl){canvas.remove();if(previousPosition!==container.style.position)container.style.position=previousPosition;throw new Error('PixelGlassEngine requires WebGL2.');}
|
|
113
|
+
const compile=(type,src)=>{const s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)){const msg=gl.getShaderInfoLog(s);gl.deleteShader(s);throw new Error(msg||'Shader compilation failed');}return s;};
|
|
114
|
+
let program,vao,buffer,texture,raf=0,destroyed=false,source=opts.source||null,resizeObserver=null,dim={w:1,h:1};
|
|
115
|
+
try{
|
|
116
|
+
program=gl.createProgram();const vs=compile(gl.VERTEX_SHADER,VERT),fs=compile(gl.FRAGMENT_SHADER,FRAG);
|
|
117
|
+
gl.attachShader(program,vs);gl.attachShader(program,fs);gl.bindAttribLocation(program,0,'aPos');gl.linkProgram(program);gl.deleteShader(vs);gl.deleteShader(fs);
|
|
118
|
+
if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw new Error(gl.getProgramInfoLog(program)||'Shader link failed');
|
|
119
|
+
gl.useProgram(program);vao=gl.createVertexArray();gl.bindVertexArray(vao);buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),gl.STATIC_DRAW);gl.enableVertexAttribArray(0);gl.vertexAttribPointer(0,2,gl.FLOAT,false,0,0);
|
|
120
|
+
texture=gl.createTexture();gl.activeTexture(gl.TEXTURE0);gl.bindTexture(gl.TEXTURE_2D,texture);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);gl.uniform1i(gl.getUniformLocation(program,'uSource'),0);
|
|
121
|
+
}catch(error){canvas.remove();throw error;}
|
|
122
|
+
const U={};for(const n of ['uResolution','uCenter','uHalf','uSourceSize','uRadius','uRefraction','uDispersion','uBezel','uShine','uOpacity','uEdgeDark','uInk','uLight','uShellDay','uShellNight','uHiNight','uLoNight','uNight'])U[n]=gl.getUniformLocation(program,n);
|
|
123
|
+
function resolveSource(){if(typeof source==='function')return source();return source;}
|
|
124
|
+
function dimensions(el){return {w:el?.videoWidth||el?.naturalWidth||el?.width||0,h:el?.videoHeight||el?.naturalHeight||el?.height||0};}
|
|
125
|
+
function draw(){
|
|
126
|
+
raf=0;if(destroyed)return;
|
|
127
|
+
const rect=container.getBoundingClientRect(),dpr=Math.min(Number(opts.dpr)||2,doc.defaultView.devicePixelRatio||1,2);
|
|
128
|
+
const W=Math.max(2,Math.round(rect.width*dpr)),H=Math.max(2,Math.round(rect.height*dpr));
|
|
129
|
+
if(!W||!H)return;
|
|
130
|
+
if(canvas.width!==W||canvas.height!==H){canvas.width=W;canvas.height=H;gl.viewport(0,0,W,H);}
|
|
131
|
+
const el=resolveSource();
|
|
132
|
+
if(!el){if(opts.background){gl.bindTexture(gl.TEXTURE_2D,texture);gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,1,1,0,gl.RGBA,gl.UNSIGNED_BYTE,new Uint8Array([245,236,217,255]));}else{if(opts.continuous)raf=doc.defaultView.requestAnimationFrame(draw);return;}}
|
|
133
|
+
else{
|
|
134
|
+
dim=dimensions(el);if(!dim.w||!dim.h){if(opts.continuous)raf=doc.defaultView.requestAnimationFrame(draw);return;}
|
|
135
|
+
gl.activeTexture(gl.TEXTURE0);gl.bindTexture(gl.TEXTURE_2D,texture);gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1);gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,el);
|
|
136
|
+
}
|
|
137
|
+
gl.clearColor(0,0,0,0);gl.clear(gl.COLOR_BUFFER_BIT);gl.useProgram(program);gl.bindVertexArray(vao);
|
|
138
|
+
const width=opts.width,height=opts.height,left=opts.left,top=opts.top;
|
|
139
|
+
const cssPx=(v,total)=>{if(typeof v==='number')return v;const value=String(v??'').trim();if(value.endsWith('%'))return (parseFloat(value)||0)*total/100;return parseFloat(value)||total;};
|
|
140
|
+
const w=cssPx(width,rect.width),h=cssPx(height,rect.height),x=cssPx(left,rect.width),y=cssPx(top,rect.height);
|
|
141
|
+
const cx=(x+w/2)*dpr,cy=H-(y+h/2)*dpr,halfX=w*dpr/2,halfY=h*dpr/2;
|
|
142
|
+
const radius=Math.min(cssPx(opts.radius,Math.min(w,h)*0.28)*dpr,halfX,halfY);
|
|
143
|
+
const bezel=Math.max(0.1,Math.min(0.3,Number(opts.bezel)||0.046))*Math.min(halfX,halfY);
|
|
144
|
+
gl.uniform2f(U.uResolution,W,H);gl.uniform2f(U.uCenter,cx,cy);gl.uniform2f(U.uHalf,halfX,halfY);gl.uniform2f(U.uSourceSize,dim?.w||W,dim?.h||H);
|
|
145
|
+
gl.uniform1f(U.uRadius,radius);gl.uniform1f(U.uRefraction,Math.max(1.001,Number(opts.refraction)||1.24));gl.uniform1f(U.uDispersion,Math.max(0,Math.min(.25,Number(opts.dispersion)||0)));gl.uniform1f(U.uBezel,bezel);gl.uniform1f(U.uShine,Math.max(0,Number(opts.shine)||0));gl.uniform1f(U.uOpacity,Math.max(0,Math.min(1,Number(opts.opacity)??1)));gl.uniform1f(U.uEdgeDark,Math.max(0,Math.min(1,Number(opts.edgeDark)??.42)));gl.uniform3fv(U.uInk,new Float32Array(opts.ink||defaults.ink));gl.uniform3fv(U.uLight,new Float32Array(opts.light||defaults.light));gl.uniform3fv(U.uShellDay,new Float32Array(opts.shellDay||defaults.shellDay));gl.uniform3fv(U.uShellNight,new Float32Array(opts.shellNight||defaults.shellNight));gl.uniform3fv(U.uHiNight,new Float32Array(opts.hiNight||defaults.hiNight));gl.uniform3fv(U.uLoNight,new Float32Array(opts.loNight||defaults.loNight));gl.uniform1f(U.uNight,Math.max(0,Math.min(1,Number(opts.night)||0)));
|
|
146
|
+
gl.drawArrays(gl.TRIANGLES,0,3);
|
|
147
|
+
if(opts.continuous)raf=doc.defaultView.requestAnimationFrame(draw);
|
|
148
|
+
}
|
|
149
|
+
function schedule(){if(!raf&&!destroyed)raf=doc.defaultView.requestAnimationFrame(draw);}
|
|
150
|
+
resizeObserver=typeof ResizeObserver!=='undefined'?new ResizeObserver(schedule):null;resizeObserver?.observe(container);
|
|
151
|
+
if(source&&typeof source==='object'&&'addEventListener'in source){source.addEventListener('loadeddata',schedule);source.addEventListener('resize',schedule);}
|
|
152
|
+
schedule();
|
|
153
|
+
return {
|
|
154
|
+
canvas,
|
|
155
|
+
setSource(next){source=next;schedule();return this;},
|
|
156
|
+
setOptions(next){Object.assign(opts,next||{});schedule();return this;},
|
|
157
|
+
render(){if(raf)doc.defaultView.cancelAnimationFrame(raf);raf=0;draw();return this;},
|
|
158
|
+
destroy(){if(destroyed)return;destroyed=true;if(raf)doc.defaultView.cancelAnimationFrame(raf);resizeObserver?.disconnect();if(source&&typeof source==='object'&&'removeEventListener'in source){source.removeEventListener('loadeddata',schedule);source.removeEventListener('resize',schedule);}gl.deleteTexture(texture);gl.deleteBuffer(buffer);gl.deleteVertexArray(vao);gl.deleteProgram(program);canvas.remove();if(previousPosition!==container.style.position&&container.style.position==='relative')container.style.position=previousPosition;}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {createGlass,version:'3.1.1'};
|
|
162
|
+
});
|
package/glass-engine.mjs
ADDED
package/index.cjs
CHANGED