fabric js: objects grouping is not working correctly - javascript

I am having problem with grouping multiple objects on canvas. When I select objects using holding down shift key and then if I make a group of those objects, the newly added group has incorrect z-index of it's items. Pleas run the snippet bellow to understand the problem.
Please select green box first, then select blue and then red while the shift key is pressed. Then clik on Group button to see the problem.
canvas = new fabric.Canvas('canvas', {
isDrawingMode: false,
preserveObjectStacking: true
});
fabric.Object.prototype.objectCaching = true;
canvas.setWidth(380);
canvas.setHeight(310);
canvas.setBackgroundColor('#F2F2F2');
canvas.renderAll();
var topMost = new fabric.Rect({
left: 90,
top: 90,
width: 50,
height: 50,
fill: 'green',
});
var middle = new fabric.Rect({
left: 70,
top: 70,
width: 50,
height: 50,
fill: 'blue',
});
var bottom = new fabric.Rect({
left: 50,
top: 50,
width: 50,
height: 50,
fill: 'red',
});
canvas.add(bottom);
canvas.add(middle);
canvas.add(topMost);
$(".group").on('click', function () {
var activegroup = canvas.getActiveGroup();
var objectsInGroup = activegroup.getObjects();
activegroup.clone(function (newgroup) {
canvas.discardActiveGroup();
objectsInGroup.forEach(function (object) {
canvas.remove(object);
});
canvas.add(newgroup);
});
});
h3{color: blue;}h5{color: red;}canvas{border: 2px solid black;}
button{padding: 8px;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.17/fabric.min.js"></script>
<h3>Please select green box first, then select blue and then red while the shift key is pressed. Then clik on Group button to see the problem</h3>
<h5>Problem: Grouping multiple objects, selected using shift key down, changes their z-index after grouped</h5>
<button class="group">Group</button>
<canvas id="canvas"></canvas>

Fabric has never automatically preserved object order in groups. I see you were using preserveObjectStacking = true, and I think this might have caused your confusion. This property does not affect the actual order of the objects to "preserve" the way they are on the canvas. Instead, it changes the order the objects appear to have while selected, as well as the click-target-finding when clicking on these objects.
Objects in the active selection are added in the order of clicking, and adding with preserveObjectStacking, they get rendered in canvas order. The first 2 objects get ordered in stack order anyway.
In your specific case, you click
green -> activeObject
blue -> blue is under green so you get activeGroup with blue, green.
red -> red gets pushed in active group at end of objects array.
The order that you get when you clone is exactly blue, green, red.
To preserve the order that the objects have on the canvas, you can easily write a sort function and run it against your group's objects before cloning it.
var sorter = function (a, b) {
var idxA = canvas._objects.indexOf(a),
idxB = canvas._objects.indexOf(b);
return idxA > idxB ? 1 : 0;
};
var activegroup = canvas.getActiveGroup();
activegroup._objects.sort(sorter);
var objectsInGroup = activegroup.getObjects(); //these are now in the canvas's order
//.....continue with your original code

Related

Adding transformer to dynamically generated shapes?

I have the button which add new group which have square, to layer when clicked very simple code I guess no need to post. But my question is that how can I add transformer to it when on clicked?, I have done it with this mouseleave and mouseenter functions.
group.on('mouseenter', () => {
transformer.borderEnabled(true);
transformer.resizeEnabled(true);
layer.draw();
});
group.on('mouseleave', () => {
transformer.borderEnabled(false);
transformer.resizeEnabled(false);
layer.draw();
});
It is in loop which creates new group named "group", It works fine but in circle when I hover it the transformer appears but then when I go to transformer's boxes to resize it consider as it is mouseleave but this is doing only in circle not in line text.
So can I have solution for active transformer on element which is clicked or for considering hover on transformer boxes as a hover on node? Thanks
The mouseleave() will always fire because the pointer must leave the group to use the transformer handles or spinner.
An alternative approach would be
click to enable the transformer,
leave the transformer in place even when the mouse moves away
wait for a click on some other shape to know you can hide the transformer.
That is the standard GUI approach I believe.
If you need to show hover focus then stick a transparent rectangle the size of the groups clientrect into the group and change its stroke from transparent to some colour in the mouseenter and back in the mouseleave. You will also maybe want to set the rect.listening to false so as it coes not interfere with mouse events on the shapes in the group, but then again it might help in dragging.
Demo below.
// Set up the canvas and shapes
let stage = new Konva.Stage({container: 'container1', width: 300, height: 200});
let layer = new Konva.Layer({draggable: false});
stage.add(layer);
// Add a transformer.
let transFormer1 = new Konva.Transformer();
layer.add(transFormer1);
// Create a sample group
let group1 = new Konva.Group();
layer.add(group1);
group1.add(new Konva.Circle({x: 20, y: 30, radius: 15, fill: 'magenta', stroke: 'black'}))
group1.add(new Konva.Circle({x: 60, y: 40, radius: 15, fill: 'magenta', stroke: 'black'}))
group1.add(new Konva.Rect({x: 90, y: 60, width: 25, height: 25, fill: 'magenta', stroke: 'black'}));
let pos = group1.getClientRect();
let boundRect1 = new Konva.Rect({name: 'boundRect', x: pos.x, y: pos.y, width: pos.width, height: pos.height, fill: 'transparent', stroke: 'transparent'});
group1.add(boundRect1);
// When mouse enters the group show a border
group1.on('mouseenter', function(){
let boundRect = this.find('.boundRect');
boundRect[0].stroke('red');
layer.draw();
})
// and remove border when mouse leaves
group1.on('mouseleave', function(){
let boundRect = this.find('.boundRect');
boundRect[0].stroke('transparent');
layer.draw();
})
// If the group is clicked, enable the transformer on that group.
group1.on('click', function(){
transFormer1.attachTo(this)
layer.batchDraw();
})
// For a more pleasing demo let us have 2 groups.
// Make a copy of group1, offset new group, and change fill on its child shapes except the bound rect
let group2 = group1.clone();
layer.add(group2)
group2.position({x: 120, y: 30});
for (let i = 0, shapes = group2.getChildren(); i < shapes.length; i = i + 1){
shapes[i].fill(shapes[i].fill() !== 'transparent' ? 'cyan' : 'transparent');
}
stage.draw();
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>Move mouse over the shapes to see the group borders, click a group to apply the transformer.
</p>
<div id='container1' style="display: inline-block; width: 300px, height: 200px; background-color: silver; overflow: hidden; position: relative;"></div>
Got the answer!, I just create a public transformer and on stage click I am adding nodes to it no transformer to each group just one public transformer which hold one node at a time.

Fabric.js: How to keep text from blurring as group grows larger

In fabricjs, version 1.7.20, I noticed that whenever a group grows beyond 1152 pixels (12in) then the text objects in the group begin to blur. I'm programmatically setting the group size using the set() method when I need to grow the group.
Is there a way to keep them crisp/normal? Should I be calling some other method on the text object or group so that the text remains readable? I realize that the fonts are vectors but I'm not sure how to keep the font size constant regardless of the group size.
In this particular case I can't scale the group as I need the text object to stay the same size regardless of the group size.
Here is an example of the blurring effect:
https://jsfiddle.net/c7fn1e08/
Here is some example code as well:
var c = new fabric.Canvas('c');
var lbl = new fabric.Text('RESZING GROUP', { fontSize: 12 });
var g = new fabric.Group([lbl],{ top: 0, left: 0});
c.add(g);
c.renderAll();
var resizeGroup = function(g, sz) {
g.set({ height: fabric.util.parseUnit(sz), width: fabric.util.parseUnit(sz)});
var l = g._objects[0];
l.set({ top: -(g.height/2), left: -(g.width/2), text: 'GROUP SIZE '+sz}).setCoords();
g.set({ dirty: true });
g.canvas.renderAll();
}
//resizeGroup(g, '12in'); // looks good
//resizeGroup(g, '24in'); // looks bad
resizeGroup(g, '36in'); // looks really bad

FabricJS: After deselect a group, the controllers appear rotated 180 degrees

After a deselect a group, if I selected a object, the controllers appears upside down (with a 180 degrees rotation).
Steps to reproduse the issue:
Select all objects
Deselect all objects
Select the triangle object (in the example of the code above)
(function() {
// Code from FabricJs tutorial - http://fabricjs.com/fabric-intro-part-1/#objects
var canvas = new fabric.Canvas('canvas');
// create a rectangle object
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 80,
height: 40
});
var triangle = new fabric.Triangle({
left: 300,
top: 100,
width: 40,
height: 40,
fill: 'blue',
flipX: true
});
canvas.add(rect);
canvas.add(triangle);
canvas.renderAll(); // this wasn't mentioned in the tutorial, but I'm pretty sure it's needed
})();
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.19/fabric.min.js"></script>
</head>
<body></body>
<h1>Flip issue after grouping objects</h1>
<h2>None of the objects have rotation. Please select both objects (create a group) and, then, deselect it. After this, select the triangle : triangle will have rotation</h2>
<canvas id="canvas" width="600" height="300"></canvas>
I already open a ticket on the github with this issue.
What I want to know, if there is any workaround that I could do to avoid this issue.
Note:
fabricJS 1.7.15 version.

Let user delete a selected fabric js object

I have a simple fabric js based application where I will let users add shapes connect them and animate them.
Following is my JS
var canvas;
window.newAnimation = function(){
canvas = new fabric.Canvas('canvas');
}
window.addRect = function(){
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 20,
height: 20,
});
canvas.add(rect);
}
window.addCircle = function(){
var circle = new fabric.Circle({
radius: 20, fill: 'green', left: 100, top: 100
});
canvas.add(circle);
}
This is my Fiddle. You can click on new animation and then add objects as of now.
I want the user to select some object and then also be able to delete it I am not sure how. I found this Delete multiple Objects at once on a fabric.js canvas in html5 But i was not able to implement it successfully. I basically want users to be able to select an object and delete it.
Since new version of fabric.js was released - you should use:
canvas.remove(canvas.getActiveObject());
Edit: This is for older versions now.
You can use the remove() method, eg.
window.deleteObject = function() {
canvas.getActiveObject().remove();
}
jsfiddle
Delete all selected objects:
canvas.getActiveObjects().forEach((obj) => {
canvas.remove(obj)
});
canvas.discardActiveObject().renderAll()
I am using Fabric JS 2.3.6.
I wanted to allow the user to select one or more objects on the canvas and delete them by clicking the delete button.
Removed methods from old versions
The following methods are no longer available since the introduction of ActiveSelection:
setActiveGroup(group);
getActiveGroup();
deactivateAll();
discardActiveGroup();
deactivateAllWithDispatch();
Here is my code that works great for me and hopefully you as well.
$('html').keyup(function(e){
if(e.keyCode == 46) {
deleteSelectedObjectsFromCanvas();
}
});
function deleteSelectedObjectsFromCanvas(){
var selection = canvas.getActiveObject();
if (selection.type === 'activeSelection') {
selection.forEachObject(function(element) {
console.log(element);
canvas.remove(element);
});
}
else{
canvas.remove(selection);
}
canvas.discardActiveObject();
canvas.requestRenderAll();
}
It's pretty simple actually.
Just use Fabric's event handling to manage the object selection, and then fire the delete function for whatever object is selected.
I'm using the canvas selection events to cover all the objects of the canvas. The idea is to add a delete button, keeping it hidden unless needed, and then handling its display on canvas selection events.
Deletion is made easy with the help of remove property of the Fabric library, which obviously triggers when the delete button is clicked.
Here is some sample code to demo what I said above.
// Grab the required elements
var canvas = new fabric.Canvas('c'),
delBtn = document.getElementById('delete')
// Hide the delete button until needed
delBtn.style.display = 'none'
// Initialize a rectangle object
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 100,
height: 50
})
// Initialize a circle object
var circle = new fabric.Circle({
left: 250,
top: 100,
radius: 20,
fill: 'purple'
})
// Add objects to the canvas
canvas.add(rect)
canvas.add(circle)
// When a selection is being made
canvas.on({
'selection:created': () => {
delBtn.style.display = 'inline-block'
}
})
// When a selection is cleared
canvas.on({
'selection:cleared': () => {
delBtn.style.display = 'none'
}
})
// Rmove the active object on clicking the delete button
delBtn.addEventListener('click', e => {
canvas.remove(canvas.getActiveObject())
})
body {
background-color: #eee;
color: #333;
}
#c {
background-color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.4.0/fabric.min.js"></script>
<h4>Select an object</h4>
<canvas id="c" width="600" height="200"></canvas>
<input type="button" id="delete" value="Delete selection">
Easy, wasn't it? Cheers!
On further improving #Rahul answer, we can also support deletion of selected object using key events like 'Delete', 'Backscape'.
// Grab the required elements
var canvas = new fabric.Canvas('c'),
delBtn = document.getElementById('delete'),
wrapper= document.getElementById('canvasWrapper')
// Hide the delete button until needed
delBtn.style.display = 'none'
// Initialize a rectangle object
var rect = new fabric.Rect({
left: 100,
top: 100,
fill: 'red',
width: 100,
height: 50
})
// Initialize a circle object
var circle = new fabric.Circle({
left: 250,
top: 100,
radius: 20,
fill: 'purple'
})
// Add objects to the canvas
canvas.add(rect)
canvas.add(circle)
// When a selection is being made
canvas.on({
'selection:created': () => {
delBtn.style.display = 'inline-block'
}
})
// When a selection is cleared
canvas.on({
'selection:cleared': () => {
delBtn.style.display = 'none'
}
})
// Rmove the active object on clicking the delete button
delBtn.addEventListener('click', e => {
canvas.remove(canvas.getActiveObject())
})
//Remove using keyboard events
wrapper.addEventListener('keyup', e => {
if (
e.keyCode == 46 ||
e.key == 'Delete' ||
e.code == 'Delete' ||
e.key == 'Backspace'
) {
if (canvas.getActiveObject()) {
if (canvas.getActiveObject().isEditing) {
return
}
canvas.remove(canvas.getActiveObject())
}
}
})
body {
background-color: #eee;
color: #333;
}
#c {
background-color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.4.0/fabric.min.js"></script>
<h4>Select an object</h4>
<div tabIndex="1000"id="canvasWrapper" #keyup="checkDelete($event)" >
<canvas id="c" width="600" height="200"></canvas>
</div>
<input type="button" id="delete" value="Delete selection">
Delete object in Fabricjs. This works completely fine.
this.canvas.getActiveObjects().forEach((o) => {
this.canvas.remove(o);
});
you can delete active object by using backspace key
$(document).keydown(function(event){
if (event.which == 8) {
if (canvas.getActiveObject()) {
canvas.getActiveObject().remove();
}
}
});

How do I make a <ul> or bullet in fabric.Text()?

I am looking for an example application that takes user input and inserts it into a inside canvas with fabric.js. Is this possible? I haven't been able to find a lists in fabric.js example.
canvas.fillText does not accept HTML markup.
A Canvas is a bitmap, it has nothing to do with HTML markup.
You can control font style as described here.
There are libraries that convert XML markup into canvas.fillText calls, maybe you could adapt one.
I realized a better way to solve this issue was to draw a circle at the same height as the text, at radius 2, to emulate a bullet point. for anybody interested its easy as:
var EDU1 = new fabric.IText("SOME TEXT GOES HERE", {fontSize: 20, fontStyle:
'italic',fontFamily: 'Hoefler Text', left: 149, top: 390});
var bullet = new fabric.Circle({
radius: 2, fill: 'black', left: 135, top: 400
});
then group them together and you have a bullet point.
function ListStyle (textObject,type,canvas) {
var styles=['\u25CF','\u25C8','\u25D8','\u25BA','\u25CB','\u25A0','-'];
var allStyles={'bullet':'\u25CF','diamond':'\u25C8','invertedBullet':'\u25D8','triangularBullet':'\u25BA','disced':'\u25CB','squared':'\u25A0','dashed':'-','none':''};
var text = textObject.text;
var textArray = text.split('\n')
var tempStr = [];
textArray.forEach((text, i) => {
if(styles.includes(text.substr(0,1))){
tempStr.push(text.replace(text.substr(0,1),allStyles[type]));
}else{
tempStr.push(allStyles[type]+''+text);
}
})
textObject['text'] = tempStr.join('\n');
canvas.renderAll();
}
ListStyle (canvas.getObjects()[0],'diamond',canvas);

Categories

Resources