用canvas给大家画几个小风车祝祝兴

102 阅读2分钟

一、效果

二、内容

  • canvas绘制基本流程
  • 生成指定范围随机数
  • 在浏览器每一帧绘制的时机做一些自己的逻辑
  • 处理canvas元素的拖拽
  • 画布元素的简单碰撞检测
  • 编写线性逻辑的代码

三、在线

codesandbox.io/p/sandbox/c…

四、代码

import { useEffect, useRef } from "react";

// 圆
class Circle {
  constructor(x, y, r, randomColor) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.color = randomColor;
    this.rotate = 0;
  }
  draw(ctx) {
    if (!ctx) {
      return;
    }
    ctx.save();
    ctx.beginPath();

    // 动态颜色
    ctx.fillStyle = this.color;

    // 动态角度
    ctx.arc(this.x, this.y, this.r, 0 + this.rotate, 1 * Math.PI + this.rotate);
    ctx.fill();
    ctx.restore();
  }
}

const Panel = () => {
  const refCanvas = useRef();
  const store = useRef({
    nodeList: [], // 存放画布添加的圆
    preX: 0, // 拖拽前的圆X偏移
    preY: 0, // 拖拽前的圆Y偏移
    target: null, // 当前正在拖拽中的圆
  });

  const draw = () => {
    const ctx = refCanvas.current.getContext("2d");
    ctx.clearRect(0, 0, 500, 500); // 绘制前先清空

    store.current?.nodeList?.forEach?.((item: any) => {
      item.rotate += 0.02;
      item.draw(ctx); // 调用每一个圆的绘制方法
    });

    // 启动绘制监测
    requestAnimationFrame(draw);
  };

  useEffect(() => {
    draw();
  }, []);

  // 获取鼠标偏移坐标
  const getPos = (e: any) => {
    const { offsetX, offsetY } = e.nativeEvent;
    return {
      x: offsetX,
      y: offsetY,
    };
  };

  // 生成随机坐标
  const genPos = () => {
  
    // [r, 500 - r]
    const start = 50;
    const end = 450;
    return {
      x: Math.round(Math.random() * (end - start) + start),
      y: Math.round(Math.random() * (end - start) + start),
    };
  };

  // 随机生成圆到花布
  const genCircle = () => {
    const randomColor = "#" + Math.floor(Math.random() * 16777215).toString(16); // 随机颜色
    const { x, y } = genPos(); // 随机坐标
    const arc = new Circle(x, y, 50, randomColor);
    store.current.nodeList.push(arc); // 放入追踪队列
  };

  // 清空圆
  const clear = () => {
    store.current.nodeList = [];
  };

  // 判断是否点击位于圆内
  const isHitNode = (node: any, pos: { x: number; y: number }) => {
    const distance = Math.hypot(node.x - pos.x, node.y - pos.y);
    return distance < node.r;
  };

  // 处理拖拽中的碰撞检测
  const compact = (x, y) => {
    const curNode = store.current.target;
    const hitNode = store.current.nodeList
      .filter((i) => i !== curNode) // 排除自己和自己对比
      .find((item) => {
        const distance = Math.hypot(item.x - x, item.y - y); // 距离
        return distance < curNode.r + item.r; // 距离之和小于半径之和
      });

    return hitNode;
  };

  const onMouseMove = (e) => {
    // 取得鼠标坐标
    const { offsetX, offsetY } = e.nativeEvent;
    const newX = offsetX - store.current.preX;
    const newY = offsetY - store.current.preY;

    // 更新坐标前先检测这个坐标是否和别的圆有重叠
    if (store.current.target && !compact(newX, newY)) {
      store.current.target.x = newX; // 修正偏移并更新坐标
      store.current.target.y = newY;
    }
  };

  const onMouseDown = (e) => {
    const { offsetX, offsetY } = e.nativeEvent;

    // 从圆列表中找到当前点击位置命中的圆
    const hitNode = store.current.nodeList.find((item) =>
      isHitNode(item, getPos(e))
    );

    if (hitNode) {
      store.current.target = hitNode; // 记住当前命中的圆

      // 记住初始和目标点的中心偏移
      store.current.preX = offsetX - hitNode.x;
      store.current.preY = offsetY - hitNode.y;
    }
  };

  const onMouseUp = (e) => {
    store.current.target = null; // 鼠标抬起,忘掉这个圆
  };

  return (
    <div>
      <button onClick={genCircle}> add </button>
      <button onClick={clear}> clear </button>

      <div
        onMouseMove={onMouseMove}
        onMouseDown={onMouseDown}
        onMouseUp={onMouseUp}
      >
        <canvas
          width={500}
          height={500}
          style={{ width: "500px", height: "500px", border: "1px solid red" }}
          ref={refCanvas}
        />
      </div>
    </div>
  );
};

export default Panel; // (ts类型暂时忽略)


@bysking