Extending Fabric.js with Custom Graphical Objects
Fabric.js provides several built-in shapes such as Rect, Circle, and Triangle. Creating custom shapes is often necessary for specific project requirements. This involves defining custom subclasses, a process that benefits from a solid understanding of the HTML Canvas API.
The Concept of Subclasses in Fabric.js
Subclasses in Fabric.js follow similar principles to JavaScript ES6 classes. You can define a class and extend it to inherit properties and methods. For example, the official docuemntation demonstrates extending a Rect to create a LabeledRect with an additional text label.
Defining a Class with fabric.util.createClass
Use fabric.util.createClass to define a new class. The method accepts a configuration object. The initialize function serves as the constructor, handling the initialization of instance properties.
const Vector2D = fabric.util.createClass({
initialize: function(xCoord, yCoord) {
this.x = xCoord || 0;
this.y = yCoord || 0;
},
getCoordinates: function() {
return `(${this.x}, ${this.y})`;
}
});
const myVector = new Vector2D(15, 20);
console.log(myVector.x); // 15
console.log(myVector.getCoordinates()); // "(15, 20)"
Creating a Subclass with Inheritance
To create a subclass, pass two arguments to fabric.util.createClass: the parent class and the subclass configuration object. Use this.callSuper to invoke the parent class's methods.
const ColoredVector = fabric.util.createClass(
// Parent class
Vector2D,
// Subclass definition
{
initialize: function(xCoord, yCoord, vecColor) {
this.callSuper('initialize', xCoord, yCoord);
this.color = vecColor || '#000000';
},
getDescription: function() {
const coords = this.callSuper('getCoordinates');
return `Vector at ${coords} with color ${this.color}`;
}
}
);
const blueVector = new ColoredVector(5, 10, '#0000FF');
console.log(blueVector.getDescription()); // "Vector at (5, 10) with color #0000FF"
Extending Built-in Fabric.js Objects
Custom graphical elements should typically extend fabric.Object or one of its built-in shape descendants (like fabric.Rect). This grants the new object the standard Fabric.js capabilities such as controls, borders, and event handling.
const LabeledRectangle = fabric.util.createClass(
fabric.Rect,
{
type: 'labeledRectangle',
initialize: function(config) {
config = config || {};
this.callSuper('initialize', config);
// Set default properties
this.set({
width: 120,
height: 60,
labelText: config.labelText || ''
});
},
// Override the `toObject` method for serialization
toObject: function() {
return fabric.util.object.extend(
this.callSuper('toObject'),
{ labelText: this.get('labelText') }
);
},
// Custom rendering logic
_render: function(ctx) {
// Render the standard rectangle
this.callSuper('_render', ctx);
// Draw the custom label
ctx.save();
ctx.font = '16px Arial';
ctx.fillStyle = this.labelColor || '#000';
ctx.textAlign = 'center';
ctx.fillText(
this.labelText,
0, // Center horizontally relative to object origin
-this.height / 2 + 20 // Position above the rectangle
);
ctx.restore();
}
}
);
const canvas = new fabric.Canvas('canvasElement');
const myLabeledRect = new LabeledRectangle({
left: 50,
top: 50,
fill: '#90EE90',
labelText: 'Hello',
labelColor: '#333'
});
canvas.add(myLabeledRect);
Because LabeledRectangle extends fabric.Rect, all rectangle properties remain available.
myLabeledRect.set({ rx: 15, ry: 15, strokeWidth: 3 });
canvas.renderAll();
Implementing a Custom Shape from Scratch
To create a completely new shape, extend the fabric.Object base class and implement the _render method using the native Canvas 2D API (ctx).
const ArcShape = fabric.util.createClass(fabric.Object, {
type: 'arcShape',
initialize: function(options) {
this.callSuper('initialize', options);
// Define default dimensions
this.set({ width: 120, height: 80 });
},
_render: function(ctx) {
const radius = this.width / 2;
const centerY = 0; // Fabric origin is at the object's center
ctx.beginPath();
// Draw a 180-degree arc (semicircle)
ctx.arc(0, centerY, radius, 0, Math.PI);
ctx.closePath();
// Apply styles from instance properties
if (this.fill) {
ctx.fillStyle = this.fill;
ctx.fill();
}
if (this.stroke && this.strokeWidth) {
ctx.lineWidth = this.strokeWidth;
ctx.strokeStyle = this.stroke;
ctx.stroke();
}
}
});
const myCanvas = new fabric.Canvas('myCanvas');
const myArc = new ArcShape({
left: 80,
top: 60,
angle: 30,
fill: '#FFB6C1',
stroke: '#4682B4',
strokeWidth: 4
});
myCanvas.add(myArc);
The _render method receives the Canvas rendering context (ctx). Mastery of the Canvas API is essential for drawing complex custom shapes.