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

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>

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.

Fabric Js Image blend Filter and change Image src issues

When I'm trying to change image src in FabricJS works fine but after add filter (blend image) to that image change src not working :
Change image src code :
function replaceImage(imgLink) {
fabric.Image.fromURL(imgLink, function(img) {
var object = canvas.getActiveObject();
object._element.src = imgLink;
canvas.renderAll();
});
}
add filter code :
filter2 = new fabric.Image.filters.BlendImage({
image: mainObject,
mode: 'mask',
alpha: 0.5
});
let object = canvas.getActiveObject();
object.filters.push(filter2);
object.applyFilters();
canvas.renderAll();
Use imageObj#setSrc to change the source of image object. and options available for BlendImage modes are multiply, add, diff, screen, subtract, darken, lighten, overlay, exclusion, tint one of them.
DEMO
var canvas = new fabric.Canvas('editorCanvas', {
backgroundColor: 'white',
width: 300,
height: 300
});
var imageObject;
fabric.Image.fromURL('https://picsum.photos/200/200', function(img) {
img.set({width:200,height:200,scaleX:1,scaleY:1})
imageObject = img;
canvas.add(img);
},{crossOrigin:'annonymous'});
var i=0;
function addFilter(){
filter2 = new fabric.Image.filters.BlendImage({
image: imageObject,
mode: 'multiply',
alpha: 0.5
});
imageObject.filters.push(filter2);
imageObject.applyFilters();
canvas.requestRenderAll();
}
function changeSrc(){
var url = 'https://picsum.photos/200/200?random='+ ++i;
imageObject.setSrc(url,function(){
canvas.requestRenderAll();
},{crossOrigin:'annonymous'})
}
canvas{
border:1px solid;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.7.0/fabric.js"></script>
<button onclick='changeSrc()'>changeSrc</button>
<button onclick='addFilter()'>addFilter</button><br>
<canvas id="editorCanvas"></canvas>

Proper way to approach loadable masks in Fabric.js

I have this bounty open Fabricjs mask object with transformation when trying to mask objects with Fabric.js.
The tool I'm developing should allow users to draw a mask over image objects, and apply transformations (skew scale rotate etc) to this object before or after the mask. I'm close to obtaining this result but objects with an angle are still not working.
I'm also trying to save this object to a database using toJSON and loadFromJSON, but after a few days trying to accomplish this I realize that this solution will not work because any references outside the ctx scope can't be accessed while loading from JSON, so they throw an error.
clipTo: function(ctx) {
mask.set({
left:
-object.width / 2 -
(mask.width / 2) * originalMaskScaleX -
originalObjLeft / originalObjScaleX,
top:
-object.height / 2 -
(mask.height / 2) * originalMaskScaleY -
originalObjTop / originalObjScaleY,
objectCaching: false
});
mask.render(ctx);
}
Is Fabric.js the proper solution to this problem? Should I be using something else? If this can be done with Fabric.js, what is the proper approach?
I extended fabric.Image with some custom attributes.
Also I attached the mask on fabric.Image.
For fabric.Image.fromObject after the image is loaded I need it to load also the mask( which I know is a path) and attach to image.
This is a fast implementation. I'm pretty sure this code can be simplified.
Please tell me know if something is not clear enougth
canvas = new fabric.Canvas("canvas", {
backgroundColor: "lightgray",
width: 1280,
height: 720,
preserveObjectStacking: true,
selection: false,
stateful: true
});
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.color = "black";
canvas.freeDrawingBrush.width = 2;
canvas.on("path:created", function(options) {
clip(options.path);
});
function clip(path) {
canvas.isDrawingMode = false;
canvas.remove(path);
let mask = new fabric.Path(path.path, {
top: object.top,
left: object.left,
objectCaching: false,
strokeWidth: 0,
scaleX: 1 / object.scaleX,
scaleY: 1 / object.scaleY,
pathOffset: {
x: 0,
y: 0
}
});
object = canvas.getObjects()[0];
object.originalObjLeft = object.left,
object.originalObjTop = object.top,
object.originalMaskScaleX = mask.scaleX,
object.originalMaskScaleY = mask.scaleY,
object.originalObjScaleX = object.scaleX,
object.originalObjScaleY = object.scaleY;
var transformedTranslate = object.translateToGivenOrigin({
x: object.left,
y: object.top
}, object.originX, object.originY, 'center', 'center');
object.originalTransformLeft = transformedTranslate.x - object.getCenterPoint().x;
object.originalTransformTop = transformedTranslate.y - object.getCenterPoint().y;
object.originalAngle = object.angle;
object.clipMask = mask;
object.set({
clipTo: function(ctx) {
ctx.save();
ctx.rotate(-this.originalAngle * Math.PI / 180);
ctx.translate(this.originalTransformLeft / this.originalObjScaleX, this.originalTransformTop / this.originalObjScaleY)
this.clipMask.set({
left: -object.width / 2 - (this.clipMask.width / 2 * this.originalMaskScaleX) - this.originalObjLeft / this.originalObjScaleX,
top: -object.height / 2 - (this.clipMask.height / 2 * this.originalMaskScaleY) - this.originalObjTop / this.originalObjScaleY,
objectCaching: false
});
this.clipMask.render(ctx);
ctx.restore();
}
});
canvas.requestRenderAll();
}
// image
let image = new Image();
image.onload = function() {
object = new fabric.Image(image, {
width: 500,
height: 500,
scaleX: 0.8,
scaleY: 0.8,
angle: 45,
top: 50,
left: 100
});
canvas.add(object);
};
image.src = "http://i.imgur.com/8rmMZI3.jpg";
fabric.util.object.extend(fabric.Image.prototype, {
clipMask: null,
originalObjLeft: 0,
originalObjTop: 0,
originalMaskScaleX: 1,
originalMaskScaleY: 1,
originalObjScaleX: 1,
originalObjScaleY: 1,
originalAngle:0,
originalTransformLeft:0,
originalTransformTop:0
});
fabric.Image.prototype.toObject = (function(toObject) {
return function(propertiesToInclude) {
return fabric.util.object.extend(toObject.call(this, propertiesToInclude), {
clipMask: this.clipMask ? this.clipMask.toObject(propertiesToInclude) : null,
originalObjLeft: this.originalObjLeft,
originalObjTop: this.originalObjTop,
originalMaskScaleX: this.originalMaskScaleX,
originalMaskScaleY: this.originalMaskScaleY,
originalObjScaleX: this.originalObjScaleX,
originalObjScaleY: this.originalObjScaleY,
originalAngle:this.originalAngle,
originalTransformLeft:this.originalTransformLeft,
originalTransformTop:this.originalTransformTop
});
}
})(fabric.Image.prototype.toObject);
fabric.Image.fromObject = (function(fromObject) {
return function(_object, callback) {
fromObject.call(this, _object, (function(callback, _object) {
return function(image) {
if (image.clipMask) {
fabric.Path.fromObject(image.clipMask, (function(callback) {
return function(path) {
path.pathOffset.x = 0;
path.pathOffset.y = 0;
image.clipMask = path;
callback(image);
}
})(callback))
} else {
callback(image);
}
}
})(callback, _object));
return;
}
})(fabric.Image.fromObject)
$("#button1").on('click', function() {
let dataJSON = canvas.toJSON();
canvas.clear();
canvas.loadFromJSON(
dataJSON,
canvas.renderAll.bind(canvas));
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.6/fabric.js"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<button id="button1">SAve/Load JSON</button>
<div class="canvas__wrapper">
<canvas id="canvas" width="1280" height="720"></canvas>
</div>
UPDATE
I updated the code to fix the problem with angle from here:

how to render an object reappeared on the canvas

Initially I instantiated a Rect object, by controlling the object's top and left values, making it beyond the canvas area, so that the Rect object will not be rendered on the canvas. After that, change the top and left values of the Rect to make it in the area of the canvas by the event handler and then how to render the Rect object on the canvas.
the following code is a demo:
<canvas id="canvas" width="800" height="600"></canvas>
<script src="js/fabric.js"></script>
<script>
(function () {
var canvas = this.__canvas = new fabric.Canvas('canvas');
fabric.Object.prototype.transparentCorners = false;
var targetLine = [], paramsG, paramsR;
for (var k = 0; k < 20; k++) {
paramsG = {
left: 200,
top: 530 - 100 * k,
width: 20,
height: 50,
visibile: false,
fill: '#62ab59',
hasBorders: false,
lockMovementX: true,
hasControls: false
};
paramsR = {
left: 200,
top: 580 - 100 * k,
width: 20,
height: 50,
visibile: false,
fill: '#ed5d5d',
hasBorders: false,
lockMovementX: true,
hasControls: false
};
canvas.add(new fabric.Rect(paramsG), new fabric.Rect(paramsR));
}
canvas.on('mouse:down', function (e) {
if (e.target) {
targetLine = getMemberByLeft(canvas._objects, e.target);
}
})
canvas.on('object:moving', function (e) {
targetLine.forEach(function (val) {
canvas._objects[val.index].set({top: e.e.movementY + canvas._objects[val.index].top});
})
canvas.renderAll();
})
function getMemberByLeft(arr, tar) {
var returnArr = [];
arr.forEach(function (value, key) {
if (value.left == tar.left && value != tar) {
returnArr.push({data: value, index: key});
}
})
return returnArr;
}
})();
</script>
Fabric has a function to skip object rendering if they are not visible on screen, to get some more speed.
If you change top and left by code, fabric will not understand that the object is again on screen unless you call object.setCoords()
If you do not want to have this behaviour automatic you can disable it using
canvas.skipOffscreen = false;

Control z-index in Fabric.js

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

Resources