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!
Table of contents
- What is Fabric.js?
- Populating objects on the canvas
- Creating the gradient of objects
- Building the grid system
- Implementing zoom and panning
- Adding annotations
- Conclusion
What is Fabric.js?
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(); }); } 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(); }
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; }
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 wrap a complex library into an uncontrolled component of React. Hopefully, Fabric.js will implement other components as well. I’m eager to check out from the comments if there’s a better alternative to Fabric.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!
The post Build indoor maps with Fabric.js and React appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/cfjQKge
via Read more