Creating Geometry Compass Using JavaScript

Introduction

Few weeks back, I was asked to look around for existing geometry drawing tools. While searching, I found numbers of dynamic geometric construction tools which allows  drawing. Most of those projects are desktop-based, and few are web-based. Most of the web-based are reusing existing platforms such as GeoGebra.

In my research, I was focusing on the ruler and the compass tools which can be reused. Unfortunately I didn’t came across a web-based tool which can reuse. There were some good stuff, which are not reusable. Since most of the other web-based systems reuse GeoGebra, so they have the same limitations that GeoGebra has. Almost all reusable web-based tools have an issue with their compass, which was not user-friendly.

Problem

Geometry compass in most web-based tools allows you to draw arcs in one direction. Mostly anti-clockwise direction. This uni-directional compass tools were annoying for me while drawing constructions. Searching furthermore, I found Geometra (a desktop-based tool) has provided a reasonable solution for that. The compass in Geometra allows drawing arcs both sides, but allows drawing only small-arcs (less than 180 degrees). In my implementation I thought of bringing Geometra’s approach to web-based environment.

Background

For this implementation I used HTML5 canvas, and JavaScript. To make the implementation easier I added fabricjs library. Though FabricJs makes it easy to use HTML5 canvas, it has the limitation of representing Arcs. Therefore I extended its circle class to cater the requirement. Furthermore I used Compass class to handle properties of compass and event.js for handling mouse events.

Implementation

In this section, I’ll go through files which I discussed earlier. “Arc.class.js” file extends Circle class of fabricJs. Then I’ve overridden the initialize, render and toSVG methods to suite the requirement.

Arc.class.js


fabric.Arc = fabric.util.createClass(fabric.Circle, {
	type: 'arc',

	counterclockwise: false,

	initialize: function (options) {
		this.counterclockwise = options.counterclockwise;
		this.callSuper('initialize', options);
	},

	_render: function (ctx, noTransform) {
		ctx.beginPath();
		ctx.arc(noTransform ? this.left + this.radius : 0,
		      noTransform ? this.top + this.radius : 0,
		      this.radius,
		      this.startAngle,
		      this.endAngle, this.counterclockwise);
		this._renderFill(ctx);
		this._renderStroke(ctx);
    },

    toSVG: function(reviver) {
    	var markup = [];

		var rx = this.left + this.radius * Math.cos(this.startAngle);
		var ry = this.top + this.radius * Math.sin(this.startAngle);

		var ex = this.left + this.radius * Math.cos(this.endAngle);
		var ey = this.top + this.radius * Math.sin(this.endAngle);

		var svgPath = '';
	    if (!this.counterclockwise) {
	    	svgPath += '<path d=\"M'+rx+','+ry+' A'+this.radius+','+this.radius+' 0 0,1 '+ex+','+ey+'\" style=\"'+this.getSvgStyles()+'\"/>';
	    } else {
	    	// Exchange starting and ending points when it's counterclockwise
	    	svgPath += '<path d=\"M'+ex+','+ey+' A'+this.radius+','+this.radius+' 0 0,1 '+rx+','+ry+'\" style=\"'+this.getSvgStyles()+'\"/>';
	    }

	    markup.push(svgPath);

    	return reviver ? reviver(markup.join('')) : markup.join('');
    }
});

The above Arc class provides generic support for drawing arcs, similar to Line and Circle classes, already comes with FabricJs. To use Arc class, I created compass JavaScript file. It has 3 public methods: redraw; which is handling mouse movements when drawing starts, complete; which concludes the drawing and toSVG; which gives out the SVG representation of the arc drawn.

Compass.js


function Compass (mouseStart) {
	// 'c' for center, 'r' for radius, 'e' for end
	this.cx = this.rx = this.ex = mouseStart.x;
	this.cy = this.ry = this.ey = mouseStart.y;
	
	this.radius = 0;

	var points = [this.cx, this.cy, this.rx, this.ry];

	this.radiusLine = new fabric.Line(points, {				
										    strokeWidth: 2,
										    fill: 'black',
										    stroke: 'black',
										    strokeDashArray: [6, 3],
										    selectable: false
										});

	fabricCanvas.add(this.radiusLine);

	this.textObj = new fabric.Text('0', {
									        fontFamily: 'Times_New_Roman',
									        left: this.x1,
									        top: this.y1,
									        fontSize: 20,
									        originX: 'center'
									    });

	fabricCanvas.add(this.textObj);

	this.status = 'radius';
}

Compass.prototype = {
	constructor : Compass,

	redraw : function (mouse) { 

		if (this.status == 'radius') {
			this.rx = mouse.x;
		 	this.ry = mouse.y;

		 	this.radiusLine.set({ x2: this.rx, y2: this.ry });

		 	var tmp = addDistanceLabel (this.textObj, {x: this.cx, y:this.cy}, {x:this.rx, y:this.ry});

		 	fabricCanvas.renderAll();

		} else if (this.status = 'end') {
			this.ex = mouse.x;
			this.ey = mouse.y;

			this.endAngle = this._getAngle({ x:this.ex, y:this.ey })


			var angleDiff = this.endAngle - this.startAngle;

			if ((-Math.PI * 2 < angleDiff) && (angleDiff < -Math.PI)) {
				this.counterclockwise = false;
			} else if ((-Math.PI < angleDiff) && (angleDiff < 0)) {
				this.counterclockwise = true;
			} else if ((0 < angleDiff) && (angleDiff < Math.PI)) {
				this.counterclockwise = false;
			} else if ((Math.PI < angleDiff) && (angleDiff < Math.PI * 2)) {
				this.counterclockwise = true;
			}

			this.fabricObj.set( {endAngle: this.endAngle, counterclockwise: this.counterclockwise} );

			fabricCanvas.renderAll();
		}

	},

	complete : function () {

		if (this.status == 'radius') {

			fabricCanvas.remove(this.radiusLine);

			fabricCanvas.remove(this.textObj);

			fabricCanvas.renderAll();

			this.radius = Math.sqrt( Math.pow((this.rx-this.cx), 2) + Math.pow((this.ry-this.cy), 2) );

			this.startAngle = this._getAngle({ x:this.rx, y:this.ry });

			this.fabricObj = new fabric.Arc({
										left: this.cx,
										top: this.cy,
										radius: this.radius,
										startAngle: this.startAngle,
										endAngle: this.startAngle,
										counterclockwise: false,
										fill: '',
										stroke: 'black',
										originX: 'center',
								        originY: 'center',
								        selectable: false,
								        strokeDashArray: [6, 3]
									});

			fabricCanvas.add(this.fabricObj);

			this.status = 'end';

		} else if (this.status = 'end') {

			this.fabricObj.set({ strokeDashArray: [] });

			fabricCanvas.renderAll();

			drawings.push(this);
		}		
		
	},

	toSVG : function () {
	    return this.fabricObj.toSVG();
	},

	_getAngle : function (point) {
		var angleRequired = 0;

		// gets the actual angle from center
		if ((point.x-this.cx) == 0) {
			// handling special cases
			if (this.cy > point.y) { 
				angleRequired = Math.PI/2;
			} else if (this.cy < point.y) {
				angleRequired = -Math.PI/2;
			}
		} else {
			// in general cases
			angleRequired = Math.atan ((point.y-this.cy) / (point.x-this.cx));

			if ((this.cy < point.y) && (angleRequired < 0)) { // handle 2nd quadrant
				angleRequired = Math.PI - Math.abs(angleRequired);
			} else if ((this.cy > point.y) && (angleRequired > 0)) { // handle 3rd quadrant
				angleRequired = Math.PI + Math.abs(angleRequired);
			} else if ((this.cy > point.y) && (angleRequired < 0)) { // handle 4th quadrant
				angleRequired = 2*Math.PI - Math.abs(angleRequired);
			}
		}

		return angleRequired;
	}
}

For handling mouse events, I’ve created event JavaScript file. Depending on mouse events, this event file calls methods belongs to compass.

events.js


var fabricCanvas = new fabric.Canvas('sheet', { selection: false });
var selectedTool = '';
var toolState = '';
var toolPreviousState = '';
var instruction = $('#instructionText');

var currentTool = null;


var compassSettings = $('#compass_settings');
var compassSettingsState = $('input[name=compass-state]');

$('input[name=tool]').click(function() {
   $('input[name=tool]').removeClass('active_tool');
   $(this).addClass('active_tool');
});

fabricCanvas.on('mouse:down', function(e) {

	// Get mouse coordinates
	var mousePointer = getMousePointer(fabricCanvas, e);

	switch(selectedTool) {

		case 'compass' :

			switch(toolState) {
				case 'center' :
					
					currentTool = new Compass(mousePointer); 
					
					instruction.text('Select Radius Point');
					toolPreviousState = toolState;
					toolState = 'radius';

				break;
				case 'radius' :

					// do radius logic here
					currentTool.complete();

					// change to next
					// currentTool.addPoint(mousePointer);

					toolPreviousState = toolState;

					instruction.text('Select Ending Point');						
					toolState = 'end';
					

				break;
				case 'end' :

					// do end logic here
					currentTool.complete();

					instruction.text('Select Center Point');
					toolPreviousState = '';
					toolState = 'center';

				break;
			}

		break;		
	}
}, false);

fabricCanvas.on('mouse:move', function(e) {
	var mousePointer = getMousePointer(fabricCanvas, e);

	switch(selectedTool) {
		case 'compass' :
			switch(toolState) {
				case 'radius':
				case 'end' :
					currentTool.redraw(mousePointer);
				break;
			}

		break;
	}

}, false);

function getMousePointer (canvas, evt) {
    var mouse = canvas.getPointer(evt.e);
    var x = (mouse.x);
    var y = (mouse.y);
    return {
        x: x,
        y: y
    };
}


function addDistanceLabel (lineObj, start, end) {
	// change text label
 	var textX = start.x + ((end.x - start.x) / 2);
 	var textY = start.y + ((end.y - start.y) / 2);

 	var distance = Math.sqrt( Math.pow((end.x-start.x), 2) + Math.pow((end.y-start.y), 2) );
 	distance = (distance / 50.0).toFixed(1); // make it centimeters

 	lineObj.set( {left: textX, top: textY } );
 	lineObj.setText(distance + ' cm');
}


function initTool (toolName) {

	compassSettings.hide();

	switch (toolName) {
		case 'compass' :
			selectedTool = 'compass';
			toolState = 'center';
			instruction.text('Select Center Point');

			compassSettings.show();

			break;
	}
}

Finally you need to include dependencies in index.html file, which is shown below.

index.html


<!DOCTYPE html>
<html>
	<head>
	    <title>Mathematical Constructions</title>
	    <meta charset="utf-8"/>
	</head>

	<body>
		<h3>Geometrical Construction Drawing</h3>
		<canvas id="sheet" style="left:10px;top:10px;bottom:10px; border:1px solid #000000;" height="550" width="1280"></canvas>
		<br/>
		<div id="instructionText">Click on Compass</div>
		<br/>
		<input type="button" name="tool" class="btn btn-default" onclick="initTool('compass')" value="Compass">

		<script type="text/javascript" src="./js/jquery-3.1.1.min.js"></script>
		<script type="text/javascript" src="./js/fabric.js"></script>
		<script type="text/javascript" src="./js/events.js"></script>

		<!-- Extended shapes -->
		<script type="text/javascript" src="./js/Arc.class.js"></script>

		<!-- Tools for drawing -->
		<script type="text/javascript" src="./js/Compass.js"></script>

		<style type="text/css">
			.active_tool {background-color: gray}
		</style>
	</body>
</html>

To use this, you need to open index.html file in a browser. Then click Compass button, then click center and radius points respectively, and click on ending of arc. It’ll draw arcs shown as below.

compass_1

compass_2

Conclusion

In this post I shared my experience on how a bi-directional geometric compass is implemented for a web-based environment. Hope this will be helpful for you.

Resources

[1] GeoGebra – https://www.geogebra.org/

[2] Geometra – https://sourceforge.net/projects/geometra/

[3] HTML5 Canvas – http://www.w3schools.com/html/html5_canvas.asp

[4] SVG Path representation for Arcs – https://developer.mozilla.org/en/docs/Web/SVG/Tutorial/Paths#Arcs

 

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s