The background,

Before, I developed many projects of tool classes in the company, among which I developed a basic component of flow chart, which felt quite interesting. At that time, the solution was based on Canvas (why not use plug-in, because the function and style given by UI were quite special, so I did it myself when I had time).

Because the company’s Intranet is closed, the code can’t be taken out. It’s a good time to get used to react by doing some of the little functions I’ve been wanting to do before I started working at the new company.

PS: I used vue and React hook before, but I always had some problems when using it. Then I deleted it and changed it. Now I feel so confused when looking at my own code.

Two, function introduction

Overall interface:

The toolbar is at the top and the action bar is at the bottom. The left-to-right buttons on the toolbar are: Brush, Rectangle, Ellipse, Eraser, Move, line color, line width, Forward, Back, Zoom, and right-most file selection.

Below the buttons from left to right function: empty (restore) canvas, download images to the local.

Development environment

React 17.0.1 framework

UI Framework: Ant Design 4.16.9 (using ICONS and buttons)

Four, some function implementation scheme selection

1. Data storage

Forward and backward is the preservation and extraction of data, to save the canvas state, with the data to identify the current number of states, on this basis for data management. Storage with array storage, and then use subscript to represent the current state, using the subscript to move forward/back function.

The focus is on what data to save.

(1) solution

1. Save the result of the canvas and the pixels of the current canvas. This can use CanvasRenderingContext2D getImageData () method, and then use CanvasRenderingContext2D. PutImageData () to apply colours to a drawing;

2. Save the status and track of each operation. For example, each time draw the color of the line, line width, moving path and so on, each time take out the path and the corresponding state rendering;

(2) contrast

ImageData: Advantages: simple logic, convenient operation; Cons: It consumes too much memory each time you save pixel data, especially if the image is complex;

Path: Advantages: Only records the status of each operation, which is lighter. Disadvantages: The rendering process will be long if there are many operations (because each operation needs to be rendered separately); State maintenance is a little more tedious.

One of imageData’s fatal problems is that when the content is scaled out of the canvas, it can’t record the pixels that are out of the area, causing the content to be lost.

So, the second option is used here.

So, how do you keep track of the action path? Record every coordinate point? The answer is to use a Path2D object to record paths, not specific coordinates.

The path2D object records the motion path, but does not record the canvas state (line width, color, etc.). You can use recT, ARC, lineTo to add paths. For details, refer to MDN

2. Zoom/pan

Zooming/panning is the overall operation on the canvas and can be recorded. After zooming and panning, interactive data is often affected. Since mouse interactive data is immutable, all interactive data needs to be considered for panning/transformation.

(1) solution

1. Modify the corresponding coordinate data according to zooming and panning;

2. Use the canvas element corresponding to the transform;

3. Convert canvas coordinate system;

(2) contrast

1. Direct pass. The disadvantages are too obvious. If you want to use this scheme, the operation data will have to save the real coordinates of each time, and every time you change the scale or translation, all the data will have to be fully modified, which is too tedious, and the performance cost is also large;

2. Acceptable. The advantage is that the operation is simple, easy to achieve, through the algebraic transformation of data to achieve the equivalent interaction effect; The downside is that rendering performance is a problem after magnification (the bigger canvas is, the more overhead it will cost per render);

3. Acceptable. Advantages: Processing is not troublesome (more steps than the second solution), and does not affect performance. Disadvantages: Need to consider algebraic conversion.

Finally, the third scheme is adopted, which is to scale and transform the canvas coordinate system. Meanwhile, all interactive data are equivalent algebraic replacement according to the zoom and shift data, and there is no need to modify the path object previously recorded.

Five, function realization

Temporarily set the canvas size as 1000 x 750

1. Initialize data

  const [mode, setMode] = useState("line");// The current drawing type
  const [isStart, setIsStart] = useState(false);// Whether to start drawing
  const [initPosition, setInitPosition] = useState({ x: 0.y: 0 });// The starting point of the current drawing
  const [lineWidth, setLineWidth] = useState(1);/ / line width
  const [recordIndex, setRecordIndex] = useState(1);// Current status subscript
  const [lineColor, setLineColor] = useState("# 000000");/ / color
  const [canvasTranslate, setTranslate] = useState([500.375]);// Canvas coordinates default translation distance
  const [scale, setScale] = useState(100);// Scale
  const [canvasSize, setCanvasSize] = useState([defaultWidth, defaultHeight]);// Canvas size
  const [canvasState, setCanvasState] = useState([getInitState()]); // Canvas data
Copy the code

2. Brush

The paintbrush is the most basic function. Press the mouse over the canvas and follow the cursor to draw a series of lines. Associated with three events: Mousedown, Mousemove, and Mouseup

You can record the initial coordinates in a Mousedown event, then get the current coordinates in real time in a Mouse event and connect to the last coordinates, and then push out the edit status in a Mouseup event. (This coordinate must be based on the upper left corner of the canvas, so use offsetX and offsetY here, not pageX and pageY, because there will be a zoom function later, using pageX and pageY is very troublesome to calculate)

Bind events:

  useEffect(() = > {
    const canvas = canvasEle.current;
    canvas.addEventListener("mousedown", mousedownEvent);
    canvas.addEventListener("mousemove", mousemoveEvent);
    window.addEventListener("mouseup", mouseupEvent);
    return () = > {
      canvas.removeEventListener("mousedown", mousedownEvent);
      canvas.removeEventListener("mousemove", mousemoveEvent);
      window.removeEventListener("mouseup", mouseupEvent);
    };
  });
Copy the code

We need a state to record whether the mouse is down or not, and then three events:

/** * Mouse movement event *@param {*} e 
   * @returns * /
function mousemoveEvent(e) {
  if(! isStart)return;
  const [x, y] = [e.offsetX, e.offsetY];
  switch (mode) {
    case "line":
      lineMove(x, y);
      break;
    case "circle":
      circleMove(x, y);
      break;
    case "rect":
      rectMove(x, y);
      break;
    case "move":
      canvasMove(x, y);
      break;
    default: abraseMove(x, y); }}/** * Mouse press event *@param {*} E Event object *@returns* /
function mousedownEvent(e) {
  setIsStart(true);
  ctx.beginPath();
  ctx.moveTo(e.offsetX, e.offsetY);
  const [x, y] = [e.offsetX, e.offsetY];
  setInitLayout({ x, y });
}

/** * Mouse release event *@param {*} e
 * @returns* /
function mouseupEvent(e) {
  if(! isStart)return;
  setIsStart(false);
}
Copy the code

Among them, lineMove logic:

const lineMove = (newX, newY) = > {
  const [x, y] = initPosition;
  ctx.lineTo(newX, newY);
  ctx.stroke();
  setInitPosition({ x: newX, y: newY });
};
Copy the code

Effect:

3. Forward/back

The reason for saying forward/backward in advance is that it relates to the format of the data store, which is relevant to all operations.

The implementation scheme was described in detail earlier, so the format of the data corresponding to each operation is:

  const getCurState = (path, fill) = > {
    return {
      type: "path",
      scale,
      path,
      lineWidth: lineWidth,
      lineColor: lineColor,
      fill,
    };
  };
Copy the code

Path is the current path object, and the rest is the state data of the canvas itself.

Because each operation only records the state, and the specific rendering is left to the hook to do, so change the rendering process:

useEffect(() = > {
  if(! canvasState.length)return;
  const ctx = canvasEle.current.getContext("2d");
  ctx.clearRect(0.0, defaultWidth, defaultHeight);

  const contentState = canvasState.slice(0, recordIndex);
  for (const item of contentState) {
    ctx.beginPath();
    ctx.lineWidth = item.lineWidth;
    ctx.strokeStyle = item.lineColor;
    if (item.fill) {
      ctx.fillStyle = "#fff";
      ctx.fill(item.path);
    } else {
      ctx.stroke(item.path);
    }
  }
}, [canvasState, recordIndex]);

Copy the code

So, the logic that I drew before should change. Add a new record to the mousedown event to allow for moving back and forth, and then replace the last record continuously in the move event (so that a record is generated for each action, rather than each event triggered).

  const lineMove = (x, y) = > {
    const pre = canvasState[canvasState.length - 1].path;
    const path = new Path2D();

    path.addPath(pre);
    path.lineTo(x, y);

    replaceState(getCurState(path));
  };
  
  // Add status records.
  const replaceState = (newState) = > {
    const state = canvasState.slice(0, canvasState.length - 1);
    setCanvasState(state.concat(newState));
  };
  
function mousedownEvent(e) {
  setIsStart(true);

  const [x, y] = [e.offsetX, e.offsetY];

  const newState = canvasState.slice(0, recordIndex);
  const newPath = new Path2D();
  newPath.moveTo(x, y);
  setCanvasState(newState.concat(getCurState(newPath)));
  setRecordIndex(recordIndex + 1);
  setInitPosition({ x, y });
}


Copy the code

Finally, the logic of going forward and backward is simple, just pay attention to the boundary case.

  const frontOrBack = (v) = > {
    setRecordIndex(Math.max(1.Math.min(canvasState.length, recordIndex + v)));
  };
Copy the code

The minimum value is 1, because the initial state of the canvas (canvas size, line width, scale, etc.) must be recorded during initialization, otherwise the original state of the canvas will be lost.

Effect:

4. Rectangle/oval

They’re put together because they’re very similar.

The difference with a line segment is that the path of the line segment is continuous, and each time you mousemove, you add a new path to the previous one.

Rectangles and ellipses need only the mousedown coordinates, and then calculate the corresponding width/height/radius and upper left/center coordinates based on the current coordinates. The current path is not related to the last path.

Processing logic:

const circleMove = (x, y) = > {
  const path = new Path2D();
  const { x: initX, y: initY } = initPosition;
  path.ellipse(
    (initX + x) / 2,
    (initY + y) / 2.Math.abs((initX - x) / 2),
    Math.abs((initY - y) / 2),
    0.0.2 * Math.PI
  );
  replaceState(getCurState(path));
};
  const rectMove = (x, y) = > {
    const path = new Path2D();
    const { x: initX, y: initY } = initPosition;
    path.rect(
      Math.min(initX, x),
      Math.min(initY, y),
      Math.abs(x - initX),
      Math.abs(y - initY)
    );
    replaceState(getCurState(path));

  };
Copy the code

Effect:

5. The eraser

For eraser, the current idea is similar to drawing a line, except that each time a continuous rectangle is filled with white, covering the pattern on the path. We don’t use circles here because practice has found problems with circle fill, which is associated with the start of the path, but not with rect.

To prevent the rectangles from moving too fast and being discontinuous, you can construct a group of continuous rectangles named PATH according to the front and back coordinates

const getLinearRect = (x, y, x2, y2, step = 5) = > {
  const path = new Path2D();
  const disx = x2 - x;
  const disy = y2 - y;
  let c = Math.abs(disx / step);
  const ypercent = disy / c;
  let flag = x2 >= x ? 1 : -1;
  for (let i = 0; i <= c; i++) {
    path.rect(x2 - i * 5 * flag - 8, y2 - i * ypercent - 8.16.16);
  }
  return path;
};
const abraseMove = (x, y) = > {
  const pre = canvasState[canvasState.length - 1].path;

  const path = getLinearRect(initPosition.x, initPosition.y, x, y);
  path.addPath(pre);
  setInitPosition({ x, y });
  replaceState(getCurState(path, true));
};

Copy the code

Effect:

6. Line width/line color

These two functions are relatively simple because there is no impact on the original graph, and you just need to maintain the state. Bind the component directly.

  const [lineWidth, setLineWidth] = useState(1);
  const [lineColor, setLineColor] = useState("# 000000");
Copy the code

Effect:

7. Zoom/pan

The logic of panning is similar to that of drawing a rectangle/ellipse, recording the coordinates of a Mousedown and updating the current relative offset as you mousemove.

Scheme introduced in front, coordinate conversion of canvas, can use CanvasRenderingContext2D. SetTransform (), each update current transformation, can also be CanvasRenderingContext2D. The transform (), Transformation before superposition. Only apply a transformation, such as scaling, can use CanvasRenderingContext2D. Scale ().

After application, all interactive data need to undergo corresponding transformation processing. Take a simple example:

Perform CanvasRenderingContext2D. Scale (2, 2), the amplification of the axis of canvas twice, so all of the interactive data to divided by 2, namely narrow twice.

The translation is similar, the coordinate system shifts x and y, and you subtract x and y from the data.

In order to distinguish the operations of graph drawing and coordinate system transformation, a Type attribute is added to the data of operation records for distinction, and the current coordinate system state is recorded with the transform attribute.

Take the mousemove event processing logic as an example:

  // Mouse press event when moving mode
  const moveMouseDown = (x, y) = > {
    if(! canvasState.length)return;
    setInitPosition({ x, y });
    const preState = canvasState.slice(0, recordIndex);
    setCanvasState(
      preState.concat({
        type: "transform".transform: [
          [scale, scale],
          [canvasTranslate[0], canvasTranslate[1]]],})); setRecordIndex(recordIndex +1);
  };
  /** * Canvas move event *@param {*} x
   * @param {*} y* /
  const canvasMove = (x, y) = > {
    const [preX, preY] = canvasTranslate;
    const { x: initX, y: initY } = initPosition;
    setTranslate([preX + x - initX, preY + y - initY]);
    replaceState({
      type: "transform".transform: [
        [scale, scale],
        [preX + x - initX, preY + y - initY],
      ],
    });

  };
Copy the code

Data conversion process:

  /** * Mouse movement event *@param {*} e
   * @returns* /
  function mousemoveEvent(e) {
    if(! isStart)return;
    ctx.lineJoin = "round";
    const [translateX, translateY] = canvasTranslate;
    const [x, y] = [
      ((e.offsetX - translateX) * 100) / scale,
      ((e.offsetY - translateY) * 100) / scale, ]; . }Copy the code

Modify render processing logic, canvas applies scaling transform

useEffect(() = > {
  const ctx = canvasEle.current.getContext("2d");
  ctx.resetTransform();
  ctx.clearRect(0.0, defaultWidth, defaultHeight);

  const contentState = canvasState
    .slice(0, recordIndex)
    .filter((item) = >item.type ! = ="transform");
  const transformState = canvasState
    .slice(0, recordIndex)
    .filter((item) = > item.type === "transform") .pop()? .transform;if(! transformState) { }else {
    const [[x, y], [x2, y2]] = transformState;
    ctx.translate(x2, y2);
    ctx.scale(x / 100, y / 100);
    setScale(x);
    setTranslate([x2, y2]);
  }
  for (const item of contentState) {
    ctx.beginPath();
    ctx.lineWidth = item.lineWidth;
    ctx.strokeStyle = item.lineColor;
    if (item.fill) {
      ctx.fillStyle = "#fff";
      ctx.fill(item.path);
    } else {
      ctx.stroke(item.path);
    }
  }
}, [canvasState, recordIndex]);

Copy the code

Effect:

8. Load images

The idea is to select the file, generate a local link, and draw it on canvas.

There is a problem that the selected image proportion is not necessarily the default size proportion, so first get the original width and height of the selected image, use the way of contain to calculate the maximum scale, calculate the width and height, and then apply to the canvas.

Because of the size change, load the image to clear the canvas and restore the scaling transform, calculating the initial translate value based on the current image size.

To distinguish image rendering from other renderings, type=’img’ is used and the IMG attribute is used to record the IMG object and size is used to record size information.

  const selectFile = (e) = > {
    const url = window.URL.createObjectURL(e.file);

    const img = new Image();
    img.src = url;
    img.onload = () = > {
      const n = Math.min(defaultWidth / img.width, defaultHeight / img.height);
      constsize = [img.width * n, img.height * n]; initCanvas(... size, [ {type: "img",
          img,
          size,
        },
      ]);
    };
    return false;
  };
Copy the code

Modify render logic:

    for (const item of contentState) {
      const { img, size } = item;
      if (item.type === "img") {
        ctx.drawImage(
          img,
          0.0,
          img.width,
          img.height,
          (-1 * size[0) /2,
          (-1 * size[1) /2,
          size[0],
          size[1]);continue;
      }
      ctx.beginPath();
      ctx.lineWidth = item.lineWidth;
      ctx.strokeStyle = item.lineColor;
      if (item.fill) {
        ctx.fillStyle = "#fff";
        ctx.fill(item.path);
      } else{ ctx.stroke(item.path); }}Copy the code

Effect:

9. Clear your canvas

Add a method to initialize the state. The state parameter below is the data when the image is loaded, but not when the canvas is emptied. Be careful to clear the URL that generated the image before loading it

  const initCanvas = (w = defaultWidth, h = defaultHeight, state = []) = > {
    for (const item of canvasState) {
      if (item.type === "img") {
        window.URL.revokeObjectURL(item.img.src);
      }
    }
    setCanvasSize([w, h]);
    setScale(100);
    setTranslate([w / 2, h / 2]);
    setCanvasState([getInitState(w / 2, h / 2), ...state]);
    setRecordIndex(state.length + 1);
  };
Copy the code

10. Download pictures

Generate the URI with Canvas.todataURL () and download it with the A tag

  const downLoadImg = () = > {
    const aEle = document.createElement("a");
    document.body.appendChild(aEle);
    aEle.href = canvasEle.current.toDataURL();
    aEle.download = `The ${Date.now()}.jpg`;
    aEle.click();
    document.body.removeChild(aEle);
  };
Copy the code

Effect:

11. Complete code


function CanvasTool() {
  const defaultWidth = 1000;
  const defaultHeight = 750;
  const canvasEle = useRef();
  constctx = canvasEle.current? .getContext("2d");
  const list = [
    {
      value: "line".title: "Line"}, {value: "rect".title: "Rectangle"}, {value: "circle".title: "Circle",},/ / {
    // value: "abrase",
    // title: "Mosaic ",
    // },
  ];
  const lineWidthList = [1.2.4.6];
  const [mode, setMode] = useState("line");
  const [isStart, setIsStart] = useState(false);
  const [initPosition, setInitPosition] = useState({ x: 0.y: 0 });
  const [recordIndex, setRecordIndex] = useState(1);
  const [lineWidth, setLineWidth] = useState(1);
  const [lineColor, setLineColor] = useState("# 000000");
  const [canvasTranslate, setTranslate] = useState([500.375]);
  const [scale, setScale] = useState(100);
  const [canvasSize, setCanvasSize] = useState([defaultWidth, defaultHeight]);

  /** * get the default data record *@param {*} x
   * @param {*} y
   * @returns* /
  const getInitState = (x = 500, y = 375) = > {
    return {
      type: "transform".transform: [[100.100],
        [x, y],
      ],
    };
  };
  const [canvasState, setCanvasState] = useState([getInitState()]);

  /** * Change the scaling scale *@param {*} v* /
  const setScale2 = (v) = > {
    setScale(v);
    const preState = canvasState.slice(0, recordIndex);
    setCanvasState(
      preState.concat({
        type: "transform".transform: [
          [v, v],
          [canvasTranslate[0], canvasTranslate[1]]],})); setRecordIndex(recordIndex +1);
  };

  /** * Change the line color *@param {*} e* /
  const setLineColor2 = (e) = > {
    setLineColor(e.target.value);
  };

  /** * replaces the last operation record *@param {*} newState* /
  const replaceState = (newState) = > {
    const state = canvasState.slice(0, canvasState.length - 1);
    setCanvasState(state.concat(newState));
  };

  /** * Handle the brush movement *@param {*} x
   * @param {*} y* /
  const lineMove = (x, y) = > {
    const pre = canvasState[canvasState.length - 1].path;
    const path = new Path2D();

    path.addPath(pre);
    path.lineTo(x, y);

    replaceState(getCurState(path));
  };

  /** * Move the circle *@param {*} x
   * @param {*} y* /
  const circleMove = (x, y) = > {
    const path = new Path2D();
    const { x: initX, y: initY } = initPosition;
    path.ellipse(
      (initX + x) / 2,
      (initY + y) / 2.Math.abs((initX - x) / 2),
      Math.abs((initY - y) / 2),
      0.0.2 * Math.PI
    );
    replaceState(getCurState(path));
  };

  /** * Move the rectangle *@param {*} x
   * @param {*} y* /
  const rectMove = (x, y) = > {
    const path = new Path2D();
    const { x: initX, y: initY } = initPosition;
    path.rect(
      Math.min(initX, x),
      Math.min(initY, y),
      Math.abs(x - initX),
      Math.abs(y - initY)
    );
    replaceState(getCurState(path));
  };

  /** * Generates a continuous rectangular path * based on the starting point and emphasis@param {*} x
   * @param {*} y
   * @param {*} x2
   * @param {*} y2
   * @param {*} step
   * @returns* /
  const getLinearRect = (x, y, x2, y2, step = 5) = > {
    const path = new Path2D();
    const disx = x2 - x;
    const disy = y2 - y;
    let c = Math.abs((disx * 100) / (step * scale));
    const ypercent = disy / c;
    let flag = x2 >= x ? 1 : -1;
    for (let i = 0; i <= c; i++) {
      path.rect(
        x2 - i * 5 * flag - 8,
        y2 - i * ypercent - 8,
        (16 * 100) / scale,
        (16 * 100) / scale
      );
    }
    return path;
  };

  /** * Eraser movement handling *@param {*} x
   * @param {*} y* /
  const abraseMove = (x, y) = > {
    const pre = canvasState[canvasState.length - 1].path;

    const path = getLinearRect(initPosition.x, initPosition.y, x, y);
    path.addPath(pre);
    setInitPosition({ x, y });
    replaceState(getCurState(path, true));
  };

  /** * Mouse press event * when moving mode@param {*} x
   * @param {*} y
   * @returns* /
  const moveMouseDown = (x, y) = > {
    if(! canvasState.length)return;
    setInitPosition({ x, y });
    const preState = canvasState.slice(0, recordIndex);
    setCanvasState(
      preState.concat({
        type: "transform".transform: [
          [scale, scale],
          [canvasTranslate[0], canvasTranslate[1]]],})); setRecordIndex(recordIndex +1);
  };

  /** * Mouse movement event *@param {*} e
   * @returns* /
  function mousemoveEvent(e) {
    if(! isStart)return;
    ctx.lineJoin = "round";
    const [translateX, translateY] = canvasTranslate;
    const [x, y] = [
      ((e.offsetX - translateX) * 100) / scale,
      ((e.offsetY - translateY) * 100) / scale,
    ];
    switch (mode) {
      case "line":
        lineMove(x, y);
        break;
      case "circle":
        circleMove(x, y);
        break;
      case "rect":
        rectMove(x, y);
        break;
      case "move":
        canvasMove(x, y);
        break;
      default: abraseMove(x, y); }}/** * Mouse press event *@param {*} E Event object *@returns* /
  function mousedownEvent(e) {
    setIsStart(true);
    const [translateX, translateY] = canvasTranslate;
    const [x, y] = [
      ((e.offsetX - translateX) * 100) / scale,
      ((e.offsetY - translateY) * 100) / scale,
    ];
    if (mode === "move") {
      moveMouseDown(x, y);
      return;
    }
    const newState = canvasState.slice(0, recordIndex);
    const newPath = new Path2D();
    newPath.moveTo(x, y);
    setCanvasState(newState.concat(getCurState(newPath)));
    setRecordIndex(recordIndex + 1);
    setInitPosition({ x, y });
  }

  /** * Mouse release event *@param {*} e
   * @returns* /
  function mouseupEvent(e) {
    if(! isStart)return;

    // recordState();
    setIsStart(false);
    // setCurPath(null);
  }

  /** * Canvas move event *@param {*} x
   * @param {*} y* /
  const canvasMove = (x, y) = > {
    const [preX, preY] = canvasTranslate;
    const { x: initX, y: initY } = initPosition;
    setTranslate([preX + x - initX, preY + y - initY]);
    replaceState({
      type: "transform".transform: [
        [scale, scale],
        [preX + x - initX, preY + y - initY],
      ],
    });
  };

  // Event binding
  useEffect(() = > {
    const canvas = canvasEle.current;
    canvas.addEventListener("mousedown", mousedownEvent);
    canvas.addEventListener("mousemove", mousemoveEvent);
    window.addEventListener("mouseup", mouseupEvent);
    return () = > {
      canvas.removeEventListener("mousedown", mousedownEvent);
      canvas.removeEventListener("mousemove", mousemoveEvent);
      window.removeEventListener("mouseup", mouseupEvent);
    };
  });

  // Change canvas cursor in canvas move mode
  useEffect(() = > {
    if (mode === "move") { canvasEle.current? .classList.add("move-canvas");
    } else{ canvasEle.current? .classList.remove("move-canvas");
    }
  }, [mode]);

  /** * forward/back events *@param {*} v* /
  const frontOrBack = (v) = > {
    setRecordIndex(Math.max(1.Math.min(canvasState.length, recordIndex + v)));
  };

  /** * Generates the current operation record *@param {*} path
   * @param {*} fill
   * @returns* /
  const getCurState = (path, fill) = > {
    return {
      type: "path",
      scale,
      path,
      lineWidth: lineWidth,
      lineColor: lineColor,
      fill,
    };
  };

  // Canvas rendering
  useEffect(() = > {
    if(! canvasState.length)return;
    const ctx = canvasEle.current.getContext("2d");
    ctx.resetTransform();
    ctx.clearRect(0.0, defaultWidth, defaultHeight);

    const contentState = canvasState
      .slice(0, recordIndex)
      .filter((item) = >item.type ! = ="transform");
    const transformState = canvasState
      .slice(0, recordIndex)
      .filter((item) = > item.type === "transform") .pop()? .transform;const [[x, y], [x2, y2]] = transformState;
    ctx.translate(x2, y2);
    ctx.scale(x / 100, y / 100);
    setScale(x);
    setTranslate([x2, y2]);

    for (const item of contentState) {
      const { img, size } = item;
      if (item.type === "img") {
        ctx.drawImage(
          img,
          0.0,
          img.width,
          img.height,
          (-1 * size[0) /2,
          (-1 * size[1) /2,
          size[0],
          size[1]);continue;
      }
      ctx.beginPath();
      ctx.lineWidth = item.lineWidth;
      ctx.strokeStyle = item.lineColor;
      if (item.fill) {
        ctx.fillStyle = "#fff";
        ctx.fill(item.path);
      } else {
        ctx.stroke(item.path);
      }
    }
  }, [canvasState, recordIndex]);

  /** * Initializes the canvas state *@param {*} w
   * @param {*} h
   * @param {*} state* /
  const initCanvas = (w = defaultWidth, h = defaultHeight, state = []) = > {
    for (const item of canvasState) {
      if (item.type === "img") {
        window.URL.revokeObjectURL(item.img.src);
      }
    }
    setCanvasSize([w, h]);
    setScale(100);
    setTranslate([w / 2, h / 2]);
    setCanvasState([getInitState(w / 2, h / 2), ...state]);
    setRecordIndex(state.length + 1);
  };

  /** * select the image *@param {*} e
   * @returns* /
  const selectFile = (e) = > {
    const url = window.URL.createObjectURL(e.file);

    const img = new Image();
    img.src = url;
    img.onload = () = > {
      const n = Math.min(defaultWidth / img.width, defaultHeight / img.height);
      constsize = [img.width * n, img.height * n]; initCanvas(... size, [ {type: "img",
          img,
          size,
        },
      ]);
    };
    return false;
  };

  /** * download image */
  const downLoadImg = () = > {
    const aEle = document.createElement("a");
    document.body.appendChild(aEle);
    aEle.href = canvasEle.current.toDataURL();
    aEle.download = `The ${Date.now()}.jpg`;
    aEle.click();
    document.body.removeChild(aEle);
  };

export default CanvasTool;

Copy the code

PS: The page part is not put

Six, think

1. Functions to be optimized

(1) the eraser

The eraser is covered with a continuous white fill rectangle, but it will cover any pixel. What I want to achieve is to cover only the graphics added by this edit and not the loaded images. The general idea is to distinguish the image from the drawing. Then use CanvasRenderingContext2D. GlobalCompositeOperation this attribute.

And I feel that the coordinate calculation of the eraser is strange, and it is easy to have discontinuous points. We’ll have to work on this later.

Difficulty: being fostered fostered fostered fostered

(2) Rendering process

Because rendering is recorded on a per-operation basis, recording too many times (tens of thousands of times) can cause render lag.

Optimization idea, feel that when the number of operations is greater than a certain length, merge the previous several records and synthesize the picture as the initial record, while limiting the number of forward/backward.

Or you can convert to buffer and call WebGL to render, which will be much faster.

Difficulty: difficult to say, see the way to achieve. Feel ☆☆☆☆ above.

2. Functions to be added

(1) Expansion transformation

So far the transformation has only been scaled and shifted, so consider adding a rotation and deformation. To do this, the data processing becomes more complex, and you can consider splitting a module as an intermediate layer that handles the transformation of data between drawing and interaction.

Difficulty: being fostered fostered fostered fostered

(2) the text

Add text function, than graphics trouble point, the key is that I can not grasp the interaction way. As usual, we define a rectangle as the Input area, and render the text when out of focus (that is, add the text to the record). This transparent Input area always feels like a pit, or maybe I think too much, we will see later.

Difficulty: being is being fostered

(3) Straight line/arrow

This is also quite common, implementation is also ok, I will try later.

Difficulty: being fostered

(4) Select a path and move/highlight it

This simple selected, and a line still, CanvasRenderingContext2D. IsPointInStroke () can be achieved (PS: the interaction area is too small, if you want to achieve fuzzy select the best processing graphics.linestyle, add a gradient, before doing the company’s flow chart, Because of this fuzzy check, the API is not used, the coordinates are recorded and the math is used to determine whether the check is selected.

After selected translation or change the line width/color style, translation may want to be an additional scaling transformation process (find the path of scaling transformation properties, then the current stack operation caused by the transformation, and then add a new record), doing so may also generate more records, behind to weigh again.

Difficulty: being fostered fostered fostered