相抵路径CSS动画可以实现元素在任意指定路径上定位或移动,用到两个业已成熟、被现代浏览器广泛支持的CSS属性:
offset-path CSS 属性指定一个供元素跟随的路径,并决定该元素在其父容器或 SVG 坐标系中的位置。这个路径可以是直线、曲线或几何图形,元素将沿着它定位或移动。在 offset-path 上定位或移动元素,通过 offset-distance CSS 属性实现。
给元素设置 offset-path,使用 path 函数,函数内的字符串用以描述路径细节,形式与含义等同于SVG <path> 元素的 d 属性值:
offset-path: path('M 20 100 L200 160 L380 20');
描述元素在 path 路径上的定位或移动位置,使用 offset-distance 属性,值通常用百分比表示:
offset-distance: 30%;
——这就是所谓的相抵路径动画的核心:约束元素的路径(offset-path)+ 元素在路径上的偏移距离(offset-distance)。
而 offset-path 路径来源于 SVG path 元素的 d 属性,路径描述的数据使用绝对单位(默认px),这样,当元素所在的父元素尺寸可以变大变小,CSS 初始设计的路径就不适用,需要动态改变,这得祭出JS,具体思路是:
- 使用 ResizeObserver API 监听父元素尺寸变化
- 父元素尺寸发生变化时给子元素动态创建 offset-path 路径
ResizeObserver 接口允许监视 Element 的内容盒、边框盒或 SVGElement 的边界框尺寸的变化。这个接口提供了一种避免无限回调循环和循环依赖的方法,它通过在后续帧中处理更深层次的 DOM 元素来实现这一点。根据规范,resize 事件应该在绘制前和布局后调用。
以下实例通过 ResizeObserver 实现对 textarea 元素的尺寸变化实施实时观察,且任何时候任何变化都同步将尺寸展示在 textarea 内。可以尝试拖曳 textarea 右下角改变其尺寸:
核心代码:
const myta = document.querySelector('#myta'); // 获取textarea
// 创建尺寸变化观察器
const Observer = new ResizeObserver(entries => {
entries.forEach(entry => {
const { width, height } = entry.contentRect; // 获取宽高
myta.value = `${width} * ${height}`; // 显示变化结果
});
});
// 对 myta 进行监视(实例化观察器)
Observer.observe(myta);
上例,ResizeObserver API 的使用并不复杂,就两个步骤:创建监视器用以观察元素尺寸变化并依据变化的尺寸处理逻辑业务、绑定指定元素进行观察。在本例基础上,把业务逻辑换成设置 offset-path 动态路径,就可以实现本文的命题。
为简化起见,这里设计的路径用Q指令绘制。Q指令继承上一个坐标点,以其为起点绘制一根到达终点的曲线,曲线弧度和朝向由控制点坐标控制,语法结构如下:
/* M 指令如果省略,Q 指令会以上一个操作的坐标点作为曲线的出发点 */
[M startX startY ]Q ctrlX ctrlY, endX endY
这里假设相抵路径从父元素的左上角(20,20)出发,曲线弧面朝下,控制点为(ctrlX,ctrlY),终点是父元素的右上角(endX,20)。其中的几个未知数:
- ctrlX :控制点x坐标 = 父元素宽度 ÷ 2
- ctrlY :控制点y坐标 = 父元素高度 × 1.8(系数可以微调)
- endX :终点x坐标 = 父元素宽度 - 20
这样设计,相抵路径在水平方向上自始至终保持始于左上角止于右上角的布局安排,曲线弧面永远朝下且基本抵近(有时超越、有时略拉开距离)父元素的下边框(实际应用应根据场景微调系数或设计更为精准的算法)。来看完整的实例演示,该示例可以调整父元素的尺寸,调整过程中子元素的运动路径会实时跟着基本适配父元素的尺寸:
完整代码:
<style>
/* 父元素 */
#papa {
width: 600px;
min-width: 200px;
height: 300px;
min-height: 100px;
border: 1px solid gray;
overflow: hidden; /* 调整元素尺寸不能使用默认值 visible */
resize: both; /* 宽高尺寸均可手动调整 */
position: relative;
margin: auto;
}
/* 子元素 */
#son {
position: absolute;
width: 40px;
height: 40px;
background: tan;
animation: move linear 6s infinite alternate;
}
/* 相抵路径动画 */
@keyframes move {
0% { offset-distance: 0%; }
100% { offset-distance: 100%; }
}
</style>
<div id="papa">
<div id="son"></div>
</div>
<script>
const papa = document.querySelector('#papa'); // 获取父元素
const son = document.querySelector('#son'); // 获取子元素
// 创建监视器(观察尺寸变化+设置动态 offset-path 路径)
const PathObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
const startX = 20, startY = 20;
const endX = width - startX, endY = startY;
const ctrlX = width / 2, ctrlY = height * 1.8;
son.style.setProperty('offset-path', `path('M ${startX} ${startY} Q ${ctrlX} ${ctrlY},${endX} ${endY}')`);
});
});
PathObserver.observe(papa); // 指定 papa 为监视对象
</script>
显然上面的演示不够专业,尤其是Q曲线控制点的算法过于粗糙,在垂直方向只实现了大体上的路径适配。这在普通应用场景来说已经足够了。我们只需注意:路径的初始设计时考虑的是基于父元素固定宽高,然后不论父元素的尺寸如何变化,我们都按一定的算法去计算路径的各个参数所需的数据,使之尽可能地适配父元素尺寸。一般来说,路径越复杂,逻辑业务的处理就越琐碎、难弄,需要准备好算法、仔细处理。本文的示例仅作为参考,如果能从中得到启发,善莫大焉。