注册
web

变“鼠”为“鸭”——为SVG Path制作FIFO路径变换动画,效果丝滑

一个月前我曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》,介绍了如何将汉字转为SVG Path路径进行展示和变换,以此为基础不妨畅想一下,用动画将一个汉字变为另一个汉字,听上去是不是很简单呢?下面动手实践一下:


我随便找了一个字体Aa剑豪体,然后随机选取了两个汉字:,再用上文提到的文章介绍的提取整体字形区块方法取出了SVG:


image.png


image.png


可以看到很简单就提取出了两个字整体的字形,下面用D3做一个简单的变换动画展示:


初始变换


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>鼠鼠我鸭</title>
</head>
<body style="text-align: center"></body>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script type="module">
const _svg = d3
.select("body")
.append("svg")
.attr("width", "1000")
.attr("height", "1000")
.style("zoom", "0.3")
.style("transform", "rotateX(180deg)");
_svg.append("g")
.attr("transform", "translate(0, 160)")
.append("path")
.attr("fill", "#3fd")
.attr("stroke", "black")
.attr("stroke-width", "4")
.attr("d", 上面提到的鼠的SVG_Path...)
.transition().delay(1000).duration(1200)
.attr("fill", "#ef2")
.attr("d", 上面提到的鸭的SVG_Path...);
</script>
</html>

这里调整了svg尺寸以及zoomtransform等属性更好的适应画面,还做了个g标签套住并将定位交给它,动画效果如下图所示:


Animation.gif


很明显的看到,效果非常奇怪,怎么一开始就突然变得很乱?一开始乱这一下显得很突兀,这是因为两段图像的path长度就相差很多,绘进方式也完全不一样,很难真正的渐变过去,我试了一个有优化此过程的库d3-interpolate-path,用上去效果也没有什么差别,而且它用的还是d3@v5版本的,不知道怎么path中还会穿插一些NaN的值,很怪异,看来只能自己做了。


想真正的点对点的渐移过去,可能还是有些难的,所以我想出了一个较为简单的方案,实现一种队列式的效果,的笔画慢慢消失,而则跟随在后面逐步画出,实现一种像队列中常说的FIFO(先进先出)的效果


首先就是拆解,做一个while拆分两个字所有的节点,然后再一步步绘上拆出来的节点以验证拆的是否完整,再才能进行后面的处理。


事先要将“鸭鼠”各自的path定义为常量sourceresult,将二者开头的M与结尾的Z都去掉(中间的M不要去掉),因为动画中字形是流动的,起止点不应提前定义。


拆分路径点


const source = 鼠的SVG_Path(没有MZ)...
const result = 鸭的SVG_Path(没有MZ)...
const actionReg = new RegExp(/[a-z]/, "gi");
const data = new Array();
let match;
let lastIndex;
while ((match = actionReg.exec(result))) {
data.push(result.substring(lastIndex, match.index));
lastIndex = match.index;
}
data.push(result.substring(lastIndex));

就这样就能把的部分拆开了,先直接累加到试验一下是否成功:


叠加试验


let tran = g
.append("path")
.attr("fill", "red")
.attr("stroke", "black")
.attr("stroke-width", "4")
.attr("d", "M" + source + "Z")
.transition()
.delay(800);

let step = "L";
data.map(item => {
step += item + " ";
tran = tran.transition().attr("d", "M" + source + step.trimEnd() + "Z").duration(20);
});

首先是把上面path独立出来改一改,变成红色的利于观看,然后下面慢慢的拼合上每个节点,效果如下:


Animation.gif


是理想中的效果,那么下一步就是加速FIFO先进先出的变换了:


FIFO先进先出


这一步是不能用SVG动画的,要用setInterval定时器进行动画调节,SVG始终还是只能处理很简单的path变化,效果不如直接变来的好,这里设计成把每一帧的动画存进一个方法数组然后交给setInterval计时器循环执行(写起来比较方便),先是改一下tran的定义,因为不是动画了,所以现在改叫path就好了,border也不需要了:


let path = g
.append("path")
.attr("fill", "red")
.attr("d", "M" + source + "Z");

就这样简单的初始化一下就好了,然后就是最核心的一个过程,path的绘制循序就像一个FIFO队列:


let step = "";
let pre = source;
const funs = new Array();
data.map(async function (item, i) {
step += item + " ";
match = pre && actionReg.exec(source);
if (!match) {
pre = "";
} else if (~["M", "L", "T"].indexOf(match[0])) {
pre = source.substring(match.index + 1);
}
const d = "M" + pre + (pre ? "L" : "") + step.trimEnd() + "Z";
funs.push(() => path.attr("d", d));
});

首先是pre负责的字形,这个字形是要慢慢消失的前部,这个前部不是所有的节点都能用的,而是"M", "L", "T"这种明确有点位的动作才行,毕竟这是动画的起始点。然后step就是代表,要一步一步累加。循环结束funs数组也就累计好了所有的帧(方法),然后用定时器执行这些带参方法即可:


const animation = setInterval(() => {
if (!funs.length) {
clearInterval(animation);
return;
}
funs.shift()();
}, 20);

这种方式虽然非常少见,不过这个定时器流程还是很好理解的过程,效果如下:


Animation.gif


是想象中的效果,但稍微有些单调,可以加上一段摇摆的动画配合变换:


摇摆动画


let pathTran = path;
Array(8)
.fill(0)
.map(function () {
pathTran = pathTran
.transition()
.attr("transform", "skewX(10)")
.duration(300)
.transition()
.attr("transform", "skewX(-10)")
.duration(300);
});
pathTran.transition().attr("transform", "").duration(600);

这段动画要不断赋值才能形成连贯动画,所以直接用path处理动画是不行的,因为上面计时器也是用到这个path对象,所以要额外定义一个pathTran专门用于动画,这段摇摆动画效果如下:


Animation.gif


时间掐的刚刚好,那边计时器停掉,这边摇摆动画也缓停了。


写的十分简便,一点小创

作者:lyrieek
来源:juejin.cn/post/7241826575951200293
意,供大家参考观赏。

0 个评论

要回复文章请先登录注册