纯前端实现交互式塔罗牌抽牌应用:洗牌算法与3D翻转动画
做这个交互式塔罗牌抽牌应用其实是有一天下班后突发奇想塔罗牌实体卡太大、翻起来麻烦手机上的小程序又总是广告弹窗不如自己花一个晚上写个干净的网页版。核心想法很简单——页面上铺开盘成一排的牌背点“洗牌”后牌序被打乱点任意一张牌它以 3D 翻转的动画翻开展示牌名、正逆位和一句通俗解释同时把抽过的牌记录在浏览器本地下次打开还能看到抽牌痕迹。整个项目不依赖后端没有数据库就是一个纯前端的单页面应用。虽然规模不大但该有的交互全都有洗牌动画、翻牌动效、随机算法、状态持久化。如果你正在学前端想找一个不太复杂又有点仪式感的小项目练手或者单纯想自己做一个随时随地能玩的抽牌工具这篇笔记里的设计思路和踩坑记录都能直接抄作业。1. 设计思路与功能拆解写代码之前我先把需求想清楚了不是做一个“看起来很酷但不好用”的演示页而是要让用户真的愿意每天打开抽一次。所以核心并不在塔罗占卜本身而是交互能不能给人“抽牌的感觉”。1.1 塔罗牌应用的核心交互模型实体塔罗牌的使用流程是洗牌、切牌、抽牌、翻牌、解读。放到网页上这五个动作必须全部保留否则用户会觉得“这是数据库查询不是抽牌”。我拆解了一下洗牌对应页面上的随机打乱操作必须让用户有“牌在动”的视觉反馈而不是直接刷新结果这是仪式感的重要来源。切牌实体操作里是从牌堆中分出一叠网页上我用“让用户在桌面上选择一张牌”替代。所有牌背面朝上平铺用户可以自由选择既还原了“切牌”里人为选择的部分又降低了实现难度。抽牌点击任意一张牌触发翻牌动画。翻牌用 CSS 的 3D 翻转实现牌背朝上时是一张图案翻过来后显示牌名和正逆位。解读每张牌预置含义文案正位和逆位展示不同内容保证解读不会千篇一律。这个交互模型是规划的核心也是后面所有代码的骨架。功能上我分为两个页面区块一个是抽牌区一个是历史记录区。抽牌区负责动画和结果展示历史记录区负责把每次抽牌的结果按时间倒序存在浏览器本地最多保留最近 50 条避免 localStorage 无限膨胀。1.2 技术方案选型为什么坚持纯前端选择纯前端方案最初是因为我不想搭服务器但做着做着发现这个选择在体验上也有明显优势页面加载后所有数据都在本地断网也能打开点“洗牌”后光影和牌背翻转的动画是即时的没有任何网络延迟。我用了原生 JavaScript 加少量现代浏览器特性没有引入框架。原因有两个一是这个项目的交互规模有限用 Vue 或 React 需要多一次构建配置反而绕远路二是纯原生代码对前端初学者更友好打开 HTML 文件就能跑理解起来没有黑盒。模块化方面我只用了 ES Module把卡组数据、洗牌算法、历史记录逻辑拆成三个独立文件。如果后续想扩展成多用户在线对抽再考虑引入 Vue 或 React 加 WebSocket 也不迟。单机场景下原生方案完全够用。2. 核心实现细节与交互设计这个项目的难点不在“随机抽一张牌”而在“如何让抽牌过程有仪式感”。这里展开聊聊数据结构、随机算法和翻牌动效三个核心细节。2.1 塔罗牌数据结构设计与正逆位处理塔罗牌标准牌组有 78 张牌分大阿卡纳22 张和小阿卡纳56 张。但很多轻量应用只使用大阿卡纳 22 张一是文案量小二是对非专业用户来说大阿卡纳的牌意更容易理解。我这个版本默认使用完整 78 张但数据结构从一开始就设计成了写 1 张和写 78 张没有区别。每张牌的数据结构我定义成这样{ id: 0, name: 愚者, arcana: major, // major 大阿卡纳minor 小阿卡纳 suit: null, // 小阿卡纳的权杖/圣杯/宝剑/星币大阿卡纳为 null number: 0, upright: 新的开始、冒险、天真, reversed: 鲁莽、缺乏方向、犹豫, image: cards/00.jpg }upright和reversed是两个固定字段翻牌后由随机结果决定显示哪个。这里有个细节正逆位的判断我用的是随机数而不是像实体洗牌那样由牌的物理方向决定。对纯前端应用来说这是最合理的方案因为一张图片随机旋转 180 度在视觉上容易显得歪斜体验不好。我在历史记录里也会存下每次是正位还是逆位方便回溯。2.2 洗牌算法从“看起来随机”到“验证随机”洗牌的常规做法是 Fisher-Yates 洗牌算法比sort(() Math.random() - 0.5)靠谱得多。sort那个方案的问题在于比较函数不符合排序的传递性不同浏览器可能产生不同的乱序效果而且分布不均匀——简单说就是有些牌更容易出现在某几个位置。Fisher-Yates 则是从后往前遍历每次从未处理的元素中随机取一个放到当前位置时间复杂度 O(n)洗完后每个排列出现的概率理论上是均等的。我实现的洗牌函数function shuffle(deck) { const arr [...deck]; for (let i arr.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [arr[i], arr[j]] [arr[j], arr[i]]; } return arr; }抽牌时并不是把这张牌从牌堆里永久移除而是从洗好的数组中取第一张。为什么不是“抽到哪张就移除哪张”因为这样会导致一局牌越抽越少最后只剩几张体验不完整。我的做法是每次“洗牌”时生成一份新的随机排列用户抽完一张后如果想继续抽可以再次点击“重新洗牌”把 78 张牌重新打乱。每局牌之间相互独立既避免了重复抽到同一张牌的尴尬又保留了“每次抽牌都是一个完整抽取过程”的仪式感。2.3 3D 翻牌动效与视觉反馈的实现翻牌是交互中最有仪式感的动作。实现用了 CSStransform-style: preserve-3d配合rotateY(180deg)做翻转牌分正面和背面两层背面的backface-visibility设为hidden。要点是正反两面的层级关系。我给每张牌设置了一个容器.card里面有两个子元素.card-back和.card-front。默认状态下面朝用户的是牌背点击后容器旋转 180 度牌面才转过来。同时我会在翻牌结束后延迟显示牌名和解释文案动画时长控制在 600 毫秒太短显得仓促太长用户会着急。.card { transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.4, 2, 0.6, 1); } .card.flipped { transform: rotateY(180deg); } .card-back, .card-front { position: absolute; inset: 0; backface-visibility: hidden; border-radius: 10px; } .card-front { transform: rotateY(180deg); }一个小细节过渡曲线用了cubic-bezier(0.4, 2, 0.6, 1)hmm等等我在实测时发现这个曲线在翻牌时会轻微回弹看起来像实体牌翻过去之后抖了一下效果很不错。但要注意回弹幅度不要太大否则会有廉价感如果你不喜欢回弹效果换回ease-in-out就好。3. 实操过程与核心环节实现这个部分我把最关键的实现代码贴出来从页面结构到逻辑一条线说清楚。3.1 页面结构与基础布局页面结构分三块顶部的标题和操作按钮中间的抽牌区底部是历史记录。基础布局我直接用 Flexbox牌桌区域宽度超过一定值后自动换行。div idapp div classheader h1赛博塔罗/h1 button idshuffleBtn重新洗牌/button /div div idtable classtable/div div idresult classresult-panel/div div idhistory classhistory-panel/div /div渲染 78 张牌时我用一个for循环生成.card结构然后把每张牌的位置设定为按顺序排布。实体塔罗牌是叠成一摞的网页上叠在一起不现实因此平铺显示。这里有个体验上的取舍78 张牌全平铺在桌面端会排得非常长。解决办法是加载时只显示一部分牌剩下的隐藏在屏幕边缘附近点击“洗牌”后再重新排列为了不煞风景我直接限定牌桌区域为两行每行最多显示 13 张多出部分横向滚动。3.2 抽牌逻辑与状态管理我在state.js里维护了几个核心状态const state { deck: [], currentPick: null, // 当前抽取的牌对象 history: [], locked: false // 动画期间禁止重复点击 };抽牌函数的核心有三步调用shuffle()生成乱序排列。点击某张牌时从该牌在 DOM 上的>function pickCard(index) { if (state.locked) return; state.locked true; const cardEl document.querySelectorAll(.card)[index]; const card state.deck[index]; cardEl.classList.add(flipped); setTimeout(() { // 正逆位判定 const isReversed Math.random() 0.5; const meaning isReversed ? card.reversed : card.upright; state.currentPick { ...card, isReversed, meaning, time: Date.now() }; renderResult(state.currentPick); saveToHistory(state.currentPick); state.locked false; }, 700); }这里有个小坑querySelectorAll(.card)[index]的方案依赖 DOM 顺序和state.deck顺序一致。如果列表被重新渲染过索引就会对不上。更稳妥的做法是在生成牌时直接用闭包绑定索引cardEl.addEventListener(click, () pickCard(cardIndex));闭包方案从根本上规避了“DOM 顺序和数据顺序不一致”的问题。我在第一版用了>function saveToHistory(record) { const history JSON.parse(localStorage.getItem(tarot_history) || []); history.unshift(record); if (history.length 50) history.length 50; localStorage.setItem(tarot_history, JSON.stringify(history)); }读取时直接在页面初始化时执行一次渲染。要注意 JSON 序列化和解析过程中的容错——localStorage 里的数据可能因为用户手滑改坏或者旧版本结构不兼容而解析失败所以解析要放在 try-catch 里解析失败就放弃这堆旧数据重新开始。4. 常见问题与排查技巧实录开发中踩了一些坑有些是逻辑问题有些是浏览器特性问题整理成速查表供参考。问题现象根因解决方案抽牌结果反复出现同一张随机范围不均或洗牌算法不当改用 Fisher-Yates 洗牌翻牌后文案还没渲染就消失动画期间状态被重置用locked锁状态localStorage 读不出记录JSON 格式被破坏try-catch 包裹解析牌背图片在部署后不显示路径是相对路径部署环境变更使用相对基路径/配置 publicPath手机端点击无反应移除了click事件兼容性使用pointerdown或保留click4.1 为什么牌越抽越像“每次都一样”这是随机应用常见的心理效应塔罗牌 78 张但大阿卡纳的牌在文案上给人印象更深抽到 5 张小阿卡纳可能完全没记忆抽到 2 次“愚者”就记住了。所以我为了防止用户觉得“算法有问题”在历史记录里增加了统计标签显示“最近 20 次中这张牌出现过几次”。用户看到统计后就会明白概率本身是这样不是应用在作弊情绪会平稳很多。这个设计建议所有做“随机结果展示”类应用的人都考虑一下在随机结果旁边直接展示一句“每次抽取相互独立”或者提供概率可视化从体验层面消除误会比用户质疑后解释要有效得多。4.2 翻牌动画与数据不同步的问题我的第一版代码在点击牌的瞬间就渲染了文案结果动画还没翻转完成文字已经在牌背下面透出来了非常出戏。后来把文案渲染放进setTimeout的 700ms 回调里也就是等翻转动画结束后再渲染。但这里也要注意setTimeout的延迟时间必须大于等于 CSS 的transition时长。如果 CSS 是transition: transform 0.6s那就至少等 600ms 再操作。用transitionend事件监听可以更精确但事件在一些极端情况下可能不触发比如标签页切到后台所以我最终用setTimeout保底给一个比动画时长略长的确定值。4.3 localStorage 数据被清空或被污染用户开了隐私模式、浏览器清理缓存、或者页面在 iframe 中被限制了存储都可能让 localStorage 读不到数据。这不是代码 bug但我需要让应用在这种环境下也能优雅降级——读不到就用空数组写入失败就静默跳过不阻塞抽牌功能本身。历史记录有最好没有也不影响核心流程。4.4 随机数生成不能重试浏览器里的Math.random()不支持“返回上一次的随机结果”所以像“重新洗牌后上次抽到的牌是什么”这种问题依赖随机数是永远无法回溯的。解决方案就是上面说的把每次抽取结果立刻写入 localStorage。这里补充一个实操建议记录里除了牌名还要把完整的时间戳存下来后续如果做“每日一张”“每周运势”这种功能直接按时间戳筛选即可不用额外迁移数据。写在最后塔罗牌应用本身是小众需求但做完这个项目我更看重它在前端基本功上的性价比数组洗牌、闭包绑定、CSS 3D 动画、本地存储四个前端核心知识点全部覆盖项目体量又控制在一个晚上能完成的范围。我实际使用中最大的感受是“仪式感”这个看不见摸不着的东西真的要靠在交互细节里一点点抠出来——洗牌按钮的微光、翻牌后的轻微回弹、文案出现的延迟时机每一样都在影响用户愿不愿意养成“每天抽一张”的习惯。如果你想在这个基础上继续玩可以扩展的方向也挺多加入韦特塔罗的完整牌面图片、增加“每日一张”的定时提醒、把结果页生成可分享的卡片图。核心代码都在剩下的就是你自己的想象力了。

相关新闻

网站错误代码最佳实践:别让500报错赶走你的客户

网站错误代码最佳实践:别让500报错赶走你的客户

网站错误代码最佳实践:别让500报错赶走你的客户 网站做好了没人访问,往往不是SEO没做好,也不是内容不行,而是用户点进来,屏幕弹出一个冰冷的“500 Internal Server Error”或者“404 Not…

2026/9/18 22:28:24 阅读更多 →
飞书不回?OpenClaw 走 TaoToken 通道后先查 channels logs

飞书不回?OpenClaw 走 TaoToken 通道后先查 channels logs

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/18 22:27:41 阅读更多 →
Claude Fable 5.1 在 Cursor 里跑长程 Agent,Key 走 TaoToken 统一通道

Claude Fable 5.1 在 Cursor 里跑长程 Agent,Key 走 TaoToken 统一通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/18 22:27:41 阅读更多 →

最新新闻

first-contributions 实战指南:用 git revert 安全撤销已推送的提交

first-contributions 实战指南:用 git revert 安全撤销已推送的提交

first-contributions 实战指南:用 git revert 安全撤销已推送的提交 【免费下载链接】first-contributions 🚀✨ Help beginners to contribute to open source projects 项目地址: https://gitcode.com/gh_mirrors/fi/first-contributions 本篇技…

2026/9/19 0:56:03 阅读更多 →
智慧医院集成平台实战:HL7/FHIR、EMPI与消息中间件选型

智慧医院集成平台实战:HL7/FHIR、EMPI与消息中间件选型

简介:这份《智慧医院集成平台建设方案》PPT面向医院信息中心、医疗信息化厂商及HIT项目规划人员,针对系统不断增多、信息孤岛、标准不统一、接口无法监管等现实痛点,给出从问题梳理到整体架构的完整设计思路。内容围绕服务总线、统一数据中心…

2026/9/19 0:56:03 阅读更多 →
校园兼职Android APP开发实战:从数据模型到上架的全链路解析

校园兼职Android APP开发实战:从数据模型到上架的全链路解析

简介:该PDF文献围绕Android大学生校园兼职APP的系统设计展开,面向移动开发初学者、毕业设计学生及对Android与PHP后台交互感兴趣的研发人员。内容完整覆盖Android客户端与PHP服务器端两大部分:客户端涉及注册、登录、发布兼职、兼职信息列表、…

2026/9/19 0:56:03 阅读更多 →
GraphPad Prism 5科研绘图核心逻辑:数据表类型决定统计可视化成败

GraphPad Prism 5科研绘图核心逻辑:数据表类型决定统计可视化成败

简介:本资源是一份面向科研人员与研究生的GraphPad Prism 5统计作图实战指南,聚焦SCI论文投稿所需的8类核心图表制作——折线图、柱状图、折线比较图、柱状比较图、散点图、气泡图、热图(Heatmap)与雷达图。内容覆盖原始数据与已处…

2026/9/19 0:56:03 阅读更多 →
Keil 5暗色护眼主题配置:从配色到global.prop详解

Keil 5暗色护眼主题配置:从配色到global.prop详解

每次打开Keil 5,那个刺眼的白色窗口都让我怀疑自己是在写代码还是在做眼科测试。做单片机开发的朋友应该都有同感——默认的MDK界面实在谈不上舒服,白底黑字配上饱和度极高的语法高亮,白天看还行,一到晚上加班,盯上两个…

2026/9/19 0:56:03 阅读更多 →
智慧矿山数据中台建设:从编码统一到冷热分层实战

智慧矿山数据中台建设:从编码统一到冷热分层实战

简介:这套70页PPT是一份面向煤矿行业信息化建设者、智慧矿山项目规划人员与方案架构师的完整解决方案,系统梳理了智慧矿山数据中台及管控一体化平台的建设路径。内容从煤矿行业背景切入,列举神东、陕北等13个亿吨级煤炭能源基地,对…

2026/9/19 0:55:03 阅读更多 →

日新闻

BP神经网络时序预测:滑窗长度与多窗口平均策略

BP神经网络时序预测:滑窗长度与多窗口平均策略

简介:面向机器学习、深度学习与数据建模学习者的一份完整研究文献,聚焦BP神经网络在农业产量预测中的应用。文档以1980—2018年全国棉花产量为样本,系统讲解数据归一化处理、激活函数原理、多层神经网络结构搭建及训练流程,展示敏…

2026/9/19 0:00:30 阅读更多 →
Transformer训练实时监控实战:基于MindSpore的损失曲线可视化方案

Transformer训练实时监控实战:基于MindSpore的损失曲线可视化方案

上个月调一个Deformable DETR模型,在单卡上要跑将近两天。第二天早上我下意识打开终端翻日志,发现loss从凌晨两点就开始往上爬,一路从0.8涨到1.35,整整六个小时没人发现。那六个小时的训练不仅白跑,还霸占着卡——等于…

2026/9/19 0:00:30 阅读更多 →
OpenCloud 中的 Go 类型安全转换库 spf13/cast:从零值回退到泛型 API 的完整实战指南

OpenCloud 中的 Go 类型安全转换库 spf13/cast:从零值回退到泛型 API 的完整实战指南

OpenCloud 中的 Go 类型安全转换库 spf13/cast:从零值回退到泛型 API 的完整实战指南 【免费下载链接】opencloud 🌤️ OpenCloud is the open source platform for file management, sharing and collaboration. Simple and sovereign. 项目地址: htt…

2026/9/19 0:00:30 阅读更多 →

周新闻

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验

AI SDK Harness 依赖更新指南:掌握 harness 包 SDK 依赖的升级、桥接同步与一致性校验 【免费下载链接】ai The AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and ag…

2026/9/16 19:03:19 阅读更多 →
Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化

Refine v5 Ant Design NumberField 组件实战:基于 Intl 的本地化数字格式化 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.com/GitH…

2026/9/17 7:57:36 阅读更多 →
Flutter应用改名全指南:从Android到iOS的配置与工具实践

Flutter应用改名全指南:从Android到iOS的配置与工具实践

刚接一个外包项目时,甲方要求把工程里临时用的应用名改成正式产品名。我本来觉得“改名”这种小事,打开配置文件改一行不就完了?结果真动手才发现,Flutter项目里“应用名称”根本不是一处配置,而是一整套散落在 Androi…

2026/9/17 10:19:14 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/16 22:31:27 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/15 21:39:18 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/16 22:32:59 阅读更多 →