有些灵感,是雨天带来的。
比如那天下午,窗外阴沉,玻璃窗上映着模糊的倒影,水珠拖曳出一道道像雾又像光的纹理。我盯着这块玻璃看了很久,突然心想:我能不能用代码还原这种“虚拟玻璃”的感觉?
就这样,一段技术与设计交织的旅程就此开始。
🎯 项目的想法从哪来?
我一直对交互式视觉效果情有独钟。从文字打字机特效到 Canvas 粒子爆炸,这些细节常常能让用户的情绪波动产生共鸣。而玻璃反射效果,既含蓄又浪漫,不是炫技,却能极大提升网页的质感。
这也是 Reflect Glass 项目的起点:用浏览器技术模拟一个逼真的虚拟玻璃,能实时反射用户的影像、带有细腻的模糊、光晕、倒影。
于是,我给自己定下几个目标:
- 能捕捉摄像头并实时显示用户倒影;
- 呈现玻璃感:柔光、模糊、阴影、倒影、边框;
- 允许截图,保存这一瞬间的虚拟“自拍”;
- 响应式设计,兼容移动端浏览;
- 保持足够性能,在低配设备上也能运行流畅。
🧩 技术选型是这样定下来的
做浏览器里的反光玻璃,我不打算用 WebGL。一方面 WebGL 对新手设备要求高,另一方面效果可控性不如 Canvas + CSS 来得清晰。我想在既有的前端体系里,完成一场自我挑战。
因此我选择了这些技术作为项目基石:
- Vue 3 + Vite:开发体验好,响应式逻辑清晰,组合式 API 可复用性强;
- Canvas API:捕捉摄像头图像,实时绘制、变换、模糊处理;
- WebRTC:通过
getUserMedia获取摄像头流; - CSS backdrop-filter / filter:为玻璃添加背景模糊、光照效果;
- TypeScript:整个项目都用 TS 编写,类型安全、补全友好;
- 组合式函数 Composables:逻辑抽离,提升可读性与可维护性。
🧱 架构与流程:从影像到反光
项目的基本渲染流程如下:
你可以理解为一个三层架构:
- Camera 层:接入用户设备,捕捉视频流,传入 Canvas;
- Effect 层:对视频帧进行水平翻转、模糊、透明度调整;
- Scene 层:展示最终效果,组合出类似现实中的玻璃窗场景。
接下来我就从这三个方面,一步步展开讲讲我是怎么实现的。
🧊 构建场景:玻璃、墙面与光影
这个项目的前端 UI 其实非常“克制”:没有多余元素,一个大大的玻璃框就是核心。
为了营造“玻璃装在墙上的感觉”,我设计了这样一个组件层次:
<template>
<div class="scene-container">
<div class="wall">
<div class="wall-texture"></div>
</div>
<div class="glass-frame">
<div class="glass-surface">
<slot></slot>
</div>
</div>
</div>
</template>
🧱 墙面设计
墙面使用纯 CSS 渐变与背景图案模拟出一种质感:
.wall-texture {
background-image:
linear-gradient(45deg, #ccc 25%, transparent 25%),
linear-gradient(-45deg, #ccc 25%, transparent 25%);
background-size: 20px 20px;
opacity: 0.05;
}
这样在大屏下不会显得单调,又不至于分散焦点。
🪟 玻璃框架设计
玻璃使用了 backdrop-filter 和阴影模拟真实玻璃的模糊、透明感:
.glass-frame {
border: 16px solid #2c3e50;
border-radius: 8px;
box-shadow:
0 0 30px rgba(0, 0, 0, 0.2),
inset 0 0 15px rgba(255, 255, 255, 0.05);
}
.glass-surface {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(3px);
position: relative;
overflow: hidden;
}
这样的设计有几个好处:
- 保持轻量,不依赖图片或贴图;
- 支持响应式缩放;
- 整体调性一致,符合现代 UI 审美。
📸 摄像头接入与帧捕捉
核心组件 CameraView.vue 负责调用用户摄像头,并将视频源渲染到隐藏的 <video> 和 <canvas> 中。
开始摄像头访问
const startCamera = async () => {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user' }
});
videoRef.value!.srcObject = stream;
startRenderLoop();
};
这一段代码会触发浏览器的摄像头授权弹窗,如果用户同意,返回的 stream 会绑定到 video 元素的 srcObject 属性。
为了让摄像头帧能被进一步处理,我不直接显示 video,而是把它绘制到 Canvas 中。
🪞 图像处理:反光与镜像渲染
这个部分是整个项目的精华。我封装了一个组合函数 useGlassEffect.ts,专门用于处理图像的“玻璃反射效果”。
水平翻转的关键
通过 canvas 的 context.scale(-1, 1) 实现“镜面效果”:
ctx.save();
ctx.scale(-1, 1);
ctx.drawImage(video, -canvas.width, 0);
ctx.restore();
注意 drawImage 的 x 坐标必须为负值,否则图像会绘制到画布外面。
添加模糊与透明度
ctx.filter = 'blur(2px) brightness(1.05)';
ctx.globalAlpha = 0.6;
这样就做出了那种“若隐若现”的反射效果。光线从人物轮廓周围淡出,看起来就像真的站在玻璃前。
🧪 动态调试 + 预设效果
为了让用户体验更灵活,我还加了几套效果预设,比如:
- 清晰玻璃(透明度低,模糊小);
- 雾化玻璃(模糊更重);
- 色调玻璃(带颜色滤镜)。
const GLASS_PRESETS = {
clear: { reflectionOpacity: 0.5, glassBlur: 2, glassColor: 'rgba(255,255,255,0.05)' },
foggy: { reflectionOpacity: 0.7, glassBlur: 5, glassColor: 'rgba(240,240,240,0.1)' },
tinted: { reflectionOpacity: 0.6, glassBlur: 3, glassColor: 'rgba(0,128,255,0.05)' }
};
我用 ref + computed 来动态响应这些参数的变化,非常自然:
const activeEffect = computed(() => {
return {
filter: `blur(${customEffect.value.glassBlur}px)`,
opacity: customEffect.value.reflectionOpacity,
backgroundColor: customEffect.value.glassColor
};
});
这样即便以后我接入用户滑块调整模糊程度、透明度,也无需改动 Canvas 逻辑,直接绑定即可。
📷 截图逻辑:记录“玻璃里的我”
实现“拍照”功能,其实并不复杂,但我希望它尽可能丝滑。
当用户点击“📷”图标时,我们抓取当前效果 Canvas 的内容,并下载为 PNG 文件。
按钮 UI 设计
按钮采用浮动样式固定在右下角,使用了现代圆形按钮设计:
<template>
<button class="capture-button" @click="captureImage">📷</button>
</template>
.capture-button {
position: absolute;
bottom: 32px;
right: 32px;
background-color: rgba(0, 0, 0, 0.6);
color: white;
border: none;
border-radius: 50%;
width: 64px;
height: 64px;
font-size: 24px;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
cursor: pointer;
transition: background-color 0.2s;
}
.capture-button:hover {
background-color: rgba(0, 0, 0, 0.8);
}
实现截图逻辑
截图只需要从 Canvas 中提取图像数据:
const captureImage = () => {
const canvas = document.querySelector('.reflection-canvas') as HTMLCanvasElement;
if (canvas) {
const link = document.createElement('a');
link.download = `reflect-glass-${new Date().toISOString()}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
}
};
通过 canvas.toDataURL 拿到图像数据,再触发一个 a 标签模拟点击,完成下载。
这个功能虽然短短几行代码,但它将虚拟空间变成了可以保存的“记忆”,对用户来说具有情绪意义。
📱 响应式设计与设备兼容
在设计之初我就设想过,很多用户可能会在手机上试这个应用,所以必须兼容各种设备。
Media Query 优化布局
我加入了一套简洁的响应式规则:
@media (max-width: 768px) {
.glass-frame {
width: 90vw;
height: 60vh;
border-width: 10px;
}
.capture-button {
width: 48px;
height: 48px;
font-size: 18px;
bottom: 16px;
right: 16px;
}
}
这样在手机上也能保持优雅布局,不至于控件重叠或被挤压。
🛠 性能与兼容性优化
虽然功能实现上已经可用,但我发现性能对用户体验影响极大,尤其在中低端 Android 手机上,最初版本会发烫掉帧。
我采取了几种方法优化性能:
🌡 降低帧率与渲染频率
针对不同设备,动态控制帧率:
const isMobile = ref(window.innerWidth < 768);
const targetFPS = computed(() => (isMobile.value ? 20 : 60));
const renderLoop = () => {
const now = performance.now();
const delta = now - lastRenderTime.value;
if (delta > 1000 / targetFPS.value) {
drawToCanvas(); // 只在需要时重绘
lastRenderTime.value = now;
}
requestAnimationFrame(renderLoop);
};
这样在移动端就不会频繁全速刷新画面,节能且稳定。
🧱 Canvas 操作避免重排
每次绘制前复用已有画布尺寸,防止多余 resize:
sourceCanvas.width = video.videoWidth;
sourceCanvas.height = video.videoHeight;
并提前获取 context 引用,避免反复 getContext 调用。
🧯 错误处理与用户反馈
当用户拒绝摄像头权限时,我们不能什么都不显示,需要有友好的提示界面:
<template>
<div v-if="!isStreamActive" class="camera-overlay">
<p v-if="error">{{ error }}</p>
<button @click="startCamera">重新启动摄像头</button>
</div>
</template>
如果浏览器不支持某些特性,我会给出 fallback 提示:
if (!navigator.mediaDevices?.getUserMedia) {
error.value = '当前浏览器不支持摄像头访问';
}
此外,Safari 对 backdrop-filter 支持有限,我用 CSS 特性检测来处理:
if (!CSS.supports('backdrop-filter', 'blur(1px)')) {
warn('backdrop-filter 在当前浏览器可能无效');
}
🧩 项目结构整理
整个项目拆分合理,所有逻辑清晰隔离,便于维护和复用:
src/
├── assets/ # 资源文件
├── components/
│ ├── CameraView.vue # 视频捕捉组件
│ ├── Scene.vue # 场景与 UI 布局
├── composables/
│ └── useGlassEffect.ts # 特效参数与处理逻辑
├── views/
│ └── ReflectGlassView.vue # 主页面容器
├── App.vue
└── main.ts
每一个组件的职责都明确,CameraView.vue 管理摄像头,Scene.vue 管理 UI,useGlassEffect 管理图像处理逻辑,互不干扰。
🧠 CSS 视觉实现再探
我特地尝试了更自然的“毛玻璃”样式,使用了多层阴影、混合模式与渐变模拟真实质感:
.glass-surface {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(3px);
box-shadow:
inset 0 0 30px rgba(255,255,255,0.05),
0 0 50px rgba(0,0,0,0.2);
}
.glass-surface::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
45deg,
rgba(255, 255, 255, 0.1),
transparent
);
pointer-events: none;
}
🔬 图像处理流程小结
为更直观展现图像反射处理流程,这里用一张图总结:
这个流程图能帮助理解:虽然用户看起来只是“对着玻璃自拍”,背后却是实时图像采集 + 图像翻转 + 模糊混合多重叠加的协同渲染。
📡 WebRTC 视频采集机制解析
Reflect Glass 能够实时抓取用户的影像,最核心的技术就是 WebRTC 提供的 getUserMedia 接口。
虽然它只有几行代码,但背后牵扯的流程其实蛮复杂的,我也遇到了一些初看容易忽略的点。
🌐 视频采集基本流程
我们一般是这样使用的:
navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
}
})
调用这个方法后,浏览器会做几件事:
- 弹出摄像头授权提示
- 找到合适的摄像头设备
- 返回一个包含视频轨道的 MediaStream 对象
- 这个对象被赋值给
<video>元素的srcObject
📦 MediaStream 工作流程
这个过程我们可以用图来梳理清楚:
这个过程非常依赖浏览器支持和用户选择,失败的可能性很高。所以我在代码中加入了多个 fallback。
🔬 图像反射与滤镜合成机制
项目的“亮点”就在于图像反射的真实感。别看只是加个模糊,其实涉及多种图像叠加方式,我从最初的模糊透明,到后来的叠加色彩、亮度提升、甚至尝试了光晕效果。
我使用的 Canvas 技术中,有一个比较关键的属性:globalCompositeOperation,它控制不同图像之间的合成方式。
ctx.globalCompositeOperation = 'lighter'; // 提亮重叠部分
这在一些特定场景下,比如白色衣服对着玻璃反射,会比直接画图更自然。
📐 UI 风格设计心得
Reflect Glass 虽然功能简单,但我花了不少心思在 UI 上。因为摄像头类应用,很容易做得像监控软件或者打卡系统,界面一不小心就显得死板。
我的做法是让它看起来像一面真实的玻璃窗,所以用了玻璃拟态的视觉语言。
🔲 色彩与边框设计
- 背景色:采用低饱和中性色,强调内容而非框架
- 边框样式:使用内外阴影、半透明效果模拟玻璃厚度
- 字体选择:用了系统默认 sans-serif 字体,确保跨平台一致性
🔘 控件风格
- 按钮是圆形、悬浮样式,便于用户点击
- 加入动效,例如
hover状态变化、拍照按钮缩放反馈
🎨 动效与动画细节
虽然没有使用大规模动画库,我还是加入了几种关键动画增强整体交互感。
拍照按钮动效
使用 CSS transform: scale() 实现按钮点击时的缩放感:
.capture-button:active {
transform: scale(0.95);
}
摄像头启动过渡
用户点击启动摄像头后,我加了一个淡入的动画:
.camera-overlay {
animation: fadeOut 0.5s ease forwards;
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; visibility: hidden; }
}
不至于让用户感觉突兀。
🧰 技术细节小贴士
项目中还有很多小技术点,我在这里一并记录:
- ✅
requestAnimationFrame:比setInterval更适合视频类 UI 更新,省电不撕裂 - ✅
backdrop-filter会吃性能,低端设备上建议不要太多层叠 - ✅
canvas.toDataURL()性能不错,但要注意调用前保证画面稳定,别还在更新 - ✅ 使用
transform: scale(-1,1)翻转图像时注意原点问题,要移动坐标或画布宽度偏移
📦 代码拆解讲解一览
为了方便你理解 Reflect Glass 每部分如何运作,我对代码组织和结构做一张总览图:
这张图帮助我理清了组件依赖关系,也让我在重构的时候更有方向。
✍️ 项目创作过程回顾
这个项目虽然功能单一,但整个开发过程对我来说意义很大。
一开始只是一个灵光一闪的点子,想把玻璃反射做得好看点,没想到会越做越深入。中途几次我都差点打退堂鼓,比如摄像头权限问题太多、某些浏览器 Canvas 滤镜不兼容等,但我都一一攻克。
特别是调试的时候,拿着 iPad 对着自己反复测试反射角度,感觉就像一位理发师对着镜子端详头型一样(笑)。
📚 技术学习收获
我通过这个项目实战了很多平时只停留在文档层的 API,尤其是:
- 💡 Canvas 图像处理
- 📸 WebRTC 视频流控制
- 💅 CSS 的高级特效组合
- 🧠 Vue3 的组合式 API
更重要的是,我知道了“交互性”不仅仅是点按钮、动页面,而是真正让用户“感觉自己在画面里”。这是我以前做工具类前端项目体会不到的部分。
🏁 总结
Reflect Glass 最终实现了我一开始的愿景:
一个可以模拟玻璃反光、让用户沉浸其中的微型镜像空间。
虽然功能不多,但在技术、体验和细节上我都尽可能打磨,希望它不仅是一面镜子,更是一次视觉和感官的小型旅程。
到现在,每次打开这个项目,我都还会下意识看一下自己的倒影,调整一下姿势,好像真有人在背后看我一样。这种沉浸感,是代码带来的,也是用户感知的,我很喜欢这种奇妙的交汇。