pixel-daylight-switch-gsvegetable 1.3.0 → 2.0.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.
Files changed (3) hide show
  1. package/README.md +484 -174
  2. package/app.js +594 -258
  3. package/package.json +21 -2
package/README.md CHANGED
@@ -1,66 +1,186 @@
1
1
  # Pixel Daylight Switch
2
2
 
3
- 一个可以拖动的像素昼夜开关组件。拖动玻璃滑块,场景会从白天流动到夜晚;像素小人生活在开关内部,随着时间变化活动。
3
+ <p align="center">
4
+ <strong>A tiny pixel world inside a draggable glass day/night switch.</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://gsvegetable.github.io/pixel-daylight-switch/">Live Demo</a> ·
9
+ <a href="https://gsvegetable.github.io/pixel-daylight-switch/playground.html">Interactive Playground</a> ·
10
+ <a href="https://github.com/GSvegetable/pixel-daylight-switch/releases">Releases</a> ·
11
+ <a href="https://www.npmjs.com/package/pixel-daylight-switch-gsvegetable">NPM</a>
12
+ </p>
4
13
 
5
14
  <p align="center">
6
- <a href="https://gsvegetable.github.io/pixel-daylight-switch/">在线演示</a> ·
7
- <a href="https://gsvegetable.github.io/pixel-daylight-switch/playground.html">参数实验室 Playground</a> ·
8
- <a href="https://github.com/GSvegetable/pixel-daylight-switch/releases">版本发布</a>
15
+ <img src="https://github.com/user-attachments/assets/f7f248a5-c39c-432c-92f6-73237abdfe6b" width="49%" alt="Pixel Daylight Switch in daytime" />
16
+ <img src="https://github.com/user-attachments/assets/b2e24e65-a378-4ac6-be7b-efd3070e4da8" width="49%" alt="Pixel Daylight Switch at night" />
9
17
  </p>
10
18
 
11
19
  <p align="center">
12
- <img src="https://github.com/user-attachments/assets/f7f248a5-c39c-432c-92f6-73237abdfe6b" width="49%" alt="白天模式" />
13
- <img src="https://github.com/user-attachments/assets/b2e24e65-a378-4ac6-be7b-efd3070e4da8" width="49%" alt="夜晚模式" />
20
+ <em>Drag the glass control. Watch a small world move from day to dusk to night.</em>
14
21
  </p>
15
22
 
16
- **中文文档在前,English documentation is below.** [跳转到 English](#english)
23
+ **中文文档在前。** [English documentation](#english)
24
+
25
+ ---
17
26
 
18
27
  ## 目录
19
28
 
29
+ - [项目简介](#项目简介)
20
30
  - [快速开始](#快速开始)
21
- - [参数实验室](#参数实验室-playground)
22
- - [HTML 参数配置](#html-参数配置)
23
- - [交互与页面主题](#交互与页面主题)
24
- - [自定义外观](#自定义外观)
25
- - [动态挂载](#动态挂载)
26
- - [兼容性与本地开发](#兼容性与本地开发)
27
- - [更新记录](#更新记录)
31
+ - [Android 与 iOS 应用](#android-与-ios-应用)
32
+ - [NPM 安装](#npm-安装)
33
+ - [框架集成](#框架集成)
34
+ - [参数实验室 Playground](#参数实验室-playground)
35
+ - [配置参数](#配置参数)
36
+ - [交互与事件](#交互与事件)
37
+ - [自定义主题](#自定义主题)
38
+ - [动态挂载与卸载](#动态挂载与卸载)
39
+ - [渲染与兼容性](#渲染与兼容性)
40
+ - [项目目录结构](#项目目录结构)
41
+ - [本地开发与测试](#本地开发与测试)
42
+ - [版本记录](#版本记录)
28
43
  - [许可证](#许可证)
29
44
 
45
+ ## 项目简介
46
+
47
+ **Pixel Daylight Switch** 是一个可拖动的昼夜切换组件。它把一个持续变化的像素世界放进玻璃开关中:拖动滑块,场景会经过白天、黄昏并进入夜晚;点击或使用键盘也可以切换昼夜。
48
+
49
+ ### 特性
50
+
51
+ - **像素世界**:场景中有持续活动的像素角色与环境细节。
52
+ - **玻璃质感**:使用 WebGL2 绘制折射玻璃效果。
53
+ - **五种场景**:乡村小屋、日本街道、现代公寓、海边灯塔、山间木屋。
54
+ - **昼夜过渡**:不是简单的二态换色,而是连续的昼夜变化。
55
+ - **可配置**:通过 HTML `data-*` 属性配置运动、玻璃、天气和场景。
56
+ - **多实例**:同一页面可以放置多个独立开关。
57
+ - **轻量接入**:原生脚本无需构建工具或运行时依赖。
58
+ - **无障碍支持**:提供 switch 语义、键盘操作,并尊重减少动态效果偏好。
59
+
60
+ ### 五种场景一览
61
+
62
+ <p align="center">
63
+ <img src="./assets/scene-gallery.png" alt="Pixel Daylight Switch — five pixel-art scenes: countryside, Japanese street, apartment, seaside, and mountain cabin" width="100%" />
64
+ </p>
65
+
30
66
  ## 快速开始
31
67
 
32
- 无需 npm、依赖安装或构建步骤。把下面两行放进 HTML:
68
+ 不使用 NPM,也不需要安装依赖。把容器和脚本放进 HTML:
33
69
 
34
70
  ```html
35
71
  <div data-ld-part="toggle-daylight"></div>
36
- <script src="https://gsvegetable.github.io/pixel-daylight-switch/app.js"></script>
72
+
73
+ <script
74
+ defer
75
+ src="https://gsvegetable.github.io/pixel-daylight-switch/app.js"
76
+ ></script>
37
77
  ```
38
78
 
39
- 脚本会自动创建组件内部的 Canvas、交互区域和辅助无障碍元素。组件宽度跟随父容器,高度保持固定比例。一个页面可以放置多个独立实例。
79
+ 脚本会自动为每个匹配的容器创建 Canvas、交互按钮和状态播报元素。容器宽度会自适应父级,组件保持约 **1.6439:1** 的宽高比。
80
+
81
+ 如果需要自行托管,可以下载仓库中的 `app.js`,将脚本地址替换为自己的静态资源地址。
82
+
83
+ ## Android 与 iOS 应用
84
+
85
+ 从 **v2.0.0** 开始,项目提供 Capacitor 原生应用构建路径。Web 版本继续独立运行;Android 与 iOS 使用同一份核心渲染器,不复制或重写场景、WebGL shader 和交互物理逻辑。
86
+
87
+ ### 技术结构
40
88
 
41
- 如果你需要离线部署,也可以直接下载仓库中的 `app.js` 并自行托管。
89
+ - **共享核心**:根目录的 `app.js` 与 `style.css` 是唯一渲染实现。
90
+ - **原生容器**:Capacitor 8 将同一套 Web 资源装入 Android WebView 和 iOS WKWebView。
91
+ - **移动外壳**:`mobile/` 只负责全屏布局、安全区域和移动端 viewport,不包含第二套场景引擎。
92
+ - **构建产物**:`mobile/www/` 由脚本生成并被 Git 忽略,避免提交重复的渲染资源。
42
93
 
43
- ### 使用 NPM
94
+ ### 环境要求
44
95
 
45
- 适合 Vite、Webpack、Rspack 等构建项目:
96
+ - Node.js 22+
97
+ - Android:Android Studio、Android SDK、JDK 21
98
+ - iOS:macOS、Xcode 26+;需要在 Mac 上构建和签名发布包
99
+
100
+ Capacitor 8 的平台最低版本为 Android 7.0(API 24)和 iOS 15。具体要求以 [Capacitor 环境文档](https://capacitorjs.com/docs/getting-started/environment-setup) 为准。
101
+
102
+ ### 初始化原生工程
103
+
104
+ 先安装依赖并生成 Web 资源:
105
+
106
+ ```bash
107
+ npm install
108
+ npm run mobile:prepare
109
+ ```
110
+
111
+ 首次创建 Android / iOS 工程(每个平台只需初始化一次):
112
+
113
+ ```bash
114
+ npm run mobile:android:init
115
+ npm run mobile:ios:init
116
+ ```
117
+
118
+ 同步后续 Web 资源变更:
119
+
120
+ ```bash
121
+ npm run mobile:android
122
+ npm run mobile:ios
123
+ ```
124
+
125
+ 在 Android Studio 或 Xcode 中打开工程:
126
+
127
+ ```bash
128
+ npm run mobile:open:android
129
+ npm run mobile:open:ios
130
+ ```
131
+
132
+ 原生工程目录 `android/` 与 `ios/` 是本地生成目录,不纳入 Git。这样可以避免提交大量 IDE 生成文件;CI 会从锁定的 Capacitor 主版本配置重新生成工程并验证构建。
133
+
134
+ ### 自动构建
135
+
136
+ `.github/workflows/mobile-build.yml` 提供 Android Debug APK 与 iOS Simulator 构建。可在 GitHub Actions 手动运行;推送 `v2.*` 标签也会触发构建。Android APK 会作为 Actions artifact 保存。iOS Simulator 构建用于验证编译,不等同于已签名、可提交 App Store 的 IPA。
137
+
138
+ **发布边界:** App Store / Google Play 上架仍需要开发者账号、应用图标与商店素材、签名证书及隐私合规信息。此仓库的 CI 不会伪造签名或自动代表开发者提交商店。
139
+
140
+ ## NPM 安装
46
141
 
47
142
  ```bash
48
143
  npm install pixel-daylight-switch-gsvegetable
49
144
  ```
50
145
 
51
- **原生 JavaScript / ESM**
146
+ ### 原生 JavaScript / ESM
52
147
 
53
148
  ```js
54
149
  import PixelDaylightSwitch from 'pixel-daylight-switch-gsvegetable';
55
150
 
56
- const root = document.querySelector('[data-ld-part="toggle-daylight"]');
57
- PixelDaylightSwitch.mount(root);
151
+ const root = document.querySelector(
152
+ '[data-ld-part="toggle-daylight"]'
153
+ );
154
+
155
+ // 页面加载后动态创建的元素需要手动挂载
156
+ const destroy = PixelDaylightSwitch.mount(root);
58
157
 
59
- // 页面卸载或组件移除前
158
+ // 不再使用时,在移除元素前卸载
60
159
  PixelDaylightSwitch.unmount(root);
61
160
  ```
62
161
 
63
- **React**
162
+ 也可以使用命名导入:
163
+
164
+ ```js
165
+ import { mount, unmount } from 'pixel-daylight-switch-gsvegetable';
166
+
167
+ mount(root);
168
+ unmount(root);
169
+ ```
170
+
171
+ ### CommonJS
172
+
173
+ ```js
174
+ const PixelDaylightSwitch = require('pixel-daylight-switch-gsvegetable');
175
+
176
+ PixelDaylightSwitch.mount(root);
177
+ ```
178
+
179
+ 包同时提供 ESM 和 CommonJS 入口。模块可以在 SSR 环境中导入;DOM 挂载只会在浏览器中执行。对于服务端渲染项目,请在客户端生命周期中调用 `mount()`。
180
+
181
+ ## 框架集成
182
+
183
+ ### React
64
184
 
65
185
  ```jsx
66
186
  import { useEffect, useRef } from 'react';
@@ -71,6 +191,8 @@ export function DaylightSwitch() {
71
191
 
72
192
  useEffect(() => {
73
193
  const root = rootRef.current;
194
+ if (!root) return;
195
+
74
196
  PixelDaylightSwitch.mount(root);
75
197
  return () => PixelDaylightSwitch.unmount(root);
76
198
  }, []);
@@ -79,7 +201,7 @@ export function DaylightSwitch() {
79
201
  }
80
202
  ```
81
203
 
82
- **Vue 3**
204
+ ### Vue 3
83
205
 
84
206
  ```vue
85
207
  <script setup>
@@ -87,8 +209,14 @@ import { ref, onMounted, onBeforeUnmount } from 'vue';
87
209
  import PixelDaylightSwitch from 'pixel-daylight-switch-gsvegetable';
88
210
 
89
211
  const root = ref(null);
90
- onMounted(() => PixelDaylightSwitch.mount(root.value));
91
- onBeforeUnmount(() => PixelDaylightSwitch.unmount(root.value));
212
+
213
+ onMounted(() => {
214
+ PixelDaylightSwitch.mount(root.value);
215
+ });
216
+
217
+ onBeforeUnmount(() => {
218
+ PixelDaylightSwitch.unmount(root.value);
219
+ });
92
220
  </script>
93
221
 
94
222
  <template>
@@ -96,45 +224,75 @@ onBeforeUnmount(() => PixelDaylightSwitch.unmount(root.value));
96
224
  </template>
97
225
  ```
98
226
 
99
- NPM 入口同时提供 CommonJS 与 ESM。导入在服务端渲染环境中不会访问 DOM;实际挂载应在浏览器端执行。
227
+ ### Svelte
228
+
229
+ ```svelte
230
+ <script>
231
+ import { onMount } from 'svelte';
232
+ import PixelDaylightSwitch from 'pixel-daylight-switch-gsvegetable';
233
+
234
+ let root;
235
+
236
+ onMount(() => {
237
+ PixelDaylightSwitch.mount(root);
238
+ return () => PixelDaylightSwitch.unmount(root);
239
+ });
240
+ </script>
241
+
242
+ <div bind:this={root} data-ld-part="toggle-daylight"></div>
243
+ ```
244
+
245
+ > **生命周期原则:** 对于框架或路由动态创建的容器,在元素进入 DOM 后调用 `mount(element)`;在元素被移除前调用 `unmount(element)`。不要在同一个容器上重复创建自己的实例。
100
246
 
101
247
  ## 参数实验室 Playground
102
248
 
103
- **不想手动试参数?打开 [Pixel Daylight Playground](https://gsvegetable.github.io/pixel-daylight-switch/playground.html)。**
249
+ 打开 [Pixel Daylight Playground](https://gsvegetable.github.io/pixel-daylight-switch/playground.html),可以在浏览器中试验组件配置。
104
250
 
105
- - 左侧实时预览组件,直接拖动开关测试手感。
106
- - 右侧按「运动反馈、玻璃光学、世界节奏、场景环境」四组调整参数。
107
- - 使用场景预设快速切换乡村小屋、日本街道、现代公寓、海边灯塔和山间木屋。
108
- - 调整参数时只更新数值;点击「应用参数」后才刷新独立预览,避免拖动滑块时反复重建 WebGL。
109
- - 预览运行在独立页面中,与控制面板隔离,减少渲染状态互相干扰。
251
+ - 拖动预览区中的开关,直接体验交互。
252
+ - 按运动反馈、玻璃光学、世界节奏和场景环境调整参数。
253
+ - 在五种像素场景之间切换。
254
+ - 选择天气、初始昼夜状态与世界时间倍率。
255
+ - 先调整参数,再点击「应用参数」更新预览。
256
+ - 复制生成的 HTML 配置,放入自己的项目。
110
257
 
111
- Playground 是独立页面,不会改变组件本身的零依赖接入方式。
258
+ Playground 是独立页面。它用于探索配置,不是组件运行时的依赖。
112
259
 
113
- ## HTML 参数配置
260
+ ## 配置参数
114
261
 
115
- 所有参数都写在组件容器的 `data-*` 属性上。每个实例可以使用不同配置。参数可省略;缺失、空值或无效数值会回退到默认值。
262
+ 所有配置都写在组件根元素的 HTML `data-*` 属性上。每个实例可以使用不同参数;未填写的参数使用默认值。
116
263
 
117
- ### 全部参数
264
+ ### 完整参数表
118
265
 
119
- | HTML 属性 | 默认值 | 作用 |
266
+ | HTML 属性 | 默认值 | 说明 |
120
267
  | --- | ---: | --- |
121
- | `data-speed` | `9.5` | 开关运动的基础速度 |
122
- | `data-refraction` | `1.24` | 玻璃镜片的折射强度 |
123
- | `data-bezel` | `0.046` | 外框厚度系数,相对于开关高度 |
124
- | `data-start-night` | `false` | 设置为 `true` 时,初始化为夜晚状态 |
125
- | `data-friction` | `0.6` | 松手后的惯性阻尼,范围 `0.1–0.9`;越小越容易继续滑动 |
126
- | `data-bounce` | `3.2` | 滑块到达边缘时的回弹力度;越大弹性反馈越明显 |
127
- | `data-dispersion` | `0.05` | 玻璃边缘 RGB 色散强度,范围 `0–0.25`;设为 `0` 可关闭彩虹边 |
128
- | `data-shine` | `0.44` | 白天玻璃表面高光强度,范围 `0–1.5` |
129
- | `data-time-speed` | `1.0` | 像素世界时间倍率,范围 `0–8`;`0` 冻结世界动态,`2` 为两倍速度 |
130
- | `data-weather` | `default` | 天气预设:`default` 或 `snow`;雪天会出现飘落的像素雪花 |
131
- | `data-scene` | `countryside` | 世界场景:`countryside`、`japanese-street`、`apartment`、`seaside` 或 `mountain-cabin` |
268
+ | `data-speed` | `9.5` | 开关运动的基础响应速度,必须为正数。 |
269
+ | `data-refraction` | `1.24` | 玻璃镜片折射强度,必须为正数。 |
270
+ | `data-bezel` | `0.046` | 玻璃边缘厚度系数,必须为正数。 |
271
+ | `data-start-night` | `false` | 精确设置为 `true` 时,从夜晚状态开始。 |
272
+ | `data-friction` | `0.6` | 松手后的惯性阻尼,范围 `0.1–0.9`;值越低,滑动越持久。 |
273
+ | `data-bounce` | `3.2` | 边缘回弹力度,范围 `0–12`。 |
274
+ | `data-dispersion` | `0.05` | 玻璃边缘 RGB 色散强度,范围 `0–0.25`;`0` 关闭色散。 |
275
+ | `data-shine` | `0.44` | 白天玻璃表面高光强度,范围 `0–1.5`。 |
276
+ | `data-time-speed` | `1` | 像素世界时间倍率,范围 `0–8`;`0` 冻结世界动态,`2` 为两倍速度。 |
277
+ | `data-weather` | `default` | 天气预设:`default` 或 `snow`。 |
278
+ | `data-scene` | `countryside` | 场景:`countryside`、`japanese-street`、`apartment`、`seaside`、`mountain-cabin`。 |
279
+
280
+ ### 场景选项
281
+
282
+ | 值 | 场景 |
283
+ | --- | --- |
284
+ | `countryside` | 乡村小屋(默认) |
285
+ | `japanese-street` | 日本街道 |
286
+ | `apartment` | 现代公寓 |
287
+ | `seaside` | 海边灯塔 |
288
+ | `mountain-cabin` | 山间木屋 |
132
289
 
133
290
  ### 完整配置示例
134
291
 
135
292
  ```html
136
293
  <div
137
294
  data-ld-part="toggle-daylight"
295
+ data-label="昼夜环境切换"
138
296
  data-speed="12"
139
297
  data-refraction="1.5"
140
298
  data-bezel="0.05"
@@ -148,58 +306,39 @@ Playground 是独立页面,不会改变组件本身的零依赖接入方式。
148
306
  data-scene="mountain-cabin"
149
307
  ></div>
150
308
 
151
- <script src="https://gsvegetable.github.io/pixel-daylight-switch/app.js"></script>
309
+ <script
310
+ defer
311
+ src="https://gsvegetable.github.io/pixel-daylight-switch/app.js"
312
+ ></script>
152
313
  ```
153
314
 
154
- ### 参数分组与调校建议
155
-
156
- **运动反馈 / Motion**
157
-
158
- - `data-speed` 控制开关运动的基础速度。
159
- - `data-friction` 控制松手后的惯性衰减。较小的值更滑,较大的值更黏。
160
- - `data-bounce` 控制滑块触及边缘时的回弹力度。
161
-
162
- **玻璃光学 / Glass Optics**
163
-
164
- - `data-refraction` 控制镜片折射强度。
165
- - `data-bezel` 控制镜片外框厚度。
166
- - `data-dispersion` 控制边缘色散。数值越大,彩虹色边缘越明显。
167
- - `data-shine` 控制白天镜片高光。
315
+ ### 参数校验规则
168
316
 
169
- **世界节奏 / World Rhythm**
317
+ - 数值参数为空、不是有限数字或不符合正数要求时,会回退到默认值。
318
+ - `friction`、`bounce`、`dispersion`、`shine` 和 `time-speed` 会限制在表格所示范围内。
319
+ - 未知的 `weather` 值回退为 `default`。
320
+ - 未知的 `scene` 值回退为 `countryside`。
321
+ - `data-start-night` 只有值为字符串 `true` 时才启用。
170
322
 
171
- - `data-time-speed="0"` 可以冻结世界时间和动态。
172
- - `data-time-speed="2"` 会让世界模拟以两倍速度运行。
173
- - `data-weather="snow"` 开启飘雪;省略或填写其他值时使用默认天气。
174
- - `data-scene` 选择完整的像素环境:乡村、日本街道、公寓室内、海边或山间木屋。
175
- - `data-start-night="true"` 让组件从夜晚开始。
323
+ ## 交互与事件
176
324
 
177
- ### 默认值与兼容性
325
+ ### 用户交互
178
326
 
179
- 场景、天气和初始昼夜状态归入「场景环境」;它们不是连续数值,因此使用选择器与开关,而不是滑动条。
327
+ - **拖动**:连续控制昼夜进度。
328
+ - **点击**:在白天和夜晚之间切换。
329
+ - **键盘**:聚焦开关后可使用方向键操作。
330
+ - **无障碍**:交互按钮使用 `role="switch"` 与 `aria-checked`;状态变化通过 live region 播报。
331
+ - **减少动态效果**:尊重系统的 `prefers-reduced-motion` 设置。
180
332
 
181
- 如果只写:
333
+ ### 读取昼夜进度
182
334
 
183
- ```html
184
- <div data-ld-part="toggle-daylight"></div>
185
- ```
186
-
187
- 组件会使用默认参数。v1.2.0 的新增参数默认值与 v1.1.0 的原有参数保持一致,未配置新属性时不会主动改变原有视觉设定。数值参数会进行数字转换、有效性检查和范围限制;未知天气值回退到 `default`。
188
-
189
- ## 交互与页面主题
335
+ 组件会在根元素上更新 CSS 自定义属性 `--night`:
190
336
 
191
- - **拖动**:拖动滑块,让世界从白天经过黄昏进入夜晚。
192
- - **点击**:点击开关,在白天和夜晚之间切换。
193
- - **键盘**:聚焦开关后使用方向键调整。
194
- - **无障碍**:使用 switch 语义,并尊重系统的减少动态效果偏好。
337
+ - `0`:白天。
338
+ - `1`:夜晚。
339
+ - `0` 到 `1` 之间:昼夜过渡。
195
340
 
196
- 组件会把当前夜晚程度写入根元素的 CSS 自定义属性 `--night`:
197
-
198
- - `0` 表示白天;
199
- - `1` 表示夜晚;
200
- - 中间值表示昼夜过渡。
201
-
202
- 例如,让页面背景跟随开关变化:
341
+ 可以用它联动页面其他部分:
203
342
 
204
343
  ```css
205
344
  body {
@@ -211,17 +350,30 @@ body {
211
350
  }
212
351
  ```
213
352
 
214
- 如果只需要在昼夜状态变化时执行逻辑,可以监听事件:
353
+ ### 监听状态事件
354
+
355
+ 组件在昼夜状态发生变化时,会在组件根元素上派发可冒泡的 `daylight:change` 事件。事件的 `detail.night` 是布尔值。
215
356
 
216
357
  ```js
217
358
  document.addEventListener('daylight:change', (event) => {
218
- document.documentElement.classList.toggle('dark', event.detail.night);
359
+ document.documentElement.classList.toggle(
360
+ 'dark',
361
+ event.detail.night
362
+ );
219
363
  });
220
364
  ```
221
365
 
222
- ## 自定义外观
366
+ 如果页面有多个实例,建议在单个根元素上监听,以区分具体是哪一个开关触发了事件:
223
367
 
224
- 在组件容器上覆盖 CSS 自定义属性:
368
+ ```js
369
+ root.addEventListener('daylight:change', (event) => {
370
+ console.log('当前是否为夜晚:', event.detail.night);
371
+ });
372
+ ```
373
+
374
+ ## 自定义主题
375
+
376
+ 在组件根元素上覆盖以下 CSS 自定义属性:
225
377
 
226
378
  ```css
227
379
  [data-ld-part="toggle-daylight"] {
@@ -230,142 +382,261 @@ document.addEventListener('daylight:change', (event) => {
230
382
  --ld-fg: #1b2146;
231
383
  --ld-muted: #7f97b3;
232
384
  --ld-accent: #efa02c;
385
+ --ld-radius: 9999px;
386
+ --ld-font: system-ui, sans-serif;
233
387
  }
234
388
  ```
235
389
 
236
390
  | CSS 变量 | 默认值 | 用途 |
237
391
  | --- | --- | --- |
238
- | `--ld-bg` | `#f5ecd9` | 背景色 |
239
- | `--ld-surface` | `#8c9a62` | 表面与场景色 |
240
- | `--ld-fg` | `#1b2146` | 前景色 |
392
+ | `--ld-bg` | `#f5ecd9` | 背景调色输入 |
393
+ | `--ld-surface` | `#8c9a62` | 表面与场景调色输入 |
394
+ | `--ld-fg` | `#1b2146` | 前景颜色 |
241
395
  | `--ld-muted` | `#7f97b3` | 次要颜色 |
242
- | `--ld-accent` | `#efa02c` | 强调色 |
396
+ | `--ld-accent` | `#efa02c` | 强调色与焦点颜色 |
397
+ | `--ld-radius` | `9999px` | 交互区域圆角 |
398
+ | `--ld-font` | `system-ui, sans-serif` | 组件字体 |
399
+ | `--ld-space` | `8px` | 焦点轮廓偏移相关尺寸 |
400
+ | `--ld-shadow` | `0 8px 24px rgb(27 33 70 / .2)` | 主题预留变量 |
401
+ | `--ld-motion` | `200ms` | 焦点轮廓过渡时长 |
243
402
 
244
- ## 动态挂载
403
+ ## 动态挂载与卸载
245
404
 
246
- 如果组件容器是在页面加载后动态创建的,可以手动挂载:
405
+ 页面加载时,脚本会自动查找已有的 `[data-ld-part="toggle-daylight"]` 元素并挂载。对于之后动态创建的元素,调用公开 API:
247
406
 
248
407
  ```js
249
- const destroy = PixelDaylightSwitch.mount(element);
408
+ const root = document.createElement('div');
409
+ root.dataset.ldPart = 'toggle-daylight';
410
+ document.querySelector('#app').appendChild(root);
250
411
 
251
- // 不再需要时:
252
- PixelDaylightSwitch.unmount(element);
412
+ const destroy = PixelDaylightSwitch.mount(root);
413
+
414
+ // 需要移除时:
415
+ PixelDaylightSwitch.unmount(root);
416
+ root.remove();
253
417
  ```
254
418
 
255
- ## 兼容性与本地开发
419
+ `mount(element)` 对同一元素是幂等的:重复调用不会创建重复实例。成功挂载时会返回清理函数;如果挂载失败则返回 `null`。通常使用 `unmount(element)` 即可完成清理。
420
+
421
+ 卸载会停止绘制并释放组件持有的事件监听器和观察器。它不会移除根元素本身。
422
+
423
+ ## 渲染与兼容性
424
+
425
+ - **WebGL2**:用于绘制玻璃折射效果。
426
+ - **Canvas 2D**:WebGL2 不可用时作为渲染回退。
427
+ - **响应式布局**:组件保持固定宽高比,并随容器宽度缩放。
428
+ - **页面可见性**:页面隐藏或组件离开视口时,会暂停不必要的绘制。
429
+ - **减少动态效果**:支持系统减少动态效果偏好。
430
+ - **运行时依赖**:原生脚本不需要第三方运行时依赖。
431
+
432
+ 组件需要现代浏览器提供 Canvas 2D;WebGL2 用于增强玻璃渲染体验。
433
+
434
+ ## 项目目录结构
435
+
436
+ 仓库采用轻量的原生 JavaScript 结构,无需前端打包工具。核心组件、演示页面、NPM 入口和自动化测试分别独立维护。
437
+
438
+ ```text
439
+ pixel-daylight-switch/
440
+ ├── assets/
441
+ │ └── scene-gallery.png # 五种像素场景总览图
442
+ ├── mobile/
443
+ │ ├── index.html # Capacitor 原生应用入口
444
+ │ └── mobile.css # 移动端安全区域与全屏布局
445
+ ├── scripts/
446
+ │ └── prepare-mobile.cjs # 生成 Capacitor Web 资源
447
+ ├── .github/
448
+ │ └── workflows/
449
+ │ ├── ci.yml # Node.js 与 Chromium 持续集成
450
+ │ └── mobile-build.yml # Android / iOS 构建验证
451
+ │ └── npm-publish.yml # 通过 v* 标签触发 NPM 发布
452
+ ├── capacitor.config.json # Capacitor 原生应用配置
453
+ ├── tests/
454
+ │ ├── browser/
455
+ │ │ └── component.spec.cjs # Playwright 浏览器回归测试
456
+ │ ├── fixture.html # 浏览器测试页面
457
+ │ ├── package-smoke.cjs # 包入口与 SSR 冒烟测试
458
+ │ └── server.cjs # 本地测试静态服务器
459
+ ├── app.js # 核心组件:渲染、交互与生命周期
460
+ ├── embed.js # 独立嵌入入口
461
+ ├── index.cjs # CommonJS 包入口
462
+ ├── index.mjs # ESM 包入口
463
+ ├── index.html # 主演示页面
464
+ ├── preview.html # 预览页面
465
+ ├── playground.html # 交互式参数实验室
466
+ ├── style.css # 演示页面样式
467
+ ├── package.json # NPM 元数据与脚本
468
+ ├── playwright.config.cjs # Playwright 配置
469
+ ├── CHANGELOG.md # 版本变更记录
470
+ ├── LICENSE # Unlicense 许可证
471
+ ├── llms.txt # 面向 AI 工具的项目说明
472
+ └── README.md # 项目文档
473
+ ```
256
474
 
257
- - 优先使用 WebGL2 渲染玻璃效果;不可用时回退到 Canvas 2D。
258
- - 支持减少动态效果偏好。
259
- - 页面不可见或组件离开视口时会暂停不必要的绘制。
260
- - 不需要构建工具。
475
+ ### 关键目录说明
261
476
 
262
- 本地运行:
477
+ - **`app.js`**:组件主体,包含昼夜状态、Canvas/WebGL 渲染、输入交互和实例生命周期管理。
478
+ - **`playground.html`**:可视化调整参数、切换场景并生成 HTML 配置。
479
+ - **`embed.js`**:面向嵌入式使用场景的入口脚本。
480
+ - **`tests/`**:包加载、SSR、挂载卸载、多实例、指针/触控和移动端回归测试。
481
+ - **`.github/workflows/`**:自动执行持续集成,并在版本标签触发时执行发布流程。
482
+
483
+ ## 本地开发与测试
484
+
485
+ 克隆项目:
263
486
 
264
487
  ```bash
265
488
  git clone https://github.com/GSvegetable/pixel-daylight-switch.git
266
489
  cd pixel-daylight-switch
267
490
  ```
268
491
 
269
- 使用浏览器打开 `index.html` 即可。主要文件:
492
+ 直接用浏览器打开 `index.html` 即可查看基础演示。项目无需打包构建。
493
+
494
+ ### 自动化测试
495
+
496
+ 运行包入口、CommonJS / ESM 和 SSR 安全检查:
497
+
498
+ ```bash
499
+ npm test
500
+ ```
501
+
502
+ 浏览器回归测试使用 Playwright。安装测试运行器和 Chromium 后执行:
270
503
 
271
- - `app.js`:组件实现。
272
- - `index.html`:基础演示页面。
273
- - `playground.html`:参数实验室与场景切换。
504
+ ```bash
505
+ npm install --no-save @playwright/test@1.56.1
506
+ npx playwright install chromium
507
+ npm run test:browser
508
+ ```
274
509
 
275
- ## 更新记录
510
+ CI 会在 Node.js 18、20、22 下执行包与 SSR 检查,并在 Chromium 中运行交互、重复挂载/卸载、多实例、事件监听器清理和移动端回归测试。
511
+
512
+ ## 版本记录
513
+
514
+ ### v1.3.1
515
+
516
+ - 修复组件卸载时主动销毁 WebGL context,导致同一 canvas/root 后续重新挂载可能异常的问题。
517
+ - 保留组件资源、事件监听器与观察器的清理,同时允许 canvas 在卸载后重新挂载。
518
+ - 增加 CommonJS、ESM、SSR、Node.js 18/20/22 和 Chromium 浏览器自动化检查。
519
+ - 增加重复挂载、多实例、指针交互、触控、移动端布局与监听器清理测试。
520
+ - 将浏览器回归纳入 NPM 发布前置检查。
276
521
 
277
522
  ### v1.3.0
278
523
 
279
- - 新增四个独立像素环境:日本街道、现代公寓、海边灯塔和山间木屋。
280
- - Playground 新增真实场景选择,可与玻璃参数、昼夜和天气效果组合。
524
+ - 新增日本街道、现代公寓、海边灯塔和山间木屋四种像素环境。
525
+ - 保留乡村小屋作为默认场景。
526
+ - Playground 支持切换场景并组合调整天气、昼夜和玻璃参数。
281
527
 
282
528
  ### v1.2.0
283
529
 
284
- 本次版本包含两轮未发布的更新:
285
-
286
- - 新增物理参数:惯性阻尼与边缘回弹。
287
- - 新增液态玻璃参数:色散与表面高光。
288
- - 新增世界时间倍率和像素飘雪天气。
289
- - 新增独立 Playground,可实时调节参数、套用预设并复制 HTML。
290
- - 重组 README,将中文说明置于文档顶部,并提供中英文跳转。
530
+ - 新增惯性阻尼、边缘回弹、玻璃色散和表面高光参数。
531
+ - 新增世界时间倍率与飘雪天气。
532
+ - 新增独立 Playground,支持参数分组、场景预设和 HTML 代码复制。
291
533
 
292
534
  ### v1.1.0
293
535
 
294
- - 新增动画速度、镜片折射、外框厚度和初始夜晚状态的 HTML 参数配置。
536
+ - 新增运动速度、镜片折射、外框厚度和初始夜晚状态配置。
295
537
 
296
538
  ### v1.0.0
297
539
 
298
540
  - 首个公开版本。
299
541
 
542
+ 详细记录见 [CHANGELOG.md](./CHANGELOG.md)。
543
+
300
544
  ## 许可证
301
545
 
302
- [Unlicense](https://unlicense.org/) — 公有领域协议。允许自由使用,包括商业用途,无需署名。
546
+ 本项目使用 [Unlicense](https://unlicense.org/) 公有领域协议。允许个人和商业用途,无需署名。
303
547
 
304
548
  ---
305
549
 
550
+ <a id="english"></a>
551
+
306
552
  ## English
307
553
 
308
- A draggable day/night switch with a tiny pixel world living inside the glass. Move the switch to transition between daylight, dusk, and night.
554
+ **Pixel Daylight Switch** is a draggable day/night UI component with a tiny animated pixel world inside a refractive glass lens. Drag the control to move through daylight, dusk, and night.
555
+
556
+ ### Features
309
557
 
310
- **Quick links:** [Live demo](https://gsvegetable.github.io/pixel-daylight-switch/) · [Interactive Playground](https://gsvegetable.github.io/pixel-daylight-switch/playground.html) · [Releases](https://github.com/GSvegetable/pixel-daylight-switch/releases)
558
+ - An animated pixel environment inside a glass switch.
559
+ - WebGL2 refraction rendering with a Canvas 2D fallback.
560
+ - Five scenes: countryside, Japanese street, modern apartment, seaside lighthouse, and mountain cabin.
561
+ - Continuous day/night transitions, pointer dragging, click, and keyboard interaction.
562
+ - Per-instance HTML data attributes for motion, optics, weather, and scene settings.
563
+ - Multiple independent instances on the same page.
564
+ - No runtime dependencies for the standalone script.
565
+ - Accessible switch semantics and reduced-motion support.
311
566
 
312
567
  ### Quick start
313
568
 
314
- No npm, dependencies, or build step required:
569
+ No package manager or build step is required:
315
570
 
316
571
  ```html
317
572
  <div data-ld-part="toggle-daylight"></div>
318
- <script src="https://gsvegetable.github.io/pixel-daylight-switch/app.js"></script>
573
+
574
+ <script
575
+ defer
576
+ src="https://gsvegetable.github.io/pixel-daylight-switch/app.js"
577
+ ></script>
319
578
  ```
320
579
 
321
- The script creates the internal canvas, interaction target, and accessibility elements. The component is responsive and supports multiple independent instances on one page.
580
+ The script automatically mounts matching elements. It creates the internal canvas, interaction button, and live status region. The component scales with its container and keeps an aspect ratio of approximately **1.6439:1**.
322
581
 
323
- ### NPM installation
582
+ ### NPM
324
583
 
325
584
  ```bash
326
585
  npm install pixel-daylight-switch-gsvegetable
327
586
  ```
328
587
 
329
- ESM / native build tools:
588
+ ESM:
330
589
 
331
590
  ```js
332
591
  import PixelDaylightSwitch from 'pixel-daylight-switch-gsvegetable';
333
592
 
334
593
  const root = document.querySelector('[data-ld-part="toggle-daylight"]');
335
594
  PixelDaylightSwitch.mount(root);
336
- // Before removing the root:
595
+
596
+ // Before removing the element:
337
597
  PixelDaylightSwitch.unmount(root);
338
598
  ```
339
599
 
340
- React and Vue 3 integrations should call `mount(element)` after the element is created and `unmount(element)` before it is removed. The package exposes both ESM and CommonJS entry points and is safe to import during SSR; mount only in the browser.
600
+ CommonJS:
601
+
602
+ ```js
603
+ const PixelDaylightSwitch = require('pixel-daylight-switch-gsvegetable');
604
+ ```
605
+
606
+ The package provides ESM and CommonJS entry points and is safe to import in SSR environments. Call `mount()` only after the element exists in the browser.
341
607
 
342
- ### Interactive Playground
608
+ ### Framework integration
343
609
 
344
- Open the [Pixel Daylight Playground](https://gsvegetable.github.io/pixel-daylight-switch/playground.html) to tune all component settings visually, try presets, preview changes, and copy generated HTML.
610
+ For React, Vue 3, and Svelte, mount the component in the client-side lifecycle and call `unmount(element)` during cleanup. See the complete examples in the [Chinese documentation above](#框架集成).
345
611
 
346
- ### HTML configuration
612
+ ### Playground
347
613
 
348
- All settings are optional and configured per instance using `data-*` attributes.
614
+ Use the [Interactive Playground](https://gsvegetable.github.io/pixel-daylight-switch/playground.html) to adjust motion, glass optics, world speed, weather, and scenes. Apply settings to the preview and copy the generated HTML.
615
+
616
+ ### Configuration
617
+
618
+ All settings are optional and belong on the component root.
349
619
 
350
620
  | Attribute | Default | Description |
351
621
  | --- | ---: | --- |
352
- | `data-speed` | `9.5` | Base switch motion speed. |
353
- | `data-refraction` | `1.24` | Glass lens refraction strength. |
354
- | `data-bezel` | `0.046` | Bezel thickness relative to component height. |
355
- | `data-start-night` | `false` | Initialize in night mode when set to `true`. |
356
- | `data-friction` | `0.6` | Release inertia damping; lower values feel more slippery. |
357
- | `data-bounce` | `3.2` | Rebound impulse at the switch edges. |
358
- | `data-dispersion` | `0.05` | RGB chromatic dispersion at the glass edge; `0` disables it. |
359
- | `data-shine` | `0.44` | Daytime glass highlight intensity. |
360
- | `data-time-speed` | `1.0` | World simulation speed; `0` freezes world dynamics and `2` doubles the speed. |
361
- | `data-weather` | `default` | Weather preset: `default` or `snow`. |
362
- | `data-scene` | `countryside` | Pixel environment: `countryside`, `japanese-street`, `apartment`, `seaside`, or `mountain-cabin`. |
622
+ | `data-speed` | `9.5` | Base motion response speed; positive number. |
623
+ | `data-refraction` | `1.24` | Glass lens refraction strength; positive number. |
624
+ | `data-bezel` | `0.046` | Glass edge thickness ratio; positive number. |
625
+ | `data-start-night` | `false` | Start at night when exactly `true`. |
626
+ | `data-friction` | `0.6` | Release inertia damping, clamped to `0.1–0.9`. |
627
+ | `data-bounce` | `3.2` | Edge rebound, clamped to `0–12`. |
628
+ | `data-dispersion` | `0.05` | RGB edge dispersion, clamped to `0–0.25`. |
629
+ | `data-shine` | `0.44` | Daytime highlight intensity, clamped to `0–1.5`. |
630
+ | `data-time-speed` | `1` | World simulation speed, clamped to `0–8`; zero freezes world dynamics. |
631
+ | `data-weather` | `default` | `default` or `snow`. Unknown values use `default`. |
632
+ | `data-scene` | `countryside` | `countryside`, `japanese-street`, `apartment`, `seaside`, or `mountain-cabin`. |
363
633
 
364
634
  Example:
365
635
 
366
636
  ```html
367
637
  <div
368
638
  data-ld-part="toggle-daylight"
639
+ data-label="Day and night switch"
369
640
  data-speed="12"
370
641
  data-refraction="1.5"
371
642
  data-bezel="0.05"
@@ -380,54 +651,93 @@ Example:
380
651
  ></div>
381
652
  ```
382
653
 
383
- Missing or invalid numeric values fall back to defaults. Unknown weather values fall back to `default`; unknown scene values fall back to `countryside`. With no new v1.2.0 or v1.3.0 attributes, the component retains the intended default appearance.
654
+ Invalid numeric values fall back to defaults. Range-limited values are clamped. Unknown weather and scene values fall back to `default` and `countryside`, respectively.
384
655
 
385
- ### Page theme integration
656
+ ### Events and page theme
386
657
 
387
- The component exposes the current night level as the `--night` CSS custom property on the root element: `0` is day, `1` is night, and intermediate values represent the transition.
658
+ The component updates `--night` on its root element: `0` means day, `1` means night, and values in between represent the transition.
388
659
 
389
- ```css
390
- body {
391
- background: color-mix(in srgb, #f5ecd9, #1b2146 calc(var(--night, 0) * 100%));
392
- }
393
- ```
394
-
395
- Listen for state changes with:
660
+ It also dispatches a bubbling, composed `daylight:change` event from the component root when the day/night state changes. `event.detail.night` is a boolean:
396
661
 
397
662
  ```js
398
- document.addEventListener('daylight:change', (event) => {
663
+ root.addEventListener('daylight:change', (event) => {
399
664
  document.documentElement.classList.toggle('dark', event.detail.night);
400
665
  });
401
666
  ```
402
667
 
403
- ### Theme colors
668
+ ### Theme customization
669
+
670
+ Override these CSS variables on the component root:
671
+
672
+ ```css
673
+ [data-ld-part="toggle-daylight"] {
674
+ --ld-bg: #f5ecd9;
675
+ --ld-surface: #8c9a62;
676
+ --ld-fg: #1b2146;
677
+ --ld-muted: #7f97b3;
678
+ --ld-accent: #efa02c;
679
+ --ld-radius: 9999px;
680
+ --ld-font: system-ui, sans-serif;
681
+ }
682
+ ```
683
+
684
+ Supported variables: `--ld-bg`, `--ld-surface`, `--ld-fg`, `--ld-muted`, `--ld-accent`, `--ld-radius`, `--ld-font`, `--ld-space`, `--ld-shadow`, and `--ld-motion`.
404
685
 
405
- Override `--ld-bg`, `--ld-surface`, `--ld-fg`, `--ld-muted`, and `--ld-accent` on the component element to customize its palette.
686
+ ### Mounting and cleanup
406
687
 
407
- ### Dynamic mounting
688
+ Elements present when the script loads are mounted automatically. For elements created later:
408
689
 
409
690
  ```js
410
- PixelDaylightSwitch.mount(element);
691
+ const destroy = PixelDaylightSwitch.mount(element);
692
+
693
+ // Before removing the root:
411
694
  PixelDaylightSwitch.unmount(element);
695
+ element.remove();
412
696
  ```
413
697
 
414
- ### Browser support
698
+ Mounting the same element more than once is safe and does not create duplicate instances. A successful `mount()` returns a cleanup function; a failed mount returns `null`. `unmount()` stops rendering and removes the component's listeners and observers without removing the root element.
699
+
700
+ ### Rendering and browser behavior
415
701
 
416
- WebGL2 is used for the glass effect when available, with Canvas 2D as a fallback. The component respects reduced-motion preferences and pauses unnecessary rendering when hidden or offscreen.
702
+ - WebGL2 is used for the refractive glass effect.
703
+ - Canvas 2D is the rendering fallback when WebGL2 is unavailable.
704
+ - The component respects `prefers-reduced-motion`.
705
+ - Unnecessary drawing pauses when the page is hidden or the component is offscreen.
706
+ - The standalone runtime has no third-party dependencies.
417
707
 
418
- ### Development
708
+ ### Development and tests
419
709
 
420
710
  ```bash
421
711
  git clone https://github.com/GSvegetable/pixel-daylight-switch.git
422
712
  cd pixel-daylight-switch
423
713
  ```
424
714
 
425
- Open `index.html` in a modern browser. No build step is needed.
715
+ Open `index.html` in a modern browser. No build step is required.
716
+
717
+ Run package and SSR checks:
718
+
719
+ ```bash
720
+ npm test
721
+ ```
722
+
723
+ Run browser regression tests after installing Playwright and Chromium:
724
+
725
+ ```bash
726
+ npm install --no-save @playwright/test@1.56.1
727
+ npx playwright install chromium
728
+ npm run test:browser
729
+ ```
730
+
731
+ ### Version history
426
732
 
427
- ### Version status
733
+ - **v1.3.1** — lifecycle/remount stability fix and expanded automated regression coverage.
734
+ - **v1.3.0** — four additional pixel environments and scene selection in Playground.
735
+ - **v1.2.0** — motion, optics, world-speed, weather settings, and the standalone Playground.
736
+ - **v1.1.0** — initial HTML configuration options.
737
+ - **v1.0.0** — first public release.
428
738
 
429
- v1.3.0 adds four new pixel environments and a reorganized Playground. See [CHANGELOG.md](./CHANGELOG.md) for the detailed history.
739
+ See [CHANGELOG.md](./CHANGELOG.md) for details.
430
740
 
431
741
  ### License
432
742
 
433
- [Unlicense](https://unlicense.org/) — public domain. Free for personal and commercial use without attribution.
743
+ [Unlicense](https://unlicense.org/) — public-domain dedication. Free for personal and commercial use without attribution.