Can not restore saved multi selection in FabricJS - 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

Related

Using Konvajs, I need to drag an object that is under another object WITHOUT bringing it to the top

Using Konvajs, I need to drag an object that is under another object WITHOUT bringing the bottom object to the top, the top object is NOT draggable.
Would appreciate any help, thank you.
Needing to detect clicks in stacked objects is a common requirement. Fortunately Konva makes this easy. Every shape will be 'listening' for events by default. So mouse over, mouse exit, click, double click, and their touch counterparts are all available to us (see docs).
In your case you need to make the shape that is higher up the stack stop listening so that one further down can hear it.
In the example below I am using the shape.listening(value) property which controls whether the shape is listening for events (true value) or not (false value).
Looking at the snippet, if we click the green spot (where the rectangles overlap), with the upper shape set listening(false) events pass through it to the next shape in the stack and we are informed that the click was on the lower shape. With the upper shape listening(true) the upper shape receives and eats the events.
I am also using an efficient approach to event detection, which is to listen for events on the layer rather than setting listeners per shape. In the case of a few shapes on the layer, using event listeners per shape is not going to be an issue, but when you have hundreds of shapes on the layer it will affect performance.
Of course delegating the listener to the layer leaves a requirement to recognise which shape was clicked. To do this I could have used logic to match on the shape names.
// when the user starts to drag shape...
layer.on('click', function(evt) {
if (evt.target.hasName('upperRect')) {
alert('Click on upperRect');
}
if (evt.target.hasName('lowerRect')) {
alert('Click on upperRect');
}
...
})
but since all I need to do here is show the name of the clicked shape, I chose to simply use
// when the user starts to drag shape...
layer.on('click', function(evt) {
showEvent('Click on ' + evt.target.name());
})
Finally it is worth noting that another approach to getting the clicked shape is to use the layer.getIntersection() method which requires a point parameter and will return the first shape (not a list of shapes!) that is on the layer at that point.
layer.on("click", function() {
var target = layer.getIntersection(stage.getPointerPosition());
if (target) {
alert('clicked on ' + target.name());
}
});
Bonus note: layer.on('click') does not detect clicks on the empty part of the layer.
// Set up a stage
stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
}),
// add a layer to draw on
layer = new Konva.Layer(),
// make the lower rect
rectLower = new Konva.Rect({
name: 'Lower rect',
x: 40,
y: 20,
width: 100,
height: 40,
stroke: 'cyan',
fill: 'cyan',
fillEnabled: true
}),
// Make the upper rect
rectUpper = rectLower.clone({
name: 'Upper rect',
stroke: 'magenta',
fill: 'magenta',
x: 0,
y: 0
}),
// add a click target for user
spot = new Konva.Circle({
x: 80,
y: 30,
fill: 'lime',
radius: 10,
listening: false
});
// Add the layer to the stage and shapes to layer
stage.add(layer);
layer.add(rectLower, rectUpper, spot)
// when the user clicks a shape...
layer.on('click', function(evt) {
showEvent('Click on ' + evt.target.name());
})
// Start / stop listening for events on to rect when checkbox changes.
$('#upperListening').on('change', function(){
switch ($(this).is(':checked')){
case true:
rectUpper.listening(true);
showEvent('Upper rect <b>is</b> listening...');
break;
case false:
rectUpper.listening(false);
showEvent('Upper rect <b>is not</b> listening...');
break;
}
// Important - if we change lsitening shapes we have to refresh hit checking list.
layer.drawHit();
})
stage.draw();
function showEvent(msg){
$('#info').html(msg)
}
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>Click the green dot. Watch the events. Click the tickbox to stop upper rect catching events.</p>
<p><input type='checkbox' id='upperListening' checked='checked' ><label for='upperListening'>Upper is listening</label>
<p id='info'>Events show here</p>
<div id="container"></div>

Move cursor ghost when creating a group by selecting multiple existing objects on canvas

fabricjs_2.3.6
I am working on an editor that will allow the user to create textboxes images and rectangles dynamically and that portion of the project works great. All objects on the canvas will pre-existing before creating groups therefore I do not want to hard-code any fabrics. I also want to preserve the positional relation between the grouped items.
I am having a problem with creating groups by selecting 2 or more existing objects on the canvas to create a group. My code works with one exception, if the selected items are not in the upper left corner of the canvas when I create the group the grouped objects remain in the original location as desired but the move handle is in the upper left corner of the canvas. If you click the move cursor handle ghost and then click a blank area on the canvas the problem goes away and everything works fine after that.
I have had no luck searching for a solution possibly because I'm not sure what controls the move cursor's location on the canvas or if it is actually called the "move cursor".
Here is a picture from my jsfiddle after clicking the group button:
Here is a jsfiddle link to demonstrate the problem: https://jsfiddle.net/Larry_Robertson/xfrd278a/
Here is my code:
HTML
<span> Test 1: select both the line and the text objects with the mouse then click group, works great.</span>
<br/>
<span> Test 2: select both the line and the text objects with the mouse, move the selction to the center of the canvas then click group. This creates the problem where a ghost move handle is left behind in the upper left corner of the canvas. The move handle did not update to the correct position when the group was created. If you hover the mouse in the upper left of canvas you will see the move cursor. Click on the move cursor then click any blank part of the canvas then you can reselect the group and it moves properly. What am I doing wrong???</span>
<br/>
<button id="group">group</button>
<canvas id="c" height="300" width="500"></canvas>
JS
var canvas = new fabric.Canvas('c');
var text = new fabric.Text('hello world', {
fontSize: 30,
originX: 'left',
originY: 'top',
left: 0,
top: 0
});
var line = new fabric.Line([10, 10, 100, 100], {
stroke: 'green',
strokeWidth: 2
});
canvas.add(line);
canvas.add(text);
canvas.renderAll();
$('#group').on(("click"), function(el) {
var activeObject = canvas.getActiveObject();
var selectionTop = activeObject.get('top');
var selectionLeft = activeObject.get('left');
var selectionHeight = activeObject.get('height');
var selectionWidth = activeObject.get('width');
if (activeObject.type === 'activeSelection') {
var group = new fabric.Group([activeObject], {
left: 0,
top: 0,
originX: 'center',
originY: 'center'
});
canvas.add(group);
deleteSelectedObjectsFromCanvas();
canvas.setActiveObject(group);
group = canvas.getActiveObject();
group.set('top', selectionTop + (selectionHeight / 2));
group.set('left', selectionLeft + (selectionWidth / 2));
group.set('originX', 'center');
group.set('originY', 'center');
canvas.renderAll();
}
});
function deleteSelectedObjectsFromCanvas() {
var selection = canvas.getActiveObject();
var editModeDected = false;
if (selection.type === 'activeSelection') {
selection.forEachObject(function(element) {
console.log(element);
if (element.type == 'textbox') {
if (element.isEditing == true) {
//alert('At least one textbox is currently being edited. No objects were deleted');
editModeDected = true;
}
}
});
if (editModeDected == true) {
return false;
}
// Its okay to delete all selected objects
selection.forEachObject(function(element) {
console.log('removing: ' + element.type);
//element.set('originX',null);
//element.set('originY',null);
canvas.remove(element);
canvas.discardActiveObject();
canvas.requestRenderAll();
});
} else {
if (selection.isEditing == true && selection.type == 'textbox') {
//alert('Textbox is currently being edited. No objects were deleted');
} else {
canvas.remove(selection);
canvas.discardActiveObject();
canvas.requestRenderAll();
}
}
}
CSS
#c {
margin: 10px;
padding: 10px;
border: 1px solid black;
}
After making changes on code always do a setCoords on the object(s) that got changed.
Here is a one liner you can add after the renderAll to fix your issue:
...
group.set('originY', 'center');
canvas.renderAll();
canvas.forEachObject(function(o) {o.setCoords()});

FabricJS double click new techniques?

could someone please point me in the direction for how to enable double click on fabric images? i came across this solution
FabricJS double click on objects
I am trying to not use FabicjsEx
but i am unable to get anything to work correctly. can someone please let me know the best way to accomplish this?
The best way to accomplish this, is to use fabric.util.addListener method.
Using that you could add a double click event for the canvas element and to restrict it to any particular object ( ie. image ), you would have to check whether you clicked on an image object or not before performing any action.
ᴅᴇᴍᴏ
var canvas = new fabric.Canvas('canvas');
// add image
fabric.Image.fromURL('https://i.imgur.com/Q6aZlme.jpg', function(img) {
img.set({
top: 50,
left: 50
})
img.scaleToWidth(100);
img.scaleToHeight(100);
canvas.add(img);
});
// add rect (for demo)
var rect = new fabric.Rect({
left: 170,
top: 50,
width: 100,
height: 100,
fill: '#07C'
});
canvas.add(rect);
// mouse event
fabric.util.addListener(canvas.upperCanvasEl, 'dblclick', function(e) {
if (canvas.findTarget(e)) {
const objType = canvas.findTarget(e).type;
if (objType === 'image') {
alert('double clicked on a image!');
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.9/fabric.js"></script>
<canvas id="canvas" width="320" height="200"></canvas>

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.

What can I get a handle on the target of a fabric js event?

In this fabricjs tutorial section it says putting listeners right on an object like so
var rect = new fabric.Rect({ width: 100, height: 50, fill: 'green' });
rect.on('selected', function() {
console.log('selected a rectangle');
});
That is fine, but I don't want to log a string to the console. I want to manipulate the rectangle. Specifically the rectangle isn't selectable by default and based on application state, I need to make it selectable.
I can't figure out how to get a handle on the rectangle from inside the event handler. How to do that escapes me.
In the example you gave, you have attached the event to the object itself. So you can access the object using this or the variable name (which is rect in your case). Here is an example
var rect = new fabric.Rect({ width: 100, height: 50, fill: 'green' });
rect.on('selected', function() {
console.log(this);
this.set({width:200});
});
Alternatively, if you attach it to the canvas so that it listens to all the object:selected events, you can access the object at which the event occurred like this:
var rect = new fabric.Rect({ width: 100, height: 50, fill: 'green' });
canvas.on('object:selected', function (e) {
console.log('selected a rectangle');
e.target.set({width:200});
});

Resources