动画,就是几何对象在若干状态之间的切换,每次切换花费时间不小于 0,切换过程肉眼可见。
科研民工的工作成果主要是论文,数据可视化主要靠静态图片。所以我对动画的了解约等于零,这次来补课。ChatGPT 推荐学 Motion 库,在此之前最好了解一下基本的 CSS 动画。
动画,就是几何对象在若干状态之间的切换,每次切换花费时间不小于 0,切换过程肉眼可见。
transform
所谓“若干状态”中,很重要的一类状态是通过对 HTML 元素默认的形状和位置进行变换得到的。
transform 不一定用在动画中,它可以表示 HTML 元素的任意仿射变换,甚至用矩阵来表示变换的方式都来自于数学。
不用矩阵,也可以用独立的函数来表示平移、缩放、旋转、扭转,代码可读性更好。
#anime {
transform: matrix(1, 2, 3, 4, 5, 6);
transform: translate(120px, 50%);
transform: scale(2, 0.5);
transform: rotate(0.5turn);
transform: skew(30deg, 20deg);
transform: scale(0.5) translate(-100%, -100%);
}
CSS 动画大体可分为两类:
transition:HTML 元素在不同 pseudo-class 之间(鼠标悬浮、点击)切换时不同属性的渐变效果,一般需要用户输入来触发animation+@keyframes:用时间轴和关键帧描述的动画,不需要外界触发
transition
此类动画的不同状态由选择器和/或各种伪类指定:
#anime {
margin-right: none;
height: 1.5em;
}
#anime:hover { margin-right: 2em; }
#anime:active { height: 2em; }
在 CSS 的选择器里指定 transition 相关的属性,决定切换过程的细节:
#anime {
transition-delay: 0s; /*动画触发后等待的时间*/
transition-duration: 0s; /*动画持续的时间*/
transition-property: all | margin-right | ...; /*发生变动的属性*/
transition-timing-function: ease; /*变速函数*/
transition-behavior: normal | allow-discrete; /*二选一*/
}
以上多个命令可以合并成一行 transition,中间用空格分开
#anime {
transition: all 4s ease-in-out 1s allow-discrete;
/* property delay timing-function duration behavior */
}
Easing 缓动函数
transition-timing-function 指定了 transition 非匀速变化的具体形式。
ChatGPT 推荐的下面两个网站,直观展示了各种取值的实际效果:
animation + @keyframes
@keyframes 定义关键帧
此类动画的时间轴和关键帧由 @keyframes 定义。
动画的名字放在 @keyframes 之后,花括号之前。
花括号内以不同百分比表示每个关键帧在时间轴上的位置,百分比后的花括号内指定关键帧中元素的不同属性的取值。
@keyframes myanime {
0% {
top: 0;
}
50% {
top: 30px;
transform: rotate(0.5turn);
}
75% {
top: 10px;
transform: rotate(0.75turn);
}
100% {
top: 0;
transform: rotate(1.0turn);
}
}
如果只有 0 和 100% 两个时间点,还可以写成 from 和 to 关键字,更有可读性。
@keyframes my-anime {
from {
top: 0;
}
to {
top: 10px;
transform: rotate(0.5turn);
}
}
animation 使用动画
当某个 CSS 选择器里有 animation-name 属性时,被选择的 HTML 元素就具有动画名指向的动画效果。animation-* 的其他属性决定动画总时长等等属性,和上文 transition 的各属性类似。
#anime {
top: 5px;
animation-name: my-anime;
animation-delay: 5s;
animation-timing-function: ease-in-out;
animation-duration: 1s;
animation-behavior: allow-discrete;
}
以上多个命令可以合并成一行 animation,中间用空格分开:
#anime {
animation: all 4s ease-in-out 1s allow-discrete;
/* name delay timing-function duration behavior */
}
例子
TailwindCSS 中的自定义动画
语法和普通 CSS 不同。在自己项目的 global.css 文件中:
@theme {
--animate-wiggle: wiggle 1s ease-in-out infinite;
@keyframes wiggle {
0%,
100% {
transform: rotate(-3deg);
}
50% {
transform: rotate(3deg);
}
}
}
这样就可以在 HTML 中用 <div class="animate-wiggle"> 使用动画。
Headless UI 的 <Transition/>
有显示和不显示两种状态,由自身的 open 变量的布尔值确定。两个状态之间有进场和退场两个过程。结合 TailwindCSS:
- 普通的 TailwindCSS 类所指定的样式应用于显示状态
- 不显示的样式由
data-closed:前缀指定 - 进场过程中的变化由
data-enter:前缀指定 - 退场过程中的变化由
data-leave:前缀指定
import { Transition } from '@headlessui/react'
import clsx from 'clsx'
import { useState } from 'react'
function Example() {
const [open, setOpen] = useState(false)
return (
<div className="relative">
<button onClick={() => setOpen((open) => !open)}>Toggle</button>
<Transition show={open}>
<div
className={clsx([
// Base styles
'absolute w-48 border transition ease-in-out',
// Shared closed styles
'data-closed:opacity-0',
// Entering styles
'data-enter:duration-100 data-enter:data-closed:-translate-x-full',
// Leaving styles
'data-leave:duration-300 data-leave:data-closed:translate-x-full',
])}
>
I will enter from the left and leave to the right
</div>
</Transition>
</div>
)
}
一个宽度 48×4px 的 <div/>,在网页加载后隐藏,透明度为 0;
用户点击 Toggle 按钮之后,<div/> 从左向右平移出现,用时 100 毫秒;
再次点击 Toggle 按钮之后,<div/> 从右向左平移消失,用时 300 毫秒;
shadcn.ui 中的 <Accordion/>
上个版本的博客用的是 shadcn.ui 组件库,每篇文章的标题卡片用的是 <Accordion/> 组件,点击右侧的箭头之后,可以展开和收起文章的摘要。
后来换成 HeadlessUI 之后,展开和收起的动画效果就消失了。
现在来看一下源码(老版本的 shadcn 还在用 Radix-ui,现在这个库已经转投 base-ui 了):
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef(function AccordionItem({ className, ...props }, ref) {
return (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
)
})
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef(function AccordionTrigger({ className, children, ...props }, ref) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className={cn("h-4 w-4 shrink-0 transition-transform duration-200")} />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
})
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef(function AccordionContent({ className, children, ...props }, ref) {
return (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
)
})
AccordionContent.displayName = AccordionPrimitive.Content.displayName
可以看到里面用了 animate-accordion-up 和 animate-accordion-down 两个动画,它们定义在项目的 tailwind.config.js 中:
module.exports = {
theme: {
extend: {
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
}
}
}
这里的技术关键是 var(--radix-accordion-content-height) 是针对每个 accordion 实例计算的,是怎么做到的?Radix 的魔法?
这超出我的能力范围了, 以上面一整个小节为 prompt 问了 ChatGPT,回答放在了《ChatGPT | Radix Accordion 开合的动画效果》
本文收录于以下合集: