这个项目很有意思:它把“头部追踪”和“变形透视”搬到了浏览器里,目标是让任意网页都能获得一种跟随观察者视角变化的伪 3D 视差效果。简单说,你用摄像头对准自己,左右移动头部,网页不再是一张平铺的 2D 页面,而像是悬浮在屏幕前方的一个 3D 面板,视角会跟着你的头位实时旋转。这就是标题里 Anamorphic 的含义——页面会针对你的观察角度做非对称拉伸与补偿,让你在不同角度看到的投影始终符合真实透视关系。
项目的核心卖点可以归纳为三件事:
- 作用于任意网页,不是只针对某个 Demo。
- 完全在浏览器端运行,摄像头画面和头部关键点检测都在本地处理。
- 不需要 GPU,普通带摄像头的笔记本即可跑,属于轻量级前端交互能力。
这篇文章我会按“技术原理 -> 如何把它注入任意网页 -> 最小可运行实现 -> 功能测试与性能观察 -> 常见坑”的顺序拆解,并给出可以直接套用的浏览器端代码框架。如果你要给自己的站点加一个“头部追踪 3D 视角”,或者想把这个项目改造成用户脚本/浏览器扩展,可以直接照着下面这套思路落地。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 浏览器端前端交互方案,类似“头部追踪 + 伪 3D 透视渲染” |
| 核心功能 | 任意网页根据用户头部位置,实时改变 CSS 3D 透视视角 |
| 关键特性 | Anamorphic 变形校正、实时头部追踪、跨页面注入 |
| 硬件要求 | 普通笔记本 + 摄像头即可,CPU 可运行 |
| 摄像头依赖 | 需要浏览器授予 getUserMedia 摄像头权限 |
| 技术栈 | JavaScript / TypeScript、CSS Transform 3D、MediaPipe FaceLandmarker |
| 适用场景 | 品牌页、产品展示页、个人博客视觉交互、可交互数字展陈 |
| 批量任务 | 不是典型的批量任务,但可批量注入到多个站点 |
| API 能力 | 可封装成浏览器端 JS API,也可作为 UserScript / 浏览器扩展使用 |
| 隐私边界 | 摄像头数据在本地处理,不应上传人脸信息 |
这里特别说明:标题里出现的是 Show HN,说明这是 Hacker News 上作者自荐的项目,早期版本可能比较轻量。实际显存、GPU 占用完全不用考虑,重点是浏览器兼容性和摄像头权限链路。
2. 技术原理拆解:Anamorphic 头部追踪是怎么工作的
想把这个项目讲清楚,需要先理解 3 个核心概念。
2.1 头部追踪:摄像头如何给出头的坐标
浏览器里做头部追踪,最常用的方案是 MediaPipe FaceLandmarker。它是一段轻量级模型,输入摄像头视频帧,输出 468 个左右的人脸关键点坐标。通过双眼、鼻子、下巴等关键点,可以估算出头部在屏幕坐标系里的 X、Y 偏移量,以及头与摄像头的相对 Z 距离。
基本链路是:
摄像头帧 -> FaceLandmarker 推理 -> 头部位置 (x, y, z) -> 更新 CSS transform代码层面最常见的写法是先领取视频流,再把视频帧交给模型:
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user", width: { ideal: 320 }, height: { ideal: 240 } } });视频分辨率不需要太高。因为头部检测只关心五官坐标,320x240 已经足够,太高反而会增加推理耗时和 CPU 占用。
拿到头部坐标后,核心不是把视频画面显示出来,而是把坐标映射成网页视角的旋转角度。
2.2 透视变换:让网页看起来悬浮在 3D 空间里
普通网页是平面 2D 文档,用 CSS 的 transform 属性可以让它具备 3D 透视:
.scene { perspective: 800px; transform-style: preserve-3d; } .page-panel { transform: rotateX(0deg) rotateY(0deg) translateZ(0px); }perspective 决定了“观察者离页面有多远”,数值越小,3D 透视感越强;rotateX / rotateY 则决定页面绕水平轴和垂直轴旋转的角度。当头部向右移动时,页面应该向左旋转一点,就像你在现实中绕着一张海报走动时看到的效果。
2.3 Anamorphic 变形校正:为什么不是普通 rotateY
这里是最关键的一点。普通的 rotateY 看起来像一个单纯旋转的卡片,但它没有考虑“观察者位置”和“屏幕平面”之间的投影关系。
实际场景里,屏幕是一个固定平面。用户头部偏移到侧面时,眼睛看到的画面是“屏幕平面上的 2D 投影”。如果你希望用户看到的投影看起来是一张正对着他的网页,你必须反过来对页面做一次透视补偿,而不是简单旋转。
这个“反向补偿”就是 Anamorphic 的核心:根据头部的实际偏移量,计算一个投影矩阵,对页面内容做非线性的横向拉伸/压缩。这样,当用户从侧面看屏幕时,他看到的页面内容在视觉上是正的,或者产生一种“页面真的悬浮在半空”的深度幻觉。
用代码角度理解,就是 transform 的拼接顺序和参数需要按头部位置动态生成:
function updatePageFromHeadPosition(head) { // head.x / head.y / head.z 是相对屏幕中心归一化后的位置 const rotateY = -head.x * maxRotateY; // 左右 const rotateX = head.y * maxRotateX; // 上下 const translateZ = head.z * depthFactor; // 前后 pageEl.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateZ(${translateZ}px)`; }这只是一个示意。真实的 Anamorphic 效果还需要根据屏幕尺寸与视角做投影矩阵计算,才能让页面在任何观察角度下都保持正确的比例关系。
3. 适用场景与使用边界
3.1 适合谁用
- 个人博客/作品集:想让访客进来第一眼看到一个有空间感的首页,这是一个低成本的差异化设计。
- 产品展示页:把产品“悬浮”在页面中央,用户头部移动时,产品呈现不同角度的透视变化。
- 营销落地页:适合那种“强视觉冲击”的推广页面。
- 前端技术研究:对这个效果进行缩放、坐标映射、性能优化,确实能学到不少 3D 渲染和浏览器媒体流知识。
3.2 不适合的场景
- 长文阅读场景:一整屏文字加头部追踪会让阅读很累,应该只在 Hero 区或某几个容器上启用,不要全局套在正文上。
- 高精度交互场景:它本质是视觉反馈,不是输入设备,不能用它代替鼠标/触摸去精确操作。
- 移动端:移动端摄像头权限和性能表现不一致,而且用户拿手机时会频繁晃动,建议桌面端优先。
- 对隐私极其敏感的站点:如果用户不信任摄像头授权,体验会直接中断,必须有 fallback。
3.3 使用边界与合规提醒
项目能够在浏览器里实时读取摄像头,这带来两个必须处理的问题:
- 摄像头数据不能上传。实现上应该保持“模型推理在浏览器本地完成”,不要把视频帧发送到任何服务器。
- 如果页面上出现他人的面部、肖像或受版权保护的素材,需要确认授权后再使用。
如果你是把它做成浏览器扩展或用户脚本,还要注意两点:一是不要对所有站点默认开启,默认开启会导致用户访问任何页面时都收到摄像头授权弹窗,体验极差;二是要提供开关和站点黑名单/白名单。
4. 环境准备与运行方式:如何让它作用于任意网页
“作用于任意网页”是这个项目最吸引人的地方。实现方式主要有三种,难度依次递增。
4.1 方式一:UserScript 用户脚本注入
最快验证效果的方式是做一个 Tampermonkey / Violentmonkey 用户脚本。脚本通过@match匹配目标站点,在页面加载后插入追踪逻辑。
// ==UserScript== // @name Head-Tracked Perspective // @namespace http://tampermonkey.net/ // @version 0.1 // @match http*://* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function () { "use strict"; // 实际逻辑:初始化摄像头、头部检测、transform 更新 })();注意:@match http*://*表示所有 http/https 页面都注入,个人测试没问题,但正式使用一定要缩小匹配范围,并且设置默认关闭。
4.2 方式二:浏览器扩展 Content Script
浏览器扩展和用户脚本本质类似,但权限控制和生命周期更规范。在扩展的 manifest 中声明 content_scripts 和 optional host_permissions,用户访问某个站点时按需点击扩展按钮开启。
4.3 方式三:直接集成到自己的网页
如果你只是想在自己的项目里用,可以直接引入项目封装的 JS 文件。典型初始化流程是:
npm install your-project-name然后在页面里导入并初始化:
import { HeadTrackedPerspective } from "your-project-name"; const tracker = new HeadTrackedPerspective({ container: document.getElementById("scene"), maxRotationX: 6, maxRotationY: 12, depthAttribute: "data-depth", smoothFactor: 0.1 }); tracker.start();具体的包名、导出的类名、构造参数,要以这个项目实际发布到 npm 的版本为准。这里只是给你一个“功能设计”层面的参考。
5. 最小可运行示例:本地 Demo 怎么搭
我们可以不依赖任何后端,写一个最小 Demo:打开一个本地 HTML 页面,摄像头对准自己,页面里的卡片跟随头部位置产生视角变化。
5.1 HTML 结构与 CSS
先定义一个 3D 场景容器:
<html> <head> <style> body { margin: 0; overflow: hidden; background: #111; display: flex; align-items: center; justify-content: center; min-height: 100vh; } #scene { perspective: 800px; transform-style: preserve-3d; } #panel { width: 360px; height: 480px; background: linear-gradient(135deg, #6366f1, #a855f7); border-radius: 16px; box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4); transform: rotateX(0deg) rotateY(0deg); will-change: transform; } </style> </head> <body> <div id="scene"> <div id="panel"></div> </div> </body> </html>perspective 放在父容器上,子元素做 transform,这样旋转的透视关系才正确。will-change 告诉浏览器提前优化 transform 变化。
5.2 JavaScript:摄像头 + 头部关键点
下面的代码是一个最小实现框架。它依赖 MediaPipe FaceLandmarker,你需要根据自己的模型路径调整。
const video = document.createElement("video"); video.style.display = "none"; document.body.appendChild(video); async function startCamera() { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user", width: { ideal: 320 }, height: { ideal: 240 } } }); video.srcObject = stream; await video.play(); return stream; } function initFaceDetector() { // 示意:具体模型加载方式以 MediaPipe 官方文档为准 return { detect: async (source) => { // 返回 { x, y, z },表示头部相对画面中心的偏移 return { x: 0, y: 0, z: 1 }; } }; } function startLoop(detector) { const panel = document.getElementById("panel"); function tick() { requestAnimationFrame(tick); detector.detect(video).then((head) => { const rotateY = -head.x * 10; const rotateX = head.y * 6; const translateZ = head.z * 20; panel.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateZ(${translateZ}px)`; }); } requestAnimationFrame(tick); } (async () => { await startCamera(); const detector = initFaceDetector(); startLoop(detector); })();这里detector.detect返回的 x/y/z 是示意结构,实际 MediaPipe FaceLandmarker 返回的是关键点数组,你需要自己计算鼻尖与画面中心的差值,再归一化到 [-1, 1] 区间。
5.3 打开方式
由于浏览器 getUserMedia 的权限策略,直接双击 HTML 文件用file://打开可能有问题,建议起一个本地静态服务:
npx serve .或者如果你已经安装了 Python:
python3 -m http.server 8080浏览器访问http://localhost:8080,第一次打开会弹出摄像头授权,允许之后就可以看到效果了。
6. 功能测试与效果验证
部署完 Demo 后,需要按下面的维度逐项验证。
6.1 基础视角跟随测试
头部向左移动时,页面应该向右旋转,模拟“绕到页面另一侧看”的效果。判断标准是:
- 页面边缘出现明显的透视缩短。
- 卡片背景的渐变方向随视角变化。
- 回到正中间时页面恢复成标准 2D 平面。
如果方向相反,说明 x 坐标到 rotateY 的映射符号写反了,调整负号即可。
6.2 Anamorphic 变形校正测试
这是标题里最核心的部分。判断标准是:从某个特定观察角度(通常是摄像头正面方向)看,页面内容不能出现明显拉伸变形。
如果你发现从侧面看时文字被严重拉长或挤压,说明你没有做反向投影校正,只做了简单的 rotateY。此时需要在 transform 链里补充透视补偿参数,或者根据屏幕 DPI 修正角度映射系数。
6.3 多容器分层测试
如果场景里存在多个元素,可以给元素增加><div class="layer">type HeadPosition = { x: number; // -1 到 1,头部相对屏幕中心的左右偏移 y: number; // -1 到 1,头部上下偏移 z: number; // 0 到 2,头部与摄像头的相对距离 }; type HeadTrackedOptions = { maxRotationX: number; maxRotationY: number; smoothFactor: number; onTrack?: (head: HeadPosition) => void; onError?: (error: Error) => void; };
调用方只要关注 onTrack 回调,把 head.x / head.y 映射到自己的业务逻辑上即可。这样可以把“头部检测”和“页面 transform 渲染”解耦。
7.2 多个站点批量启用
如果要做成一个“任意网页可用”的工具,建议维护一份配置列表:
{ "enabledSites": ["example.com", "docs.example.org"], "excludeSites": ["bank.example.com"], "defaultOff": true, "maxRotationX": 6, "maxRotationY": 12 }用户脚本或扩展启动时读取这份配置,按域名决定是否注入。千万不要默认所有站点全开。
7.3 面向多页面场景的批量注入脚本
如果你想把脚本注入到一批页面,可以写一个简单的 Node.js 脚本,利用 Puppeteer 批量打开页面并注入同一份 head-tracker 脚本,用于截图或视觉回归测试:
import puppeteer from "puppeteer"; const urls = [ "https://example.com/page-1", "https://example.com/page-2", "https://example.com/page-3" ]; const browser = await puppeteer.launch({ headless: false, args: ["--use-fake-ui-for-media-stream"] }); for (const url of urls) { const page = await browser.newPage(); await page.goto(url, { waitUntil: "networkidle2" }); // 注入脚本并截图 await page.evaluate(() => { // 注入你的 head-tracker 逻辑 }); await page.screenshot({ path: `shot-${Date.now()}.png` }); await page.close(); } await browser.close();Puppeteer 的摄像头摄像头流需要浏览器启动参数配合,这属于测试代码,不影响正式部署。
8. 资源占用与性能观察
8.1 CPU 占用
头部追踪在浏览器端是典型的 CPU 密集型任务。影响 CPU 的主要因素有三个:
- 摄像头视频分辨率:320x240 足够,不要用 1080p。
- 推理频率:不需要每帧都跑模型,可以每秒推理 15-20 次,然后插值到渲染循环。
- FaceLandmarker 模型大小:不同版本模型速度和精度差异很大,实际要用 Lighthouse 或 Performance panel 观察。
8.2 内存与电量
如果摄像头一直开着,浏览器标签页无法被自动休眠,笔记本电脑耗电会比较快。建议在用户离开页面、标签页隐藏时自动暂停检测:
document.addEventListener("visibilitychange", () => { if (document.hidden) { tracker.stop(); } else { tracker.start(); } });8.3 如何观察性能
打开浏览器开发者工具,用 Performance 面板录制 5 秒交互过程,重点看:
- requestAnimationFrame 回调的耗时是否超过 16ms。
- 主线程 Long Tasks 是否频繁出现。
- 摄像头视频流解码是否成为瓶颈。
如果帧率不足,优先降低模型推理频率,而不是降低 CSS 动画质量。
8.4 降低开销的建议
- 追踪用独立循环,渲染用 requestAnimationFrame。
- 在 onTrack 回调里做坐标平滑,避免直接高频改写 transform。
- transform 不要作用在超大容器上,尽量作用在带
will-change: transform的独立层。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 摄像头授权弹窗不出现 | 页面不是 https 或 localhost | 查看浏览器地址栏安全状态 | 启用本地服务或 https,file:// 协议通常无法调用摄像头 |
| getUserMedia 一直 pending | 其他标签页正在占用摄像头 | 依次关闭可能占用摄像头的页面 | 关闭后重新刷新当前页 |
| 页面识别不到头部 | Video 元素未播放或模型未加载完成 | 在控制台打印 video.readyState 和 detector 状态 | 等待视频 loadedmetadata 后再初始化检测器 |
| 头部移动无效果 | transform 被覆盖或选择器不对 | 检查 DOM 结构和 computed style | 改用独立的容器,避免与其他框架的样式冲突 |
| 页面旋转方向反了 | x 坐标映射符号错误 | 打印 head.x 数值并左右移动头部 | 调整 rotateY 符号 |
| 文字被拉伸变形 | 没有做 Anamorphic 反向校正 | 用截图对比不同角度下的页面 | 加入投影矩阵补偿,而不是单纯 rotate |
| 使用 iframe 时失效 | iframe 缺少 allow="camera" 属性 | 检查 iframe 属性 | 增加allow="camera; microphone" |
| Tampermonkey 脚本不注入页面 | 页面开启了 CSP,或 match 规则不对 | 查看控制台错误 | 改用扩展方式,或添加 GM 权限受限时使用 content script |
| 滚动页面时 transform 抖动 | 场景容器随滚动变化 | 固定场景容器或用 position: sticky | 将场景容器固定在视口内 |
| 批量注入脚本时摄像头冲突 | 多个页面同时抢占摄像头 | 检查浏览器标签页的摄像头占用 | 测试时一次只开一个页面 |
10. 最佳实践与使用建议
10.1 默认关闭,按需开启
无论做成用户脚本还是浏览器扩展,都应该默认关闭。用户点击按钮后,再请求摄像头授权并启用头部追踪。这样可以避免大量用户一进来就被摄像头弹窗劝退。
10.2 给出降级方案
如果用户拒绝授权,页面仍然要能完整呈现。头部追踪只是增强效果,不能成为核心内容的屏障。
10.3 平滑插值,避免生硬跳动
设备检测到的头部坐标波动比较大,直接用会造成抖动。推荐在渲染循环里做指数平滑:
let smoothX = 0; let smoothY = 0; const factor = 0.12; function tick() { const currentX = tracker.getHeadX(); smoothX += (currentX - smoothX) * factor; }factor 值越小越平滑,但响应越迟钝,建议从 0.1 到 0.2 之间调。
10.4 缩小注入范围
如果你注入的脚本会影响页面布局,尽量只对特定容器生效,不要劫持body的 transform。否则页面自身的 CSS 布局、瀑布流、滚动定位都可能被破坏。
10.5 注意隐私合规
摄像头流必须在本地处理。如果后续扩展成产品,需要明确告知用户摄像头数据不会被上传。在涉及他人面部时,确认已获得相应授权。
11. 总结与下一步
这个项目最值得尝试的点在于:它用纯浏览器技术,把传统的“变形艺术”变成了实时交互能力。不要以为只有大厂才能做 3D 视觉页面,一个用户脚本级别的方案就能完成。对于前端开发者,建议先跑通最小的本地 Demo,重点关注两个地方:
- 头部坐标到 transform 的映射逻辑。
- Anamorphic 反向补偿是否真正让侧面视角变得自然。
最容易踩的坑有两个:一个是把普通 rotateY 当成 Anamorphic 透视,导致文字拉伸变形;另一个是摄像头权限与页面注入机制的兼容问题。先解决这两点,再去做分层视差、扩展封装和批量多页面适配。
后续可以扩展的方向包括:把 FaceLandmarker 换成更轻量的模型以降低 CPU 占用;增加手势触发逻辑;把头部追踪与 WebGL/Three.js 场景结合,做更复杂的 3D 页面叙事。这个方向的价值不在“炫”,而在于它验证了一条浏览器端实时交互的完整链路:获取媒体流、本地模型推理、CSS 3D 渲染、跨页面注入。把它吃透,你做其他浏览器视觉交互项目时思路会清晰很多。