Mac Duo:合上 MacBook 盖子,屏幕内容跟着「翻倒」——用 Metal 实时渲染出 iPhone Duo 同款 3D 景深效果
iPhone 上那个「Duo」效果应该很多人见过:拿起手机的时候,锁屏画面不再是贴在玻璃上的一层平面,而是像一张照片立在小盒子里,随着手机角度变化产生透视、位移和景深。它好看的本质不是动画,而是把屏幕内容当成一个有厚度的物体,然后让它在三维空间里被观察。
那如果把同样的直觉搬到 MacBook 上会怎样?合盖本来是一个纯机械动作——屏幕「啪」地灭掉。Mac Duo 这个项目做的事情就是:在你合上盖子的过程中,捕捉屏幕上的画面,把它放进一个虚拟空间里,让它随着盖子角度一起倾斜、后退、模糊、变暗。你看到的不再是屏幕熄灭,而是画面「倒进」了机身里。
这篇介绍的就是今天 GitHub Trending 上的新项目 sumimakito/Mac-Duo(约 685 stars,Swift / macOS 14+,Apache-2.0 许可)。
它到底做了什么
用一句话概括:Mac Duo 读取 MacBook 的屏幕开合角度,并在合盖过程中用一个 GPU 渲染的覆盖层,把屏幕画面的内容实时绘制成有透视、有景深的样子。
具体流程是这样的:
- 从 MacBook 内置的屏幕角度传感器读取当前盖子张开的角度(0° 表示完全合上,正常打开大约是 130°)。
- 当角度降到设定的阈值(默认 90°)以下,效果触发。
- 用 ScreenCaptureKit 实时抓取屏幕内容,交给 Metal 渲染。
- 渲染器为画面施加透视变换、高斯模糊和压暗,画面仿佛向机身内部「退进去」。
- 数值随角度连续变化:越接近合上,模糊越强、越暗、透视越深。
- 效果层不拦截点击——鼠标事件会穿透到下面的应用。
它不是单纯的静态截图动画。项目有一个「Live rendering(实时渲染)」开关,打开时效果层里的画面会持续更新;关闭时则冻结在触发那一刻的那一帧画面——后者功耗更低,视觉上像「拍立得」被留下来的样子。
技术上是怎么做出来的
这个项目代码量不大,但每一块都挺讲究。仓库结构大致分为三部分:LidAngleKit(传感器读取)、MacDuo(应用主体与渲染)、lidprobe(传感器诊断工具)。
1. 读盖子角度:直接跟 HID 设备说话
核心在 Sources/LidAngleKit/LidAngleSensor.swift。它通过 IOKit 的 HID 接口,匹配苹果的屏幕角度传感器——usage page 0x20、usage 0x8A。有意思的是它的取值方式:
- Report 1:3 字节
[0x01, lo, hi],整数角度,范围 0–360,作为兜底。 - Report 7:5 字节
[0x07, b0, b1, b2, b3],小端序的百分之一度,能做到 0.01° 精度——但不是所有机型都声明这个 report,所以打开时先试 report 7,失败就退回 report 1。
传感器大约每 100ms 刷新一次,而且不需要任何权限。代码里还特意处理了一个坑:外接显示器可能带有相同 usage 的传感器但读数恒为 0,所以它会检查设备是否被 macOS 标记为内置(kIOHIDBuiltInKey),把外接屏排除掉。
2. 渲染:Metal + 高斯金字塔 + 单次全屏 pass
渲染在 Sources/MacDuo/DepthRenderer.swift。画面被放在一张带黑色边距的纹理上(边距固定 120pt,刻意大于最大模糊半径,这样模糊之后边缘是真正的黑,不会拖出脏边)。每帧对整屏做一次 render pass。
模糊用的不是逐帧重新算高斯,而是 MPSImageGaussianPyramid 构建的高斯金字塔——着色器里 DepthShaders.swift 负责取不同层级的模糊结果做混合,从而按「距离」给出连续景深。整个效果的关键参数被抽象成一个 Uniforms 结构体(透视矩阵、屏幕原点、模糊强度、形状、光照),每帧重新编码上传。
3. 画面来源:实时流 or 定格
ScreenCaptureKit 的部分在 ScreenStreamer.swift 和 ScreenSnapshotter.swift。为了不让效果出现「卡住」的断层,渲染器维护了一个「待采用」的最新帧(pendingFrame),谁先到用谁——如果实时帧先到就丢弃预先准备好的定格图,因为它更新。
4. 防卡顿:预热(pre-warm)机制
合盖是个快速动作,如果等到触发阈值才开始抓屏、建纹理,第一帧往往来不及,会看到明显的跳变。所以项目做了一个预热逻辑:当合盖速度超过 8°/s、且角度还在阈值上方 70° 以内时,就开始以 0.25 秒的间隔提前截图,并在盖子停止移动后继续 2 秒。这样等到真正触发时,纹理已经在手上,效果一上来就是顺的。
另外一个细节是滞回(hysteresis):重新打开盖子时,要高出阈值 4° 才会释放效果层,避免在阈值附近来回抖动导致效果反复横跳。
可调参数:几乎把「摄影棚」交到你手上
项目通过菜单栏图标打开设置面板,用 UserDefaults 持久化。默认值(工厂值)相当能说明它的设计取向:
| 参数 | 默认值 | 含义 |
|---|---|---|
| Start angle(触发角度) | 90° | 合盖低于此角度开始出现效果 |
| Full effect after | 60° | 再合多少度达到最大效果 |
| Blur(模糊半径) | 135 pt | 远端边缘的高斯模糊半径 |
| Blur spread | 0% | 0 只模糊远端,100 整屏均匀模糊 |
| Dimming(压暗) | 100% | 远端边缘最暗时的黑色叠加强度 |
| Viewing distance | 6.0 | 眼睛到屏幕中心的距离(以屏幕高度为单位) |
| Recession | 1.0 | 盖子每合 1°,画面远离玻璃多少度;1.0 表示画面在房间里保持不动 |
| Live rendering | 开 | 效果层画面持续更新,而非定格 |
| Show angle in menu bar | 关 | 把实时角度画在菜单栏图标旁 |
其中 Viewing distance 和 Recession 这一对参数很有意思:前者决定你「站在多远看这台笔记本」,后者决定画面是跟着盖子一起转(数值偏小,更「贴玻璃」),还是在房间里保持静止(1.0 时,你合盖子像是在转动一个装着照片的框)。把这两个调好,效果才会显得「自然」——README 里也提到,默认设置下建议正对着 MacBook 观看效果最佳。
面板还提供 Reset 和 Quit,设置界面已经做了英文和简体中文两套本地化(Sources/MacDuo/Resources/en.lproj 与 zh-Hans.lproj)。早期版本遗留的参数(blurFrontWidth、maxTilt、tiltDegrees 等)会在启动时被清理掉。
安装与使用
项目提供两种现成的开发版构建(Apple Silicon 与 Intel 均可):
要求 macOS 14 或更高,而且必须是带兼容屏幕角度传感器的 MacBook。首次运行会请求「屏幕录制」权限,授予后效果才可用。
想自己编译的话,需要 Swift 6.0 或更高版本(Xcode 工具链)。在项目目录下执行:
./build.sh
脚本会生成带 ad-hoc 签名的 build/Mac Duo.app;也可以直接编译并运行:
./build.sh --run
注意 ad-hoc 重新签名之后,macOS 可能再次要求屏幕录制权限,这是正常的。
需要提前知道的限制
项目在 README 里很坦诚地列了已知限制,这几点值得注意:
- 只有带兼容角度传感器的 MacBook 能用;没有传感器时应用会明确报出来,而不是静默失效。
- 传感器必须是 macOS 认定为「内置」的那个;带同类传感器的外接显示器会被忽略。
- 效果只作用于内置显示屏。
- 合盖导致 macOS 进入睡眠时,效果会停止——也就是说你看到的是「睡眠前」那段过程。
- 点击会穿过效果层,落到下面的应用上。
为什么它出现在 Trending 上
Mac Duo 的魅力在于它把一件「操作系统本来替你隐藏掉的事」变成了可以看见的过程。合盖在 macOS 里是个二值事件——开或关;而这个项目从最底层的 HID 传感器里把连续的角度读出来,再用 GPU 把这个连续量映射成透视、模糊和明暗,于是机械动作被重新解释成了一次三维空间里的运动。
它的实现也很「教科书式地克制」:不碰系统 UI,不做注入,不劫持显示;只是叠加一个自己渲染的窗口,读一个不需要权限的传感器,剩下的全部交给 Metal。项目作者也标注了「This project is built with AI assistance」——在 AI 编程助手普及的当下,这类「小而巧、把一个想法做到位」的桌面项目正在变多,Mac Duo 是个不错的样本。
项目地址:https://github.com/sumimakito/Mac-Duo
如果你手上刚好是带角度传感器的 MacBook,可以去仓库下个开发版试试——记得正对着屏幕看,效果会更好。
















暂无评论内容