简体   繁体   中英

Need help converting code for fabric canvas from Vanilla JS to ReactJS

so I was playing around with the fabricjs canvas library and I found this fiddle written in vanillajs which lets you draw polygons on the canvas. I wanted to implement this exact thing in my react project so I tried to convert the entire code into react ( https://codesandbox.io/s/jolly-kowalevski-tjt58 ). The code works somewhat but there are some new bugs which are not in the original fiddle and I'm having trouble fixing them.

for eg: try to create a polygon by clicking the draw button, when you do this first time, the polygon is drawn without any bugs, but when you click the draw button again for the second time, the canvas starts acting weird and a weird polygon is created.

So basically I need help in converting the vanilla code to react with 0 bugs.

extra information:
fabric version used in the fiddle: 4.0.0
fabric version in sandbox: 4.0.0

Vanilla Js Code:

const getPathBtn = document.getElementById("get-path");
const drawPolygonBtn = document.getElementById("draw-polygon");
const showPolygonBtn = document.getElementById("show-polygon");
const editPolygonBtn = document.getElementById("edit-polygon");

const canvas = new fabric.Canvas("canvas", {
  selection: false
});

let line, isDown;
let prevCords;
let vertices = [];
let polygon;

const resetCanvas = () => {
  canvas.off();
  canvas.clear();
};

const resetVariables = () => {
  line = undefined;
  isDown = undefined;
  prevCords = undefined;
  polygon = undefined;
  vertices = [];
};

const addVertice = (newPoint) => {
  if (vertices.length > 0) {
    const lastPoint = vertices[vertices.length - 1];
    if (lastPoint.x !== newPoint.x && lastPoint.y !== newPoint.y) {
      vertices.push(newPoint);
    }
  } else {
    vertices.push(newPoint);
  }
};

const drawPolygon = () => {
  resetVariables();
  resetCanvas();

  canvas.on("mouse:down", function(o) {
    isDown = true;
    const pointer = canvas.getPointer(o.e);

    let points = [pointer.x, pointer.y, pointer.x, pointer.y];

    if (prevCords && prevCords.x2 && prevCords.y2) {
      const prevX = prevCords.x2;
      const prevY = prevCords.y2;
      points = [prevX, prevY, prevX, prevY];
    }

    const newPoint = {
      x: points[0],
      y: points[1]
    };
    addVertice(newPoint);

    line = new fabric.Line(points, {
      strokeWidth: 2,
      fill: "black",
      stroke: "black",
      originX: "center",
      originY: "center",
    });
    canvas.add(line);
  });

  canvas.on("mouse:move", function(o) {
    if (!isDown) return;
    const pointer = canvas.getPointer(o.e);
    const coords = {
      x2: pointer.x,
      y2: pointer.y
    };
    line.set(coords);
    prevCords = coords;
    canvas.renderAll();
  });

  canvas.on("mouse:up", function(o) {
    isDown = false;

    const pointer = canvas.getPointer(o.e);

    const newPoint = {
      x: pointer.x,
      y: pointer.y
    };
    addVertice(newPoint);
  });

  canvas.on("object:moving", function(option) {
    const object = option.target;
    canvas.forEachObject(function(obj) {
      if (obj.name == "Polygon") {
        if (obj.PolygonNumber == object.polygonNo) {
          const points = window["polygon" + object.polygonNo].get(
            "points"
          );
          points[object.circleNo - 1].x = object.left;
          points[object.circleNo - 1].y = object.top;
          window["polygon" + object.polygonNo].set({
            points: points,
          });
        }
      }
    });
    canvas.renderAll();
  });
};

const showPolygon = () => {
  resetCanvas();

  if (!polygon) {
    polygon = new fabric.Polygon(vertices, {
      fill: "transparent",
      strokeWidth: 2,
      stroke: "black",
      objectCaching: false,
      transparentCorners: false,
      cornerColor: "blue",
    });
  }

  polygon.edit = false;
  polygon.hasBorders = true;
  polygon.cornerColor = "blue";
  polygon.cornerStyle = "rect";
  polygon.controls = fabric.Object.prototype.controls;
  canvas.add(polygon);
};

// polygon stuff

// define a function that can locate the controls.
// this function will be used both for drawing and for interaction.
function polygonPositionHandler(dim, finalMatrix, fabricObject) {
  let x = fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x,
    y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y;
  return fabric.util.transformPoint({
      x: x,
      y: y
    },
    fabric.util.multiplyTransformMatrices(
      fabricObject.canvas.viewportTransform,
      fabricObject.calcTransformMatrix()
    )
  );
}

// define a function that will define what the control does
// this function will be called on every mouse move after a control has been
// clicked and is being dragged.
// The function receive as argument the mouse event, the current trasnform object
// and the current position in canvas coordinate
// transform.target is a reference to the current object being transformed,
function actionHandler(eventData, transform, x, y) {
  let polygon = transform.target,
    currentControl = polygon.controls[polygon.__corner],
    mouseLocalPosition = polygon.toLocalPoint(
      new fabric.Point(x, y),
      "center",
      "center"
    ),
    polygonBaseSize = polygon._getNonTransformedDimensions(),
    size = polygon._getTransformedDimensions(0, 0),
    finalPointPosition = {
      x: (mouseLocalPosition.x * polygonBaseSize.x) / size.x +
        polygon.pathOffset.x,
      y: (mouseLocalPosition.y * polygonBaseSize.y) / size.y +
        polygon.pathOffset.y,
    };
  polygon.points[currentControl.pointIndex] = finalPointPosition;
  return true;
}

// define a function that can keep the polygon in the same position when we change its
// width/height/top/left.
function anchorWrapper(anchorIndex, fn) {
  return function(eventData, transform, x, y) {
    let fabricObject = transform.target,
      absolutePoint = fabric.util.transformPoint({
          x: fabricObject.points[anchorIndex].x -
            fabricObject.pathOffset.x,
          y: fabricObject.points[anchorIndex].y -
            fabricObject.pathOffset.y,
        },
        fabricObject.calcTransformMatrix()
      ),
      actionPerformed = fn(eventData, transform, x, y),
      newDim = fabricObject._setPositionDimensions({}),
      polygonBaseSize = fabricObject._getNonTransformedDimensions(),
      newX =
      (fabricObject.points[anchorIndex].x -
        fabricObject.pathOffset.x) /
      polygonBaseSize.x,
      newY =
      (fabricObject.points[anchorIndex].y -
        fabricObject.pathOffset.y) /
      polygonBaseSize.y;
    fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
    return actionPerformed;
  };
}

function editPolygon() {
  canvas.setActiveObject(polygon);

  polygon.edit = true;
  polygon.hasBorders = false;

  let lastControl = polygon.points.length - 1;
  polygon.cornerStyle = "circle";
  polygon.cornerColor = "rgba(0,0,255,0.5)";
  polygon.controls = polygon.points.reduce(function(acc, point, index) {
    acc["p" + index] = new fabric.Control({
      positionHandler: polygonPositionHandler,
      actionHandler: anchorWrapper(
        index > 0 ? index - 1 : lastControl,
        actionHandler
      ),
      actionName: "modifyPolygon",
      pointIndex: index,
    });
    return acc;
  }, {});

  canvas.requestRenderAll();
}

// Button events

drawPolygonBtn.onclick = () => {
  drawPolygon();
};

showPolygonBtn.onclick = () => {
  showPolygon();
};

editPolygonBtn.onclick = () => {
  editPolygon();
};

getPathBtn.onclick = () => {
  console.log("vertices", polygon.points);
};

On 2nd draw (click the draw button again for the second time), the line is always connected to same point. So there is a problem with prevCords. By adding a console.log to handler function of "mouse:mouse" confirmed above statement:

fabricCanvas.on("mouse:move", function (o) {
  console.log("mousemove fired", prevCords); // always the same value
  if (isDown.current || !line.current) return;
  const pointer = fabricCanvas.getPointer(o.e);
  const coords = {
    x2: pointer.x,
    y2: pointer.y
  };
  line.current.set(coords);
  setPrevCords(coords); // the line should connect to this new point
  fabricCanvas.renderAll();
});

It's because of closure , the function handler of mouse:move will always remember the value of prevCords when it was created (ie when you click on Draw button) not the value that was updated by setPrevCords

To solve above problem, simply use useRef to store prevCords (or use reference) Line 6:

  const [fabricCanvas, setFabricCanvas] = useState();
  const prevCordsRef = useRef();
  const line = useRef();

Line 35:

  const resetVariables = () => {
    line.current = undefined;
    isDown.current = undefined;
    prevCordsRef.current = undefined;
    polygon.current = undefined;
    vertices.current = [];
  };

Line 65:

  if (prevCordsRef.current && prevCordsRef.current.x2 && prevCordsRef.current.y2) {
    const prevX = prevCordsRef.current.x2;
    const prevY = prevCordsRef.current.y2;
    points = [prevX, prevY, prevX, prevY];
  }

Line 96:

  prevCordsRef.current = coords;

One last suggestion is to change Line 89 (so the feature match the demo):

  if (!isDown.current) return;

On summary:

  1. Don't use useState for variable that must have latest value in another function handler. Use useRef instead
  2. Use useState for prevCords is a wasted since React will re-render on every setState

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM