Control z-index in Fabric.js - z-index

In fabricjs, I want to create a scene in which the object under the mouse rises to the top of the scene in z-index, then once the mouse leaves that object, it goes back to the z-index where it came from. One cannot set object.zindex (which would be nice). Instead, I'm using a placeholder object which is put into the object list at the old position, and then the old object is put back in the position where it was in the list using canvas.insertAt. However this is not working.
See http://jsfiddle.net/rFSEV/ for the status of this.
var canvasS = new fabric.Canvas('canvasS', { renderOnAddition: false, hoverCursor: 'pointer', selection: false });
var bars = {}; //storage for bars (bar number indexed by group object)
var selectedBar = null; //selected bar (group object)
var placeholder = new fabric.Text("XXXXX", { fontSize: 12 });
//pass null or a bar
function selectBar(bar) {
if (selectedBar) {
//remove the old topmost bar and put it back in the right zindex
//PROBLEM: It doesn't go back; it stays at the same zindex
selectedBar.remove();
canvasS.insertAt(selectedBar, selectedBar.XZIndex, true);
selectedBar = null;
}
if (bar) {
//put a placeholder object ("XXX" for now) in the position
//where the bar was, and put the bar in the top position
//so it shows topmost
selectedBar = bar;
canvasS.insertAt(placeholder, selectedBar.XZIndex, true);
canvasS.add(bar);
canvasS.renderAll();
}
}
canvasS.on({
'mouse:move': function(e) {
//hook up dynamic zorder
if (!e.target) return;
if (bars[e.target])
selectBar(e.target);
else
selectBar(null);
},
});
var objcount = canvasS.getObjects().length;
//create bars
for (var i = 0; i < 20; ++i) {
var rect = new fabric.Rect({
left: 0,
top: 0,
rx: 3,
ry: 3,
stroke: 'red',
width: 200,
height: 25
});
rect.setGradientFill({
x1: 0,
y1: 0,
x2: 0,
y2: rect.height,
colorStops: {
0: '#080',
1: '#fff'
}
});
var text = new fabric.Text("Bar number " + (i+1), {
fontSize: 12
});
var group = new fabric.Group([ rect, text ], {
left: i + 101,
top: i * 4 + 26
});
group.hasControls = group.hasBorders = false;
//our properties (not part of fabric)
group.XBar = rect;
group.XZIndex = objcount++;
canvasS.add(group);
bars[group] = i;
}
canvasS.renderAll();

Since fabric.js version 1.1.4 a new method for zIndex manipulation is available:
canvas.moveTo(object, index);
object.moveTo(index);
I think this is helpful for your use case. I've updated your jsfiddle - i hope this is what you want:
jsfiddle

Also make sure you change z-index AFTER adding object to canvas.
So code will looks like:
canvas.add(object);
canvas.moveTo(object, index);
Otherwise fabricjs don`t care about z-indexes you setup.

After I added a line object, I was make the line appear under the object using:
canvas.add(line);
canvas.sendToBack(line);
Other options are
canvas.sendBackwards
canvas.sendToBack
canvas.bringForward
canvas.bringToFront
see: https://github.com/fabricjs/fabric.js/issues/135

You can modify your _chooseObjectsToRender method to have the following change at the end of it, and you'll be able to achieve css-style zIndexing.
objsToRender = objsToRender.sort(function(a, b) {
var sortValue = 0, az = a.zIndex || 0, bz = b.zIndex || 0;
if (az < bz) {
sortValue = -1;
}
else if (az > bz) {
sortValue = 1;
}
return sortValue;
});
https://github.com/fabricjs/fabric.js/pull/5088/files

You can use these two functions to get z-index of a fabric object and modify an object's z-index, since there is not specific method to modify z-index by object index :
fabric.Object.prototype.getZIndex = function() {
return this.canvas.getObjects().indexOf(this);
}
fabric.Canvas.prototype.moveToLayer = function(object,position) {
while(object.getZIndex() > position) {
this.sendBackwards(object);
}
}

Related

Fabricjs - selection only via border

I'm using Fabric.js to draw some rectangles on a canvas. The default behavior is that clicking inside a rectangle selects it. How can I change the behavior such that it is only selected when clicking on the border of the rectangle?
Clicking inside the rectangle but not on the border should do nothing.
You can see this behavior by drawing a rectangle on a TradingView.com chart
It there an option for this in fabric, and if not how could I go around implementing it?
This approach overrides the _checkTarget method within FabricJS to reject clicks that are more than a specified distance from the border (defined by the clickableMargin variable).
//sets the width of clickable area
var clickableMargin = 15;
var canvas = new fabric.Canvas("canvas");
canvas.add(new fabric.Rect({
width: 150,
height: 150,
left: 25,
top: 25,
fill: 'green',
strokeWidth: 0
}));
//overrides the _checkTarget method to add check if point is close to the border
fabric.Canvas.prototype._checkTarget = function(pointer, obj, globalPointer) {
if (obj &&
obj.visible &&
obj.evented &&
this.containsPoint(null, obj, pointer)){
if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) {
var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y);
if (!isTransparent) {
return true;
}
}
else {
var isInsideBorder = this.isInsideBorder(obj);
if(!isInsideBorder) {
return true;
}
}
}
}
fabric.Canvas.prototype.isInsideBorder = function(target) {
var pointerCoords = target.getLocalPointer();
if(pointerCoords.x > clickableMargin &&
pointerCoords.x < target.getScaledWidth() - clickableMargin &&
pointerCoords.y > clickableMargin &&
pointerCoords.y < target.getScaledHeight() - clickableMargin) {
return true;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<canvas id="canvas" height="300" width="400"></canvas>
Fabric.js uses Object.containsPoint() to determine whether a mouse event should target the object. This method, in turn, calculates the object's edges via Object._getImageLines() and checks how many times the projection of a mouse pointer crossed those lines.
The solution below calculates additional inner edges based on the coordinates of each corner, therefore object scale and rotation are taken care of automatically.
const canvas = new fabric.Canvas('c', {
enableRetinaScaling: true
})
const rect = new fabric.Rect({
left: 0,
top: 0,
width: 100,
height: 100,
dragBorderWidth: 15, // this is the custom attribute we've introduced
})
function innerCornerPoint(start, end, offset) {
// vector length
const l = start.distanceFrom(end)
// unit vector
const uv = new fabric.Point((end.x - start.x) / l, (end.y - start.y) / l)
// point on the vector at a given offset but no further than side length
const p = start.add(uv.multiply(Math.min(offset, l)))
// rotate point
return fabric.util.rotatePoint(p, start, fabric.util.degreesToRadians(45))
}
rect._getInnerBorderLines = function(c) {
// the actual offset from outer corner is the length of a hypotenuse of a right triangle with border widths as 2 sides
const offset = Math.sqrt(2 * (this.dragBorderWidth ** 2))
// find 4 inner corners as offsets rotated 45 degrees CW
const newCoords = {
tl: innerCornerPoint(c.tl, c.tr, offset),
tr: innerCornerPoint(c.tr, c.br, offset),
br: innerCornerPoint(c.br, c.bl, offset),
bl: innerCornerPoint(c.bl, c.tl, offset),
}
return this._getImageLines(newCoords)
}
rect.containsPoint = function(point, lines, absolute, calculate) {
const coords = calculate ? this.calcCoords(absolute) : absolute ? this.aCoords : this.oCoords
lines = lines || this._getImageLines(coords)
const innerRectPoints = this._findCrossPoints(point, lines);
const innerBorderPoints = this._findCrossPoints(point, this._getInnerBorderLines(coords))
// calculate intersections
return innerRectPoints === 1 && innerBorderPoints !== 1
}
canvas.add(rect)
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<canvas id="c" width="400" height="300"></canvas>
here is my approach, when rect is clicked I am calculating where it is clicked and
if it is not clicked on border I have to set canvas.discardActiveObject , see comments on code
var canvas = new fabric.Canvas('c', {
selection: false
});
var rect = new fabric.Rect({
left: 50,
top: 50,
width: 100,
height: 100,
strokeWidth: 10,
stroke: 'red',
selectable: false,
evented: true,
hasBorders: true,
lockMovementY: true,
lockMovementX: true
})
canvas.on("mouse:move", function(e) {
if (!e.target || e.target.type != 'rect') return;
// when selected event is fired get the click position.
var pointer = canvas.getPointer(e.e);
// calculate the click distance from object to be exact
var distanceX = pointer.x - rect.left;
var distanceY = pointer.y - rect.top;
// check if click distanceX/Y are less than 10 (strokeWidth) or greater than 90 ( rect width = 100)
if ((distanceX <= rect.strokeWidth || distanceX >= (rect.width - rect.strokeWidth)) || (distanceY <= rect.strokeWidth || distanceY >= (rect.height - rect.strokeWidth))) {
rect.set({
hoverCursor: 'move',
selectable: true,
lockMovementY: false,
lockMovementX: false
});
document.getElementById('result').innerHTML = 'on border';
} else {
canvas.discardActiveObject();
document.getElementById('result').innerHTML = 'not on border';
rect.set({
hoverCursor: 'default',
selectable: false,
lockMovementY: true,
lockMovementX: true
});
}
});
canvas.add(rect);
canvas.renderAll();
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.min.js"></script>
<div id="result" style="width: 100%; "></div>
<canvas id="c" width="600" height="200"></canvas>
<pre>
</pre>
ps: you can also set the rect property to selectable: false and call canvas.setActiveObject(this); to make it selection inside if statement.

FabricJS 2- Setting fill on SVG paths when only single path

So adding an SVG to the canvas as follows:
// load the svg
fabric.loadSVGFromURL(self.currentDraggedIcon, function(objects, d) {
var iconGroup = fabric.util.groupSVGElements(objects, d);
iconGroup.set({
left: e.layerX,
top: e.layerY,
width: d.width,
height: d.height,
lockUniScaling: true,
// scaleY:self.currentObjectDesigner.scaleFactor,
// scaleX:self.currentObjectDesigner.scaleFactor,
dtype: 'UserIcon'
});
self.currentObjectDesigner.fabric.add(iconGroup);
self.currentObjectDesigner.fabric.bringToFront(iconGroup);
self.currentObjectDesigner.fabric.renderAll();
});
Later on there is a button to say change the paths colour to red inside the group, the code to do this is:
for (var i = 0; i < self.currentObjectDesigner.selectedObject._objects.length; i++) {
if (self.currentObjectDesigner.selectedObject.item(i).fill == findColor || self.currentObjectDesigner.selectedObject.item(i).fill == findColorAlt) {
self.currentObjectDesigner.selectedObject.item(i).fill = color;
}
}
self.currentObjectDesigner.selectedObject.addWithUpdate();
This works perfectly fine if the SVG has multiple paths, but when only a single path exists the _objects property doesn't exist so we are unable to perform the loop and item(i) part to set the fill on the path.
Question is: How do we now set a fill when _objects doesn't exist and the item() method doesn't exist because it's just a single path? - I.e it's not a group.
If more than one path present groupSVGElements returns group objects else returns a single object.
DEMO
var canvas = new fabric.Canvas('c');
function loadSvg(url, left, top) {
fabric.loadSVGFromURL(url, function(objects, d) {
var iconGroup = fabric.util.groupSVGElements(objects, d);
//for more than one path
iconGroup.set({
left: left,
top: top
})
if (iconGroup.type == 'group') {
//do your logic for group object
iconGroup.item(0).fill = 'yellow';
iconGroup.addWithUpdate();
} else {
iconGroup.fill = 'red';
}
iconGroup.set({
scaleX: 150 / iconGroup.width,
scaleY: 150 / iconGroup.height,
})
canvas.add(iconGroup);
}, function() {}, {
crossOrigin: 'anonymous'
});
}
loadSvg('https://upload.wikimedia.org/wikipedia/commons/2/22/Wikimapia_logotype.svg', 10, 20);
loadSvg('https://upload.wikimedia.org/wikipedia/commons/a/a0/Circle_-_black_simple.svg', 200, 50);
canvas{
border:2px solid #000;
}
<script src="https://rawgit.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id='c' width=400 height=400></canvas>

how to remove grid lines with a button click in fabric.js canvas

I have to two buttons, one to add grids and another to remove grids.
When I am clicking add button, I am writing following lines of code:
var gridsize = 5;
var gridoption={ stroke: "#000000", strokeWidth: 1, selectable:false, strokeDashArray: [5, 5],evented:false};
for(var x=1;x<(canvas.width/gridsize);x++)
{
canvas.add(new fabric.Line([100*x, 0, 100*x, 600],gridoption));
canvas.add(new fabric.Line([0, 100*x, 600, 100*x],gridoption));
}
With these codes, the grids appear with a click of button,but I also want to remove the grids once another button is clicked without making any change to the other objects in the canvas. Since I am not selecting any object here. remove() is also not working. Please help.
Create a group of grids then add to canvas, while removing remove the group object;
DEMO
var canvas = new fabric.Canvas('c', {
width: 600,
height: 600
})
var gridGroup;
function addGrid() {
if (gridGroup) return;
var gridsize = 5;
var gridoption = {
stroke: "#000000",
strokeWidth: 1,
strokeDashArray: [5, 5]
};
var gridLines = [];
for (var x = 1; x < (canvas.width); x += 100) {
gridLines.push(new fabric.Line([x, 0, x, canvas.width], gridoption));
}
for (var x = 1; x < (canvas.height); x += 100) {
gridLines.push(new fabric.Line([0, x, canvas.height, x], gridoption));
}
gridGroup = new fabric.Group(gridLines, {
selectable: false,
evented: false
})
gridGroup.addWithUpdate();
canvas.add(gridGroup);
}
function removeGrid() {
gridGroup && canvas.remove(gridGroup);
gridGroup = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.22/fabric.js"></script>
<button onclick='addGrid()'>Add</button>
<button onclick='removeGrid()'>Remove</button>
<canvas id="c" style="border:1px solid black"></canvas>

javascript fabricjs - Adding an object to multiple canvases in a single click

I'm building an app that can design your own business card. I have to add an object to two canvases in a single click. Here are my codes:
$('#image-list').on('click','.image-option',function(e) {
var el = e.target;
/*temp code*/
var offset = 50;
var left = fabric.util.getRandomInt(0 + offset, 200 - offset);
var top = fabric.util.getRandomInt(0 + offset, 400 - offset);
var angle = fabric.util.getRandomInt(-20, 40);
var width = fabric.util.getRandomInt(30, 50);
var opacity = (function(min, max){ return Math.random() * (max - min) + min; })(0.5, 1);
var canvasObject;
// if ($('#flip').attr('data-facing') === 'front') {
// canvasObject = canvas;
// } else {
// canvasObject = canvas2;
// }
fabric.Image.fromURL(el.src, function(image) {
image.set({
left: left,
top: top,
angle: 0,
padding: 10,
cornersize: 10,
hasRotatingPoint:true
});
canvas.add(image);
canvas2.add(image);
});
})
The problem is, when I resized or move the image on the 'front canvas', it also renders the same way in the 'back canvas'. In my case, I don't want the object to be that way. So is there a way to prevent the obect 'mirroring' on the other canvas? Thanks.
You cannot add the same object to 2 canvases.
You have to create 2 objects.
Also take note that if you have an html image element on your page you do not need to load it from URL again. Is already loaded, so pass the image element to the constructor directly
$('#image-list').on('click','.image-option',function(e) {
var el = e.target;
/*temp code*/
var offset = 50;
var left = fabric.util.getRandomInt(0 + offset, 200 - offset);
var top = fabric.util.getRandomInt(0 + offset, 400 - offset);
var angle = fabric.util.getRandomInt(-20, 40);
var width = fabric.util.getRandomInt(30, 50);
var opacity = (function(min, max){ return Math.random() * (max - min) + min; })(0.5, 1);
var canvasObject;
// if ($('#flip').attr('data-facing') === 'front') {
// canvasObject = canvas;
// } else {
// canvasObject = canvas2;
// }
image = new fabric.Image(el, {
left: left,
top: top,
angle: 0,
padding: 10,
cornersize: 10,
hasRotatingPoint:true
});
image2 = new fabric.Image(el, {
left: left,
top: top,
angle: 0,
padding: 10,
cornersize: 10,
hasRotatingPoint:true
});
canvas.add(image);
canvas2.add(image2);
});
})

Phaser: remove a circle previously drawn with drawCircle

In Phaser (2.4.x), I'm drawing a circle around a sprite when it is dragged:
function dragStart(sprite, pointer, dragX, dragY) {
var graphics = game.add.graphics(0, 0);
graphics.lineStyle(6, 0x909090, 0.3);
graphics.drawCircle(dragX, dragY, 200);
}
That works fine, but now I need to remove the circle when the drag ends, and I can't figure that part out:
function dragStop() {
// ?
}
Is it possible to remove graphics? Is there a better or simpler option to draw a circle and remove it later?
You could kill() the object
But be carefull with the scope of the var you want kill (you are defining it inside the function).
Or you could just create the graphic and then show or hide depending of your event (drag)
I leave you a very simple example with both solutions:
var game = new Phaser.Game(500, 500, Phaser.AUTO, 'game');
var mainState = {
create:function(){
var graphics = game.add.graphics(0, 0);
graphics.lineStyle(6, 0x909090, 0.3);
graphics.drawCircle(game.world.centerX+100,game.world.centerY+100, 200);
console.log(graphics);
setTimeout(function(){
graphics.kill();
},2000);
this.graphics2 = game.add.graphics(0, 0);
this.graphics2.lineStyle(6, 0xff0000, 1);
this.graphics2.drawCircle(game.world.centerX-100,game.world.centerY-100, 200);
this.graphics2.visible = false;
this.show_later = game.time.now + 2000;
this.hide_again = game.time.now + 4000;
},
update:function(){
if(this.show_later < game.time.now){
this.graphics2.visible = false;
}
if(this.hide_again < game.time.now){
this.graphics2.visible = true;
}
},
};
game.state.add('main', mainState);
game.state.start('main');
<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/2.4.4/phaser.min.js"></script>
<div id="game"></div>

Resources