花里胡哨的Canvas动画效果
我正在参加「掘金·启航计划」
作者的话
最近五一放假,本来打算在家好好科研的,但是确实不是科研的料,就不想科研,然后为了打发时间做了点前端小Demo,然后看见有码上掘金的比赛,就选了赛题一打算动手做一个好看的动画效果,花了半天的时间完成了 🥳。
五一总结,假期确实高产,可能也是因为找到暑期实习了吧,然后也想着放松放松,假期回去后就要开始好好把科研推进一下了,然后就趁假期写了三篇博客(因为找实习找了一个多月,好久没写博客了 😀):
效果展示
如果大家看了效果比较喜欢,那作者还有个不情之请,如果喜欢能不能帮忙给我的作品点点赞或者点点收藏或者浏览一下,下面附上链接:码上掘金,在玩转动画板块中哦 🥳
功能介绍
-
【功能1】:本动画主要使用Canvas实现,动画效果为粒子在页面上首先呈现为乱序的效果,接着在相同时间内,所有的粒子都运动到指定的位置上,最终拼成目标文字,如果有多行文字,会在指定时间间隔下相继展示出来。
-
【功能2】:本动画效果可以自适应横屏和竖屏,也就是在PC端浏览该效果文字是横向排列,如果是在移动端浏览该效果文字是纵向排列。
-
【功能3】:自适应文字大小,如果设置的文字太大超出屏幕,该效果会自动调整字体至最大占满页面的大小。
实现思路
这里大致说一下实现的思路吧,因为详细说明比较复杂,就简单介绍一下:
-
正如大家看到的效果一样(可以看封面动图,或者在代码中运行都可),这个动画由许多的点运动组成,因此需要定义一个点类,来控制每一步点的运动轨迹;
-
接着就是如何确定每个点的最终位置,那就需要先把文字写在自己的(不是最终显示的Canvas,自己可以额外创建一个)Canvas上,然后获取当前Canvas的像素信息,判断是否不为透明的像素,然后记录该像素的坐标和颜色;
-
然后控制每个点由随机位置在相同时间内同时运动到指定的位置上,其实也就是把每一帧的图画出来,在下一帧的时候清空画布,再把当前帧的位置画出来,这样就能有一个动画的形式,那么控制画图的函数可以使用
setInterval或者requestAnimationFrame,这个看自己的选择吧
💡 总结一下:其实很简单,就是获得目标文字在Canvas上的坐标和颜色,然后把每个点运动到正确的位置上即可。
定义每个点的类
这个类主要控制点的绘制和更新功能,因为每个点需要在每一帧更新当前的坐标,然后绘制出来。
因为之前也说过要绘制每个点点位置,因此需要知道如下几个参数:
- 起始坐标的X和Y
- 结束坐标的X和Y
- 每个点在X和Y方向上的移动速度
- 每个点的颜色和半径
下面展示一下具体的类的结构:
class Dot {
constructor(config) {
const { x, y, duration, color, radius } = config
// ...
// 点的颜色
this.color = color;
// 点的半径
this.radius = radius;
// 点的X起始位置
this.startX = Math.random() * width;
// 点的Y起始位置
this.startY = Math.random() * height;
// 点的X终点位置
this.endX = x;
// 点的Y终点位置
this.endY = y;
// 每帧动画中点的X方向的速度
this.speedX = (this.endX - this.startX) / duration / 60;
// 每帧动画中点的Y方向的速度
this.speedY = (this.endY - this.startY) / duration / 60;
}
// 画出点的位置
draw() {
// ...
}
// 更新点的位置
updated(flag) {
// ...
}
}
Draw函数比较简单,就是简单的Canvas画图操作,这里不再细说,具体代码可以在代码片段里查看;然后就是update函数,其实只要每次将当前点的X和Y坐标增加在该方向上的移动速度即可,这里需要注意的是因为计算机存储的原因,可能不能完全增加到完全相等,因此需要进行判断,作者这里就进行了简单的判断:
if (Math.abs(end - start) < Number.EPSILON) {
// ...
}
这里计算最终位置
end与当前位置start的差值,如果小于一个比较小的数字(Number.EPSILON),就认为到达最终位置了。
科普时间
这里插一句,介绍一下Number.EPSILON ,它是 JavaScript 中一个常量,表示浮点数的计算精度。它的值是 2 的负 52 次方,即 2^-52,约等于 0.0000000000000002220446049250313。
在进行浮点数比较时,可以使用 Number.EPSILON 这个极小值来判断两个数是否相等。例如:
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true
这里使用 Math.abs() 函数来获取两个数之间的绝对值,然后与 Number.EPSILON 进行比较,如果小于 Number.EPSILON 就认为两个数相等。这是因为在进行浮点数计算时,由于计算精度的限制,可能会出现一些微小的误差,使用 Number.EPSILON 可以消除这种误差的影响。
画图的类
这个类说起来比较麻烦,这里就介绍一下大概的执行流程吧,如果想要源码,可以去看代码片段。
执行流程:
flowchart TD
init -->|初始化画布| F
G -->|改变|H
F --> |"有任务 [1]"|M
F --> L
D --> |完成当前任务|N(pop)
subgraph init
H(初始化画布参数)-->I(设置文本渐变)-->J(设置font size)-->K[是否超过画布]
K --> |"是 [2]"|J
K -->|否|L(Canvas绘制文本)
end
subgraph run
F[isRunning] --> |无任务|G[文本是否改变]--> |不改变|B(getTextCoordinates) -->|坐标和颜色| C(draw) --> |画出每个点|D(move)
end
subgraph taskQueue
M(push)-->|压入任务|E("task=[]")
N-->|弹出任务|G
end
[1] 这里使用的是队列的思想,同时也借鉴了Vue2的异步更新策略
[2] 这里是调整字体的font size直到最大可能的占满页面,搜索时使用的是二分查找的思想,使得时间复杂度降至
自适应屏幕
也就是实现了功能2,可以自适应横屏和竖屏,其实判断横竖屏不难,这个功能比较难的地方是怎么把文字竖着显示,然后获取到转换后的坐标。其实有想过使用ctx.rotate(90deg)然后再把转换后的坐标的横坐标X再加上Canvas的height就可以得到竖屏时的坐标。
🧐 如果大家有什么更好的想法可以评论区留言,让我学习一下
因此,作者想直接使用公式完成rotate(-90deg)和translateX(canvas.height)这两个步骤,然后想起了CSS中的matrix,因此那就用线性代数的矩阵变换来实现。
现在回想一下学过的线性代数,rotate(-90deg)的变换矩阵如下:
其中和是每个点的横纵坐标,同理可以得到rotate(-90deg) translateX(canvas.height)的变换矩阵为:
width为竖屏时的页面宽度
公式推导
如果不想看可以跳过
transform: rotate(-90deg)的推导
至于为什么rotate(-90deg)的变换矩阵为:
下面进行一下数学公式的简单推导,如下是二维的笛卡尔坐标系,在第一象限中有原来的点,在第二象限有逆时针旋转变换后的坐标,在第一象限与y的正方向的夹角为,在第二象限与y的正方向的夹角为,且满足,具体参数如下图所示:
那么根据简单的三角函数性质可以得到如下等式:
根据,可以得到如下等式:
利用三角函数公式将上述等式展开,可以得到:
将上述公式进行化简可以得到:
那么根据上述的公式,可以得到逆时针旋转角度的矩阵计算公式:
因此,rotate(-90deg)的变换矩阵可以把代入可以得到:
🤪 这里为什么把带入而不是把代入,这是因为推导的时候已经默认是正值,然后公式已经默认推导的是逆时针旋转的公式,因此只需要代入即可。
transform: translateX(canvas.height)的推导
同理,可以得到translateX(canvas.height)的变换矩阵如下:
💡 这里变成width是因为先在横屏中写文字,因此横屏Canvas的height就等于变换后竖屏的width。
transform: rotate(-90deg) translateX(canvas.height)的推导
根据上面的推导我们已经得到了rotate(-90deg)和translateX(canvas.height)的变换矩阵,因此transform: rotate(-90deg) translateX(canvas.height)就等于rotate(-90deg)的变换矩阵乘translateX(canvas.height)的变换矩阵,那么:
写在最后
大家有什么问题或者建议可以评论区讨论一下,如果喜欢的话也可以点赞➕收藏 🌟,感谢大家的支持。
最后,再求一波点赞或者收藏,给作者的【花里胡哨的散点动画】点个小星星 🌟吧!