theme: cyanosis
一个月前我曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》,介绍了如何将汉字转为SVG Path
路径进行展示和变换,以此为基础不妨畅想一下,用动画将一个汉字变为另一个汉字,听上去是不是很简单呢?下面动手实践一下:
我随便找了一个字体Aa剑豪体,然后随机选取了两个汉字:鼠和鸭,再用上文提到的文章介绍的提取整体字形区块方法取出了SVG:
可以看到很简单就提取出了两个字整体的字形,下面用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尺寸以及zoom
、transform
等属性更好的适应画面,还做了个g
标签套住并将定位交给它,动画效果如下图所示:
很明显的看到,效果非常奇怪,怎么一开始就突然变得很乱?一开始乱这一下显得很突兀,这是因为两段图像的path
长度就相差很多,绘进方式也完全不一样,很难真正的渐变过去,我试了一个有优化此过程的库d3-interpolate-path,用上去效果也没有什么差别,而且它用的还是d3@v5
版本的,不知道怎么path
中还会穿插一些NaN
的值,很怪异,看来只能自己做了。
想真正的点对点的渐移过去,可能还是有些难的,所以我想出了一个较为简单的方案,实现一种队列式的效果,鼠的笔画慢慢消失,而鸭则跟随在后面逐步画出,实现一种像队列中常说的FIFO
(先进先出)的效果
首先就是拆解,做一个while
拆分两个字所有的节点,然后再一步步绘上拆出来的节点以验证拆的是否完整,再才能进行后面的处理。
事先要将“鸭鼠”各自的path定义为常量source
、result
,将二者开头的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
独立出来改一改,变成红色的利于观看,然后下面慢慢的拼合上每个节点,效果如下:
是理想中的效果,那么下一步就是加速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);
这种方式虽然非常少见,不过这个定时器流程还是很好理解的过程,效果如下:
是想象中的效果,但稍微有些单调,可以加上一段摇摆的动画配合变换:
摇摆动画
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
专门用于动画,这段摇摆动画效果如下:
时间掐的刚刚好,那边计时器停掉,这边摇摆动画也缓停了。
写的十分简便,一点小创意,供大家参考观赏。
本文写作于2023年6月7日并发布于lyrieek的掘金,于2023年7月18日进行修订发布于lyrieek的阿里云开发者社区。