FabricJS 如何在缩放或平移 canvas 后更新多边形的自定义控制点?

FabricJS How can I update the custom control point of a polygon after Zoom or Pan the canvas?

有人可以建议我在缩放或平移 canvas 后更新自定义多边形(通过鼠标移动 + 单击绘制)的变换点和自定义控制点的正确方法吗?

重现步骤:

  1. 单击绘制多边形

  2. 在canvas

  3. 中画一个多边形
  4. 缩放(向上或向下滚动)或平移(alt + 鼠标拖动)

这是我的代码:https://jsfiddle.net/ckitisak/qv6y283p/

    /**
     * based on:
     * 1. https://codepen.io/durga598/pen/gXQjdw?editors=0010
     * 2. http://fabricjs.com/custom-controls-polygon
     */


    let activeLine;
    let activeShape;
    let canvas;
    let lineArray = [];
    let pointArray = [];
    let drawMode = false;

    function initCanvas() {
        canvas = new fabric.Canvas('c');
        canvas.backgroundColor = 'rgba(0,0,0,0.3)';
        canvas.setHeight(500);
        canvas.setWidth(500);

        fabric.Object.prototype.originX = 'center';
        fabric.Object.prototype.originY = 'center';

        canvas.on('mouse:down', onMouseDown);
        canvas.on('mouse:up', onMouseUp);
        canvas.on('mouse:move', onMouseMove);
        canvas.on('object:moving', onObjectMove);
        canvas.on('mouse:wheel', onMouseWheel);
    }

    function onMouseDown(options) {    
        if (drawMode) {
            if (options.target && options.target.id === pointArray[0].id) {
                // when click on the first point
                generatePolygon(pointArray);
            } else {
                addPoint(options);
            }
        }
        
        var evt = options.e;
        if (evt.altKey === true) {
            this.isDragging = true;
            this.selection = false;
            this.lastPosX = evt.clientX;
            this.lastPosY = evt.clientY;
        }
    }

    function onMouseUp(options) {
      this.isDragging = false;
      this.selection = true;
    }

    function onMouseMove(options) {
     if (this.isDragging) {
         var e = options.e;
         this.viewportTransform[4] += e.clientX - this.lastPosX;
            this.viewportTransform[5] += e.clientY - this.lastPosY;
            this.requestRenderAll();
            this.lastPosX = e.clientX;
            this.lastPosY = e.clientY;
        } 
        if (drawMode) {
            if (activeLine && activeLine.class === 'line') {
                const pointer = canvas.getPointer(options.e);
                activeLine.set({
                    x2: pointer.x,
                    y2: pointer.y
                });
                const points = activeShape.get('points');
                points[pointArray.length] = {
                    x: pointer.x,
                    y: pointer.y,
                };
                activeShape.set({
                    points
                });
            }
            canvas.renderAll();
        }
    }

    function onMouseWheel(options) {
      var delta = options.e.deltaY;
      var pointer = canvas.getPointer(options.e);
      var zoom = canvas.getZoom();  
      if (delta > 0) {
       zoom += 0.1;
      } else {
       zoom -= 0.1;
      }
      if (zoom > 20) zoom = 20;
      if (zoom < 0.1) zoom = 0.1;
      canvas.zoomToPoint({ x: options.e.offsetX, y: options.e.offsetY }, zoom);
      options.e.preventDefault();
      options.e.stopPropagation();
    }

    function onObjectMove(option) {
        const object = option.target;
        object._calcDimensions();
        object.setCoords();    
        canvas.renderAll();
    }

    function toggleDrawPolygon(event) {
        if (drawMode) {
            // stop draw mode
            activeLine = null;
            activeShape = null;
            lineArray = [];
            pointArray = [];
            canvas.selection = true;
            drawMode = false;
        } else {
            // start draw mode
            canvas.selection = false;
            drawMode = true;
        }
    }

    function addPoint(options) {
        const pointOption = {
            id: new Date().getTime(),
            radius: 5,
            fill: '#ffffff',
            stroke: '#333333',
            strokeWidth: 0.5,
            left: options.e.layerX / canvas.getZoom(),
            top: options.e.layerY / canvas.getZoom(),
            selectable: false,
            hasBorders: false,
            hasControls: false,
            originX: 'center',
            originY: 'center',
            objectCaching: false,
        };
        const point = new fabric.Circle(pointOption);

        if (pointArray.length === 0) {
            // fill first point with red color
            point.set({
                fill: 'red'
            });
        }

        const linePoints = [
            options.e.layerX / canvas.getZoom(),
            options.e.layerY / canvas.getZoom(),
            options.e.layerX / canvas.getZoom(),
            options.e.layerY / canvas.getZoom(),
        ];
        const lineOption = {
            strokeWidth: 2,
            fill: '#999999',
            stroke: '#999999',
            originX: 'center',
            originY: 'center',
            selectable: false,
            hasBorders: false,
            hasControls: false,
            evented: false,
            objectCaching: false,
        };
        const line = new fabric.Line(linePoints, lineOption);
        line.class = 'line';

        if (activeShape) {
            const pos = canvas.getPointer(options.e);
            const points = activeShape.get('points');
            points.push({
                x: pos.x,
                y: pos.y
            });
            const polygon = new fabric.Polygon(points, {
                stroke: '#333333',
                strokeWidth: 1,
                fill: '#cccccc',
                opacity: 0.3,
                selectable: false,
                hasBorders: false,
                hasControls: false,
                evented: false,
                objectCaching: false,
            });
            canvas.remove(activeShape);
            canvas.add(polygon);
            activeShape = polygon;
            canvas.renderAll();
        } else {
            const polyPoint = [{
                x: options.e.layerX / canvas.getZoom(),
                y: options.e.layerY / canvas.getZoom(),
            }, ];
            const polygon = new fabric.Polygon(polyPoint, {
                stroke: '#333333',
                strokeWidth: 1,
                fill: '#cccccc',
                opacity: 0.3,
                selectable: false,
                hasBorders: false,
                hasControls: false,
                evented: false,
                objectCaching: false,
            });
            activeShape = polygon;
            canvas.add(polygon);
        }

        activeLine = line;
        pointArray.push(point);
        lineArray.push(line);

        canvas.add(line);
        canvas.add(point);
    }

    function generatePolygon(pointArray) {
        const points = [];
        // collect points and remove them from canvas
        for (const point of pointArray) {
            points.push({
                x: point.left,
                y: point.top,
            });
            canvas.remove(point);
        }

        // remove lines from canvas
        for (const line of lineArray) {
            canvas.remove(line);
        }

        // remove selected Shape and Line 
        canvas.remove(activeShape).remove(activeLine);

        // create polygon from collected points
        const polygon = new fabric.Polygon(points, {
            id: new Date().getTime(),
            stroke: '#eee',
            fill: '#f00',
            objectCaching: false,
            moveable: false,
            //selectable: false
        });
        canvas.add(polygon);

        toggleDrawPolygon();
        editPolygon();
    }

    /**
     * define a function that can locate the controls.
     * this function will be used both for drawing and for interaction.
     */
    function polygonPositionHandler(dim, finalMatrix, fabricObject) {
        const transformPoint = {
            x: fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x,
            y: fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y,
        };
        return fabric.util.transformPoint(transformPoint, 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) {
        const polygon = transform.target;
        const currentControl = polygon.controls[polygon.__corner];
        const mouseLocalPosition = polygon.toLocalPoint(new fabric.Point(x, y), 'center', 'center');
        const size = polygon._getTransformedDimensions(0, 0);
        const finalPointPosition = {
            x: (mouseLocalPosition.x * polygon.width) / size.x + polygon.pathOffset.x,
            y: (mouseLocalPosition.y * polygon.height) / 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) {
            const fabricObject = transform.target;
            const point = {
                x: fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x,
                y: fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y,
            };

            // update the transform border
            fabricObject._setPositionDimensions({});

            // Now newX and newY represent the point position with a range from
            // -0.5 to 0.5 for X and Y.
            const newX = point.x / fabricObject.width;
            const newY = point.y / fabricObject.height;

            // Fabric supports numeric origins for objects with a range from 0 to 1.
            // This let us use the relative position as an origin to translate the old absolutePoint.
            const absolutePoint = fabric.util.transformPoint(point, fabricObject.calcTransformMatrix());
            fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);

            // action performed
            return fn(eventData, transform, x, y);
        };
    }

    function editPolygon() {
        let activeObject = canvas.getActiveObject();
        if (!activeObject) {
            activeObject = canvas.getObjects()[0];
            canvas.setActiveObject(activeObject);
        }

        activeObject.edit = true;
        activeObject.objectCaching = false;

        const lastControl = activeObject.points.length - 1;
        activeObject.cornerStyle = 'circle';
        activeObject.controls = activeObject.points.reduce((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;
        }, {});

        activeObject.hasBorders = false;

        canvas.requestRenderAll();
    }

    function resizePolygon() {
        let activeObject = canvas.getActiveObject();
        if (!activeObject) {
            activeObject = canvas.getObjects()[0];
            canvas.setActiveObject(activeObject);
        }

        activeObject.edit = false;
        activeObject.objectCaching = false;
        activeObject.controls = fabric.Object.prototype.controls;
        activeObject.cornerStyle = 'rect';
        activeObject.hasBorders = true;

        canvas.requestRenderAll();
    }

    initCanvas();
    <script src="https://unpkg.com/fabric@4.0.0-beta.12/dist/fabric.js"></script>

    <button type="button" onclick="toggleDrawPolygon()">Draw Polygon</button>
    <button type="button" onclick="editPolygon()">Edit Polygon</button>
    <button type="button" onclick="resizePolygon()">Resize/Move Polygon</button>

    <canvas id="c"></canvas>

我修改了代码以考虑 canvas 缩放和平移。

计算控件位置时需要考虑视口变换。

/**
     * based on:
     * 1. https://codepen.io/durga598/pen/gXQjdw?editors=0010
     * 2. http://fabricjs.com/custom-controls-polygon
     */


    let activeLine;
    let activeShape;
    let canvas;
    let lineArray = [];
    let pointArray = [];
    let drawMode = false;

    function initCanvas() {
        canvas = new fabric.Canvas('c');
        canvas.backgroundColor = 'rgba(0,0,0,0.3)';
        canvas.setHeight(500);
        canvas.setWidth(500);

        fabric.Object.prototype.originX = 'center';
        fabric.Object.prototype.originY = 'center';

        canvas.on('mouse:down', onMouseDown);
        canvas.on('mouse:up', onMouseUp);
        canvas.on('mouse:move', onMouseMove);
        canvas.on('object:moving', onObjectMove);
        canvas.on('mouse:wheel', onMouseWheel);
    }

    function onMouseDown(options) {    
        if (drawMode) {
            if (options.target && options.target.id === pointArray[0].id) {
                // when click on the first point
                generatePolygon(pointArray);
            } else {
                addPoint(options);
            }
        }
        
        var evt = options.e;
        if (evt.altKey === true) {
            this.isDragging = true;
            this.selection = false;
            this.lastPosX = evt.clientX;
            this.lastPosY = evt.clientY;
        }
    }

    function onMouseUp(options) {
      this.isDragging = false;
      this.selection = true;
    }

    function onMouseMove(options) {
     if (this.isDragging) {
         var e = options.e;
         this.viewportTransform[4] += e.clientX - this.lastPosX;
            this.viewportTransform[5] += e.clientY - this.lastPosY;
            this.requestRenderAll();
            this.lastPosX = e.clientX;
            this.lastPosY = e.clientY;
        } 
        if (drawMode) {
            if (activeLine && activeLine.class === 'line') {
                const pointer = canvas.getPointer(options.e);
                activeLine.set({
                    x2: pointer.x,
                    y2: pointer.y
                });
                const points = activeShape.get('points');
                points[pointArray.length] = {
                    x: pointer.x,
                    y: pointer.y,
                };
                activeShape.set({
                    points
                });
            }
            canvas.renderAll();
        }
    }

    function onMouseWheel(options) {
      var delta = options.e.deltaY;
      var pointer = canvas.getPointer(options.e);
      var zoom = canvas.getZoom();  
      if (delta > 0) {
       zoom += 0.1;
      } else {
       zoom -= 0.1;
      }
      if (zoom > 20) zoom = 20;
      if (zoom < 0.1) zoom = 0.1;
      canvas.zoomToPoint({ x: options.e.offsetX, y: options.e.offsetY }, zoom);
      options.e.preventDefault();
      options.e.stopPropagation();
    }

    function onObjectMove(option) {
        const object = option.target;
        object._calcDimensions();
        object.setCoords();    
        canvas.renderAll();
    }

    function toggleDrawPolygon(event) {
        if (drawMode) {
            // stop draw mode
            activeLine = null;
            activeShape = null;
            lineArray = [];
            pointArray = [];
            canvas.selection = true;
            drawMode = false;
        } else {
            // start draw mode
            canvas.selection = false;
            drawMode = true;
        }
    }

    function addPoint(options) {
        const pointOption = {
            id: new Date().getTime(),
            radius: 5,
            fill: '#ffffff',
            stroke: '#333333',
            strokeWidth: 0.5,
            left: options.e.layerX / canvas.getZoom(),
            top: options.e.layerY / canvas.getZoom(),
            selectable: false,
            hasBorders: false,
            hasControls: false,
            originX: 'center',
            originY: 'center',
            objectCaching: false,
        };
        const point = new fabric.Circle(pointOption);

        if (pointArray.length === 0) {
            // fill first point with red color
            point.set({
                fill: 'red'
            });
        }

        const linePoints = [
            options.e.layerX / canvas.getZoom(),
            options.e.layerY / canvas.getZoom(),
            options.e.layerX / canvas.getZoom(),
            options.e.layerY / canvas.getZoom(),
        ];
        const lineOption = {
            strokeWidth: 2,
            fill: '#999999',
            stroke: '#999999',
            originX: 'center',
            originY: 'center',
            selectable: false,
            hasBorders: false,
            hasControls: false,
            evented: false,
            objectCaching: false,
        };
        const line = new fabric.Line(linePoints, lineOption);
        line.class = 'line';

        if (activeShape) {
            const pos = canvas.getPointer(options.e);
            const points = activeShape.get('points');
            points.push({
                x: pos.x,
                y: pos.y
            });
            const polygon = new fabric.Polygon(points, {
                stroke: '#333333',
                strokeWidth: 1,
                fill: '#cccccc',
                opacity: 0.3,
                selectable: false,
                hasBorders: false,
                hasControls: false,
                evented: false,
                objectCaching: false,
            });
            canvas.remove(activeShape);
            canvas.add(polygon);
            activeShape = polygon;
            canvas.renderAll();
        } else {
            const polyPoint = [{
                x: options.e.layerX / canvas.getZoom(),
                y: options.e.layerY / canvas.getZoom(),
            }, ];
            const polygon = new fabric.Polygon(polyPoint, {
                stroke: '#333333',
                strokeWidth: 1,
                fill: '#cccccc',
                opacity: 0.3,
                selectable: false,
                hasBorders: false,
                hasControls: false,
                evented: false,
                objectCaching: false,
            });
            activeShape = polygon;
            canvas.add(polygon);
        }

        activeLine = line;
        pointArray.push(point);
        lineArray.push(line);

        canvas.add(line);
        canvas.add(point);
    }

    function generatePolygon(pointArray) {
        const points = [];
        // collect points and remove them from canvas
        for (const point of pointArray) {
            points.push({
                x: point.left,
                y: point.top,
            });
            canvas.remove(point);
        }

        // remove lines from canvas
        for (const line of lineArray) {
            canvas.remove(line);
        }

        // remove selected Shape and Line 
        canvas.remove(activeShape).remove(activeLine);

        // create polygon from collected points
        const polygon = new fabric.Polygon(points, {
            id: new Date().getTime(),
            stroke: '#eee',
            fill: '#f00',
            objectCaching: false,
            moveable: false,
            //selectable: false
        });
        canvas.add(polygon);

        toggleDrawPolygon();
        editPolygon();
    }

    /**
     * define a function that can locate the controls.
     * this function will be used both for drawing and for interaction.
     */
 function polygonPositionHandler(dim, finalMatrix, fabricObject) {
   var 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) {
  var 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) {
      var 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() {
        let activeObject = canvas.getActiveObject();
        if (!activeObject) {
            activeObject = canvas.getObjects()[0];
            canvas.setActiveObject(activeObject);
        }

        activeObject.edit = true;
        activeObject.objectCaching = false;

        const lastControl = activeObject.points.length - 1;
        activeObject.cornerStyle = 'circle';
        activeObject.controls = activeObject.points.reduce((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;
        }, {});

        activeObject.hasBorders = false;

        canvas.requestRenderAll();
    }

    function resizePolygon() {
        let activeObject = canvas.getActiveObject();
        if (!activeObject) {
            activeObject = canvas.getObjects()[0];
            canvas.setActiveObject(activeObject);
        }

        activeObject.edit = false;
        activeObject.objectCaching = false;
        activeObject.controls = fabric.Object.prototype.controls;
        activeObject.cornerStyle = 'rect';
        activeObject.hasBorders = true;

        canvas.requestRenderAll();
    }

    initCanvas();
<script src="https://unpkg.com/fabric@4.0.0-beta.12/dist/fabric.js"></script>

    <button type="button" onclick="toggleDrawPolygon()">Draw Polygon</button>
    <button type="button" onclick="editPolygon()">Edit Polygon</button>
    <button type="button" onclick="resizePolygon()">Resize/Move Polygon</button>

    <canvas id="c"></canvas>