Let's continue adding useful tools to our FabricJS canvas by implementing text and freeform lines!
The Sample Project
As with all my code-heavy series, there is an example project over on GitHub that shows all the code we have developed and will include in this series. Check it out!
Text
Continuing from the previous post, let's create the drawer and display component classes for a text object. The below drawer uses the FabricJS Text object to represent text on the canvas.
class TextDrawer implements IObjectDrawer {
drawingMode: DrawingMode = DrawingMode.Text;
make(x: number, y: number,
options: fabric.IObjectOptions): Promise<fabric.Object> {
//We will need to render a textbox for the text to draw
const text = <HTMLInputElement>document.getElementById('textComponentInput');
return new Promise<fabric.Object>(resolve => {
resolve(new fabric.Text(text.value, {
left: x,
top: y,
...options
}));
});
}
resize(object: fabric.Text, x: number, y: number): Promise<fabric.Object> {
object.set({
left: x,
top: y
}).setCoords();
return new Promise<fabric.Object>(resolve => {
resolve(object);
});
}
}
Now, let's see the component:
class TextDisplayComponent extends DisplayComponent {
constructor(target: string, parent: DrawingEditor) {
const options = new DisplayComponentOptions();
Object.assign(options, {
altText: 'Text',
classNames: 'fa fa-font',
childName: 'textComponentInput'
});
super(DrawingMode.Text, target, parent, options);
}
render(): void {
super.render();
//We need to render a hidden textbox next to the text button.
$(this.target).parent().append(`<input id="${this.childName}" class="col-sm-6 form-control hidden" />`);
}
//The two methods below, selectionUpdated and selectedChanged,
//only exist on the base DisplayComponent class
//because this TextDisplayComponent class needs them.
//Show the textbox if the text button is selected
selectionUpdated(newTarget: string) {
$(newTarget).removeClass('hidden');
}
selectedChanged(componentName: string): void {
//If the text button is selected, show the textbox
if (componentName === this.target) {
$(`#${this.childName}`).removeClass('hidden');
}
//Otherwise, hide the textbox.
else {
$(`#${this.childName}`).addClass('hidden').val('');
}
}
}
Finally, let's update the main DrawingEditor
class, the Razor Page markup, and the script:
class DrawingEditor {
//...Properties
constructor(private readonly selector: string,
//...Rest of constructor
this.drawers = [
new LineDrawer(),
new RectangleDrawer(),
new OvalDrawer(),
new TriangleDrawer(),
new TextDrawer()
];
}
//...Other methods
addComponent(target: string, component: string) {
switch (component) {
case 'line':
this.components[component]
= [new LineDisplayComponent(target, this)];
break;
case 'rect':
this.components[component]
= [new RectangleDisplayComponent(target, this)];
break;
case 'oval':
this.components[component]
= [new OvalDisplayComponent(target, this)];
break;
case 'tria':
this.components[component]
= [new TriangleDisplayComponent(target, this)];
break;
case 'text':
//New component
this.components[component]
= [new TextDisplayComponent(target, this)];
break;
}
}
<div class="row bg-secondary text-light">
<div class="col-sm-11">
<div class="row col-12">
<div class="row drawingToolbar">
<label class="col-form-label controlLabel">Tools:</label>
<div class="d-inline-block">
<div class="btn-group btn-group-toggle" role="group" data-toggle="buttons" aria-label="Drawing Components">
<div id="lineDisplayComponent"></div>
<div id="rectangleDisplayComponent"></div>
<div id="ovalDisplayComponent"></div>
<div id="triangleDisplayComponent"></div>
<!-- New Component -->
<div id="textDisplayComponent"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts{
//Initialize scripts
<script>
$(function () {
//Instantiate DrawingEditor
//Create a list of available display components
const components = [
{ id: '#lineDisplayComponent', type: 'line' },
{ id: '#rectangleDisplayComponent', type: 'rect' },
{ id: '#ovalDisplayComponent', type: 'oval' },
{ id: '#triangleDisplayComponent', type: 'tria' },
{ id: '#textDisplayComponent', type: 'text' },
];
//Add the components to the DrawingEditor, which will render them.
editor.addComponents(components);
$('#lineDisplayComponent').click();
});
</script>
}
With all of these changes in place, we can now write text onto our canvas, as shown in this GIF:
Note that our implementation did not require us to change things like the text font, size, style, or color, and so I leave those kinds of improvements up to you, my dear readers.
Let's also take a few minutes to implement another useful FabricJS feature: polylines.
Freeform Lines (AKA Polylines)
"Freeform" lines are lines which are drawn freely onto the canvas: think using the pencil tool in Paint. FabricJS terms these "polylines" because in reality a "freeform" line consists of many tiny straight lines that combine to form what looks like curves. These straight lines are created by storing a list of ordinal points, between which lines are connected (essentially like playing a giant version of connect-the-dots).
As with Text, we need two parts: a drawer and a display component. Here's the drawer class:
class PolylineDrawer implements IObjectDrawer {
drawingMode: DrawingMode = DrawingMode.Polyline;
make(x: number, y: number, options: fabric.IObjectOptions,
rx?: number, ry?: number): Promise<fabric.Object> {
return new Promise<fabric.Object>(resolve => {
resolve(new fabric.Polyline(
[{ x, y }],
{ ...options, fill: 'transparent' }
));
});
}
resize(object: fabric.Polyline, x: number, y: number)
: Promise<fabric.Object> {
//Create and push a new Point for the Polyline
object.points.push(new fabric.Point(x, y));
const dim = object._calcDimensions();
object.set({
left: dim.left,
top: dim.top,
width: dim.width,
height: dim.height,
dirty: true,
pathOffset: new fabric.Point(dim.left + dim.width / 2, dim.top + dim.height / 2)
}).setCoords();
return new Promise<fabric.Object>(resolve => {
resolve(object);
});
}
}
And here's the display component:
class PolylineDisplayComponent extends DisplayComponent {
constructor(target: string, parent: DrawingEditor) {
const options = new DisplayComponentOptions();
Object.assign(options, {
altText: 'Pencil',
classNames: 'fa fa-pencil',
childName: null
});
super(DrawingMode.Polyline, target, parent, options);
}
}
Just as with the earlier shape drawers and components, we need to modify the DrawingEditor
class...
class DrawingEditor {
//...Properties
constructor(private readonly selector: string,
//...Initialize canvas
this.drawers = [
new LineDrawer(),
new RectangleDrawer(),
new OvalDrawer(),
new TriangleDrawer(),
new TextDrawer(),
new PolylineDrawer()
];
//...Rest of constructor
}
//...Other methods
addComponent(target: string, component: string) {
switch (component) {
case 'line':
this.components[component]
= [new LineDisplayComponent(target, this)];
break;
case 'rect':
this.components[component]
= [new RectangleDisplayComponent(target, this)];
break;
case 'oval':
this.components[component]
= [new OvalDisplayComponent(target, this)];
break;
case 'tria':
this.components[component]
= [new TriangleDisplayComponent(target, this)];
break;
case 'text':
this.components[component]
= [new TextDisplayComponent(target, this)];
break;
//New component
case 'polyline':
this.components[component]
= [new PolylineDisplayComponent(target, this)];
break;
}
}
}
...as well as the markup and script on our Razor Page.
<div class="row bg-secondary text-light">
<div class="col-sm-11">
<div class="row col-12">
<div class="row drawingToolbar">
<label class="col-form-label controlLabel">Tools:</label>
<div class="d-inline-block">
<div class="btn-group btn-group-toggle" role="group" data-toggle="buttons" aria-label="Drawing Components">
<!-- New Component -->
<div id="polylineDisplayComponent"></div>
<div id="lineDisplayComponent"></div>
<div id="rectangleDisplayComponent"></div>
<div id="ovalDisplayComponent"></div>
<div id="triangleDisplayComponent"></div>
<div id="textDisplayComponent"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts{
//Add scripts
<script>
var editor;
$(function () {
//Instantiate DrawingEditor
//Create a list of available display components
const components = [
{ id: '#lineDisplayComponent', type: 'line' },
{ id: '#rectangleDisplayComponent', type: 'rect' },
{ id: '#ovalDisplayComponent', type: 'oval' },
{ id: '#triangleDisplayComponent', type: 'tria' },
{ id: '#textDisplayComponent', type: 'text' },
//New component
{ id: '#polylineDisplayComponent', type: 'polyline' }
];
//Add the components to the DrawingEditor, which will render them.
editor.addComponents(components);
$('#lineDisplayComponent').click();
//...Other methods
});
</script>
}
GIF Time!
All of this together allows us to draw freeform lines, as shown in the below GIF:
Ta-da! Now we have some very useful tools added to our FabricJS canvas toolbox!
Summary
Just like with the basic shapes, for both Text and Polylines we needed to implement a drawer and a display component. Those classes then needed to be wired up to the main DrawingEditor class, and into the Razor Page markup and script.
Don't forget to check out the sample project over on GitHub!
In the next part of this series, we will implement a toolbar button and hotkey functionality to delete objects from the canvas.
Happy Drawing!