线条流动动画

发布时间 2023-06-17 10:08:56作者: ScarlettK

简介

流线动画效果,适合做网页背景

效果展示

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>