dsh-ambient-gallery
onlyinliu/dsh-ambient-gallery
挂在主面板背后的一面照片墙:大小不一、各自倾斜、彼此留白。自带二十张真实风景照,首次启动就挂上墙;每个画框最多二十张图轮播,最下面三个框可以放本地视频,一个快捷键让视频退回照片轮播。
安装
dsh plugin --profile web add github:onlyinliu/dsh-ambient-galleryREADME
Ambient Gallery · 氛围图墙
主面板背后的一层装饰。开箱是一面照片墙:二十个框,每一张大小不一、各自倾斜、彼此留白,横向只落在面板中段;每个框里最多 20 张图轮播,自带 20 张真实风景照(首次启动自动铺好)。最下面三个框可以放你的本地视频。
所有图片与视频都只存在本机,不上传、不联网。
它做了什么
- 照片,不是格子:默认 20 个框,落位、尺寸、长宽比与倾角都是确定性抖动的结果。它们靠拒绝采样彼此错开,永不重叠,也从不共享同一水平线——像一面照片墙上的装饰,而不是等分网格。
- 开箱有内容:包里带着 20 张真实风景照(
assets/photos/,Pixabay,长边 720)。首次启动时复制进~/.dsh/ambient-gallery/media/并落成 20 个框、每框 3 张(偏移错开,相邻不重图),和用户自己挑的图走同一条路径。没有网络的机器上也是完整的墙——一张加载不出来的远程图不是留白,是墙上的洞。 - 不过大:单张上限 21% × 26% 的面板尺寸,纵使只放 3 张也不会长成一块板子。
- 只占面板中段:整面墙横向默认落在面板宽度的 20%–80% 之间(可在设置里各自调),左右两侧是阅读留白,边缘还有渐变淡出。
- 只有长方形:形状选择已经删掉(m10311),所有框都是长方形——一面照片墙该有的样子。
- 每张动态轮播:一个框里可以放最多 20 张图,按设定间隔交叉淡入(1.6s 过渡)。每个框的首帧延迟带确定性相位偏移,所以整面墙是「涟漪式」亮起,而不是整齐地闪。
- 满 20 张之后:再选图不会失败 —— 新图进、最旧的自动轮换出去(滚动窗口语义,两端都按
.slice(-20)执行)。 - 没有图也能看:某个框空着时,用 canvas 现场生成 3 张不同母题(光池/条纹/弧线)的渐变图,同一位置每次刷新都一致(种子来自框编号)。
- 视频只在最下面三个框(m10076):照片墙的二十个框里,只有最靠近底部的三个接受视频,其余框永远轮播静图。选中后视频在该框循环播放、保留声音。一个快捷键(默认
Shift+V,可重绑)暂停/恢复——按下去没有任何提示,暂停就是暂停,再按一次才播放,就是一个普通播放器的行为。暂停时那个框不是变黑或空掉,而是退回自己的静图(没有的话用生成图),照常跟其它框一起轮播,所以整面墙永远是满的。 - 浓度可调:0–100%。
- 底层、不挡操作:整面墙挂在
shell.overlay上,根节点pointer-events:none,每张照片低透明度 +mix-blend-mode,文字从上面流过依然清晰;它从不拦截点击、滚动或键盘输入(快捷键除外,那是它唯一主动监听的东西)。
装
dsh plugin --profile web add dsh-ambient-gallery
或从源码目录安装:设置 → 插件 → 安装本地包,指向本目录。
用(全部在设置页里)
设置 → 氛围图墙,一页搞定,没有浮层、没有弹出的小面板:
- 滑杆:照片数量(3–24)、左边界、右边界、浓度、模糊、间距、圆角、轮播间隔。
- 视频快捷键:点一下那个键位按钮,然后按下你想用的组合键(必须带 Ctrl/Alt/Shift/Cmd 之一)。
- 照片管理:每个框一行,列出它现在所有的图(缩略图 + 各自的
×),旁边是「加图片」按钮——只有最下面三个框会多一个「加视频」。删哪张就点它的×,只影响那一张。 - 底部:恢复默认、清空全部。
存储与接口
- 状态:
~/.dsh/ambient-gallery/state.json(config + 每个位置的帧列表) - 媒体:
~/.dsh/ambient-gallery/media/(导入时重命名为随机 id,客户端文件名只作为展示信息保留) - 首次启动(即
state.json还不存在时)会把包里assets/photos/的 20 张照片铺成 20 个框、每框 3 张。一旦state.json存在就不再 seed —— 你清空过这面墙,插件就该记得,而不是每次开机都跟你商量一次。 - 路由(Host 半,仅本机 loopback 可访问):
GET/POST /plugins/dsh-ambient-gallery/statePOST /plugins/dsh-ambient-gallery/import(原始文件体 +X-File-Name)GET/HEAD /plugins/dsh-ambient-gallery/media/<id>.<ext>(支持Range,视频可流式 seek)POST /plugins/dsh-ambient-gallery/remove?id=<id>
图片走 HTTP 而不是 localStorage/data URL,是因为手机照片几张就会撞到 5MB 配额,而视频 base64 进内存既慢又会重复持有。
兼容性
engines.dsh: ">=0.2.0-rc.2 <0.3.0-0",engines.node: ">=18"。四个 peer 依赖全部 optional。
已知取舍
- 图墙在
shell.overlay里,而宿主的 overlay 层(z-index:20)恒在各列之上、且中心列自带不透明底色 —— 所以「画在内容下面」在实现上做不到,只能用低浓度 + 混合模式做出视觉底纹的效果,同时保证永不拦截事件。这是刻意选择,不是遗漏。 - 没有「布置模式」这个开关。 之前的版本在角落放了一个圆钮,点开会在应用之上再压一条控制条——那既是多出来的一层,也是那次交互事故的来源。现在墙本身永远是
pointer-events:none,编辑只发生在设置页里。 prefers-reduced-motion: reduce时图墙自动隐藏轮播与整面墙。- 位置是确定性的(种子由照片数量与形状共同决定):同一组参数下每次刷新落位一致,换参数会得到另一面墙 —— 这样不会每次打开都在跳动。
- 快捷键是全局按键监听(宿主的 Client 没有 shortcut 服务),所以在输入框里打字也会命中它;这是刻意的取舍——一个「光标在输入框时就不灵」的快捷键等于不存在,而光标几乎总在输入框里。唯一让路的是快捷键录制器本身。
- 「视频已暂停」这个状态刻意不持久化:它是一次「先安静一下」的手势,如果存盘,下次开机会看到一面莫名其妙的静音墙。
- 三种场景模式已删除(m10311):照片墙是唯一的样式;旧的场景照片、
/scenes路由与样式都已移除。
开发
node --check lib/index.js
node --check lib/client.js
node tools/gallery-smoke.mjs lib/client.js # 离线:加载器 / 槽位 / 渲染 / 布局 / 界面断言
node tools/gallery-probe.mjs # 端到端:20 张上限、合并、clamp、模式与形状、快捷键、Range、视频
node tools/dump-places.mjs 5 8 12 # 导出 placements() 的真实几何到 tools/_places.json
python tools/preview-layout.py # 把上面的几何画成 PNG 版式预览(几何预览,不是截图)
gallery-smoke.mjs 的两段断言:
--- layout ---:不重叠(按旋转后包围盒)、单张不超过 21% × 26%、尺寸/倾角/顶边都不均匀、总覆盖面积 < 40%、横向带为 20%/20%、每个框都有一根吊线。--- surfaces ---:样式表里不存在.agg-panel与.agg-toggle(浮层确实被删了)、.agg-root仍然是pointer-events:none、场景类名一个都不剩、设置页里没有任何分段选择器、20 行缩略图、3 个「加视频」按钮、每个已列出的帧各有一个×。
assets/icon-180.png 来自 tools/make-icon.py(Pillow),它从 icon.svg 的几何重绘同一套五窗格与播放三角,所以图标是「一份设计,两种格式」,不会各自漂移。
装入 profile 后,Host 改动需要 touch lib/index.js 再 plugin_manager set_plugin enabled=false → true —— 只切开关不会让已加载的 Host 模块重新读盘。
License
MIT
随包照片来源
assets/photos/ 里的 20 张照片下载自 Pixabay(CDN cdn.pixabay.com),按 Pixabay Content License 使用:可免费用于商业与非商业用途、可修改、无需署名。这里不用作图库素材再分发,仅作为插件首次启动时的默认墙纸,随插件一起提供,长边缩到 720px、progressive JPEG。如果你要把这些换成自己的照片,直接替换 assets/photos/ 下的文件即可,seedShipped() 会在没有 state.json 时把它们铺上墙。