@lemoncat7/dsh-theme-xiaohei 0.3.0-alpha.22 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +37 -17
  2. package/THIRD_PARTY_NOTICES.md +41 -0
  3. package/lib/appearance.js +26 -10
  4. package/lib/blink.js +3 -3
  5. package/lib/bloub-heixiu/ambient.d.ts +3 -0
  6. package/lib/bloub-heixiu/ambient.js +12 -0
  7. package/lib/bloub-heixiu/character.d.ts +33 -0
  8. package/lib/bloub-heixiu/character.js +124 -0
  9. package/lib/bloub-heixiu/effects.d.ts +4 -0
  10. package/lib/bloub-heixiu/effects.js +91 -0
  11. package/lib/bloub-heixiu/flight.d.ts +15 -0
  12. package/lib/bloub-heixiu/flight.js +66 -0
  13. package/lib/bloub-heixiu/follow.d.ts +13 -0
  14. package/lib/bloub-heixiu/follow.js +23 -0
  15. package/lib/bloub-heixiu/layout.d.ts +18 -0
  16. package/lib/bloub-heixiu/layout.js +12 -0
  17. package/lib/bloub-heixiu/renderer.d.ts +16 -0
  18. package/lib/bloub-heixiu/renderer.js +94 -0
  19. package/lib/bloub-heixiu/runtime.d.ts +6 -0
  20. package/lib/bloub-heixiu/runtime.js +395 -0
  21. package/lib/bloub-heixiu/signals.d.ts +17 -0
  22. package/lib/bloub-heixiu/signals.js +53 -0
  23. package/lib/bloub-heixiu/styles.d.ts +2 -0
  24. package/lib/bloub-heixiu/styles.js +98 -0
  25. package/lib/chrome/accessibility.d.ts +1 -1
  26. package/lib/chrome/accessibility.js +5 -13
  27. package/lib/chrome/composer-avatar.d.ts +10 -0
  28. package/lib/chrome/composer-avatar.js +599 -0
  29. package/lib/chrome/composer-send-heixiu.d.ts +3 -0
  30. package/lib/chrome/composer-send-heixiu.js +143 -0
  31. package/lib/chrome/composer.d.ts +2 -2
  32. package/lib/chrome/composer.js +84 -95
  33. package/lib/chrome/controls.d.ts +8 -0
  34. package/lib/chrome/controls.js +348 -0
  35. package/lib/chrome/conversation-messages.d.ts +1 -1
  36. package/lib/chrome/conversation-messages.js +207 -9
  37. package/lib/chrome/conversation.d.ts +1 -1
  38. package/lib/chrome/conversation.js +6 -0
  39. package/lib/chrome/frames.d.ts +1 -1
  40. package/lib/chrome/frames.js +14 -0
  41. package/lib/chrome/identity.js +4 -18
  42. package/lib/chrome/portal.d.ts +1 -1
  43. package/lib/chrome/portal.js +1 -1
  44. package/lib/chrome/sidebar.d.ts +1 -1
  45. package/lib/chrome/sidebar.js +118 -153
  46. package/lib/chrome/tokens.d.ts +1 -1
  47. package/lib/chrome/tokens.js +121 -68
  48. package/lib/chrome/workspace.d.ts +2 -2
  49. package/lib/chrome/workspace.js +35 -254
  50. package/lib/chrome.js +4 -0
  51. package/lib/client.d.ts +5 -12
  52. package/lib/client.js +5864 -4447
  53. package/lib/composer-avatar.d.ts +18 -0
  54. package/lib/composer-avatar.js +441 -0
  55. package/lib/composer-send-heixiu.d.ts +8 -0
  56. package/lib/composer-send-heixiu.js +133 -0
  57. package/lib/gaze.js +3 -3
  58. package/lib/generated-backgrounds.d.ts +14 -0
  59. package/lib/generated-backgrounds.js +14 -0
  60. package/lib/generated-identity.d.ts +1 -0
  61. package/lib/generated-identity.js +1 -0
  62. package/lib/generated-keyart.d.ts +18 -3
  63. package/lib/generated-keyart.js +18 -3
  64. package/lib/host-contract.d.ts +9 -0
  65. package/lib/host-contract.js +13 -0
  66. package/lib/host-dom.d.ts +11 -0
  67. package/lib/host-dom.js +60 -0
  68. package/lib/loading-heixiu.js +3 -3
  69. package/lib/metallic-brand-mark.d.ts +10 -0
  70. package/lib/metallic-brand-mark.js +309 -0
  71. package/lib/plugin.d.ts +1 -1
  72. package/lib/plugin.js +31 -16
  73. package/lib/portal.js +11 -10
  74. package/lib/reactions.js +3 -3
  75. package/lib/scene/avatar-model.d.ts +6 -0
  76. package/lib/scene/avatar-model.js +646 -0
  77. package/lib/scene/avatar-motion.d.ts +3 -0
  78. package/lib/scene/avatar-motion.js +799 -0
  79. package/lib/scene/character-idle.d.ts +17 -0
  80. package/lib/scene/character-idle.js +65 -0
  81. package/lib/scene/character-layout.d.ts +24 -0
  82. package/lib/scene/character-layout.js +51 -0
  83. package/lib/scene/character-poses.d.ts +26 -0
  84. package/lib/scene/character-poses.js +11 -0
  85. package/lib/scene/heixiu-feedback.d.ts +1 -1
  86. package/lib/scene/heixiu-feedback.js +0 -17
  87. package/lib/scene/pointer-bridge.d.ts +21 -0
  88. package/lib/scene/pointer-bridge.js +155 -0
  89. package/lib/scene/runtime.d.ts +12 -0
  90. package/lib/scene/runtime.js +87 -0
  91. package/lib/scene/styles.d.ts +7 -0
  92. package/lib/scene/styles.js +75 -0
  93. package/lib/scene/sylva-performance.d.ts +11 -0
  94. package/lib/scene/sylva-performance.js +58 -0
  95. package/lib/scene/wallpaper-character-styles.d.ts +3 -0
  96. package/lib/scene/wallpaper-character-styles.js +43 -0
  97. package/lib/scene/wallpaper-character.d.ts +4 -0
  98. package/lib/scene/wallpaper-character.js +195 -0
  99. package/lib/scene.d.ts +7 -13
  100. package/lib/scene.js +7 -1023
  101. package/lib/sidebar-glass.js +88 -35
  102. package/lib/sidebar-heixiu-roaming.js +5 -5
  103. package/lib/state.d.ts +14 -2
  104. package/lib/state.js +12 -4
  105. package/lib/theme.js +2 -2
  106. package/lib/vendor/bloub/decor.d.ts +114 -0
  107. package/lib/vendor/bloub/decor.js +198 -0
  108. package/lib/vendor/bloub/engine.d.ts +213 -0
  109. package/lib/vendor/bloub/engine.js +452 -0
  110. package/lib/vendor/bloub/expressions.d.ts +35 -0
  111. package/lib/vendor/bloub/expressions.js +143 -0
  112. package/lib/vendor/bloub/eyefit.d.ts +75 -0
  113. package/lib/vendor/bloub/eyefit.js +393 -0
  114. package/lib/vendor/bloub/face.d.ts +63 -0
  115. package/lib/vendor/bloub/face.js +106 -0
  116. package/lib/vendor/bloub/math.d.ts +22 -0
  117. package/lib/vendor/bloub/math.js +36 -0
  118. package/lib/vendor/bloub/profiles.d.ts +8 -0
  119. package/lib/vendor/bloub/profiles.js +20 -0
  120. package/lib/vendor/bloub/repere.d.ts +30 -0
  121. package/lib/vendor/bloub/repere.js +32 -0
  122. package/lib/vendor/bloub/shape.d.ts +80 -0
  123. package/lib/vendor/bloub/shape.js +268 -0
  124. package/lib/vendor/bloub/skins.d.ts +36 -0
  125. package/lib/vendor/bloub/skins.js +74 -0
  126. package/lib/vendor/bloub/states.d.ts +87 -0
  127. package/lib/vendor/bloub/states.js +468 -0
  128. package/package.json +23 -6
  129. package/lib/appearance.d.ts.map +0 -1
  130. package/lib/appearance.js.map +0 -1
  131. package/lib/blink.d.ts.map +0 -1
  132. package/lib/blink.js.map +0 -1
  133. package/lib/boot-appearance.d.ts.map +0 -1
  134. package/lib/boot-appearance.js.map +0 -1
  135. package/lib/boot-loader.d.ts.map +0 -1
  136. package/lib/boot-loader.js.map +0 -1
  137. package/lib/chrome/accessibility.d.ts.map +0 -1
  138. package/lib/chrome/accessibility.js.map +0 -1
  139. package/lib/chrome/blink.d.ts.map +0 -1
  140. package/lib/chrome/blink.js.map +0 -1
  141. package/lib/chrome/composer.d.ts.map +0 -1
  142. package/lib/chrome/composer.js.map +0 -1
  143. package/lib/chrome/conversation-messages.d.ts.map +0 -1
  144. package/lib/chrome/conversation-messages.js.map +0 -1
  145. package/lib/chrome/conversation.d.ts.map +0 -1
  146. package/lib/chrome/conversation.js.map +0 -1
  147. package/lib/chrome/frames.d.ts.map +0 -1
  148. package/lib/chrome/frames.js.map +0 -1
  149. package/lib/chrome/gaze.d.ts.map +0 -1
  150. package/lib/chrome/gaze.js.map +0 -1
  151. package/lib/chrome/identity.d.ts.map +0 -1
  152. package/lib/chrome/identity.js.map +0 -1
  153. package/lib/chrome/overlays.d.ts.map +0 -1
  154. package/lib/chrome/overlays.js.map +0 -1
  155. package/lib/chrome/portal.d.ts.map +0 -1
  156. package/lib/chrome/portal.js.map +0 -1
  157. package/lib/chrome/reactions.d.ts.map +0 -1
  158. package/lib/chrome/reactions.js.map +0 -1
  159. package/lib/chrome/sidebar.d.ts.map +0 -1
  160. package/lib/chrome/sidebar.js.map +0 -1
  161. package/lib/chrome/tokens.d.ts.map +0 -1
  162. package/lib/chrome/tokens.js.map +0 -1
  163. package/lib/chrome/workspace.d.ts.map +0 -1
  164. package/lib/chrome/workspace.js.map +0 -1
  165. package/lib/chrome.d.ts.map +0 -1
  166. package/lib/chrome.js.map +0 -1
  167. package/lib/client.d.ts.map +0 -1
  168. package/lib/client.js.map +0 -7
  169. package/lib/gaze.d.ts.map +0 -1
  170. package/lib/gaze.js.map +0 -1
  171. package/lib/generated-identity.d.ts.map +0 -1
  172. package/lib/generated-identity.js.map +0 -1
  173. package/lib/generated-keyart.d.ts.map +0 -1
  174. package/lib/generated-keyart.js.map +0 -1
  175. package/lib/generated-loading-assets.d.ts.map +0 -1
  176. package/lib/generated-loading-assets.js.map +0 -1
  177. package/lib/generated-sidebar-assets.d.ts +0 -4
  178. package/lib/generated-sidebar-assets.d.ts.map +0 -1
  179. package/lib/generated-sidebar-assets.js +0 -4
  180. package/lib/generated-sidebar-assets.js.map +0 -1
  181. package/lib/heixiu-interactions.d.ts.map +0 -1
  182. package/lib/heixiu-interactions.js.map +0 -1
  183. package/lib/index.d.ts.map +0 -1
  184. package/lib/index.js.map +0 -1
  185. package/lib/loading-heixiu.d.ts.map +0 -1
  186. package/lib/loading-heixiu.js.map +0 -1
  187. package/lib/plugin.d.ts.map +0 -1
  188. package/lib/plugin.js.map +0 -1
  189. package/lib/portal.d.ts.map +0 -1
  190. package/lib/portal.js.map +0 -1
  191. package/lib/reactions.d.ts.map +0 -1
  192. package/lib/reactions.js.map +0 -1
  193. package/lib/scene/heixiu-feedback.d.ts.map +0 -1
  194. package/lib/scene/heixiu-feedback.js.map +0 -1
  195. package/lib/scene/heixiu-interactions.d.ts.map +0 -1
  196. package/lib/scene/heixiu-interactions.js.map +0 -1
  197. package/lib/scene/state-transitions.d.ts.map +0 -1
  198. package/lib/scene/state-transitions.js.map +0 -1
  199. package/lib/scene.d.ts.map +0 -1
  200. package/lib/scene.js.map +0 -1
  201. package/lib/sidebar-glass.d.ts.map +0 -1
  202. package/lib/sidebar-glass.js.map +0 -1
  203. package/lib/sidebar-heixiu-escape.d.ts.map +0 -1
  204. package/lib/sidebar-heixiu-escape.js.map +0 -1
  205. package/lib/sidebar-heixiu-roaming.d.ts.map +0 -1
  206. package/lib/sidebar-heixiu-roaming.js.map +0 -1
  207. package/lib/state.d.ts.map +0 -1
  208. package/lib/state.js.map +0 -1
  209. package/lib/theme.d.ts.map +0 -1
  210. package/lib/theme.js.map +0 -1
package/README.md CHANGED
@@ -1,33 +1,43 @@
1
1
  # Xiaohei Night for DSH
2
2
 
3
- `Xiaohei Night` 是一个基于 DeepSeek Harness 官方主题注册接口开发的非官方粉丝主题插件。主题使用成对的二维动画背景建立人类城市与妖灵森林共存的场景,并以炭黑、雾灰、林地绿和青色妖灵力统一 DSH 的工作界面。
3
+ `Xiaohei Night` 是一个基于 DeepSeek Harness 官方主题注册接口开发的非官方粉丝主题插件。主题以低饱和天空、日夜光线和坐在小平台上的人形小黑表达陪伴感,保留原生工作台布局。
4
+
5
+ ## 兼容性
6
+
7
+ 正式版 `0.3.1` 针对 DeepSeek Harness `0.1.2-rc.1` 构建,需要 Node.js `22.19+` 或 `24+`。Conversation、Renderer、Sidebar、Slots 与 Theme 接口均对应 `0.1.2-rc.1`。本版包含侧栏玻璃收放同步和启动配色交接修复,发布为 npm `latest`。“跟随系统”读取当前设备/浏览器的配色偏好,不按服务器时间切换。
4
8
 
5
9
  ## 当前范围
6
10
 
7
11
  - 保留 DSH 原有布局、密度和交互层级。
8
12
  - 使用半透明墨黑表面,让背景可见但不压过对话内容。
9
13
  - 使用青绿色作为唯一交互强调色,错误、成功、警告保留独立语义色。
10
- - 背景与角色是独立位图图层,角色不使用 CSS 符号或几何块拼接。
11
- - 角色没有常驻漂浮、缩放或抖动;待机时使用对齐精灵帧完成自然眨眼和稀疏微反应。
12
- - 鼠标或嘿咻进入局部注视范围后,小黑会先注视再按方向轻动一次耳朵;纯待机 12–24 秒自然摆尾一次,回答完成后快速摆尾一次。
13
- - 使用 DSH 官方会话快照呈现七种状态:待机、聚能思考、流式回答、工具执行、等待用户、完成庆祝与错误提示。
14
- - 状态只跟随当前打开的会话;后台运行中的其他会话不会抢占角色,切换会话也不会误触发完成庆祝。
15
- - 所有角色位图随插件内联并预载,运行时只切换状态与完整帧精灵表,不发起图片请求;非待机状态始终优先于待机微反应。
16
- - 聚能、回答、工具、等待、完成和错误分别使用轻量局部光效,离开对应状态后动画立即停止。
17
- - 深浅模式使用同一林城交界构图的昼夜背景:远处城市、古树会馆、小型空间门和沿树根流动的妖灵力共用一套位置,不在小黑脚下叠加底圈或常驻漩涡动效。
18
- - 左侧栏直接共享全局水墨灵域,仅使用向主场景渐隐的半透明同色材质组织品牌区、主操作、工作区与底部工具,并以潦草竖写的生图“罗小黑”签名占据纵向留白,不新增无功能入口。
19
- - 工作区、工具区和会话输入区不使用额外铭牌,分别以猫尾灵珠、空间裂环、蓝嘴黑猫牌作为轻量识别点;工作区右侧额外内收,收起后所有工具回到同一条 35px 视觉轴。
14
+ - 背景与人物分层:保留日间/夜间的柔和天空;人物使用独立高清、无损 WebP,不随整张壁纸放大。坐姿、托腮和打盹统一为 720×1019,探头素材宽 360;最大显示尺寸仍有约 3 倍像素密度。姿势按需解码后再显示,保留轮廓与五官细节。
15
+ - 根据真实剩余空间决定姿势:右侧宽裕时坐在小平台上,中等窗口扶着侧栏探头;细侧栏空位使用更窄的半脸偷看。没有安全空间时暂时隐藏,不覆盖消息、头像或侧栏按钮,不修改业务布局。
16
+ - 坐姿下,用户空闲 30 秒后托腮、3 分钟后闭眼打盹;鼠标、键盘、触摸点击或滚轮操作后恢复。页面隐藏或不再使用坐姿时暂停计时,返回后重新开始。最多一个空闲计时器,睡着后不继续轮询,不绑定 AI 状态,不读取输入内容。姿势以 220ms 淡化过渡,不是连续骨骼动画。
17
+ - 装饰背景使用稳定的大视口高度;姿势仅做短暂 transform / opacity 入退场,不横穿会话、不做骨骼扭曲。不读取会话内容或执行状态,无持续渲染循环;流式文本高度变化不触发布局重测。软键盘兼容性仍需 iOS / Android 真机验收。
18
+ - bloub 嘿咻保留 alpha.171 略宽于高的小黑团轮廓和圆润奶白眼圈。单击依次眨眼回应、轻跳、歪头、感叹号惊讶;双击或键盘 Shift+Enter 轮换散开重聚、蛋形、环绕、六边形、单眼眨眼、睁大眼、感叹号、警觉、休眠、思考、通知。每隔随机 20–40 秒自动选择一种待机动作(含眨眼、休眠,不含思考、通知、感叹号),不连续重复;打字、飞行或其他动作尚未结束时顺延。后台/离屏/减少动态时取消调度,恢复后重新等待,不补播。
19
+ - 嘿咻通过可选的官方 `sessions.list` / `binding().session` 只读订阅当前会话:运行中显示三点思考(代表本轮处理中,不细分推理/输出/工具),观察到本轮结束时提醒一次,新执行错误显示感叹号。提醒表示本轮结束,不承诺任务成功,手动停止也可能触发。初次加载历史、切换会话、重新加载历史不会补发完成提醒。通知徽点采用暖金色和真正透明的缺口;不申请浏览器通知权限、不发送消息、不操作会话、不关联壁纸。
20
+ - 新会话将同一只嘿咻停在输入框上方、Logo 左边;输入区落到底部时,用可打断的约 950 ms 飞行动画转移到输入框左侧。手机给 Logo 左侧留出空间并缩小角色;普通窗口缩放不触发跨区域飞行。仅使用现有 Hero 品牌元素和输入框位置,不读取或改变会话业务数据。
21
+ - 慢浮动周期 14 秒,宽屏横向 ±7 px、纵向 ±10 px;窄屏自动减小幅度。视线跟随窗口内鼠标,身体只在近处轻微靠近(宽屏最多 5 / 3.5 px),约 480 ms 柔和回位。CSS transform 合成浮动和跟随,不增加持续 JS 帧计算或逐帧布局测量;除短暂换位飞行外,点击区域保持固定。减少动态模式关闭浮动、跟随、飞行和自动动作;不改发送键。
22
+ - 嘿咻初始只有 10 个 SVG 节点;首次触发特殊动作才一次性建立粒子/轨道池及通知缺口,之后固定复用(总计 102 节点),帧循环不新增节点、不测量布局,结束后隐藏特效。小尺寸变形中的眼圈适当收窄,避免相互粘连。空闲间歇唤醒,离屏、隐藏和减少动态时暂停,卸载释放监听、观察器、帧请求和定时器。宽屏使用框外空位,窄屏使用框内左侧预留位,不改发送键。原项目 MIT 许可与固定源码版本见 `THIRD_PARTY_NOTICES.md`;观察及改动记录见 `docs/heixiu-motion-reference.md`。
23
+ - 对话双方保留静态身份头像;消息气泡使用更舒适的横向留白。
24
+ - 左侧栏只有一层独立磨砂材质,目录与会话不再各自模糊。展开目录、Hover、选中会话使用逐级增强的中性底色;移除拼接空间框和底部卡片,原生滚动、拖拽和菜单保持不变。
25
+ - 新会话使用统一圆角控件;发送键嘿咻继续保留完整帧眨眼。左栏行宽、缩进、滚动条预留和收起布局交给 DSH 管理,避免主题强行计算宽度。
20
26
  - 支持减少动态、增强对比度、强制颜色和打印模式降级。
21
- - 插件加载后自动启用 `xiaohei-night`,卸载后由 DSH 主题服务恢复系统主题。
22
-
23
- 状态优先级为:错误 → 等待用户 → 工具执行 → 流式回答 → 聚能思考 → 完成 → 待机。完成态仅在同一个当前会话结束后显示约 1.6 秒。
27
+ - 主题通过 DSH Theme Runtime 覆盖 Light / Dark / System 的语义 token,不替换用户的主题偏好;卸载后由 DSH 恢复原始 token。
24
28
 
25
29
  ## 安装
26
30
 
27
- [GitHub Releases](https://github.com/lemoncat7/dsh-theme-xiaohei/releases) 下载 `0.3.0-alpha.15` 的完整构建包后安装:
31
+ 通过 npm 安装正式版:
28
32
 
29
33
  ```bash
30
- dsh plugin --profile web add ./lemoncat7-dsh-theme-xiaohei-0.3.0-alpha.15.tgz
34
+ dsh plugin --profile web add @lemoncat7/dsh-theme-xiaohei@0.3.1
35
+ ```
36
+
37
+ 也可从 [GitHub Releases](https://github.com/lemoncat7/dsh-theme-xiaohei/releases/tag/v0.3.1) 下载完整包后安装:
38
+
39
+ ```bash
40
+ dsh plugin --profile web add ./lemoncat7-dsh-theme-xiaohei-0.3.1.tgz
31
41
  ```
32
42
 
33
43
  卸载:
@@ -52,6 +62,16 @@ npm run pack:check
52
62
  npm run pack:docker
53
63
  ```
54
64
 
65
+ ### 代码边界
66
+
67
+ - `src/index.ts` 只负责 Host 首帧加载装饰,保持自包含并在客户端插件启动前运行。
68
+ - `src/plugin.ts` 通过 DSH 生命周期装配主题 token、Chrome、场景与交互行为。
69
+ - `src/chrome/` 管理控件和公共插件表面;`src/scene/` 分离场景样式与运行时 DOM。
70
+ - `src/host-contract.ts` 集中维护宿主插槽选择器;`src/host-dom.ts` 为运行期模块共享一个批处理 DOM 观察器。左栏只在尺寸或节点变化时测量布局,流式文本更新不重复强制布局。
71
+ - `src/scene/character-layout.ts` 只计算安全位置,`character-idle.ts` 只管理空闲期限,`character-poses.ts` 统一素材目录;`wallpaper-character.ts` 负责生命周期、按需解码和呈现,卸载时释放监听与计时器。
72
+ - `src/vendor/bloub/` 保留上游动画计算,`src/bloub-heixiu/` 分离 SVG 呈现、布局、样式和生命周期。`signals.ts` 可选地只读订阅当前会话执行状态,不读取会话内容,不操作会话。
73
+ - 旧树洞/落叶挂载器已移除,历史图片保留供版本回溯;背景、材质和业务状态各自独立。
74
+
55
75
  ## 扩展工具接入主题
56
76
 
57
77
  SSH、伙伴、Git 等后续工具不需要复制主题 CSS。插件根节点使用公开的框架属性即可继承小黑主题:
@@ -77,7 +97,7 @@ SSH、伙伴、Git 等后续工具不需要复制主题 CSS。插件根节点使
77
97
 
78
98
  ## 视觉资产
79
99
 
80
- 主题主视觉使用 `gpt-image-2` 通过标准 Images API 生成,并压缩为 WebP 后以内联资源打入浏览器插件。深浅两张背景共享连续的水墨山林构图:左侧和中央保持低对比度以承载导航与对话,青色灵流贯穿墨域,右下角以较亮雾池和冷色背光托起独立角色图层;背景本身不含人物、动物、文字或水印。待机耳朵和尾巴动作从正式待机帧通过平滑局部形变离线生成,每一格都是完整角色画面,运行时不切割耳朵或尾巴。
100
+ 背景使用内置的日夜天空 WebP、CSS 渐变及按需加载的人物透明素材,不加载 Three.js、外部 CDN 或角色模型。品牌头像、消息静态头像、加载动画与发送键嘿咻作为内联资源随插件交付;嘿咻交互使用 SVG。
81
101
 
82
102
  ## License
83
103
 
@@ -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
- const bootAppearance = doc.documentElement.getAttribute(XIAOHEI_BOOT_APPEARANCE_ATTRIBUTE);
9
- if (bootAppearance === 'light' || bootAppearance === 'dark') {
10
- doc.documentElement.setAttribute(XIAOHEI_APPEARANCE_ATTRIBUTE, bootAppearance);
11
- if (snapshot.active.colorScheme !== bootAppearance)
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
- sync(ctx.theme.getTheme());
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 mountObserver = new win.MutationObserver(attach);
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
- mountObserver.disconnect();
101
+ unsubscribeHostDom();
102
102
  stateObserver.disconnect();
103
103
  doc.removeEventListener('visibilitychange', onEnvironmentChange);
104
104
  doc.removeEventListener(XIAOHEI_HEIXIU_GREETING_EVENT, onHeixiuGreeting);
@@ -0,0 +1,3 @@
1
+ export declare function heixiuAmbientDelay(random?: () => number): number;
2
+ export declare function heixiuAmbientIndex(count: number, previous: number, random?: () => number): number;
3
+ //# sourceMappingURL=ambient.d.ts.map
@@ -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