Search code examples
javascriptfabricjs

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


Can someone please suggest me the right way to update the transform points and custom control points of my custom polygon (draw by mouse move + click) after zoom or pan the canvas?

Step to reproduce:

  1. click on Draw Polygon

  2. draw a polygon in canvas

  3. zoom (scroll up or down) or pan (alt + mouse drag)

Here is my code: 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>


Solution

  • i modified the code to take in account of the canvas zoom and panning.

    The viewport transform needs to be take in account when calculating the position of the controls.

    /**
         * 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>