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

简介: 曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》介绍了如何将汉字转为SVG Path路径进行展示和变换,以此为基础用动画将一个汉字变为另一个汉字,感官上很好玩

theme: cyanosis

一个月前我曾撰文《使用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标签套住并将定位交给它,动画效果如下图所示:

d8cd6bc2bcf747a3bfe9a79c44b6678d~tplv-k3u1fbpfcp-watermark.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独立出来改一改,变成红色的利于观看,然后下面慢慢的拼合上每个节点,效果如下:

524cdb41fb0f43b9b5bfa4bcc1573ca7~tplv-k3u1fbpfcp-watermark.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);

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

e01f73b2d06345c7a289e9503b94a010~tplv-k3u1fbpfcp-watermark.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专门用于动画,这段摇摆动画效果如下:

shu.gif

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

写的十分简便,一点小创意,供大家参考观赏。

本文写作于2023年6月7日并发布于lyrieek的掘金,于2023年7月18日进行修订发布于lyrieek的阿里云开发者社区。

目录
相关文章
|
存储
CocosCreator3.8研究笔记(二十二)CocosCreator 动画系统-动画剪辑和动画组件介绍
CocosCreator3.8研究笔记(二十二)CocosCreator 动画系统-动画剪辑和动画组件介绍
316 0
[笔记]音视频学习之SDL篇《十一》图片 缩放 旋转
[笔记]音视频学习之SDL篇《十一》图片 缩放 旋转
147 0
|
数据安全/隐私保护 iOS开发 MacOS
CocosCreator3.8研究笔记(二十四)CocosCreator 动画系统-动画编辑器实操-关键帧实现动态水印动画效果(2)
CocosCreator3.8研究笔记(二十四)CocosCreator 动画系统-动画编辑器实操-关键帧实现动态水印动画效果
137 0
|
数据安全/隐私保护
CocosCreator3.8研究笔记(二十四)CocosCreator 动画系统-动画编辑器实操-关键帧实现动态水印动画效果(1)
CocosCreator3.8研究笔记(二十四)CocosCreator 动画系统-动画编辑器实操-关键帧实现动态水印动画效果
130 0
|
Python 容器
tkinter模块高级操作(二)—— 界面切换效果、立体阴影字效果及gif动图的实现
tkinter模块高级操作(二)—— 界面切换效果、立体阴影字效果及gif动图的实现
276 0
|
Python
Python实现超级玛丽游戏系列教程04背景滚动及摄像机(Camera)原理
Python实现超级玛丽游戏系列教程04背景滚动及摄像机(Camera)原理
108 0
|
人工智能 前端开发
Photoshop - 如何用 PS 合成一张 Sprite 图(雪碧图)
Photoshop - 如何用 PS 合成一张 Sprite 图(雪碧图)
398 0
Photoshop - 如何用 PS 合成一张 Sprite 图(雪碧图)
|
前端开发 JavaScript
巧用 SVG 滤镜还能制作表情包?
巧用 SVG 滤镜还能制作表情包?
288 0
巧用 SVG 滤镜还能制作表情包?
|
前端开发
我用css精灵图拼接了自己的英文名字,不会还有人不知道精灵图技术吧?
今天学习css精灵图技术,并且通过用它拼接自己的英文名字,拿起小本本记好了哦
182 0
我用css精灵图拼接了自己的英文名字,不会还有人不知道精灵图技术吧?