I'm having some issues trying to get the objects to resize according to the grid size.
Here's my fiddle: /
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.
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.
Share Improve this question asked May 24, 2017 at 2:04 A. LA. L 12.6k29 gold badges98 silver badges178 bronze badges5 Answers
Reset to default 10Might seem pretty complicated but, the following will get the job done :
canvas.on('object:scaling', options => {
var target = options.target,
w = target.width * target.scaleX,
h = target.height * target.scaleY,
snap = { // Closest snapping points
top: Math.round(target.top / grid) * grid,
left: Math.round(target.left / grid) * grid,
bottom: Math.round((target.top + h) / grid) * grid,
right: Math.round((target.left + w) / grid) * grid
},
threshold = grid,
dist = { // Distance from snapping points
top: Math.abs(snap.top - target.top),
left: Math.abs(snap.left - target.left),
bottom: Math.abs(snap.bottom - target.top - h),
right: Math.abs(snap.right - target.left - w)
},
attrs = {
scaleX: target.scaleX,
scaleY: target.scaleY,
top: target.top,
left: target.left
};
switch (target.__corner) {
case 'tl':
if (dist.left < dist.top && dist.left < threshold) {
attrs.scaleX = (w - (snap.left - target.left)) / target.width;
attrs.scaleY = (attrs.scaleX / target.scaleX) * target.scaleY;
attrs.top = target.top + (h - target.height * attrs.scaleY);
attrs.left = snap.left;
} else if (dist.top < threshold) {
attrs.scaleY = (h - (snap.top - target.top)) / target.height;
attrs.scaleX = (attrs.scaleY / target.scaleY) * target.scaleX;
attrs.left = attrs.left + (w - target.width * attrs.scaleX);
attrs.top = snap.top;
}
break;
case 'mt':
if (dist.top < threshold) {
attrs.scaleY = (h - (snap.top - target.top)) / target.height;
attrs.top = snap.top;
}
break;
case 'tr':
if (dist.right < dist.top && dist.right < threshold) {
attrs.scaleX = (snap.right - target.left) / target.width;
attrs.scaleY = (attrs.scaleX / target.scaleX) * target.scaleY;
attrs.top = target.top + (h - target.height * attrs.scaleY);
} else if (dist.top < threshold) {
attrs.scaleY = (h - (snap.top - target.top)) / target.height;
attrs.scaleX = (attrs.scaleY / target.scaleY) * target.scaleX;
attrs.top = snap.top;
}
break;
case 'ml':
if (dist.left < threshold) {
attrs.scaleX = (w - (snap.left - target.left)) / target.width;
attrs.left = snap.left;
}
break;
case 'mr':
if (dist.right < threshold) attrs.scaleX = (snap.right - target.left) / target.width;
break;
case 'bl':
if (dist.left < dist.bottom && dist.left < threshold) {
attrs.scaleX = (w - (snap.left - target.left)) / target.width;
attrs.scaleY = (attrs.scaleX / target.scaleX) * target.scaleY;
attrs.left = snap.left;
} else if (dist.bottom < threshold) {
attrs.scaleY = (snap.bottom - target.top) / target.height;
attrs.scaleX = (attrs.scaleY / target.scaleY) * target.scaleX;
attrs.left = attrs.left + (w - target.width * attrs.scaleX);
}
break;
case 'mb':
if (dist.bottom < threshold) attrs.scaleY = (snap.bottom - target.top) / target.height;
break;
case 'br':
if (dist.right < dist.bottom && dist.right < threshold) {
attrs.scaleX = (snap.right - target.left) / target.width;
attrs.scaleY = (attrs.scaleX / target.scaleX) * target.scaleY;
} else if (dist.bottom < threshold) {
attrs.scaleY = (snap.bottom - target.top) / target.height;
attrs.scaleX = (attrs.scaleY / target.scaleY) * target.scaleX;
}
break;
}
target.set(attrs);
});
Here is a working example :
var canvas = new fabric.Canvas('c', {
selection: false
});
var grid = 50;
// create grid
for (var i = 0; i < (600 / grid); i++) {
canvas.add(new fabric.Line([i * grid, 0, i * grid, 600], {
stroke: '#ccc',
selectable: false
}));
canvas.add(new fabric.Line([0, i * grid, 600, i * grid], {
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
}));
// snap to grid
canvas.on('object:moving', options => {
options.target.set({
left: Math.round(options.target.left / grid) * grid,
top: Math.round(options.target.top / grid) * grid
});
});
canvas.on('object:scaling', options => {
var target = options.target,
w = target.width * target.scaleX,
h = target.height * target.scaleY,
snap = { // Closest snapping points
top: Math.round(target.top / grid) * grid,
left: Math.round(target.left / grid) * grid,
bottom: Math.round((target.top + h) / grid) * grid,
right: Math.round((target.left + w) / grid) * grid
},
threshold = grid,
dist = { // Distance from snapping points
top: Math.abs(snap.top - target.top),
left: Math.abs(snap.left - target.left),
bottom: Math.abs(snap.bottom - target.top - h),
right: Math.abs(snap.right - target.left - w)
},
attrs = {
scaleX: target.scaleX,
scaleY: target.scaleY,
top: target.top,
left: target.left
};
switch (target.__corner) {
case 'tl':
if (dist.left < dist.top && dist.left < threshold) {
attrs.scaleX = (w - (snap.left - target.left)) / target.width;
attrs.scaleY = (attrs.scaleX / target.scaleX) * target.scaleY;
attrs.top = target.top + (h - target.height * attrs.scaleY);
attrs.left = snap.left;
} else if (dist.top < threshold) {
attrs.scaleY = (h - (snap.top - target.top)) / target.height;
attrs.scaleX = (attrs.scaleY / target.scaleY) * target.scaleX;
attrs.left = attrs.left + (w - target.width * attrs.scaleX);
attrs.top = snap.top;
}
break;
case 'mt':
if (dist.top < threshold) {
attrs.scaleY = (h - (snap.top - target.top)) / target.height;
attrs.top = snap.top;
}
break;
case 'tr':
if (dist.right < dist.top && dist.right < threshold) {
attrs.scaleX = (snap.right - target.left) / target.width;
attrs.scaleY = (attrs.scaleX / target.scaleX) * target.scaleY;
attrs.top = target.top + (h - target.height * attrs.scaleY);
} else if (dist.top < threshold) {
attrs.scaleY = (h - (snap.top - target.top)) / target.height;
attrs.scaleX = (attrs.scaleY / target.scaleY) * target.scaleX;
attrs.top = snap.top;
}
break;
case 'ml':
if (dist.left < threshold) {
attrs.scaleX = (w - (snap.left - target.left)) / target.width;
attrs.left = snap.left;
}
break;
case 'mr':
if (dist.right < threshold) attrs.scaleX = (snap.right - target.left) / target.width;
break;
case 'bl':
if (dist.left < dist.bottom && dist.left < threshold) {
attrs.scaleX = (w - (snap.left - target.left)) / target.width;
attrs.scaleY = (attrs.scaleX / target.scaleX) * target.scaleY;
attrs.left = snap.left;
} else if (dist.bottom < threshold) {
attrs.scaleY = (snap.bottom - target.top) / target.height;
attrs.scaleX = (attrs.scaleY / target.scaleY) * target.scaleX;
attrs.left = attrs.left + (w - target.width * attrs.scaleX);
}
break;
case 'mb':
if (dist.bottom < threshold) attrs.scaleY = (snap.bottom - target.top) / target.height;
break;
case 'br':
if (dist.right < dist.bottom && dist.right < threshold) {
attrs.scaleX = (snap.right - target.left) / target.width;
attrs.scaleY = (attrs.scaleX / target.scaleX) * target.scaleY;
} else if (dist.bottom < threshold) {
attrs.scaleY = (snap.bottom - target.top) / target.height;
attrs.scaleX = (attrs.scaleY / target.scaleY) * target.scaleX;
}
break;
}
target.set(attrs);
});
canvas {border: 1px solid #ccc}
<script src="https://rawgithub.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id="c" width="600" height="600"></canvas>
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
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
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
andbl
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);
}
}