pixel-daylight-switch-gsvegetable 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +232 -0
- package/README.md +137 -648
- package/README.zh-CN.md +232 -0
- package/app.js +69 -8
- package/package.json +3 -3
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Pixel Daylight Switch
|
|
2
|
+
|
|
3
|
+
[](https://github.com/GSvegetable/pixel-daylight-switch/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/pixel-daylight-switch-gsvegetable)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**语言切换:** [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md)
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
*一个装在折射玻璃开关里的微缩像素世界。*
|
|
12
|
+
|
|
13
|
+
Pixel Daylight Switch 是一个可拖动的昼夜切换 UI 组件。拖动玻璃开关,可以让内部的像素世界从白天经过黄昏切换到夜晚。项目使用原生 JavaScript、WebGL2 和 Canvas 2D 回退渲染,不依赖第三方运行时库。
|
|
14
|
+
|
|
15
|
+
**[直接体验在线演示](https://gsvegetable.github.io/pixel-daylight-switch/)** · **[打开参数实验室](https://gsvegetable.github.io/pixel-daylight-switch/playground.html)** · **[npm 安装](https://www.npmjs.com/package/pixel-daylight-switch-gsvegetable)**
|
|
16
|
+
|
|
17
|
+
## 快速入口
|
|
18
|
+
|
|
19
|
+
| 你想做什么 | 直接打开 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 体验昼夜开关 | [在线演示](https://gsvegetable.github.io/pixel-daylight-switch/) |
|
|
22
|
+
| 调整场景与视觉参数 | [交互式参数实验室](https://gsvegetable.github.io/pixel-daylight-switch/playground.html) |
|
|
23
|
+
| 查看组件预览 | [Preview 页面](https://gsvegetable.github.io/pixel-daylight-switch/preview.html) |
|
|
24
|
+
| 用一个脚本嵌入网站 | [嵌入加载器源码](embed.js) |
|
|
25
|
+
| 在 JavaScript 项目中使用 | [npm 包](https://www.npmjs.com/package/pixel-daylight-switch-gsvegetable) |
|
|
26
|
+
| 阅读核心实现 | [运行时 app.js](app.js) · [样式 style.css](style.css) |
|
|
27
|
+
| 获取移动端构建 | [构建说明与限制](builds/README.md) · [GitHub Actions 构建任务](https://github.com/GSvegetable/pixel-daylight-switch/actions/workflows/mobile-build.yml) |
|
|
28
|
+
| 查看自动化测试 | [CI 工作流](https://github.com/GSvegetable/pixel-daylight-switch/actions/workflows/ci.yml) |
|
|
29
|
+
| 查看版本变化 | [更新日志](CHANGELOG.md) |
|
|
30
|
+
| 反馈问题 | [GitHub Issues](https://github.com/GSvegetable/pixel-daylight-switch/issues) |
|
|
31
|
+
|
|
32
|
+
## 功能特点
|
|
33
|
+
|
|
34
|
+
- 五种场景:乡村、日本街道、现代公寓、海边灯塔和山间木屋。
|
|
35
|
+
- WebGL2 折射玻璃效果;WebGL2 不可用或初始化失败时回退到 Canvas 2D。
|
|
36
|
+
- 支持连续拖动、点击/触控切换和键盘操作。
|
|
37
|
+
- 可配置运动速度、惯性、回弹、折射、玻璃细节、世界速度、天气和场景。
|
|
38
|
+
- 同一页面支持多个独立实例。
|
|
39
|
+
- 提供 switch 语义、状态播报和 `prefers-reduced-motion` 支持。
|
|
40
|
+
- 提供 `mount()`、`unmount()` 生命周期方法,适合动态页面。
|
|
41
|
+
- 页面隐藏或组件离开视口时暂停绘制。
|
|
42
|
+
|
|
43
|
+
## 快速开始:浏览器直接使用,无需构建
|
|
44
|
+
|
|
45
|
+
在页面中放置挂载节点,并引入运行脚本:
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<div data-ld-part="toggle-daylight" style="width: min(100%, 800px);">
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<script defer src="https://gsvegetable.github.io/pixel-daylight-switch/app.js"></script>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
浏览器运行时会自动查找挂载节点并生成组件界面。请为挂载元素设置可用宽度。
|
|
55
|
+
|
|
56
|
+
### 使用单脚本嵌入
|
|
57
|
+
|
|
58
|
+
如果希望脚本自动插入组件,可在希望显示组件的位置添加:
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<script src="https://gsvegetable.github.io/pixel-daylight-switch/embed.js"></script>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
加载器会自动插入容器、样式和运行脚本。目前它创建一个默认实例,并使用固定的最大展示尺寸;如果需要精确控制布局或配置属性,请使用上面的挂载节点方式。
|
|
65
|
+
|
|
66
|
+
## 通过 npm 安装
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install pixel-daylight-switch-gsvegetable
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**ES Modules**
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import PixelDaylightSwitch from 'pixel-daylight-switch-gsvegetable';
|
|
76
|
+
|
|
77
|
+
const root = document.querySelector('[data-ld-part="toggle-daylight"]');
|
|
78
|
+
PixelDaylightSwitch.mount(root);
|
|
79
|
+
|
|
80
|
+
// 从 DOM 移除 root 之前调用:
|
|
81
|
+
PixelDaylightSwitch.unmount(root);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**CommonJS**
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const PixelDaylightSwitch = require('pixel-daylight-switch-gsvegetable');
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
npm 包提供 ESM 和 CommonJS 入口,可在 SSR 环境中安全导入;只有在浏览器中且目标元素已经存在时,才能调用 `mount()`。npm 发布文件清单以 [package.json](package.json) 为准。
|
|
91
|
+
|
|
92
|
+
## 配置参数
|
|
93
|
+
|
|
94
|
+
所有参数均为可选项,写在挂载元素上。省略参数或参数值无效时使用默认值。
|
|
95
|
+
|
|
96
|
+
| HTML 属性 | 默认值 | 说明 |
|
|
97
|
+
| --- | ---: | --- |
|
|
98
|
+
| `data-speed` | `9.5` | 开关运动的基础响应速度,必须大于零。 |
|
|
99
|
+
| `data-refraction` | `1.24` | 玻璃镜片折射强度,必须大于零。 |
|
|
100
|
+
| `data-bezel` | `0.046` | 玻璃边缘的相对厚度,必须大于零。 |
|
|
101
|
+
| `data-start-night` | `false` | 精确设置为字符串 `"true"` 时,从夜晚状态开始。 |
|
|
102
|
+
| `data-friction` | `0.6` | 松手后的惯性阻尼,范围 `0.1–0.9`。 |
|
|
103
|
+
| `data-bounce` | `3.2` | 边缘回弹力度,范围 `0–12`。 |
|
|
104
|
+
| `data-dispersion` | `0.05` | 玻璃边缘 RGB 色散强度,范围 `0–0.25`。 |
|
|
105
|
+
| `data-shine` | `0.44` | 白天玻璃高光强度,范围 `0–1.5`。 |
|
|
106
|
+
| `data-time-speed` | `1` | 像素世界时间倍率,范围 `0–8`;设为 `0` 可冻结动态。 |
|
|
107
|
+
| `data-weather` | `default` | 支持 `default` 或 `snow`;未知值回退为 `default`。 |
|
|
108
|
+
| `data-scene` | `countryside` | 支持 `countryside`、`japanese-street`、`apartment`、`seaside`、`mountain-cabin`。 |
|
|
109
|
+
|
|
110
|
+
示例:
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<div
|
|
114
|
+
data-ld-part="toggle-daylight"
|
|
115
|
+
data-start-night="true"
|
|
116
|
+
data-scene="seaside"
|
|
117
|
+
data-weather="snow"
|
|
118
|
+
data-time-speed="1.4"
|
|
119
|
+
data-refraction="1.3">
|
|
120
|
+
</div>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 事件与状态
|
|
124
|
+
|
|
125
|
+
组件根元素通过 CSS 自定义属性 `--night` 暴露过渡进度:`0` 表示白天,`1` 表示夜晚,中间值表示过渡中。
|
|
126
|
+
|
|
127
|
+
当已确认的昼夜状态发生变化时,根元素会派发可冒泡、可跨越 Shadow DOM 边界的 `daylight:change` 事件,`event.detail.night` 为布尔值。该事件不会在每一帧动画中触发。
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
const root = document.querySelector('[data-ld-part="toggle-daylight"]');
|
|
131
|
+
|
|
132
|
+
root.addEventListener('daylight:change', (event) => {
|
|
133
|
+
console.log(event.detail.night ? '夜晚' : '白天');
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 渲染、无障碍与性能
|
|
138
|
+
|
|
139
|
+
- 使用 WebGL2 渲染玻璃折射;不可用或初始化失败时回退到 Canvas 2D。
|
|
140
|
+
- 根据容器尺寸调整渲染区域。
|
|
141
|
+
- 页面隐藏或组件离开视口时暂停绘制。
|
|
142
|
+
- 提供键盘操作、switch 语义和实时状态播报。
|
|
143
|
+
- 尊重系统的 `prefers-reduced-motion` 设置。
|
|
144
|
+
|
|
145
|
+
实际性能取决于浏览器、图形硬件、设备像素比以及同时显示的实例数量。
|
|
146
|
+
|
|
147
|
+
## 开发与测试
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
git clone https://github.com/GSvegetable/pixel-daylight-switch.git
|
|
151
|
+
cd pixel-daylight-switch
|
|
152
|
+
npm test
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
运行 Chromium 浏览器回归测试:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm install --no-save @playwright/test@1.56.1
|
|
159
|
+
npx playwright install chromium
|
|
160
|
+
npm run test:browser
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
GitHub Actions 会在 Node.js 18、20、22 环境中执行包与 SSR 检查,并运行 Chromium 交互和移动端回归测试。最新状态见 [CI 工作流](https://github.com/GSvegetable/pixel-daylight-switch/actions/workflows/ci.yml)。
|
|
164
|
+
|
|
165
|
+
## Android 与 iOS
|
|
166
|
+
|
|
167
|
+
Capacitor 移动端现已提供完整的场景控制界面:可在乡间小屋、日本街道、现代公寓、海边灯塔、山间木屋五个世界之间切换,并可单独开启飘雪效果。切换时复用同一套渲染器,不改动玻璃 Shader 和昼夜拖动交互。界面会根据设备语言显示简体中文、日文或英文。布局同时适配竖屏和横屏:竖屏采用纵向浏览;较宽的横屏将开关放在左侧、场景与天气控制放在右侧;窄屏横向手机则采用更紧凑的单列布局。GitHub Actions 会生成 Android Debug APK 和未签名的 iOS 模拟器应用压缩包。它们属于开发测试产物,不是可直接上架的商店版本:Android APK 不是 Play Store 发布包,iOS 模拟器版本也不能安装到实体 iPhone/iPad。请查看[移动端构建说明](builds/README.md)和[移动端工作流](https://github.com/GSvegetable/pixel-daylight-switch/actions/workflows/mobile-build.yml)。
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
### 快速嵌入到网页
|
|
172
|
+
|
|
173
|
+
如果只需要在网页中放入一个昼夜开关,无需安装依赖,直接粘贴下面这一行即可。它会自动加载样式和组件:
|
|
174
|
+
|
|
175
|
+
```html
|
|
176
|
+
<script src="https://gsvegetable.github.io/pixel-daylight-switch/embed.js"></script>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
这是一键式基础嵌入,当前展示单个默认场景开关;如果需要自定义场景、天气或多个实例,请使用上面的 npm API(`data-scene`、`data-weather` 等参数)。
|
|
180
|
+
|
|
181
|
+
### 集成到自己的 Android / iOS Capacitor 工程
|
|
182
|
+
|
|
183
|
+
项目目前提供的是 **Capacitor 应用模板与构建脚本**,不是可直接 `npm install` 的原生 UI 插件。若你的工程已经使用 Capacitor 8,可将本仓库的 `mobile/`、`app.js`、`style.css` 和 `scripts/prepare-mobile.cjs` 集成到工程,再执行:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npm install
|
|
187
|
+
npm run mobile:prepare
|
|
188
|
+
npm run mobile:android:init # 首次创建 Android 工程
|
|
189
|
+
npm run mobile:android # 后续同步更新
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
iOS 首次创建使用 `npm run mobile:ios:init`,后续使用 `npm run mobile:ios`;需要 macOS 与 Xcode。打开原生工程分别执行 `npm run mobile:open:android` 或 `npm run mobile:open:ios`。详细脚本以 [package.json](package.json) 为准。
|
|
193
|
+
|
|
194
|
+
## 项目 / 目录结构
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
pixel-daylight-switch/
|
|
198
|
+
├── .github/workflows/ # CI、原生移动端构建、npm 发布
|
|
199
|
+
├── assets/
|
|
200
|
+
│ └── scene-gallery.png # 五种场景总览图
|
|
201
|
+
├── builds/
|
|
202
|
+
│ └── README.md # 移动端产物说明;二进制由工作流生成
|
|
203
|
+
├── mobile/
|
|
204
|
+
│ ├── index.html # Capacitor 应用入口
|
|
205
|
+
│ └── mobile.css # 移动端专用布局
|
|
206
|
+
├── scripts/
|
|
207
|
+
│ └── prepare-mobile.cjs # 为 Capacitor 准备共用 Web 运行时
|
|
208
|
+
├── tests/
|
|
209
|
+
│ ├── browser/ # Playwright 交互与移动端回归
|
|
210
|
+
│ ├── fixture.html
|
|
211
|
+
│ ├── mobile-smoke.cjs
|
|
212
|
+
│ ├── package-smoke.cjs
|
|
213
|
+
│ └── server.cjs
|
|
214
|
+
├── app.js # 组件运行时、场景、交互与渲染
|
|
215
|
+
├── style.css # 组件样式
|
|
216
|
+
├── embed.js # 单脚本网站嵌入加载器
|
|
217
|
+
├── index.html # 在线演示主页
|
|
218
|
+
├── preview.html # 组件预览
|
|
219
|
+
├── playground.html # 交互式参数实验室
|
|
220
|
+
├── index.mjs / index.cjs # ESM 与 CommonJS 包入口
|
|
221
|
+
├── package.json # npm 元数据与脚本
|
|
222
|
+
├── capacitor.config.json # Capacitor 配置
|
|
223
|
+
├── playwright.config.cjs # 浏览器测试配置
|
|
224
|
+
├── CHANGELOG.md
|
|
225
|
+
├── llms.txt
|
|
226
|
+
├── LICENSE
|
|
227
|
+
└── README*.md # 英文、简体中文、日文文档
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## 许可证
|
|
231
|
+
|
|
232
|
+
项目采用 [Unlicense](LICENSE)。你可以自由复制、修改、发布、分发,并用于个人或商业项目,无需署名。软件按现状提供,不附带任何担保。
|
package/app.js
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
(function () {
|
|
11
11
|
"use strict";
|
|
12
|
+
const reportDiagnostic = (title, detail) => {
|
|
13
|
+
try {
|
|
14
|
+
if (typeof window !== 'undefined' && window.__PDS_DIAG__ && typeof window.__PDS_DIAG__.show === 'function') {
|
|
15
|
+
window.__PDS_DIAG__.show(title, detail);
|
|
16
|
+
}
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
};
|
|
12
19
|
const CORE_W = 192;
|
|
13
20
|
const CORE_H = 112;
|
|
14
21
|
const HORIZON = 82;
|
|
@@ -1038,12 +1045,53 @@ function mount(root) {
|
|
|
1038
1045
|
const GRID_W = 72; let GRID_H = 44; let glowRaw = new Float32Array(1); let glowNearBuf = new Float32Array(1); let glowFarBuf = new Float32Array(1); let glowTemp = new Float32Array(1);
|
|
1039
1046
|
const boxBlur = (src, dst, w, h, r) => { const size = r * 2 + 1; for (let y = 0; y < h; y++) { const row = y * w * 4; for (let c = 0; c < 3; c++) { let sum = 0; for (let x = -r; x <= r; x++) sum += src[row + clamp(x, 0, w - 1) * 4 + c]; for (let x = 0; x < w; x++) { dst[row + x * 4 + c] = sum / size; sum += src[row + clamp(x + r + 1, 0, w - 1) * 4 + c] - src[row + clamp(x - r, 0, w - 1) * 4 + c]; } } } for (let x = 0; x < w; x++) { for (let c = 0; c < 3; c++) { let sum = 0; for (let y = -r; y <= r; y++) sum += dst[clamp(y, 0, h - 1) * w * 4 + x * 4 + c]; for (let y = 0; y < h; y++) { src[y * w * 4 + x * 4 + c] = sum / size; sum += dst[clamp(y + r + 1, 0, h - 1) * w * 4 + x * 4 + c] - dst[clamp(y - r, 0, h - 1) * w * 4 + x * 4 + c]; } } } };
|
|
1040
1047
|
let gl = null;
|
|
1041
|
-
|
|
1048
|
+
let webglContextError = '';
|
|
1049
|
+
try {
|
|
1050
|
+
gl = canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: true, antialias: false, depth: false, stencil: false, powerPreference: 'high-performance' });
|
|
1051
|
+
if (!gl) webglContextError = 'canvas.getContext("webgl2") returned null. WebGL2 may be unavailable, disabled, or blocked by the device WebView.';
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
webglContextError = error && (error.stack || error.message) || String(error);
|
|
1054
|
+
gl = null;
|
|
1055
|
+
}
|
|
1042
1056
|
let program = null; const loc = {}; let vao = null; let quad = null; let texWorld = null; let texNear = null;
|
|
1043
|
-
const compile = (type, source) => {
|
|
1057
|
+
const compile = (type, source) => {
|
|
1058
|
+
const label = type === gl.VERTEX_SHADER ? 'Vertex Shader' : 'Fragment Shader';
|
|
1059
|
+
const shader = gl.createShader(type);
|
|
1060
|
+
if (!shader) throw new Error(label + ': gl.createShader returned null');
|
|
1061
|
+
gl.shaderSource(shader, source);
|
|
1062
|
+
gl.compileShader(shader);
|
|
1063
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1064
|
+
const log = gl.getShaderInfoLog(shader) || 'No shader compiler log was provided.';
|
|
1065
|
+
gl.deleteShader(shader);
|
|
1066
|
+
throw new Error(label + ' compilation failed:\\n' + log);
|
|
1067
|
+
}
|
|
1068
|
+
return shader;
|
|
1069
|
+
};
|
|
1044
1070
|
const setupGl = () => {
|
|
1045
|
-
if (!gl)
|
|
1046
|
-
|
|
1071
|
+
if (!gl) {
|
|
1072
|
+
reportDiagnostic('WebGL2 Failed', webglContextError || 'WebGL2 context could not be created.');
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
program = gl.createProgram();
|
|
1077
|
+
if (!program) throw new Error('gl.createProgram returned null');
|
|
1078
|
+
const v = compile(gl.VERTEX_SHADER, VERT);
|
|
1079
|
+
const f = compile(gl.FRAGMENT_SHADER, FRAG);
|
|
1080
|
+
gl.attachShader(program, v);
|
|
1081
|
+
gl.attachShader(program, f);
|
|
1082
|
+
gl.bindAttribLocation(program, 0, 'aPos');
|
|
1083
|
+
gl.linkProgram(program);
|
|
1084
|
+
gl.deleteShader(v);
|
|
1085
|
+
gl.deleteShader(f);
|
|
1086
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1087
|
+
throw new Error('Shader program link failed:\\n' + (gl.getProgramInfoLog(program) || 'No program link log was provided.'));
|
|
1088
|
+
}
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
const detail = error && (error.stack || error.message) || String(error);
|
|
1091
|
+
console.warn('cannot create shader', error);
|
|
1092
|
+
reportDiagnostic('WebGL2 Failed', detail);
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1047
1095
|
gl.useProgram(program);
|
|
1048
1096
|
for (const name of ['uWorld', 'uGlowNear', 'uRes', 'uPill', 'uOrigin', 'uWorldSize', 'uGlowSize', 'uLens', 'uBezel', 'uK', 'uNight', 'uLift', 'uSeam', 'uCalm', 'uRefraction', 'uDispersion', 'uShine', 'uShellDay', 'uShellNight', 'uInk', 'uLight', 'uHiNight', 'uLoNight', 'uSkyDay', 'uHomeCol', 'uSkyCol', 'uHomePos', 'uSkyPos']) { loc[name] = gl.getUniformLocation(program, name); }
|
|
1049
1097
|
vao = gl.createVertexArray(); gl.bindVertexArray(vao); quad = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, quad); 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);
|
|
@@ -1052,7 +1100,17 @@ function mount(root) {
|
|
|
1052
1100
|
texWorld = makeTex(); texNear = makeTex(); gl.uniform1i(loc.uWorld, 0); gl.uniform1i(loc.uGlowNear, 1); gl.disable(gl.BLEND); return true;
|
|
1053
1101
|
};
|
|
1054
1102
|
let ready = setupGl(); let lost = !ready && !!gl && gl.isContextLost(); let ctx2d = null; let lowCanvas = null;
|
|
1055
|
-
const useCanvas2d = () => {
|
|
1103
|
+
const useCanvas2d = () => {
|
|
1104
|
+
const fresh = canvas.cloneNode(false);
|
|
1105
|
+
canvas.replaceWith(fresh);
|
|
1106
|
+
canvas = fresh;
|
|
1107
|
+
try { ctx2d = canvas.getContext('2d'); }
|
|
1108
|
+
catch (error) {
|
|
1109
|
+
reportDiagnostic('Canvas 2D Failed', error && (error.stack || error.message) || String(error));
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (!ctx2d) reportDiagnostic('Canvas 2D Failed', 'canvas.getContext("2d") returned null after WebGL2 initialization failed.');
|
|
1113
|
+
};
|
|
1056
1114
|
if (!ready && !lost) useCanvas2d();
|
|
1057
1115
|
const shape = { W: 1, H: 1, cssW: 1, cssH: 1, dpr: 1, pillW: 1, pillH: 1, bezel: 1, winW: 1, winH: 1, r0: 1, travel: 1, travelCss: 1, ox: 0, oy: 0 };
|
|
1058
1116
|
let renderScale = 1; let fullK = 1; let thumbSeam = 1;
|
|
@@ -1089,12 +1147,14 @@ function mount(root) {
|
|
|
1089
1147
|
const clockOf = () => { if (branch === 'am') return mix(36, amStart, x); if (x >= 1) return nightClock; return CLOCK_DAY + (CLOCK_NIGHT - CLOCK_DAY) * x; };
|
|
1090
1148
|
const beginTravel = () => { if (x <= 0.0005) branch = 'pm'; else if (x >= 0.9995) { branch = 'am'; amStart = nightClock; } };
|
|
1091
1149
|
const setStatus = (text) => { if (statusEl.textContent !== text) statusEl.textContent = text; };
|
|
1092
|
-
const
|
|
1150
|
+
const locale = (() => { const el = root.closest('[lang]') || root.ownerDocument.documentElement; const value = (el.getAttribute('lang') || 'en').toLowerCase(); return value.startsWith('ja') ? 'ja' : value.startsWith('zh') ? 'zh-CN' : 'en'; })();
|
|
1151
|
+
const phaseNames = locale === 'ja' ? ['夜','夜明け','朝','昼','夕方'] : locale === 'zh-CN' ? ['夜晚','黎明','早晨','白天','傍晚'] : ['Night','Dawn','Morning','Day','Evening'];
|
|
1152
|
+
const phaseName = (h) => h < 5 || h >= 20 ? phaseNames[0] : h < 7 ? phaseNames[1] : h < 10 ? phaseNames[2] : h < 16.5 ? phaseNames[3] : phaseNames[4];
|
|
1093
1153
|
const commit = (value) => {
|
|
1094
1154
|
const night = value >= 1; x = night ? 1 : 0; velocity = 0; moving = false;
|
|
1095
1155
|
if (night) { nightClock = branch === 'am' ? amStart : CLOCK_NIGHT; branch = 'pm'; } else { branch = 'pm'; }
|
|
1096
1156
|
if (checked !== night) { checked = night; button.setAttribute('aria-checked', String(night)); root.dispatchEvent(new CustomEvent('daylight:change', { bubbles: true, composed: true, detail: { night } })); }
|
|
1097
|
-
setStatus(night ? '夜' : '昼');
|
|
1157
|
+
setStatus(night ? (locale === 'ja' ? '夜' : locale === 'zh-CN' ? '夜晚' : 'Night') : (locale === 'ja' ? '昼' : locale === 'zh-CN' ? '白天' : 'Day'));
|
|
1098
1158
|
};
|
|
1099
1159
|
const drawGl = (night) => {
|
|
1100
1160
|
gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(program); gl.bindVertexArray(vao);
|
|
@@ -1236,7 +1296,7 @@ function ensureMarkup(root) {
|
|
|
1236
1296
|
if (!root.querySelector('.scene')) root.appendChild(make('canvas', 'scene', { 'aria-hidden': 'true' }));
|
|
1237
1297
|
if (!root.querySelector('.hit')) {
|
|
1238
1298
|
const lang = ((root.closest('[lang]') || doc.documentElement).getAttribute('lang') || '').toLowerCase();
|
|
1239
|
-
const label = root.getAttribute('data-label') || (lang.indexOf('zh') === 0 ? '昼夜开关' : 'Day/night switch');
|
|
1299
|
+
const label = root.getAttribute('data-label') || (lang.indexOf('ja') === 0 ? '昼夜スイッチ' : lang.indexOf('zh') === 0 ? '昼夜开关' : 'Day/night switch');
|
|
1240
1300
|
root.appendChild(make('button', 'hit', { type: 'button', role: 'switch', 'aria-checked': 'false', 'aria-label': label }));
|
|
1241
1301
|
}
|
|
1242
1302
|
if (!root.querySelector('[data-status]')) {
|
|
@@ -1256,6 +1316,7 @@ function mountOne(root) {
|
|
|
1256
1316
|
return destroy;
|
|
1257
1317
|
} catch (err) {
|
|
1258
1318
|
console.error('[pixel-daylight-switch]', err);
|
|
1319
|
+
reportDiagnostic('App Initialization Failed', err && (err.stack || err.message) || String(err));
|
|
1259
1320
|
return null;
|
|
1260
1321
|
}
|
|
1261
1322
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixel-daylight-switch-gsvegetable",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A draggable pixel-art day/night switch with a living world inside a refractive glass lens.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pixel-art",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"test": "node tests/package-smoke.cjs && node tests/mobile-smoke.cjs",
|
|
58
58
|
"test:browser": "playwright test",
|
|
59
59
|
"mobile:prepare": "node scripts/prepare-mobile.cjs",
|
|
60
|
-
"mobile:android": "npm run mobile:prepare && cap sync android",
|
|
60
|
+
"mobile:android": "npm run mobile:prepare && cap sync android && node scripts/set-android-version.cjs android/app/build.gradle",
|
|
61
61
|
"mobile:ios": "npm run mobile:prepare && cap sync ios",
|
|
62
|
-
"mobile:android:init": "npm run mobile:prepare && cap add android",
|
|
62
|
+
"mobile:android:init": "npm run mobile:prepare && cap add android && node scripts/set-android-version.cjs android/app/build.gradle",
|
|
63
63
|
"mobile:ios:init": "npm run mobile:prepare && cap add ios",
|
|
64
64
|
"mobile:open:android": "cap open android",
|
|
65
65
|
"mobile:open:ios": "cap open ios"
|