Search code examples
javascriptfabricjs

Objects snap to custum grid on fabric.js


I am building an app where I need the objects snap to a grid on mouseMove but I don't need a regular grid but a custumized one. I found this Fiddle and tried to place my own grid but objects do not snap to it. What I need is the center of the objects to snap to the cross formed by two grid lines.

This is my code:

var canvas = new fabric.Canvas('c', { selection: false });
var grid = 20;

// create grid

for (var i = 0; i < (canvas.height / grid); i++) {
  canvas.add(new fabric.Path('m 260.75,122.82188 0,349.31925', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 309.75,122.82188 0,349.31925', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 353.75,122.82188 0,349.31925', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 391.25,122.82188 0,349.31925', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 419.25,122.82188 0,349.31925', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 435.25,122.82188 0,349.31925', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 216.5,122.82188 0,349.31925', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 179,122.82188 0,349.31925', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 151,122.82188 0,349.31925', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 135,122.82188 0,349.31925 ', { stroke: '#ccc', selectable: false }));

  canvas.add(new fabric.Path('m 110.91682,147.25 348.63852,0', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 110.91682,161.25 348.63852,0', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 110.91682,188 348.63852,0', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 110.91682,224.75 348.63852,0', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 110.91682,269.75 348.63852,0', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 110.91682,326.05147 348.63852,0', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 110.91682,370.83102 348.63852,0', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 110.91682,407.75 348.63852,0', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 110.91682,434.25 348.63852,0', { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Path('m 110.91682,448.5 348.63852,0', { stroke: '#ccc', selectable: false }));
}

// add objects

canvas.add(new fabric.Rect({
  left: 100,
  top: 100,
  width: 20,
  height: 20,
  fill: '#faa',
  originX: 'center',  originY: 'center',
  centeredRotation: true
}));

// snap to grid

canvas.on('object:moving', function(options) { 
  options.target.set({
    left: Math.round(options.target.left / grid) * grid,
    top: Math.round(options.target.top / grid) * grid
  });
});
<html>
<head>
<script src="https://rawgithub.com/kangax/fabric.js/master/dist/fabric.js"></script>


</head>
<body>
<canvas id="c" width="600" height="600"></canvas>

If someone able to help it will be welcomed.

After analyzing this again I have found that the svg is snapping to a grid maybe given in this var var grid = 20; but not to the grid I've created using canvas.add(new fabric.Path('m 110.91682,147.25 348.63852,0', { stroke: '#ccc', selectable: false }));. This is likely the culprit though I am not sure.


Solution

  • Indeed the var grid = 20 is exactly what is causing your issue as you have a grid that is not spaced the same. You have statically created your grid, however using an object that defined the starting points and spacing would be more dynamic and allow you to use that same object to snap your event to the dynamically created grid points.

    I was able to do this using an array of objects for the grid spacing and instantiating both a start and coords object with an array for both x and y placing the lines into the coords object under each coord key.

    The coords object is actually the space between each line from the starting position. The start object is the start position of the edge of the lines being drawn both in x and y axis.

    We will use these objects to dynamically create the grid and a points object to reference later for snapping the mouse move to the grid.

    Define the starting point using gridSpacing[0]['start']['x'] and gridSpacing[0]['start']['y']. Then two arrays to hold the values for the dynamically created x axis lines and y axis lines - xArr and yArr.

    We then create a nested for loop, looping over both xArr and yArr to create a points object.

    Two more for loops to dynamically create the grid using the xArr* and yArr arrays.

    Lastly the snapping portion within the object:moving event. Instantiate an axis object that will hold the event.target.left and event.target.top properties as key/value pairs. We use this to iterate over the points object and use conditionals to check the position of the mouse move event in relation to the points objects key/values => keys for the x axis and values for the y axis.

    var canvas = new fabric.Canvas('c', {
      selection: false
    });
    
    // insantiate object for starting and coordinates of spacing
    const gridSpacing = [{
      start: {
        x: 111,
        y: 123.25
      },
      coords: {
        x: [24, 16, 28, 37.5, 44.25, 49, 44, 37.5, 28, 16],
        y: [24, 16, 28, 37.5, 44.25, 49, 44, 37.5, 28, 16]
      }
    }]
    
    // create grid
    
    // define the starting coords
    let xCoords = gridSpacing[0]['start']['x'];
    let yCoords = gridSpacing[0]['start']['y'];
    
    // instantiate x axis and y axis arrays
    let xArr = []
    let yArr = []
    
    // fill xArr with values using gridSpacing object
    for (let x = 0; x < gridSpacing[0].coords['x'].length; x++) {
      let spacing = gridSpacing[0].coords['x'][x]
      xArr.push(xCoords += spacing)
    }
    // fill yArr with values using gridSpacing object
    for (let y = 0; y < gridSpacing[0].coords['y'].length; y++) {
      let spacing = gridSpacing[0].coords['y'][y]
      yArr.push(yCoords += spacing)
    }
    // instantiate points obj using xArr and yArr for in loops
    const points = []
    for (let i in xArr) {
      for (let y in yArr) {
        points.push({
          [xArr[i]]: yArr[y]
        })
      }
    }
    
    /* EDIT: This is the section that draws the grid lines. No need for it as you are using the points object and xArr and yArr to make your mouse snap to the spacing. 
    
    // create and draw x axis grid dynamically using xArr
    for (let x = 0; x < xArr.length; x++) {
      canvas.add(new fabric.Path(`m ${xArr[x]},122.82188 0,349.31925`, {
        stroke: '#ccc',
        selectable: false
      }));
    }
    // create and draw y axis grid dynamically using yArr
    for (let y = 0; y < gridSpacing[0].coords['y'].length; y++) {
      canvas.add(new fabric.Path(`m 110.91682,${yArr[y]} 348.63852,0`, {
        stroke: '#ccc',
        selectable: false
      }));
    }
    */
    
    // add objects
    
    canvas.add(new fabric.Rect({
      left: 100,
      top: 100,
      width: 20,
      height: 20,
      fill: '#faa',
      originX: 'center',
      originY: 'center',
      centeredRotation: true
    }));
    
    canvas.add(new fabric.Circle({
      left: xArr[5],
      top: yArr[5],
      radius: 60,
      fill: '#9f9',
      originX: 'center',
      originY: 'center',
      centeredRotation: true
    }));
    
    // snap to grid
    
    canvas.on('object:moving', function(event) {
      // create objbect that will hold current mouse move events x and y
      let axis = {
        [event.target.left]: event.target.top
      }  
      let x, y = '';
      // iterate over our points and check event.target values
      points.forEach((point, i) => {
        // conditional to check x axis boundaries and point variations
        // left
        if (parseInt(Object.keys(axis)) < xArr[1] - xArr[1] * .1 || parseInt(Object.keys(axis)) < xArr[0]) {
          x = xArr[0]
        // right
        }else if (parseInt(Object.keys(axis)) > xArr[xArr.length - 1]) {
          x = xArr[xArr.length - 1]
        // boundaries
        }else if (Object.keys(axis) < Object.keys(points[i]) && Object.keys(axis) > Object.keys(points[i - 1]) && Object.keys(axis) > xArr[0]) {
          x = Object.keys(points[i])
        }
        
        // conditional to check y axis boundaries and point variations
        // up
        if (parseInt(Object.values(axis)) < yArr[1] - yArr[1] * .1 || parseInt(Object.values(axis)) < yArr[0]) {
          y = yArr[0]
        // down
        }else if (parseInt(Object.values(axis)) > yArr[yArr.length - 1]) {
          y = yArr[yArr.length - 1]
          // boundaries
        }else if (Object.values(axis) < Object.values(points[i]) && Object.values(axis) > Object.values(points[i - 1]) && Object.values(axis) > xArr[0]) {
          y = Object.values(points[i])
        }
      }) 
      // set event.target left and top properties
      event.target.set({
        left: parseInt(x),
        top: parseInt(y)
      })
    })
    <html>
    
    <head>
      <script src="https://rawgithub.com/kangax/fabric.js/master/dist/fabric.js"></script>
    
    
    </head>
    
    <body>
      <canvas id="c" width="600" height="600"></canvas>
    </body>