react使用canvas再已有图片上进行标注

1,158 阅读2分钟

需求: 对远程已有的图片进行标注功能(在图片上画方框,圆形和椭圆形)

原理: 使用canvas画布进行绘制

遇到的问题

  1. canvas使用background-image将图片渲染上,但是保存的时候使用toDataUrl方法转换的base64并没有渲染上background-image

  2. 远程图片跨域

  3. base64转换为二进制文件

解决办法

  1. 在当前的canvas下写一个隐藏的canvas,将图片使用drawImage的方式渲染在隐藏画布上,保存的时候将第一张画布所画的图形渲染到隐藏画布上,最后得到的是隐藏画布的DataUrl

  2. 在远程设置Access-Control-Allow-Origin: *

代码

/**
 * @function Draw 绘制组件
 * @param {Number} width 画布的宽
 * @param {Number} height 画布的高
 * @param {Number} brushRadius 笔刷的大小
 * @param {String} color 笔刷的颜色  
 * @param {String} imgSrc 背景图的地址
 * @param {String} type 绘制的类型 
 * @param {String} fileName 二进制图片的文件名 
 */
const Draw = (
  { width, height, brushRadius, color, imgSrc, type, fileName },
  ref
) => {
  const canvasElem = useRef();
  const hiddenElem = useRef();
  let writingCtx;
  let hiddenCtx;
  let isDrawingShape = false;
  let coordinateScaleX;
  let coordinateScaleY;
  let mouseDownX;
  let mouseDownY;

  const drawRect = (e) => {
    const { offsetX, offsetY } = e;
    const positionX = mouseDownX / coordinateScaleX;
    const positionY = mouseDownY / coordinateScaleY;
    const dataX = (offsetX - mouseDownX) / coordinateScaleX;
    const dataY = (offsetY - mouseDownY) / coordinateScaleY;
    writingCtx.clearRect(0, 0, width, height);
    writingCtx.beginPath();
    writingCtx.strokeRect(positionX, positionY, dataX, dataY);
  };

  const drawCircle = (e) => {
    const { offsetX, offsetY } = e;
    const rx = (offsetX - mouseDownX) / 2;
    const ry = (offsetY - mouseDownY) / 2;
    const radius = Math.sqrt(rx * rx + ry * ry);
    const centreX = rx + mouseDownX;
    const centreY = ry + mouseDownY;
    writingCtx.clearRect(0, 0, width, height);
    writingCtx.beginPath();
    writingCtx.arc(
      centreX / coordinateScaleX,
      centreY / coordinateScaleY,
      radius,
      0,
      Math.PI * 2
    );
    writingCtx.stroke();
  };

  const drawEllipse = (e) => {
    const { offsetX, offsetY } = e;
    const radiusX = Math.abs(offsetX - mouseDownX) / 2;
    const radiusY = Math.abs(offsetY - mouseDownY) / 2;
    const centreX =
      offsetX >= mouseDownX ? radiusX + mouseDownX : radiusX + offsetX;
    const centreY =
      offsetY >= mouseDownY ? radiusY + mouseDownY : radiusY + offsetY;
    const positionX = centreX / coordinateScaleX;
    const positionY = centreY / coordinateScaleY;
    const dataX = radiusX / coordinateScaleX;
    const dataY = radiusY / coordinateScaleY;
    writingCtx.clearRect(0, 0, width, height);
    writingCtx.beginPath();
    writingCtx.ellipse(positionX, positionY, dataX, dataY, 0, 0, Math.PI * 2);
    writingCtx.stroke();
  };

  const handleMouseDown = (e) => {
    isDrawingShape = true;
    if (canvasElem.current !== undefined) {
      coordinateScaleX = canvasElem.current.clientWidth / width;
      coordinateScaleY = canvasElem.current.clientHeight / height;
    }
    writingCtx.lineWidth = brushRadius / coordinateScaleX;
    writingCtx.strokeStyle = color;
    const { offsetX, offsetY } = e;
    mouseDownX = offsetX;
    mouseDownY = offsetY;
  };

  const handleMouseMove = (e) => {
    if (isDrawingShape) {
      switch (type) {
        case "square":
          drawRect(e);
          break;
        case "circle":
          drawCircle(e);
          break;
        case "ellipse":
          drawEllipse(e);
          break;
        default:
          console.log("no type");
      }
    }
  };

  const handleMouseUp = () => {
    isDrawingShape = false;
    writingCtx.save();
  };

  const base64toFile = (dataurl) => {
    const arr = dataurl.split(",");
    const mime = arr[0].match(/:(.*?);/)[1];
    const suffix = mime.split("/")[1];
    const bstr = atob(arr[1]);
    let n = bstr.length;
    const u8arr = new Uint8Array(n);
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }
    return new File([u8arr], `${fileName}.${suffix}`, {
      type: mime,
    });
  };

  useImperativeHandle(ref, () => ({
    getFile() {
      hiddenCtx.drawImage(canvasElem.current, 0, 0);
      const result = base64toFile(hiddenElem.current.toDataURL());
      return result;
    },
    getDataURL() {
      hiddenCtx.drawImage(canvasElem.current, 0, 0);
      const result = hiddenElem.current.toDataURL();
      return result;
    },
  }));

  useEffect(() => {
    writingCtx = canvasElem.current.getContext("2d");
    hiddenCtx = hiddenElem.current.getContext("2d");
    const img = new Image();
    img.src = imgSrc;
    img.crossOrigin = "anonymous";
    img.onload = function () {
      img.width = width;
      img.height = height;
      hiddenCtx.drawImage(img, 0, 0, width, height);
    };

    if (canvasElem.current) {
      canvasElem.current.addEventListener("mousedown", handleMouseDown);
      canvasElem.current.addEventListener("mousemove", handleMouseMove);
      canvasElem.current.addEventListener("mouseup", handleMouseUp);
    }
  }, []);

  return (
    <>
      <canvas width={width} height={height} className="draw" ref={canvasElem} />
      <canvas
        width={width}
        height={height}
        style={{ display: "none" }}
        ref={hiddenElem}
      />
    </>
  );
};