纯前端Canvas美食转盘|解决选择困难!午餐随机抽签工具(附完整源码) 📅 2026/7/23 23:53:03 手机apk下载地址连接:https://download.csdn.net/download/javacainiao222/93155266一、项目背景每天一到饭点就陷入选择困难症:吃米饭、面条还是麻辣烫?纠结半小时都拿不定主意。 网上很多转盘工具功能简陋,不能自定义菜品、没有历史记录、界面粗糙,还不支持移动端适配。于是基于原生 HTML5 Canvas 手写一款午餐美食随机转盘,零依赖、纯前端本地存储,手机电脑都能流畅使用。项目核心亮点原生 Canvas 绘制转盘,无第三方图表库,加载速度快、动画丝滑;完整菜品管理:新增、编辑、删除、一键恢复默认,上限 20 个菜品;智能防重复抽取:近 3 天吃过的美食降低抽取优先级,避免天天吃同款;丰富动效:转盘加速 / 匀速 / 减速缓动动画、外圈彩灯闪烁、弹窗弹出动画;Web Audio 音效:转盘滚动滴答声、中奖提示音,支持一键开关音效;本地 LocalStorage 持久化:自定义菜品、抽取历史刷新页面不丢失;抽取记录面板:保存近 30 条午餐记录,自动折叠无记录区域;全端响应式布局,手机平板自适应,禁止双指缩放;轻量无框架,单 HTML 文件直接打开运行,无需后端、无需部署。二、功能整体介绍1. 转盘核心交互页面中央 Canvas 绘制彩色扇形转盘,点击转盘任意区域启动抽签:动画分三段:加速旋转→匀速滚动→缓动减速停下,4~5.5 秒随机时长,模拟真实抽奖手感;外圈 24 颗彩灯,旋转时交替闪烁,静止状态缓慢渐变;指针固定顶部,停下后自动弹出弹窗展示今日推荐美食;旋转过程锁定所有操作按钮,防止重复点击冲突。2. 菜品管理模块页面下方提供美食增删改全套能力:输入框添加菜品,限制单名称 10 字符,最大存储 20 个选项;标签形式展示所有菜品,支持在线编辑名称、一键删除;限制最少保留 2 个菜品,避免转盘无法转动;一键恢复系统默认美食列表(面条、饺子、麻辣烫、汉堡等 12 种常见午餐);重复添加、空输入、数量超限都会弹出 Toast 轻提示。3. 辅助实用功能音效开关:一键开启 / 关闭滚动滴答声、中奖提示音;抽取历史:自动记录每次抽到的菜品 + 日期,最多保存 30 条,无记录自动隐藏板块;智能规避重复:抽取时优先排除近 3 天吃过的美食,所有选项重复才随机全量抽取;全局轻提示 Toast:操作反馈悬浮顶部,2 秒自动消失;弹窗遮罩:中奖弹窗居中弹出,点击遮罩或确认按钮关闭。4. 存储机制使用浏览器 LocalStorage 本地缓存两组数据:lunch_wheel_foods:用户自定义菜品列表;lunch_wheel_history:历史抽取记录; 关闭页面、刷新浏览器数据不会丢失,清除浏览器缓存才会重置。三、关键技术实现解析1. Canvas 转盘绘制逻辑核心分为多层绘制,由外到内依次:彩灯外圈底色→菜品扇形分区→中心渐变圆→顶部红色指针:根据菜品数量均分 360° 圆周,循环绘制扇形并分配预设配色;扇形内文字自动缩放,长文本截断加省略号,增加文字阴影提升可读性;旋转通过全局currentAngle角度变量控制,每次重绘时整体旋转画布;彩灯通过循环计算圆周坐标,根据旋转状态切换亮灯 / 暗灯颜色。2. 分段缓动旋转动画采用requestAnimationFrame实现流畅动画,进度分为三个阶段:加速阶段(0~12% 时长):二次缓入,转速持续提升;匀速阶段(12%~45% 时长):保持最高速度滚动,滴答音效持续触发;减速阶段(45%~100% 时长):立方缓出,转速缓慢降低直至停止;停止前提前计算目标扇形角度,保证指针精准对准中奖菜品,不会出现偏差。3. Web Audio 原生音效不使用音频文件,通过 Web Audio API 动态生成波形音效:滚动滴答声:随机 800~1200Hz 正弦短音,扇形切换时触发;中奖提示音:三段递进三角波音符,形成简短喜庆旋律; 内置音效开关控制 AudioContext 启停,无外部资源依赖。4. 本地持久化存储封装简易 Storage 工具类封装读取、保存、清空逻辑,增加异常捕获,兼容低版本浏览器:const Storage = { save(key, data) { try { localStorage.setItem(key, JSON.stringify(data)); } catch(e) {} }, load(key, fallback) { try { const d = localStorage.getItem(key); return d ? JSON.parse(d) : fallback; } catch(e) { return fallback; } } };5. 响应式画布适配通过devicePixelRatio处理高清屏模糊问题:动态计算画布尺寸,限制最大宽度 90vw,适配手机屏幕;画布逻辑尺寸乘以设备像素比,CSS 显示尺寸缩小还原;窗口 resize 时重新计算尺寸、重绘转盘,无拉伸模糊。6. 防重复抽取算法抽取前读取近 3 天历史记录,生成排除集合:过滤掉近 3 天出现过的菜品,生成候选数组;候选数组有内容则仅在新菜品中随机;若所有菜品近 3 天都吃过,则放开限制全量随机; 解决连续几天抽到同款饭的痛点。四、完整源码使用说明使用方式新建文本文档,复制全部代码粘贴;修改后缀为.html;双击文件直接用浏览器打开,无需服务器、无需联网;支持 Chrome、Edge、微信内置浏览器、手机浏览器。操作步骤打开页面直接点击转盘中间「开始」启动抽签;下方输入框输入菜品名称,点击添加自定义选项;菜品标签点击铅笔图标编辑、× 图标删除;「恢复默认」一键重置内置午餐清单;点击音效按钮切换声音开关;每次抽取自动保存历史记录,下滑查看近期午餐。五、拓展改造思路(二次开发建议)自定义配色:修改代码中SECTOR_COLORS数组,替换转盘扇形主题色;增加权重抽取:给每个菜品设置权重,热门菜品抽取概率更高;导入导出菜品:增加 JSON 导入导出功能,多设备同步菜单;切换主题:深色模式、美食可爱风、商务简约风多套 CSS;增加自定义转盘标题:适配晚餐、下午茶、团建抽签等场景;增加清空历史按钮,一键删除所有抽取记录;导出历史记录:复制文本 / 下载 txt 保存每日吃饭记录。!DOCTYPE html html lang="zh-CN" head meta charset="UTF-8" meta content="width=device-width, initial-scale=1.0, user-scalable=no" title每日午餐美食随机转盘/title style /* ========== CSS Reset Base ========== */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { font-size: 16px; -webkit-tap-highlight-color: transparent; } body { font-family: "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif; background: #FFF8F0; color: #333; min-height: 100vh; display: flex; flex-direction: column; align-items: center; overflow-x: hidden; padding-bottom: 40px; } /* ========== Header ========== */ .header { text-align: center; padding: 24px 16px 8px; width: 100%; max-width: 600px; } .header h1 { font-size: 28px; color: #E8732A; font-weight: 700; letter-spacing: 2px; } .header p { font-size: 14px; color: #999; margin-top: 4px; } /* ========== Wheel Container ========== */ .wheel-container { position: relative; display: flex; justify-content: center; align-items: center; margin: 12px auto 20px; } #wheelCanvas { display: block; max-width: 90vw; max-height: 90vw; cursor: pointer; } /* ========== Result Modal ========== */ .modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.45); z-index: 1000; justify-content: center; align-items: center; animation: fadeIn 0.3s ease; } .modal-overlay.active { display: flex; } .modal-box { background: #fff; border-radius: 20px; padding: 36px 32px 28px; text-align: center; max-width: 340px; width: 85%; box-shadow: 0 12px 40px rgba(0,0,0,0.18); animation: popIn 0.4s cubic-bezier(0.34,1.56,0.64,1); } .modal-box .result-label { font-size: 16px; color: #999; margin-bottom: 8px; } .modal-box .result-food { font-size: 32px; font-weight: 700; color: #E8732A; margin-bottom: 20px; word-break: break-all; } .modal-box .btn-confi