「掘金·启航计划」参加码上掘金编程比赛中实现花里胡哨的Canvas散点动画效果

1,446 阅读7分钟

花里胡哨的Canvas动画效果

我正在参加「掘金·启航计划」

作者的话

最近五一放假,本来打算在家好好科研的,但是确实不是科研的料,就不想科研,然后为了打发时间做了点前端小Demo,然后看见有码上掘金的比赛,就选了赛题一打算动手做一个好看的动画效果,花了半天的时间完成了 🥳。

五一总结,假期确实高产,可能也是因为找到暑期实习了吧,然后也想着放松放松,假期回去后就要开始好好把科研推进一下了,然后就趁假期写了三篇博客(因为找实习找了一个多月,好久没写博客了 😀):

效果展示

如果大家看了效果比较喜欢,那作者还有个不情之请,如果喜欢能不能帮忙给我的作品点点赞或者点点收藏或者浏览一下,下面附上链接:码上掘金,在玩转动画板块中哦 🥳

截屏2023-05-04 15.26.40.png

功能介绍

  1. 【功能1】:本动画主要使用Canvas实现,动画效果为粒子在页面上首先呈现为乱序的效果,接着在相同时间内,所有的粒子都运动到指定的位置上,最终拼成目标文字,如果有多行文字,会在指定时间间隔下相继展示出来。

  2. 【功能2】:本动画效果可以自适应横屏和竖屏,也就是在PC端浏览该效果文字是横向排列,如果是在移动端浏览该效果文字是纵向排列。

  3. 【功能3】:自适应文字大小,如果设置的文字太大超出屏幕,该效果会自动调整字体至最大占满页面的大小。

实现思路

这里大致说一下实现的思路吧,因为详细说明比较复杂,就简单介绍一下:

  1. 正如大家看到的效果一样(可以看封面动图,或者在代码中运行都可),这个动画由许多的点运动组成,因此需要定义一个点类,来控制每一步点的运动轨迹;

  2. 接着就是如何确定每个点的最终位置,那就需要先把文字写在自己的(不是最终显示的Canvas,自己可以额外创建一个)Canvas上,然后获取当前Canvas的像素信息,判断是否不为透明的像素,然后记录该像素的坐标和颜色

  3. 然后控制每个点由随机位置在相同时间内同时运动到指定的位置上,其实也就是把每一帧的图画出来,在下一帧的时候清空画布,再把当前帧的位置画出来,这样就能有一个动画的形式,那么控制画图的函数可以使用setInterval或者requestAnimationFrame,这个看自己的选择吧

💡 总结一下:其实很简单,就是获得目标文字在Canvas上的坐标和颜色,然后把每个点运动到正确的位置上即可。

定义每个点的类

这个类主要控制点的绘制和更新功能,因为每个点需要在每一帧更新当前的坐标,然后绘制出来。

因为之前也说过要绘制每个点点位置,因此需要知道如下几个参数:

  1. 起始坐标的X和Y
  2. 结束坐标的X和Y
  3. 每个点在X和Y方向上的移动速度
  4. 每个点的颜色和半径

下面展示一下具体的类的结构:

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直到最大可能的占满页面,搜索时使用的是二分查找的思想,使得时间复杂度降至 log2Nlog_2N

自适应屏幕

也就是实现了功能2,可以自适应横屏和竖屏,其实判断横竖屏不难,这个功能比较难的地方是怎么把文字竖着显示,然后获取到转换后的坐标。其实有想过使用ctx.rotate(90deg)然后再把转换后的坐标的横坐标X再加上Canvas的height就可以得到竖屏时的坐标。

🧐 如果大家有什么更好的想法可以评论区留言,让我学习一下

因此,作者想直接使用公式完成rotate(-90deg)translateX(canvas.height)这两个步骤,然后想起了CSS中的matrix,因此那就用线性代数的矩阵变换来实现。

现在回想一下学过的线性代数,rotate(-90deg)的变换矩阵如下:

(010100001)(xy1)=(yx1)\begin{pmatrix} 0 & -1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \\ \end{pmatrix} = \begin{pmatrix} -y \\ x \\ 1 \\ \end{pmatrix}

其中xxyy是每个点的横纵坐标,同理可以得到rotate(-90deg) translateX(canvas.height)的变换矩阵为:

(01width100001)\begin{pmatrix} 0 & -1 & width \\ 1 & 0 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix}

width为竖屏时的页面宽度

公式推导

如果不想看可以跳过

transform: rotate(-90deg)的推导

至于为什么rotate(-90deg)的变换矩阵为:

(010100001)\begin{pmatrix} 0 & -1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix}

下面进行一下数学公式的简单推导,如下是二维的笛卡尔坐标系,在第一象限中有原来的点(x,y)(x,y),在第二象限有逆时针旋转变换后的坐标(x,y)(x',y'),在第一象限与y的正方向的夹角为α\alpha,在第二象限与y的正方向的夹角为β\beta,且满足α+β=θ\alpha+\beta=\theta,具体参数如下图所示:

1.png

那么根据简单的三角函数性质可以得到如下等式:

{lsinα=xlcosα=ylsinβ=xlcosβ=y\left\{ \begin{aligned} & l \cdot sin\alpha=x \\ & l \cdot cos\alpha=y \\ & l \cdot sin\beta=x' \\ & l \cdot cos\beta=y' \end{aligned} \right.

根据α+β=θ\alpha+\beta=\theta,可以得到如下等式:

{lsin(θα)=xlcos(θα)=y\left\{ \begin{aligned} & l \cdot sin(\theta-\alpha)=-x' \\ & l \cdot cos(\theta-\alpha)=y' \end{aligned} \right.

利用三角函数公式将上述等式展开,可以得到:

{l(sinθcosαcosθsinα)=xl(cosθcosα+sinθsinα)=y\left\{ \begin{aligned} & l \cdot (sin\theta cos\alpha-cos\theta sin\alpha)=-x' \\ & l \cdot (cos\theta cos\alpha+sin\theta sin\alpha)=y' \end{aligned} \right.

将上述公式进行化简可以得到:

{ysinθ+xcosθ=xycosθ+xsinθ=y\left\{ \begin{aligned} & -y \cdot sin\theta +x \cdot cos\theta=x' \\ & y \cdot cos\theta +x\cdot sin\theta=y' \end{aligned} \right.

那么根据上述的公式,可以得到逆时针旋转θ\theta角度的矩阵计算公式:

(cosθsinθ0sinθcosθ0001)(xy1)=(xy1)\begin{pmatrix} cos\theta & -sin\theta & 0 \\ sin\theta & cos\theta & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \\ \end{pmatrix} = \begin{pmatrix} x' \\ y' \\ 1 \\ \end{pmatrix}

因此,rotate(-90deg)的变换矩阵可以把θ=90°\theta=90\degree 代入可以得到:

(cos(90°)sin(90°)0sin(90°)cos(90°)0001)=(010100001)\begin{pmatrix} -cos(90\degree) & sin(90\degree) & 0 \\ sin(90\degree) & cos(90\degree) & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} = \begin{pmatrix} 0 & -1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix}

🤪 这里为什么把90°90\degree带入而不是把90°-90\degree代入,这是因为推导的时候已经默认θ\theta是正值,然后公式已经默认推导的是逆时针旋转的公式,因此只需要代入90°90\degree即可。

transform: translateX(canvas.height)的推导

同理,可以得到translateX(canvas.height)的变换矩阵如下:

(10width010001)\begin{pmatrix} 1 & 0 & width \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix}

💡 这里变成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)的变换矩阵,那么:

(10width010001)(010100001)=(01width100001)\begin{pmatrix} 1 & 0 & width \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} \begin{pmatrix} 0 & -1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix} = \begin{pmatrix} 0 & -1 & width \\ 1 & 0 & 0 \\ 0 & 0 & 1 \\ \end{pmatrix}

写在最后

大家有什么问题或者建议可以评论区讨论一下,如果喜欢的话也可以点赞➕收藏 🌟,感谢大家的支持。

最后,再求一波点赞或者收藏,给作者的【花里胡哨的散点动画】点个小星星 🌟吧!