
简介HTML5跑马灯抽奖页面是一份面向前端学习者和活动页面开发者的网页源码适合电商大促、年会庆典等需快速营造互动氛围的场景。页面利用HTML5语义化标签组织骨架使用CSS3完成滚动动画、响应式布局和悬停反馈再借助JavaScript实现随机中奖、定时滚动与移动端触摸操作使用者可直接运行并按需调整。压缩包共19个文件仅167KB包含1个网页结构文件、1个样式表文件、1个JavaScript脚本文件、15张PNG图片和1张JPG背景图样式表控制关键帧动画与界面排版脚本处理抽奖逻辑和动画节奏图片素材覆盖背景、按钮、彩灯以及奖项展示位便于替换与二次开发。目前已有758人浏览学习分析入口文件中的语义化标签可理解活动页面模块划分阅读样式表的变换、过渡与关键帧写法可掌握跑马灯平滑滚动动效思路调试脚本中的随机数、定时器与触摸事件能掌握抽奖核心逻辑和移动端适配方法。整体代码紧凑、依赖少既适合快速搭建活动抽奖应用也是前端初学者理解页面动画与交互的实用素材。 去年八九月份帮一个做饮品店的朋友做门店周年庆他要的抽奖页面有两个硬性要求奖品一定要在屏幕上像跑马灯一样来回滚动滚起来要有那种“彩票开奖前心跳加速”的感觉同时页面必须是一个纯HTML5的单页店里那台老电视直接浏览器打开就能用。折腾完成之后这成了我手边被问得最多的前端小项目之一。这个项目最吸引人的地方在于它上手门槛不高一个下午就能把核心流程跑通但想做到滚动顺滑、停位准确、给别人复制去改还不翻车里面全是细节。这篇就把完整实现过程记录下来从布局到逻辑、从纯作业版到可以直接上线的活动版一步步拆开讲。无论你是正在发愁网页设计作业做什么还是要给社团、门店、年会做活动页这篇都适用前端零基础也能跟着做出来。1. 做之前先想清楚跑马灯抽奖的玩法与方案选型拿到需求先别急着敲代码得先分清“跑马灯抽奖”到底要做成什么形态。很多同学一搜“跑马灯”就直接去复制轮播图代码结果做出来的东西要么不能控制停止位置要么到了中奖环节全靠运气“随缘停”体验很糟糕。1.1 三种常见跑马灯形态先对号入座横向跑马灯奖品一字排开横向循环滚动中间放一个高亮框或者指示箭头点按钮后滚动速度逐渐减慢最终某个奖品停在高亮框中。这是最常见的形态适合门店大屏、网页设计作业视觉冲击力强实现上也是三种里最好上手的。纵向跑马灯中奖名单或奖品名称竖向滚动停在高亮横条里类似“开奖名单滚动播出”。这种形态比较适合颁奖环节气场更正式代码思路和横向一模一样无非是把X轴换成Y轴。环绕式跑马灯奖品围绕中心一圈匀速旋转中间放抽奖按钮。这种一般用于盲盒抽取、周年庆大屏视觉效果最炫但卡片角度、朝向、点击区域的处理成本会高不少。这几种形态没有绝对谁好谁坏关键看你现场用什么设备播、观众在几米外看。我这套页面按横向跑马灯来做代码主干通用纵向版本你只需要把位移方向改一下环绕版本等你有基础了再往后扩展。1.2 技术方案选型为什么是原生JS requestAnimationFrame跑马灯滚动效果的实现方案我盘过一遍大致有下面几条路各有各的“坑”。方案优点缺点适用场景纯CSS animation写法简单滚动平滑停止位置难以精确控制动态中奖结果不好对接只当背景装饰用setInterval 修改left/top逻辑直白计时不稳定低端设备上容易一顿一顿切后台会乱跳新手练手requestAnimationFrame transform跟随屏幕刷新率帧率稳定缓动可自己控制需要自己管理动画状态推荐本文采用Canvas / WebGL性能最强特效上限高开发量大选中状态、文字排版都要自己画礼品数量极大或需要粒子特效重点说一下为什么移动元素用transform而不是left。left属性移动会触发浏览器重新计算布局相当于每次动画都在“重新排队”性能开销大容易卡。transform则直接在合成层做平移GPU参与计算动画流畅很多。你可以理解为left是让整个队伍重新排队transform是把一个人平移过去省掉了一堆排队流程。配合requestAnimationFrame简称rAF控制每帧位置动画跟屏幕刷新率同步60Hz屏幕就是每秒60次更新比setInterval固定间隔要可靠得多。2. 页面结构与核心样式先让奖品“滚”起来我习惯先把滚动效果和样式跑通最后再塞业务逻辑。这一步做完页面已经能像模像样地循环滚动哪怕抽奖逻辑还没写光放那儿当背景也足够唬人。2.1 HTML结构三层结构各管一摊结构上拆成三层最外层是视口容器viewport设固定宽度和overflow: hidden负责“只让一部分奖品露出来”中间是轨道track所有奖品项都在这个长条轨道上排列靠transform做位移里层是单个奖品卡片li。另外再放两个装饰层左右渐变遮罩和高亮中奖框。div classdraw-container div classlamp-viewport idviewport ul classlamp-track idlampTrack/ul /div div classhighlight-frame idhighlightFrame/div p classresult-text iddrawResult点击按钮开始抽奖/p button classdraw-btn iddrawBtn抽 奖/button /div奖品项我在JS里统一生成不写死在HTML里。这样做的好处是后面改奖品列表、加权重、做奖品池剔除都只需要改一个数组不用去动DOM结构。2.2 无缝循环的关键复制奖品列表 transform跑马灯要看起来一直在滚坑在于滚动到最后一个奖品时不能露底。解决办法是把奖品列表复制三份拼在一个轨道上比如5个奖品轨道里就是15个卡片。因为整个轨道宽度远大于视口宽度滚动过程中用户永远能看到“内容不断接续”的效果根本不知道你已经循环了好几圈。const prizes [免单, 半价券, 第2杯半价, 小礼品, 再来一次, 奶茶券]; const COUNT prizes.length; function initTrack() { let html ; for (let i 0; i 3; i) { html prizes.map(p li p /li).join(); } document.getElementById(lampTrack).innerHTML html; }为什么复制三份而不是两份因为停止时目标位置要留出“再滚动一段距离”的余量如果只有两份停在第二份开头附近时归位操作会非常容易穿帮。三份是最稳的。2.3 CSS样式统一尺寸、渐变遮罩、高亮框轨道和卡片的CSS是这套页面里的重头戏。几个关键点每个li必须固定宽度而且不允许收缩轨道用flex布局位移只用transform。.lamp-viewport { position: relative; width: 700px; height: 110px; margin: 40px auto 20px; overflow: hidden; border-radius: 12px; } .lamp-track { display: flex; list-style: none; padding: 0; margin: 0; will-change: transform; } .lamp-track li { flex: 0 0 140px; width: 140px; height: 90px; margin: 10px 8px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 600; color: #ffd66e; background: rgba(255, 255, 255, 0.06); border: 1px solid #a67c2e; border-radius: 8px; }JS里计算位移时会用到ITEM_WIDTH这个值不是单纯的li宽度而是“一个卡片左右外边距”的总宽。上面代码里li宽140px、左右margin各8pxITEM_WIDTH就是156px。稍不留神就会在这里翻车后面第5节专门讲。渐变遮罩和高亮框是为了让视觉焦点更集中。遮罩用伪元素在视口左右两侧盖一层由实到透明的渐变高亮框则用绝对定位精确框住中间某个卡片的位置比如第4个卡片的位置。.draw-container::before, .draw-container::after { content: ; position: absolute; top: 0; bottom: 0; width: 90px; z-index: 2; pointer-events: none; } .draw-container::before { left: 0; background: linear-gradient(90deg, #1a1a2e 0%, transparent 100%); } .draw-container::after { right: 0; background: linear-gradient(270deg, #1a1a2e 0%, transparent 100%); }3. 抽奖核心逻辑先定结果再滚动画这是整个页面的大脑。很多新手的做法是“动画边滚边随机”滚动停了再判断停在哪个奖品上这种方式看着随机实际很难控制也容易出现结果尴尬的情况。我在这类页面上始终坚持一个原则先确定中奖结果再播放动画最后停到那个结果上。3.1 先定结果再放动画体验会完全不一样用户点下按钮那一刻系统立刻通过随机算法得出“本次中奖的是第几个奖品”然后动画只是把这个结果“演出来”。这样做有几个好处一是整个流程可控动画时长、减速曲线都由你说了算二是结果可以提前校验重复奖品剔除、库存检查都能在动画之前完成三是不管用户怎么连点最终结果都不会变。正式的商业活动场景里“抽奖结果”应该由后端接口返回前端只负责展示动画防止用户在浏览器控制台篡改逻辑。这篇文章先讲纯前端核心流程第4节再补线上注意细节。3.2 动画核心目标位移计算与缓动滚动动画的目标距离不是随便写个数字它要算清楚。假设当前已经滚了current像素希望最终停在数组里第index个奖品的位置目标偏移量应该是目标偏移 current ITEM_WIDTH × COUNT × (滚动圈数 1) index × ITEM_WIDTH后两项的意思是先在当前基础上多滚几整圈保证动画有足够时长和视觉冲击力再加一段偏移精确落到目标卡片。滚动圈数这里定3圈加上一个“1”是为了让目标点落在第二份列表上方便最后无缝归位。归位是关键中的关键。动画结束后如果直接把transform设成0或者设成某个小值页面会“闪一下”因为视口内容瞬间跳变了。正确做法是在关闭transition的情况下把当前偏移量对“一份列表总宽度”取模让轨道瞬间切回第一份列表对应的位置由于三份列表内容完全相同用户眼睛根本察觉不到。3.3 完整核心JS代码可直接复制体验这里给一份完整可跑的JavaScript核心代码配合上面的HTML和CSS就能工作。const prizes [免单, 半价券, 第2杯半价, 小礼品, 再来一次, 奶茶券]; const ITEM_WIDTH 156; const COUNT prizes.length; const CYCLE 3; const DURATION 4000; const track document.getElementById(lampTrack); const btn document.getElementById(drawBtn); const result document.getElementById(drawResult); let current 0; let running false; let lastIndex -1; // 初始化轨道复制三份奖品 function initTrack() { let html ; for (let i 0; i 3; i) { html prizes.map(p li p /li).join(); } track.innerHTML html; } // 加权随机权重越大越容易中 function randomIndex(excludeIndex) { const weights prizes.map((_, i) { if (i excludeIndex) return 0; // 不重复抽中同一个 return [1, 2, 4, 6, 8, 10][i] || 1; }); const total weights.reduce((a, b) a b, 0); let rand Math.random() * total; for (let i 0; i COUNT; i) { rand - weights[i]; if (rand 0) return i; } return COUNT - 1; } function draw() { if (running) return; running true; btn.disabled true; const index randomIndex(lastIndex); const target current ITEM_WIDTH * COUNT * (CYCLE 1) index * ITEM_WIDTH; const from current; let startTime 0; function step(now) { if (!startTime) startTime now; const progress Math.min((now - startTime) / DURATION, 1); const eased 1 - Math.pow(1 - progress, 3); const offset from (target - from) * eased; track.style.transform translateX(- offset px); if (progress 1) { requestAnimationFrame(step); } else { finishDraw(index); } } function finishDraw(index) { const normalized target % (ITEM_WIDTH * COUNT); current normalized; // 关闭过渡并强制回流避免归位闪烁 track.style.transition none; track.style.transform translateX(- normalized px); void track.offsetWidth; track.style.transition ; running false; btn.disabled false; result.textContent 恭喜抽中 prizes[index]; lastIndex index; } requestAnimationFrame(step); } btn.addEventListener(click, draw); window.addEventListener(DOMContentLoaded, initTrack);3.4 防连点与抽奖次数限制每次抽奖开始立刻把按钮置灰、并用running变量做状态锁双保险防止用户疯狂点击导致多个动画并发。如果活动限制每人一天只能抽3次可以配合localStorage做本地计数在点击时判断是否还能继续抽。不过正式活动这些属于底线保障真正的在线名额控制还得靠后端前端计数只是优化体验。4. 从作业到落地移动端适配与主题化改造核心功能跑通之后这个项目才是“半成品”。不管你是交网页设计作业还是真的要在门店投屏主题化和适配这两关绕不过去。4.1 移动端适配别只想着桌面大屏门店很可能用手推车推着一台iPad或者旧手机当展示屏你写的700px固定宽度直接溢出一屏幕。适配思路很简单卡片宽度根据视口动态计算。比较稳妥的方法是让奖品卡片宽度跟随屏幕宽度缩放const VIEW_COUNT 5; // 视口内预计展示几个卡片 const ITEM_WIDTH Math.min(156, Math.floor(window.innerWidth / VIEW_COUNT));页面加入标准viewport meta标签后再给容器宽度设成百分比或min(90vw, 700px)就能cover住手机、平板、大屏三类主要设备。字体大小建议用clamp()比如font-size: clamp(14px, 2.5vw, 20px)在不同屏幕上都不会显得太挤或者太大。还有一点容易被忽略奖品卡片里的图片一定统一尺寸不然加载过程中卡片高度跳来跳去跑马灯整体高度也会跟着抖。正式投屏前记得关掉浏览器的自动播放限制有些机器上首次点击按钮会伴随一个防抖延迟最好在真机上多试几次。4.2 主题化改造作业拿高分的关键并不在JS跑马灯抽奖核心逻辑大家都差不多拉开差距的地方全在主题和视觉。同样是交网页设计作业一个交白底黑字的朴素版一个交像素复古风格分数完全是两个档位。比如做复古像素风可以借鉴8-bit游戏那种气质深色背景铺像素纹理中奖框做成马里奥风格的金色问号砖块奖品卡片用Pixel字体抽中时让卡片边框闪三下。操作上其实不需要动JS只需要改CSS变量和几张装饰图。学年作业如果想做更热闹的“格斗游戏风”可以把按钮改成“挑战开始”中奖展示区做成血条样式每抽一次掉一格血血条空了活动结束。这种形式的本质还是跑马灯抽奖但交互叙事完全不同放在作业展示环节是很出彩的。不管什么主题记住一个思路逻辑层和样式层分开。奖品列表、JS核心全部不动只改外层视觉就能套出十几种不同风格页面。5. 真实项目里最容易踩的坑这几条全是我自己掉进去过的坑写出来给各位省点时间。5.1 停止位置对不上明明算好了要停在“免单”结果中奖框对准的是旁边的卡片。绝大多数原因是ITEM_WIDTH和实际渲染宽度不一致。CSS里如果给li设置了margin或gapITEM_WIDTH必须包含这部分如果li内部图片被拉伸导致实际尺寸变化那就要用JS动态测量第一个li的offsetWidth。动态测量是最稳妥的const firstItem track.querySelector(li); const ITEM_WIDTH firstItem.offsetWidth 16; // 16为左右margin之和5.2 动画结束闪一下多半是归位没处理好。如果直接把transform设回0或者设置了transition再改transform浏览器会播放一个“缩回去”的过程露馅是必然的。记住一个组合拳先设transition: none再改transform然后加一行void track.offsetWidth强制浏览器同步刷新样式再恢复原始transition保证下一次动画不受影响。5.3 抽奖结果不够“公正”纯前端代码里的随机算法理论上是能被懂行的人通过控制台修改的。正式商业活动、抽奖涉及真实奖品价值的结果一定要由后端生成前端只接收一个“第几个奖品”的参数。页面代码写得再好也不能拿来做有实际价值的抽奖。5.4 奖品概率跟直觉完全不一样直接用Math.random()均匀取下标意味着1元小礼品和免单大锦鲤的中奖概率是一样的这不符合大部分活动方的期望。加权随机是基础需求把每个奖品配一个权重数字权重越大越容易被抽中。想控制“一等奖百年难遇、再来一次满地跑”调权重表就行。我在实际活动里最常用的一组权重长这样奖品权重免单1半价券3第2杯半价8小礼品15再来一次20最后分享一个压箱底的小技巧每次活动上线前一定要找至少三个人分别抽五轮不是为了测“能不能中”而是盯着动画最开始的300毫秒和停止前500毫秒看。绝大多数用户对页面忙中的印象都来自启动那一瞬间的手感和减速停止的节奏感。把减速曲线里的三次缓动改成二次缓动停止手感都不太一样我最终保留了1 - Math.pow(1 - progress, 3)这条曲线加速够快、结尾够稳大屏上观看体验最好。本文还有配套的精品资源点击获取