I have a custom subclass in FabricJS that is based on a FabricJS group:
var canvasObject = fabric.util.createClass(fabric.Group, {
type: 'canvasObject',
initialize: function(options) {
options || (options = { });
this.callSuper('initialize', options);
var rectBase = new fabric.Rect({
fill : 'red',
originX: 'left',
originY: 'top',
left: options.margin.l,
top: options.margin.t,
width : (options.width-options.margin.l-options.margin.r),
height :(options.height-options.margin.t-options.margin.b),
});
var labeledRect = new LabeledRect({
width: options.width,
height: options.height,
'idname': options.idname,
background: 'transparent',
labelFont: '14px Helvetica',
});
this.callSuper('initialize', [labeledRect, rectBase], options);
},
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
idname: this.get('idname')
});
},
_render: function(ctx) {
this.callSuper('_render', ctx);
}
});
How do I include an SVG within this custom group. I'd like to be able to load an SVG from a URL similar to this:
fabric.loadSVGFromURL('example.svg', function(objects, options) {
var shape = fabric.util.groupSVGElements(objects, options);
canvas.add(shape.scale(1));
canvas.renderAll();
});
But have it add to the canvas within my custom group - so that each canvasObject added can have an SVG attached.
I've tried adding the SVG to a PathGroup object - but I'm not sure the asynchronous load is working with it.
I'd also like to be able to update the SVG images within each group. How can I update an SVG path content similar to updating an objects properties?
Related
I've been getting familiar w/ FabricJS on a project.
I have a custom class that extends fabric.Polyline. This custom class has many custom methods. My problem is : When I serialize the whole canvas and later on load it again with loadFromJSON(), the custom methods are not there anymore...
I noticed that the object before/after serialization are not exactly the same. Indeed, we can see that after serialization the method myCustomClassMethod() is not in the object's __proto__ anymore :
I know that loadFromJSON() has a reviver() method for re-adding event listeners, but how can I re-add methods? The tutorials say that I need to add a fromObject() method to save/restore objects, I tried playing with that but no luck.
It might also be that my de-serialized object is a "regular" object, and not a fabricjs object instance. But I haven't seen anywhere how to make it a fabric object again.
MRE down below. As you can see, method works if called before canvas reloading, but not if called afterwards.
// create a wrapper around native canvas element (with id="treeCanvas")
var canvas = new fabric.Canvas('treeCanvas', {
allowTouchScrolling: true,
});
// un-comment log below to inspect objects
canvas.on('mouse:up', function (e) {
if (e.target != null) {
//console.log(e.target);
}
});
// My TreeNode custom class
var TreeNode = fabric.util.createClass(fabric.Polyline, {
initialize: function (X, Y, armsArray, options) {
options || (options = {});
this.callSuper('initialize', options);
this.X = X;
this.Y = Y;
this.armsArray = armsArray;
this.set({ width: 160, height: 50, originX: 'center', originY: 'center' });
this.set({ left: this.X, top: this.Y, fill: 'rgba(255, 255, 255, 0)', stroke: 'black', selectable: true });
this.setCoords();
this.set({ pathOffset: { x: 0, y: 25 } });
for (point of this.armsArray) {
this.points.push({ x: point[0], y: point[1] });
this.points.push({ x: 0, y: 0 });
}
},
_render: function (ctx) {
this.callSuper('_render', ctx);
},
// this is the method that disappears after serializing
myCustomClassMethod: function () {
this.set({ stroke: 'green' });
canvas.renderAll();
}
});
// Adding an instance of my custom class to canvas
var node1 = new TreeNode(100, 100, [[-80, 50], [80, 50]], []);
canvas.add(node1);
canvas.renderAll();
// node1.myCustomClassMethod(); // method works before serializing + deserializing
var stringifiedCanvas = JSON.stringify(canvas);
canvas.loadFromJSON(stringifiedCanvas, canvas.renderAll.bind(canvas));
node1.myCustomClassMethod(); // method DOES NOT WORK anymore after deserializing
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.3.1/fabric.min.js"></script>
<div class="container">
<canvas id="treeCanvas" width="500" height="300"></canvas>
</div>
<script src="canvas.js"></script>
I am very far from being a fabricjs expert and I do things somewhat differently but you were missing a few important parts (marked with "** CHANGE" in the code below)
Hope this is of some help!
// create a wrapper around native canvas element (with id="treeCanvas")
var canvas = new fabric.Canvas('treeCanvas', {
allowTouchScrolling: true,
});
// un-comment log below to inspect objects
canvas.on('mouse:up', function(e) {
if (e.target != null) {
//console.log(e.target);
}
});
// ** CHANGE: define Treenode on the fabric instance
fabric.TreeNode = fabric.util.createClass(fabric.Polyline, {
// ** CHANGE: define the type
type: 'treeNode',
initialize: function(X, Y, armsArray, options) {
options || (options = {});
this.callSuper('initialize', options);
this.X = X;
this.Y = Y;
this.armsArray = armsArray;
this.set({
width: 160,
height: 50,
originX: 'center',
originY: 'center'
});
this.set({
left: this.X,
top: this.Y,
fill: null,
stroke: 'black',
selectable: true
});
this.setCoords();
this.set({
pathOffset: {
x: 0,
y: 25
}
});
for (point of this.armsArray) {
this.points.push({
x: point[0],
y: point[1]
});
this.points.push({
x: 0,
y: 0
});
}
},
_render: function(ctx) {
this.callSuper('_render', ctx);
},
// ** CHANGE: export the custom method when serializing
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
myCustomClassMethod: this.myCustomClassMethod
});
},
myCustomClassMethod: function(x, y, color) {
// console.log("myCustomClassMethod")
// console.log(this.myCustomClassMethod)
this.set({
left: x,
top: y,
stroke: color,
strokeWidth: 10
});
canvas.renderAll();
}
});
fabric.TreeNode.fromObject = function(object, callback) {
// console.log(object)
};
var node1 = new fabric.TreeNode(100, 100, [
[-80, 50],
[80, 50]
], []);
canvas.add(node1);
canvas.renderAll();
// DESERIALIZE
var stringifiedCanvas = JSON.stringify(canvas);
canvas.loadFromJSON(stringifiedCanvas, canvas.renderAll.bind(canvas));
// ** CHANGE: need to get a reference to the restored object
var restored = canvas.getObjects()[0]
///console.log(restored)
restored.myCustomClassMethod(200, 50, "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.3.1/fabric.min.js"></script>
<div class="container">
<canvas id="treeCanvas" width="500" height="300"></canvas>
</div>
<script src="canvas.js"></script>
I am working at a project with fabric js. I tried to minimize my problem, so I'm hoping that the code isn't too messed up.
I am creating some Objects which are linked with each other:
A Line, which contains a Start and an Endpoint
A Circle, which is StartPoint of 1 line and Endpoint of another line
with this combination i can create different shapes(like a polygon) and modify my move-functions for them too.
When a Circle is dragged, the related Lines are scaling and moving too. (in my code you can move the lines too and the shape is resized after that, but i didnt put it into this example, bc this short extract should be enough to show what my problem is.)
I got a little example in jsfiddle: https://jsfiddle.net/bxgox7cr/
When you look at the ends of the lines, you can clearly see a cut, so the eye soon recognize, that this is not a connected shape but rather some lines which are close to each other. Is there a way to modify the look of the lines, that the shape looks "closed"?
Here is my code, i tried to put some comments, that it is easy to read:
var canvas = new fabric.Canvas('canvas');
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
document.getElementById("canvas").tabIndex = 1000;
/** ------------creating a Line Object, which contains a start and an endpoint ------------**/
fabric.LineWithPoints = fabric.util.createClass(fabric.Line, {
initialize: function(points, options) {
options || (options = {});
this.callSuper('initialize', points, options);
options &&
this.set('type', options.type),
this.set('name', options.name),
this.set('start_point', options.start_point),
this.set('end_point', options.end_point),
this.set('current_x', options.current_x),
this.set('current_y', options.current_y)
},
setStartPointAndEndPoint: function(start_point, end_point) {
this.set({
start_point: start_point,
end_point: end_point
});
},
setValues: function(new_x1, new_y1, new_x2, new_y2) {
// console.log(this);
this.set({
x1: new_x1,
x2: new_x2,
y1: new_y1,
y2: new_y2
});
this.setCoords();
}
});
/**--- modifie the circle element, adding new functions for the movement of the object-------*/
fabric.LinePoint = fabric.util.createClass(fabric.Circle, {
initialize: function(options) {
options || (options = {});
this.callSuper('initialize', options);
options &&
this.set('subtype', 'line_point'),
this.set('x', this.left),
this.set('y', this.top)
},
setPointCoordinates: function(new_left, new_top) {
this.set({
x: new_left,
y: new_top,
left: new_left,
top: new_top
});
this.setCoords();
},
move: function(new_left, new_top) {
var wall_1 = this.line1;
var wall_2 = this.line2;
this.setPointCoordinates(new_left, new_top);
wall_1.setValues(wall_1.x1, wall_1.y1, this.getLeft(), this.getTop());
wall_2.setValues(this.getLeft(), this.getTop(), wall_2.x2, wall_2.y2);
canvas.renderAll();
},
});
/**------------------- Moving Function------------------------------------------------- */
canvas.on('object:moving', function(event) {
var object = event.target;
if (object.subtype == "line_point") {
object.move(object.getLeft(), object.getTop());
}
});
/**------------------------------ create functions for the objects -----------------------*/
function newCircleObject(left, top, wall_1, wall_2) {
var circle = new fabric.LinePoint({
left: left,
top: top,
strokeWidth: 2,
radius: 15,
fill: 'grey',
stroke: 'black',
opacity: 0.1,
perPixelTargetFind: true,
subtype: 'line_point',
includeDefaultValues: false
});
circle.hasControls = false;
circle.hasBorders = false;
circle.line1 = wall_1;
circle.line2 = wall_2;
return circle;
}
function newWallObject(coords) {
var wall = new fabric.LineWithPoints(coords, {
stroke: 'black',
strokeWidth: 6,
lockScalingX: true,
lockScalingY: true,
perPixelTargetFind: true,
subtype: 'line',
type: 'line',
padding: 10,
includeDefaultValues: false
});
wall.hasControls = false;
wall.hasBorders = false;
return wall;
}
/**------------------------------ adding the shapes--------------------------------*/
var wall_1 = newWallObject([100, 100, 100, 500]);
var wall_2 = newWallObject([100, 500, 500, 500]);
var wall_3 = newWallObject([500, 500, 500, 100]);
var wall_4 = newWallObject([500, 100, 100, 100]);
var end_point_1 = newCircleObject(wall_1.x1, wall_1.y1, wall_4, wall_1);
var end_point_2 = newCircleObject(wall_2.x1, wall_2.y1, wall_1, wall_2);
var end_point_3 = newCircleObject(wall_3.x1, wall_3.y1, wall_2, wall_3);
var end_point_4 = newCircleObject(wall_4.x1, wall_4.y1, wall_3, wall_4);
wall_1.setStartPointAndEndPoint(end_point_1.name, end_point_2.name);
wall_2.setStartPointAndEndPoint(end_point_2.name, end_point_3.name);
wall_3.setStartPointAndEndPoint(end_point_3.name, end_point_4.name);
wall_4.setStartPointAndEndPoint(end_point_4.name, end_point_1.name);
canvas.add(wall_1, wall_2, wall_3, wall_4, end_point_1, end_point_2, end_point_3, end_point_4);
Add strokeLineCap: 'round',:
function newWallObject(coords) {
var wall = new fabric.LineWithPoints(coords, {
stroke: 'black',
strokeWidth: 6,
lockScalingX: true,
lockScalingY: true,
perPixelTargetFind: true,
strokeLineCap: 'round',
subtype: 'line',
type: 'line',
padding: 10,
includeDefaultValues: false
});
wall.hasControls = false;
wall.hasBorders = false;
return wall;
}
I looked up: http://fabricjs.com/docs/fabric.Object.html#strokeLineCap
I'm trying to bind an event to a background image:
var imgInstance = fabric.Image.fromURL(path, function(oImg) {
oImg.on("selected", function() {
alert();
});
canvas.setBackgroundImage(oImg, canvas.renderAll.bind(canvas), {
top: 0,
left: 0
});
oImg.on("selected", function() {
alert();
});
});
I can get it work. I'm trying to bind mouse events to handle drag, so i can move the image when it's bigger than the canvas, like background-cover css property.
Thoughts?
Thanks guys!
SOLUTION:
var DragStart = false;
canvas.on('mouse:down', function(options) {
if (!options.target) {
DragStart = true;
}else {
DragStart = false;
}
});
canvas.on('mouse:move', function(options) {
if (DragStart) {
var bounds = getBounds(options.e); // validate boundaries between image and canvas
canvas.backgroundImage.top = bounds.y;
canvas.backgroundImage.left = bounds.x;
canvas.renderAll();
}
});
It doesn't seem like you can add events to the background image... but the canvas does emit events and if you click the canvas and no target is set then they must be clicking the background. Background image are not forced to take up the whole background but I'm not sure reasons to not make it take up the whole background.
You do get the mouse event passed in though so if the background image does not take up the whole background you could do that math and figure out where they clicked.
window.canvas = new fabric.Canvas('c', { selection: false, preserveObjectStacking:true });
fabric.Image.fromURL('http://fabricjs.com/assets/pug_small.jpg', function(o) {
canvas.setBackgroundImage(o, canvas.renderAll.bind(canvas), {
top: 0,
left: 0
});
});
canvas.on('mouse:down', function(options) {
if (!options.target) {
console.log('target is null')
}
});
canvas.add(new fabric.Rect({
left: 100,
top: 100,
width: 50,
height: 50,
fill: '#faa',
originX: 'left',
originY: 'top',
centeredRotation: true
}));
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.4/fabric.min.js"></script>
<canvas id="c" width="600" height="600"></canvas>
I'm doing a responsive pie chart which holds title in centered position inside it.I've used,
title: {
text: "",
margin: 0,
y:0,
x:0,
align: 'center',
verticalAlign: 'middle',
},
but it's not perfectly centered inside the chart.Any suggestions would be appreciated.Thank you in advance.
Here's the Link : http://jsfiddle.net/LHSey/128/
Better is remove title and use renderer which allows to add custom text, which can be repositioned each time (when you redraw chart). Only what you need is catch this event.
function addTitle() {
if (this.title) {
this.title.destroy();
}
var r = this.renderer,
x = this.series[0].center[0] + this.plotLeft,
y = this.series[0].center[1] + this.plotTop;
this.title = r.text('Series 1', 0, 0)
.css({
color: '#4572A7',
fontSize: '16px'
}).hide()
.add();
var bbox = this.title.getBBox();
this.title.attr({
x: x - (bbox.width / 2),
y: y
}).show();
}
chart:{
events: {
load: addTitle,
redraw: addTitle,
},
}
Example: http://jsfiddle.net/LHSey/129/
We can customize any properties of title using chart.setTitle() as shown below.I've added a title and set useHTML property to true.
chart: {
events: {
load: function () {
this.setTitle({text: 'Title',useHTML:true});
}
}
}
I'm working on a project with Fabric.js.
Now I need to create a custom class and to export it to SVG.
I'm using Fabric.js tutorial to begin:
http://www.sitepoint.com/fabric-js-advanced/
Here is the javascript code:
var LabeledRect = fabric.util.createClass(fabric.Rect, {
type: 'labeledRect',
initialize: function(options) {
options || (options = { });
this.callSuper('initialize', options);
this.set('label', options.label || '');
},
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
label: this.get('label')
});
},
_render: function(ctx) {
this.callSuper('_render', ctx);
ctx.font = '20px Helvetica';
ctx.fillStyle = '#333';
ctx.fillText(this.label, -this.width/2, -this.height/2 + 20);
}
});
var canvas = new fabric.Canvas('container');
var labeledRect = new LabeledRect({
width: 100,
height: 50,
left: 100,
top: 100,
label: 'test',
fill: '#faa'
});
canvas.add(labeledRect);
document.getElementById('export-btn').onclick = function() {
canvas.deactivateAll().renderAll();
window.open('data:image/svg+xml;utf8,' + encodeURIComponent(canvas.toSVG()));
};
Here the HTML:
<canvas id="container" width="780" height="500"></canvas>
Export SVG
Here is my jsfiddle..
http://jsfiddle.net/QPDy5/
What I'm doing wrong?
Thanks in advance.
Every "class" in Fabric (rectangle, circle, image, path, etc.) knows how to output its SVG markup. The method responsible for it is toSVG. You can see toSVG of fabric.Rect, for example, or fabric.Text one.
Since you created subclass of fabric.Rect here and didn't specify toSVG, toSVG of the next object in prototype chain is used. The next "class" in the inheritance chain happens to be fabric.Rect, so you're seeing a result of fabric.Rect.prototype.toSVG.
The solution is simple: specify toSVG in your subclass. Theoretically, you would need to combine the code from fabric.Rect#toSVG and fabric.Text#toSVG, but to avoid repetition and keep things maintainable, we can utilize a bit of a hack:
toSVG: function() {
var label = new fabric.Text(this.label, {
originX: 'left',
originY: 'top',
left: this.left - this.width / 2,
top: this.top - this.height / 2,
fontSize: 20,
fill: '#333',
fontFamily: 'Helvetica'
});
return this.callSuper('toSVG') + label.toSVG();
}
The "fontSize", "fill", and "fontFamily" should ideally be moved to an instance-level property of course, to avoid repeating them there.
Here's a modified test — http://jsfiddle.net/fabricjs/QPDy5/1/
What #Sergiu Paraschiv suggested with a group is another just-as-viable solution.
Problem is there's no SVG equivalent to fillText so FabricJS seems to ignore it.
My workaround is to use a fabric.Group and build an inner Rect and Text
var LabeledRect = function(options) {
var rect = new fabric.Rect({
width: options.width,
height: options.height,
fill: options.fill
});
var label = new fabric.Text(options.label);
var group = new fabric.Group([rect, label]);
group.left = options.left;
group.top = options.top;
group.toSVG = function() {
var e=[];
for(var t = 0; t < this._objects.length; t++)
e.push(this._objects[t].toSVG());
return'<g transform="'+this.getSvgTransform()+'">'+e.join("")+"</g>"
};
return group;
};
The reason I overwrote toSVG is because the default implementation cycles through the children in reverse order (for(var t=this._objects.length;t--;)). I have no clue why it does that, but the text renders underneath the rectangle.