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>
Related
I'm creating a shape with certain width and height (this shape is a clipping rect) inside canvas. Then inside this shape I'm loading an object which can be moved and rotated.
I wrote a custom function for get center point inside this shape, and it works good if object are not rotated.
If I rotate this object over 90deg and click "center" button - the object is moved up outside the clipping rect.
I cannot use a native FabricJs "centerV" function, because I would like that object will be centered inside clipping rect - not the canvas container, so that is why I made a variable "objectVerticalCenter".
My code is presented here:
var canvas = new fabric.Canvas('canvas', {
'selection': false
});
var clippingRect = new fabric.Rect({
left: 170,
top: 90,
width: 185,
height: 400,
fill: 'transparent',
stroke: 1,
opacity: 1,
hasBorders: false,
hasControls: false,
hasRotatingPoint: false,
selectable: false,
preserveObjectStacking: true,
objectCaching: false
});
var retinaScalling = canvas.getRetinaScaling();
var pol = new fabric.Polygon([{
x: 200,
y: 0
}, {
x: 250,
y: 50
}, {
x: 250,
y: 100
}, {
x: 150,
y: 100
}, {
x: 150,
y: 50
}], {
left: 250,
top: 150,
angle: 0,
fill: 'green'
});
pol.scaleX = 0.5;
pol.scaleY = 0.5;
pol.set('id', 'shape');
pol.scaleToWidth(clippingRect.getWidth());
pol.setCoords();
pol.clipTo = function(ctx) {
ctx.save();
ctx.setTransform(retinaScalling, 0, 0, retinaScalling, 0, 0);
clippingRect.render(ctx);
ctx.restore();
};
canvas.centerObjectH(pol);
canvas.add(pol);
canvas.renderAll();
canvas.setActiveObject(pol);
canvas.add(pol);
canvas.renderAll();
document.getElementById('rotate').onclick = function() {
var activeObject = canvas.getActiveObject();
activeObject.setAngle(200);
activeObject.setCoords();
canvas.renderAll();
};
document.getElementById('center').onclick = function() {
var activeObject = canvas.getActiveObject();
var rectHeight = activeObject.getBoundingRectHeight();
var objectVerticalCenter = (clippingRect.getHeight() / 2) - (rectHeight / 2) + clippingRect.top;
activeObject.set('top', objectVerticalCenter);
activeObject.setCoords();
canvas.renderAll();
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.21/fabric.min.js"></script>
<button id="rotate">Rotate</button>
<button id="center">Center</button>
<canvas id="canvas" width="530" height="600"></canvas>
JSFiddle
To reproduce my problem you have to, click "rotate", then "center" button and you will see that shape is moved up outside the clipping rect.
If object is not rotated centering works fine.
Is there any way to fix my centering function or way to use "centerV" function inside the clipping rect?
Regards
If you want to center an object inside another, the safest thing to do is:
get the center point of the container
set the center point of the object in that point
in code this could equal to:
var clipCenter = clippingRect.getCenterPoint();
activeObject.setPositionByOrigin(clipCenter,'center','center');
var canvas = new fabric.Canvas('canvas', {
'selection': false
});
var clippingRect = new fabric.Rect({
left: 170,
top: 90,
width: 185,
height: 400,
fill: 'transparent',
stroke: 1,
opacity: 1,
hasBorders: false,
hasControls: false,
hasRotatingPoint: false,
selectable: false,
preserveObjectStacking: true,
objectCaching: false
});
var retinaScalling = canvas.getRetinaScaling();
var pol = new fabric.Polygon([{
x: 200,
y: 0
}, {
x: 250,
y: 50
}, {
x: 250,
y: 100
}, {
x: 150,
y: 100
}, {
x: 150,
y: 50
}], {
left: 250,
top: 150,
angle: 0,
fill: 'green'
});
pol.scaleX = 0.5;
pol.scaleY = 0.5;
pol.set('id', 'shape');
pol.scaleToWidth(clippingRect.getWidth());
pol.setCoords();
pol.clipTo = function(ctx) {
ctx.save();
ctx.setTransform(retinaScalling, 0, 0, retinaScalling, 0, 0);
clippingRect.render(ctx);
ctx.restore();
};
canvas.centerObjectH(pol);
canvas.setActiveObject(pol);
canvas.add(pol);
canvas.renderAll();
document.getElementById('rotate').onclick = function() {
var activeObject = canvas.getActiveObject();
activeObject.setAngle(200);
activeObject.setCoords();
canvas.renderAll();
};
document.getElementById('center').onclick = function() {
var activeObject = canvas.getActiveObject();
var clipCenter = clippingRect.getCenterPoint();
activeObject.setPositionByOrigin(clipCenter,'center','center');
canvas.renderAll();
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.21/fabric.min.js"></script>
<button id="rotate">Rotate</button>
<button id="center">Center</button>
<canvas id="canvas" width="530" height="600"></canvas>
I am having issues getting drag, drop and delete to work on a FabricJS using JQuery.
Select an object and click delete is working with no issues.
Here's a demo of my code Fiddle
I can get drag, drop and delete to work outside of FabricJS (Just dragging unrelated elements to delete) but I'm having difficulty combing the two.
Maybe I need to incorporate the delete button inside the canvas in order for the 'drop' to work? When I try this the #delete element disappears.
JS
var canvas = new fabric.Canvas('canvas');
$("#delete").click(function() {
deleteObjects();
});
//Test objects on the canvas
//Circle
var circle = new fabric.Circle({
left: 200,
top: 150,
radius: 30,
fill: "#ff0072"
});
circle.hasRotatingPoint = true;
canvas.add(circle);
// adding triangle
var triangle = new fabric.Triangle({
left: 130,
top: 150,
strokeWidth: 1,
width: 70,
height: 60,
fill: '#00b4ff',
selectable: true,
originX: 'center'
});
triangle.hasRotatingPoint = true;
canvas.add(triangle);
//Select all objects
function deleteObjects() {
var activeObject = canvas.getActiveObject(),
activeGroup = canvas.getActiveGroup();
if (activeObject) {
canvas.remove(activeObject);
}
}
//Drag and drop delete
$(function() {
$('#delete').droppable({
drop: function(event, ui) {
deleteObjects();
}
});
});
HTML
<section class="canvas__container">
<canvas id="canvas" width="400" height="400"></canvas>
<div id="delete">♻</div>
</section>
(I didn't paste my CSS as that's not particularly relevant)
This should get you close:
var bin;
var selectedObject;
fabric.Image.fromURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADUAAAA1CAIAAABuhDQnAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAPPSURBVGhDzZd9U+IwEIfv+3+2Gxln9PAEcYRBURQVkVeFe9rdC2laIFsK+Pzh2Cab/ZF9Sfpr9bMpr+/r6+v5+fn+/v5vyuXl5e8U/uGx2Wwy9Pr6yjQ1KIVZ32w2e3x8xL2oiYHJmGCoS1gw6MPB7e2t+iwF5laVUfqIEcGq1WrqZw9YhKXig75bHzlUiTIfFmRZdbCVHfrIG13yALC4utnMRn2EYM9siwEX22NdrA8b1y8ODY62SCzWd4Sd88GdOs5RoO+gObeJTbkY6qOs1OLoFFZ0Rh95UHkriQfX+UTM6Ot2uzr3RCBApfxnrY+T54SbJyAgOADX+krX7NnZGTeAm5ub8/NzfbUHQS2rPlTruJFOp7NYLGQR+P7+5vHz8/Pl5eX6+lonGfG3UPWV6ykPDw/L5VJWyIPQcjHxe43qM93nBEz8nSvk4+OD6KtBNKys9qKPqtYRC3d3d/P5XFYRptMpBei/ZGXyUg0suEaT6OOarq/toHI8HrPIZDLhWn91dTUajdKVE4g+tz2dasH16kSfte1Rp29vb71e7+LiQt78SeFRtPqQ7ExmVGZG4hphoo8fra+jQRx7A5QqJeySjJoNgs5jueQW8zL6Wq0WqSb2wnA41LG0gbm6YRqKUW+9rSFJVkj0mYxJsqDFs4s0Gh1OISlJcGJdr9cRR3ytvQZJsniiT9/FQeMVSwe+86240WiQjuDKxXozEiuzPupULB00OR3Lgmg/DTA09UKxMsfX10fuE8cguA6izHGnU9PJ5IaO7SITX1N9uPjijx1CXKFXCpDR4MfEO8rUh6n+3a5QFqQUGSbvaYrtdnswGBBThqghioO6Tr0kUCUyM4ZMfzH1Z3IoSEGksFVPT0/6nELcqQz2TB4R7X5JDJn+bD3fAilsJx0xyDYftDKqxnFkzjfs9XUcHPmYiD0QTQ5ZttDtVkC/31fLaNz6iT6wHkFsuRgKoiB/+ML7+7uprYBLPlB91vspLjFxv1J6L3/JRU5kV0C0RlPaCQX3U9bVQSOcEySftBguCpSwvN8H//xUfVDuLl45yFBBKWt9qP4J35d+5cFaH1gvqpXj2p4jow/tJ9zC/OZBRh9Yb0EV4nqyT6gPyn0L74nfU3wK9MGRazmoWZ9ifeSB9YuhNDjKp52jWB9gc4RdxMUWcbBRn3DQXNyUcz479AFlVXnTYcHCas2zWx8QAjpnJSpZhKW2x9QnSp/AAbhnRmLun/0xGPQJOCBvTPdFJmNiVSaY9TmIETlEsHDP55brR/zDIy8ZYkJ8KAspr+84/Gx9q9U/+FfCZwDz5ogAAAAASUVORK5CYII=', function(img) {
img.set({
left: 174,
top: 329,
selectable: false
});
bin = img;
canvas.add(img, circle, triangle);
});
canvas.on('object:selected', function(evn) {
selectedObject = evn.target;
})
canvas.on('mouse:up', function(evn) {
var x = evn.e.offsetX;
var y = evn.e.offsetY;
if (x > bin.left && x < (bin.left + bin.width) &&
y > bin.top && y < (bin.top + bin.height)) {
canvas.remove(selectedObject);
}
});
Here's your JSFiddle updated, https://jsfiddle.net/rekrah/jgruwse0/.
Let me know if you have any further questions.
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 try to add several times the same element in my paper.
Then I created it thanks to embed and then I cloned it with deep:true.
My problem is that deep:true change some properties (I don't know which) but I'm now enable to apply a translation to it.
here is my code, that work without deep:true but doesn't with. (but clone a rect and not my combined element) :
var width=400, height=1000;
var graph = new joint.dia.Graph; //define a graph
var paper = new joint.dia.Paper({ //define a paper
el: $('#myholder'),
width: width,
height: height,
gridSize: 1,
model: graph, //include paper in my graph
restrictTranslate: function(element) { //restriction of drag n drop to the vertical line (x never changes)
return {
x: element.model.attributes.position.x, //this represent the original x position of my element
y: 0,
width: 0,
height: height
};
}
});
var rect1 = new joint.shapes.basic.Rect({ //create first rect (with input and output)
position: { x: 100, y: 20 },
size: { width: 90, height: 90 },
attrs: {
rect: { fill: '#2ECC71' }
}
});
var add_block= new joint.shapes.basic.Circle({ //create add button
position: { x: 170, y: 20 },
size: { width: 20, height: 20 },
attrs: {
circle: { fill: '#2ECC71'},
text:{text:'+', "font-size":20}
}
});
rect1.embed(add_block);
graph.addCell([rect1,add_block]);
var clones=rect1.clone({deep:true});
graph.addCells(clones);
var line = V('line', { x1: 250, y1: 10, x2: 250, y2: 500, stroke: '#ddd' });
V(paper.viewport).append(line);
//Create a new block and link between two blocks
//chek if click on + button
//if click on this button, create a new block and link between them
paper.on('cell:pointerclick', function(cellView, evt, x, y) {
var elementBelow = graph.get('cells').find(function(cell) {
if (!cell.get('parent')) return false;//not interested in parent
if (cell instanceof joint.dia.Link) return false; // Not interested in links.
if (cell.getBBox().containsPoint(g.point(x, y))) {
return true;
}
return false;
});
if(elementBelow) //if we start from a block, let's go!
{
rect=rect1.clone({deep:true});
graph.addCell(rect);
rect.translate(200, 0);
}
});
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.