Fabricjs snap to grid on resize

Viewed 3358

I'm having some issues trying to get the objects to resize according to the grid size.

Here's my fiddle: http://jsfiddle.net/csh6c6fw/1/

this is the code that I'm applying:

canvas.on('object:scaling', (options) => {
  var newWidth = (Math.round(options.target.getWidth() / grid)) * grid;
  var newHeight = (Math.round(options.target.getHeight() / grid)) * grid;

  if (options.target.getWidth() !== newWidth) {
    options.target.set({ width: newWidth, height: newHeight });
  }

});

Expected Result

It's supposed to snap to the grids like the movement.

5 Answers

This is an update to the accepted answer.

target.getWidth() and target.getHeight() don't seem to work anymore, so I updated the fiddle from the accepted answer to replace them with target.width * target.scaleX and target.height * target.scaleY.

Here is the updated fiddle

First of all neither of all answers from above worked with rotated objects and it was a big deal for me. I spent few day on a grid snapping and finally I found a solution that works well, at least it fits my needs. On the first time I did calculations with school math: Math.tan + Pythagorean theorem, but it was too hard and I was pretty sure that I'm doing something wrong.

First of all find and check how wrapWithFixedAnchor function works. Basically you can change any attribute of your target: width, height, scaleX, scaleY and then wrapWithFixedAnchor will help you to translate your target to the anchor point.

Here is the working example: http://jsfiddle.net/kod57cwb

I don't think that calling it as a 'snapping' is a good idea, because in reality it would snap to a grid only when objects are zero rotated. Currently it works pretty similar to http://draw.io snapping

The answers above don't seem to work anymore. When the object is resized fabric seems to update the scaleX/scaleY instead of the width/height.

Here is a new fiddle: http://jsfiddle.net/ej2hrqm8/

I disabled TL/TR/BL because I cannot get those three corners to work.

// Build using FabricJS v3.4

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

// create grid

for (var i = 0; i < (600 / gridSize); i++) {
  canvas.add(new fabric.Line([ i * gridSize, 0, i * gridSize, 600], { stroke: '#ccc', selectable: false }));
  canvas.add(new fabric.Line([ 0, i * gridSize, 600, i * gridSize], { stroke: '#ccc', selectable: false }))
}

// add objects

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

canvas.add(new fabric.Circle({ 
  left: 300, 
  top: 300, 
  radius: 50, 
  fill: '#9f9', 
  originX: 'left', 
  originY: 'top',
  centeredRotation: true
}));

function Snap(value)
{
  return Math.round(value / snapSize) * snapSize;
}

function SnapMoving(options)
{
  options.target.set({
    left: Snap(options.target.left),
    top: Snap(options.target.top)
  });
}

function SnapScaling(options)
{
  var target = options.target;
  var pointer = options.pointer;

  var px = Snap(pointer.x);
  var py = Snap(pointer.y);
  var rx = (px - target.left) / target.width;
  var by = (py - target.top) / target.height;
  var lx = (target.left - px + (target.width * target.scaleX)) / (target.width);
  var ty = (target.top - py + (target.height * target.scaleY)) / (target.height);

  var a = {};

  // Cannot get snap to work on some corners :-(
  switch (target.__corner)
  {
    case "tl":
      // Not working
      //a = { scaleX: lx, scaleY: ty, left: px, top: py };
      break;
    case "mt":
      a = { scaleY: ty, top: py };
      break;
    case "tr":
      // Not working
      //a = { scaleX: rx, scaleY: ty, top: py  };
      break;
    case "ml":
      a = { scaleX: lx, left: px };
      break;
    case "mr":
      a = { scaleX: rx };
      break;
    case "bl":
      // Not working
      //a = { scaleX: lx, scaleY: by, left: px };
      break;
    case "mb":
      a = { scaleY: by };
      break;
    case "br":
      a = { scaleX: rx, scaleY: by };
      break;
  }

  options.target.set(a);
}

canvas.on({
  "object:moving": SnapMoving,
  "object:scaling": SnapScaling,
});

Please note that fabric.js internally computes the size by adding strokeWidth to height and width see. This prevents both the proposed solutions from working properly:

  • Etherman's solutions drift when resizing tl and bl corners.
  • GRUNT's one makes the lower right corner wobble while resizing (due to numeric error).

I while share what has worked for me (sorry is in TypeScript)


import { fabric } from 'fabric';

export class GridSnapFabric extends fabric.Canvas {

  protected gridGranularity = 20;

  constructor(canvas: HTMLCanvasElement) {
    super(canvas);
    this.on('object:scaling', this.onFabricObjectScaling.bind(this));
  }

  private snapGrid(cord: number): number {
    return Math.round(cord / this.gridGranularity) * this.gridGranularity;
  }

  private onFabricObjectScaling(e: fabric.IEvent) {
    const active = this.getActiveObject();
    const [width, height] = [active.getScaledWidth(), active.getScaledHeight()];

    // X
    if (['tl', 'ml', 'bl'].indexOf(e.transform.corner) !== -1) {
      const tl = this.snapGrid(active.left);
      active.scaleX = (width + active.left - tl) / (active.width + active.strokeWidth);
      active.left = tl;
    } else if (['tr', 'mr', 'br'].indexOf(e.transform.corner) !== -1) {
      const tl = this.snapGrid(active.left + width);
      active.scaleX = (tl - active.left) / (active.width + active.strokeWidth);
    }

    // Y
    if (['tl', 'mt', 'tr'].indexOf(e.transform.corner) !== -1) {
      const tt = this.snapGrid(active.top);
      active.scaleY = (height + active.top - tt) / (active.height + active.strokeWidth);
      active.top = tt;
    } else if (['bl', 'mb', 'br'].indexOf(e.transform.corner) !== -1) {
      const tt = this.snapGrid(active.top + height);
      active.scaleY = (tt - active.top) / (active.height + active.strokeWidth);
    }

    // Avoid singularities
    active.scaleX = (active.scaleY >= 0 ? 1 : -1) * Math.max(Math.abs(active.scaleX), 0.001);
    active.scaleY = (active.scaleY >= 0 ? 1 : -1) * Math.max(Math.abs(active.scaleY), 0.001);
  }

}
Related