fabric.js 3.6 - Bounding box for group doesn't update when grouped objects are changed - fabricjs

The problem I'm having with fabric.js seems simple, but I've been searching for a solution without any luck, so here I am.
I create a simple group to draw an arrow, consisting of a line object and a rotated triangle placed at its end point. This works fine, but when I dynamically update the stroke width of the line and the width/height of the triangle, the group's bounding box doesn't update to match the new dimensions. Here's an image of the result:
Here's a live demo. Just drag the slider to change the stroke width and triangle size and you'll see the issue.
let canvas = new fabric.Canvas(document.querySelector('canvas'), {
backgroundColor: 'white'
}),
object,
lineCoords = [100, 75, 250, 75],
strokeWidth = parseFloat(document.querySelector('.strokewidth').value),
createArrow = () => {
return new fabric.Group(
[
new fabric.Line(lineCoords, {
left: lineCoords[0],
top: lineCoords[1],
stroke: 'red',
strokeWidth: strokeWidth,
strokeLineCap: "round",
}),
new fabric.Triangle({
width: strokeWidth + 22,
height: strokeWidth + 22,
fill: 'red',
left: lineCoords[0] + 150 + strokeWidth + 22 + ((strokeWidth + 22) / 2) - 14,
top: lineCoords[1] - (Math.floor((strokeWidth + 22) / 2)) + (Math.floor(strokeWidth / 2)),
angle: 90,
})
],
{ objectCaching: false }
)
}
let arrow = createArrow()
canvas.add(arrow)
canvas.setActiveObject(arrow)
canvas.renderAll()
$('body').on('input change', '.strokewidth', e => {
if (object = canvas.getActiveObject()) {
let value = parseFloat(e.target.value),
[ line, triangle ] = object.getObjects()
line.set({
strokeWidth: value
})
triangle.set({
width: value + 22,
height: value + 22,
left: line.left + line.width + value + 22 + ((value + 22) / 2) - 14,
top: line.top - (Math.floor((value + 22) / 2)) + (Math.floor(value / 2))
})
canvas.renderAll()
}
})
body {
background: #f0f0f0;
padding: 0;
margin: 0;
}
canvas {
border: 1px solid lightgray;
}
div {
margin: 0.5em 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.0/fabric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas width="400" height="150"></canvas>
<div>
<input type="range" class="strokewidth" min="1" max="40" value="5">
</div>
I'm using version 3.6 of fabric.js. Apparently there was a way to refresh the bounding box in older versions, but none of the code works with 3.6 - the functions no longer exist. I've tried several kludges to fix it, including ungrouping and regrouping the objects, without any luck so far.
Does anyone know how to force a recalculation of the bounding box in this situation? Thanks for any help.

In FabricJS 3 the best way to scale a group of objects is to change the scale value of the whole group:
group.set({ scaleX: value, scaleY: value })
I assume that sometimes it is required to scale only some objects inside a group or scale them with a different proportion. I think historically it was decided that this heavy operation needs to be triggered by the developer. There is a method for doing that addWithUpdate and it can be called without passing an object to it.
let canvas = new fabric.Canvas(document.querySelector('canvas'), {
backgroundColor: 'white'
}),
object,
lineCoords = [100, 75, 250, 75],
strokeWidth = parseFloat(document.querySelector('.strokewidth').value),
createArrow = () => {
return new fabric.Group(
[
new fabric.Line(lineCoords, {
left: lineCoords[0],
top: lineCoords[1],
stroke: 'red',
strokeWidth: strokeWidth,
strokeLineCap: "round",
}),
new fabric.Triangle({
width: strokeWidth + 22,
height: strokeWidth + 22,
fill: 'red',
left: lineCoords[0] + 150 + strokeWidth + 22 + ((strokeWidth + 22) / 2) - 14,
top: lineCoords[1] - (Math.floor((strokeWidth + 22) / 2)) + (Math.floor(strokeWidth / 2)),
angle: 90,
})
],
{ objectCaching: false }
)
}
let arrow = createArrow()
canvas.add(arrow)
canvas.setActiveObject(arrow)
canvas.renderAll()
$('body').on('input change', '.strokewidth', e => {
if (object = canvas.getActiveObject()) {
let value = parseFloat(e.target.value),
[ line, triangle ] = object.getObjects()
line.set({
strokeWidth: value
})
triangle.set({
width: value + 22,
height: value + 22,
left: line.left + line.width + value + 22 + ((value + 22) / 2) - 14,
top: line.top - (Math.floor((value + 22) / 2)) + (Math.floor(value / 2))
})
canvas.renderAll()
arrow.addWithUpdate()
}
})
body {
background: #f0f0f0;
padding: 0;
margin: 0;
}
canvas {
border: 1px solid lightgray;
}
div {
margin: 0.5em 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.0/fabric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas width="400" height="150"></canvas>
<div>
<input type="range" class="strokewidth" min="1" max="40" value="5">
</div>

Related

How to align Top/Left/Bottom/Right of selected objects (FabricJS)

var canvas = window._canvas = new fabric.Canvas('c');
var red = new fabric.Rect({
top: 100,
left: 0,
width: 80,
height: 50,
fill: 'red'
});
var blue = new fabric.Rect({
top: 0,
left: 100,
width: 50,
height: 70,
fill: 'blue'
});
var green = new fabric.Rect({
top: 100,
left: 100,
width: 60,
height: 60,
fill: 'green'
});
canvas.add(red, blue, green);
const alignLeft = document.getElementById('align_left');
alignLeft.onclick = function() {
const objs = canvas.getActiveObjects();
}
<script src="https://rawgithub.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id="c" ></canvas>
<button id="align_left">Align Left</button>
Looking for a utility method to align edges (top,right,bottom,left) of array of objects from canvas.getActiveObjects in FabricJS v3
http://jsfiddle.net/rlightner/bzs0798x/1/
$('#align_left').click(function(){canvas.getActiveObject().set({left:0);})

Fabricjs mask object with transformation

I'm trying to mask an object using Fabric.js free drawing brush. It works fine if the object is in its default position and without any transformations. But once I add transformations to the object, the mask is placed in the wrong position. I'm not sure how to solve this. Can someone take a look?
I want to be able to apply any transformations, before or after the mask, without messing up the mask.
let canvas = new fabric.Canvas("canvas", {
backgroundColor: "lightgray",
width: 1280,
height: 720,
preserveObjectStacking: true,
selection: false,
stateful: true
});
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.color = "black";
canvas.freeDrawingBrush.width = 2;
canvas.on("path:created", function(options) {
clip(options.path);
});
function clip(path) {
canvas.isDrawingMode = false;
canvas.remove(path);
let mask = new fabric.Path(path.path, {
top: object.top,
left: object.left,
objectCaching: false,
strokeWidth: 0,
pathOffset: {
x: 0,
y: 0
}
});
let originalObjLeft = object.left,
originalObjTop = object.top;
object.set({
clipTo: function(ctx) {
mask.set({
left: -object.width / 2 - mask.width / 2 - originalObjLeft,
top: -object.height / 2 - mask.height / 2 - originalObjTop,
objectCaching: false
});
mask.render(ctx);
}
});
canvas.requestRenderAll();
}
// image
let image = new Image();
let object;
image.onload = function() {
object = new fabric.Image(image, {
width: 500,
height: 500,
//scaleX: 0.8,
//scaleY: 0.8,
//angle: 45,
top: 50,
left: 300
});
canvas.add(object);
};
image.src = "http://i.imgur.com/8rmMZI3.jpg";
I implement an exemple with some transformations (scaleX,scaleY,left,top).
I'm strugle to find a solution when the inital object have an angle different than 0. For the current solution I need it to divide the maskscale with the object scale and also adjust the positions.
let canvas = new fabric.Canvas("canvas", {
backgroundColor: "lightgray",
width: 1280,
height: 720,
preserveObjectStacking: true,
selection: false,
stateful: true
});
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.color = "black";
canvas.freeDrawingBrush.width = 2;
canvas.on("path:created", function(options) {
clip(options.path);
});
function clip(path) {
canvas.isDrawingMode = false;
canvas.remove(path);
let mask = new fabric.Path(path.path, {
top: object.top,
left: object.left,
objectCaching: false,
strokeWidth: 0,
scaleX : 1/object.scaleX,
scaleY : 1/object.scaleY,
pathOffset: {
x: 0,
y: 0
}
});
let originalObjLeft = object.left,
originalObjTop = object.top,
originalMaskScaleX = mask.scaleX,
originalMaskScaleY = mask.scaleY,
originalObjScaleX = object.scaleX,
originalObjScaleY = object.scaleY;
object.set({
clipTo: function(ctx) {
mask.set({
left: -object.width / 2 -( mask.width / 2 * originalMaskScaleX) - originalObjLeft/originalObjScaleX ,
top: -object.height / 2 -( mask.height / 2 * originalMaskScaleY) - originalObjTop/originalObjScaleY ,
objectCaching: false
});
mask.render(ctx);
}
});
canvas.requestRenderAll();
}
// image
let image = new Image();
image.onload = function() {
object = new fabric.Image(image, {
width: 500,
height: 500,
scaleX: 0.8,
scaleY: 0.8,
// angle: 45,
top: 50,
left: 100
});
canvas.add(object);
};
image.src = "http://i.imgur.com/8rmMZI3.jpg";
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.6/fabric.js"></script>
<div class="canvas__wrapper">
<canvas id="canvas" width="1280" height="720"></canvas>
</div>
You can check here for loadFromJSON support.
The only problem remains is when the object is rotated.
Basically whenever you set an angle, your context matrix has been transformed. In order to mask properly you need to return to initial state of the Transformation Matrices. Fabricjs handles first matrix with center point of an object (calculates center of an object with or without an angle). Second matrix is rotating matrix, and third - scaling.
To display image with all options which are set to an object, you need to multiply all Matrices:
(First Matrix * Second Matrix) * Third Matrix
So the idea of clipping will be reverse engineering of rotating context and multiplications of matrices:
difference between center points of regular object without rotation and center point of the same object but with rotation. After that take result of subtractions and divide by original object scale value.
let canvas = new fabric.Canvas("canvas", {
backgroundColor: "lightgray",
width: 1280,
height: 720,
preserveObjectStacking: true,
selection: false,
stateful: true
});
const angle = 45;
let objectHasBeenRotated = false;
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.color = "black";
canvas.freeDrawingBrush.width = 2;
canvas.on("path:created", function (options) {
clip(options.path);
});
function clip(path) {
canvas.isDrawingMode = false;
canvas.remove(path);
let mask = new fabric.Path(path.path, {
top: 0,
left: 0,
objectCaching: false,
strokeWidth: 0,
scaleX: 1 / object.scaleX,
scaleY: 1 / object.scaleY,
pathOffset: {
x: 0,
y: 0,
}
});
let originalObjLeft = object.left,
originalObjTop = object.top,
originalMaskScaleX = mask.scaleX,
originalMaskScaleY = mask.scaleY,
originalObjScaleX = object.scaleX,
originalObjScaleY = object.scaleY,
transformedTranslate = object.translateToGivenOrigin({
x: object.left,
y: object.top
}, object.originX, object.originY, 'center', 'center'),
originalTransformLeft = transformedTranslate.x - object.getCenterPoint().x,
originalTransformTop = transformedTranslate.y - object.getCenterPoint().y;
object.set({
clipTo: function (ctx) {
ctx.save();
ctx.rotate(-angle * Math.PI / 180);
ctx.translate(originalTransformLeft / originalObjScaleX, originalTransformTop / originalObjScaleY)
mask.set({
left: -object.width / 2 - (mask.width / 2 * originalMaskScaleX) - originalObjLeft / originalObjScaleX,
top: -object.height / 2 - (mask.height / 2 * originalMaskScaleY) - originalObjTop / originalObjScaleY,
objectCaching: false
});
mask.render(ctx);
ctx.restore();
}
});
canvas.requestRenderAll();
}
// image
let image = new Image();
image.onload = function () {
object = new fabric.Image(image, {
width: 500,
height: 500,
scaleX: 0.8,
scaleY: 0.8,
angle: angle,
top: 50,
left: 300,
id: 'pug'
});
canvas.add(object);
};
image.src = "http://i.imgur.com/8rmMZI3.jpg";
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.6/fabric.js"></script>
<div class="canvas__wrapper">
<canvas id="canvas" width="1280" height="720"></canvas>
</div>

How to align object by bounding box in FabricJS?

Wondering if there is a way to align objects in FabricJs by their bounding box?
I'm using obj.getBoundingRect() function to determine objects bounders, then compare them with a bounding box (BB) coordinates of an Active one (that one which I move). If I see that something falls between some gap (let's say 10px) I assign an active object top to be the same top as a comparable element by using a .setTop() property.
The problem is that TOP is not a right attribute to use, since the top of the bounding box may differ between elements. For example, 2 elements with the same top but different angle will have different Bounding Box Top...
Hope you see my point...
https://jsfiddle.net/redlive/hwcu1p4f/
var canvas = this.__canvas = new fabric.Canvas('canvas');
//fabric.Object.prototype.transparentCorners = false;
var red = new fabric.Rect({
id: 1,
left: 100,
top: 50,
width: 100,
height: 100,
fill: 'red',
angle: 0,
padding: 10
});
canvas.add(red);
var green = new fabric.Rect({
id: 2,
left: 250,
top: 180,
width: 100,
height: 100,
fill: 'green',
angle: 45,
padding: 10
});
canvas.add(green);
canvas.renderAll();
canvas.on("object:moving", function(e){
const draggableObj = e.target;
const draggableObjBound = draggableObj.getBoundingRect();
canvas.forEachObject(function(obj) {
if (obj.id !== draggableObj.id) {
var bound = obj.getBoundingRect();
if (draggableObjBound.top > bound.top - 10 && draggableObjBound.top < bound.top + 10) {
draggableObj.setTop(obj.getTop());
}
}
});
});
canvas.forEachObject(function(obj) {
var setCoords = obj.setCoords.bind(obj);
obj.on({
moving: setCoords,
scaling: setCoords,
rotating: setCoords
});
});
canvas.on('after:render', function() {
canvas.contextContainer.strokeStyle = '#555';
canvas.forEachObject(function(obj) {
var bound = obj.getBoundingRect();
canvas.contextContainer.strokeRect(
bound.left,
bound.top,
bound.width,
bound.height
);
})
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.0.0-rc.3/fabric.js"></script>
<canvas id="canvas" width="800" height="500" style="border:1px solid #ccc"></canvas>
You should use the center to align them, that is not gonna change.
to align the bounding box at left 5 for example:
1) calculate bounding box.
2) set the position of the object to 5 + bb.width/2 considering center.
In this case the bounding rects get aligned.
var canvas = this.__canvas = new fabric.Canvas('canvas');
//fabric.Object.prototype.transparentCorners = false;
var red = new fabric.Rect({
id: 1,
left: 100,
top: 50,
width: 100,
height: 100,
fill: 'red',
angle: 0,
padding: 10
});
canvas.add(red);
var green = new fabric.Rect({
id: 2,
left: 250,
top: 180,
width: 100,
height: 100,
fill: 'green',
angle: 45,
padding: 10
});
canvas.add(green);
//ALIGN EVERYTHING TO 5
canvas.forEachObject(function(object) {
var bb = object.getBoundingRect();
object.setPositionByOrigin({ x: 5 + bb.width/2, y: bb.top }, 'center', 'center');
object.setCoords();
});
canvas.renderAll();
canvas.on("object:moving", function(e){
const draggableObj = e.target;
const draggableObjBound = draggableObj.getBoundingRect(true, true);
canvas.forEachObject(function(obj) {
if (obj.id !== draggableObj.id) {
var bound = obj.getBoundingRect(true, true);
if (draggableObjBound.top > bound.top - 10 && draggableObjBound.top < bound.top + 10) {
draggableObj.setPositionByOrigin({ x: draggableObj.left, y: bound.top + draggableObjBound.height/2 }, draggableObj.originX, 'center');
}
}
});
});
canvas.forEachObject(function(obj) {
var setCoords = obj.setCoords.bind(obj);
obj.on({
moving: setCoords,
scaling: setCoords,
rotating: setCoords
});
});
canvas.on('after:render', function() {
canvas.contextContainer.strokeStyle = '#555';
canvas.forEachObject(function(obj) {
var bound = obj.getBoundingRect(true, true);
canvas.contextContainer.strokeRect(
bound.left,
bound.top,
bound.width,
bound.height
);
})
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.0.0-rc.3/fabric.js"></script>
<canvas id="canvas" width="800" height="500" style="border:1px solid #ccc"></canvas>

FabricJS. How to add dimensions displaying in controls?

How to add dimensions displaying in controls like on screenshot?
Create a text object and set it hidden. And when you are scalling object set the scale width and height to hidden text and make visible. On object modified set visible false to the text.
DEMO
var canvas = new fabric.Canvas("c");
canvas.setHeight(200);
canvas.setWidth(300);
var dimText = new fabric.Text("demo", {
fontSize: 15,
visible: false
});
canvas.add(dimText);
var circle = new fabric.Circle({
left: 15,
top: 15,
radius: 20,
fill:'',
stroke: 'red'
});
canvas.add(circle);
var text = new fabric.Text("2018", {
padding: 30,
lineHeight: 30
});
canvas.add(text);
canvas.centerObject(text);
text.setCoords();
canvas.on('object:scaling', function(option) {
var object = option.target;
var pointer = canvas.getPointer(option.e);
dimText.set({
left: pointer.x - 20,
top: pointer.y - 20,
text: parseInt(object.width * object.scaleX) + 'x' + parseInt(object.height * object.scaleY),
visible: true
})
});
canvas.on('object:modified', function(option) {
dimText.set('visible', false);
});
canvas {
border: 1px solid #dddddd;
margin-top: 10px;
border-radius: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.7/fabric.min.js"></script>
<canvas id="c"></canvas>

fabricjs: Manipulate selected objects

i try to change the color of all selected objects on my canvas. I figured out that i can change properties of the group itself (this.canvas.getActiveGroup().setFill(...)).
Since i want to change different properties depending on the type of the selected object, i can't use this function. Instead i try to iterate over all selected objects and manipulate them.
this.canvas.getActiveGroup().getObjects().forEach(obj =>
{
if(obj instanceof fabric.Path)
{
obj.setStroke(color);
}
else
{
obj.setFill(color);
}
});
this.canvas.renderAll();
The color will be changed, but unfortunately after deselecting the objects all styles will be changed back.
I think the styles will be applied to the group, because when selecting again all elements will appear in the right color...
When i remove the activeGroup (this.canvas.discardActiveGroup()) before, it will work but will bring some other problems.
Btw.: the inspector (dev tools) displays the right color. After resizing a specific element it will also update its color.
Has anyone similar problems and knows a solution?
Best regards
Daniel
DEMO
var color = 'black';
function changeColor() {
var r = getRandomArbitrary(0, 225);
var g = getRandomArbitrary(0, 225);
var b = getRandomArbitrary(0, 225);
color = 'rgb(' + r + ',' + g + ',' + b + ')';
fillColorOb();
}
function getRandomArbitrary(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var c = new fabric.Canvas('canvas');
c.add(new fabric.Circle({
left: 50,
top: 50,
radius: 50,
originX: 'center',
originY: 'center',
}))
c.add(new fabric.Circle({
left: 50,
top: 150,
radius: 50,
originX: 'center',
originY: 'center',
}))
c.add(new fabric.Circle({
left: 150,
top: 100,
radius: 50,
originX: 'center',
originY: 'center',
}))
c.renderAll();
function fillColorOb() {
var objs = c.getActiveGroup();
if(!objs) return;
objs.forEachObject(function(obj) {
if (obj instanceof fabric.Path) {
obj.setStroke(color);
} else {
obj.setFill(color);
}
c.renderAll();
});
}
fabric.Group.prototype._restoreObjectState = function(object) {
this.realizeTransform(object);
object.setCoords();
object.hasControls = object.__origHasControls;
delete object.__origHasControls;
object.set('active', false);
delete object.group;
object.dirty = true; // it will render on next renderAll() call
return this;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.16/fabric.min.js"></script>
<button type="button" name="button" onclick="changeColor();">color</button><br>
<canvas id="canvas" width="400" height="400"></canvas>
If I am , then this gonna work, check demo, add this function
fabric.Group.prototype._restoreObjectState = function(object) {
this.realizeTransform(object);
object.setCoords();
object.hasControls = object.__origHasControls;
delete object.__origHasControls;
object.set('active', false);
delete object.group;
object.dirty = true; // it will render on next renderAll() call
return this;
}

Resources