Skip to content
dsh.fish
Bundle

dsh-scroll-piano

滚动钢琴:鼠标滑过 DSH 右侧边缘的滚动条竖条时,按纵向位置发出钢琴音(越高音越高,特雷门琴式)。

Source
439436269-ctrl
License
MIT
Updated
Updated 7 days ago

Readme

# dsh-scroll-piano · 滚动钢琴

鼠标滑过 DSH **聊天区右边缘的竖条**(滚动条/把手那一条)时,按鼠标的**纵向位置**发出钢琴音:**越靠上音越高、越靠下音越低**(特雷门琴式)。

## 效果

- 聊天区右边缘一条细彩色竖条(开启时淡淡发光),鼠标进入该区间即触发。
- 鼠标纵向移动 = 连续刮奏;竖条上有个白点跟随鼠标位置。
- 竖条上有**「我的消息」小地图刻度**:你每发一条消息,对应位置一根蓝色小横杠——**按「在整段对话里的位置」定位,与滚动无关**,所以不管你滚到对话多深,自己的消息始终标在那、点得到。
- **气泡只在鼠标贴近某条刻度(±16px)时出现**,内容就是那条消息的正文(最多 3 行);**其余位置整个不显示**(不显示音名、不显示 MIDI 数字)。
- **点蓝色小横杠 → 跳到那条消息**(平滑滚动、滚到视口中央)。蓝条命中区放大到 18×12px(可视化仍是 7×2 细条,悬停时加宽变亮),比原来 2px 高好点得多;**点气泡也能跳**(两者同一个 `jumpTo`)。
- 左下角一枚 `🎵 滚动钢琴 ON/OFF` 胶囊:点开关切换、右侧滑块调音量(设置存 `localStorage`)。**胶囊可拖动**(`cursor:grab`):位移 >6px 判定为拖、松手位置存 `localStorage("dsh-scroll-piano:pillPos")`、拖完 400ms 内不吞点击;**按在音量滑块上不进拖动**,滑块照常调音量。
- 默认音域 C3–C6(3 个八度),只按半音触发(同音不重复触发)。

> 预览**只显示用户自己发的消息**,**不做「取全局最近一条」兜底**(否则同一条会在整片区域反复出现 = 重复显示),位置感交给刻度。**声音与预览彼此独立**——任何位置滑动都照常出声,只有预览挑消息。
> 拿不到聊天区时(欢迎页 / 新会话还没有消息),触发区**退回窗口右边缘**,声音照出——否则那类页面连声音都没有(踩过)。

## 消息预览怎么定位

聊天流的稳定锚点是 **`[data-chat-flow]`**:它的**直接子元素**一条消息一个,key 形如:

| key 前缀 | 含义 |
|---|---|
| `13:input-message<uuid>` | 用户消息 → **预览这条**(角色显示「你」) |
| `14:assistant-stepN:M` | AI 消息 → 跳过 |
| `12:turn-processN` / `["process",…]` / `9:turn-tailN` | 过程 / 工具调用 / 轮次尾 → 跳过 |

刻度 y = **消息在滚动内容里的偏移 ÷ 内容总长 × 视口高**(`r.top − 视口top + scrollTop` 得到内容坐标,再归一化),**与 scrollTop 无关**——滚多远都不变,消息不会"滚出屏幕"而看不见。鼠标贴近某条刻度(±16px)就取它 `innerText` 的正文(去掉末尾 `HH:MM` 时间戳)显示;**点那根蓝条**(容器 `click` 事件委托到 `.dshscp_tick`,读 `data-i` 找回消息元素)就 `scrollIntoView({ block: "center" })`。**只认这些 `data-*` 属性**——容器类名(`icaHSq_flowItem`、`ST7X_W_scrollBody`)都带 hash,不碰。

## 触发区:聊天区右边缘,不是窗口右边缘

滚动条本身是浏览器**原生滚动条**(不在 DOM 里、类名带 hash),所以触发用全局 `pointermove` + `clientX` 判定。但**判定基准是聊天区滚动容器的右边缘**(从 `[data-chat-flow]` 往上找第一个 `overflow-y:auto` 的祖先),不是窗口右边缘——否则右侧栏(文件预览等)打开时会在面板上触发、气泡还会盖住面板。

竖条 / 圆点 / 气泡的位置也跟着这个右边缘走,用 `ResizeObserver`(容器变宽)+ `MutationObserver`(面板开合是 DOM 增删)双重兜底重算。

## 可调参数

代码内 `DEFAULTS`(`lib/client.js`):

| 键 | 默认 | 说明 |
|---|---|---|
| `on` | `true` | 开关 |
| `volume` | `0.25` | 音量 |
| `stripWidth` | `30` | 右侧竖条宽度(px) |
| `lowNote` / `highNote` | `48` / `84` | 音域(MIDI,C3–C6) |
| `inset` | `56` | 上下留白(px),让最高/最低音够得着 |

## 技术要点

| 项 | 做法 |
|---|---|
| 挂载槽位 | `shell.overlay` |
| 触发 | `document.addEventListener("pointermove")`,`clientX >= innerWidth - stripWidth` 才生效 |
| 音高 | `midi = highNote - t*(highNote-lowNote)`,`t` 为纵向归一化位置 |
| 音频 | Web Audio 合成(泛音 + 快速起音 + 指数衰减 + 噪声锤击瞬态 + 低通 + 压缩器) |
| 客户端形态 | 手写 `window.__ModuleLoader__.load` 零构建 bundle,`platform: web` |

## 安装

```sh
dsh plugin --profile desktop add dsh-scroll-piano
```

装好后左下角出现 `🎵 滚动钢琴 ON/OFF` 胶囊(包名会进 `dsh.profile.bundles`,实测热载入,一般不用重启;没生效就刷新页面)。

### 本地开发安装

```sh
NODE=~/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/node/bin/node
PNPM=~/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/pnpm/bin/pnpm.mjs
# profile/package.json 的 dependencies 加: "dsh-scroll-piano": "link:<本目录>"
# profile/cordis.patch.yml 加:
#   - insert:
#       - id: dsh-scroll-piano
#         name: dsh-scroll-piano
cd ~/.dsh/profiles/desktop && "$NODE" "$PNPM" install --no-frozen-lockfile
```

> 顺序很重要:**先 pnpm install 建软链,再触发 cordis.patch.yml 热载入**,否则 loader 首次 require 找不到包、之后不会自动重试。

## 开发

```sh
npm test          # node test/bundle-check.mjs
```

## 说明

- 首次出声需要一次用户手势(点开关或首次滑动即自动解锁 AudioContext)。
- 与 `dsh-piano`(滑动钢琴键盘)是两个独立插件,可同时装。

Install

dsh plugin --profile web add github:439436269-ctrl/dsh-scroll-piano

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source