几周前的某天下午,我走进了一家没什么游客的水族馆,阳光透过水面撒在池底,那些流动的光斑像无声地在地面上跳舞。我站了很久,脑中突然冒出一个想法:我能不能用代码,把这静谧又灵动的场景复现出来?
于是,就有了这个项目 —— 一个可以随着指尖触碰而泛起水波的互动页面。
开端:从一个灵感开始的工程
我没打算一开始就做得多复杂。只是希望当用户滑动鼠标或触屏的时候,能看到一圈圈类似水面的波纹扩散出去。随着这个想法越来越清晰,我开始着手设计和实现它的核心机制:基于 Canvas 的涟漪动画。
工具栈我没有纠结太久。为了效率和现代化,我选了 Vue 3 + TypeScript + Vite 的组合。Canvas 自然是负责绘图的载体,渐变背景+圆形扩散+随机颜色,这几个关键点成了项目的基底。
我在纸上画了这样一张逻辑流图,简要概括了系统的运转机制:
用户交互(鼠标 / 触摸)
↓
计算位置坐标
↓
创建涟漪对象并记录
↓
动画帧循环更新
↓
清空画布,绘制背景
↓
绘制所有当前涟漪再配上主题配置系统,整个系统就具备了可视风格与参数的可定制能力。
架构搭建:把灵感落在文件夹里
为了方便管理和后续维护,我将整个项目按以下结构进行划分:
src/
├── assets/ # 静态资源(主题配置)
│ └── themes.json
├── components/ # UI 组件
│ └── RippleCanvas.vue
├── composables/ # 组合式函数逻辑
│ └── useRippleEffect.ts
├── App.vue # 根组件
└── main.ts # 应用入口入口 main.ts 并没有过多逻辑,只负责创建并挂载 Vue 实例:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')这样可以保证后续逻辑的清晰度和可扩展性。
可配置的主题系统:颜色、速度与风格的总控中心
我希望即便不是开发者的人也能轻松调整涟漪效果,因此将所有动画参数、颜色方案、背景样式封装到一个 JSON 文件中:
{
"themes": {
"deepOcean": {
"name": "深海之夜",
"background": {
"gradient": {
"start": "#001f3f",
"end": "#003366"
}
},
"ripples": {
"colors": ["#00B4D8", "#0077B6", "#0096C7"],
"speed": 1.5,
"fadeSpeed": 0.01,
"maxRadius": 150,
"lineWidth": 2
}
}
},
"defaultTheme": "deepOcean"
}设计中我尤其注意到:
colors使用多个蓝色调作为随机选择项,避免视觉疲劳。speed和fadeSpeed分别控制扩散速度与淡出速率,通过参数化让风格更具延展性。- 背景渐变使用的是线性模式,从顶部到底部逐渐变深,模仿光线穿透海水的感觉。
涟漪动画的诞生:Canvas + JS + 一点灵感
整个动画核心的绘制和逻辑控制被封装在了一个组合函数中:useRippleEffect.ts。这个文件就像项目的“发动机”,驱动着每一次涟漪的形成、扩展与消散。
整体流程结构
我用流程图还原了一次完整的动画迭代过程:

每个涟漪对象都有如下属性:
x, y: 出现位置radius: 当前半径alpha: 当前透明度color: 本次随机选中的颜色lineWidth: 线宽
这些数据保存在一个数组中,在每一帧循环里逐一更新它们的状态并绘制。
主组件实现:一个 canvas 就足够
UI 的主体是 RippleCanvas.vue 组件,代码并不多,但结构清晰。核心就是 Canvas 元素的挂载与监听:
<template>
<canvas
ref="canvasRef"
@mousemove="createRipple"
@touchmove="handleTouch"
/>
</template>其中:
canvasRef用于获取 DOM 元素createRipple是鼠标事件创建涟漪的处理器handleTouch则是移动端触摸支持
组件挂载后,我设置了:
- Canvas 自动适应全屏窗口
- 监听窗口变化自动 resize
- 启动动画帧循环
- 离开页面时清理资源
onMounted(() => {
const canvas = canvasRef.value
const ctx = canvas?.getContext('2d')
if (!canvas || !ctx) return
resizeCanvas(canvas)
window.addEventListener('resize', () => resizeCanvas(canvas))
init(canvas, ctx, currentTheme)
startAnimation()
})
onUnmounted(() => {
stopAnimation()
})交互细节:从一次触碰开始
项目支持两种交互方式:
- 鼠标移动:直接触发单点涟漪
- 触摸滑动:沿轨迹创建连续波纹
我在触摸事件中加了节流处理,防止频繁创建导致性能问题:
let lastTouchTime = 0
const touchInterval = 80 // 毫秒
const handleTouch = (e: TouchEvent) => {
const now = Date.now()
if (now - lastTouchTime < touchInterval) return
lastTouchTime = now
// 创建涟漪逻辑
}此外,每次创建涟漪时会从主题中随机选一个颜色,确保画面不会太单调。
涟漪绘制的秘密:渐隐 + 线性扩散
每个涟漪其实就是一圈透明度逐渐降低、半径逐渐扩大的圆:
ctx.beginPath()
ctx.arc(ripple.x, ripple.y, ripple.radius, 0, 2 * Math.PI)
ctx.lineWidth = ripple.lineWidth
ctx.strokeStyle = ripple.color
ctx.globalAlpha = ripple.alpha
ctx.stroke()涟漪状态更新则是:
ripple.radius += speed
ripple.alpha -= fadeSpeed当透明度降为0或半径超出最大值时,涟漪就从数组中删除,等待下一次点击创建新的波纹。
高DPI 支持:避免 Retina 模糊
我在测试中发现一个问题:在 Retina 屏幕上,Canvas 渲染出来很模糊。查资料后才知道,Canvas 的像素密度需要手动适配高 DPI:
const ratio = window.devicePixelRatio || 1
canvas.width = window.innerWidth * ratio
canvas.height = window.innerHeight * ratio
canvas.style.width = `${window.innerWidth}px`
canvas.style.height = `${window.innerHeight}px`
ctx.scale(ratio, ratio)通过设置真实像素与显示像素比例,并缩放绘图上下文,就能让画面变得锐利清晰。
构建与部署:Vite 带来的极速体验
这个项目在本地开发体验上几乎是无可挑剔的,主要得益于 Vite 的极速冷启动和模块热替换。构建配置相对简单,我的 vite.config.ts 中只做了基础设定:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 8080
},
build: {
outDir: 'dist',
emptyOutDir: true
}
})部署则更简单,打包后将 dist 文件夹上传到任何支持静态资源托管的平台即可,例如 GitHub Pages、Netlify、Vercel。
package.json 的脚本定义:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}实际部署时,我使用了 GitHub Pages,只需将打包目录推送至 gh-pages 分支即可完成。
Canvas 性能优化:内存与帧率之间的权衡
虽然 Canvas 本身性能不差,但在某些旧手机上,如果快速滑动或频繁点击,依然可能出现卡顿。我针对这类问题做了以下优化:
1. 控制涟漪最大数量
const MAX_RIPPLES = 100
if (ripples.value.length > MAX_RIPPLES) {
ripples.value.shift()
}这样可以避免内存堆积和帧率下降的问题,尤其在多点快速触发时,效果很明显。
2. 使用 requestAnimationFrame
原本我也考虑过 setInterval,但显然动画类的更新更适合 requestAnimationFrame,它能自动与浏览器刷新率保持一致,避免丢帧。
UI 设计考量:视觉流动与静谧感并存
整个页面没有 UI 元素,只有一整块画布。最初我也考虑加上切换主题、调节参数的面板,但那反而会破坏沉浸感。
我想营造的,是一种“屏保”式的互动场景 —— 你可以什么都不做,也能安静地欣赏画面;你也可以用指尖激起涟漪,与深蓝交互。
背景颜色选择:
渐变从 #001f3f 到 #003366,这两个色值来自我在 Photoshop 里取色,还原了当时水族馆那个角落的光线深度。
涟漪颜色选择:
全部采用冷色系,但加入了轻微的亮度层级,从而让不同涟漪重叠时不会“糊”成一片,视觉上也有层次。
结构重用性设计:将逻辑与展示彻底分离
这个项目的 Canvas 动画逻辑虽然专属场景明显,但我仍刻意将其封装为一个组合式函数,原因有三:
- 便于测试和复用:将
useRippleEffect拆出来后,可以独立测试其逻辑,未来有机会也可嵌入其他 UI 中。 - 便于参数注入与主题切换:组合函数接受外部参数,能够在不同配置下重复使用,不需要硬编码。
- 逻辑分离更易维护:未来即使我要添加其他动画形式(比如粒子、星星、下雪),也不会影响当前结构。
效果呈现:就像水光在数字中跳舞
最终的成品运行起来,正如我一开始想象的那样 —— 安静、纯粹,充满深度。它没有多余的交互或信息噪声,只有你、光影和流动。
每一圈波纹从指尖缓缓展开,融合进深蓝色的背景,又悄然消失;每一次点击,都像是一滴水落入数字的湖泊,泛起一场柔和的回响。
我用代码,模拟出了一种记忆中的自然场景——那种朦胧的、几乎要被遗忘的静谧。
项目复盘:我在涟漪中学到的那些东西
回头看这个项目,从构想到完成用了大约两周时间,虽然体量不大,但过程里我确实收获了很多。
我重新学习了 Canvas 动画
原以为这东西已经很熟,但真写起来才发现很多细节过去忽视了,比如 devicePixelRatio 的处理,比如 globalAlpha 与 stroke 的叠加效果,再比如如何做到既清空画布又保持层次感。
我深入用了 Vue 3 的组合式 API
过去我更多习惯使用 Options API,这次彻底用组合式写了一遍后,体会到逻辑拆分的好处,尤其在动画这种逻辑密集型的模块里,简直就是刚需。
我更理解了“交互”的设计
不只是加按钮、做响应,而是一种体验设计。什么时候该触发反馈?反馈该持续多久?是同步还是延迟?这些都需要揣摩和调试。

写在最后:这不是“酷炫动画”,而是内心的投影
如果你问我,“这个项目对你来说最特别的地方是什么?”我不会说是用了什么技术,也不会说用了多新的框架。我会说,是它让我用指尖,去触碰了记忆里那个水光闪动的瞬间。
我没有想要做一个“炫技”的作品,也没有打算做成开源库。它就像是我写的一首小诗,藏在浏览器里的静静一页。
当别人点开页面,滑动一下,看见那圈圈蓝色扩散出去,我希望他们也能短暂地感受到那种 —— 属于海洋、属于光、属于我们心底深处 —— 那份静谧与波动。
















