说在前面
办公室下午 4 点,产品同学把电脑转过来:
“这个轮播图能不能高级一点?现在一切图就像 PPT 翻页”
我:“要多高级?”
产品:“像水面一样扩散开那种,最好再有点电影感”
大家平时做轮播图,常见方案基本是 opacity 淡入淡出、translateX 滑动、或者 3D 翻转。够用是够用,但“惊喜感”比较有限。
效果展示
实现思路
整体结构
HTML :一个 canvas + 左右按钮 + 底部指示器。
<div class="carousel" id="carousel">
<canvas id="canvas"></canvas>
</div>
<button class="nav-btn prev" id="prevBtn">◂</button>
<button class="nav-btn next" id="nextBtn">▸</button>
<div class="indicators" id="indicators"></div>
- 轮播“画面”只交给 Canvas 负责。
- 所有控件都是常规 DOM,交互实现更简单。
- 视觉重活在 JS 里做,样式层只负责外观。
cover 裁剪 + DPR 适配
很多同学写 Canvas 容易糊,问题常出在高清屏适配。
function resizeCanvas() {
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * devicePixelRatio;
canvas.height = rect.height * devicePixelRatio;
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
}
这段代码的作用:
- 物理像素按
devicePixelRatio放大,保证清晰度。 - 再用
setTransform把坐标系映射回 CSS 像素,避免你后面算坐标时崩溃。
另外它还实现了 cover 裁剪(类似 CSS background-size: cover),保证不同尺寸图片都能完整铺满画布,不留黑边。
“谁该出现”判定
const relDist = waveFront - dist;
if (relDist > waveWidth * waveCount) {
useNewImage = true;
} else if (relDist > 0) {
const wavePhase = (relDist / waveWidth) * Math.PI * 2;
const amplitude = waveAmplitude * Math.sin(wavePhase)
* Math.exp(-relDist / (waveWidth * waveCount) * 2);
const angle = Math.atan2(dy, dx);
srcX = x + Math.cos(angle) * amplitude;
srcY = y + Math.sin(angle) * amplitude;
useNewImage = relDist > waveWidth;
}
拆开理解:
dist是当前像素到波纹中心的距离。waveFront是当前时刻波前推进到哪里。relDist = waveFront - dist可以理解成“波前相对这个像素的位置关系”。- 根据
relDist分三段:- 波已过去:显示新图。
- 波正在影响:做径向位移(扭曲),并按圈层逐步切换到新图。
- 波还没到:继续显示旧图。
这就是为什么你会看到“像水波推着画面前进”的感觉,而不是生硬切换。
为什么要用离屏 Canvas?
这里用了 fromCanvas、toCanvas、outputCanvas 三个离屏画布
const fromData = fromCtx.getImageData(0, 0, pw, ph);
const toData = toCtx.getImageData(0, 0, pw, ph);
const outputData = outputCtx.createImageData(pw, ph);
作用有三点:
- 避免每一帧重复去主画布读像素(成本高)。
- 输出先在离屏里
putImageData,最后一次性drawImage到主画布,流程更稳。 - 绕开
putImageData不受 transform 影响的问题,DPR 场景更可控。
性能小技巧
像素级循环最怕卡顿,这里用了一个很实用的策略:按块采样。
const step = 2;
for (let y = 0; y < ph; y += step) {
for (let x = 0; x < pw; x += step) {
// 计算一次后,填充 step x step 区域
}
}
这相当于“每 2x2 像素算一次”,能明显减轻运算量。因为波纹本身是连续变化的,人眼很难察觉这点采样损失,性能却能换来很大提升。
交互
这个文件里交互是完整闭环:
goTo(index)统一处理切换入口,防抖靠isTransitioning。next/prev处理循环索引。setInterval(next, 4500)自动播放。- 鼠标悬停暂停、离开恢复。
- 键盘左右箭头也能切。
visibilitychange时页面隐藏就停播,避免后台空转。
源码地址
gitee
github
🌟 觉得有帮助的可以点个 star~
🖊 有什么问题或错误可以指出,欢迎 pr~
📬 有什么想要实现的功能或想法可以联系我~
公众号推广
关注公众号 前端也能这么有趣 ,获取更多有趣内容。
发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~
说在后面
🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。