在浏览网站时,有的时候会看到网站背景有许多点,这些点在一定范围内会连线,感觉很好玩。
我实现了一个简单版的,记录下这次的实现过程。
动态效果
这一步主要考虑画布大小位置等,点的大小半径以及移动速度,生成点的个数。
const canvas: Ref<HTMLCanvasElement | null> = ref(null);
const dotCoord: Coord[] = [];
let dotCoordFrame: CoordFrame[] = [];
const cW = 800, // canvas宽
cH = 400, // canvas高
oX = cW / 2, // 中心点x
oY = cH / 2, // 中心点y
r = 5, // 点半径
dotNumber = 50, // 点数
rangeLineV = 100, // 连线范围
moveV = 0.5; // 初始移动速度
let frameA: number | null = null;
onMounted(() => {
initCanvas();
canvas.value?.addEventListener('mouseenter', cancelFrame);
canvas.value?.addEventListener('mouseleave', startFrame);
});
onUnmounted(() => {
canvas.value?.removeEventListener('mouseenter', cancelFrame);
canvas.value?.removeEventListener('mouseleave', startFrame);
});
const initCanvas = () => {
if (canvas.value) {
canvas.value.width = cW;
canvas.value.height = cH;
console.log(canvas.value);
randomCreateDotCoord(dotNumber);
createStart();
}
};
初始化随机点时要保证点坐标不重复。
// 随机生成指定点坐标(坐标不重复)
const randomCreateDotCoord = (num = 1) => {
function randomDot(): Coord {
let aX = Math.floor(Math.random() * (cW - 2 * r)) + r;
let aY = Math.floor(Math.random() * (cH - 2 * r)) + r;
return {
x: aX,
y: aY,
r,
};
}
let i = 0;
dotCoord.length = 0;
while (i < num) {
let dot = randomDot();
if (!jumpDotIsCollision(dot, dotCoord)) {
dotCoord.push(dot);
i++;
}
}
};
使用帧动画让圆点坐标实时改变,更新canvas,更新时记得清空上一帧。
//帧动画函数
function animationFrame() {
let ctx = canvas.value?.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, cW, cH);
// 更新点
dotCoordFrame = dotCoordFrame.map((dot) => {
return getFrameV(dot);
});
dotCoordFrame.forEach(({ fx, fy, r }) => {
drawArc(ctx, {
x: fx,
y: fy,
r,
});
});
// 点如在一定范围内则连线
dotCoordFrame.forEach((dot) => {
dotCoordFrame.forEach((dot2) => {
if (calcHypotenuse(dot.fx - dot2.fx, dot.fy - dot2.fy) < rangeLineV) {
drawRangleLine(ctx, { x: dot.fx, y: dot.fy }, { x: dot2.fx, y: dot2.fy });
}
});
});
frameA = window.requestAnimationFrame(animationFrame);
}
const startFrame = () => {
frameA = window.requestAnimationFrame(animationFrame);
};
const cancelFrame = () => {
window.cancelAnimationFrame(frameA as number);
};
使用vue3写的,逻辑都在ts里。
<template>
<div class="start-alignment">
<h3>星星连线</h3>
<div>
<canvas class="canvas" ref="canvas"></canvas>
</div>
</div>
</template>
<script lang="ts" setup>
interface CoordBase {
x: number;
y: number;
}
interface Coord extends CoordBase {
r: number;
}
interface CoordFrame extends Coord {
fx: number;
fy: number;
xAdd: boolean; // 1
yAdd: boolean;
v: number; // 移动速度
}
type Ctx = CanvasRenderingContext2D | null | undefined;
const canvas: Ref<HTMLCanvasElement | null> = ref(null);
const dotCoord: Coord[] = [];
let dotCoordFrame: CoordFrame[] = [];
const cW = 800, // canvas宽
cH = 400, // canvas高
oX = cW / 2, // 中心点x
oY = cH / 2, // 中心点y
r = 5, // 点半径
dotNumber = 50, // 点数
rangeLineV = 100, // 连线范围
moveV = 0.5; // 初始移动速度
let frameA: number | null = null;
onMounted(() => {
initCanvas();
canvas.value?.addEventListener('mouseenter', cancelFrame);
canvas.value?.addEventListener('mouseleave', startFrame);
});
onUnmounted(() => {
canvas.value?.removeEventListener('mouseenter', cancelFrame);
canvas.value?.removeEventListener('mouseleave', startFrame);
});
const initCanvas = () => {
if (canvas.value) {
canvas.value.width = cW;
canvas.value.height = cH;
console.log(canvas.value);
randomCreateDotCoord(dotNumber);
createStart();
}
};
// 随机生成指定点坐标(坐标不重复)
const randomCreateDotCoord = (num = 1) => {
function randomDot(): Coord {
let aX = Math.floor(Math.random() * (cW - 2 * r)) + r;
let aY = Math.floor(Math.random() * (cH - 2 * r)) + r;
return {
x: aX,
y: aY,
r,
};
}
let i = 0;
dotCoord.length = 0;
while (i < num) {
let dot = randomDot();
if (!jumpDotIsCollision(dot, dotCoord)) {
dotCoord.push(dot);
i++;
}
}
};
// 两点是否碰撞
function jumpDotIsCollision(dot: Coord, arr: any[]) {
for (let i = 0; i < arr.length; i++) {
let cDot = arr[i];
let c = calcHypotenuse(cDot.x - dot.x, cDot.y - dot.y);
if (c <= cDot.r + dot.r) {
return true;
}
}
return false;
}
// 生成星星
const createStart = () => {
let ctx = canvas.value?.getContext('2d');
dotCoord.forEach((dot) => {
drawArc(ctx, dot);
});
// 初始化动态点数组
dotCoordFrame.length = 0;
dotCoordFrame.push(
...dotCoord.map((dot) => {
return {
...dot,
fx: dot.x,
fy: dot.y,
xAdd: dot.x > oX ? true : false,
yAdd: dot.y > oY ? true : false,
v: moveV,
};
}),
);
startFrame();
};
//帧动画函数
function animationFrame() {
let ctx = canvas.value?.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, cW, cH);
// 更新点
dotCoordFrame = dotCoordFrame.map((dot) => {
return getFrameV(dot);
});
dotCoordFrame.forEach(({ fx, fy, r }) => {
drawArc(ctx, {
x: fx,
y: fy,
r,
});
});
// 点如在一定范围内则连线
dotCoordFrame.forEach((dot) => {
dotCoordFrame.forEach((dot2) => {
if (calcHypotenuse(dot.fx - dot2.fx, dot.fy - dot2.fy) < rangeLineV) {
drawRangleLine(ctx, { x: dot.fx, y: dot.fy }, { x: dot2.fx, y: dot2.fy });
}
});
});
frameA = window.requestAnimationFrame(animationFrame);
}
const startFrame = () => {
frameA = window.requestAnimationFrame(animationFrame);
};
const cancelFrame = () => {
window.cancelAnimationFrame(frameA as number);
};
// 根据坐标画dot
const drawArc = (ctx: Ctx, dot: Coord) => {
if (!ctx) return;
const { x, y, r } = dot;
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fill();
};
// 两点画线
const drawRangleLine = (ctx: Ctx, x1: CoordBase, x2: CoordBase) => {
if (!ctx) return;
ctx.beginPath();
ctx.moveTo(x1.x, x1.y);
ctx.lineTo(x2.x, x2.y);
ctx.stroke();
};
//处理坐标下一帧移动值
const getFrameV = (dot: CoordFrame): CoordFrame => {
let { x, xAdd, yAdd, fx, fy, y, v } = dot;
let nx = fx,
ny = fy;
// 边缘机制-start
{
if (fx > cW || fx < 0) {
xAdd = !xAdd;
v = moveV; // 重新赋初始初始速度
}
if (fy > cH || fy < 0) {
yAdd = !yAdd;
v = moveV;
}
}
// end
// 碰撞机制
{
let arr = dotCoordFrame.map((v) => ({ x: v.fx, y: v.fy, r })).filter((v) => v.x !== fx);
let isUse = jumpDotIsCollision(
{
x: nx,
y: ny,
r,
},
arr,
);
if (isUse) {
xAdd = !xAdd;
yAdd = !yAdd;
v = moveV;
}
}
if (xAdd) {
nx += v;
} else {
nx -= v;
}
if (yAdd) {
ny += v;
} else {
ny -= v;
}
// 改变下次的速度
const fn = () => {
if (v >= 0.1) v -= 0.1;
setTimeout(fn, 200);
};
setTimeout(fn, 200);
return {
x: fx,
y: fy,
xAdd,
yAdd,
fx: nx,
fy: ny,
r,
v,
};
};
/**
* 勾股定理
* @param a number
* @param b
* @returns
*/
function calcHypotenuse(a: number, b: number) {
return Math.sqrt(a * a + b * b);
}
</script>
<style lang="scss" scoped>
.canvas {
border: 1px solid #000;
}
</style>
这是一个特别简单的圆点连线canvas动画。如果你有兴趣,可以根据此基础上扩展出更好玩的互动效果,点击改变中心点,然后圆点朝点击坐标缓动等等。
通过这次实现,较为重要的是:
可优化点:是碰撞判断逻辑、圆点数据处理、requestAnimationFrameAPI封装一个可以指定ms数更新的函数(这样圆点数据变多浏览器不会报超时警告,根据实际数据处理速度动态变化画布更新时机)。