# 源码核对与修复依据 参考项目为 [stackedlol/ascii-sharks](https://github.com/stackedlol/ascii-sharks),本地读取的提交是 `c521e24a787e6d4cc460d4cf2b9815f25e4fe453`。此前版本只取得 README 和文件目录,抓取源码失败后继续自行实现,未验证出画面问题。0.3.0 修复前已拉取仓库,阅读以下实际实现。 ## 绘制 [renderer.ts:643](https://github.com/stackedlol/ascii-sharks/blob/c521e24a787e6d4cc460d4cf2b9815f25e4fe453/src/renderer.ts#L643) 按鱼的包围盒遍历屏幕字格。每格以中心和四个子样本检测轮廓覆盖,保存深度、部位、亮度和身体坐标键。 [renderer.ts:749](https://github.com/stackedlol/ascii-sharks/blob/c521e24a787e6d4cc460d4cf2b9815f25e4fe453/src/renderer.ts#L749) 每格只在固定格中心绘制一个字符。上游还在实体格绘制底色,配合灰度和字符密度表现体积。 本插件保持鱼体由用户中文字组成,不添加上游的实体方块底色。修复后用原创鲸体三角面在每格中心和四个子样本做覆盖与深度检测,填充表面内部。汉字按 Canvas 实际测量值分配字格,统一字号、加粗、对齐设备像素,每格居中绘制一字。 旧版仅选择格内最近表面点,却仍在点的任意投影位置绘字。两个相邻字格中的字中心可只差 0.4px。旧版默认中心示例透明度为 `0.28 × 0.5 × 0.28 = 0.0392`,修复后光照主要控制字色,可见度不再经过远近和光照反复相乘。 ## 游动 [world.ts:459](https://github.com/stackedlol/ascii-sharks/blob/c521e24a787e6d4cc460d4cf2b9815f25e4fe453/src/world.ts#L459) 的 `stepSharks` 保存每条鱼的状态,以动量、持续游走、巡游目标、其他鱼身体分离及边缘响应计算期望方向,再限制转弯速度。后续脊柱跟随鱼头,不会瞬间原地反向。 [engine.ts:79](https://github.com/stackedlol/ascii-sharks/blob/c521e24a787e6d4cc460d4cf2b9815f25e4fe453/src/engine.ts#L79) 使用固定时间步推进运动,动画帧负责累计时间和绘制。 0.3.0 重新实现了确定性运动状态:每条鲸鱼保存位置、速度、朝向、目标和随机种子,预测接近并避让,提前减速回转。投影读取真实运动位置,但当时只有简单的尾部弯曲,没有逐节跟随脊柱。 ## 0.4.0 对照两张参考图 用户提供的是同一段视频 0:03 和 0:04 的静态帧。图中躯干、尾柄呈连续曲线,近处鱼身有明显深浅块,部分鱼更浅。这些帧可证明形状与明暗差异,无法单独测定摆尾频率或转身速度。 重新读取 [world.ts:566](https://github.com/stackedlol/ascii-sharks/blob/c521e24a787e6d4cc460d4cf2b9815f25e4fe453/src/world.ts#L566) 可确认上游脊柱跟随鱼头,每节约束长度和最大弯角。鱼尾保留过去的位置,整个身体不只随当前朝向转动。 [renderer.ts:657](https://github.com/stackedlol/ascii-sharks/blob/c521e24a787e6d4cc460d4cf2b9815f25e4fe453/src/renderer.ts#L657) 在运动脊柱上叠加向尾部增大的传播波;[renderer.ts:680](https://github.com/stackedlol/ascii-sharks/blob/c521e24a787e6d4cc460d4cf2b9815f25e4fe453/src/renderer.ts#L680) 根据转弯收展左右胸鳍。[renderer.ts:761](https://github.com/stackedlol/ascii-sharks/blob/c521e24a787e6d4cc460d4cf2b9815f25e4fe453/src/renderer.ts#L761) 用远近系数、部位灰度及实体格底色表达层次。 本次独立实现了 18 节位置历史脊柱,保持段长,限制每关节弯角并平滑插值;渲染时再叠加覆盖整个躯干、向尾部增强的传播波。胸鳍围绕各自根部摆动和收展,转向时左右不对称;尾鳍围绕尾柄摆动,并有鳍叶弯曲。附属部位绑定到身体的固定位置,变形后同步旋转法线。游速参与摆尾节奏,转弯有轻微身体倾斜。 字色改为 32 档朝向光照与 16 档实际相机深度。远处混入浅色有上限,仍保留阴影与可读对比。没有复制上游的实心方块、ASCII 字符密度变化或随机色闪;鲸体继续只由用户文字组成。 旧版运行 `node --test tests/rig.test.js` 三项实际失败:无独立转身历史、躯干一个周期仅摆约 0.83px、胸鳍尖约 3.6px。0.4.0 后通过,并另测脊柱段长、关节限弯、鳍根连接、法线及弯曲时完整表面覆盖。所有数值检查只说明代码行为,不作为真实窗口动感的视觉验收。 ## 0.5.0 海面俯视与背部体积 用户明确界面代表海面,观察者位于海面正上方。此前版本使用斜侧视相机,还把身体弯曲放在纵向和高度构成的平面中。旧版 `overhead.test.js` 的五项检查有四项失败:高度进入屏幕平面,尾鳍横向跨度从 0.96 被压到约 0.382,背部顶面色调 0.812 比侧面 0.280 更浅,可见身体样本只有约 60.5% 属于背部。 0.5.0 固定相机垂直向下。模型 x 为纵向、y 为高度、z 为横向;相机映射到屏幕 x/-z 和深度 y。相机不随时间自动俯仰或偏航,heading 只控制鲸鱼在海面内转向。脊柱的历史曲线重新绑定到物理 x/z 平面,真实 y 高度独立保留,因此身体跟随仍在俯视画面里可见,双侧胸鳍和尾鳍完整展开。附属鳍的关节仍在三维模型中运动,法线同步旋转。 在相机变换前保存真实隆起高度,字色以高处浓、低处淡的水下可见度规则塑形。中央隆起和背鳍更深,两侧渐浅;表面朝向只提供适度侧向差异,细小斑纹减弱,避免遮盖体积。整条鲸鱼的水深仍另外控制远近浅化。这个深色凸起的表现是用户指定的艺术方向,不等同于将普通漫反射光照顶面提亮。 新增七项俯视检查涵盖高度与海面平面分离、完整横向跨度、背部实际 RGB 更深、高度与色调单调关系、相机固定、只显示背面及水面内身体跟随。保留适度透视,高度仍可通过透视比例产生轻微径向缩放,不是正交投影。代码与数值检查不等于真实窗口视觉验收。 ## 验证界限 修复前的回归检查实际失败,捕获相邻格汉字重叠、默认文字过淡和无持续转向状态。模型、完整表面覆盖、字格间距、运动范围、边界、避让和固定步长检查只验证实现行为。它们不能证明真实中文字体显示、画面美观或设置点击交互。 本会话没有可控制的现有浏览器入口;不使用独立模拟页面或自制图像渲染器代替 DSH 画面验收。安装后另外核对原生配置、版本和 Client 注册。 上游克隆未找到 LICENSE、LICENCE 或 COPYING,未确认可再分发许可。本次仅学习上述算法思路,未复制上游源码或素材进发行包。 ## 0.6.0 真实鲸类轮廓与上下推进 0.5.0 只有一套体型。蓝鲸胸鳍的根尖直线约占模型总长 34%,更接近长胸鳍鲸类;统一吻部又在尖端收成一点。身体半径每个控制站都使用零端点导数的 smoothstep,容易形成逐段鼓起。尾叶用细长椭圆鳍片,无法明确控制前后缘与中央缺刻。 本次读取 NOAA 航拍照片及 Segre 2022 图 1 的真实俯视照片板,对照 Naturalis 鉴别资料,新增蓝鲸、座头鲸、抹香鲸、长须鲸、弓头鲸。原文支持的比例与未经标定的绘制参数分别记录在 [WHALE-REFERENCES.md](./WHALE-REFERENCES.md)。[species.js](../../src/species.js) 集中放置体型及运动参数;默认混合鲸群,原配置缺少 species 时自动补默认,不改变已有文字、色调或浓度。 [model.js](../../src/model.js) 改用保持轮廓单调的三次插值,分别定义头部、躯干和尾柄;胸鳍长短与后掠各自控制。尾叶由前缘、后缘、尾展与中央缺刻生成;曲面各环规则排列,保留完整末环。无背鳍的弓头鲸不生成背鳍,抹香鲸和弓头鲸不绘须鲸喉褶。表面仍由用户原文填充字格。 [Smithsonian 标本资料](https://qrius.si.edu/browse/object/10026911)确认鲸类上下拍尾并用胸鳍转向。旧关节在海面 x/z 平面叠加幅度沿尾部增大的传播波,导致正俯视时像鲨鱼左右蛇行。[rig.js](../../src/rig.js) 现将转向位置历史留在 x/z 平面,推进改为 x/y 的尾柄起伏、尾叶整体俯仰及小幅弹性滞后。尾叶左右同步上下,前躯幅度较小;胸鳍仅小幅调姿,转向倾斜受限。法线随各关节旋转。相机保持固定正俯视。 [motion.js](../../src/motion.js) 的基础巡游参数由 50 至 68 调为各物种 18 至 29,转速基准由 0.80 至 0.98 rad/s 调为 0.18 至 0.25 rad/s,并延长目标与随机游走的变化时间。摆尾使用平滑包络,随后进入持续前进的滑行。以上均是对话背景的艺术设定,不是生物学统一实测值;参照研究并不支持把所有大鲸描述为极慢或不会敏捷转弯。 独立复核发现并修复两项新几何问题:尾叶参数绕向导致两面法线向内,现修正绕向,并验证俯视选中上表面;7 至 8 条鲸时背鳍预算只够两环,漏采最宽中段,现每片至少分配 24 点,验证 6 至 8 条鲸的完整横截面。另补上预览从浏览器往返缓存恢复后的重新挂载。 原 43 项检查在修改前全部通过。其中“必须较大横摆”“胸鳍大幅拍动”和快速回转时间窗与本次需求相反,现改为验证较小的垂直躯干起伏、胸鳍小幅调姿、克制的历史跟随和较长的完整回转窗口。新增 [species.test.js](../../tests/species.test.js) 的七项检查,涵盖五物种及配置兼容、俯视轮廓、上下拍尾轴、左右尾叶同向、鳍根连接、单位法线、尾叶上表面、拥挤预算与移动滑行。七组共 50 项通过。 [whale-preview.html](../../preview/whale-preview.html) 使用与插件相同的真实渲染器;原版对照读取改动前本地备份。浏览器截图可以验证中文字体与字格中的造型。另在隔离的真实 Chrome 中验证五种鲸类切换后均有实际 Canvas 像素、暂停后画面稳定、恢复后变化、减少动态效果时静止、390px 窄视口仍有鲸体,并模拟 pagehide/pageshow 的往返缓存恢复;未捕获页面运行异常。模拟恢复事件的通过不等于已复现浏览器真实缓存命中。当前 DSH 页面另有认证,未对运行中的 DSH 做最终叠层观感验收,也没有把独立预览当作现有 DSH 画面。 ## 0.7.0 正文背后的圆角毛玻璃 本版要解决的是「鱼和正文文字混在一起」。先确认绘制顺序:鲸鱼画布注册在 `shell.overlay`,其容器是 `.BynINW_overlayLayer{z-index:20;pointer-events:none;position:absolute;inset:0}`,位于正文之上。因此**面板不可能真的垫在文字下面**,只能作为主题层压在文字上,这也决定了整个设计。 面板几何取自 DSH 公开的 DOM 钩子,任何缺失都会退化为纯鱼群背景而不报错:滚动视口 `[data-conversation-scroll]`、内容列 `[data-chat-flow]`(没有时退回 `--dsh-chat-content-width` 居中估算)、`[data-ds-dark-theme]` 判断明暗。所有读取都只在几何可能变化时进行(400ms 节流,滚动、缩放、可见性变化时立即失效),复用同一批数组,不逐帧查询 DOM。 第一版把「同样的字」再画一遍到低分辨率缓冲,结果在 11px 下糊成噪点,几乎看不见鱼;第二版改用字格覆盖度画方块,出现明显台阶;第三版才定为剪影:合并相邻格、边缘按 `glassDistance` 羽化,配合 `ctx.filter = blur()` 的真实高斯模糊。模糊在**低分辨率缓冲内**执行(半径除以降采样倍数),因此开销与窗口大小无关,实测 1440×900 / DPR 2 下稳定 60fps;不支持 canvas filter 时自动退回纯降采样糊化。 关键发现(测试驱动):面板叠在文字之上,任何较高不透明度的填充都会直接吃掉正文对比度。0.5 alpha 的底雾就能把黑字白底从 21:1 压到约 3:1,换任何颜色都救不回来。因此 `mist` 被可读性预算卡死在 0.24 至 0.30,材质感改由描边、顶部高光、底部阴影和「里面被模糊」来承担。新增检查按 WCAG 相对亮度公式,对明暗两套主题 × 0/0.55/1 三档强度计算最坏对比度并断言 ≥ 7:1;实测最低 7.41:1(深色主题满强度)。这条检查在开发中真实拦下了 6.78、4.27、6.16 三版不合格参数。 验证范围:`glass.test.js` 10 项覆盖面板几何、视口裁剪、圆角签名距离与羽化、强度单调性、缓冲区上限、圆角路径、设置兼容与对比度。全套 60 项通过。另用与 DSH 同构的 DOM 仿真页(相同的 `data-conversation-scroll` / `data-chat-flow` / `data-composer-seat` 嵌套)在无头 Chrome 中截图明暗两套主题、滚动后、窄窗口四种状态,并验证 `prefers-reduced-transparency: reduce` 时正文区海水被完全清空(该处 alpha 全为 0)、页面无异常。仿真页只能验证几何与像素行为,**不能替代现有 DSH 窗口中的最终观感验收**。 并发修改说明:本版实现期间,工作区里有另一进程同时加入了 `renderStyle`/`fillMode` 与 `surface.js`,与本版改动在同一批文件上交错。已核对两者可共存(玻璃绘制委托给新的 painter 接口),并补齐了对方新增两个字段缺失的中英文案与提示,否则设置页会出现空标签。 ## 0.8.0 可选字格底色和 ASCII 填充 用户提供原作者画面,要求把字格底色和字符来源做成插件设置选项。已重新阅读固定提交 renderer.ts 的字格采样、实体与轮廓判定、填色和字符选择。证据在 [ASCII-RENDER-NOTES.md](./ASCII-RENDER-NOTES.md)。上游实体以 RGB 色阶铺底,再叠字符;少量彩色前景按时间变换。它的字格是长方形,圆背为二维解析式,均不能从截图直接推成三维材质或随机彩虹底色。 新增独立 `renderStyle=glyphs|cells` 与 `fillMode=custom|ascii`,默认 `glyphs/custom`,不改写原文字。自定义模式保留原有分字与去空白规则。ASCII 使用独立密度串 `.:-=+*ox%#@`,明暗决定疏密,不写入粒子上的用户文字。 [surface.js](../../src/surface.js) 中的共享 painter 复用已有三维表面明暗、字格覆盖及深度结果。内部覆盖至少 0.6 且中心命中才铺格,边缘只绘浅字符。色表有 32 档明暗、16 档水深及四套小幅色偏;色偏锚定身体位置,不按时间跳色。字符前景考虑底色与整体透明度的合成结果,再选择深浅。底色和字符服从同一玻璃羽化,不穿透阅读区。当前玻璃几何和对比度预算保持原实现。 新增静态样例绘制,设置草稿改变时更新,不启动 RAF;ResizeObserver 随卸载清理。ASCII 时仅禁用文字输入,不删除草稿文字。预览页改为四组合等长对照及可控制实时场景,使用正式 painter。 68 项检查全部通过,其中 [surface.test.js](../../tests/surface.test.js) 新增八项覆盖配置兼容、四组合、动态ASCII选字、字体格尺寸、稳定色表、轮廓与玻璃淡出、合成对比和分数DPR。隔离 Chrome 实测四种组合均有实际像素,彩色字格显著增加实体覆盖;另检查明暗主题、磨砂区、暂停恢复、减少动态效果、减少透明效果、窄窗口及模拟往返缓存恢复。生成客户端在真实 React 设置仿真环境中完成选择、保存、重新载入、自定义中文和emoji保留,以及静态样例显示;无页面运行异常。复核另发现设置静态样例及减少动态效果背景缺少主题更新触发,现通过监听 `data-ds-dark-theme` 在主题变化时同步重绘,卸载时断开监听;设置样例的即时换色另外进入真实浏览器检查。独立环境不能替代当前 DSH 窗口验收。本次不修改运行中 DSH 的安装和个人配置。 ## 0.9.0 鲸鱼大小、全滑块设置与「选模型刷新鱼群」根因 用户反馈两个现象:设置里选项散落、希望都能用滑块调整;以及**在输入框切换模型时背景鱼群会整体跳回初始位置**。后者的根因不在本插件代码,而在 DSH 的设置同步路径,因此先做源码核对再改实现。 ### 根因:设置文档更新会把每个配置表单的 value 换成新对象 在本机 DSH 安装包 `resources/app.asar` 内解包读取的源码(副本保存在 `.scratch/asar-all/`,与安装版本一致): 1. 选模型:`dsh-api-session-controller__lib__index.js:793-812` 的 `selectModel()` 在把选择装到当前 Session 之后,还会 `this.ctx.agentDefaultModel.saveSelection(selected)`(:809)——把默认模型写进 profile 配置。 2. 落盘:`dsh-agent-default-model__lib__index.js:53-65` 的 `saveSelection()` 调 `configEditor.edit(entry, ...)` 写入 profile。 3. 广播:`dsh-settings__lib__index.js:413-464` 的 `describe()` 发现条目 raw 变化后 `emit("settings/document-updated", ...)`(:435,条目被移除时 :461 同样广播)。 4. 客户端重读:`dsh-client-ui-settings__lib__client.js:1485-1503` 订阅该事件并 `mirror.load()`;`SettingsDescribeMirror.run()`(:1435-1470)把整份 describe 结果换成新 `view`。 5. 每个表单重新派生:同一个文件里的 `ConfigFormController.derive()`(:1209-1231)在每次 mirror 变化时 `store.update(draft => { ... draft.value = decoded; })`。`decoded` 就是本次 describe 解出来的新对象,**哪怕插件自己的设置一个字没变,`snapshot.value` 的引用也变了**。 6. 插件旧实现按引用判断:0.8.0 的 `settings.js:44` `React.useMemo(() => normalizeConfig(snapshot.value), [snapshot.value])` 得到新 config 对象;`settings.js:19` 的 `useEffect(..., [config])` 于是卸载重挂整层,`engine.js` 里的 `createScene` 重建鲸群,所有鲸鱼回到初始布局。 这也解释了为什么其它设置文档写入(换主题、切界面语言等)会触发同类现象,以及为什么单纯在插件里监听 `enabled` 之类的开关无法规避。修法只能是:**按值比较配置**,相同则完全不动,不同则原地更新并在结构项变化时迁移运动状态。 ### 实现 `model.js` 新增 `configSignature(value)`(归一化后的稳定 JSON 指纹,与对象引用无关);`FIELDS` 新增 `whaleSize`(0.55–1.55、step 0.05、默认 1),Host schema 与设置页由 FIELDS 自动派生,旧配置缺字段时 `normalizeConfig` 补默认,不改变画面。 `engine.js` 新增 `createWhaleLayer(canvas, config)`(返回 `{ update, dispose }`)与 `diffWhaleConfig(previous, next)`;`mountWhales` 保留为兼容入口。`update` 先比较取值,五组全等时直接返回,不测字体、不建深度网格、不建场景、不打断 RAF。分组:`count/text/species/whaleSize` → 重建鲸群并迁移状态;`glyphSize/fillMode/renderStyle` → 重算字体、字符集、字格尺寸;`color` → 重建配色;`glass/glassStrength` → 重测玻璃几何;`speed/opacity/quietCenter` 等 → 绘制时读取,下一帧生效。引擎持有唯一 config 对象并原地 `Object.assign`,painter 在 paint 阶段读 opacity/quietCenter,所以这些项不需要重建。 `motion.js` 新增 `adoptSceneState(previous, next)`(纯函数):按 id 迁移 `x/y/heading/turn/speed/vx/vy` 与整份 `_motion`(浅拷贝,不与旧场景共享);rig 先 `resetWhaleRig` 再复制旧节点并用 `updateWhaleRig` 重新锚定到新体长,避免大小变化后有一帧使用旧脊柱。 `whaleSize` 只乘体长:`whale.length = Math.min(560 * max(1, size), 公式值 * anatomy.size * size)`。默认 1 时上限和公式都与 0.8.0 逐值相同;大于 1 时上限随尺寸放宽(否则宽窗口下 580px 已撞旧上限,滑杆超过约 1.1 会失效)。粒子预算 `min(8000, round(count*2200*max(0.36, size²)))`,默认 1 与旧值完全一致;体长、避让半径、边界回旋和采样密度随尺寸联动。 `settings.js` 全部选项滑块化:数值项用原生 `input[type=range]` + 实时读数 + 最小/最大刻度;枚举项用一条覆盖档位序号的 range + 轨道下方档位文字(可拖、可点、方向键、Home/End 均沿用原生语义,`aria-valuetext` 为当前档位);布尔项沿用 switch;只有「鱼身文字」保留文本框。`Overlay` 的 canvas 只挂载一次,配置变化走 `layer.update(config)`,并用 `configSignature` 做 memo,内容不变连 update 都不会触发。静态样例用 `requestAnimationFrame` 合并连续拖动。 ### 验证范围与结果 单测:`update.test.js` 10 项覆盖配置指纹只认取值、`diffWhaleConfig` 五组分类与全等 no-op、默认尺寸粒子数与 0.8.0 逐值一致、尺寸预算与线性体长、`adoptSceneState` 不串 id、DOM stub 下「相同快照完全 no-op / opacity 真变原地重绘 / text 变化后逐点坐标不变 / reduced-motion 下 update 立即重绘 / dispose 后调用无害」;另有「未布局时重建仍落位正确」的测试做过变异验证(改回画布坐标空间即失败)。全套测试与 `npm run check` 结果见 README。 浏览器验收(真实 Chrome 155 + CDP,仿真 DSH `shell.overlay`,非真实 DSH 窗口,共 175 项断言全部通过): - 基线 0.8.0:初始引擎挂载 1 次;发出一次**内容完全相同**的设置快照后挂载 +1、dispose 复位 +1、64×36 缩略指纹距初始布局 **0.000**(先前游动 3 秒距初始 3.102,噪声下限 0)——鱼群精确跳回起点。证据 `.scratch/v090/baseline-0.8.0.json`。 - 0.9.0:连续 4 次内容不变的设置快照,挂载次数与 dispose 复位 **Δ 均为 0**、canvas 节点不变、逐次连续性指纹差 0.402/0.458/0.387/0.372(同页 300ms 正常漂移的标定阈值为 1.46),4 次后距初始仍 3.2,没有回到起点。opacity 0.6→0.35 不重挂,墨量 alpha 总和 924,302→537,405(比值 0.58,理论 0.583);whaleSize 1→1.55 与 count 3→5 不重挂且 500ms 后仍有两帧差异。设置页:15 行、11 个 range(7 数值 + 4 枚举)、0 个 select、13 个档位标签,方向键步进与读数同步,保存/刷新持久,ASCII 时文本禁用且保留,390px 无横向溢出。预览页:四组合、五物种、三档体长的墨量 5.66M/7.02M/9.42M 严格单调(最大/最小 1.66),暂停冻结、减少动态静止、窄屏与深色均有像素,运行期异常 0。证据 `.scratch/v090/final-validation.json`、`overlay-0.9.0.json`、`settings-0.9.0.json`、`preview-0.9.0.json`。 边界:以上是**独立仿真环境**的实测,真实 DSH 窗口中的叠层观感、玻璃磨砂和单位时间帧率仍未验收。解包源码只用于定位机制,未复制其代码进插件。 ## 0.10.0 输入框毛玻璃与鱼群避让输入框 用户要求:输入框也能变成毛玻璃(做成可开关的设置项,默认关以免影响可读性),并新增「鱼会自动避让输入框」选项,两者互斥。 ### 为什么玻璃必须由插件画布绘制 先从本机安装包解包源码确认输入框的 DOM 与层级: - 输入框卡片 `[data-composer-card]`(`dsh-client-ui-conversation__lib__client.js:18987-18990`),圆角取 `var(--dsw-radius-panel)`、背景 `var(--dsw-specific-input-major)`; - 外层座位 `[data-composer-seat]`(:17855-17860)是 `position:sticky; bottom:0; z-index:7`,自带自上而下的透明→底色渐变; - 插件画布注册在 `shell.overlay`(`z-index:20`),**画在输入框之上**。 所以给 DOM 元素加 `backdrop-filter` 无法模糊画布上的鲸鱼(画布是前景而不是背景)。最终沿用阅读区玻璃的做法:由插件画布在卡片位置画一块同材质的磨砂面板——框内锐利字符淡出,只留模糊后的鲸体剪影,再加描边、顶部微光和底部阴影。 ### 实现 `glass.js` 新增 `glassComposerPane({ canvas, card, radius })`(页面坐标转画布坐标、四边裁进画布、圆角 clamp、过小返回 null、`feather:14`)与 `glassComposerTone`:材质主体(tint/veil/mist/silhouette)与阅读区共用同一对比度预算,只把顶部高光带 crest 的透明度降到 0.22 倍、高度限制在 18px。这样做的原因是把顶部高光和底部阴影也纳入 WCAG 最坏情况后,原阅读区的 crest 强度盖在 14px 的输入文字上只有 2.69:1;0.22 倍时最坏 4.57:1,满足小字号 AA(4.5:1)要求。 `engine.js` 的 `measure()` 重构为阅读区与输入框独立测量:`composerGlass`、`composerAvoid` 任一开启都会找卡片(缺失时退回座位),`diffWhaleConfig` 增加第六组 `composer`;只有阅读区玻璃关闭也能单独开输入框玻璃。`draw()` 的字形淡出同时考虑两块面板(各自 feather),先画阅读区再画输入框;减少透明效果时**不画材质、但仍把字形挡在面板外**——这是 0.9.0 的可读性行为,开发中一度按“clear 时连淡出也不做”实现,复核时发现那会让鲸鱼重新压到正文与输入框上,已改回。 `motion.js` 新增 `setSceneObstacles(scene, obstacles)`:画布坐标的软障碍,安装时立即把身体夹到障碍外(减少动态的静态帧同样正确),集成循环里由接近距离产生法线与切向转向力并适当减速,路线目标落在障碍内时改到障碍上方最近点。 ### 避让要按“身体”而不是“中心”算 第一版只夹鲸鱼中心,实测(真实 Chrome 跑生成物,60 秒等效)仍有最多 133 个字格中心落进卡片、卡片区域 ink 最高 130,504、减少动态静态帧 ink 38,682——中心在卡片上方 39px 时,几百像素长的身体、尾叶和胸鳍照样压进输入框。观感上不成立。 第二版按鲸体采样:沿朝取 5 个站点(尾叶、尾柄、中段、胸鳍根、吻端,覆盖 `-0.536L` 到 `+0.464L`),每站带该部位的轮廓半宽(尾展、胸鳍尖、体半径)再加 12px 字格余量。任何一站进入「卡片 + 该站半宽」的软边界,就按最深穿透计算平移与转向,把整条身体挡在卡片外。这样即使鲸鱼贴着卡片上方游过,字格中心也不会落进框内。 ### 验证 单测 `composer.test.js` 10 项:面板几何/裁切/圆角/过小、输入框材质对比度(明暗两主题 × 0/0.55/1 三档,最坏 ≥4.5:1)、互斥归一化、障碍安装/清除、三档体型静态帧与 60 秒身体采样点不进入、resize 后跟随、关闭后与孪生场景逐值一致。另有 `update.test.js` 的 DOM stub 用例验证“单独开输入框玻璃也能画”“字形只在面板内淡出”“双面板叠加不重挂”“减少透明时无材质但字形仍被挡在面板外”“避让模式下画布不把字形画进卡片、关闭避让的对照会画进去”。 浏览器验收(真实 Chrome 155 + CDP,仿真 DSH `shell.overlay` 与同构输入框,非真实 DSH 窗口,共 215 条主断言 + 7 条诊断断言全部通过,`.scratch/v0100/final-validation.json`): - 输入框玻璃:卡片区域内画布 ink 15,276 → 1,012,959(+997,683,84,560 个像素变化),卡片外 changedPixels **0**,关闭后回落 **0**;减少透明效果下卡片内材质 ink 降到 **360**,圆角面板内锐利字形 **0**、面板外仍有 180 个;全程 mounts/resets Δ=0、canvas 节点不变。 - 输入框避让(尺寸 1、3 鲸、glass=false 口径,speed=2 × 30,111ms ≈ **60,222ms 等效**、30 次采样):字格中心进入卡片 **0**,卡片区域 ink **0**、painted **0**(上限为卡片面积 0.5%);减少动态静态帧同样 **0/0**;关闭避让后 36.5 秒恢复穿越。鲸体在卡片上方 170px 内仍有大量墨量(近区 ink 峰值 291,560),说明不是“没有鲸鱼经过”而是确实绕开。 - 尺寸 1.55 双口径:几何字格数(fillText 落点)**0**,画布可见鲸体像素 ink/painted **0**;渲染侧对卡片区域另有遮罩兜底,几何与可见两个口径已分开记录。 - 设置页 127/127:17 行、11 个 range、13 个档位标签;两个新开关互斥(开毛玻璃→避让 false,开避让→毛玻璃 false)、保存快照正确、刷新持久、390px 无溢出。预览页 45/45:假输入框 620×120/圆角 24/白底/底部居中,开毛玻璃 ink +1.04M,避让时卡片区域 ink 0。0.9.0 的无重挂、滑杆、暂停、减少动态等回归全部通过。 边界:输入框位置、圆角、sticky 行为来自源码与 DOM 钩子,仿真页按同构结构模拟,**真实 DSH 桌面窗口中的观感未验收**;玻璃在文字之上,任何强度都会轻微降低输入文字对比度,所以默认关闭。 ## 实现与运行范围(0.10.0,自 README 迁入) [species.js](../../src/species.js) 集中定义五种体型及动作参数。[model.js](../../src/model.js) 构建表面、投影与字格覆盖,并用 `configSignature` 只按取值生成配置指纹;[motion.js](../../src/motion.js) 推进巡游、转向和滑行,提供 `adoptSceneState` 在重建鲸群时迁移运动状态,并用 `setSceneObstacles` 把输入框当作软障碍;[rig.js](../../src/rig.js) 将海面内转向与上下尾拍分别计算,并保持鳍根连接。[shading.js](../../src/shading.js) 处理背部隆起与水深字色;[glass.js](../../src/glass.js) 计算阅读区与输入框两套圆角面板几何及明暗材质;[engine.js](../../src/engine.js) 测量字体、绘制 Canvas,并以 `createWhaleLayer` 提供原地更新;[settings.js](../../src/settings.js) 保存 DSH 原生设置。 相机保持固定正俯视,模型 x 为纵向、y 为水深、z 为横向。隆起处使用深色文字,两侧和深水处较浅。相机保留适度透视,高度会造成轻微径向缩放。 纯字符模式每格只绘一个字符;彩色字格模式在内部先绘带间隙的底格,再绘字符。自定义文字按实际字体测量值保留间距;彩色字格和 ASCII 使用可容纳当前字体的方格。[surface.js](../../src/surface.js) 共享字符选择、色表、字格绘制与静态样例实现。曲面截面规则排列,背鳍在高数量预算下保留中间截面。 `createWhaleLayer` 按 `diffWhaleConfig` 把选项分成六组:数量/文字/鲸类/大小只重建鲸群并迁移运动状态,字号/填充内容/渲染样式重建字体与字格,色调重建配色,阅读区玻璃与输入框选项重测面板/障碍几何,其余项(浓度、速度、减淡、留白、开关)绘制时直接读取。内容完全相同的配置不做任何工作。 模拟以 1/60 秒推进,绘制最多 30fps,DPR 最多 2,画布最多 1200 万像素,表面样本最多 8000。复用投影、字格、脊柱和色表,不逐帧更新 React。没有进行实际 DSH 窗口帧率测量。 隐藏页面暂停;系统减少动态效果时显示静态姿态。离开对话页面或关闭插件时清理 RAF、尺寸观察器及监听器。装饰层不接收点击,中央适度减淡,底部淡出。保留软避让及提前边界回转,软避让不保证鲸体永不交叠。 桌面版跟随 DSH 公开的侧栏宽度和顶部留白。普通浏览器可调整**浏览器左侧留白**。当前接口没有任意右侧面板或弹窗的完整边界信息。 ## 0.11.0 内容框毛玻璃与「圆角不统一」根因 用户对 0.10.0 的反馈有两条:输入框毛玻璃「还是没有毛玻璃效果」,以及「圆角没有统一,有点难看」。前者是设计问题,后者是实测可定位的渲染缺陷。 ### 根因:两条近重合的描边在四角错开 用安装版 0.10.0 的 client.js 在 DSH 忠实仿真页(1920×1040、DPR1.25、`[data-composer-card]` 1032×118、radius 28、白底 + `.5px` 中性 elevation ring)复现后测量: - 0.10.0 的 pane 矩形与 `[data-composer-card]` 完全一致(画布坐标 x=112、y=710、1032×118、radius 28,四边误差 0),说明玻璃没有内缩; - 但 pane 在卡片自身 `.5px` 中性 ring 之上又画了一条 1px 偏蓝 `rgba(24,34,48)` 描边,峰值偏移左 0px、上 +1px; - 对用户原图做行扫描与圆弧拟合,外弧半径约 28.5px、颜色中性(卡片 ring),内弧约 33.4px、偏蓝(插件描边)。两条线在直边上并成一条,在 125% 缩放的亚像素错位下于四角分开——这就是「两圈不同半径的圆角」。 同时,0.10.0 只给输入框做了玻璃,消息卡片里仍有大量锐利字形落点(复现中单卡 3221 个),白卡上的极淡剪影几乎不可见,所以观感仍是「没有毛玻璃效果」。 ### 实现 `glass.js` 删除原「整块阅读区」与「单独输入框」面板的几何/材质路径,新增 `glassContentPanes({ canvas, boxes, max })`:每个候选框转画布坐标后只做裁边,面板矩形严格等于裁进画布后的框矩形(不内缩不外扩),半径逐块取自 DOM,feather 10;近重合去重、嵌套保留、超限按面积保留。材质 `glassContentTone` 复用 tint/veil/silhouette 与 `glassStrength` 强度曲线,crest 高度/透明度与底部 shade 降低,**不再提供 stroke**。 `engine.js` 的测量改为扫描 `[data-conversation-scroll]` 子树(最多 600 个元素,父先子后,先几何过滤再读计算样式)并追加 `[data-composer-card]`:可见、背景 alpha ≥ 0.9、有圆角、面积 2000 到画布 55%、高度小于画布 0.9、排除 fixed/隐藏元素。绘制改用独立低分辨率「海面」:每帧用同一个 painter 把整片字格场画进 factor 4–6 的小画布(不含 pane 淡出),做一次模糊;每块 pane `clip → 表面 tint → 模糊海面 → 顶部微光 → 底部阴影`,不画任何描边;框内锐利字形按 pane feather 淡出。无 pane 时不分配海面缓冲、不增加绘制;减少透明效果时不画材质但仍把字形挡在面板外。 避让(`composerAvoid`)与内容框玻璃互斥:同时开启时按玻璃优先解析;设置页开一项自动关另一项。0.10.0 的独立 `composerGlass` 字段保留为隐藏字段以兼容旧配置,读取时若旧配置只开过它而没有 `glass` 字段,则迁移为内容框玻璃开启。 ### 验证范围 单测:`content-glass.test.js` 覆盖面板几何/裁切/圆角/去重/嵌套/上限、海面缓冲尺寸与 factor、材质分层、**对比度预算**(cells 调色板最暗色与 ascii 深色、明暗两主题 × 强度 0/0.55/1,含顶部与底部渐变带,最坏 ≥4.5:1;实测浅色 4.88:1、深色 5.29:1)。`update.test.js` 的 DOM stub 断言每卡一 pane、`paneStrokes === 0`、每 pane 一条路径只用于 clip、一次模糊海面、两条渐变、玻璃关闭后字形回到框内且不重挂、减少透明时不分配海面;`model.test.js` 覆盖 hidden 字段、迁移、互斥与版本。 浏览器验收(真实 Chrome + CDP,DSH 忠实仿真页,非真实 DSH 窗口,见 `.scratch/v0110/final-validation.json`):面板与 DOM 框误差 ≤1px、边界只剩卡片自身一条中性 ring、框内无锐利字形且开/关玻璃的框内墨量差 >50%、正文对比 ≥4.5:1、8–12 个卡片时每帧绘制耗时中位数、设置页隐藏行/互斥/保存与刷新、减少透明与减少动态、预览页回归。具体数字以该 JSON 为准。 边界:候选框依赖「不透明背景 + 圆角」这两个 DOM 特征;透明底的卡片不会得到玻璃(避免误伤)。结论为仿真环境实测,真实 DSH 窗口观感仍未验收。 ## 未发布:滚动暂退改为可选(`pauseOnScroll`) 用户反馈「现在滚动会暂停渲染,效果我不喜欢」。0.14.0 的滚动阅读响应把「暂退装饰层 + 暂停仿真」做成了无条件行为,所以本轮不是修 bug,而是把这个观感决定交还给用户:新增布尔字段 `pauseOnScroll`,放在设置页「高级设置」内,**默认关闭**。 ### 实现 - `model.js`:`pauseOnScroll: { kind: 'boolean', default: false }`。旧配置没有这个键时按默认值走,即升级后滚动不再暂停;`index.js` 的 Host schema 由 `FIELDS` 自动生成,无需另改。 - `engine.js`:`onScroll` 的前半段保持不变——先做便宜的来源过滤,再只把 `readingTextDirty` 置真。开关关闭时到此返回,不再把 `measuredAt` 打成 `-Infinity`、不取消 RAF、不改 `canvas.style.opacity`、不安排 140ms 恢复计时器。文字几何因此仍会刷新,只是走 `draw()` 原有的 120ms 测量预算(`MEASURE_INTERVAL`),一次滚动突发最多一次文字扫描,而不是每个事件一次。 - 关闭时若系统正在「减少动态效果」(动画循环本来就停着),滚动会调用 `onForegroundChange()` 安排一帧合并后的静态重绘,否则遮罩会停在滚动前的位置直到别的东西变化。 - 开启时行为逐字保留:立即隐藏、暂停仿真、静止 140ms 后 `sync()` 重绘再 120ms 淡入。 - 新增 `leaveScrollPause()`(只停止让路:清计时器、`scrolling = false`)与 `revealLayer()`(只恢复画面:设置 transition 与 opacity),`finishScroll` 与 `update()` 共用。原来的恢复顺序是「先隐藏着重绘,再淡入」,避免用户看见遮罩跳位;两个函数拆开后这条顺序在两条路径上都保持不变。在滚动突发中间把开关关掉时,`update()` 会走同一顺序当场恢复——否则被放弃的那条计时器是唯一会让海面重新出现的东西,图层会被永久留在隐藏状态。 - `pauseOnScroll` 归入 `LIVE_KEYS`:它只改变后续事件的处理方式,不重建场景、字体、调色板或玻璃分组。`update.test.js` 的「每个字段恰好落在一个重建组」断言覆盖这一点。 ### 验证范围 逻辑与命令流(`node --test`,与产品代码同一份 `src/`): - `media-occlusion.test.js` 新增三项。默认关闭时,10 帧 40ms 的滚动突发内 `canvas.style.opacity` 始终不是 `'0'`、没有安排恢复计时器、绘制调用持续增长;四次间隔 10ms 的事件合计文字扫描 ≤ 24 行(即至多一次扫描,证明走的是预算而非每事件)。诊断输出为 `{"geometry":140,"style":46,"ranges":44}`,即 10 帧约两次完整测量。 - 中途关开关:先断言隐藏态与一个待触发计时器,`update({ pauseOnScroll: false })` 后断言不再隐藏、计时器清空、当帧即重绘。 - 减少动态 + 关闭:滚动后确实安排了一帧合并重绘且没有恢复计时器。 - 原有三项暂退断言(`glow/blur/frost` 突发零布局读取、滚动中关文字效果、`wheel` 隐藏与释放)改为显式 `pauseOnScroll: true`,仍然通过,说明开启路径没有回归。 - `model.test.js` 断言默认值、非法值回落与 `FIELDS` 顺序;`settings.test.js` 断言该开关只出现在 `details.aw-advanced` 内、不在四个主分组里、默认 `aria-checked=false`、点击只改草稿、保存只写一条 `set pauseOnScroll`。 - `pnpm verify` 全绿:346 项通过(本轮前基线 342 项),含 `client.js` 与 `preview/whale-preview.html` 产物一致性检查。 指针与滚轮交互(ego lite 真实浏览器 + 真实 `mouse.wheel`,页面为正式产物 `preview/whale-preview.html`,最后一轮跑的是重新构建后的产物): - 关闭(新默认):10 次真实滚轮事件,`opacity` 采样 10/10 为默认值、0 次隐藏;画布像素指纹 9/10 帧在变,即滚动期间鲸群继续巡游。处理器内采到的 21 个滚动事件全部为可见(`hidden: 0, visible: 14`)。 - 开启:同样输入下,处理器内 21/21 个滚动事件读到 `opacity === '0'`(`hidden: 21, visible: 0`),即滚动期间装饰层确实暂退;静止 800ms 后恢复为默认可见且仍有像素。两条路径只差这个开关。 - 两种情况下 `settled` 的 `opacity` 都不是 `'0'`,即不存在把海面永久留在隐藏状态的路径。 - 证据文件:`.scratch/pause-on-scroll-20261024/round-0{2..8}.mjs` 与该目录下 `canvas-pause-off.png`、`canvas-pause-on.png`(由 `toDataURL` 导出的真实画布像素)。 设置页(真实 React 组件,`.scratch/settings-preview.html`,ego lite 真实浏览器):`[data-aw-field="pauseOnScroll"]` 位于 `details.aw-advanced` 内、与 `leftInset` 并列,四个主分组(外观/鲸群/阅读/海面)都不含它;`
` 默认折叠。`label` 的 `htmlFor` 指向开关 `id`,`aria-labelledby`/`aria-describedby` 与提示段落 id 一致,初始 `aria-checked="false"`。真实鼠标点击、聚焦后 Space 与 Enter 均可切换;切到英文显示 "Pause rendering while scrolling" 与对应说明;保存只提交一条 `{ op: 'set', path: ['pauseOnScroll'], value: true }`。证据 `round-09/10/11.mjs`。 边界与未验证项: - 本轮浏览器验收在独立预览页完成,**未在当前运行的 DSH 桌面窗口内验收**,也没有确认正在使用的插件来源与构建。 - `Page.captureScreenshot` 在该环境超时,画面证据改用画布自身像素导出;整页叠层观感(玻璃、正文对比度)本轮未重新测量。 - 前几轮脚本的失败是几何问题而非产品缺陷:`#stage` 的 `overflow` 为 `visible` 且 `scrollHeight === clientHeight`,它本身不可滚动,真实滚轮滚动的是文档;早期一次采样点落在视口外(`y = -370`、`top = -345`)。第四轮的事件计数与 `scrollIntoView` 修正后才是上表数据。 - 关闭状态下滚动期间的布局读取高于开启状态(开启时突发内为 0 次),这是把观感交给用户带来的直接代价;本轮未做逐帧耗时测量,不能据此推断设备帧率。 ## 1.0.0:铺满海面的方格(`seaGrid`) 用户提供两张参考图(「Ask the pond」与一张放大的字格地图),要求「让海面充满方格」:海面读起来是一张字格纸,鲸体的彩色字格落在同一套格线上。参考图来自其他站点,只作为观感目标;本轮没有复制其素材、字体或颜色。 ### 实现 - 新增 [sea-grid.js](../../src/sea-grid.js)。相邻字格之间描一条共享的 1 设备像素细线(只描内部边界,不额外给最外圈加框):坐标取整到半设备像素、`lineWidth = 1`、在单位变换下描边,因此分数 DPR 下也不会出现两条相邻线重叠。线色随主题(浅色 `rgb(36,54,70)`、深色 `rgb(203,219,236)`),浓度 = 基础值 × `opacity / 0.6`(浅色 0.105、深色 0.075,上限 0.16)。默认 0.6 浓度时白底合成值约 `#e8eaeb`(浏览器像素实测 232/234/235),与参考图取样值(2× 图片中约 `#ececec`)接近。 - 格距直接使用引擎现有的 `createDepthGrid` 字格:`grid.cellWidth × grid.cellHeight`。彩色字格模式的格是正方形(默认 16×16 CSS px),纯字符模式是字符格;字符格宽度跟随字体的实际度量,真实浏览器里微软雅黑 600 11px 约 15×16 CSS px,而产物测试适配器的假字体度量得到 13×16,两者都不是写死的固定值。鲸体格线与海面格线因此严格同源,而不是另起一套背景网格。 - 方格是海面底层,不读时间、不存随机种子,所以暂停、页面隐藏、减少动态效果都不需要额外状态;也不做窗口边缘淡出——水面延伸到窗口之外,只有会绕回另一侧的鲸鱼需要在边上淡出。文字附近沿用现有阅读保护:`readingProtection` 在文字行下 `destination-out` 擦除整层海面(含方格),模糊/毛玻璃再把低分辨率海面缓冲贴回。 - `engine.js` 在 `glassClipExterior` 之后、`drawAtmosphere` 之前画主画布;`paintBlurredSea` 在 `drawAtmosphere` 之前把同一层画进共享低分辨率缓冲,参数为 `scale = 1 / seaFactor`、`offset = seaPad`。设置页预览的 letterbox 通过 `scale = ratio`、`offsetX/offsetY` 传入。 - 设置页顶部的分组预览(`LivePreview`)把 1160×852 的对话视口按高度缩放进 1044×450 的画布,左右各留一大片空白。方格默认开启后,如果只在逻辑海面内描线,预览卡里会看到一个硬边格纸矩形;因此 `drawSeaGrid` 支持 `extend`,在 viewport 模式下以相同相位把格线延续到画布边缘。`glassClipExterior` 也相应接受一个可选 `extent` 矩形:预览传“整块画布反算回未缩放坐标”的矩形,对话覆盖层仍传原来的画布尺寸,行为不变。独立复审复现了这个硬边矩形,修复后用真实 `src/engine.js` + `viewport/demoCard` 复测左右两侧都有格线。设置页的预览里存在小数偏移,格线按“绝对设备坐标取整”,画笔的格缝按“逻辑坐标取整后再平移”,两者可能相差不到 1 设备像素;预览本身是缩放后的画面,这点偏差未再改公式,留给真机点验。 - `model.js` 新增 `seaGrid: { kind: 'boolean', default: true }`;归入 `LIVE_KEYS`(只影响下一帧,不重建场景);设置页放入「海面与天气」组;两语 locale、独立预览开关同步。`drawSeaGrid` 返回实际描出的线数,便于测试;非法尺寸、非有限值或浓度 0 时不描任何线。 ### 验证范围 逻辑与命令流(`node --test`,与产品代码同源): - `sea-grid.test.js`(9 项):内部格线数量与端点、1 设备像素与半像素对齐、letterbox 映射、逻辑范围外不描线、letterbox 预览延续到画布边缘、`Date.now`/`Math.random`/`performance.now` 被替换成抛错函数时仍能完成(证明不读时钟与随机)、浓度缩放与上限、主题墨色、退化输入不描线且不抛错。 - `sea-grid-artifact.test.js`(6 项,跑 `client.js` 与 `preview/whale-preview.html` 两份真实产物):两个入口逐命令一致;格距 16(彩色字格)与 13×16(纯字符,测试适配器度量);开关热更新不重建画布,两个入口同时生效;深色主题换成浅色线;共享海面缓冲(含 blur 前的低分辨率缓冲)确实收到方格,关开关后缓冲内也没有。 - `glass.test.js` 新增 1 项:`glassClipExterior` 的 `extent` 覆盖整块预览画布时,外圈起点从 `(0,0)` 变为扩展矩形起点,窗格仍随后追加,无窗格时不产生裁剪;不传 `extent` 时路径与旧行为一致。 - `model.test.js` 更新 `FIELDS` 顺序断言;`update.test.js` 把 `seaGrid` 归入 live 组;`rain-integration.test.js` 与 `update.test.js` 的玻璃统计用例显式关掉方格,避免把海面细线算进「雨丝/玻璃面板描边」的计数里。 - `pnpm verify` 全绿:421 项通过(本轮前基线 413 项),49 个测试文件,含产物一致性检查。 指针无关的画布像素证据(ego lite,真实浏览器加载正式产物 `preview/whale-preview.html`,用 `toDataURL` 导出画布像素;脚本与图片在 `C:/Users/Jerry/ego/ascii-whales-sea-grid-20261025/`): - 隔离测量(关玻璃、风纹、云影、尾流、雨、任务徽记):彩色字格模式竖向/横向格距均为 16 CSS px;纯字符模式真实浏览器为 15×16 CSS px(测试适配器假字体为 13×16,两者都跟随字体);单条格线像素剖面 `[255, 255, 227, 255, 255]`,即恰好 1 设备像素宽;关掉方格后同一区域检测不到任何格线(暗像素比例 0.141 → 0.018,剩余为鲸身字符)。 - 观感图:`final-light-open.png`、`final-light-glass.png`、`final-dark-open.png`、`final-dark-glass.png`,以及同帧开关对照 `final-light-open-nogrid.png`。毛玻璃面板内部重新取样没有周期性暗线(面板均值 245–249、无格线凹点),说明低分辨率缓冲里的方格经强模糊后确实不可见,面板仍保持原来的柔白。 - 设置页 letterbox(复用真实 `src/engine.js`,`viewport = 1160×852`、`demoCard`、画布 1044×450、DPR 2,脚本 `.scratch/sea-grid-final/letterbox.html`):修复前格线只覆盖逻辑海面(设备 x 431–1651),左右白色边距完全没有格线;修复后左右各约 45,000 / 44,000 个不透明像素落在画布两侧 20% 区域,格距与相位和逻辑海面内一致。复测时必须禁用 HTTP 缓存重新加载模块,否则浏览器可能继续执行旧的 `glass.js`,把修复“测没”(本轮踩过一次)。 - 参考图取样:2× 截图中格距约 8.3×13 CSS px、格线亮度约 236/255;本插件按自身字格取 16×16(彩色字格)或 15×16(纯字符,真实浏览器),不照搬参考字号。两者只作为「薄格纸 + 彩色字格」的观感对照,不主张像素级一致。 ### 未测 - **没有在当前运行的 DSH 桌面窗口内验收**:插件在 desktop profile 中为 `link:` 安装,`client.js` 已重建,但运行中的窗口是否重新加载取决于宿主(本轮未重启 DSH,也未重新加载页面);真实对话页上的网格与正文/玻璃叠层观感仍未测。 - `Page.captureScreenshot` 在该 ego lite 环境对任何页面都超时,因此只有画布像素证据,没有整页 DOM 叠层截图;设置页里「海面方格」开关的真实点击、真实 React 设置页的 letterbox 视觉与排版未在本轮点验(组件测试只证明它被渲染一次且保存为一条 `set seaGrid` 写入;letterbox 修复是在引擎层复现并复测的)。 - 未做逐帧耗时测量。每帧新增的描边为 `宽/格宽 + 高/格高` 条线段(1440×900、16px 格约 146 条),主画布与低分辨率缓冲各一次;没有据此推断设备帧率或移动端能耗。