该文章也发布在我的博客上了哦:博客地址
效果预览
我们在很多网站上也许会见到如下背景:
这个背景其实很简单就能复刻,使用Canvas绘制,之后使用requestAnimationFrame
来做帧动画平滑过渡就可以了。
代码解析
画布相关
<canvas id="canvas"></canvas>
这一段就是布置一张画布,id为canvas
const dpr = window.devicePixelRatio;
这是获取当前设备的缩放,来保证图像清晰的
class Graphic {
...
}
这个就是我封装的一个类,包含了初始化,绘制,清空以及更新位置。
构造方法:
constructor() {
//点的数量
this.graphicNum = 40;
this._canvas = document.querySelector("#canvas");
this._ctx = this._canvas.getContext("2d");
this._canvas.width = window.innerWidth * dpr;
this._canvas.height = window.innerHeight * dpr;
this._ctx.scale(dpr, dpr);
this._points = new Array(this.graphicNum).fill(0).map(() => ({
...randomInnerPoint(),//起点坐标
target: randomInnerPoint(),//终点坐标
speed: 2.5, // 控制速度
}));
}
这里我默认端点数量为40,使用getContext
方法获取到上下文对象对画布进行绘制。points中存放了起点坐标、终点坐标以及动画的速度。
这里需要注意:宽高的设置不能使用
style.XXXX
,因为我们需要设置的是画布的宽高,而不是外围背景的宽高
绘制、清除以及更新
// 绘制,属性为相距最远的路线
draw(maxDis = 200) {
//绘制线条
this._points.forEach((point, index) => {
const point1 = point;
for (let i = index; i < this.graphicNum; i++) {
const point2 = this._points[i % this.graphicNum];
this._ctx.beginPath();
this._ctx.moveTo(point1.x, point1.y);
this._ctx.lineTo(point2.x, point2.y);
const dis = Math.sqrt(
Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2)
);
//控制透明度
this._ctx.strokeStyle = `rgba(200,200,200,${1 - dis / maxDis})`;
this._ctx.lineWidth = 1;
this._ctx.stroke();
}
});
// 绘制端点
this._points.forEach((point) => {
this._ctx.beginPath();
this._ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI);
this._ctx.fillStyle = `rgba(200, 200, 200)`;
this._ctx.fill();
});
}
clear() {
//清空画布
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
}
update() {
this._points.forEach(point => {
// 计算距离
const dx = point.target.x - point.x;
const dy = point.target.y - point.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 如果点到达目标位置,生成新的目标位置
if (distance < point.speed) {
point.target = randomInnerPoint();
} else {
// 按照速度移动点的位置
point.x += (dx / distance) * point.speed;
point.y += (dy / distance) * point.speed;
}
});
}
这里绘制的时候,我将所有的点都与其后方的点之间连接了一条线,这条线的透明度是由距离控制的,maxDis
就是最远距离
清空就是将整个画布全部清空
更新函数就是对每个点进行位置计算,然后移动位置,位置的动画交由requestAnimationFrame
控制。如果这个点到达了目标位置,那么重新给一个目标。这里计算是否到达终点是计算的当前距离对速度的大小,不然会出现一些抖动现象。
完整代码
<script setup>
import {onMounted} from "vue";
const dpr = window.devicePixelRatio;
function randomInnerPoint() {
return {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
};
}
class Graphic {
constructor() {
//点的数量
this.graphicNum = 40;
this._canvas = document.querySelector("#canvas");
this._ctx = this._canvas.getContext("2d");
this._canvas.width = window.innerWidth * dpr;
this._canvas.height = window.innerHeight * dpr;
this._ctx.scale(dpr, dpr);
this._points = new Array(this.graphicNum).fill(0).map(() => ({
...randomInnerPoint(),//起点坐标
target: randomInnerPoint(),//终点坐标
speed: 2.5, // 控制速度
}));
}
// 绘制,属性为相距最远的路线
draw(maxDis = 200) {
//绘制线条
this._points.forEach((point, index) => {
const point1 = point;
for (let i = index; i < this.graphicNum; i++) {
const point2 = this._points[i % this.graphicNum];
this._ctx.beginPath();
this._ctx.moveTo(point1.x, point1.y);
this._ctx.lineTo(point2.x, point2.y);
const dis = Math.sqrt(
Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2)
);
//控制透明度
this._ctx.strokeStyle = `rgba(200,200,200,${1 - dis / maxDis})`;
this._ctx.lineWidth = 1;
this._ctx.stroke();
}
});
// 绘制端点
this._points.forEach((point) => {
this._ctx.beginPath();
this._ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI);
this._ctx.fillStyle = `rgba(200, 200, 200)`;
this._ctx.fill();
});
}
clear() {
//清空画布
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
}
update() {
this._points.forEach(point => {
// 计算距离
const dx = point.target.x - point.x;
const dy = point.target.y - point.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 如果点到达目标位置,生成新的目标位置
if (distance < point.speed) {
point.target = randomInnerPoint();
} else {
// 按照速度移动点的位置
point.x += (dx / distance) * point.speed;
point.y += (dy / distance) * point.speed;
}
});
}
}
function initBackGraphic() {
const graphics = new Graphic();
let animationFrameId;
function animate() {
graphics.clear();
graphics.update();
graphics.draw();
// 递归调用下一帧
animationFrameId = requestAnimationFrame(animate);
}
animate(); // 开始动画
}
onMounted(() => {
initBackGraphic()
});
</script>
<template>
<canvas id="canvas"></canvas>
</template>
<style scoped>
#canvas {
position: fixed;
box-sizing: border-box;
background-color: #363636;
width: 100%;
height: 100%;
}
</style>