一、前言
本文是在此基础上收到启发然后进行的变化,当然,观看与否不会影响接下来的阅读体验。
二、实现思路
其实整个波浪动画其实可以看成:在相对坐标系静止的视角下,一个正弦函数在直角坐标系上匀速平移时我们所观察到的效果。根据所选参照系的不同,我们也可以看成是:坐标系上有个点,我们在相对点静止的视角下,一个点在坐标系上按着正弦函数的规律来进行移动,此时我们所观察到的点走过的痕迹的变化就是波浪的动画效果。
从绘制角度来说,就是我们要绘制一条曲线按照正弦函数的规律去变化。在canvas中,能让曲线以正弦函数方式绘制的其中一个方法就是使用canvas提供的贝塞尔曲线绘制方法。
canvas提供的贝塞尔曲线方法有很多变种,比如一次贝塞尔、二次贝塞尔、三次贝塞尔等等,在这里我们主要使用了三次贝塞尔,三次贝塞尔曲线有一个起点、终点和两个控制点。其他的在此不做讨论。
言归正传,来讲讲Fabric.js。Fabric.js 没有提供类似于canvas中的绘制贝塞尔曲线的API(quadraticCurveTo
、bezierCurveTo
等),但是在线段绘制类Path
里,Fabric.js引入了SVG中path元素的线段绘制写法,而这就让我们在Fabric.js中绘制贝塞尔曲线的梦想得以触手可及。
在这里我们简单介绍一下SVG中path元素的部分语法:
2.1、M(moveto)移动和L(lineto)画直线
其中 M 表示移动到某点,L 表示画一条直接到某点,后面跟一个坐标点,建议格式如下:
"M x y" // M是命令:x横轴坐标、y纵轴坐标
"L x y" // L是命令:x横轴坐标、y纵轴坐标
2.2、 Z(cloasPath)闭合
结束点到开始点画一条直线,形成一个闭合的区域。
语法格式就是在字符串的最后写一个Z,表示闭合。
2.3、C(curveto)三次贝塞尔曲线
三次贝塞尔曲线有一个起点、终点和两个控制点,跟二次贝塞尔相比多了一个控制点,如图,P0、P3是起点和终点,P1、P2是控制点。
// 起点(x0,y0),控制点1 (x1,y1),控制点2 (x2,y2),终点 (x3,y3)
"
M x0 y0
C x1 y1, x2 y2, x3 y3
"
介绍完了SVG语法之后我们来看看如何实现波浪的效果。
三、代码实现
3.1、运行环境
fabric v5.2.4
vue v3.2.25
3.2、画布搭建
HTML
<template><div class="container"><canvas class="canvas" id="fabric" width="500" height="500"></canvas></div>
</template>
SCSS
.container {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;.canvas {border: 1px solid #ccc;}
}
TS
interface WaveConfig {/** 波浪颜色 */colors: string[];/** 想要的波浪高度 */waveHeight: number;
}
const config:WaveConfig ={waveHeight: 240,colors: ['#44c7b399'],
};
const canvas = new fabric.StaticCanvas('fabric'); // 初始化绑定画布
const height = canvas.getHeight(); // 获取画布的高度
const width = canvas.getWidth(); // 获取画布的宽度
const waveHeight = height - config.waveHeight; // 波浪高度
let step = 0; // 位移长度
上述代码中的step
变量代表点在x轴上走过的路程。
3.3、绘制贝塞尔曲线
接下来我们先利用Path
类画出一条贝塞尔曲线。由于画线时我们需要获取点的y轴值,所以我们把step
比作正弦函数的x
,带入进正弦函数中拿到曲线对应的y
值。
const angle = step * Math.PI / 180; // 由于step是长度,首先得转换成角度
const Y1 = Math.sin(angle) + waveHeight; // 拿到第一个控制点的高度
// 拿到第二个控制点的高度,也可以写成 Math.sin(angle + Math.PI / 2) + waveHeight
const Y2 = Math.cos(angle) + waveHeight;
在这里我们选取画布对称分布的两个位置为控制点的x
值
const X1 = width / 3;
const X2 = width / 3 * 2;
接下来绘制我们的贝塞尔曲线
const line = new fabric.Path(`M 0 ${waveHeight + Y1} C ${X1} ${waveHeight + Y1}, ${X2} ${waveHeight + Y2}, ${width} ${waveHeight + Y2}L ${width} ${height}L 0 ${height}L 0 ${height / 2 + Y1}Z`,{ fill: config.colors[0], stroke: '' });
最后让step改变,以启动我们的波浪动画
let step = 0;interval = setInterval(() => {// 清空画布canvas.clear();step += config.value.speed; // 控制波浪的变化速度const angle = step * Math.PI / 180;const Y1 = Math.sin(angle) + waveHeight;const Y2 = Math.cos(angle) + waveHeight;const X1 = width / 3;const X2 = width / 3 * 2;const line = new fabric.Path(`M 0 ${waveHeight + Y1} C ${X1} ${waveHeight + Y1}, ${X2} ${waveHeight + Y2}, ${width} ${waveHeight + Y2}L ${width} ${height}L 0 ${height}L 0 ${height / 2 + Y1}Z`,{ fill: config.colors[0], stroke: '' });// 添加到画布中canvas.add(line);}, 15);
四、完整代码
到此一个基本的动画就完成了,我们可以在上面进行拓展,通过调整一些参数来改变波浪动画的细节效果,例如波浪速度、波浪起伏高度、波浪条数等等。
HTML
<template><div class="container"><canvas class="canvas" id="fabric" width="500" height="500"></canvas></div>
</template>
SCSS
.container {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;.canvas {border: 1px solid #ccc;}
}
TS
import { fabric } from "fabric";
import { onMounted, ref, onBeforeUnmount } from "vue";
onMounted(() => {const canvas = new fabric.StaticCanvas('fabric');const height = canvas.getHeight();const width = canvas.getWidth();const waveTrajectory = config.value.waveTrajectory; // 波浪运动轨迹const waveFluctuate = config.value.waveFluctuate; // 波浪起伏高度const waveHeight = height - config.value.waveHeight; // 波浪高度waterStartMove();
});
onBeforeUnmount(() => {clearInterval(interval);
})
interface WaveConfig {/** 波浪条数 */lines: number;/** 波浪颜色 */colors: string[];/** 波浪高度 */waveHeight: number;/** 波浪的变化速度 */speed: number;/** 波浪运动轨迹 */waveTrajectory: number;/** 波浪的起伏高度 */waveFluctuate: number;
}
const config = ref<WaveConfig>({lines: 2,waveHeight: 240,colors: ['#44c7b399', '#44c7b34d'],speed: 2,waveTrajectory: 90,waveFluctuate: 8
});
let interval: any;
/**
* 让水动起来
*/
function waterStartMove(): void {let step = 0;// 清空canvas.clear();step += config.value.speed; // 控制波浪的变化速度for (let i = 0; i < config.value.lines; i++) {const angle = (step + i * waveTrajectory / config.value.lines) * Math.PI / 180;const deltaHeight = Math.sin(angle) * waveFluctuate;const deltaHeightRight = Math.cos(angle) * waveFluctuate;const line = new fabric.Path(`M 0 ${waveHeight + deltaHeight} C ${width / 3} ${waveHeight + deltaHeight - waveFluctuate}, ${width / 3 * 2} ${waveHeight + deltaHeightRight - waveFluctuate}, ${width} ${waveHeight + deltaHeightRight}L ${width} ${height}L 0 ${height}L 0 ${height / 2 + deltaHeight}Z`,{ fill: config.value.colors[i], stroke: '' });canvas.add(line);}requestAnimationFrame(() => {waterStartMove();});
}
最后
整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。
有需要的小伙伴,可以点击下方卡片领取,无偿分享