因真实抽奖需精准停驻指定扇区、可复位重试及动态更新奖品,css的transform:rotate()易丢帧跳变、无法精确角度映射;canvas可控每帧旋转状态,支持平滑缓动停稳、像素级定位与服务端结果校验。

为什么用 canvas 而不是 CSS 动画做转盘
因为真实抽奖需要「精准停在指定扇区」+「可复位重试」+「支持动态更新奖品」,CSS 的 transform: rotate() 在动画中途取消或重设时容易丢帧、跳变,且无法精确计算最终角度对应哪个奖品。而 canvas 可控每一帧的旋转状态,配合 requestAnimationFrame 能平滑减速停稳,也方便后续加阴影、粒子、音效等增强反馈。
常见错误:直接用 transition + rotate() 做“伪转盘”,结果中奖后用户截图发现指针偏了 2°,奖品对不上——这不是视觉误差,是浏览器渲染和 timing function 累积导致的角度漂移。
- 必须用
canvas绘制扇区和指针,不要用 DOM 元素拼转盘 - 旋转逻辑写在 JS 里,用累计角度(
currentAngle)而非目标角度直接赋值 - 停转时用缓动函数(如
cubic-bezier(0.25, 0.46, 0.45, 0.94))模拟惯性,别用linear
Math.random() 不能直接当抽奖逻辑用
前端生成随机数只是“伪随机”,且无法防刷。用户打开控制台反复调 startRotate(),只要没服务端校验,就能无限抽。真正抽奖必须走后端接口,前端只负责「播动画」和「收结果」。
典型翻车场景:活动上线后发现某奖品被领光了,但前端还在继续显示“恭喜中 iPhone”,因为 JS 里写的 prizes[Math.floor(Math.random() * prizes.length)] 完全没查库存。
- 点击“开始”后,立即发请求到
/api/draw,带用户唯一标识(如uid或token) - 后端返回结构必须含
prizeId和angleOffset(该奖品对应扇区中心角度),前端据此算出总旋转角度 - 前端绝不自行决定中什么奖,
Math.random()只能用于非关键路径(比如转盘初始扰动)
怎么让转盘「看起来」转够三圈再停
用户心理预期是“转久一点才像真抽奖”,但实际只需保证最低旋转量(比如 1080° + 目标扇区偏移),再叠加一个固定基础圈数即可。硬编码“转三圈”反而会出问题:如果目标刚好在起始位置,不加偏移就变成原地不动。
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
错误写法:targetAngle = baseAngle + prizeAngle —— 缺少最小圈数兜底,用户会觉得“还没转就停了”。
- 设基础圈数为 3 圈(
3 * 360 = 1080),再加prizeAngle(目标扇区中心角),最后减去当前指针角度归零偏差 - 最终总旋转角度 =
1080 + prizeAngle - currentAngle(单位:度),然后转成弧度喂给 canvas - 动画结束回调里,必须校验后端返回的
prizeId和本地映射表是否一致,防止中间出错显示错奖
移动端点击失效或连点怎么办
H5 转盘最常卡在「点不动」——iOS Safari 对快速连续点击有 300ms 延迟,安卓部分 WebView 会吞掉第一次 touch 事件。单纯加 cursor: pointer 没用。
现象:用户猛点十次,只触发一次请求;或者点完立刻刷新页面,导致中奖状态丢失。
- 按钮绑定
touchstart而非click,并立即event.preventDefault() - 点击后立刻置灰按钮、禁用交互,并显示“抽奖中…”,避免重复提交
- 用
localStorage记录本次抽奖的requestId,若接口超时,重试前先查本地有没有未完成记录
复杂点在于:转盘动画期间用户切到后台,回来时 canvas 可能已失活,requestAnimationFrame 被暂停。得监听 visibilitychange,切回前台后手动补帧或重置状态——这点很容易被忽略。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










