FabricJS resizing groups and objects in them - fabricjs

I need to programmaticaly resize a group and it's contained elements on a fabric canvas.
By default, fabric applies scaling to objects. I can get around this easily enough by using the scalex & scaley to calculate the new height and width then I set them back to 1. This works fine for the group but I cant figure out how to set the new size for the objects contained in this group.
Eg. Before resize:
After resize:
My (typescript) code is like:
redraw() {
this.shapeGroup.set({
scaleX: 1,
scaleY: 1,
left: this.shape.origin.x,
top: this.shape.origin.y,
width: this.shape.extent.width + this.shape.line.lineWidth,
height: this.shape.extent.height + this.shape.line.lineWidth,
dirty: true
});
// const ac = this.shapeGroup.calcCoords();
this.shapeElement.set({
rx: this.shape.cornerRadius,
ry: this.shape.cornerRadius,
width: this.shape.extent.width,
height: this.shape.extent.height,
dirty: true
});
// this.shapeElement.setCoords(ac);
if (this.shape.text) {
this.textElement.set({
width: this.shapeElement.width - (this.cornerRadius * 2),
height: this.shapeElement.height - (this.cornerRadius * 2)
});
}
this.canvas.fabric.renderAll();
}
(this.shape is the underlying model of the object which is represented by 1 or more fabric objects in a group).
Has anyone done anything like this with success?

I used a similar code to resize the group items, so maybe this can lead you to a solution.
canvas.setActiveGroup(this.shapeGroup.setCoords());
var objs = canvas.getActiveGroup().getObjects();
for(i in objs){
objs[i].set({
...
});
}

Related

Change size directly by Controls in fabricjs?

I develop web aplication with fabricjs ,At present, scaleX or scaleY is changed by Controls in fabricjs, but I want to change width or height directly by Controls. How can I do that?
try this Code :
canvas.on({
'object:scaling': function(e){
var selected = e.target;
selected.set({
height: selected.height * selected.scaleY,
width: selected.width * selected.scaleX,
scaleX: 1,
scaleY: 1
});
canvas.renderAll();
}
})
set height and width after calculation of scaling and make scale 1.

How to get top left of an object inside group fabricjs

How can I get canvas-relative position (top, left) of triangle inside an group as bellow image?
I followed this topic: How to get the canvas-relative position of an object that is in a group? but it only right when group is not rotated.
Working example you may find here: http://jsfiddle.net/mmalex/2rsevdLa/
Fabricjs provides a comprehensive explanation of how transformations are applied to the objects: http://fabricjs.com/using-transformations
Quick answer: the coordinates of an object inside the group is a point [0,0] transformed exactly how the object in the group was transformed.
Follow my comments in code to get the idea.
// 1. arrange canvas layout with group of two rectangles
var canvas = new fabric.Canvas(document.getElementById('c'));
var rect = new fabric.Rect({
width: 100,
height: 100,
left: 50,
top: 50,
fill: 'rgba(255,0,0,0.25)'
});
var smallRect = new fabric.Rect({
width: 12,
height: 12,
left: 150 - 12,
top: 50 + 50 - 12 / 2 - 10,
fill: 'rgba(250,250,0,0.5)'
});
// 2. add a position marker (red dot) for visibility and debug reasons
var refRect = new fabric.Rect({
width: 3,
height: 3,
left: 100,
top: 100,
fill: 'rgba(255,0,0,0.75)'
});
var group = new fabric.Group([rect, smallRect], {
originX: 'center',
originY: 'center'
});
canvas.add(group);
canvas.add(refRect);
canvas.renderAll();
// 3. calculate coordinates of child object in canvas space coords
function getCoords() {
// get transformation matrixes for object and group individually
var mGroup = group.calcTransformMatrix(true);
// flag true means that we need local transformation for the object,
// i.e. how object is positioned INSIDE the group
var mObject = smallRect.calcTransformMatrix(true);
console.log("group: ", fabric.util.qrDecompose(mGroup));
console.log("rect: ", fabric.util.qrDecompose(mObject));
// get total transformattions that were applied to the child object,
// the child is transformed in following order:
// canvas zoom and pan => group transformation => nested object => nested object => etc...
// for simplicity, ignore canvas zoom and pan
var mTotal = fabric.util.multiplyTransformMatrices(mGroup, mObject);
console.log("total: ", fabric.util.qrDecompose(mTotal));
// just apply transforms to origin to get what we want
var c = new fabric.Point(0, 0);
var p = fabric.util.transformPoint(c, mTotal);
console.log("coords: ", p);
document.getElementById("output").innerHTML = "Coords: " + JSON.stringify(p);
// do some chores, place red point
refRect.left = p.x - 3 / 2;
refRect.top = p.y - 3 / 2;
canvas.bringToFront(refRect);
canvas.renderAll();
}
a very simple way to get topleft is
var cords = object._getLeftTopCoords();
cords.x and cord.y will give you the result

Can not restore saved multi selection in FabricJS

Can you help me figure out what is going on here.
Basically I want to use FabricJS with ReactJS and Redux. I want to store the currently selected objects in redux store. But it seems it has a weird behavior when it comes to saving the active objects of multi selection.
let selectedObjects = null;
let canvas = new fabric.Canvas('canvas-area', {
preserveObjectStacking: true,
width: 500,
height: 500
});
canvas.add(new fabric.Rect({
left: 100,
top: 100,
fill: 'blue',
width: 100,
height: 100
}));
canvas.add(new fabric.Circle({
radius: 50,
fill: 'green',
left: 200,
top: 200
}));
canvas.renderAll();
// =======================
document.querySelector('#save-selection').addEventListener('click', ()=> {
//selectedObjects = Object.assign({}, canvas.getActiveObject());
selectedObjects = canvas.getActiveObject();
canvas.discardActiveObject();
canvas.renderAll();
});
document.querySelector('#apply-saved-selection').addEventListener('click', ()=> {
canvas.setActiveObject(selectedObjects);
canvas.renderAll();
});
<html>
<body>
<button id='save-selection'>Save Selection</button>
<button id='apply-saved-selection'>Apply Saved Selection</button>
<canvas id='canvas-area'></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.3/fabric.min.js"></script>
</body>
</html>
Basically the way you use the code snippet above are:
Use Case 1 : Single selected object
Select a single object.
Click Save Selection ( it will save the current selected object and clear current canvas selection ).
Click Apply Saved Selection.
Drag the currently selected object, the selection and the selected object stays in synced and the app works as expected.
Use Case 2 : Multiple selection
Select multiple objects.
Click Save Selection.
Click Apply Saved Selection.
Drag the currently selected objects, as you can see, only the selection is dragged, the actual selected objects are left behind (unsynced).
Can you help me figure out how to properly saved the selected object/s for later use?
Thanks in advance
Note:
I'm using latest FabricJS (2.3.3)
Update:
It is not wise to store selected elements in redux store. It is very hard to reload saved selected elements specially when we handle grouped selection, grouped objects, clipped images, etc...
Bottom Line, Do not save selected elements in redux store.
==================
Update:
My answer below have a flaw, and that is the rotation angle of the selection is not preserved when saved rotated selection is being loaded.
Still looking for a much better answer than what I currently have.
==================
Ok so it seems we need to have different approach in reloading multi selected objects.
Summary
We need to create a new selection using the exact objects in the canvas that correspond to the objects inside the saved selection.
// Extend fabric js so that all objects inside the canvas
// will have the custom id attribute
// We will use this later to determine which is which
fabric.Object.prototype.id = undefined;
let selectedObjects = null;
let canvas = new fabric.Canvas('canvas-area', {
preserveObjectStacking: true,
width: 500,
height: 500
});
canvas.add(new fabric.Rect({
id: 'unique-id-01',
left: 100,
top: 100,
fill: 'blue',
width: 100,
height: 100
}));
canvas.add(new fabric.Circle({
id: 'unique-id-02',
radius: 50,
fill: 'green',
left: 200,
top: 200
}));
canvas.add(new fabric.Rect({
id: 'unique-id-03',
left: 300,
top: 300,
fill: 'red',
width: 100,
height: 100
}));
canvas.renderAll();
// =======================
document.querySelector('#save-selection').addEventListener('click', ()=> {
//selectedObjects = Object.assign({}, canvas.getActiveObject());
selectedObjects = canvas.getActiveObject();
canvas.discardActiveObject();
canvas.renderAll();
});
document.querySelector('#apply-saved-selection').addEventListener('click', ()=> {
if (selectedObjects.type === 'activeSelection') {
let canvasObjects = canvas.getObjects(); // Get all the objects in the canvas
let selObjs = [];
// Loop through all the selected objects
// Then loop through all the objects in the canvas
// Then check the id of each objects if they matched
// Then store the matching objects to the array
for (let so of selectedObjects._objects) {
for (let obj of canvasObjects) {
if (obj.id === so.id) {
selObjs.push(obj); // Store the canvasObjects item, not the selectedObjects item.
break;
}
}
}
// Create a new selection instance using the filtered objects above
let selection = new fabric.ActiveSelection(selObjs, {
canvas: canvas
});
canvas.setActiveObject(selection); // Profit
} else { // Single object selected, load directly
canvas.setActiveObject(selectedObjects);
}
canvas.renderAll();
});
<html>
<body>
<button id='save-selection'>Save Selection</button>
<button id='apply-saved-selection'>Apply Saved Selection</button>
<canvas id='canvas-area'></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.3/fabric.min.js"></script>
</body>
</html>
We need to extend fabric js and add custom id attribute.
We will use this later to compare between the objects inside the fabric js instance and the objects inside the saved selection.
Loop through the fabric js instance objects and the saved selection objects and determine which of the objects in fabric js instance are selected by using the custom id we added earlier.
Create new selection instance, new fabric.ActiveSelection, using the filtered objects in step 2.
Set the fabric.ActiveSelection instance as the activeObject of the fabric js instance.
Profit
Fabric JS Selection Handling Reference

Fabricjs How to scale object but keep the border (stroke) width fixed

I'm developing a diagram tool based on fabricjs. Our tool has our own collection of shape, which is svg based. My problem is when I scale the object, the border (stroke) scale as well. My question is: How can I scale the object but keep the stroke width fixed. Please check the attachments.
Thank you very much!
Here is an easy example where on scale of an object we keep a reference to the original stroke and calculate a new stroke based on the scale.
var canvas = new fabric.Canvas('c', { selection: false, preserveObjectStacking:true });
window.canvas = canvas;
canvas.add(new fabric.Rect({
left: 100,
top: 100,
width: 50,
height: 50,
fill: '#faa',
originX: 'left',
originY: 'top',
stroke: "#000",
strokeWidth: 1,
centeredRotation: true
}));
canvas.on('object:scaling', (e) => {
var o = e.target;
if (!o.strokeWidthUnscaled && o.strokeWidth) {
o.strokeWidthUnscaled = o.strokeWidth;
}
if (o.strokeWidthUnscaled) {
o.strokeWidth = o.strokeWidthUnscaled / o.scaleX;
}
})
canvas {
border: 1px solid #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.4/fabric.min.js"></script>
<canvas id="c" width="600" height="600"></canvas>
There is a property called: strokeUniform
Use it like this
shape.set({stroke: '#f55b76', strokeWidth:2, strokeUniform: true })
I have found what feels like an even better solution, works really well with SVG paths.
You can override fabricjs' _renderStroke method and add ctx.scale(1 / this.scaleX, 1 / this.scaleY); before ctx.stroke(); as shown below.
fabric.Object.prototype._renderStroke = function(ctx) {
if (!this.stroke || this.strokeWidth === 0) {
return;
}
if (this.shadow && !this.shadow.affectStroke) {
this._removeShadow(ctx);
}
ctx.save();
ctx.scale(1 / this.scaleX, 1 / this.scaleY);
this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke);
this._applyPatternGradientTransform(ctx, this.stroke);
ctx.stroke();
ctx.restore();
};
You may also need to override fabric.Object.prototype._getTransformedDimensions to adjust the bounding box to account for the difference in size.
Also a more complete implementation would probably add a fabric object property to conditionally control this change for both overridden methods.
Another way is to draw a new object on scaled and remove the scaled one.
object.on({
scaled: function()
{
// store new widht and height
var new_width = this.getScaledWidth();
var new_height = this.getScaledHeight();
// remove object from canvas
canvas.remove(this);
// add new object with same size and original options like strokeWidth
canvas.add(new ...);
}
});
Works perfect for me.

JointJs: Scale custom shape html along with element (using paperScroller.zoom)

Using something like paperScroller.zoom(0.2, { max: 5 }); only causes svg elements to be zoomed, whereas in my custom shape, I've used html as well, which doesn't scale in tandem.
Since there's no model.change event firing on scaling, the updateBox method in ElementView doesn't get called and so the html elements don't sync their dimensions and positioning accordingly. Is there a way to work around this?
Extending on Marc_Alx's answer.
Calling paper.translate() and paper.scale() fires 'translate' and 'scale' events on the paper.
Custom Elements can listen to these events on their custom ElementView.
For example, if you scale on mousewheel event:
paper.on('blank:mousewheel', (event, x, y, delta) => {
const scale = paper.scale();
paper.scale(scale.sx + (delta * 0.01), scale.sy + (delta * 0.01),);
});
Override render methods of your custom ElementView to listen to 'scale' event on the paper.
render(...args) {
joint.dia.ElementView.prototype.render.apply(this, args);
this.listenTo(this.paper, 'scale', this.updateBox);
this.listenTo(this.paper, 'translate', this.updateBox);
this.paper.$el.prepend(this.$box);
this.updateBox();
return this;
},
And the custom ElementView's updateBox should retrieve the scale value from the paper.
updateBox() {
if (!this.paper) return;
const bbox = this.getBBox({ useModelGeometry: true });
const scale = joint.V(this.paper.viewport).scale();
// custom html updates
this.$box.find('label').text(this.model.get('label'));
this.$box.find('p').text(this.model.get('response'));
// end of custom html updates
this.$box.css({
transform: `scale(${scale.sx},${scale.sy})`,
transformOrigin: '0 0',
width: bbox.width / scale.sx,
height: bbox.height / scale.sy,
left: bbox.x,
top: bbox.y,
});
}
I've found a workaround for those who are not using rappid.js (paperScroller is only available for rappid).
Assuming you have an element similar to this one : http://jointjs.com/tutorial/html-elements
My answer is inspired from Murasame answer's from How to scale jonitjs graphs? and assume that you update scale of your paper on mousewheel (+0.1 -0.1).
First inside your custom ElementView store a numeric property (initialized to 1) that will store the scale, let's name it scale (accessed via this.scale).
Next in the overrided method render (of your cutom ElementView) listen to mousewheel events of the paper : this.paper.$el.on('mousewheel',…) in the handler update the scaleproperty (+=0.1 or -=0.1), then call updateBox
Finally in the updateBox method at this line :
this.$box.css({ width: bbox.width, height: bbox.height, left: bbox.x, top: bbox.y, transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)' });
Multiply width, height, x, y by this.scale to get :
this.$box.css({ width: bbox.width*this.scale, height: bbox.height*this.scale, left: bbox.x*this.scale, top: bbox.y*this.scale, transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)' });
I think it's a good start for implementing this behavior, you should also size the content of your element.
Answering this since I've spent hours trying to find a solution for this. And it turned out there's a perfectly working demo (but somehow Google always gives your the outdated tutorial. See: https://github.com/clientIO/joint/issues/1220) on JointJS. It's quite similar to Raunak Mukhia's answer though.
https://github.com/clientIO/joint/blob/master/demo/shapes/src/html.js

Resources