【Canvas】使用canvas实现多点连线效果

发布时间:2024年01月12日

前言

在浏览网站时,有的时候会看到网站背景有许多点,这些点在一定范围内会连线,感觉很好玩。
我实现了一个简单版的,记录下这次的实现过程。
在这里插入图片描述
动态效果
在这里插入图片描述

实现

实现思路

  1. 创建画布,设置一些全局属性
  2. 生成点,并画点
  3. 利用requestAnimationFrame更新点,并画点,同时判断各点间的距离。
    4.完成。

实现过程

创建画布,设置一些全局属性。

这一步主要考虑画布大小位置等,点的大小半径以及移动速度,生成点的个数。

  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++;
      }
    }
  };

使用 requestAnimationFrame

使用帧动画让圆点坐标实时改变,更新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数更新的函数(这样圆点数据变多浏览器不会报超时警告,根据实际数据处理速度动态变化画布更新时机)。

文章来源:https://blog.csdn.net/qq_43231248/article/details/135548218
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。