DEV Community

Cover image for Drawing with FabricJS and TypeScript Part 4: Text and Freeform Lines
Matthew Jones
Matthew Jones

Posted on • Originally published at exceptionnotfound.net

Drawing with FabricJS and TypeScript Part 4: Text and Freeform Lines

Let's continue adding useful tools to our FabricJS canvas by implementing text and freeform lines!

Maybe letter magnets would be a good toolbar item? Photo by Jason Leung / Unsplash

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:

Alt Text

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!

Discussion (0)