Event handler for object selection on fabricjs canvas - javascript

I have a canvas that has some elements and i would like to add an event handler when the user selects an element(clicks on an element) on the canvas.
I tried using selection: created and object: selected events to handle the same but It only works when a user selects an element and then clicks on somewhere else in the canvas and then tries to select another element it works perfectly, But When the user clicks on one element(event handler triggered) and then clicks on another element the event handler is not triggering. How do I handle this?
i see that there is an option to use as mentioned here in the issue with selection:created and
selection:updated https://github.com/fabricjs/fabric.js/issues/4886
But is there a way i could use them together ?
const canvas = new fabric.Canvas('paper', {
preserveObjectStacking: true,
enableRetinaScaling: false,
imageSmoothingEnabled: false
});
canvas.setHeight(500);
canvas.setWidth(700);
const red = new fabric.Rect({
id: 'red',
left: 10,
top: 10,
width: 100,
height: 100,
fill: 'red'
});
const green = new fabric.Rect({
id: 'green',
left: 150,
top: 150,
width: 100,
height: 100,
fill: 'green'
});
canvas.add(red);
canvas.add(green);
canvas.renderAll();
canvas.on("selection:created", function(obj){
alert(obj.target.id)
});
canvas.on("selection:updated", function(obj){
alert(obj.target.id)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.2.3/fabric.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<canvas id="paper" width="400" height="400" style="border:1px solid #ccc"></canvas>

Here is a pretty simple solution for having both the event handlers combined and linking both of them to the same method will be a simple solution for this, Any better solutions will be appreciated.
canvas.on({
'selection:updated': HandleElement,
'selection:created': HandleElement
});
function HandleElement(obj){
//Handle the object here
}

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.

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.

fabric.js: Always visible controls

I am using fabric.js for image manipulation and it works great, but I need the controls to be always visible even when I click outside the object/image.
By default they are visible only when you click the objec/image, if you click outside of it the controls disappear.
Is it possibile to do so?
Thank you.
Unfortunately, there is no built-in method in FabricJS, to achieve this at the moment.
However, here is a workaround (function) , which will simulate this functionality ...
function showControls(...objs) {
objs.forEach(obj => {
obj.set('active', true);
canvas.renderAll();
canvas.on('mouse:down', function(e) {
obj.set('active', true);
});
})
}
after adding the image object on the canvas, call the above function along with passing the image object as a parameter, that you wish to show controls for.
ᴅᴇᴍᴏ
var canvas = new fabric.Canvas('c');
// add rectangle (for demo purposes only)
var rect = new fabric.Rect({
top: 100,
left: 290,
width: 100,
height: 100,
fill: '#07C',
originX: 'center',
originY: 'center',
transparentCorners: false
});
canvas.add(rect);
// add image (for demo purposes only)
fabric.Image.fromURL('https://i.imgur.com/Q6aZlme.jpg', function(img) {
img.set({
top: 100,
left: 110,
width: 100,
height: 100,
originX: 'center',
originY: 'center',
transparentCorners: false
})
canvas.add(img);
showControls(img); // pass an object that you wish to show controls for
});
// always show controls (multi-object support)
function showControls(...objs) {
objs.forEach(obj => {
obj.set('active', true);
canvas.renderAll();
canvas.on('mouse:down', function(e) {
obj.set('active', true);
});
})
}
canvas{border:1px solid #ccc}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.11/fabric.min.js"></script>
<canvas id="c" width="400" height="200"></canvas>
not perfect, but a start
// fabricjs object - always show controls
//fabric.Object.prototype.render = (function (render) {
fabric.Image.prototype.render = (function (render) {
return function (ctx) {
render.apply(this, arguments)
// show controls
// but most controls are not usable
// until you activate [click] the object
this._renderControls(ctx)
// activate this object
// to make all controls usable
// only one active object per canvas
// when another object is active
// the controls become not usable
if (!this.canvas._activeObject)
this.canvas._activeObject = this
}
//})(fabric.Object.prototype.render)
})(fabric.Image.prototype.render)
// fabricjs object - activate on mouseover
// this is just a quick hack
// to make controls always usable
//fabric.Object.prototype.initialize = (function (initialize) {
fabric.Image.prototype.initialize = (function (initialize) {
return function () {
initialize.apply(this, arguments)
this.on('mouseover',
function(event) {
this.canvas.setActiveObject(this)
this.canvas.renderAll() // TODO cheaper?
})
}
//})(fabric.Object.prototype.initialize)
})(fabric.Image.prototype.initialize)

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();
}
}
});

Canvas draggable with kineticJS and Clip function

I've a problem which deals with canvas.
I would like to use kinetic to use mobile events (most particularly for draggable) and I would also like to use the clip() function at the same time.
Here is my code :
<!DOCTYPE HTML>
<html>
<head>
<style>
canvas {
border: 1px solid #9C9898;
}
</style>
<script src="kinetic-v3.9.4.js"></script>
<script>
window.onload = function() {
var stage = new Kinetic.Stage({
container: "container",
width: 708,
height: 500
});
var layer = new Kinetic.Layer();
var circle1 = new Kinetic.Circle({
x: 150,
y: 150,
radius: 75,
fill: "red",
draggable: true
});
var circle2 = new Kinetic.Circle({
x: 350,
y: 150,
radius: 75,
fill: "blue",
});
layer.add(circle2);
layer.add(circle1);
stage.add(layer);
};
</script>
</head>
<body onmousedown="return false;">
<div id="container"></div>
</body>
</html>
I've the first circle draggable but I would like to use it and the clip function but I don't really know how to do it. In addition, I tried to get the 2d context like this:
var context = this.getContext();
But it didn't work. If there's someone that can help me thanks.
I also saw this example but it doesn't work with mobile devices.
The reason why the example you mentioned doesn't work on mobile is because its only using mouse event handlers:
stage.on("mousemove", function(){})
But you'll need to add touch events like this:
stage.on("mousemove touchmove", function(){})
touchstart, touchmove, touchend
Good luck!

Categories

Resources