dsh-whale-particles 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-whale-particles contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # dsh-whale-particles
2
+
3
+ [![npm version](https://img.shields.io/npm/v/dsh-whale-particles)](https://www.npmjs.com/package/dsh-whale-particles)
4
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-whale-particles)](https://www.npmjs.com/package/dsh-whale-particles)
5
+ [![license](https://img.shields.io/npm/l/dsh-whale-particles)](LICENSE)
6
+
7
+ 把“点阵鲸鱼/照片点阵”作为 DSH Web 界面背景的浏览器端插件。
8
+
9
+ ![dsh-whale-particles 效果示例](assets/whale-preview.png)
10
+
11
+ > 上图为界面示例(点阵主体 + 界面透明化后的观感)。
12
+
13
+ - 每颗粒子都有自己**独立、低频、平滑**的“外力”(非统一流场、非逐帧白噪声),像官网鲸鱼一样:每颗各自动作柔和、彼此不齐;
14
+ - **手动模式**:光标靠近时圈内各点柔和躁动,移开即平滑归位;
15
+ - **自动模式(屏保)**:“波”沿 **360° 任意方向**扫过主体(自动避开与上一道过近的方向,斜向按系数减速)+ “全屏呼吸”(缓缓起、维持、缓缓落),间隔默认 0(波连着波);
16
+ - **主体可切换**:
17
+ - 🐋 鲸鱼(默认剪影);
18
+ - 🖼 照片:导入**透明 PNG**,引擎按 alpha 取主体、内部压成几 KB 小蒙版,仅存本地(localStorage),不上传;
19
+ - **参数面板**(设置 → 插件 → 「鲸鱼点阵背景」标签页,直接铺开、无需展开):按「鼠标扰动 / 自动模式 / 外观 / 主体来源 / 轮廓 / 模式」六组排列;手动·自动、鲸鱼·照片都是高亮分段控件(当前选中项一眼可见);全部即时生效并自动保存,底部带「恢复默认」与版本号;
20
+ - **主界面不占位**:没有右下角悬浮条,参数与手动/自动切换都在设置页里;
21
+ - 附带调整:**空会话(hero)输入栏整体下移**,避免盖住主体;有对话后自动恢复贴底布局;鲸鱼中心自动跟随左侧栏宽窄(内容区居中,平滑补间)。
22
+
23
+ ## 安装
24
+
25
+ **方式一:npm(推荐,之后可在插件市场里一键更新)**
26
+
27
+ ```sh
28
+ dsh plugin --profile desktop add dsh-whale-particles
29
+ ```
30
+
31
+ 装好后 profile 会把它记成依赖,`npm` 上发了新版本时,DSH 插件市场
32
+ (设置 → 插件)就能检测到并一键更新。
33
+
34
+ **方式二:本地目录**
35
+
36
+ ```sh
37
+ dsh plugin --profile desktop add ./dsh-whale-particles
38
+ ```
39
+
40
+ **方式三:离线包(免命令行)**
41
+
42
+ 见 [`release/`](release/) 目录:解压 `dsh-whale-particles-offline-*.zip` 后双击
43
+ `install-whale.bat`,脚本会自动备份 profile 的 `package.json`、拷入插件并注册 bundle。
44
+
45
+ **不借助 dsh CLI 手动安装**(与离线包内部做法相同):
46
+ 1. 把 `dsh-whale-particles` 文件夹复制到
47
+ `%USERPROFILE%\.dsh\profiles\<profile>\node_modules\dsh-whale-particles`;
48
+ 2. 在 `…\profiles\<profile>\package.json` 的 `dsh.profile.bundles` 里追加
49
+ `"dsh-whale-particles"`(改前先备份);
50
+ 3. 重启 DSH Desktop。
51
+
52
+ > 注意:手动拷进去的副本不会出现在插件市场的"已安装"列表里,也就没有一键更新;
53
+ > 想走市场更新请用方式一。
54
+
55
+ profile 名默认 `desktop`(桌面版);若你的 profile 不叫这个,换成实际的即可。
56
+ 安装/更新后**需要重启 DSH Desktop**(客户端 bundle 在启动时装载)。
57
+
58
+ ## 版本适配(重要)
59
+
60
+ 插件的“界面透明化”与“空会话输入栏下移”依赖 DSH Web 界面里 **CSS Module 哈希类名**;
61
+ 不同 DSH/Desktop 版本这些前缀会变。**点阵引擎本身不依赖界面类名,任何版本都能渲染**,
62
+ 变了的只是“让界面透明、露出背景”那几条规则和 composer 下移规则。
63
+
64
+ 需要调整时,编辑 `lib/client.js` 中 `STYLE` 常量开头几条与
65
+ `measureContentCenter()` 里的选择器,把哈希前缀换成本机实际类名:
66
+
67
+ | 作用 | 选择器(0.1.5-rc.1 / Desktop v2.0.9,与 0.1.2-rc.1 / v2.0.5 相同) |
68
+ | --- | --- |
69
+ | 主框架透明 | `._1qAH1q_frame` |
70
+ | 会话/根容器透明 | `.uPhUma_root` |
71
+ | 侧栏半透明 | `._1qAH1q_sidebarCol` |
72
+ | 内容区中心测量 | `._1qAH1q_sidebarCol`、`[data-slot="sidebar"]` |
73
+ | 空会话输入栏下移 | `.uPhUma_root[data-phase=hero] .uPhUma_composerSeat` |
74
+
75
+ 实测:**DSH Desktop v2.0.5**(web 前端 0.1.2-rc.1)的哈希前缀是 `_1qAH1q`
76
+ (主框架/侧栏,来自 `@deepseek-ai/dsh-client-ui-layout`)与 `uPhUma`
77
+ (会话/根容器/输入栏,来自 `@deepseek-ai/dsh-client-ui-conversation`);旧的
78
+ v2.0.3(0.1.1-rc.2)使用 `pI_x6G`、`wSkVaW`、`ydkMvW`。换版本后按上述办法
79
+ 重新取值(DevTools 选中元素,取 `_frame`、`_sidebarCol`、`_composerSeat`
80
+ 对应项的新前缀;全局替换这两处前缀即可)。改动后重启生效。
81
+
82
+ ## 卸载 / 临时隐藏
83
+
84
+ ```sh
85
+ dsh plugin --profile desktop remove dsh-whale-particles
86
+ ```
87
+
88
+ 或 Settings → 插件里停用;临时隐藏可在 DevTools 执行:
89
+
90
+ ```js
91
+ document.body.removeAttribute('data-dsh-whale');
92
+ document.getElementById('dsh-whale-particles-layer')?.remove();
93
+ ```
94
+
95
+ ## 说明
96
+
97
+ - 默认鲸鱼轮廓路径内联自 dsh-homepage-skin(MIT,来源见该仓库 README),保留为默认主体;
98
+ 如需商用/换形,替换 `lib/client.js` 里的 `WHALE_PATH`(Path2D 数据)即可,照片模式不受影响。
99
+ - 纯浏览器插件:宿主半区 `lib/index.js` 为空实现;构建产物 `lib/client.js` 为
100
+ `window.__ModuleLoader__.load` 格式,免二次构建。
101
+ - 参数与照片蒙版保存在**浏览器 localStorage**(键 `dsh-whale:cfg` / `dsh-whale:auto` /
102
+ `dsh-whale:photo`),每台机器各自保存,不随插件文件迁移。
103
+
104
+ ## 仓库结构
105
+
106
+ ```text
107
+ package.json dsh.bundle.patch(bundle 资格)+ dsh.client(Web 客户端半边)
108
+ cordis.patch.yml 把本插件 insert 进 profile 的层栈
109
+ lib/index.js 宿主半边(空实现,纯浏览器插件)
110
+ lib/client.js 全部实现:点阵引擎 + 界面透明化 CSS + 参数面板(免构建)
111
+ assets/ README 用示例图
112
+ release/ 发行物:最新 tgz、离线安装 zip、一键安装/卸载脚本
113
+ ```
Binary file
@@ -0,0 +1,4 @@
1
+ # dsh-whale-particles bundle patch: inserts this plugin into a profile's layer stack.
2
+ - insert:
3
+ - id: whale-particles
4
+ name: dsh-whale-particles
package/lib/client.js ADDED
@@ -0,0 +1,1013 @@
1
+ /* dsh-whale-particles — browser half.
2
+ * Compiled-for-the-loader bundle: window.__ModuleLoader__.load({id, factory}).
3
+ * No ESM import/export statements allowed in this file.
4
+ *
5
+ * Engine = whale-dots.html v11 (validated by the author):
6
+ * - every dot has its own low-frequency smooth force (not a shared field,
7
+ * not per-frame white noise);
8
+ * - manual mode: disturbance radiates from the cursor;
9
+ * - auto (screensaver) mode: waves sweeping the whale artwork from any
10
+ * 360-degree angle + full-screen breathing, no repeated neighbour.
11
+ */
12
+ window.__ModuleLoader__.load({
13
+ id: "dsh-whale-particles",
14
+ factory: (require) => {
15
+ "use strict";
16
+ var module = { exports: {} };
17
+ var exports = module.exports;
18
+
19
+ /* ------------------------------------------------------------------
20
+ * 版本号:优先读运行时加载的 package.json(每次发版自动跟上),
21
+ * 读不到时回退到下面的常量 —— 发版时请与 package.json 一起改。
22
+ * ------------------------------------------------------------------ */
23
+ var VERSION_FALLBACK = "0.3.0";
24
+ function resolveVersion() {
25
+ try {
26
+ var pkg = require("dsh-whale-particles/package.json");
27
+ if (pkg && pkg.version) return String(pkg.version);
28
+ } catch (e) { /* 宿主模块解析未开放该子路径 */ }
29
+ return VERSION_FALLBACK;
30
+ }
31
+ var VERSION = resolveVersion();
32
+
33
+ /* ------------------------------------------------------------------
34
+ * 鲸鱼剪影路径(内联自 DeepSeek Harness hero 素材,随 dsh-homepage-skin
35
+ * 以 MIT 授权分发;发布前可替换为自绘/合规轮廓)。
36
+ * ------------------------------------------------------------------ */
37
+ var WHALE_PATH = "M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018ZM11.1749 14.4736C9.15936 12.889 8.18184 12.3675 7.77832 12.39C7.40081 12.4125 7.46881 12.8445 7.55182 13.126C7.63882 13.404 7.75182 13.5955 7.91033 13.8396C8.01983 14.0011 8.09533 14.2411 7.80083 14.4216C7.15181 14.8231 6.02327 14.2866 5.97027 14.2601C4.65673 13.4865 3.5587 12.4655 2.78467 11.069C2.03715 9.72493 1.60314 8.28289 1.53164 6.74384C1.51264 6.37233 1.62214 6.24082 1.99215 6.17332C2.47916 6.08332 2.98118 6.06432 3.46769 6.13582C5.52476 6.43633 7.27581 7.35586 8.74385 8.8129C9.58188 9.64243 10.2159 10.634 10.8689 11.6025C11.5634 12.631 12.3105 13.611 13.262 14.4146C13.598 14.6961 13.866 14.9101 14.1225 15.0681C13.349 15.1546 12.058 15.1731 11.1749 14.4746V14.4736ZM12.141 8.25988C12.141 8.09488 12.273 7.96338 12.439 7.96338C12.4765 7.96338 12.5105 7.97088 12.541 7.98188C12.5825 7.99688 12.6205 8.01938 12.6505 8.05338C12.7035 8.10588 12.7335 8.18088 12.7335 8.25988C12.7335 8.42489 12.6015 8.55639 12.4355 8.55639C12.2695 8.55639 12.141 8.42489 12.141 8.25988ZM15.1415 9.79893C14.949 9.87793 14.7565 9.94544 14.5715 9.95294C14.2845 9.96794 13.9715 9.85143 13.8015 9.70893C13.5375 9.48742 13.3485 9.36342 13.2695 8.97691C13.2355 8.8119 13.2545 8.55639 13.2845 8.40989C13.3525 8.09438 13.277 7.89187 13.0545 7.70787C12.8735 7.55786 12.643 7.51636 12.39 7.51636C12.2955 7.51636 12.209 7.47486 12.1445 7.44136C12.039 7.38886 11.9519 7.25735 12.035 7.09585C12.0615 7.04335 12.19 6.91584 12.22 6.89334C12.5635 6.69784 12.9595 6.76184 13.326 6.90834C13.6655 7.04735 13.9225 7.30236 14.292 7.66287C14.6695 8.09838 14.7375 8.21838 14.9525 8.54539C15.1225 8.8009 15.277 9.06341 15.3831 9.36392C15.4471 9.55142 15.3641 9.70493 15.1415 9.79893Z";
38
+
39
+ /* ---------------- 配置(对应 HTML 原型里作者确认的最终参数) ------------- */
40
+ var CFG = { repelR: 200, // 扰动半径 px
41
+ repelA: 6500, // 径向推斥(净空)
42
+ agiA: 18000, // 每粒自有力强度
43
+ springS: 140, // 回位刚度
44
+ damping: 17, // 阻尼
45
+ density: 4000, // 鲸鱼点数(目标)
46
+ whaleScale: 0.4, // 鲸鱼宽度/视口
47
+ baseY: 0.44,
48
+ waveSpeed: 100,
49
+ waveHalf: 100,
50
+ waveDiag: 0.7,
51
+ autoGap: 0,
52
+ dirRandom: true,
53
+ gridOn: true,
54
+ outlineOnly: false,
55
+ shape: "square",
56
+ subject: "whale", // "whale" 鲸鱼 | "photo" 照片(透明 PNG alpha 蒙版)
57
+ photoAlpha: 32, // 照片 alpha 阈值 0..255(低于=透明)
58
+ photoInvert: false,
59
+ };
60
+
61
+ // 出厂参数快照(“恢复默认值”用;须在读取 localStorage 之前生成)
62
+ var CFG_DEFAULT = JSON.parse(JSON.stringify(CFG));
63
+
64
+ // 载入用户保存的参数(localStorage),缺失项用上面的默认值
65
+ var CFG_KEYS = ["repelR", "repelA", "agiA", "springS", "damping", "density",
66
+ "whaleScale", "baseY", "waveSpeed", "waveHalf", "waveDiag", "autoGap",
67
+ "dirRandom", "gridOn", "outlineOnly", "shape",
68
+ "subject", "photoAlpha", "photoInvert"];
69
+ try {
70
+ var savedCfg = JSON.parse(localStorage.getItem("dsh-whale:cfg") || "{}");
71
+ if (savedCfg && typeof savedCfg === "object") {
72
+ for (var kk in savedCfg) {
73
+ if (CFG_KEYS.indexOf(kk) >= 0 && typeof savedCfg[kk] === typeof CFG[kk]) CFG[kk] = savedCfg[kk];
74
+ }
75
+ }
76
+ } catch (e) {}
77
+
78
+ /* --------------------- 注入样式(门控于 body 属性) --------------------- */
79
+ var STYLE = "body[data-dsh-whale]{background:transparent}" +
80
+ "html:has(body[data-dsh-whale][data-ds-dark-theme]){background-color:#06090f}" +
81
+ "html:has(body[data-dsh-whale]:not([data-ds-dark-theme])){background-color:#f7f6f3}" +
82
+ "body[data-dsh-whale] ._1qAH1q_frame{background:transparent;isolation:isolate}" +
83
+ "body[data-dsh-whale] .uPhUma_root{background:transparent}" +
84
+ "body[data-dsh-whale][data-ds-dark-theme] ._1qAH1q_sidebarCol{background:rgba(9,19,32,.55)}" +
85
+ "body[data-dsh-whale]:not([data-ds-dark-theme]) ._1qAH1q_sidebarCol{background:rgba(255,255,255,.5)}" +
86
+ /* 空会话(hero):输入栏整体下移,别挡住鲸鱼;有对话(active)时自然失效 */
87
+ "body[data-dsh-whale] .uPhUma_root[data-phase=hero] .uPhUma_composerSeat{margin-top:clamp(380px,56vh,860px)}" +
88
+ "#dsh-whale-particles-layer{position:fixed;inset:0;z-index:-1;pointer-events:none}" +
89
+ "#dsh-whale-particles-layer canvas{width:100%;height:100%;display:block}" +
90
+ "#dsh-whale-panel{position:fixed;top:54px;right:14px;z-index:9999;width:262px;max-height:calc(100vh - 90px);overflow:auto;" +
91
+ "background:rgba(15,23,38,.94);color:#dbe7ff;border:1px solid rgba(255,255,255,.16);border-radius:12px;" +
92
+ "padding:10px 12px 12px;font:12px/1.5 system-ui,sans-serif;box-shadow:0 12px 34px rgba(0,0,0,.38)}" +
93
+ "#dsh-whale-panel h3{margin:0 0 2px;font-size:12px;font-weight:600}" +
94
+ "#dsh-whale-panel .hint3{opacity:.6;margin-bottom:4px}" +
95
+ "#dsh-whale-panel label{display:flex;align-items:center;gap:8px;margin-top:6px}" +
96
+ "#dsh-whale-panel label span{width:96px;flex:none;opacity:.85}" +
97
+ "#dsh-whale-panel input[type=range]{flex:1;min-width:0;accent-color:#4d7cfe}" +
98
+ "#dsh-whale-panel input[type=checkbox]{accent-color:#4d7cfe}" +
99
+ "#dsh-whale-panel .val{width:34px;flex:none;text-align:right;opacity:.75;font-variant-numeric:tabular-nums}" +
100
+ "#dsh-whale-panel .row{display:flex;gap:8px;margin-top:10px}" +
101
+ "#dsh-whale-panel button{flex:1;padding:5px 0;border-radius:8px;border:1px solid rgba(255,255,255,.18);" +
102
+ "background:rgba(255,255,255,.07);color:inherit;cursor:pointer;font:inherit}" +
103
+ "#dsh-whale-panel button.on{background:rgba(77,124,254,.35);border-color:rgba(130,165,255,.85)}";
104
+
105
+ var DARK = {
106
+ grid: "rgba(150,180,235,", gridA: 0.10,
107
+ dot: [148, 190, 255], dotA: 0.95,
108
+ };
109
+ var LIGHT = {
110
+ grid: "rgba(70,95,150,", gridA: 0.16,
111
+ dot: [40, 68, 130], dotA: 0.9,
112
+ };
113
+
114
+ /* ---------------------------- 状态/工具 ---------------------------- */
115
+ var W = 0, H = 0, dpr = 1;
116
+ var contentCX = 0; // 鲸鱼水平中心(内容区中心,自动跟随左侧栏宽度)
117
+ var centerTarget = 0; // 平滑补间的目标中心
118
+ var fish = { pts: [], home: [] };
119
+ var gridPts = [];
120
+ var mouse = { x: -1e4, y: -1e4, active: false };
121
+ var raf = 0, last = 0, running = true;
122
+ var theme = document.body && document.body.hasAttribute("data-ds-dark-theme") ? DARK : LIGHT;
123
+ var auto = { manual: true, item: null, idleAt: 0, lastKind: null, lastAngle: null };
124
+ var disposed = [];
125
+
126
+ var smoothstep = (a, b, x) => { var t = Math.max(0, Math.min(1, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
127
+ var easeInOut = (p) => (p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2);
128
+ var frac = (v) => v - Math.floor(v);
129
+
130
+ var path = null, bbox = null, testCtx = null;
131
+ var photoA = null, photoUrl = null, photoErr = null, photoLoading = false;
132
+
133
+ // ---------- 照片(透明 PNG alpha 蒙版)支持 ----------
134
+ // 统一坐标系:照片宽度 = 20 单位(与鲸鱼路径同一量级,whaleScale 按 bbox 宽度缩放)
135
+ function alphaAt(x, y) {
136
+ if (!photoA) return 0;
137
+ var s = 20 / (photoA.w - 1 || 1);
138
+ var px = Math.min(photoA.w - 1, Math.max(0, Math.round(x / s)));
139
+ var py = Math.min(photoA.h - 1, Math.max(0, Math.round(y / s)));
140
+ return photoA.data[(py * photoA.w + px) * 4 + 3];
141
+ }
142
+ function subjectHit(x, y) {
143
+ if (CFG.subject === "photo" && photoA) {
144
+ var a = alphaAt(x, y);
145
+ return CFG.photoInvert ? a < CFG.photoAlpha : a >= CFG.photoAlpha;
146
+ }
147
+ return !!path && !!testCtx && testCtx.isPointInPath(path, x, y);
148
+ }
149
+ function photoBBoxUnits() {
150
+ var minPx = Infinity, minPy = Infinity, maxPx = -1, maxPy = -1;
151
+ for (var py = 0; py < photoA.h; py++) {
152
+ for (var px = 0; px < photoA.w; px++) {
153
+ var a = photoA.data[(py * photoA.w + px) * 4 + 3];
154
+ var on = CFG.photoInvert ? a < CFG.photoAlpha : a >= CFG.photoAlpha;
155
+ if (on) {
156
+ if (px < minPx) minPx = px;
157
+ if (px > maxPx) maxPx = px;
158
+ if (py < minPy) minPy = py;
159
+ if (py > maxPy) maxPy = py;
160
+ }
161
+ }
162
+ }
163
+ if (minPx === Infinity) return null;
164
+ var s = 20 / (photoA.w - 1 || 1);
165
+ return { minX: minPx * s, minY: minPy * s, maxX: maxPx * s, maxY: maxPy * s };
166
+ }
167
+ function decodePhoto(img) {
168
+ var iw = img.naturalWidth || img.width, ih = img.naturalHeight || img.height;
169
+ var sc = Math.min(1, 160 / Math.max(iw, ih));
170
+ var w = Math.max(1, Math.round(iw * sc)), h = Math.max(1, Math.round(ih * sc));
171
+ var cv = document.createElement("canvas");
172
+ cv.width = w; cv.height = h;
173
+ var c2 = null;
174
+ try { c2 = cv.getContext("2d", { willReadFrequently: true }); } catch (e) {}
175
+ if (!c2) c2 = cv.getContext("2d");
176
+ c2.drawImage(img, 0, 0, w, h);
177
+ var id = c2.getImageData(0, 0, w, h);
178
+ photoA = { w: w, h: h, data: new Uint8ClampedArray(id.data.buffer) };
179
+ photoErr = null;
180
+ // 存一个小蒙版 PNG(几 KB)供下次启动用,不保存原图
181
+ try {
182
+ var small = cv.toDataURL("image/png");
183
+ if (small && small.length < 400000) {
184
+ photoUrl = small;
185
+ localStorage.setItem("dsh-whale:photo", small);
186
+ }
187
+ } catch (e) {}
188
+ buildPath();
189
+ resampleWhale();
190
+ }
191
+ function ensurePhotoLoad() {
192
+ if (CFG.subject !== "photo" || photoA || photoLoading) return;
193
+ if (photoUrl === null) { try { photoUrl = localStorage.getItem("dsh-whale:photo") || null; } catch (e) {} }
194
+ if (!photoUrl) return;
195
+ photoLoading = true;
196
+ var img = new Image();
197
+ img.onload = function () { photoLoading = false; try { decodePhoto(img); } catch (e) {} };
198
+ img.onerror = function () { photoLoading = false; photoErr = "蒙版读取失败"; };
199
+ img.src = photoUrl;
200
+ }
201
+ function importPhoto(file) {
202
+ var rd = new FileReader();
203
+ rd.onload = function () {
204
+ try {
205
+ var url = String(rd.result);
206
+ CFG.subject = "photo";
207
+ persistCfg();
208
+ photoUrl = url;
209
+ var img = new Image();
210
+ img.onload = function () { try { decodePhoto(img); } catch (e) { photoErr = "处理失败"; } };
211
+ img.onerror = function () { photoErr = "图片读取失败"; };
212
+ img.src = url;
213
+ } catch (e) { photoErr = "无法读取该文件"; }
214
+ };
215
+ rd.readAsDataURL(file);
216
+ }
217
+
218
+ function buildPath() {
219
+ if (CFG.subject === "photo") {
220
+ if (photoA) { path = null; testCtx = null; bbox = photoBBoxUnits(); return; }
221
+ return; // 照片尚未就绪:保留当前画面(鲸鱼),解码完成后会重建
222
+ }
223
+ try { path = new Path2D(WHALE_PATH); } catch (e) { path = null; }
224
+ if (!path) return;
225
+ testCtx = document.createElement("canvas").getContext("2d");
226
+ var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
227
+ for (var gy = 0; gy <= 190; gy++) {
228
+ for (var gx = 0; gx <= 250; gx++) {
229
+ if (testCtx.isPointInPath(path, gx / 10, gy / 10)) {
230
+ if (gx / 10 < minX) minX = gx / 10;
231
+ if (gx / 10 > maxX) maxX = gx / 10;
232
+ if (gy / 10 < minY) minY = gy / 10;
233
+ if (gy / 10 > maxY) maxY = gy / 10;
234
+ }
235
+ }
236
+ }
237
+ bbox = (minX === Infinity) ? null : { minX: minX, minY: minY, maxX: maxX, maxY: maxY };
238
+ }
239
+
240
+ function resampleWhale() {
241
+ var usePhoto = CFG.subject === "photo" && !!photoA;
242
+ if (!usePhoto && (!path || !bbox)) return;
243
+ if (usePhoto && !bbox) return;
244
+ var bw = Math.max(1, bbox.maxX - bbox.minX);
245
+ var bh = Math.max(1, bbox.maxY - bbox.minY);
246
+ var unit = (W * CFG.whaleScale) / bw;
247
+ var cxp = contentCX || W / 2, cyp = H * CFG.baseY;
248
+ var step = Math.max(0.12, Math.sqrt((bw * bh) / Math.max(100, CFG.density * 2.2)));
249
+ var pts = [];
250
+ var x0 = bbox.minX, y0 = bbox.minY;
251
+ for (var y = y0 - step / 2; y <= bbox.maxY + step / 2; y += step) {
252
+ for (var x = x0 - step / 2; x <= bbox.maxX + step / 2; x += step) {
253
+ var inside = subjectHit(x, y);
254
+ var onEdge = !inside && (subjectHit(x - step / 4, y)
255
+ || subjectHit(x + step / 4, y)
256
+ || subjectHit(x, y - step / 4)
257
+ || subjectHit(x, y + step / 4));
258
+ if (CFG.outlineOnly ? onEdge : (inside || onEdge)) {
259
+ var jit = unit * step * 0.42;
260
+ var s = 0.9 + Math.pow(Math.random(), 2.2) * 3.8;
261
+ pts.push({
262
+ hx: cxp + (x - x0 - bw / 2) * unit + (Math.random() - 0.5) * jit,
263
+ hy: cyp + (y - y0 - bh / 2) * unit + (Math.random() - 0.5) * jit,
264
+ s: s, rot: Math.random() * Math.PI,
265
+ });
266
+ }
267
+ }
268
+ }
269
+ var want = CFG.outlineOnly ? Math.min(CFG.density * 0.5, pts.length) : CFG.density;
270
+ if (pts.length > want * 1.15) {
271
+ pts.sort(function () { return Math.random() - 0.5; });
272
+ pts.length = Math.floor(want);
273
+ }
274
+ var home = pts;
275
+ var r0 = Math.max(W, H) * 0.55;
276
+ fish = { home: home, pts: home.map(function (p) {
277
+ return {
278
+ x: p.hx + (Math.random() - 0.5) * 2 * r0,
279
+ y: p.hy + (Math.random() - 0.5) * 2 * r0,
280
+ vx: 0, vy: 0, s: p.s, rot: Math.random() * Math.PI,
281
+ w1: 0.6 + Math.random() * 1.9, w2: 0.6 + Math.random() * 1.9,
282
+ f1: Math.random() * 6.2832, f2: Math.random() * 6.2832,
283
+ f3: Math.random() * 6.2832, f4: Math.random() * 6.2832,
284
+ };
285
+ }) };
286
+ }
287
+
288
+ function buildGrid() {
289
+ gridPts = [];
290
+ var gap = Math.max(56, Math.min(104, W / 14));
291
+ for (var y = gap / 2; y < H; y += gap) {
292
+ for (var x = gap / 2; x < W; x += gap) {
293
+ gridPts.push({ x: x, y: y, hx: x, hy: y, vx: 0, vy: 0 });
294
+ }
295
+ }
296
+ }
297
+
298
+ /* ---------- 内容区中心(左侧栏展开/收起时鲸鱼保持居中) ---------- */
299
+ function measureContentCenter() {
300
+ var w = W || window.innerWidth;
301
+ var cands = ["._1qAH1q_sidebarCol", '[data-slot="sidebar"]'];
302
+ for (var i = 0; i < cands.length; i++) {
303
+ var el = document.querySelector(cands[i]);
304
+ if (!el) continue;
305
+ var r = el.getBoundingClientRect();
306
+ if (r.width > 30 && r.left >= 0 && r.left < w * 0.5) {
307
+ return r.right + Math.max(0, (w - r.right)) / 2;
308
+ }
309
+ }
310
+ return w / 2;
311
+ }
312
+ // 中心变化时平移整只鲸鱼(不打断动画),并同步正在进行的波
313
+ function shiftWhaleCenter(dx) {
314
+ if (Math.abs(dx) < 0.01) return;
315
+ contentCX += dx;
316
+ for (var i = 0; i < fish.home.length; i++) {
317
+ fish.home[i].hx += dx;
318
+ fish.pts[i].x += dx;
319
+ }
320
+ var it = auto.item;
321
+ if (it && it.kind === "wave") {
322
+ it.q0 += dx * it.ux;
323
+ it.q1 += dx * it.uy;
324
+ }
325
+ }
326
+ // 每帧向目标中心做指数趋近(≈0.3~0.5s 平滑滑过去,而非瞬移)
327
+ function updateCenter(dt) {
328
+ var diff = centerTarget - contentCX;
329
+ if (Math.abs(diff) < 0.5) {
330
+ if (diff !== 0) shiftWhaleCenter(diff);
331
+ return;
332
+ }
333
+ shiftWhaleCenter(diff * (1 - Math.exp(-9 * dt)));
334
+ }
335
+
336
+ /* ---------- 设置持久化 / 参数面板 / 手动·自动模式 ---------- */
337
+ var React = null;
338
+ try { React = require("react"); } catch (e) { React = null; }
339
+ var panelEl = null; // 浮动面板(兼容:section 内的按钮会调用 openSettings)
340
+ var panelHost = null; // 设置页 section 里的控件容器
341
+ var modeListeners = []; // section 内手动/自动按钮的刷新回调
342
+
343
+ function persistCfg() { try { localStorage.setItem("dsh-whale:cfg", JSON.stringify(CFG)); } catch (e) {} }
344
+ function patchCfg(k, v) {
345
+ CFG[k] = v;
346
+ persistCfg();
347
+ var rebuild = ["density", "whaleScale", "baseY", "outlineOnly",
348
+ "photoAlpha", "photoInvert", "subject"];
349
+ if (rebuild.indexOf(k) >= 0) {
350
+ if (k === "subject" && CFG.subject === "photo") ensurePhotoLoad();
351
+ buildPath();
352
+ resampleWhale();
353
+ }
354
+ }
355
+ function setMode(m) {
356
+ auto.manual = m !== "auto";
357
+ auto.item = null;
358
+ auto.idleAt = 0;
359
+ try { localStorage.setItem("dsh-whale:auto", auto.manual ? "0" : "1"); } catch (e) {}
360
+ for (var i = 0; i < modeListeners.length; i++) { try { modeListeners[i](); } catch (e) {} }
361
+ }
362
+ function rebuildAllControls() {
363
+ // 参数改动后把两个界面里的滑杆/勾选都刷新一遍
364
+ modeListeners = [];
365
+ if (panelHost) { panelHost.innerHTML = ""; buildControls(panelHost, false); }
366
+ if (panelEl) { panelEl.innerHTML = ""; buildPanelBody(panelEl, true); }
367
+ }
368
+ function closeSettings() {
369
+ if (panelEl && panelEl.parentNode) panelEl.parentNode.removeChild(panelEl);
370
+ panelEl = null;
371
+ }
372
+ function openSettings() {
373
+ if (panelEl && panelEl.parentNode) { closeSettings(); return; }
374
+ var el = document.createElement("div");
375
+ el.id = "dsh-whale-panel";
376
+ document.body.appendChild(el);
377
+ panelEl = el;
378
+ buildPanelBody(el, true);
379
+ }
380
+ // 面板/设置页共用的控件区:host 是容器,isPanel 决定标题与“关闭”按钮
381
+ function buildPanelBody(host, withClose) {
382
+ var el = host;
383
+ var h = document.createElement("h3");
384
+ h.textContent = "🐋 鲸鱼背景参数";
385
+ el.appendChild(h);
386
+ var hint = document.createElement("div");
387
+ hint.className = "hint3";
388
+ hint.textContent = "改动即时生效并自动保存";
389
+ el.appendChild(hint);
390
+ buildControls(el, true);
391
+ }
392
+ // 设置页里的排版:行/分组/控件都照官方设置页的 token 与尺寸来做
393
+ var ROW_L = { display: "flex", alignItems: "center", gap: "12px", padding: "10px 0", borderBottom: "0.5px solid var(--dsw-alias-border-l2)" };
394
+ var ROW_LAST = { display: "flex", alignItems: "center", gap: "12px", padding: "10px 0" };
395
+ var ROW_TEXT = { display: "flex", flexDirection: "column", flex: 1, gap: "2px", minWidth: 0 };
396
+ var TEXT_TITLE = { color: "var(--dsw-alias-label-primary)", fontSize: "14px", lineHeight: "22px" };
397
+ var TEXT_HINT = { color: "var(--dsw-alias-label-tertiary)", fontSize: "12px", lineHeight: "18px" };
398
+ var GROUP = { marginTop: "18px" };
399
+ var GROUP_TITLE = { color: "var(--dsw-alias-label-primary)", fontSize: "14px", fontWeight: 600, margin: "0 0 2px" };
400
+ var RANGE = { flex: "1 1 120px", minWidth: "100px", maxWidth: "260px", accentColor: "var(--dsw-alias-brand-primary)", height: "20px" };
401
+ var VAL = { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", width: "52px", textAlign: "right", fontVariantNumeric: "tabular-nums", flex: "none" };
402
+ var CHECKBOX = { width: "16px", height: "16px", flex: "none", accentColor: "var(--dsw-alias-brand-primary)" };
403
+ var SEG = { display: "inline-flex", gap: "4px", padding: "3px", borderRadius: "999px", background: "var(--dsw-alias-bg-module-platform)", flex: "none" };
404
+ var SEG_BTN = { appearance: "none", font: "inherit", fontSize: "13px", lineHeight: "20px", cursor: "pointer", border: 0, borderRadius: "999px", padding: "4px 14px", background: "transparent", color: "var(--dsw-alias-label-secondary)" };
405
+ var SEG_ON = { appearance: "none", font: "inherit", fontSize: "13px", lineHeight: "20px", fontWeight: 600, cursor: "pointer", border: 0, borderRadius: "999px", padding: "4px 14px", background: "var(--dsw-alias-interactive-bg-hover)", color: "var(--dsw-alias-label-primary)" };
406
+ var BTN = { appearance: "none", font: "inherit", fontSize: "13px", lineHeight: "20px", cursor: "pointer", border: "0.5px solid var(--dsw-alias-border-l4)", borderRadius: "10px", padding: "6px 14px", background: "transparent", color: "var(--dsw-alias-label-primary)", flex: "none" };
407
+
408
+ function buildControls(el, withClose) {
409
+ var target = el; // 当前写入容器:分组切换后指向该分组
410
+ function row(title, hint, control, last) {
411
+ var r = document.createElement("div");
412
+ Object.assign(r.style, last ? ROW_LAST : ROW_L);
413
+ var text = document.createElement("div");
414
+ Object.assign(text.style, ROW_TEXT);
415
+ var tn = document.createElement("span"); tn.textContent = title; Object.assign(tn.style, TEXT_TITLE);
416
+ text.appendChild(tn);
417
+ if (hint) {
418
+ var hn = document.createElement("span"); hn.textContent = hint; Object.assign(hn.style, TEXT_HINT);
419
+ text.appendChild(hn);
420
+ }
421
+ r.appendChild(text);
422
+ if (control) r.appendChild(control);
423
+ target.appendChild(r);
424
+ return { row: r, text: text, hint: hint ? text.lastChild : null };
425
+ }
426
+ function group(title) {
427
+ var g = document.createElement("div");
428
+ Object.assign(g.style, GROUP);
429
+ var h = document.createElement("div"); h.textContent = title; Object.assign(h.style, GROUP_TITLE);
430
+ g.appendChild(h);
431
+ el.appendChild(g);
432
+ target = g;
433
+ return g;
434
+ }
435
+ function rangeControl(k, min, max, step, valEl) {
436
+ var inp = document.createElement("input");
437
+ inp.type = "range"; inp.min = String(min); inp.max = String(max);
438
+ inp.step = String(step); inp.value = String(CFG[k]);
439
+ Object.assign(inp.style, RANGE);
440
+ inp.addEventListener("input", function () {
441
+ var n = parseFloat(inp.value);
442
+ CFG[k] = n; persistCfg();
443
+ if (valEl) valEl.el.textContent = String(n);
444
+ if (k === "density" || k === "whaleScale" || k === "baseY" || k === "outlineOnly") resampleWhale();
445
+ if (k === "density" && valEl && valEl.hint) valEl.hint.textContent = "当前 " + fish.home.length + " 点";
446
+ });
447
+ return inp;
448
+ }
449
+ function withValue(inp) {
450
+ var v = document.createElement("span");
451
+ v.textContent = inp.value;
452
+ Object.assign(v.style, VAL);
453
+ var wrap = document.createElement("div");
454
+ Object.assign(wrap.style, { display: "flex", alignItems: "center", gap: "8px", flex: "1 1 150px", justifyContent: "flex-end" });
455
+ wrap.appendChild(inp); wrap.appendChild(v);
456
+ return { el: wrap, val: v };
457
+ }
458
+ function checkControl(k) {
459
+ var inp = document.createElement("input");
460
+ inp.type = "checkbox"; inp.checked = !!CFG[k];
461
+ Object.assign(inp.style, CHECKBOX);
462
+ inp.addEventListener("change", function () { patchCfg(k, inp.checked); });
463
+ return inp;
464
+ }
465
+ // shape 是二选一枚举(不是布尔),单独给一个勾选框
466
+ function shapeControl() {
467
+ var inp = document.createElement("input");
468
+ inp.type = "checkbox"; inp.checked = CFG.shape === "square";
469
+ Object.assign(inp.style, CHECKBOX);
470
+ inp.addEventListener("change", function () { patchCfg("shape", inp.checked ? "square" : "dot"); });
471
+ return inp;
472
+ }
473
+ function segment(items, onPick, isOn) {
474
+ var box = document.createElement("div");
475
+ Object.assign(box.style, SEG);
476
+ var btns = items.map(function (it) {
477
+ var b = document.createElement("button");
478
+ b.type = "button";
479
+ b.textContent = it.label;
480
+ Object.assign(b.style, isOn(it.key) ? SEG_ON : SEG_BTN);
481
+ b.addEventListener("click", function () {
482
+ onPick(it.key);
483
+ btns.forEach(function (bb, idx) { Object.assign(bb.style, isOn(items[idx].key) ? SEG_ON : SEG_BTN); });
484
+ });
485
+ box.appendChild(b);
486
+ return b;
487
+ });
488
+ return { el: box, btns: btns, refresh: function () { btns.forEach(function (bb, idx) { Object.assign(bb.style, isOn(items[idx].key) ? SEG_ON : SEG_BTN); }); } };
489
+ }
490
+
491
+ /* ---- 运动:手动扰动 ---- */
492
+ group("鼠标扰动");
493
+ var vRepelR = withValue(rangeControl("repelR", 60, 300, 5, null));
494
+ row("扰动半径", "光标影响范围(px)", vRepelR.el);
495
+ var vRepelA = withValue(rangeControl("repelA", 0, 60000, 500, null));
496
+ row("径向推斥", "把点推开的力度", vRepelA.el);
497
+ var vAgi = withValue(rangeControl("agiA", 0, 60000, 1000, null));
498
+ row("扰动强度", "每颗粒子自有外力的幅度", vAgi.el);
499
+ var vSpring = withValue(rangeControl("springS", 30, 400, 5, null));
500
+ row("回位刚度", "离开后弹回原位的快慢", vSpring.el);
501
+ var vDamp = withValue(rangeControl("damping", 2, 30, 0.5, null));
502
+ row("阻尼", "越大越稳、越小越弹", vDamp.el, true);
503
+
504
+ /* ---- 自动(屏保) ---- */
505
+ group("自动模式(屏保)");
506
+ var vSpeed = withValue(rangeControl("waveSpeed", 80, 1400, 10, null));
507
+ row("波速", "波前扫过的速度(px/s)", vSpeed.el);
508
+ var vHalf = withValue(rangeControl("waveHalf", 30, 300, 5, null));
509
+ row("波带宽", "扰动带半宽(px)", vHalf.el);
510
+ var vDiag = withValue(rangeControl("waveDiag", 0.3, 1, 0.05, null));
511
+ row("斜向减速", "45° 方向的速度系数", vDiag.el);
512
+ var vGap = withValue(rangeControl("autoGap", 0, 12, 0.5, null));
513
+ row("自动间隔", "两段效果之间的安静时长(秒)", vGap.el);
514
+ row("波向随机", "关闭则固定为左→右", checkControl("dirRandom"), true);
515
+
516
+ /* ---- 外观 ---- */
517
+ group("外观");
518
+ var vDensity = withValue(rangeControl("density", 400, 9000, 100, null));
519
+ var densityRow = row("点密度", "当前 " + fish.home.length + " 点", vDensity.el);
520
+ vDensity.hint = densityRow.hint;
521
+ var vScale = withValue(rangeControl("whaleScale", 0.3, 0.9, 0.01, null));
522
+ row("主体大小", "占视口宽度的比例", vScale.el);
523
+ var vBaseY = withValue(rangeControl("baseY", 0.2, 0.8, 0.01, null));
524
+ row("垂直位置", "0 = 顶部,1 = 底部", vBaseY.el);
525
+ row("方块颗粒", "关闭则用圆点", shapeControl(), true);
526
+ row("点阵网格", "在主体之外铺一层背景点阵", checkControl("gridOn"), true);
527
+
528
+ /* ---- 主体来源 ---- */
529
+ group("主体来源");
530
+ var subjSeg = segment(
531
+ [{ key: "whale", label: "🐋 鲸鱼" }, { key: "photo", label: "🖼 照片" }],
532
+ function (key) {
533
+ patchCfg("subject", key);
534
+ if (key === "photo") {
535
+ ensurePhotoLoad();
536
+ if (!photoA && !photoLoading) fileInput.click();
537
+ }
538
+ syncPhoto();
539
+ },
540
+ function (key) { return CFG.subject === key; }
541
+ );
542
+ row("主体", "鲸鱼剪影,或导入透明 PNG 照片", subjSeg.el);
543
+
544
+ var fileInput = document.createElement("input");
545
+ fileInput.type = "file"; fileInput.accept = "image/*"; fileInput.style.display = "none";
546
+ var photoStatus = document.createElement("span");
547
+ Object.assign(photoStatus.style, TEXT_HINT);
548
+ var fileBtn = document.createElement("button");
549
+ fileBtn.type = "button"; fileBtn.textContent = "选择 PNG";
550
+ Object.assign(fileBtn.style, BTN);
551
+ var photoCtl = document.createElement("div");
552
+ Object.assign(photoCtl.style, { display: "flex", alignItems: "center", gap: "10px", flex: "1 1 200px", justifyContent: "flex-end" });
553
+ photoCtl.appendChild(photoStatus); photoCtl.appendChild(fileBtn); photoCtl.appendChild(fileInput);
554
+ row("照片蒙版", "按 alpha 取主体,仅存本地", photoCtl);
555
+
556
+ var vAlpha = withValue(rangeControl("photoAlpha", 0, 255, 4, null));
557
+ var alphaRow = row("alpha 阈值", "低于该值视为透明", vAlpha.el);
558
+ var invertRow = row("反相", "改选透明区域作为主体", checkControl("photoInvert"), true);
559
+
560
+ fileInput.addEventListener("change", function () {
561
+ if (fileInput.files && fileInput.files[0]) importPhoto(fileInput.files[0]);
562
+ fileInput.value = "";
563
+ });
564
+ fileBtn.addEventListener("click", function () { fileInput.click(); });
565
+ function syncPhoto() {
566
+ var on = CFG.subject === "photo";
567
+ photoStatus.textContent = on
568
+ ? (photoA ? "已导入蒙版" : (photoLoading ? "读取中…" : (photoErr ? photoErr : "未导入")))
569
+ : "切换到照片后可导入";
570
+ fileBtn.disabled = !on;
571
+ fileBtn.style.opacity = on ? "1" : "0.45";
572
+ alphaRow.row.style.display = on ? "" : "none";
573
+ invertRow.row.style.display = on ? "" : "none";
574
+ }
575
+
576
+ /* ---- 轮廓 ---- */
577
+ group("轮廓");
578
+ row("只保留轮廓", "只画主体边缘的点", checkControl("outlineOnly"), true);
579
+
580
+ /* ---- 模式 ---- */
581
+ group("模式");
582
+ var modeSeg = segment(
583
+ [{ key: "manual", label: "🖱 手动" }, { key: "auto", label: "🌀 自动" }],
584
+ function (key) { setMode(key); },
585
+ function (key) { return (key === "auto") === !auto.manual; }
586
+ );
587
+ row("驱动方式", "手动跟随鼠标,自动当屏保", modeSeg.el);
588
+ statusEl = document.createElement("div");
589
+ Object.assign(statusEl.style, Object.assign({ padding: "8px 0 2px" }, TEXT_HINT));
590
+ el.appendChild(statusEl);
591
+ modeListeners.push(function () {
592
+ modeSeg.refresh();
593
+ statusEl.textContent = auto.manual
594
+ ? "手动:鼠标靠近处点阵被扰动,移开自动归位"
595
+ : "自动:360° 波扫过 + 全屏呼吸,可当屏保";
596
+ });
597
+
598
+ var actions = document.createElement("div");
599
+ Object.assign(actions.style, { display: "flex", gap: "8px", marginTop: "16px" });
600
+ var bReset = document.createElement("button");
601
+ bReset.type = "button"; bReset.textContent = "↺ 恢复默认";
602
+ bReset.title = "把所有参数恢复成出厂值并即时生效";
603
+ Object.assign(bReset.style, BTN);
604
+ bReset.addEventListener("click", resetDefaults);
605
+ actions.appendChild(bReset);
606
+ if (withClose) {
607
+ var bClose = document.createElement("button");
608
+ bClose.type = "button"; bClose.textContent = "关闭";
609
+ Object.assign(bClose.style, BTN);
610
+ bClose.addEventListener("click", closeSettings);
611
+ actions.appendChild(bClose);
612
+ }
613
+ el.appendChild(actions);
614
+ syncPhoto();
615
+ modeListeners[modeListeners.length - 1]();
616
+ }
617
+ function resetDefaults() {
618
+ for (var i = 0; i < CFG_KEYS.length; i++) {
619
+ CFG[CFG_KEYS[i]] = CFG_DEFAULT[CFG_KEYS[i]];
620
+ }
621
+ persistCfg();
622
+ resampleWhale();
623
+ rebuildAllControls();
624
+ }
625
+ /* ---------- 设置页:挂在 设置 → 插件 里的一个 tab ----------
626
+ * 为什么不用 settings.section(独立侧栏板块):实测在这台 Desktop 上注册成功
627
+ * 但界面里不出现(内置 section 与 dshmarket 都走同一条路,原因待查)。
628
+ * 为什么不做成「插件配置」里的一张卡片:`settings.plugin.item` 是 keyed 槽,
629
+ * 只渲染宿主 settingsScope.describe() 里出现的命名空间;实测第三方插件在宿主
630
+ * 侧没有注册设置命名空间的 api(已装插件里只有 ctx.provide/tools/skills 之类),
631
+ * 所以卡片进不去那个列表。
632
+ * 折中:在「插件」板块里加一个同级 tab,参数直接铺开(不再折叠);行的排版
633
+ * (16px 行距、0.5px border-l2 分隔线、label-primary/tertiary 两级文字、胶囊分段控件)
634
+ * 全部照官方设置页与卡片的 token 与尺寸,观感与「插件配置」一致。
635
+ */
636
+ var NS = "dsh-whale-particles";
637
+ var LOCALE = {
638
+ zh: {
639
+ "nav": "鲸鱼点阵背景",
640
+ "title": "鲸鱼点阵背景",
641
+ "intro": "点阵主视觉的全部参数,改动即时生效并自动保存。",
642
+ "manual": "手动:鼠标靠近处点阵被扰动,移开自动归位",
643
+ "auto": "自动:360° 波扫过 + 全屏呼吸,可当屏保"
644
+ },
645
+ en: {
646
+ "nav": "Whale particles",
647
+ "title": "Whale particle background",
648
+ "intro": "Every knob for the dot-matrix backdrop; changes apply instantly and save themselves.",
649
+ "manual": "Manual: the cursor disturbs nearby dots, they settle back when it leaves",
650
+ "auto": "Auto: 360-degree waves + full-screen breathing, usable as a screensaver"
651
+ }
652
+ };
653
+ // 版面:标题 + 一行说明 + 直接铺开的参数区(不再折叠)
654
+ var S_SECTION = { maxWidth: "760px", color: "var(--dsw-alias-label-primary)" };
655
+ var S_HEADING = { margin: "0 0 4px", fontSize: "18px", fontWeight: 600, color: "var(--dsw-alias-label-primary)" };
656
+ var S_INTRO = { color: "var(--dsw-alias-label-tertiary)", margin: "0 0 4px", fontSize: "13px" };
657
+ var S_FOOT = { marginTop: "18px", paddingTop: "12px", borderTop: "0.5px solid var(--dsw-alias-border-l2)", color: "var(--dsw-alias-label-tertiary)", fontSize: "12px" };
658
+
659
+ // 控件容器:挂载时把共享的滑杆/按钮建进去
660
+ function ControlsHost(props) {
661
+ var hostRef = React.useRef(null);
662
+ React.useEffect(function () {
663
+ var host = hostRef.current;
664
+ if (!host) return;
665
+ panelHost = host;
666
+ buildControls(host, false);
667
+ }, []);
668
+ return React.createElement("div", { ref: hostRef, style: props && props.style });
669
+ }
670
+ function WhaleTab(props) {
671
+ var t = (props && props.t) || function (k) { return LOCALE.zh[k] || k; };
672
+ return React.createElement("div", { style: S_SECTION },
673
+ React.createElement("h2", { style: S_HEADING }, "🐋 " + t("title")),
674
+ React.createElement("p", { style: S_INTRO }, t("intro")),
675
+ React.createElement(ControlsHost, { style: { marginTop: "10px" } }),
676
+ React.createElement("div", { style: S_FOOT }, "dsh-whale-particles v" + VERSION)
677
+ );
678
+ }
679
+
680
+ function warn(what, err) {
681
+ try { console.warn("[dsh-whale-particles] " + what + " 注册失败:", err); } catch (e) {}
682
+ }
683
+ function registerSettingsUI(ctx) {
684
+ var t = (ctx && ctx.locale && typeof ctx.locale.bind === "function")
685
+ ? ctx.locale.bind(NS)
686
+ : function (k) { return LOCALE.zh[k] || k; };
687
+ if (ctx && ctx.locale && typeof ctx.locale.register === "function" && typeof ctx.effect === "function") {
688
+ try { ctx.effect(function () { return ctx.locale.register(NS, LOCALE); }, NS + ": dictionaries"); }
689
+ catch (e) { warn("locale", e); }
690
+ }
691
+ if (!ctx || !ctx.slots || !React) { warn("slots/react 不可用,跳过设置界面", null); return; }
692
+ try {
693
+ ctx.slots.inject("settings.plugins.tab", function () {
694
+ return ctx.slots.register({
695
+ name: "settings.plugins.tab",
696
+ id: "whale-particles",
697
+ order: 10, // 排在「插件配置」(order 0)后面
698
+ label: function () { return t("nav"); },
699
+ locale: NS
700
+ }, WhaleTab);
701
+ });
702
+ } catch (e) { warn("settings.plugins.tab", e); }
703
+ try {
704
+ ctx.slots.inject("settings.plugin.item", function () {
705
+ return ctx.slots.register({
706
+ name: "settings.plugin.item",
707
+ key: NS, // 宿主按命名空间分派卡片
708
+ locale: NS
709
+ }, WhaleTab);
710
+ });
711
+ } catch (e) { warn("settings.plugin.item", e); }
712
+ }
713
+
714
+ /* ---------------------------- 自动驱动器 ---------------------------- */
715
+ // 主界面控制条已移除:自动模式的状态只写到设置页里的状态行(未打开设置时忽略)
716
+ var statusEl = null; // 由 buildControls 赋值
717
+ function setStatus(text) {
718
+ if (statusEl && statusEl.isConnected) statusEl.textContent = text;
719
+ }
720
+ function breathEnv(p) {
721
+ if (p < 0.3) return smoothstep(0, 1, p / 0.3);
722
+ if (p < 0.5) return 1;
723
+ return 1 - smoothstep(0, 1, (p - 0.5) / 0.5);
724
+ }
725
+ function spawnAuto(t) {
726
+ var kind = Math.random() < 0.72 ? "wave" : "breath";
727
+ if (kind === "breath" && auto.lastKind === "breath") kind = "wave";
728
+ if (kind === "wave") {
729
+ var ux, uy, deg = 0;
730
+ if (CFG.dirRandom) {
731
+ var pick = null;
732
+ for (var tries = 0; tries < 12 && pick === null; tries++) {
733
+ var a = Math.random() * Math.PI * 2;
734
+ if (auto.lastAngle !== null) {
735
+ var d = Math.abs(a - auto.lastAngle) % (Math.PI * 2);
736
+ if (d > Math.PI) d = Math.PI * 2 - d;
737
+ if (d < 0.6) continue;
738
+ }
739
+ pick = { ux: Math.cos(a), uy: Math.sin(a) };
740
+ }
741
+ if (!pick) { var a2 = Math.random() * Math.PI * 2; pick = { ux: Math.cos(a2), uy: Math.sin(a2) }; }
742
+ ux = pick.ux; uy = pick.uy;
743
+ auto.lastAngle = Math.atan2(uy, ux);
744
+ deg = Math.round(((Math.atan2(uy, ux) * 180 / Math.PI) % 360 + 360) % 360);
745
+ } else {
746
+ ux = 1; uy = 0;
747
+ }
748
+ auto.lastKind = "wave";
749
+ var am = Math.max(Math.abs(ux), Math.abs(uy));
750
+ var diag01 = (1 - am) / (1 - Math.SQRT1_2);
751
+ var scale = Math.max(0.2, 1 - diag01 * (1 - CFG.waveDiag));
752
+
753
+ var q0 = Infinity, q1 = -Infinity;
754
+ for (var i = 0; i < fish.home.length; i++) {
755
+ var q = fish.home[i].hx * ux + fish.home[i].hy * uy;
756
+ if (q < q0) q0 = q;
757
+ if (q > q1) q1 = q;
758
+ }
759
+ if (!isFinite(q0)) {
760
+ var corners = [[0, 0], [W, 0], [0, H], [W, H]];
761
+ for (var c = 0; c < corners.length; c++) {
762
+ var qc = corners[c][0] * ux + corners[c][1] * uy;
763
+ if (qc < q0) q0 = qc;
764
+ if (qc > q1) q1 = qc;
765
+ }
766
+ }
767
+ var pad = CFG.waveHalf + 70;
768
+ q0 -= pad; q1 += pad;
769
+ var dur = Math.min(24, Math.max(1.0, (q1 - q0) / (CFG.waveSpeed * scale)));
770
+ auto.item = { kind: "wave", start: t, dur: dur, amp: 1.0, ux: ux, uy: uy, q0: q0, q1: q1, half: CFG.waveHalf };
771
+ var slow = scale < 0.98 ? "(斜向已减速) " : "";
772
+ setStatus("自动 · 波≈" + deg + "° " + slow);
773
+ } else {
774
+ auto.lastKind = "breath";
775
+ var durB = 4.5 + Math.random() * 3.5;
776
+ auto.item = { kind: "breath", start: t, dur: durB, amp: 0.95 };
777
+ setStatus("自动 · 全屏躁动…");
778
+ }
779
+ }
780
+ function updateAuto(t) {
781
+ if (auto.manual) return;
782
+ var it = auto.item;
783
+ if (it && t >= it.start && t < it.start + it.dur) return;
784
+ if (it && t >= it.start + it.dur) {
785
+ auto.item = null;
786
+ auto.idleAt = t + CFG.autoGap * (0.75 + Math.random() * 0.5);
787
+ setStatus("自动 · 归于平静…");
788
+ return;
789
+ }
790
+ if (!it && t >= auto.idleAt) spawnAuto(t);
791
+ }
792
+ function fieldGain(x, y, t) {
793
+ if (auto.manual) {
794
+ if (!mouse.active) return 0;
795
+ var dx = x - mouse.x, dy = y - mouse.y;
796
+ var d = Math.sqrt(dx * dx + dy * dy);
797
+ if (d >= CFG.repelR || d < 0.001) return 0;
798
+ return 1 - d / CFG.repelR;
799
+ }
800
+ var it = auto.item;
801
+ if (!it || t < it.start || t > it.start + it.dur) return 0;
802
+ var lt = (t - it.start) / it.dur;
803
+ if (it.kind === "wave") {
804
+ var q = x * it.ux + y * it.uy;
805
+ var front = it.q0 + (it.q1 - it.q0) * easeInOut(lt);
806
+ var dd = Math.abs(q - front);
807
+ if (dd > it.half) return 0;
808
+ return 1 - dd / it.half;
809
+ }
810
+ return breathEnv(lt);
811
+ }
812
+
813
+ /* ------------------------------ 物理 ------------------------------ */
814
+ function stepAll(dt, t) {
815
+ var r = CFG.repelR, R2 = r * r;
816
+ var amp = auto.manual ? 1 : (auto.item ? auto.item.amp : 0);
817
+ var bounce = function (p) { if (p.x < 0 || p.x > W) p.vx *= -0.5; if (p.y < 0 || p.y > H) p.vy *= -0.5; };
818
+
819
+ for (var i = 0; i < fish.pts.length; i++) {
820
+ var p = fish.pts[i], h = fish.home[i];
821
+ var ax = CFG.springS * (h.hx - p.x) - CFG.damping * p.vx;
822
+ var ay = CFG.springS * (h.hy - p.y) - CFG.damping * p.vy;
823
+ if (auto.manual && mouse.active) {
824
+ var dxm = p.x - mouse.x, dym = p.y - mouse.y, d2m = dxm * dxm + dym * dym;
825
+ if (d2m < R2 && d2m > 0.01) {
826
+ var dm = Math.sqrt(d2m);
827
+ var fm = CFG.repelA * Math.pow(1 - dm / r, 2);
828
+ ax += (dxm / dm) * fm; ay += (dym / dm) * fm;
829
+ }
830
+ }
831
+ var gain = fieldGain(p.x, p.y, t) * amp;
832
+ if (gain > 0.002) {
833
+ var A = CFG.agiA * gain * gain;
834
+ var cx = (Math.sin(p.w1 * t + p.f1) + 0.4 * Math.sin(p.w1 * 2.3 * t + p.f2)) / 1.4;
835
+ var cy = (Math.sin(p.w2 * t + p.f3) + 0.4 * Math.sin(p.w2 * 2.3 * t + p.f4)) / 1.4;
836
+ ax += cx * A; ay += cy * A;
837
+ }
838
+ p.vx += ax * dt; p.vy += ay * dt;
839
+ var sp2 = p.vx * p.vx + p.vy * p.vy;
840
+ if (sp2 > 5000 * 5000) { var s = 5000 / Math.sqrt(sp2); p.vx *= s; p.vy *= s; }
841
+ p.x += p.vx * dt; p.y += p.vy * dt;
842
+ bounce(p);
843
+ }
844
+
845
+ if (CFG.gridOn) {
846
+ var gi = 0;
847
+ for (var g = 0; g < gridPts.length; g++) {
848
+ var gp = gridPts[g];
849
+ var gax = 60 * (gp.hx - gp.x) - 10 * gp.vx;
850
+ var gay = 60 * (gp.hy - gp.y) - 10 * gp.vy;
851
+ if (auto.manual && mouse.active) {
852
+ var dxg = gp.x - mouse.x, dyg = gp.y - mouse.y, d2g = dxg * dxg + dyg * dyg;
853
+ if (d2g < R2 && d2g > 0.01) {
854
+ var dg = Math.sqrt(d2g);
855
+ var fg = CFG.repelA * 0.3 * Math.pow(1 - dg / r, 2);
856
+ gax += (dxg / dg) * fg; gay += (dyg / dg) * fg;
857
+ }
858
+ }
859
+ if (!auto.manual) {
860
+ var gainG = fieldGain(gp.x, gp.y, t) * amp;
861
+ if (gainG > 0.002) {
862
+ var AG = CFG.agiA * 0.10 * gainG * gainG;
863
+ gax += Math.sin(1.1 * t + frac(Math.sin(gi * 127.1) * 43758.5453) * 6.2832) * AG;
864
+ gay += Math.cos(0.9 * t + frac(Math.cos(gi * 311.7) * 104729.0) * 6.2832) * AG;
865
+ }
866
+ }
867
+ gp.vx += gax * dt; gp.vy += gay * dt;
868
+ gp.x += gp.vx * dt; gp.y += gp.vy * dt;
869
+ gi++;
870
+ }
871
+ }
872
+ }
873
+
874
+ /* ------------------------------ 渲染 ------------------------------ */
875
+ function render(ctx) {
876
+ ctx.clearRect(0, 0, W, H);
877
+ if (CFG.gridOn) {
878
+ ctx.fillStyle = theme.grid + theme.gridA + ")";
879
+ for (var g = 0; g < gridPts.length; g++) {
880
+ ctx.fillRect(gridPts[g].x - 0.7, gridPts[g].y - 0.7, 1.4, 1.4);
881
+ }
882
+ }
883
+ var baseA = theme.dotA;
884
+ var square = CFG.shape === "square";
885
+ for (var i = 0; i < fish.pts.length; i++) {
886
+ var p = fish.pts[i], h = fish.home[i];
887
+ var off = Math.min(1, Math.hypot(p.x - h.hx, p.y - h.hy) / 260);
888
+ var a = Math.min(1, baseA * (0.82 + 0.22 * off + 0.06 * (i % 3)));
889
+ ctx.fillStyle = "rgba(" + theme.dot[0] + "," + theme.dot[1] + "," + theme.dot[2] + "," + a.toFixed(3) + ")";
890
+ if (square) {
891
+ ctx.save();
892
+ ctx.translate(p.x, p.y);
893
+ ctx.rotate(p.rot);
894
+ var s2 = p.s / 2;
895
+ ctx.fillRect(-s2, -s2, p.s, p.s);
896
+ ctx.restore();
897
+ } else {
898
+ ctx.beginPath();
899
+ ctx.arc(p.x, p.y, p.s * 0.55, 0, Math.PI * 2);
900
+ ctx.fill();
901
+ }
902
+ }
903
+ }
904
+
905
+ /* --------------------------- 挂载/卸载 --------------------------- */
906
+ function mount() {
907
+ var style = document.createElement("style");
908
+ style.id = "dsh-whale-particles-style";
909
+ style.textContent = STYLE;
910
+ document.head.appendChild(style);
911
+
912
+ var layer = document.createElement("div");
913
+ layer.id = "dsh-whale-particles-layer";
914
+ var canvas = document.createElement("canvas");
915
+ layer.appendChild(canvas);
916
+ document.body.appendChild(layer);
917
+ var ctx = canvas.getContext("2d");
918
+
919
+ // 主界面不再放控制条:手动/自动与全部参数都搬到了 设置 → 鲸鱼点阵背景
920
+ try { auto.manual = localStorage.getItem("dsh-whale:auto") !== "1"; } catch (e) {}
921
+
922
+ document.body.setAttribute("data-dsh-whale", "");
923
+ function teardown() {
924
+ running = false;
925
+ cancelAnimationFrame(raf);
926
+ if (style.parentNode) style.parentNode.removeChild(style);
927
+ closeSettings();
928
+ if (layer.parentNode) layer.parentNode.removeChild(layer);
929
+ document.body.removeAttribute("data-dsh-whale");
930
+ for (var i = 0; i < disposed.length; i++) { try { disposed[i](); } catch (e) {} }
931
+ disposed = [];
932
+ }
933
+ var onPointer = function (e) { mouse.x = e.clientX; mouse.y = e.clientY; mouse.active = true; };
934
+ var onLeave = function () { mouse.active = false; };
935
+ var onVis = function () {
936
+ running = !document.hidden;
937
+ if (running) { last = 0; raf = requestAnimationFrame(frame); }
938
+ else cancelAnimationFrame(raf);
939
+ };
940
+ var onResize = function () {
941
+ dpr = Math.min(2, window.devicePixelRatio || 1);
942
+ W = window.innerWidth; H = window.innerHeight;
943
+ canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr);
944
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
945
+ contentCX = measureContentCenter(); // 侧栏变化时重定位鲸鱼中心
946
+ centerTarget = contentCX;
947
+ ensurePhotoLoad(); // 照片模式:确保蒙版已解码
948
+ buildPath(); resampleWhale(); buildGrid();
949
+ };
950
+ var themeObs = new MutationObserver(function () {
951
+ var dark = document.body.hasAttribute("data-ds-dark-theme");
952
+ theme = dark ? DARK : LIGHT;
953
+ });
954
+ themeObs.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
955
+
956
+ window.addEventListener("pointermove", onPointer, { passive: true });
957
+ window.addEventListener("pointerdown", onPointer, { passive: true });
958
+ window.addEventListener("mouseleave", onLeave);
959
+ document.addEventListener("visibilitychange", onVis);
960
+ window.addEventListener("resize", onResize);
961
+ disposed.push(function () {
962
+ window.removeEventListener("pointermove", onPointer);
963
+ window.removeEventListener("pointerdown", onPointer);
964
+ window.removeEventListener("mouseleave", onLeave);
965
+ document.removeEventListener("visibilitychange", onVis);
966
+ window.removeEventListener("resize", onResize);
967
+ themeObs.disconnect();
968
+ });
969
+
970
+ function frame(now) {
971
+ if (!running) return;
972
+ var t = now / 1000;
973
+ var dt = Math.min(0.033, Math.max(0.001, t - (last || t)));
974
+ last = t;
975
+ updateCenter(dt); // 侧栏宽度变化时的平滑平移
976
+ updateAuto(t);
977
+ stepAll(dt, t);
978
+ render(ctx);
979
+ raf = requestAnimationFrame(frame);
980
+ }
981
+
982
+ onResize();
983
+ centerTarget = contentCX = measureContentCenter();
984
+ // 侧栏展开/收起、布局变化:只更新“目标中心”,由帧循环平滑补间过去
985
+ var watchIv = setInterval(function () {
986
+ centerTarget = measureContentCenter();
987
+ }, 400);
988
+ disposed.push(function () { clearInterval(watchIv); });
989
+ var prefersReduced = window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches;
990
+ if (prefersReduced) {
991
+ for (var k = 0; k < fish.pts.length; k++) { fish.pts[k].x = fish.home[k].hx; fish.pts[k].y = fish.home[k].hy; }
992
+ render(ctx);
993
+ } else {
994
+ running = true;
995
+ raf = requestAnimationFrame(frame);
996
+ }
997
+ return teardown;
998
+ }
999
+
1000
+ module.exports = {
1001
+ inject: ["slots", "locale"],
1002
+ apply: function (ctx) {
1003
+ registerSettingsUI(ctx);
1004
+ if (ctx && typeof ctx.effect === "function") {
1005
+ ctx.effect(function () { return mount(); }, "dsh-whale-particles: background layer");
1006
+ } else {
1007
+ mount();
1008
+ }
1009
+ },
1010
+ };
1011
+ return module.exports;
1012
+ },
1013
+ });
package/lib/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Host half: nothing to do in Node — everything happens in the browser client.
2
+ export const name = 'dsh-whale-particles';
3
+ export const inject = [];
4
+
5
+ export function apply() {
6
+ // no-op: the whale background is rendered by lib/client.js in the Web UI.
7
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "dsh-whale-particles",
3
+ "version": "0.3.0",
4
+ "description": "Interactive whale particle background for the DSH Web UI: every dot carries its own slow force; mouse disturbs them, plus an auto screensaver mode with 360-degree waves and full-screen breathing.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/fewoot/dsh-whale-particles.git"
9
+ },
10
+ "homepage": "https://github.com/fewoot/dsh-whale-particles#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/fewoot/dsh-whale-particles/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "lib/index.js",
16
+ "exports": {
17
+ ".": "./lib/index.js",
18
+ "./client": "./lib/client.js",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "lib",
23
+ "cordis.patch.yml",
24
+ "README.md",
25
+ "LICENSE",
26
+ "assets"
27
+ ],
28
+ "dsh": {
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ },
32
+ "client": {
33
+ "platform": "web",
34
+ "inject": [],
35
+ "immediately": true
36
+ }
37
+ },
38
+ "keywords": [
39
+ "deepseek-harness",
40
+ "dsh",
41
+ "dsh-plugin",
42
+ "wallpaper",
43
+ "background",
44
+ "whale",
45
+ "particles"
46
+ ]
47
+ }