fabricjs2 selection error after programmatically resize - fabricjs

I'm using fabrics v2.4.3 in an angular 6 project.
In my project I want to move and resize some objects (a fabric.Group) both by mouse and by properties editing through a form.
The problem is on the second method.
I put an object into the canvas, and I selected it by mouse. Now I'm subscribed to the form valueChanges to apply in real time the new props of the selected object.
this.subscription = this.form.valueChanges.subscribe((value)=>{
this.panelView.setConfig(value);
})
This is the setConfig method:
setConfig(config:PanelParameters){
this.config = config;
let param = {
top: this.config.y,
left: this.config.x,
width: this.view.width, //this.config.width,
height: this.view.height,
scaleX: this.config.width/this.view.width,
scaleY: this.config.height/this.view.height,
fill : this.config.background_color
}
this.view.set(param)
for(let obj of this.view.getObjects()){
if( obj instanceof fabric.Rect){
obj.set({
fill: this.config.background_color
})
}else if( obj instanceof fabric.Text ){
obj.set({
fill: this.config.title_text_color
})
}
}
this.canvas.requestRenderAll();
}
Now into the canvas the object are rendered correctly but if I try to select it
I have to click onto the old object area.
What I'm doing wrong?

You're missing a call to this.view.setCoords().
In fabric.js, mouse interactions are evaluated against an object's oCoords. When you programmatically set object's properties that should result in a change of coordinates, you have to explicitly call setCoords() to recalculate them. See When to call setCoords.

Related

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

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

How can i set a custom cursor for fabric object?

This is not Drawing Mode.
I want according to some condition to be able to change the cursor when I am over some element. Something like
$('#canvasID').css('cursor','pointer');
but this is not working for me. Do you know some property from their library?
After some tests this is working for me:
canvas.observe('mouse:over', function (e) {
if (e.target.get('type') == 'line') {
e.target.hoverCursor = 'crosshair';
}
});
FabricJS have an in-built mechanism for setting custom cursor (unfortunatelly only for hover and move events):
var canvas = new fabric.Canvas('myCanvas');
var circle = new fabric.Circle({
radius: 20, fill: 'red', left: 10, top: 10
});
circle.hoverCursor = 'no-drop';
canvas.add(circle);
More cursor types you can find here.

Is there a way to crop only the design from a the canvas and ignoring all the white/transparent space?

I have a canvas built using fabricJS with the dimension of 600x500. I have added an image to this canvas which is of size 200x300 and also a text element just below it.
$canvasObj.toDataURL();
exports the whole canvas area including the white spaces surrounding the design on the canvas.
Is there a way to get the cropped output of the design on the canvas alone instead of all the whitespace?
This can be done by cloning objects to a group, getting the group boundingRect, and then passing the boundingRect parameters to toDataUrl() function (see fiddle).
e.g.
// make a new group
var myGroup = new fabric.Group();
canvas.add(myGroup);
// ensure originX/Y 'center' is being used, as text uses left/top by default.
myGroup.set({ originX: 'center', originY: 'center' });
// put canvas things in new group
var i = canvas.getObjects().length;
while (i--) {
var objType = canvas.item(i).get('type');
if (objType==="image" || objType==="text" || objType==="itext" || objType==="rect") {
var clone = fabric.util.object.clone(canvas.item(i));
myGroup.addWithUpdate(clone).setCoords();
// remove original lone object
canvas.remove(canvas.item(i));
}
}
canvas.renderAll();
// get bounding rect for new group
var i = canvas.getObjects().length;
while (i--) {
var objType = canvas.item(i).get('type');
if (objType==="group") {
var br = canvas.item(i).getBoundingRect();
}
}
fabric.log('cropped png dataURL: ', canvas.toDataURL({
format: 'png',
left: br.left,
top: br.top,
width: br.width,
height: br.height
}));
p.s. I should probably mention that i've not worked with image types, so i just guessed that it's called 'image'..

Passing data between views in Durandal

I have a view with a grid from where I launch a different view in a new window. The parent view needs to pass data to the child view and it cannot be passed as a query string since it is pretty large. I cannot use a native modal. I am just using window.open and have not created a custom modal context.Things that I have tried
1. Tried populating hidden field elements in the new window using something like the code below
var popup = window.open('/#/viewname');
popup.onload=function(){document.findbyid('hiddenfield')= newvalue };
I am using a splash page to display loading in durandal , onload of the new window the router is still navigating and I get the splash page document
2. Tried requiring the parent view in the child view and then grabbing the parent view data using something like the code below
define(['viewmodels/parent'],function ('parent'){
function activate(){
localvariablevalue= parent.observable;
}
return{
activate:activate
}
});
I get parent.observable as undefined. More than likely it is out of context as this is a new window.
3.Tried a singleton global.js file and requiring it in both parent and child but I think that is also losing context as I get undefined even with that approach
Any ideas on how to achieve this.. if it can be achieved using v 1.2 ? If it can be achieved by creating a custom Modal context then can someone give some pointers on how to define the addContext function for a new window .
By using window.open an independent instance of a #/viewname SPA gets created that has no relation to the first SPA. If you really have to open a new window than you'd need to look into other ways to exchange information between windows like e.g. localstorage.
But you're probably better off rethinking the requirements and see if you can't achieve the goal with modals instead.
Edit based on comment: That has nothing to do with Durandal loosing context, it's about window.open not opening in the same session. see e.g. window.open doesn't open in same session
Edit2 As an alternative you might use the param #/viewname in the second windows vm.activate to retrieve the data either from the server directly (if applicable) or via webstorage e.g. localstorage or sessionstorage from the first browser.
Edit3 I did a quick test do see if that can be accomplished using window.open alone. Tested in Firefox/Firebug. I would still recommend to checkout the links to webstorage to ensure that this is working cross browser.
Go to http://dfiddle.github.io/dFiddle-1.2/#/, open up a console and create a global var:
window.myVar = {complex: true};
create a new window
var myWindow = window.open('http://dfiddle.github.io/dFiddle-1.2/#/view-composition');
go back first console and create a property myvar on myWindow
myWindow.myVar = myVar;
change to the console of the second window and check
myVar // output {complex: true}
The alternative that I was suggesting uses the unique hash value that you pass in to the second window and then either retrieves the complex data (that can be added as query string) as part of the activate callback. The data could be stored on the server or via webstorage.
Rainer thanks for trying to help out. The following code will generate maximised modals. At this stage I am resigning to this solution and passing my data as activation in showModal. It is an exact copy of the default modal context with minor css variations.But posting it if it helps someone out.
1. CSS
.modalHostMax {
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
position: fixed;
opacity: 0;
-webkit-backface-visibility: hidden;
-webkit-transition: opacity 0.1s linear;
-moz-transition: opacity 0.1s linear;
-o-transition: opacity 0.1s linear;
transition: opacity 0.1s linear;
}
.modalMax {
width: 100%;
height: 100%;
}
2.New modal context
var addContext = function (contextName) {
modalDialog.addContext(contextName, {
blockoutOpacity: .2,
removeDelay: 200,
addHost: function (modal) {
var body = $('body');
var blockout = $('<div class="modalBlockout"></div>')
.css({ 'z-index': modalDialog.getNextZIndex(), 'opacity': this.blockoutOpacity })
.appendTo(body);
var host = $('<div class="modalHostMax"></div>')
.css({ 'z-index': modalDialog.getNextZIndex() })
.appendTo(body);
modal.host = host.get(0);
modal.blockout = blockout.get(0);
if (!modalDialog.isModalOpen()) {
modal.oldBodyMarginRight = $("body").css("margin-right");
var html = $("html");
var oldBodyOuterWidth = body.outerWidth(true);
var oldScrollTop = html.scrollTop();
$("html").css("overflow-y", "hidden");
var newBodyOuterWidth = $("body").outerWidth(true);
body.css("margin-right", (newBodyOuterWidth - oldBodyOuterWidth + parseInt(modal.oldBodyMarginRight)) + "px");
html.scrollTop(oldScrollTop); // necessary for Firefox
$("#simplemodal-overlay").css("width", newBodyOuterWidth + "px");
}
},
removeHost: function (modal) {
$(modal.host).css('opacity', 0);
$(modal.blockout).css('opacity', 0);
setTimeout(function () {
$(modal.host).remove();
$(modal.blockout).remove();
}, this.removeDelay);
if (!modalDialog.isModalOpen()) {
var html = $("html");
var oldScrollTop = html.scrollTop(); // necessary for Firefox.
html.css("overflow-y", "").scrollTop(oldScrollTop);
$("body").css("margin-right", modal.oldBodyMarginRight);
}
},
afterCompose: function (parent, newChild, settings) {
var $child = $(newChild);
var width = $child.width();
var height = $child.height();
$child.attr('class', 'modalMax');
$(settings.model.modal.host).css('opacity', 1);
if ($(newChild).hasClass('autoclose')) {
$(settings.model.modal.blockout).click(function () {
settings.model.modal.close();
});
}
$('.autofocus', newChild).each(function () {
$(this).focus();
});
}
});
};
3. In your target view make sure that the container min-height is set to $(document).height()
4. To use just set custom context by calling addContext('yourcontextname').Then create your modal - app.showModal('viewname',{data},'yourcontextname')

Resources