How to build a polygon drawing tool as a functional react component?

Viewed 1028

So I'm currently working on a project that requires similar functionality to what you would expect in a floorplan designing application, and I need to be able to essentially draw a polygon (room) by connecting points generated on-click, with lines (walls).

I've found a few really great examples that resemble the functionality I'm looking for, but I'm unsure of how to approach implementing it as a functional react component.

I've seen Fabric.js and Konva.js used in a few other implementations of this kind of functionality, but I can't seem to be able to find a react example to use as a reference..

Below is an example I found on codepen using jQuery

var min = 99;
var max = 999999;
var polygonMode = true;
var pointArray = new Array();
var lineArray = new Array();
var activeLine;
var activeShape = false;
var canvas
$(window).load(function() {
  prototypefabric.initCanvas();
  $('#create-polygon').click(function() {
    prototypefabric.polygon.drawPolygon();
  });
});
var prototypefabric = new function() {
  this.initCanvas = function() {
    canvas = window._canvas = new fabric.Canvas('c');
    canvas.setWidth($(window).width());
    canvas.setHeight($(window).height() - $('#nav-bar').height());
    //canvas.selection = false;

    canvas.on('mouse:down', function(options) {
      if (options.target && options.target.id == pointArray[0].id) {
        prototypefabric.polygon.generatePolygon(pointArray);
      }
      if (polygonMode) {
        prototypefabric.polygon.addPoint(options);
      }
    });
    canvas.on('mouse:up', function(options) {

    });
    canvas.on('mouse:move', function(options) {
      if (activeLine && activeLine.class == "line") {
        var pointer = canvas.getPointer(options.e);
        activeLine.set({
          x2: pointer.x,
          y2: pointer.y
        });

        var points = activeShape.get("points");
        points[pointArray.length] = {
          x: pointer.x,
          y: pointer.y
        }
        activeShape.set({
          points: points
        });
        canvas.renderAll();
      }
      canvas.renderAll();
    });
  };
};



prototypefabric.polygon = {
  drawPolygon: function() {
    polygonMode = true;
    pointArray = new Array();
    lineArray = new Array();
    activeLine;
  },
  addPoint: function(options) {
    var random = Math.floor(Math.random() * (max - min + 1)) + min;
    var id = new Date().getTime() + random;
    var circle = new fabric.Circle({
      radius: 5,
      fill: '#ffffff',
      stroke: '#333333',
      strokeWidth: 0.5,
      left: (options.e.layerX / canvas.getZoom()),
      top: (options.e.layerY / canvas.getZoom()),
      selectable: false,
      hasBorders: false,
      hasControls: false,
      originX: 'center',
      originY: 'center',
      id: id,
      objectCaching: false
    });
    if (pointArray.length == 0) {
      circle.set({
        fill: 'red'
      })
    }
    var points = [(options.e.layerX / canvas.getZoom()), (options.e.layerY / canvas.getZoom()), (options.e.layerX / canvas.getZoom()), (options.e.layerY / canvas.getZoom())];
    line = new fabric.Line(points, {
      strokeWidth: 2,
      fill: '#999999',
      stroke: '#999999',
      class: 'line',
      originX: 'center',
      originY: 'center',
      selectable: false,
      hasBorders: false,
      hasControls: false,
      evented: false,
      objectCaching: false
    });
    if (activeShape) {
      var pos = canvas.getPointer(options.e);
      var points = activeShape.get("points");
      points.push({
        x: pos.x,
        y: pos.y
      });
      var polygon = new fabric.Polygon(points, {
        stroke: '#333333',
        strokeWidth: 1,
        fill: '#cccccc',
        opacity: 0.3,
        selectable: false,
        hasBorders: false,
        hasControls: false,
        evented: false,
        objectCaching: false
      });
      canvas.remove(activeShape);
      canvas.add(polygon);
      activeShape = polygon;
      canvas.renderAll();
    } else {
      var polyPoint = [{
        x: (options.e.layerX / canvas.getZoom()),
        y: (options.e.layerY / canvas.getZoom())
      }];
      var polygon = new fabric.Polygon(polyPoint, {
        stroke: '#333333',
        strokeWidth: 1,
        fill: '#cccccc',
        opacity: 0.3,
        selectable: false,
        hasBorders: false,
        hasControls: false,
        evented: false,
        objectCaching: false
      });
      activeShape = polygon;
      canvas.add(polygon);
    }
    activeLine = line;

    pointArray.push(circle);
    lineArray.push(line);

    canvas.add(line);
    canvas.add(circle);
    canvas.selection = false;
  },
  generatePolygon: function(pointArray) {
    var points = new Array();
    $.each(pointArray, function(index, point) {
      points.push({
        x: point.left,
        y: point.top
      });
      canvas.remove(point);
    });
    $.each(lineArray, function(index, line) {
      canvas.remove(line);
    });
    canvas.remove(activeShape).remove(activeLine);
    var polygon = new fabric.Polygon(points, {
      stroke: '#333333',
      strokeWidth: 0.5,
      fill: 'red',
      opacity: 1,
      hasBorders: false,
      hasControls: false
    });
    canvas.add(polygon);

    activeLine = null;
    activeShape = null;
    polygonMode = false;
    canvas.selection = true;
  }
};
* {
  font-family: "Roboto", sans-serif;
  font-weight: 100;
}

body {
  overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.2.0/fabric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" rel="stylesheet" />

<nav class="grey darken-4" role="navigation" id="nav-bar" style="height:50px;">
  <div class="nav-wrapper container">
    <a id="logo-container" class="brand-logo" style="line-height: 50px;font-size: 20px;">Fabric Polygon</a>
    <ul id="nav-mobile" class="right hide-on-med-and-down">
      <li><a id="create-polygon" style="line-height: 50px;font-size: 20px;cursor:pointer;">Create Polygon</a></li>
    </ul>
  </div>
</nav>
<div class="section no-pad-bot no-pad-top">
  <canvas id="c"></canvas>
</div>

And this is essentially the end functionality I'd like to achieve.

enter image description here

I really appreciate any help or advice!

1 Answers

To begin with, you should keep in mind that react is meant for structuring the visual components of your application and their interaction with the rest of your code, that is to say react is not a framework that covers everything you may need.

I'm not even sure that react is what you need, maybe you need a canvas. Maybe the canvas resides in a react component.

Nevertheless, if you decide that you would like to go at it with react then lets break down what you need, visually:

  • a "canvas" component which contains all your polygons or lines
  • a "polygon" which contains lines
  • a "line" component.

to begin with, before the interaction, make sure you create a state that makes sense that will be sent to the canvas, maybe an array of polygon or line objects. a line object is clearly two points, maybe a color, etc.

If you got this far with a mock of your polgyon state, its time to add a state manager (like redux) and some event listeners to update the state when events happen in the DOM (like mouse or keyboard events.)

To summarize:

  • Use react for visual elements.
  • Use redux to manage the state of the canvas.
  • When initializing the parent component (the "canvas"), create event listeners that fire actions that affect the state.
Related