421 行 CSS 做出完整卡牌交互:纯前端动效的克制做法

举报
哦啦啦啦啦 发表于 2026/09/26 17:23:02 2026/09/26
【摘要】 先看最硬核的一手:翻牌只用一条 transition塔罗交互的视觉核心是翻牌。这个项目用经典 CSS 3D 双面翻转实现:/* style.css */.tarot-card { width: 130px; height: 200px; position: relative; transform-style: preserve-3d; transition: transform .7...

先看最硬核的一手:翻牌只用一条 transition

塔罗交互的视觉核心是翻牌。这个项目用经典 CSS 3D 双面翻转实现:

/* style.css */
.tarot-card {
  width: 130px; height: 200px;
  position: relative;
  transform-style: preserve-3d;
  transition: transform .7s cubic-bezier(.4,.2,.2,1);
}
.tarot-card.is-flipped { transform: rotateY(180deg); }
/* 两面共用 backface-visibility: hidden; */
/* 正面(牌面)预旋转: transform: rotateY(180deg); */

三个关键点:

  1. transform-style: preserve-3d 在卡片上建立 3D 上下文,正反两面才能共用一条旋转轴;
  2. backface-visibility: hidden 保证只有朝外的一面可见——翻转过程中背面不可见,既正确又省绘制;
  3. 正面预先 rotateY(180deg) 是这套手法的精髓:父级旋转 180° 时,正面刚好转回朝前,"先翻过去再显示正面"就变成了"翻完正好是正面",动画语义零跳变。

JS 只负责加类:setTimeout(() => inner.classList.add("is-flipped"), 600 + i * 180)——把动画完全交给 CSS transition,代码里没有任何 requestAnimationFrame、没有 Web Animations、没有逐帧驱动。为什么?因为浏览器合成器对 transform 的 transition 是硬件加速路径,让浏览器管动画比自己一帧帧算又稳又快。

cubic-bezier(.4,.2,.2,1) 是 ease-out 风格的缓动——起跳快、收尾慢,符合"翻牌先有动作感、后稳稳落定"的观感。0.7s 的时长既让翻转可感知,又不拖沓。

洗牌与布局:洗牌是纯逻辑,动画在翻牌前

一个容易误解的点:这个项目没有 CSS 层面的洗牌动画。"洗牌"发生在 JavaScript 里、翻牌之前——TAROT_CARDS.slice() 复制全量牌池,循环 Math.floor(Math.random() * pool.length) 取下标、splice 摘除,即无放回的随机抽样;正逆位用 Math.random() < 0.5 掷硬币。

布局用 flex 一行搞定:

.card-area {
  display: flex; flex-wrap: wrap; gap: 18px; justify-content: center;
}

每张牌是一个 .tarot-slot(纵排:牌本体 + 位置徽章),卡尺寸 130×200,移动端 640px 断点降为 108×168——一套 flex + 一个媒体查询覆盖了所有屏幕。

入场动画是一段克制的小编排:

.tarot-slot {
  animation: slotIn .5s ease both;  /* opacity 0→1 + translateY(18px→0) */
}

JS 按 animationDelay = i * 0.18s 逐张错峰入场,翻牌延迟 600 + i*180 紧随其后——入场先错峰,翻牌再错峰,两段式编排让"抽了一排牌"有了先后层次,而不是同时扑出来。

状态机:语义化类 + 单对象 + 两个 setTimeout

整套交互的状态表达很统一:

  • CSS 状态全部用语义化类:.is-flipped(已翻开)、.is-active(牌阵选中/展示区激活)、.is-reversed(逆位卡样式)、按钮 :disabled;
  • JS 状态收敛在单个对象:state = { selectedSpread, question, drawnCards },配 classList.toggle 驱动上面的类。

整个流程是一条线性状态链:

选牌阵 → 提问(可不填)→ 抽牌(未选牌阵时按钮禁用)
  → 错峰入场动画 → 交错翻牌 → 渲染解读结果 + scrollIntoView 平滑滚动 → 重置

时序编排手段只有两个 setTimeout(入场延迟和翻牌延迟),总时长 = 600 + 张数×180 + 700。对一个最多 10 张牌的单页面,这就是够用的状态机——不需要状态管理库,不需要动画库,甚至不需要 Promise 链。

这个"极简状态机"还有个值得一提的细节:逆位不旋转几何,只换类。.is-reversed 改变正面的背景色/边框色/关键词色,而不是把牌转 180°——对用户,逆位语义通过视觉色差传达就够了;对浏览器,零布局成本(改颜色只触发 paint,不触发 layout)。

性能:只动合成器属性,其余不做

回看所有动画属性:翻牌 rotateY、入场 opacity + translateY、hover 抬升 translateY——全部是 transform/opacity,没有任何会触发 layout/paint 的 width/left/top 动画。这是性能决策的第一原则:动画只发生在合成器路径上,翻牌 0.7s 在独立合成层运行,不会引起整页重排。

第二个细节是省略:全项目没有 will-change、没有 translateZ(0) 提示。这在"性能优化玄学"盛行的前端圈里反而是清醒的——will-change 是写给浏览器的"提前分配合成层"提示,它对大规模动画有意义;但对 ≤10 张卡、单页面的场景,加了只会多占内存,不给任何可感知的收益。"够用即止"在这里不是偷懒,是对成本的正确评估。

DEVLOG 里能看到这套 CSS 的增长方式:390 行 → 420 行,只加了 30 行。这说明交互增强主要发生在 JS 和引擎层,样式层一直保持稳定——克制不是一次设计出来的,是每次改动都问"这 30 行真有必要吗"长出来的。

总结

421 行 CSS 做出完整卡牌交互,方法论可以浓缩成四条:

  1. 动画交给 CSS:transform transition + 加类,不用 JS 逐帧;
  2. 只动合成器属性:transform/opacity 之外不碰,天然高性能;
  3. 状态用语义化类:is-flipped/is-active/is-reversed,CSS 和 JS 通过类名对话;
  4. 规模决定取舍:小场景不加 will-change,两个 setTimeout 就够编排时序。

它证明了:纯前端动效的"高级感"不来自技术栈,来自对组合时机和缓动的控制。翻牌、错峰、抬升这三板斧用对,20 行 CSS 就足以撑起一个让人"哇"的瞬间。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。