Martin Kimani Innovative frontend developer with five years of experience building responsive websites. Proficient in HTML, CSS, and JavaScript, as well as modern libraries and frameworks. I'm passionate about usability and I possess a working knowledge of graphic design.

Build indoor maps with Fabric.js and React

9 min read 2521

Fabricjs React Indoor Mapping

Indoor mapping is a novel concept that uses a digital 2D or 3D map to visualize an indoor venue or geographic data. By displaying places, people, and assets on a digital map, you can recreate indoor locations with navigation functionality, allowing for many business use cases that improve workflows and efficiencies.

For example, you can use indoor mapping to provide deeper insights into visitor behavior, improving managers’ capacity to discover and identify assets quickly and easily. Managers then have the option to use this knowledge to restructure for more efficient operations.

To build indoor maps, developers can use Fabric.js with React to grasp the basic functionalities of the grid system, zooming, panning, and annotations. In this article, we’ll cover how to use Fabric.js inside the component’s render method.

To follow along with this article, you’ll need basic knowledge of React, HTML, CSS, and JavaScript. You’ll also need a canvas element with an ID and the function that returns the fabric.Canvas object. Finally, you need a basic understanding of how to use npm.
To follow along with this article, you can find the full code for this project on GitHub. Let’s get started!

Indoor Mapping Gif

Table of contents

What is Fabric.js used for?

A powerful and simple JavaScript library that provides an interactive platform to work with React, Fabric.js allows you to create various objects and shapes on a canvas, ranging from simple geometric shapes to more complex ones.

With Fabric.js, you can work with both images and animations. Fabric.js allows you to drag, scale, and rotate images; you can also group shapes and objects to be manipulated together. Fabric.js even provides functionality to serialize the canvas to SVG or JSON and reuse it as and when needed. With the help of node-canvas libraries, Fabric.js is supported by Node.js.

Populating objects on the canvas

To create objects on the Fabric.js canvas, first create the Canvas class before populating the required objects into it. Use the createElement function to upload the canvas into the document and its container. Now, create the different objects that will be populated on the canvas, as shown below. Populate them using the necessary functions:

import Base from '../core/Base';
import { Arrow } from './Arrow';

const Modes = {
  SELECT: 'select',
  DRAWING: 'drawing',
  ARROW: 'arrow',
  TEXT: 'text'
};

export class Canvas extends Base {
  constructor(container, options) {
    super(options);

    this.container = container;

    const canvas = document.createElement('canvas');
    this.container.appendChild(canvas);
    canvas.setAttribute('id', 'indoorjs-canvas');

    canvas.width = this.width || this.container.clientWidth;
    canvas.height = this.height || this.container.clientHeight;

    this.currentColor = this.currentColor || 'black';
    this.fontFamily = this.fontFamily || 'Roboto';

    this.canvas = new fabric.Canvas(canvas, {
      freeDrawingCursor: 'none',
      freeDrawingLineWidth: this.lineWidth
    });
    this.arrows = [];

    this.setLineWidth(this.lineWidth || 10);
    this.addCursor();
    this.addListeners();

    this.setModeAsArrow();
  }

  setModeAsDrawing() {
    this.mode = Modes.DRAWING;
    this.canvas.isDrawingMode = true;
    this.canvas.selection = false;
    this.onModeChanged();
  }

  isDrawingMode() {
    return this.mode === Modes.DRAWING;
  }

 setModeAsSelect() {
    this.mode = Modes.SELECT;
    this.canvas.isDrawingMode = false;
    this.canvas.selection = true;
    this.onModeChanged();
  }

  isSelectMode() {
    return this.mode === Modes.SELECT;
  }

  setModeAsArrow() {
    this.mode = Modes.ARROW;
    this.canvas.isDrawingMode = false;
    this.canvas.selection = false;
    this.onModeChanged();
  }

  isArrowMode() {
    return this.mode === Modes.ARROW;
  }

  setModeAsText() {
    this.mode = Modes.TEXT;
    this.canvas.isDrawingMode = false;
    this.canvas.selection = false;
    this.onModeChanged();
  }

Creating the gradient of objects

Since the gradient is essential for the measurement of objects on the canvas, use the measurement class to implement the x and y-axis. The code below shows how to use the x and y-axis and the onMouseMove function to create the gradient of objects:

import Measurer from './Measurer';

class Measurement {
  constructor(map) {
    this.map = map;
    this.measurer = null;
  }

  onMouseMove(e) {
    const point = {
      x: e.absolutePointer.x,
      y: e.absolutePointer.y,
    };

    if (this.measurer && !this.measurer.completed) {
     this.measurer.setEnd(point);
      this.map.canvas.requestRenderAll();
    }
  }

  onClick(e) {
    const point = {
      x: e.absolutePointer.x,
      y: e.absolutePointer.y,
    };
    if (!this.measurer) {
      this.measurer = new Measurer({
        start: point,
        end: point,
       map: this.map,
      });

      // this.map.canvas.add(this.measurer);
    } else if (!this.measurer.completed) {
      this.measurer.setEnd(point);
      this.measurer.complete();
    }
  }
}

export default Measurement;

Building the grid system

Import alpha, grid-style, Axis, and Point from Geometry. Before proceeding to the next step, create a constructor of the canvas inside the Grid class. Use the getCenterCoords function to get the coordinates, width, height, and states of the different shapes.



Reevaluate the lines with the x and y-axis to calculate the options for renderer and recalculate their state. Get state object with calculated parameters ready for rendering. Finally, calculate the real offset/range:

import alpha from '../lib/color-alpha';
import Base from '../core/Base';
import {
  clamp, almost, len, parseUnit, toPx, isObj
} from '../lib/mumath/index';
import gridStyle from './gridStyle';
import Axis from './Axis';
import { Point } from '../geometry/Point';

// constructor
class Grid extends Base {
  constructor(canvas, opts) {
    super(opts);
    this.canvas = canvas;
    this.context = this.canvas.getContext('2d');
    this.state = {};
    this.setDefaults();
    this.update(opts);
  }

  render() {
    this.draw();
    return this;
  }

  getCenterCoords() {
    let state = this.state.x;
    let [width, height] = state.shape;
    let axisCoords = state.opposite.coordinate.getCoords(
      [state.coordinate.axisOrigin],
      state.opposite
    );
    const y = pt + axisCoords[1] * (height - pt - pb);
    state = this.state.y;
    [width, height] = state.shape;
    [pt, pr, pb, pl] = state.padding;
    axisCoords = state.opposite.coordinate.getCoords([state.coordinate.axisOrigin], state.opposite);
    const x = pl + axisCoords[0] * (width - pr - pl);
    return { x, y };
  }

  setSize(width, height) {
    this.setWidth(width);
    this.setHeight(height);
  }

  setWidth(width) {
    this.canvas.width = width;
  }

  setHeight(height) {
    this.canvas.height = height;
  }

  update(opts) {
    if (!opts) opts = {};
    const shape = [this.canvas.width, this.canvas.height];

    // recalc state
    this.state.x = this.calcCoordinate(this.axisX, shape, this);
    this.state.y = this.calcCoordinate(this.axisY, shape, this);
    this.state.x.opposite = this.state.y;
    this.state.y.opposite = this.state.x;
    this.emit('update', opts);
    return this;
  }

  // re-evaluate lines,
  update2(center) {
    const shape = [this.canvas.width, this.canvas.height];
    Object.assign(this.center, center);
    // recalc state
    this.state.x = this.calcCoordinate(this.axisX, shape, this);
    this.state.y = this.calcCoordinate(this.axisY, shape, this);
    this.state.x.opposite = this.state.y;
    this.state.y.opposite = this.state.x;
    this.emit('update', center);

    this.axisX.offset = center.x;
    this.axisX.zoom = 1 / center.zoom;

    this.axisY.offset = center.y;
    this.axisY.zoom = 1 / center.zoom;
  }

  calcCoordinate(coord, shape) {
    const state = {
      coordinate: coord,
      shape,
      grid: this
    };
    // calculate real offset/range
    state.range = coord.getRange(state);
    state.offset = clamp(
      Math.max(coord.min, -Number.MAX_VALUE + 1),
      Math.min(coord.max, Number.MAX_VALUE) - state.range
    );

Implementing zoom and panning

Since there are a few zoom features in the previous code, we’ll implement zoom and panning features inside the grid. The stub methods use the visible range parameters, labels, line, and axis parameters to return coordinates for the values redefined by the axes.

Now, declare the Zoom function with important variables like height, width, minimum, and maximum zoom positions. At this point, it’s also critical to declare the pan and its features. Finally, to return the screen to default features after zooming and panning, use the reset function as shown below:

setZoom(zoom) {
    const { width, height } = this.canvas;
    this.zoom = clamp(zoom, this.minZoom, this.maxZoom);
    this.dx = 0;
    this.dy = 0;
    this.x = width / 2.0;
    this.y = height / 2.0;
    this.update();
    process.nextTick(() => {
      this.update();
    });
  }
    if (layer.shape.keepOnZoom) {
      const scale = 1.0 / this.zoom;
      layer.shape.set('scaleX', scale);
      layer.shape.set('scaleY', scale);
      layer.shape.setCoords();
      this.emit(`${layer.class}scaling`, layer);
    }
  this.zoom = Math.min(scaleX, scaleY);

    this.canvas.setZoom(this.zoom);

    this.canvas.absolutePan({
      x: this.originX + this.center.x * this.zoom,
      y: this.originY - this.center.y * this.zoom
    });

reset() {
    const { width, height } = this.canvas;
    this.zoom = this._options.zoom || 1;
    this.center = new Point();
    this.originX = -this.canvas.width / 2;
    this.originY = -this.canvas.height / 2;
    this.canvas.absolutePan({
      x: this.originX,
      y: this.originY
    });
 const objects = canvas.getObjects();
    let hasKeepZoom = false;
    for (let i = 0; i < objects.length; i += 1) {
      const object = objects[i];
      if (object.keepOnZoom) {
        object.set('scaleX', 1.0 / this.zoom);
        object.set('scaleY', 1.0 / this.zoom);
        object.setCoords();
        hasKeepZoom = true;
  this.emit(`${object.class}scaling`, object);
      }
    }
    if (hasKeepZoom) canvas.requestRenderAll();
  }

  panzoom(e) {
    // enable interactions
    const { width, height } = this.canvas;

    const prevZoom = 1 / this.zoom;
    let curZoom = prevZoom * (1 - zoom);
    curZoom = clamp(curZoom, this.minZoom, this.maxZoom);

    // pan
    const oX = 0.5;
    const oY = 0.5;
    if (this.isGrabMode() || e.isRight) {
      x -= prevZoom * e.dx;
      y += prevZoom * e.dy;
      this.setCursor('grab');
    } else {
      this.setCursor('pointer');
    }

    if (this.zoomEnabled) {
      x -= width * (curZoom - prevZoom) * tx;
      y -= height * (curZoom - prevZoom) * ty;
    }
    this.center.setX(x);
    this.center.setY(y);
    this.zoom = 1 / curZoom;
    this.dx = e.dx;
    this.dy = e.dy;
    this.x = e.x0;
    this.y = e.y0;
    this.isRight = e.isRight;
    this.update();
  }

Scaling the canvas

To scale the canvas, we begin by initializing scaleX and scaleY values which are used to convert the dimensions of the objects relative to the original canvas dimensions. We use a math function to scale and zoom objects and the canvas. The canvas changes size depending on the users’ input and their use of the scroll wheel to zoom in and out on an object. The canvas is zoomed in and out depending on the height and width, enabling the user to have a view of the entire canvas.

Adding annotations

Annotation refers to labeling text or images. When the default label options don’t fit our needs, we can use annotation to improve the taxonomy. To annotate our code, we’ll first import the image annotation tools into the component. To use a nested array of objects, the labels must start with the coordinates of the labels or annotations.

Finally, we convert the hashmap labels or annotations to lines and colors, making them visible when the application is running:

    let labels;
    if (coord.labels === true) labels = state.lines;
    else if (coord.labels instanceof Function) {
      labels = coord.labels(state);
    } else if (Array.isArray(coord.labels)) {
      labels = coord.labels;
    } else if (isObj(coord.labels)) {
      labels = coord.labels;
    } else {
      labels = Array(state.lines.length).fill(null);
    }
    state.labels = labels;
    // convert hashmap labels to lines
    if (isObj(ticks)) {
      state.ticks = Array(lines.length).fill(0);
    }
    if (isObj(labels)) {
      state.labels = Array(lines.length).fill(null);
    }
    if (isObj(ticks)) {
      // eslint-disable-next-line
      Object.keys(ticks).forEach((value, tick) => {
        state.ticks.push(tick);
       state.lines.push(parseFloat(value));
      state.lineColors.push(null);
       state.labels.push(null);
      });
    }

    if (isObj(labels)) {
      Object.keys(labels).forEach((label, value) => {
        state.labels.push(label);
        state.lines.push(parseFloat(value));
        state.lineColors.push(null);
        state.ticks.push(null);
      });
    }

    return state;
  }

Alternatives to Fabric.js for mapping

At the time of writing, Fabric.js is still in development. However, there are several alternatives to Fabric.js for mapping. Let’s take a look at a few.

Maptalks

By pitching and rotating 2D maps, the Maptalks flexible, open source library mixes 2D and 3D maps. Any preferred technology, such as CSS3, Canvas, and WebGL, can be used to enhance Maptalks. Go ahead and create your own; it’s easy and fun. Using Maptalks and plugins, you can render 10,000 geometries with Canvas 2D.

OpenLayers

OpenLayers is an open source, high-performance JavaScript framework that can be used to build interactive maps that employ a variety of mapping services. From a selection of map services, you can choose a tiled layer or a vector layer for the map layer source.

Because the tool is mobile-ready out of the box, it is appropriate for creating maps across platforms and browsers. With CSS, you can change how your map looks.


More great articles from LogRocket:


Raphael.js

A little JavaScript package called Raphael.js aims to simplify using vector graphics on the web. With the help of this library, you can quickly and easily create your own unique chart or image crop and rotate widget.

Raphael uses VML and the SVG W3C recommendations to produce graphics. As a result, each visual object you create also becomes a DOM object, enabling you to later attach or change JavaScript event handlers. Raphael’s mission is to provide an adapter that makes it easier to create vector graphics that are cross-browser compatible.

Kartograph.js

Kartograph is a framework for creating SVG maps that do not require any additional mapping services. Kartograph.js is a JavaScript library that allows you to create interactive maps using Kartograph SVG maps. It is built on Raphael and jQuery and gracefully degrades to IE7 and above.

Kartograph lacks a ready-to-use map collection, but it does support any SVG map and comes with a map-creation tool called Kartograph.py. This framework offers a fluid mapping experience, and the library documentation and API reference (integrated in a single page) make it simple and pleasant to set up interactive maps in your projects. However, Kartograph is not dependency-free; it requires Raphael.js for drawing and jQuery.

Leaflet

Leaflet is a free, open source JavaScript library for making dynamic, mobile-friendly maps. It’s a compact application that supports all major browsers and operating systems and has a ton of functionality, plugins, and an intuitive API. Here are a few Leaflet examples.
Leaflet is a fantastic option for mobile applications or other circumstances when load time or size is constrained due to its core library’s reasonable size. Additionally, Leaflet has a huge selection of plugins, so you can add nearly any feature that would be available with a larger mapping library.

Paper.js

Paper.js is an open source canvas-based programming environment for vector graphics. In addition to providing a robust collection of tools for creating and modifying vector graphics and Bezier curves, it offers a clean, well-designed programming interface. It also provides a Scene Graph / Document Object Model for vector graphics. Paper.js is built on Scriptographer, an environment for scripting Adobe Illustrator.

p5.js

The metaphor of a software sketchbook served as the foundation for the p5.js library’s comprehensive sketching features. However, this tool does not limit you to a simple sketching surface; instead, imagine your entire browser page as your sketch!

You may interact with HTML5 components including text, input, video, webcam, and sound by using the p5.js add-on libraries.

If you’re already familiar with the fundamentals of JavaScript or Processing, then p5.js may be a good option as it is an ongoing development interpretation of both languages.

Conclusion

Fabric.js is one of the best drawing libraries on the market at the time of writing. In this article, we learned how to use Fabric.js to wrap a complex library into an uncontrolled component of React. Hopefully, Fabric.js will implement other components as well.

We also reviewed several mapping alternatives to Fabric.js: Maptalks, OpenLayers, Raphael.js, Kartograph.js, leaflet, paper.js, and p5.js. I’ve used Fabric.js with great success in the past despite it being under development at the time of writing. Thanks for reading!

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Martin Kimani Innovative frontend developer with five years of experience building responsive websites. Proficient in HTML, CSS, and JavaScript, as well as modern libraries and frameworks. I'm passionate about usability and I possess a working knowledge of graphic design.

Leave a Reply