首页 / 资讯中心 / 文章详情

原神抽卡点名程序:纯前端HTML+txt实现随机抽取动画

原神抽卡点名程序:纯前端HTML+txt实现随机抽取动画 ★ FEATURED ARTICLE
1. 抽卡点名程序到底是个什么东西先说说我为什么折腾这个。前阵子帮朋友组织一个线下小活动几十号人围在一起需要随机抽人上台用手机上的随机数生成器吧屏幕太小大家看不清气氛也起不来。后来有人提议干脆做个“原神抽卡”风格的点名程序——按下按钮金光一闪角色立绘弹出来被点到的人名字跟着出现。这个点子一出来全场都觉得有意思。所谓“原神抽卡点名程序”本质上就是一个本地运行的随机抽取工具只不过把抽取结果的展示包装成了游戏里抽卡动画的样子。它的核心功能拆开来看其实很朴素读取一份名单、随机选一个、用动画和音效把结果呈现出来。但就是这层包装让它在课堂点名、年会抽奖、直播互动、社团破冰这些场景里特别好用。适合谁来参考这篇内容如果你是完全不懂代码的小白可以直接用我打包好的版本改个名单就能跑如果你有一点前端基础可以顺着我的思路自己改动画、换素材、加功能如果你是老师或者活动组织者只关心“怎么快速搞出一个能用的”那直接跳到实操部分照着做就行。这里要提前说明一点网上流传的很多版本会附带一些来路不明的可执行文件我强烈建议只使用纯前端方案也就是一个HTML文件加一个名单文件用浏览器打开就能跑。这样既不需要安装任何东西也不会有安全风险。下面我讲的这套方案就是基于纯前端实现的核心文件只有两个一个index.html一个names.txt。2. 整体设计思路与方案选型2.1 为什么选纯前端而不是桌面程序一开始我也考虑过用Python写个带界面的桌面程序或者用Electron打包成exe。但实际用下来纯前端方案有三个压倒性的优势。第一是零门槛分发。你做好之后把文件夹压缩发给别人对方解压双击HTML就能用不用装Python、不用配环境、不用担心系统兼容。我试过让一个完全不懂电脑的同事操作她三十秒就学会了。第二是动画表现力强。抽卡那种金光扩散、卡片翻转、粒子飘散的效果用CSS动画和Canvas实现起来非常顺手浏览器本身就是个成熟的渲染引擎。用桌面框架反而要额外折腾图形库。第三是名单文件好维护。名单就是一个普通的txt文件一行一个名字用记事本就能改。UTF-8编码保存中文不会乱码。这一点特别重要因为很多活动开始前名单还在变动能随时改是刚需。当然纯前端也有局限比如没法直接读取本地文件夹、没法调用系统级的功能。但对于“抽卡点名”这个需求来说这些局限完全不影响。2.2 名单文件为什么用txt而不是其他格式热搜词里出现了“UTF-8”“txt”这些关键词说明很多人卡在名单文件的编码和格式上。我专门说一下为什么选txt。txt是最通用的纯文本格式任何系统、任何编辑器都能打开。相比之下如果用JSON多一个逗号就报错对非技术人员不友好如果用CSV名字里带逗号就会出问题。txt一行一个名字简单粗暴容错率最高。但txt有个坑就是编码问题。Windows记事本默认可能保存成ANSI或者GBK而浏览器读取时如果按UTF-8解析中文就会变成乱码。所以保存名单时一定要选UTF-8编码。具体操作我在后面实操部分会详细讲。2.3 随机算法怎么保证公平点名程序最核心的要求是公平。我见过一些粗糙的实现用的是Math.random()直接取整理论上没问题但在实际使用中如果短时间内连续抽可能会因为随机种子的问题出现重复或者规律性。我的做法是洗牌算法加抽取队列。先把整个名单数组用Fisher-Yates算法打乱一遍然后按顺序取取完一轮再重新洗牌。这样保证一轮之内不会重复抽到同一个人而且每个人的概率完全相等。Fisher-Yates的核心是从后往前遍历每次和前面随机一个位置交换时间复杂度O(n)对于几百人的名单瞬间完成。如果你需要“抽过的人不再抽”就用这个队列模式如果需要“每次都可能抽到任何人”那就每次抽取前重新洗牌。两种模式我在代码里都留了开关。3. 核心细节解析与实操要点3.1 文件结构长什么样整个项目就三个东西结构极其简单抽卡点名/ ├── index.html 主程序包含所有逻辑和样式 ├── names.txt 名单文件一行一个名字 └── assets/ 可选放音效和图片素材 ├── draw.mp3 └── card.pngindex.html是唯一需要双击的文件。names.txt和它放在同一个目录下。assets文件夹是可选的如果你不需要音效和自定义图片可以不要。注意names.txt的文件名必须和代码里读取的路径完全一致大小写敏感。我踩过一次坑把文件命名成Names.txt结果在Windows上能跑传到Linux服务器上就404了。3.2 名单文件的编写规范打开记事本一行写一个名字不要有空行不要有空格不要有序号。像这样张三 李四 王五 赵六保存的时候文件名填names.txt编码选UTF-8。如果你用的是Windows 10以上的记事本右下角会显示当前编码点一下就能切换。如果用VS Code或者Notepad在菜单里选“编码”然后选“UTF-8”或者“UTF-8无BOM”。这里有个细节UTF-8带BOM和不带BOM的区别。BOM是文件开头的一个隐藏标记有些浏览器读取时会把BOM当成内容的一部分导致第一个名字前面多出奇怪的字符。所以建议保存成“UTF-8无BOM”。VS Code里叫“UTF-8”Notepad里叫“UTF-8”记事本里叫“UTF-8”一般默认都是无BOM的。3.3 读取名单的关键代码逻辑浏览器出于安全考虑不能直接用JavaScript读取本地文件。所以我们需要用fetch或者XMLHttpRequest来读取同目录下的names.txt。但这里有个问题如果你直接双击HTML文件浏览器用的是file://协议fetch会被跨域策略拦截。解决办法有两个。第一个是把文件放到一个本地服务器上比如用Python的http.server起一个或者用VS Code的Live Server插件。第二个是把名单直接内嵌到HTML里用一个script标签定义数组。对于完全不懂技术的用户第二种更省事。我的做法是提供一个双模式默认从names.txt读取如果读取失败就自动回退到HTML里内嵌的备用名单。这样无论用户怎么打开都能用。读取的核心代码大概长这样async function loadNames() { try { const response await fetch(names.txt); if (!response.ok) throw new Error(读取失败); const text await response.text(); // 按行分割去掉空行和首尾空格 return text.split(\n) .map(line line.trim()) .filter(line line.length 0); } catch (e) { // 回退到内嵌名单 console.warn(txt读取失败使用内嵌名单); return [张三, 李四, 王五, 赵六]; } }这段代码里split(\n)按换行符切分map去掉每行首尾的空格filter过滤掉空行。这三步缺一不可因为从不同系统保存的txt换行符可能是\n也可能是\r\n空格也可能混进去。3.4 抽卡动画的实现要点动画部分是这个程序的灵魂。我拆解一下金光扩散效果的实现思路。整个动画分三个阶段蓄力、爆发、展示。蓄力阶段是一个光圈从小到大旋转持续大概1.5秒制造期待感爆发阶段是白光一闪屏幕短暂变白然后金色粒子从中心炸开展示阶段是角色卡片从缩小到放大弹出来同时名字文字淡入。用CSS的keyframes就能实现大部分效果。比如光圈旋转keyframes spin { from { transform: rotate(0deg) scale(0.5); opacity: 0; } to { transform: rotate(360deg) scale(1.2); opacity: 1; } }粒子效果我用的是Canvas在爆发阶段生成几十个随机方向的小圆点每个有自己的速度和生命周期用requestAnimationFrame逐帧更新位置和透明度。这个比纯CSS灵活性能也更好。实操心得动画时长不要超过3秒。我一开始做了5秒的动画结果活动现场大家等得不耐烦。后来压缩到2.5秒左右节奏刚刚好。另外动画期间要禁用按钮防止连点导致状态错乱。4. 完整实操流程与核心环节4.1 从零开始搭建的详细步骤假设你完全没有基础跟着下面这五步走二十分钟内能跑起来。第一步建文件夹。在桌面新建一个文件夹名字随便起比如“抽卡点名”。第二步写名单。打开记事本一行一个名字写完后点“文件”→“另存为”文件名填names.txt保存类型选“所有文件”编码选“UTF-8”存到刚才的文件夹里。第三步创建HTML文件。再打开一个记事本把下面这段完整代码复制进去!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title抽卡点名/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #1a1a2e; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; font-family: Microsoft YaHei, sans-serif; overflow: hidden; } #stage { position: relative; width: 320px; height: 480px; display: flex; align-items: center; justify-content: center; } #halo { position: absolute; width: 200px; height: 200px; border: 4px solid #ffd700; border-radius: 50%; opacity: 0; } #halo.active { animation: spin 1.5s ease-out forwards; } keyframes spin { 0% { transform: rotate(0deg) scale(0.3); opacity: 0; } 50% { opacity: 1; } 100% { transform: rotate(360deg) scale(1.5); opacity: 0; } } #card { width: 280px; height: 400px; background: linear-gradient(135deg, #2d2d5e, #1a1a3e); border: 2px solid #ffd700; border-radius: 16px; display: flex; align-items: center; justify-content: center; transform: scale(0); opacity: 0; box-shadow: 0 0 40px rgba(255, 215, 0, 0.5); } #card.show { animation: pop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } keyframes pop { 0% { transform: scale(0.3); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } #name { font-size: 48px; color: #ffd700; text-shadow: 0 0 20px rgba(255, 215, 0, 0.8); font-weight: bold; letter-spacing: 4px; } #btn { margin-top: 40px; padding: 16px 64px; font-size: 20px; background: linear-gradient(135deg, #ffd700, #ff8c00); border: none; border-radius: 40px; color: #1a1a2e; font-weight: bold; cursor: pointer; transition: transform 0.2s; } #btn:active { transform: scale(0.95); } #btn:disabled { opacity: 0.5; cursor: not-allowed; } /style /head body div idstage div idhalo/div div idcardspan idname/span/div /div button idbtn开始抽卡/button script let names []; let queue []; let drawing false; async function loadNames() { try { const res await fetch(names.txt); if (!res.ok) throw new Error(fail); const text await res.text(); names text.split(\n).map(s s.trim()).filter(s s); } catch (e) { names [张三, 李四, 王五, 赵六]; } if (names.length 0) names [名单为空]; shuffleQueue(); } function shuffleQueue() { queue [...names]; for (let i queue.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [queue[i], queue[j]] [queue[j], queue[i]]; } } function draw() { if (drawing) return; drawing true; const btn document.getElementById(btn); const halo document.getElementById(halo); const card document.getElementById(card); const nameEl document.getElementById(name); btn.disabled true; card.classList.remove(show); nameEl.textContent ; halo.classList.remove(active); void halo.offsetWidth; halo.classList.add(active); setTimeout(() { if (queue.length 0) shuffleQueue(); const picked queue.pop(); nameEl.textContent picked; card.classList.add(show); setTimeout(() { drawing false; btn.disabled false; }, 800); }, 1500); } document.getElementById(btn).addEventListener(click, draw); loadNames(); /script /body /html第四步保存HTML。另存为index.html编码同样选UTF-8存到同一个文件夹。第五步打开运行。双击index.html浏览器会打开页面。如果名单读取成功点按钮就能抽卡如果读取失败会用内嵌的备用名单。4.2 参数调整与个性化修改上面这套代码是最简版本但已经能跑。如果你想调整下面这几个参数最常改。参数位置作用建议值keyframes spin的1.5s蓄力动画时长1.2s到2s之间keyframes pop的0.6s卡片弹出时长0.4s到0.8s#name的font-size名字字号根据名字长度调2到4个字用48px5个字以上用36pxsetTimeout的1500从点击到出结果的延迟必须和蓄力动画时长一致setTimeout的800出结果后的冷却时间防止连点500到1000ms改字号这个事我特别说一下。如果名单里有长名字比如“欧阳娜娜”四个字还好但如果是“阿卜杜拉·买买提”这种48px就会溢出卡片。稳妥的做法是用JavaScript动态判断名字长度超过4个字就自动缩小字号。代码里加一行nameEl.style.fontSize picked.length 4 ? 32px : 48px;4.3 音效的添加方法没有音效的抽卡是没有灵魂的。加音效很简单在HTML里加一个audio标签在抽卡时播放。audio idsfx srcassets/draw.mp3 preloadauto/audio然后在draw()函数里点击按钮后立即调用document.getElementById(sfx).play()。注意浏览器的自动播放策略首次播放必须由用户交互触发也就是必须在点击事件里调用不能页面一加载就播。音效文件哪里来我一般用免费音效网站找或者自己用Audacity录一段。文件不要太大100KB以内就够了格式用mp3兼容性最好。踩坑记录有一次我把音效文件放在assets文件夹里但路径写成了asset/draw.mp3少了一个s结果现场没声音尴尬得很。路径一定要仔细核对。5. 常见问题与排查技巧实录5.1 中文乱码问题全解析这是被问得最多的问题。现象是名单里的中文在页面上显示成“æŽå››”这样的乱码。原因只有一个txt文件的编码和HTML声明的编码不一致。HTML里我写了meta charsetUTF-8所以浏览器会按UTF-8解析。如果txt是GBK保存的就会乱码。解决办法就是把txt重新用UTF-8保存一遍。怎么确认当前编码用VS Code打开txt右下角会显示编码名称。如果是“GBK”或者“GB2312”点一下选“通过编码保存”然后选“UTF-8”。Notepad的话菜单栏“编码”→“转为UTF-8编码”注意是“转为”不是“使用”这两个选项不一样。还有一种情况是BOM导致的。如果第一个名字前面出现一个奇怪的符号那就是BOM。用VS Code打开右下角如果显示“UTF-8 with BOM”点一下改成“UTF-8”再保存。5.2 名单读取失败怎么办如果页面能打开但抽出来永远是备用名单说明fetch失败了。打开浏览器按F12看Console面板有没有红色报错。常见的报错和原因如下报错信息原因解决办法Failed to fetch用了file://协议起本地服务器或改用内嵌名单404 Not Found文件名或路径不对检查names.txt是否在同目录大小写是否一致CORS policy跨域限制同上起本地服务器Unexpected tokentxt里有特殊字符用纯文本编辑器重新保存最省事的办法其实是把名单直接写进HTML。在script开头把names数组直接赋值跳过fetch。对于不打算频繁改名单的场景这是最稳的。5.3 抽到重复名字怎么处理如果你希望一轮内不重复用我代码里的队列模式queue.pop()取完就没了取完自动重新洗牌。如果你希望每次独立随机把queue.pop()改成names[Math.floor(Math.random() * names.length)]就行。但要注意独立随机模式下人少的时候很容易连续抽到同一个人。我做过测试10个人的名单独立随机抽5次至少出现一次重复的概率超过60%。所以如果活动要求“不重复”一定要用队列模式。5.4 页面卡顿或动画不流畅老电脑或者手机浏览器上粒子效果可能会卡。解决办法是减少粒子数量或者干脆去掉粒子只保留光圈和卡片动画。我实测下来光圈加卡片这两个动画在所有设备上都很流畅粒子是锦上添花的东西性能不够就砍掉。另外box-shadow和filter: blur()这两个CSS属性很吃性能能少用就少用。我的代码里卡片用了box-shadow如果卡顿可以去掉换成简单的边框。5.5 常见问题速查表现象最可能的原因快速解决中文乱码txt编码不是UTF-8重新用UTF-8保存抽出来是备用名单fetch被拦截起本地服务器或内嵌名单按钮点了没反应JavaScript报错F12看Console动画播完不出名字setTimeout时长不匹配检查两个时长是否一致手机上看排版错乱没加viewport meta加上viewport标签音效不播放路径错误或格式不支持检查路径换mp3格式6. 进阶玩法与场景扩展6.1 多轮抽取与分组功能基础版一次抽一个人但实际活动中经常需要一次抽多个或者把抽到的人自动分组。这个扩展不难把draw()函数改一下用一个循环抽N次把结果拼成一个字符串显示就行。分组的话先抽够总人数然后按顺序每K个人一组。比如抽12个人分3组就是前4个一组中间4个一组后4个一组。显示的时候用换行或者卡片列表呈现。我做过一个版本一次抽6个人屏幕上并排显示6张卡片每张卡片依次弹出间隔0.2秒视觉效果很震撼。代码改动量不大主要是把单个卡片元素复制成多个用setTimeout错开动画时间。6.2 适配不同屏幕尺寸活动现场可能用投影仪也可能用手机。投影仪一般是16:9的宽屏手机是竖屏。我的做法是用vw和vh单位代替固定像素让卡片和字号随屏幕缩放。比如卡片宽度用min(280px, 80vw)字号用min(48px, 10vw)。这样在大屏上不会太小在小屏上不会溢出。min()函数现代浏览器都支持放心用。6.3 名单的动态更新如果活动进行中需要临时加人不用改代码直接改names.txt然后刷新页面就行。但如果用的是内嵌名单就得改HTML。所以我还是推荐用txt方案配合本地服务器改完刷新即可。起本地服务器最简单的方法在文件夹里打开命令行输入python -m http.server 8000然后在浏览器访问http://localhost:8000。前提是电脑装了Python。如果没装用VS Code的Live Server插件也行点一下按钮就起服务。6.4 数据统计与记录有些场景需要记录谁被抽到过、抽了几次。这个用localStorage就能实现每次抽取后把结果存到浏览器本地。刷新页面数据还在除非手动清除。代码很简单const history JSON.parse(localStorage.getItem(drawHistory) || []); history.push({ name: picked, time: Date.now() }); localStorage.setItem(drawHistory, JSON.stringify(history));需要看记录的时候遍历这个数组显示出来就行。注意localStorage有大小限制一般5MB存几千条记录没问题。7. 我踩过的那些坑和最后的小技巧先说几个我实际踩过的坑。第一个是文件名大小写在Windows上names.txt和Names.txt都能读但传到某些环境就报错所以从一开始就统一用小写。第二个是换行符Mac上保存的txt用\r换行Windows用\r\nLinux用\n我的split(\n)能处理大部分情况但保险起见可以先用replace(/\r/g, )把\r去掉再分割。第三个是空行名单末尾多敲了一个回车就会多出一个空名字filter那一步就是干这个的千万别省。再分享一个提升现场效果的小技巧在抽卡前加一个倒计时。点击按钮后先显示“3、2、1”然后再播动画。这个心理预期拉满现场气氛会好很多。实现就是在draw()开头加一个倒计时逻辑用setInterval每秒更新数字数到0再执行原来的动画。还有一个是关于名单顺序的。虽然洗牌算法保证了公平但如果名单本身是按某种顺序排列的比如按学号观众可能会觉得“怎么老是前面的人被抽到”。解决办法是在加载名单后先做一次可视化打乱或者干脆在页面上显示“名单已随机打乱”的提示消除心理疑虑。最后说一个扩展方向如果你想让这个程序更有“原神”味可以把卡片背景换成角色立绘抽到不同的人显示不同的图片。图片放在assets文件夹里用名字做文件名比如张三.png。代码里根据抽到的名字动态设置背景图。这样每个人都有自己的专属卡片仪式感直接拉满。图片尺寸建议统一成 280x400格式用png或者webp体积控制在200KB以内加载才快。这套东西我从最初的一个简陋版本改到现在这个能直接用的版本前后迭代了七八次。每次都是活动现场发现问题回来就改。现在这个版本在几十人的场子里跑过好几次没出过岔子。你拿到手之后先用自己的名单跑一遍确认没问题再上正式场合。名单文件记得多备份一份改坏了随时能恢复。
阅读完成 · 觉得有帮助?
咨询建站