@lemoncat7/dsh-theme-xiaohei 0.3.0-alpha.23 → 0.3.2
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.md +39 -17
- package/THIRD_PARTY_NOTICES.md +41 -0
- package/lib/appearance.js +26 -10
- package/lib/blink.js +3 -3
- package/lib/bloub-heixiu/ambient.d.ts +3 -0
- package/lib/bloub-heixiu/ambient.js +12 -0
- package/lib/bloub-heixiu/character.d.ts +33 -0
- package/lib/bloub-heixiu/character.js +124 -0
- package/lib/bloub-heixiu/effects.d.ts +4 -0
- package/lib/bloub-heixiu/effects.js +91 -0
- package/lib/bloub-heixiu/flight.d.ts +15 -0
- package/lib/bloub-heixiu/flight.js +66 -0
- package/lib/bloub-heixiu/follow.d.ts +13 -0
- package/lib/bloub-heixiu/follow.js +23 -0
- package/lib/bloub-heixiu/layout.d.ts +18 -0
- package/lib/bloub-heixiu/layout.js +12 -0
- package/lib/bloub-heixiu/renderer.d.ts +16 -0
- package/lib/bloub-heixiu/renderer.js +94 -0
- package/lib/bloub-heixiu/runtime.d.ts +6 -0
- package/lib/bloub-heixiu/runtime.js +395 -0
- package/lib/bloub-heixiu/signals.d.ts +17 -0
- package/lib/bloub-heixiu/signals.js +53 -0
- package/lib/bloub-heixiu/styles.d.ts +2 -0
- package/lib/bloub-heixiu/styles.js +98 -0
- package/lib/chrome/accessibility.d.ts +1 -1
- package/lib/chrome/accessibility.js +5 -13
- package/lib/chrome/composer-avatar.d.ts +10 -0
- package/lib/chrome/composer-avatar.js +599 -0
- package/lib/chrome/composer-send-heixiu.d.ts +3 -0
- package/lib/chrome/composer-send-heixiu.js +143 -0
- package/lib/chrome/composer.d.ts +2 -2
- package/lib/chrome/composer.js +84 -95
- package/lib/chrome/controls.d.ts +8 -0
- package/lib/chrome/controls.js +348 -0
- package/lib/chrome/conversation-messages.d.ts +1 -1
- package/lib/chrome/conversation-messages.js +207 -9
- package/lib/chrome/conversation.d.ts +1 -1
- package/lib/chrome/conversation.js +6 -0
- package/lib/chrome/identity.js +4 -18
- package/lib/chrome/portal.d.ts +1 -1
- package/lib/chrome/portal.js +1 -1
- package/lib/chrome/sidebar.d.ts +1 -1
- package/lib/chrome/sidebar.js +134 -153
- package/lib/chrome/tokens.d.ts +1 -1
- package/lib/chrome/tokens.js +121 -68
- package/lib/chrome/workspace.d.ts +2 -2
- package/lib/chrome/workspace.js +35 -254
- package/lib/chrome.js +4 -0
- package/lib/client.d.ts +5 -12
- package/lib/client.js +6262 -4455
- package/lib/composer-avatar.d.ts +18 -0
- package/lib/composer-avatar.js +441 -0
- package/lib/composer-send-heixiu.d.ts +8 -0
- package/lib/composer-send-heixiu.js +133 -0
- package/lib/gaze.js +3 -3
- package/lib/generated-backgrounds.d.ts +14 -0
- package/lib/generated-backgrounds.js +14 -0
- package/lib/generated-character-motion.d.ts +4 -0
- package/lib/generated-character-motion.js +2 -0
- package/lib/generated-identity.d.ts +1 -0
- package/lib/generated-identity.js +1 -0
- package/lib/generated-keyart.d.ts +18 -3
- package/lib/generated-keyart.js +18 -3
- package/lib/host-contract.d.ts +9 -0
- package/lib/host-contract.js +13 -0
- package/lib/host-dom.d.ts +11 -0
- package/lib/host-dom.js +60 -0
- package/lib/loading-heixiu.js +3 -3
- package/lib/metallic-brand-mark.d.ts +10 -0
- package/lib/metallic-brand-mark.js +323 -0
- package/lib/plugin.d.ts +1 -1
- package/lib/plugin.js +31 -16
- package/lib/portal.js +11 -10
- package/lib/reactions.js +3 -3
- package/lib/scene/avatar-model.d.ts +6 -0
- package/lib/scene/avatar-model.js +646 -0
- package/lib/scene/avatar-motion.d.ts +3 -0
- package/lib/scene/avatar-motion.js +799 -0
- package/lib/scene/character-idle.d.ts +17 -0
- package/lib/scene/character-idle.js +65 -0
- package/lib/scene/character-layout.d.ts +24 -0
- package/lib/scene/character-layout.js +51 -0
- package/lib/scene/character-motion-clock.d.ts +18 -0
- package/lib/scene/character-motion-clock.js +57 -0
- package/lib/scene/character-motion-types.d.ts +12 -0
- package/lib/scene/character-motion-types.js +2 -0
- package/lib/scene/character-motion.d.ts +6 -0
- package/lib/scene/character-motion.js +126 -0
- package/lib/scene/character-poses.d.ts +26 -0
- package/lib/scene/character-poses.js +11 -0
- package/lib/scene/character-rig-renderer.d.ts +4 -0
- package/lib/scene/character-rig-renderer.js +85 -0
- package/lib/scene/character-rig.d.ts +20 -0
- package/lib/scene/character-rig.js +53 -0
- package/lib/scene/heixiu-feedback.d.ts +1 -1
- package/lib/scene/heixiu-feedback.js +0 -17
- package/lib/scene/pointer-bridge.d.ts +21 -0
- package/lib/scene/pointer-bridge.js +155 -0
- package/lib/scene/runtime.d.ts +12 -0
- package/lib/scene/runtime.js +87 -0
- package/lib/scene/styles.d.ts +7 -0
- package/lib/scene/styles.js +75 -0
- package/lib/scene/sylva-performance.d.ts +11 -0
- package/lib/scene/sylva-performance.js +58 -0
- package/lib/scene/wallpaper-character-styles.d.ts +3 -0
- package/lib/scene/wallpaper-character-styles.js +53 -0
- package/lib/scene/wallpaper-character.d.ts +4 -0
- package/lib/scene/wallpaper-character.js +213 -0
- package/lib/scene.d.ts +7 -13
- package/lib/scene.js +7 -1023
- package/lib/sidebar-glass.js +99 -36
- package/lib/sidebar-heixiu-roaming.js +5 -5
- package/lib/sidebar-reveal.d.ts +8 -0
- package/lib/sidebar-reveal.js +51 -0
- package/lib/state.d.ts +14 -2
- package/lib/state.js +12 -4
- package/lib/theme.js +2 -2
- package/lib/vendor/bloub/decor.d.ts +114 -0
- package/lib/vendor/bloub/decor.js +198 -0
- package/lib/vendor/bloub/engine.d.ts +213 -0
- package/lib/vendor/bloub/engine.js +452 -0
- package/lib/vendor/bloub/expressions.d.ts +35 -0
- package/lib/vendor/bloub/expressions.js +143 -0
- package/lib/vendor/bloub/eyefit.d.ts +75 -0
- package/lib/vendor/bloub/eyefit.js +393 -0
- package/lib/vendor/bloub/face.d.ts +63 -0
- package/lib/vendor/bloub/face.js +106 -0
- package/lib/vendor/bloub/math.d.ts +22 -0
- package/lib/vendor/bloub/math.js +36 -0
- package/lib/vendor/bloub/profiles.d.ts +8 -0
- package/lib/vendor/bloub/profiles.js +20 -0
- package/lib/vendor/bloub/repere.d.ts +30 -0
- package/lib/vendor/bloub/repere.js +32 -0
- package/lib/vendor/bloub/shape.d.ts +80 -0
- package/lib/vendor/bloub/shape.js +268 -0
- package/lib/vendor/bloub/skins.d.ts +36 -0
- package/lib/vendor/bloub/skins.js +74 -0
- package/lib/vendor/bloub/states.d.ts +87 -0
- package/lib/vendor/bloub/states.js +468 -0
- package/package.json +23 -6
- package/lib/appearance.d.ts.map +0 -1
- package/lib/appearance.js.map +0 -1
- package/lib/blink.d.ts.map +0 -1
- package/lib/blink.js.map +0 -1
- package/lib/boot-appearance.d.ts.map +0 -1
- package/lib/boot-appearance.js.map +0 -1
- package/lib/boot-loader.d.ts.map +0 -1
- package/lib/boot-loader.js.map +0 -1
- package/lib/chrome/accessibility.d.ts.map +0 -1
- package/lib/chrome/accessibility.js.map +0 -1
- package/lib/chrome/blink.d.ts.map +0 -1
- package/lib/chrome/blink.js.map +0 -1
- package/lib/chrome/composer.d.ts.map +0 -1
- package/lib/chrome/composer.js.map +0 -1
- package/lib/chrome/conversation-messages.d.ts.map +0 -1
- package/lib/chrome/conversation-messages.js.map +0 -1
- package/lib/chrome/conversation.d.ts.map +0 -1
- package/lib/chrome/conversation.js.map +0 -1
- package/lib/chrome/frames.d.ts.map +0 -1
- package/lib/chrome/frames.js.map +0 -1
- package/lib/chrome/gaze.d.ts.map +0 -1
- package/lib/chrome/gaze.js.map +0 -1
- package/lib/chrome/identity.d.ts.map +0 -1
- package/lib/chrome/identity.js.map +0 -1
- package/lib/chrome/overlays.d.ts.map +0 -1
- package/lib/chrome/overlays.js.map +0 -1
- package/lib/chrome/portal.d.ts.map +0 -1
- package/lib/chrome/portal.js.map +0 -1
- package/lib/chrome/reactions.d.ts.map +0 -1
- package/lib/chrome/reactions.js.map +0 -1
- package/lib/chrome/sidebar.d.ts.map +0 -1
- package/lib/chrome/sidebar.js.map +0 -1
- package/lib/chrome/tokens.d.ts.map +0 -1
- package/lib/chrome/tokens.js.map +0 -1
- package/lib/chrome/workspace.d.ts.map +0 -1
- package/lib/chrome/workspace.js.map +0 -1
- package/lib/chrome.d.ts.map +0 -1
- package/lib/chrome.js.map +0 -1
- package/lib/client.d.ts.map +0 -1
- package/lib/client.js.map +0 -7
- package/lib/gaze.d.ts.map +0 -1
- package/lib/gaze.js.map +0 -1
- package/lib/generated-identity.d.ts.map +0 -1
- package/lib/generated-identity.js.map +0 -1
- package/lib/generated-keyart.d.ts.map +0 -1
- package/lib/generated-keyart.js.map +0 -1
- package/lib/generated-loading-assets.d.ts.map +0 -1
- package/lib/generated-loading-assets.js.map +0 -1
- package/lib/generated-sidebar-assets.d.ts +0 -4
- package/lib/generated-sidebar-assets.d.ts.map +0 -1
- package/lib/generated-sidebar-assets.js +0 -4
- package/lib/generated-sidebar-assets.js.map +0 -1
- package/lib/heixiu-interactions.d.ts.map +0 -1
- package/lib/heixiu-interactions.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/loading-heixiu.d.ts.map +0 -1
- package/lib/loading-heixiu.js.map +0 -1
- package/lib/plugin.d.ts.map +0 -1
- package/lib/plugin.js.map +0 -1
- package/lib/portal.d.ts.map +0 -1
- package/lib/portal.js.map +0 -1
- package/lib/reactions.d.ts.map +0 -1
- package/lib/reactions.js.map +0 -1
- package/lib/scene/heixiu-feedback.d.ts.map +0 -1
- package/lib/scene/heixiu-feedback.js.map +0 -1
- package/lib/scene/heixiu-interactions.d.ts.map +0 -1
- package/lib/scene/heixiu-interactions.js.map +0 -1
- package/lib/scene/state-transitions.d.ts.map +0 -1
- package/lib/scene/state-transitions.js.map +0 -1
- package/lib/scene.d.ts.map +0 -1
- package/lib/scene.js.map +0 -1
- package/lib/sidebar-glass.d.ts.map +0 -1
- package/lib/sidebar-glass.js.map +0 -1
- package/lib/sidebar-heixiu-escape.d.ts.map +0 -1
- package/lib/sidebar-heixiu-escape.js.map +0 -1
- package/lib/sidebar-heixiu-roaming.d.ts.map +0 -1
- package/lib/sidebar-heixiu-roaming.js.map +0 -1
- package/lib/state.d.ts.map +0 -1
- package/lib/state.js.map +0 -1
- package/lib/theme.d.ts.map +0 -1
- package/lib/theme.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
# Xiaohei Night for DSH
|
|
2
2
|
|
|
3
|
-
`Xiaohei Night` 是一个基于 DeepSeek Harness
|
|
3
|
+
`Xiaohei Night` 是一个基于 DeepSeek Harness 官方主题注册接口开发的非官方粉丝主题插件。主题以低饱和天空、日夜光线和坐在小平台上的人形小黑表达陪伴感,保留原生工作台布局。
|
|
4
|
+
|
|
5
|
+
## 兼容性
|
|
6
|
+
|
|
7
|
+
正式版 `0.3.2` 针对 DeepSeek Harness `0.1.2-rc.1` 构建,需要 Node.js `22.19+` 或 `24+`。Conversation、Renderer、Sidebar、Slots 与 Theme 接口均对应 `0.1.2-rc.1`。本版增加人物局部 2D 骨骼动画,优化侧栏切换性能和文字入场时序,发布为 npm `latest`。“跟随系统”读取当前设备/浏览器的配色偏好,不按服务器时间切换。
|
|
4
8
|
|
|
5
9
|
## 当前范围
|
|
6
10
|
|
|
7
11
|
- 保留 DSH 原有布局、密度和交互层级。
|
|
8
12
|
- 使用半透明墨黑表面,让背景可见但不压过对话内容。
|
|
9
13
|
- 使用青绿色作为唯一交互强调色,错误、成功、警告保留独立语义色。
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
14
|
+
- 背景与人物分层:保留日间/夜间的柔和天空;人物使用独立高清、无损 WebP,不随整张壁纸放大。坐姿、托腮和打盹统一为 720×1019,探头素材宽 360;最大显示尺寸仍有约 3 倍像素密度。姿势按需解码后再显示,保留轮廓与五官细节。
|
|
15
|
+
- 根据真实剩余空间决定姿势:右侧宽裕时坐在小平台上,中等窗口扶着侧栏探头;细侧栏空位使用更窄的半脸偷看。没有安全空间时暂时隐藏,不覆盖消息、头像或侧栏按钮,不修改业务布局。
|
|
16
|
+
- 坐姿下,用户空闲 30 秒后托腮、3 分钟后闭眼打盹;鼠标、键盘、触摸点击或滚轮操作后恢复。页面隐藏或不再使用坐姿时暂停计时,返回后重新开始。最多一个空闲计时器,睡着后不继续轮询,不绑定 AI 状态,不读取输入内容。姿势以 220ms 淡化过渡,不是连续骨骼动画。
|
|
17
|
+
- 耳朵以独立耳根转动,坐姿和托腮的尾巴通过 7 个关节点与网格蒙皮连续变形,鼠标靠近可触发联动。WebP 仅用于静态图层,不播放帧条;眨眼仍为眼睑贴图合成,身体与四肢没有完整骨骼绑定,并非完整 Live2D。动作最高 30 帧,静止、后台和减少动态模式不持续绘制。
|
|
18
|
+
- 装饰背景使用稳定的大视口高度;姿势切换不横穿会话,不读取会话内容或执行状态;流式文本高度变化不触发布局重测。软键盘兼容性仍需 iOS / Android 真机验收。
|
|
19
|
+
- bloub 嘿咻保留 alpha.171 略宽于高的小黑团轮廓和圆润奶白眼圈。单击依次眨眼回应、轻跳、歪头、感叹号惊讶;双击或键盘 Shift+Enter 轮换散开重聚、蛋形、环绕、六边形、单眼眨眼、睁大眼、感叹号、警觉、休眠、思考、通知。每隔随机 20–40 秒自动选择一种待机动作(含眨眼、休眠,不含思考、通知、感叹号),不连续重复;打字、飞行或其他动作尚未结束时顺延。后台/离屏/减少动态时取消调度,恢复后重新等待,不补播。
|
|
20
|
+
- 嘿咻通过可选的官方 `sessions.list` / `binding().session` 只读订阅当前会话:运行中显示三点思考(代表本轮处理中,不细分推理/输出/工具),观察到本轮结束时提醒一次,新执行错误显示感叹号。提醒表示本轮结束,不承诺任务成功,手动停止也可能触发。初次加载历史、切换会话、重新加载历史不会补发完成提醒。通知徽点采用暖金色和真正透明的缺口;不申请浏览器通知权限、不发送消息、不操作会话、不关联壁纸。
|
|
21
|
+
- 新会话将同一只嘿咻停在输入框上方、Logo 左边;输入区落到底部时,用可打断的约 950 ms 飞行动画转移到输入框左侧。手机给 Logo 左侧留出空间并缩小角色;普通窗口缩放不触发跨区域飞行。仅使用现有 Hero 品牌元素和输入框位置,不读取或改变会话业务数据。
|
|
22
|
+
- 慢浮动周期 14 秒,宽屏横向 ±7 px、纵向 ±10 px;窄屏自动减小幅度。视线跟随窗口内鼠标,身体只在近处轻微靠近(宽屏最多 5 / 3.5 px),约 480 ms 柔和回位。CSS transform 合成浮动和跟随,不增加持续 JS 帧计算或逐帧布局测量;除短暂换位飞行外,点击区域保持固定。减少动态模式关闭浮动、跟随、飞行和自动动作;不改发送键。
|
|
23
|
+
- 嘿咻初始只有 10 个 SVG 节点;首次触发特殊动作才一次性建立粒子/轨道池及通知缺口,之后固定复用(总计 102 节点),帧循环不新增节点、不测量布局,结束后隐藏特效。小尺寸变形中的眼圈适当收窄,避免相互粘连。空闲间歇唤醒,离屏、隐藏和减少动态时暂停,卸载释放监听、观察器、帧请求和定时器。宽屏使用框外空位,窄屏使用框内左侧预留位,不改发送键。原项目 MIT 许可与固定源码版本见 `THIRD_PARTY_NOTICES.md`;观察及改动记录见 `docs/heixiu-motion-reference.md`。
|
|
24
|
+
- 对话双方保留静态身份头像;消息气泡使用更舒适的横向留白。
|
|
25
|
+
- 左侧栏只有一层独立磨砂材质,目录与会话不再各自模糊。展开目录、Hover、选中会话使用逐级增强的中性底色;移除拼接空间框和底部卡片,原生滚动、拖拽和菜单保持不变。
|
|
26
|
+
- 侧栏展开时,内容等待列宽到位后统一淡入,避免文字先出现再被展开裁切。切换期间暂停装饰角色布局和绘制;侧栏品牌金属画面缓存复用,不再反复创建 WebGL 和编译着色器。
|
|
27
|
+
- 新会话使用统一圆角控件;发送键嘿咻继续保留完整帧眨眼。左栏行宽、缩进、滚动条预留和收起布局交给 DSH 管理,避免主题强行计算宽度。
|
|
20
28
|
- 支持减少动态、增强对比度、强制颜色和打印模式降级。
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
状态优先级为:错误 → 等待用户 → 工具执行 → 流式回答 → 聚能思考 → 完成 → 待机。完成态仅在同一个当前会话结束后显示约 1.6 秒。
|
|
29
|
+
- 主题通过 DSH Theme Runtime 覆盖 Light / Dark / System 的语义 token,不替换用户的主题偏好;卸载后由 DSH 恢复原始 token。
|
|
24
30
|
|
|
25
31
|
## 安装
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
通过 npm 安装正式版:
|
|
28
34
|
|
|
29
35
|
```bash
|
|
30
|
-
dsh plugin --profile web add
|
|
36
|
+
dsh plugin --profile web add @lemoncat7/dsh-theme-xiaohei@0.3.2
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
也可从 [GitHub Releases](https://github.com/lemoncat7/dsh-theme-xiaohei/releases/tag/v0.3.2) 下载完整包后安装:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dsh plugin --profile web add ./lemoncat7-dsh-theme-xiaohei-0.3.2.tgz
|
|
31
43
|
```
|
|
32
44
|
|
|
33
45
|
卸载:
|
|
@@ -52,6 +64,16 @@ npm run pack:check
|
|
|
52
64
|
npm run pack:docker
|
|
53
65
|
```
|
|
54
66
|
|
|
67
|
+
### 代码边界
|
|
68
|
+
|
|
69
|
+
- `src/index.ts` 只负责 Host 首帧加载装饰,保持自包含并在客户端插件启动前运行。
|
|
70
|
+
- `src/plugin.ts` 通过 DSH 生命周期装配主题 token、Chrome、场景与交互行为。
|
|
71
|
+
- `src/chrome/` 管理控件和公共插件表面;`src/scene/` 分离场景样式与运行时 DOM。
|
|
72
|
+
- `src/host-contract.ts` 集中维护宿主插槽选择器;`src/host-dom.ts` 为运行期模块共享一个批处理 DOM 观察器。左栏只在尺寸或节点变化时测量布局,流式文本更新不重复强制布局。
|
|
73
|
+
- `src/scene/character-layout.ts` 只计算安全位置,`character-idle.ts` 只管理空闲期限,`character-poses.ts` 统一素材目录;`wallpaper-character.ts` 负责生命周期、按需解码和呈现,卸载时释放监听与计时器。
|
|
74
|
+
- `src/vendor/bloub/` 保留上游动画计算,`src/bloub-heixiu/` 分离 SVG 呈现、布局、样式和生命周期。`signals.ts` 可选地只读订阅当前会话执行状态,不读取会话内容,不操作会话。
|
|
75
|
+
- 旧树洞/落叶挂载器已移除,历史图片保留供版本回溯;背景、材质和业务状态各自独立。
|
|
76
|
+
|
|
55
77
|
## 扩展工具接入主题
|
|
56
78
|
|
|
57
79
|
SSH、伙伴、Git 等后续工具不需要复制主题 CSS。插件根节点使用公开的框架属性即可继承小黑主题:
|
|
@@ -77,7 +99,7 @@ SSH、伙伴、Git 等后续工具不需要复制主题 CSS。插件根节点使
|
|
|
77
99
|
|
|
78
100
|
## 视觉资产
|
|
79
101
|
|
|
80
|
-
|
|
102
|
+
背景使用内置的日夜天空 WebP、CSS 渐变及按需加载的人物透明素材,不加载 Three.js、外部 CDN 或角色模型。品牌头像、消息静态头像、加载动画与发送键嘿咻作为内联资源随插件交付;嘿咻交互使用 SVG。
|
|
81
103
|
|
|
82
104
|
## License
|
|
83
105
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# bloub
|
|
2
|
+
|
|
3
|
+
Source: https://github.com/jeremy-prt/bloub
|
|
4
|
+
Revision: b4bb3c1b5f93c7b87a2e8d620f667c4093d97749
|
|
5
|
+
|
|
6
|
+
`src/vendor/bloub/` contains the framework-free animation engine from bloub.
|
|
7
|
+
Authored body profiles, state choreography, gaze projection and interruption
|
|
8
|
+
blending are retained. Local changes: NodeNext `.js` import extensions; lazy
|
|
9
|
+
custom-shape fitting table (unknown profiles bypass the catalogue); repeat the
|
|
10
|
+
blink calendar for long-running sessions; expose eye bounds, uncompressed tangent
|
|
11
|
+
and lid openness for a separate hollow-eye renderer; interpolate notification
|
|
12
|
+
badge/notch radii during entry, exit and interrupted morphs. The theme supplies a custom
|
|
13
|
+
Heixiu radial profile and face through the upstream customisation API. Its gesture
|
|
14
|
+
tracks, DOM lifecycle, placement and scheduler are separate from the upstream core.
|
|
15
|
+
No Vue application, editor, video exporter or media dependencies are included.
|
|
16
|
+
|
|
17
|
+
The original project recreates an x.ai avatar; its MIT license covers code, not
|
|
18
|
+
ownership of the character designs. This theme is an unofficial fan work and is
|
|
19
|
+
not endorsed by the owners of Luo Xiaohei or x.ai.
|
|
20
|
+
|
|
21
|
+
## MIT License
|
|
22
|
+
|
|
23
|
+
Copyright (c) 2026 Jérémy Perret
|
|
24
|
+
|
|
25
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
26
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
27
|
+
in the Software without restriction, including without limitation the rights
|
|
28
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
29
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
30
|
+
furnished to do so, subject to the following conditions:
|
|
31
|
+
|
|
32
|
+
The above copyright notice and this permission notice shall be included in all
|
|
33
|
+
copies or substantial portions of the Software.
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
36
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
37
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
38
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
39
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
40
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
41
|
+
SOFTWARE.
|
package/lib/appearance.js
CHANGED
|
@@ -4,22 +4,38 @@ export const XIAOHEI_APPEARANCE_ATTRIBUTE = 'data-xiaohei-appearance';
|
|
|
4
4
|
export function bindXiaoheiAppearance(ctx, doc = typeof document === 'undefined' ? undefined : document) {
|
|
5
5
|
if (doc === undefined)
|
|
6
6
|
return () => { };
|
|
7
|
+
const win = doc.defaultView;
|
|
8
|
+
let handoff;
|
|
9
|
+
let disposed = false;
|
|
10
|
+
const cancelHandoff = () => {
|
|
11
|
+
if (handoff !== undefined)
|
|
12
|
+
win?.clearTimeout(handoff);
|
|
13
|
+
handoff = undefined;
|
|
14
|
+
};
|
|
7
15
|
const sync = (snapshot) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
doc.documentElement.setAttribute(XIAOHEI_APPEARANCE_ATTRIBUTE, snapshot.active.colorScheme);
|
|
16
|
-
}
|
|
16
|
+
if (disposed)
|
|
17
|
+
return;
|
|
18
|
+
cancelHandoff();
|
|
19
|
+
doc.documentElement.setAttribute(XIAOHEI_APPEARANCE_ATTRIBUTE, snapshot.active.colorScheme);
|
|
17
20
|
releaseXiaoheiBootAppearance(doc);
|
|
18
21
|
};
|
|
19
22
|
const off = ctx.on('theme/change', sync);
|
|
20
|
-
|
|
23
|
+
const initial = ctx.theme.getTheme();
|
|
24
|
+
const boot = doc.documentElement.getAttribute(XIAOHEI_BOOT_APPEARANCE_ATTRIBUTE);
|
|
25
|
+
if ((boot === 'light' || boot === 'dark') && boot !== initial.active.colorScheme) {
|
|
26
|
+
// Preserve the Host's first frame during settings hydration, but never
|
|
27
|
+
// let a stale boot marker veto later theme events or lock a palette.
|
|
28
|
+
doc.documentElement.setAttribute(XIAOHEI_APPEARANCE_ATTRIBUTE, boot);
|
|
29
|
+
if (win)
|
|
30
|
+
handoff = win.setTimeout(() => sync(ctx.theme.getTheme()), 1000);
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
sync(initial);
|
|
21
34
|
return () => {
|
|
35
|
+
disposed = true;
|
|
36
|
+
cancelHandoff();
|
|
22
37
|
off();
|
|
38
|
+
releaseXiaoheiBootAppearance(doc);
|
|
23
39
|
doc.documentElement.removeAttribute(XIAOHEI_APPEARANCE_ATTRIBUTE);
|
|
24
40
|
};
|
|
25
41
|
}
|
package/lib/blink.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { XIAOHEI_BLINK_CSS } from './chrome/blink.js';
|
|
2
2
|
import { XIAOHEI_HEIXIU_GREETING_EVENT } from './heixiu-interactions.js';
|
|
3
|
+
import { subscribeXiaoheiHostDom } from './host-dom.js';
|
|
3
4
|
export const XIAOHEI_BLINK_STYLE_ID = 'dsh-theme-xiaohei/blink-style';
|
|
4
5
|
const INITIAL_BLINK_DELAY_MS = 4780;
|
|
5
6
|
const IDLE_BLINK_INTERVAL_MS = 5065;
|
|
@@ -84,8 +85,7 @@ export function installXiaoheiBlink(doc = typeof document === 'undefined' ? unde
|
|
|
84
85
|
});
|
|
85
86
|
};
|
|
86
87
|
const onEnvironmentChange = () => restartIdleBlink();
|
|
87
|
-
const
|
|
88
|
-
mountObserver.observe(doc.body, { childList: true, subtree: true });
|
|
88
|
+
const unsubscribeHostDom = subscribeXiaoheiHostDom(doc, attach);
|
|
89
89
|
const stateObserver = new win.MutationObserver(onEnvironmentChange);
|
|
90
90
|
stateObserver.observe(doc.documentElement, {
|
|
91
91
|
attributes: true,
|
|
@@ -98,7 +98,7 @@ export function installXiaoheiBlink(doc = typeof document === 'undefined' ? unde
|
|
|
98
98
|
return () => {
|
|
99
99
|
disposed = true;
|
|
100
100
|
clearTimers();
|
|
101
|
-
|
|
101
|
+
unsubscribeHostDom();
|
|
102
102
|
stateObserver.disconnect();
|
|
103
103
|
doc.removeEventListener('visibilitychange', onEnvironmentChange);
|
|
104
104
|
doc.removeEventListener(XIAOHEI_HEIXIU_GREETING_EVENT, onHeixiuGreeting);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const unit = (n) => Number.isFinite(n) ? Math.max(0, Math.min(.999999, n)) : .5;
|
|
2
|
+
export function heixiuAmbientDelay(random = Math.random) {
|
|
3
|
+
return 20_000 + Math.floor(unit(random()) * 20_000);
|
|
4
|
+
}
|
|
5
|
+
export function heixiuAmbientIndex(count, previous, random = Math.random) {
|
|
6
|
+
if (count <= 1)
|
|
7
|
+
return 0;
|
|
8
|
+
const exclude = previous >= 0 && previous < count;
|
|
9
|
+
const index = Math.floor(unit(random()) * (exclude ? count - 1 : count));
|
|
10
|
+
return exclude && index >= previous ? index + 1 : index;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=ambient.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BotExpression } from '../vendor/bloub/expressions.js';
|
|
2
|
+
export declare const HEIXIU_PROFILE: number[];
|
|
3
|
+
export declare const HEIXIU_FACE: BotExpression;
|
|
4
|
+
export declare const HEIXIU_FOOT: number;
|
|
5
|
+
export interface HeixiuReaction {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
sx: number;
|
|
9
|
+
sy: number;
|
|
10
|
+
roll: number;
|
|
11
|
+
lid: number;
|
|
12
|
+
cheer: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const HEIXIU_REST: HeixiuReaction;
|
|
15
|
+
export declare const HEIXIU_REACTIONS: readonly ["greet", "hop", "wiggle"];
|
|
16
|
+
export type HeixiuReactionId = typeof HEIXIU_REACTIONS[number];
|
|
17
|
+
/** One bounded, interruptible gesture track; never queues clicks or runs timers. */
|
|
18
|
+
export declare class HeixiuReactions {
|
|
19
|
+
private start;
|
|
20
|
+
private keys;
|
|
21
|
+
private hover;
|
|
22
|
+
private hoverAt;
|
|
23
|
+
private hoverFrom;
|
|
24
|
+
private hoverTo;
|
|
25
|
+
get end(): number;
|
|
26
|
+
reset(): void;
|
|
27
|
+
settle(time: number): void;
|
|
28
|
+
aim(direction: number, time: number): void;
|
|
29
|
+
private track;
|
|
30
|
+
play(kind: HeixiuReactionId, time: number, side?: number): void;
|
|
31
|
+
sample(time: number): HeixiuReaction;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=character.d.ts.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { superellipseProfile } from '../vendor/bloub/shape.js';
|
|
2
|
+
// Slightly wider than tall: a soft black body, not an upright head or a flat
|
|
3
|
+
// pill. Side tips sit near the equator. Keep bloub's 64 radial samples.
|
|
4
|
+
export const HEIXIU_PROFILE = superellipseProfile(2.2, 1, .89).map((r, i, radii) => {
|
|
5
|
+
const angle = i / radii.length * Math.PI * 2;
|
|
6
|
+
const bump = (center, width, amount) => {
|
|
7
|
+
const d = Math.atan2(Math.sin(angle - center), Math.cos(angle - center));
|
|
8
|
+
return amount * Math.exp(-.5 * (d / width) ** 2);
|
|
9
|
+
};
|
|
10
|
+
return r * (1 + .016 * Math.sin(angle))
|
|
11
|
+
+ bump(6.12, .12, .065) + bump(3.30, .12, .065)
|
|
12
|
+
+ bump(4.04, .10, .025);
|
|
13
|
+
});
|
|
14
|
+
export const HEIXIU_FACE = {
|
|
15
|
+
id: 'neutre', gaze: { yaw: 0, pitch: -7, roll: -2 }, split: 26,
|
|
16
|
+
eyes: [{ w: .355, h: .48, open: 1 }, { w: .34, h: .465, open: 1 }],
|
|
17
|
+
};
|
|
18
|
+
// Derive the squash pivot from the body itself so a proportion adjustment
|
|
19
|
+
// cannot leave the character rotating/compressing around an invisible foot.
|
|
20
|
+
export const HEIXIU_FOOT = Math.max(...HEIXIU_PROFILE.map((r, i) => r * Math.sin(i / HEIXIU_PROFILE.length * Math.PI * 2))) * 100;
|
|
21
|
+
export const HEIXIU_REST = { x: 0, y: 0, sx: 1, sy: 1, roll: 0, lid: 1, cheer: 0 };
|
|
22
|
+
export const HEIXIU_REACTIONS = ['greet', 'hop', 'wiggle'];
|
|
23
|
+
const smooth = (x) => { const k = Math.max(0, Math.min(1, x)); return k * k * (3 - 2 * k); };
|
|
24
|
+
const pose = (over) => ({ ...HEIXIU_REST, ...over });
|
|
25
|
+
const CHANNELS = Object.keys(HEIXIU_REST);
|
|
26
|
+
// Shape-preserving Hermite tangents: a rising/falling arc passes through its
|
|
27
|
+
// intermediate samples without stopping at every key. Extrema still settle,
|
|
28
|
+
// and lids/scales never overshoot their authored bounds.
|
|
29
|
+
function tangent(keys, index, channel) {
|
|
30
|
+
if (index === 0 || index === keys.length - 1)
|
|
31
|
+
return 0;
|
|
32
|
+
const a = keys[index - 1], b = keys[index], c = keys[index + 1];
|
|
33
|
+
const h0 = b.at - a.at, h1 = c.at - b.at;
|
|
34
|
+
const d0 = (b.pose[channel] - a.pose[channel]) / h0, d1 = (c.pose[channel] - b.pose[channel]) / h1;
|
|
35
|
+
if (d0 * d1 <= 0)
|
|
36
|
+
return 0;
|
|
37
|
+
const w0 = 2 * h1 + h0, w1 = h1 + 2 * h0;
|
|
38
|
+
return (w0 + w1) / (w0 / d0 + w1 / d1);
|
|
39
|
+
}
|
|
40
|
+
/** One bounded, interruptible gesture track; never queues clicks or runs timers. */
|
|
41
|
+
export class HeixiuReactions {
|
|
42
|
+
start = 0;
|
|
43
|
+
keys = [];
|
|
44
|
+
hover = 0;
|
|
45
|
+
hoverAt = 0;
|
|
46
|
+
hoverFrom = 0;
|
|
47
|
+
hoverTo = 0;
|
|
48
|
+
get end() { return this.start + (this.keys.at(-1)?.at ?? 0); }
|
|
49
|
+
reset() { this.keys = []; this.hover = this.hoverFrom = this.hoverTo = 0; }
|
|
50
|
+
settle(time) {
|
|
51
|
+
const origin = this.track(time);
|
|
52
|
+
this.start = time;
|
|
53
|
+
this.keys = [{ at: 0, pose: origin }, { at: .22, pose: { ...HEIXIU_REST } }];
|
|
54
|
+
this.aim(0, time);
|
|
55
|
+
}
|
|
56
|
+
aim(direction, time) {
|
|
57
|
+
this.hover = this.hoverFrom + (this.hoverTo - this.hoverFrom) * smooth((time - this.hoverAt) / .28);
|
|
58
|
+
this.hoverFrom = this.hover;
|
|
59
|
+
this.hoverTo = Math.max(-1, Math.min(1, direction));
|
|
60
|
+
this.hoverAt = time;
|
|
61
|
+
}
|
|
62
|
+
track(time) {
|
|
63
|
+
if (!this.keys.length || time >= this.end)
|
|
64
|
+
return { ...HEIXIU_REST };
|
|
65
|
+
const t = Math.max(0, time - this.start);
|
|
66
|
+
let left = this.keys[0];
|
|
67
|
+
for (let i = 1; i < this.keys.length; i++) {
|
|
68
|
+
const right = this.keys[i];
|
|
69
|
+
if (t <= right.at) {
|
|
70
|
+
const span = right.at - left.at, k = (t - left.at) / span;
|
|
71
|
+
const k2 = k * k, k3 = k2 * k;
|
|
72
|
+
const out = { ...left.pose };
|
|
73
|
+
for (const key of CHANNELS) {
|
|
74
|
+
out[key] = (2 * k3 - 3 * k2 + 1) * left.pose[key] + (-2 * k3 + 3 * k2) * right.pose[key]
|
|
75
|
+
+ (k3 - 2 * k2 + k) * span * tangent(this.keys, i - 1, key)
|
|
76
|
+
+ (k3 - k2) * span * tangent(this.keys, i, key);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
left = right;
|
|
81
|
+
}
|
|
82
|
+
return { ...HEIXIU_REST };
|
|
83
|
+
}
|
|
84
|
+
play(kind, time, side = 1) {
|
|
85
|
+
const origin = this.track(time), sign = side < 0 ? -1 : 1;
|
|
86
|
+
const keys = [{ at: 0, pose: origin }];
|
|
87
|
+
const add = (at, over) => keys.push({ at, pose: pose(over) });
|
|
88
|
+
if (kind === 'greet') {
|
|
89
|
+
add(.09, { lid: .25, sy: .99, cheer: .35 });
|
|
90
|
+
add(.16, { lid: 0, sy: .985, cheer: .35, roll: -2 * sign });
|
|
91
|
+
add(.26, { y: -2, sy: 1.005, roll: 1 * sign });
|
|
92
|
+
add(.43, { y: -7, sx: .99, sy: 1.015, roll: 3 * sign });
|
|
93
|
+
add(.67, { sx: 1.018, sy: .982, roll: -1 * sign });
|
|
94
|
+
add(.95, {});
|
|
95
|
+
}
|
|
96
|
+
else if (kind === 'hop') {
|
|
97
|
+
add(.12, { sx: 1.065, sy: .94, lid: .85, roll: -2 * sign });
|
|
98
|
+
add(.23, { sx: .97, sy: 1.045, y: -14, roll: 2 * sign });
|
|
99
|
+
add(.40, { sx: .99, sy: 1.02, y: -28, roll: 3 * sign });
|
|
100
|
+
add(.57, { sy: 1.015, y: -14, roll: 1 * sign });
|
|
101
|
+
add(.69, { sx: 1.06, sy: .94, lid: .55, cheer: .3 });
|
|
102
|
+
add(.80, { sx: .985, sy: 1.02 });
|
|
103
|
+
add(1.02, {});
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
add(.18, { x: -2 * sign, roll: -7 * sign });
|
|
107
|
+
add(.38, { x: 2 * sign, roll: 7 * sign });
|
|
108
|
+
add(.55, { x: -1 * sign, roll: -3 * sign, lid: .3, cheer: .2 });
|
|
109
|
+
add(.64, { roll: -1 * sign, lid: 0, cheer: .2 });
|
|
110
|
+
add(.78, {});
|
|
111
|
+
add(.96, {});
|
|
112
|
+
}
|
|
113
|
+
this.start = time;
|
|
114
|
+
this.keys = keys;
|
|
115
|
+
}
|
|
116
|
+
sample(time) {
|
|
117
|
+
const out = this.track(time);
|
|
118
|
+
this.hover = this.hoverFrom + (this.hoverTo - this.hoverFrom) * smooth((time - this.hoverAt) / .28);
|
|
119
|
+
out.roll += this.hover * 5;
|
|
120
|
+
out.x += this.hover * 4;
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=character.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BotFrame } from '../vendor/bloub/engine.js';
|
|
2
|
+
/** Allocated once on first explicit special action, never from the frame loop. */
|
|
3
|
+
export declare function createHeixiuEffects(doc: Document, id: string, defs: SVGDefsElement, behind: SVGGElement, front: SVGGElement, body: SVGGElement): (frame: BotFrame) => void;
|
|
4
|
+
//# sourceMappingURL=effects.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
2
|
+
/** Allocated once on first explicit special action, never from the frame loop. */
|
|
3
|
+
export function createHeixiuEffects(doc, id, defs, behind, front, body) {
|
|
4
|
+
const el = (name) => doc.createElementNS(NS, name);
|
|
5
|
+
const attrs = (node, values) => {
|
|
6
|
+
for (const [key, value] of Object.entries(values)) {
|
|
7
|
+
const text = String(value);
|
|
8
|
+
if (node.getAttribute(key) !== text)
|
|
9
|
+
node.setAttribute(key, text);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const dotsBack = el('g'), dotsFront = el('g');
|
|
13
|
+
behind.append(dotsBack);
|
|
14
|
+
front.append(dotsFront);
|
|
15
|
+
// Real transparent notch, never a wallpaper-coloured disc over the body.
|
|
16
|
+
const mask = el('mask'), surface = el('rect'), notch = el('circle'), badge = el('circle');
|
|
17
|
+
attrs(mask, { id: `${id}-notification`, maskUnits: 'userSpaceOnUse', x: -200, y: -200, width: 400, height: 400, 'mask-type': 'luminance' });
|
|
18
|
+
attrs(surface, { x: -200, y: -200, width: 400, height: 400, fill: 'white' });
|
|
19
|
+
attrs(notch, { fill: 'black', r: 0, 'data-heixiu-notch': '' });
|
|
20
|
+
attrs(badge, { fill: '#dca964', display: 'none', 'data-heixiu-notification': '' });
|
|
21
|
+
mask.append(surface, notch);
|
|
22
|
+
defs.append(mask);
|
|
23
|
+
front.append(badge);
|
|
24
|
+
const dots = Array.from({ length: 12 }, () => {
|
|
25
|
+
const node = el('path');
|
|
26
|
+
attrs(node, { display: 'none', 'data-heixiu-particle': '' });
|
|
27
|
+
dotsFront.append(node);
|
|
28
|
+
return node;
|
|
29
|
+
});
|
|
30
|
+
const arcs = Array.from({ length: 12 }, (_, i) => {
|
|
31
|
+
const gradient = el('linearGradient'), back = el('path'), fore = el('path');
|
|
32
|
+
attrs(gradient, { id: `${id}-arc-${i}`, gradientUnits: 'userSpaceOnUse' });
|
|
33
|
+
const stops = Array.from({ length: 3 }, () => el('stop'));
|
|
34
|
+
gradient.append(...stops);
|
|
35
|
+
defs.append(gradient);
|
|
36
|
+
for (const node of [back, fore])
|
|
37
|
+
attrs(node, {
|
|
38
|
+
fill: 'none', 'stroke-linecap': 'round', stroke: `url(#${id}-arc-${i})`, display: 'none', 'data-heixiu-orbit': '',
|
|
39
|
+
});
|
|
40
|
+
behind.append(back);
|
|
41
|
+
front.append(fore);
|
|
42
|
+
return { gradient, back, fore, stops };
|
|
43
|
+
});
|
|
44
|
+
return (frame) => {
|
|
45
|
+
if (frame.notif && frame.notch) {
|
|
46
|
+
attrs(body, { mask: `url(#${id}-notification)` });
|
|
47
|
+
attrs(notch, { cx: frame.notch.x, cy: frame.notch.y, r: frame.notch.r });
|
|
48
|
+
attrs(badge, { display: 'inline', cx: frame.notif.x, cy: frame.notif.y, r: frame.notif.r });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (body.getAttribute('mask'))
|
|
52
|
+
body.removeAttribute('mask');
|
|
53
|
+
attrs(notch, { r: 0 });
|
|
54
|
+
attrs(badge, { display: 'none' });
|
|
55
|
+
}
|
|
56
|
+
for (const [i, node] of dots.entries()) {
|
|
57
|
+
const dot = frame.dots[i];
|
|
58
|
+
if (!dot) {
|
|
59
|
+
attrs(node, { display: 'none' });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const parent = frame.dotsBehind ? dotsBack : dotsFront;
|
|
63
|
+
if (node.parentNode !== parent)
|
|
64
|
+
parent.append(node);
|
|
65
|
+
const r = dot.r;
|
|
66
|
+
attrs(node, { display: 'inline',
|
|
67
|
+
d: dot.d ?? `M${-r} 0a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0`,
|
|
68
|
+
transform: `translate(${dot.x} ${dot.y}) rotate(${dot.rot ?? 0}) scale(${dot.d ? 100 : 1})`,
|
|
69
|
+
fill: dot.color ?? 'var(--heixiu-ink)', opacity: dot.opacity * (dot.depth ?? 1),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
for (const [i, nodes] of arcs.entries()) {
|
|
73
|
+
const arc = frame.arcs[i];
|
|
74
|
+
if (!arc) {
|
|
75
|
+
attrs(nodes.back, { display: 'none' });
|
|
76
|
+
attrs(nodes.fore, { display: 'none' });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
attrs(nodes.gradient, { x1: arc.grad.x1, y1: arc.grad.y1, x2: arc.grad.x2, y2: arc.grad.y2 });
|
|
80
|
+
for (const [j, stop] of nodes.stops.entries())
|
|
81
|
+
attrs(stop, {
|
|
82
|
+
offset: j / 2, 'stop-color': arc.grad.stops[Math.min(j, arc.grad.stops.length - 1)] ?? '#ddd',
|
|
83
|
+
});
|
|
84
|
+
for (const [node, d] of [[nodes.back, arc.back], [nodes.fore, arc.front]])
|
|
85
|
+
attrs(node, {
|
|
86
|
+
display: 'inline', d, 'stroke-width': arc.width, opacity: arc.opacity,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=effects.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface HeixiuBox {
|
|
2
|
+
left: number;
|
|
3
|
+
top: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
/** FLIP between native docks; the existing SVG and reaction clock stay alive. */
|
|
8
|
+
export declare function createHeixiuFlight(button: HTMLElement): {
|
|
9
|
+
readonly active: boolean;
|
|
10
|
+
capture: (fallback?: HeixiuBox) => HeixiuBox | undefined;
|
|
11
|
+
target(measured: HeixiuBox): HeixiuBox;
|
|
12
|
+
move(from: HeixiuBox | undefined, to: HeixiuBox, reduced: boolean): void;
|
|
13
|
+
cancel: () => void;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=flight.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const copyBox = (box) => ({ left: box.left, top: box.top, width: box.width, height: box.height });
|
|
2
|
+
/** FLIP between native docks; the existing SVG and reaction clock stay alive. */
|
|
3
|
+
export function createHeixiuFlight(button) {
|
|
4
|
+
let animation;
|
|
5
|
+
let route;
|
|
6
|
+
const active = () => animation?.playState === 'running';
|
|
7
|
+
const progress = () => Math.max(0, Math.min(1, animation?.effect?.getComputedTiming().progress ?? 0));
|
|
8
|
+
// Native borders can settle by half a CSS pixel; this is not a new flight.
|
|
9
|
+
const same = (a, b) => ['left', 'top', 'width', 'height'].every(k => Math.abs(a[k] - b[k]) < 1);
|
|
10
|
+
function position(fallback) {
|
|
11
|
+
if (!active() || !route)
|
|
12
|
+
return fallback;
|
|
13
|
+
const p = progress();
|
|
14
|
+
const lerp = (key) => route.from[key] + (route.to[key] - route.from[key]) * p;
|
|
15
|
+
return { left: lerp('left'), top: lerp('top'), width: lerp('width'), height: lerp('height') };
|
|
16
|
+
}
|
|
17
|
+
function cancel() {
|
|
18
|
+
if (animation) {
|
|
19
|
+
animation.onfinish = null;
|
|
20
|
+
animation.cancel();
|
|
21
|
+
animation = undefined;
|
|
22
|
+
}
|
|
23
|
+
route = undefined;
|
|
24
|
+
button.dataset.flying = 'false';
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
get active() { return active(); },
|
|
28
|
+
// Read the previous viewport trajectory, not a box already displaced by
|
|
29
|
+
// the moving composer. Capturing must not cancel/restart an equal target.
|
|
30
|
+
capture: position,
|
|
31
|
+
target(measured) {
|
|
32
|
+
if (!active() || !route)
|
|
33
|
+
return measured;
|
|
34
|
+
const p = progress(), { from, to } = route;
|
|
35
|
+
const scale = (from.width / to.width) * (1 - p) + p;
|
|
36
|
+
const width = measured.width / scale, height = measured.height / scale;
|
|
37
|
+
const dx = (from.left + from.width / 2 - to.left - to.width / 2) * (1 - p);
|
|
38
|
+
const dy = (from.top + from.height / 2 - to.top - to.height / 2) * (1 - p);
|
|
39
|
+
return { left: measured.left - dx + (measured.width - width) / 2,
|
|
40
|
+
top: measured.top - dy + (measured.height - height) / 2, width, height };
|
|
41
|
+
},
|
|
42
|
+
move(from, to, reduced) {
|
|
43
|
+
if (!reduced && active() && route && same(route.to, to))
|
|
44
|
+
return;
|
|
45
|
+
const duration = active() && route ? Math.max(80, route.duration - Number(animation.currentTime ?? 0)) : 950;
|
|
46
|
+
cancel();
|
|
47
|
+
if (!from || reduced || typeof button.animate !== 'function' || !from.width || !to.width)
|
|
48
|
+
return;
|
|
49
|
+
const dx = from.left + from.width / 2 - to.left - to.width / 2;
|
|
50
|
+
const dy = from.top + from.height / 2 - to.top - to.height / 2;
|
|
51
|
+
if (Math.hypot(dx, dy) < 4 && Math.abs(from.width - to.width) < 1)
|
|
52
|
+
return;
|
|
53
|
+
route = { from: copyBox(from), to: copyBox(to), duration };
|
|
54
|
+
animation = button.animate([
|
|
55
|
+
{ transform: `translate(${dx}px, ${dy}px) scale(${from.width / to.width})` },
|
|
56
|
+
{ transform: 'translate(0px, 0px) scale(1)' },
|
|
57
|
+
], { duration, easing: 'cubic-bezier(.22,.75,.2,1)' });
|
|
58
|
+
button.dataset.flying = 'true';
|
|
59
|
+
const current = animation;
|
|
60
|
+
current.onfinish = () => { if (animation === current)
|
|
61
|
+
cancel(); };
|
|
62
|
+
},
|
|
63
|
+
cancel,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=flight.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Attraction measured from the stationary hit area, not the moving artwork. */
|
|
2
|
+
export declare function resolveHeixiuGaze(dx: number, dy: number): {
|
|
3
|
+
yaw: number;
|
|
4
|
+
pitch: number;
|
|
5
|
+
mix: number;
|
|
6
|
+
spin: number;
|
|
7
|
+
wander: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function resolveHeixiuFollow(dx: number, dy: number, compact: boolean): {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=follow.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Attraction measured from the stationary hit area, not the moving artwork. */
|
|
2
|
+
export function resolveHeixiuGaze(dx, dy) {
|
|
3
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy))
|
|
4
|
+
return { yaw: 0, pitch: -7, mix: 1, spin: 0, wander: .35 };
|
|
5
|
+
return { yaw: Math.max(-14, Math.min(14, dx / 16)),
|
|
6
|
+
// The old upper limit was -7 + 9 = 2 degrees: barely above straight ahead.
|
|
7
|
+
pitch: -7 - Math.max(-31, Math.min(9, dy / 16)), mix: 1, spin: 0, wander: 0 };
|
|
8
|
+
}
|
|
9
|
+
export function resolveHeixiuFollow(dx, dy, compact) {
|
|
10
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy))
|
|
11
|
+
return { x: 0, y: 0 };
|
|
12
|
+
const distance = Math.hypot(dx, dy), radius = compact ? 90 : 140;
|
|
13
|
+
if (distance >= radius)
|
|
14
|
+
return { x: 0, y: 0 };
|
|
15
|
+
const k = Math.max(0, Math.min(1, (distance - radius * .5) / (radius * .5)));
|
|
16
|
+
const fade = 1 - k * k * (3 - 2 * k);
|
|
17
|
+
const divisor = Math.max(compact ? 32 : 50, distance);
|
|
18
|
+
return {
|
|
19
|
+
x: dx / divisor * (compact ? 2 : 5) * fade,
|
|
20
|
+
y: dy / divisor * (compact ? 1.5 : 3.5) * fade,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=follow.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function resolveHeixiuDock(composerLeft: number, sidebarRight: number, viewportWidth: number): {
|
|
2
|
+
readonly mode: "outside" | "compact";
|
|
3
|
+
readonly size: 96 | 52;
|
|
4
|
+
};
|
|
5
|
+
export declare function resolveHeixiuWelcome(hero: {
|
|
6
|
+
left: number;
|
|
7
|
+
top: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}, composer: {
|
|
11
|
+
left: number;
|
|
12
|
+
top: number;
|
|
13
|
+
}, sidebarRight: number, viewportWidth: number): {
|
|
14
|
+
size: number;
|
|
15
|
+
left: number;
|
|
16
|
+
top: number;
|
|
17
|
+
} | undefined;
|
|
18
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function resolveHeixiuDock(composerLeft, sidebarRight, viewportWidth) {
|
|
2
|
+
const outside = viewportWidth > 760 && composerLeft - sidebarRight >= 114;
|
|
3
|
+
return { mode: outside ? 'outside' : 'compact', size: outside ? 96 : 52 };
|
|
4
|
+
}
|
|
5
|
+
export function resolveHeixiuWelcome(hero, composer, sidebarRight, viewportWidth) {
|
|
6
|
+
if (hero.width <= 0 || hero.height <= 0 || hero.top + hero.height > composer.top)
|
|
7
|
+
return undefined;
|
|
8
|
+
const size = viewportWidth <= 760 ? 52 : 96;
|
|
9
|
+
const left = Math.max(sidebarRight + 6, hero.left - size - (size === 52 ? 10 : 24));
|
|
10
|
+
return { size, left: left - composer.left, top: hero.top + hero.height / 2 - size / 2 - composer.top };
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=layout.js.map
|