线条流动动画
简介
流线动画效果,适合做网页背景
效果展示
ts代码
注意:动画定时刷新的机制使用到了之前写的一篇文章《Vue3中循环任务优化方案》
import { useSchedule } from "@/use/sys/useSchedule";
import { Ref } from "vue";
class segm {
b: number;
x0: number;
y0: number;
a: number;
x1: number;
y1: number;
l: number;
constructor(x: number, y: number, l: number) {
this.b = Math.random() * 1.9 + 0.1
this.x0 = x
this.y0 = y
this.a = Math.random() * 2 * Math.PI
this.x1 = this.x0 + l * Math.cos(this.a)
this.y1 = this.y0 + l * Math.sin(this.a)
this.l = l
}
update(x: number, y: number) {
this.x0 = x
this.y0 = y
this.a = Math.atan2(this.y1 - this.y0, this.x1 - this.x0)
this.x1 = this.x0 + this.l * Math.cos(this.a)
this.y1 = this.y0 + this.l * Math.sin(this.a)
}
}
class rope {
c: CanvasRenderingContext2D;
color: Ref<string>;
res: number;
type: string;
l: number;
segm: segm[];
b: number;
constructor(tx: number, ty: number, l: number, b: number, slq: number, typ: string, c: CanvasRenderingContext2D, color: Ref<string>) {
this.color = color;
this.c = c;
if (typ == "l") {
this.res = l / 2
}
else {
this.res = l / slq
}
this.type = typ
this.l = l
this.segm = []
this.segm.push(new segm(tx, ty, this.l / this.res))
for (let i = 1; i < this.res; i++) {
this.segm.push(
new segm(this.segm[i - 1].x1, this.segm[i - 1].y1, this.l / this.res)
)
}
this.b = b
}
update(t: { x: number, y: number }) {
this.segm[0].update(t.x, t.y)
for (let i = 1; i < this.res; i++) {
this.segm[i].update(this.segm[i - 1].x1, this.segm[i - 1].y1)
}
}
show() {
if (this.type == "l") {
this.c.beginPath()
for (let i = 0; i < this.segm.length; i++) {
this.c.lineTo(this.segm[i].x0, this.segm[i].y0)
}
this.c.lineTo(
this.segm[this.segm.length - 1].x1,
this.segm[this.segm.length - 1].y1
)
this.c.strokeStyle = this.color.value;
this.c.lineWidth = this.b
this.c.stroke()
this.c.beginPath()
this.c.arc(this.segm[0].x0, this.segm[0].y0, 1, 0, 2 * Math.PI)
this.c.fillStyle = this.color.value;
this.c.fill()
this.c.beginPath()
this.c.arc(
this.segm[this.segm.length - 1].x1,
this.segm[this.segm.length - 1].y1,
2,
0,
2 * Math.PI
)
this.c.fillStyle = this.color.value;
this.c.fill()
}
else {
for (let i = 0; i < this.segm.length; i++) {
this.c.beginPath()
this.c.arc(this.segm[i].x0, this.segm[i].y0, this.segm[i].b, 0, 2 * Math.PI)
this.c.fillStyle = this.color.value;
this.c.fill()
}
this.c.beginPath()
this.c.arc(
this.segm[this.segm.length - 1].x1,
this.segm[this.segm.length - 1].y1,
2, 0, 2 * Math.PI
)
this.c.fillStyle = this.color.value;
this.c.fill()
}
}
}
const schedule = useSchedule().schedule;
/**
* 线段流动动画
*/
export class RopeFlow {
canvas: HTMLCanvasElement;
c: CanvasRenderingContext2D;
id: string;
h: number;
w: number;
ropes: rope[] = [];
randl: number[] = [];
da: number[] = [];
target: { x: number, y: number, errx: number, erry: number } = {x: 0, y: 0, errx: 0, erry: 0};
rl: number = 50;
t: number = 0;
q: number = 10;
constructor(canvas: HTMLCanvasElement, id: string, color: Ref<string>) {
this.canvas = canvas;
this.id = id;
const c = canvas.getContext("2d") as CanvasRenderingContext2D,
w = (canvas.width = window.innerWidth),
h = (canvas.height = window.innerHeight);
c.fillStyle = "rgba(30,30,30,1)";
c.fillRect(0, 0, w, h);
this.c = c;
this.w = (canvas.width = window.innerWidth - 10)
this.h = (canvas.height = window.innerHeight - 10)
let type = "l";
for (let i = 0; i < 100; i++) {
type = Math.random() > 0.25 ? 'l' : 'o';
this.ropes.push(
new rope(
w / 2,
h / 2,
(Math.random() + 0.5) * 500,
Math.random() * 0.4 + 0.1,
Math.random() * 15 + 5,
type,
c,
color
)
)
this.randl.push(Math.random() * 2 - 1)
this.da.push(0)
}
this.target.x = this.w / 2;
this.target.y = this.h / 2;
}
run() {
schedule.setLoopTask(
this.id,
() => {
window.requestAnimationFrame(() => {
this.loop();
})
},
1000 / 40
)
window.addEventListener("resize", () => {
this.w = this.canvas.width = window.innerWidth;
this.h = this.canvas.height = window.innerHeight;
this.loop();
})
}
stop() {
schedule.removeTask(this.id)
}
private loop() {
this.c.clearRect(0, 0, this.w, this.h)
this.draw()
}
private draw() {
this.target.errx =
this.w / 2 +
(this.h / 2 - this.q) *
Math.sqrt(2) *
Math.cos(this.t) /
(Math.pow(Math.sin(this.t), 2) + 1) -
this.target.x
this.target.erry =
this.h / 2 +
(this.h / 2 - this.q) *
Math.sqrt(2) *
Math.cos(this.t) *
Math.sin(this.t) /
(Math.pow(Math.sin(this.t), 2) + 1) -
this.target.y
this.target.x += this.target.errx / 10
this.target.y += this.target.erry / 10
this.t += 0.01
for (let i = 0; i < this.ropes.length; i++) {
if (this.randl[i] > 0) {
this.da[i] += (1 - this.randl[i]) / 10
}
else {
this.da[i] += (-1 - this.randl[i]) / 10
}
this.ropes[i].update({
x:
this.target.x +
this.randl[i] * this.rl * Math.cos((i * 2 * Math.PI) / this.ropes.length + this.da[i]),
y:
this.target.y +
this.randl[i] * this.rl * Math.sin((i * 2 * Math.PI) / this.ropes.length + this.da[i])
})
this.ropes[i].show()
}
}
}
使用示例
<!-- 定义一个canvas用来绘画 -->
<canvas id="login-background-animation" class="absolute w-full h-full"/>
....somecode
<script lang="ts" setup>
import { useThemeStore } from "@/stores/theme";
import { computed, onMounted, onUnmounted, ref } from "vue";
import { RopeFlow } from "@/util/effects/animationUtil";
let animationInstance: RopeFlow;
onMounted(() => {
const canvas = document.getElementById("login-background-animation") as HTMLCanvasElement;
if (canvas) {
const ropeColor = computed(() => {
return useThemeStore().colorScheme === 'light' ? "#bbddff" : '#fff7ed';
})
// 设置动画
animationInstance = new RopeFlow(
canvas,
"Login-Background-Rope-Flow-Animation",
ropeColor
)
// 运行动画
animationInstance.run();
}
})
onUnmounted(() => {
if (animationInstance) {
animationInstance.stop();
}
})
</script>