前端 · 设计 · 动效
纸卡动效拆解
打开菜单、翻过卡片、切换场景。先看原样 Demo 和实录,再用一个只改旋转中心的小实验,弄清网页的手感从哪里来。
Yhazrin
把菜单移到角落,点一下。它没有从屏幕正中间凭空冒出来,而是绕着一个角展开。再往下走,一张卡片翻了过去,另一面接着说话。画面里的东西仿佛有厚薄、有正反,也知道自己该从哪里进场。
这些动作并不罕见,难的是让它们属于同一个世界。菜单像纸,内容却像一块发光玻璃;卡片有了背面,文字却在翻转中倒着透出来——只要一处关系不成立,手感就散了。多加一点弹性,通常救不了它。
这篇文章从 playful-motion-patterns 1.1.0 学习包出发。它研究 Maxima Therapy,也附了一份叫 little wonders 的独立演示。下面保留这份演示自己的字体、颜色、插画和交互,不给它换成博客皮肤。我们先看它怎样运动,再把其中一个问题缩小到可以亲手比较。
先玩,再拆
先看学习包自带的完整 Demo
独立打开,不受文章布局限制 ↗
点击后在独立窗口中操作。字体、颜色、插画和交互保持学习包原貌;它是独立学习示例,不是 Maxima Therapy 网站的源码。
打开演示后,可以顺着三个位置看:点 Next world 切换首屏场景;展开 Our world 看纸卡导航;找到 Find your wonder 下的卡组,把一张卡翻过来。完整网页在独立查看器中运行,关掉后回到这里继续读。
如果你现在只想看看运动的节奏,下面是这份 Demo 的一段无声实录。视频适合看先后关系,截图适合停下来找位置;它们不能互相替代。
9 秒看一遍场景切换
画面文字说明
无声实录:学习包 Demo 先显示首屏,点击 Next world 后,圆形场景旋转到下一组内容。字体和插画保持原样。这不是 Maxima Therapy 原站录屏。
直接打开视频 ↗
直接录制学习包自带 Demo 的真实交互;未替换成本站视觉。视频用于先看节奏,完整演示用于亲手操作。
本文独立实验本次从学习包原样 Demo 录制 · 2026-10-19
这里先分清三件事:Maxima Therapy 是研究对象;little wonders 是学习包中的独立示例;后面蓝灰色的旋转中心实验,是本文新写的教学工具。 它们有联系,但不是同一份源码,也不该被说成同一个作品。关于完整项目的制作背景,可以读文末链接的原作者复盘。
菜单展开之前,先决定哪里不能动
一个元素从小变大,读者只会知道“它出现了”。如果它的一角始终钉在同一个地方,剩下的纸面绕着那里转,读者就有了另一种解释:“它原来收在那里,现在被打开了。”
这就是 transform-origin 值得先于 easing 调整的原因。缓动曲线控制速度怎样变化,旋转中心控制物体与空间的关系。后者没确定,前者越夸张,越容易像一张没有重量的贴图。
纸卡打开时,铰链和画面发生了什么
来自学习包原样 Demo 的实际截图,不是 Maxima Therapy 原站截图,也不是新画的一张本站风格示意图。
提供的原样素材playful-motion-patterns 1.1.0,原样运行截图
看截图时可以找两个位置:按钮在哪里,展开后的纸面压到了哪里。再回到演示里收起、展开,留意纸面从哪个角开始走。如果只盯着最终轮廓,就容易漏掉那个让动作成立的固定点。
学习包的历史捕获资料记录,所研究的纸卡导航采用右下枢轴,内部旋转时间线从约 90° 收到约 −4.23°,外层再控制这条时间线的进度。这是一份捕获版本的参数记录,不是本文重新测出的时长或通用配方。 内部时间线的长度,也不等于用户真正等候的时间;还要看它被怎样推进。
换到自己的界面里,不必先复制那几个小数。先把旋转中心画出来,检查展开和收起是否真的绕着它。只要这个关系清楚,幅度小一些也会有手感。
只改一个变量,看看差别
下面的实验有意不复刻完整网页。它只保留一块面板、一个展开动作,以及两种旋转中心。配色和字体属于实验本身,与上面的作品和外面的博客都分开。
只改旋转中心,手感会怎样变化
查看实际运行的源码
实际运行的 CSS
css
/* Geometry only; the shared kit owns typography, controls and working surfaces. */
.lab{max-width:760px;margin:auto}.lab header span{display:none}.controls label{font-size:12px}.reduce{margin-left:auto;display:flex;gap:6px;align-items:center}.scene{height:240px;position:relative}.paper{width:220px;height:164px;position:absolute;left:calc(50% - 110px);top:36px;padding:20px;background:#fff;border:0;border-radius:10px;box-shadow:0 7px 20px #192b4114;transform-origin:100% 100%;transform:rotate(75deg);transition:transform .7s cubic-bezier(.22,1,.36,1)}.paper.open{transform:rotate(-4deg)}.paper-label{font-size:12px;color:var(--jc-muted)}.paper-rule{height:8px;margin:9px 0}.scene .paper p{font-size:20px;line-height:1.4;font-weight:600;margin:0;color:var(--jc-ink)}.pivot{position:absolute;right:-5px;bottom:-5px;width:10px;height:10px;background:#e65d4d;border-radius:50%;outline:3px solid #e65d4d30}.paper.center .pivot{right:calc(50% - 5px);bottom:calc(50% - 5px)}#value{font:12px ui-monospace,monospace;color:var(--jc-muted)}.calm .paper{transition:none}@media(prefers-reduced-motion:reduce){.paper{transition:none}}
实际运行的 JavaScript
js
(() => {
const paper=document.querySelector('.paper');
const origin=document.querySelector('#origin');
const play=document.querySelector('#play');
const reset=document.querySelector('#reset');
const reduce=document.querySelector('#reduce');
const preference=matchMedia('(prefers-reduced-motion: reduce)');
let open=false;
const render=()=>{
paper.style.transformOrigin=origin.value;
paper.classList.toggle('center',origin.value==='50% 50%');
paper.classList.toggle('open',open);
document.documentElement.classList.toggle('calm',reduce.checked||preference.matches);
document.querySelector('#state').textContent=`旋转中心:${origin.selectedOptions[0].textContent} · ${open?'展开':'收起'}`;
document.querySelector('#value').textContent=`transform-origin: ${origin.value};`;
};
origin.addEventListener('change',render);
play.addEventListener('click',()=>{open=!open;render();});
reset.addEventListener('click',()=>{open=false;origin.value='100% 100%';render();});
reduce.addEventListener('change',render);
preference.addEventListener('change',render);
render();
})();
独立打开,不受文章布局限制 ↗
本文单独制作的变量实验,不是原作。它使用自己的中性实验界面;下方源码与实际运行的文件是同一份。
本文独立实验本文实验:旋转中心对照
先保持“右下角”,展开一次;收起,把旋转中心改成“中心”,再展开。面板、角度和过渡曲线没有换,换的只是绕哪里转。右下角更容易读成一个被固定住的铰链;中心更像一块在空中转动的面板。哪一个好,不取决于谁更热闹,而取决于界面想表达什么。
下方展开的 CSS 和 JavaScript 就是这个实验正在运行的文件,不是另摘的一段简化示意代码。想移植时,先读完整文件里的状态更新,再决定留下哪些部分。
这里还有一个容易被忽略的细节:动作允许中途改变主意。 试着连续点“展开 / 收起”。CSS 过渡会从当前插值状态转向新目标,而不是每次跳回第一帧。复杂的 GSAP 时间线也要面对同一个问题:输入会打断动画,不能假设用户总会等它播完。
“减少动态”则换了一道问题:状态是否一定要靠大幅度运动才能说清楚?在这个实验里,缩短或去掉过渡,并不妨碍读者知道面板已展开。完整作品可以有自己的处理策略;教学实验没有替它宣称做过无障碍验证。
翻卡片,不是把一张图片转过去
菜单解决了“从哪里来”,翻卡接着回答“另一面是什么”。
如果只给整个容器加 rotateY(180deg),前面的字也会跟着转过去;若正反面同时可见,翻到一半还会叠在一起。浏览器并不知道你想做的是纸卡。它只知道一组元素在三维空间里被变换了。
翻过去之后,背面仍然属于同一张纸
卡组演示实截图。这里要观察的是前后面的关系,不是把读者的注意力重新拉回博客的按钮和边框。
提供的原样素材playful-motion-patterns 1.1.0,翻卡状态截图
最小的双面结构通常长这样。它是结构示意,不是 little wonders 的源码摘录:
css
.card { perspective: 900px; }
.card__body {
transform-style: preserve-3d;
transition: transform 600ms ease;
}
.card__body.is-flipped { transform: rotateY(180deg); }
.card__face { backface-visibility: hidden; }
.card__back { transform: rotateY(180deg); }
perspective 提供观看距离;preserve-3d 保留子元素之间的三维关系;backface-visibility 避免一面的反向文字透出来;背面先转半圈,等整个卡体翻过去时,它才正好朝向读者。这里的 900px 和 600ms 只是说明写法的示例值,不是原作参数。
但视觉结构只完成了一半。背面藏起来时,它的链接和按钮是否还在键盘焦点顺序里?按返回,焦点回到哪里?读屏是否仍会读出被挡住的内容?这些问题不能用“看起来已经背过去了”来回答。实际项目要随状态管理 inert、可见性和焦点,而不是只改一个旋转类名。
卡片也不适合塞下所有正文。若背面需要读很长一段、还要滚动和操作多个按钮,翻面的趣味会变成找内容的阻力。让一张卡回答一个问题,通常比把整篇文章折进去更有效。
画面一起动,为什么反而会显得平?
轮盘切换、文字入场、插画偏移,如果全部同一时刻开始、沿同一个方向走、用同一条速度曲线,画面虽然很忙,空间却可能更平。读者看见的像是一整张截图在移动。
可以先把关系拆成三层:大形状交代场景变化,标题负责阅读入口,小装饰稍晚一点回应。不要把“稍晚”理解成每层统一加一个固定延迟。真正重要的是:哪个动作让人知道场景已经换了,哪个内容现在应该能读,哪个装饰可以不动。
回看上面的九秒视频,先只看大圆,再看字。这样比一次盯着所有元素更容易分辨主次。想检查一张卡的正反关系,就看静态图;想知道场景切换时谁先动,就回到视频或实际演示。选对观察方式,比多放几个 GIF 更有用。
另一个实现陷阱是让几个系统争用同一个 transform。外层负责滚动位移,内层负责 hover 倾斜,图案层负责局部旋转;每层只管一件事,往往比在一条字符串里拼出所有变换更容易维护。要不要增加这些层,应由当前效果决定,而不是预先搭一个庞大的动效框架。
借走机制,别连适用条件一起忘了
这种明亮、夸张的纸卡语言,适合希望人停下来探索的页面。换成结算、搜索结果或后台表格,同样的动作可能会挡住任务。评判它有没有帮助,不是数用了多少个动画,而是问:
- 展开过程有没有说明入口与内容的关系?
- 翻面以后,读者是否更清楚“同一对象的另一组信息”?
- 场景移动有没有提示变化,还是让正文更难读?
- 用户连续操作、只用键盘或减少动态时,状态还能不能成立?
真正值得带走的是一组约束:有固定点,就围绕它运动;有另一面,就管理正反关系;有前后景,就给运动分工。它们不要求你的页面也变成 little wonders,更不要求文章里的每个作品都长成这个博客的样子。
下一次做菜单,先试着只改旋转中心。下一次做翻卡,先把背面的焦点顺序走通。等这两件小事成立,再去挑弹性、加装饰,选择会清楚很多。
继续看原作者如何组织完整网站Nicolas Garnier 的制作复盘:轮盘、Lottie、路由层次与动画生命周期。读它能把局部技巧放回完整项目里。查看资料 ↗卡片预览使用学习 Demo 的截图;链接指向原作者文章,不把两者当成同一个作品。
资料与边界
本文依据 2026 年 10 月 15 日整理的 playful-motion-patterns 1.1.0 学习包及其历史捕获说明。原样演示的文件校验记录、第三方资源说明和字体许可随演示保留。文中的截图与九秒视频均来自这份学习 Demo 的本次运行,不是 Maxima Therapy 原站录屏;蓝灰色变量实验为本文另写。机制解释和适用性判断是本文的分析,不能当成对原网站所有运行状态的测试结论。