Canvas JavaScript

Line Through Two Points and Bisector

This JavaScript program demonstrates how to draw a line through two points on a 2d graph and the perpendicular bisector. The two points are randomly generated and shown in dark green with labeled coordinates. The line through the two points is drawn in green and the perpendicular bisector is drawn in red. The mouse can be used to pan the graph by pressing the left mouse button and moving the cursor or zoom the graph by using the mousewheel to zoom in and out at the cursor location.

LineAndPerpBisectorOfTwoPoints.html

<!DOCTYPE html>
<html>
  <head>
    <title>XoaX.net's Javascript</title>
    <script type="text/javascript" src="LineAndPerpBisectorOfTwoPoints.js"></script>
  </head>
  <body onload="Initialize()">
  	<div style="width:708px;height:622px;">
    	<canvas id="idCanvas" width="600" height ="600" style="background-color:#F0F0F0;float:left;"></canvas>
    	<div style="width:108px;height:600px;float:right;">
    		<input id="idHighY" type="text" size="9" style="width:100px;height:16px;"/>
    		<div style="width:108px;height:556px;"></div>
    		<input id="idLowY" type="text" size="9" style="width:100px;height:16px;"/>
    	</div>
    	<div style="width:600px;height:22px;float:left;">
    		<input id="idLowX" type="text" size="9" style="width:100px;height:16px;float:left;"/>
    		<div style="width:384px;height:22px;float:left;"></div>
    		<input id="idHighX" type="text" size="9" style="width:100px;height:16px;float:left;"/>    		
    	</div>
    	<div style="width:108px;height:22px;float:right;"></div>
    </div>
  </body>
</html>

LineAndPerpBisectorOfTwoPoints.js

class CPoint2D {
	constructor(dX, dY) {
		this.mdX = dX;
		this.mdY = dY;
	}
	Set(dX, dY) {
		this.mdX = dX;
		this.mdY = dY;
	}
	GetX() {
		return this.mdX;
	}
	GetY() {
		return this.mdY;
	}
	DistanceSquared(dP){
		var dDX = dP.mdX - this.mdX;
		var dDY = dP.mdY - this.mdY;
		return dDX*dDX + dDY*dDY;
	}
}
// Ax + By + C = 0
class CLine2D {
	constructor(dA, dB, dC) {
		this.mdA = dA;
		this.mdB = dB;
		this.mdC = dC;
	}
	CalculateXGivenY(dY){
		// x = -(By + C)/A
		return -(this.mdB*dY + this.mdC)/this.mdA;
	}
	CalculateYGivenX(dX){
		// y = -(Ax + C)/B
		return -(this.mdA*dX + this.mdC)/this.mdB;
	}
	// | x   y   z |
  // | x1  y1  1 | = 0
  // | x2  y2  1 |
  // The set of linearly dependent vectors in the plane defined by (x1, y1, 1), (x2, y2, 1)
  // Ax + By + C = 0, where A = y1 - y2, B = x2 - x1, C = x1*y2 - x2*y1
  // Get the line through 2 given points
	static GetIntersectingLine(qP1, qP2) {
		var dA = qP1.GetY() - qP2.GetY();
		var dB = qP2.GetX() - qP1.GetX();
		var dC = qP1.GetX()*qP2.GetY() - qP2.GetX()*qP1.GetY();
		return new CLine2D(dA, dB, dC);
	}
	static GetPerpendicularBisector(qP1, qP2) {
		var dMidX = (qP1.GetX() + qP2.GetX())/2.0;
		var dMidY = (qP1.GetY() + qP2.GetY())/2.0;
		// Get the vector from P1 to P2
		var dVx = qP2.GetX() - qP1.GetX();
		var dVy = qP2.GetY() - qP1.GetY();
		// Rotate the vector by pi/4 x' = -y, y' = x
		var dRotVx = -dVy;
		var dRotVy = dVx;
		// Add the rotation vector to the midpoint to get another point on the perpendicular bisetor
		var dP2X = dMidX + dRotVx;
		var dP2Y = dMidY + dRotVy;
		var dA = dMidY - dP2Y;
		var dB = dP2X - dMidX;
		var dC = dMidX*dP2Y - dP2X*dMidY;
		return new CLine2D(dA, dB, dC);
	}
}

class CTwoPoints {
	constructor(kqP1, kqP2) {
		this.mqP1 = kqP1;
		this.mqP2 = kqP2;
		this.mqLine = CLine2D.GetIntersectingLine(this.mqP1, this.mqP2);
		this.mqPerp = CLine2D.GetPerpendicularBisector(this.mqP1, this.mqP2);
	}
	DrawPointsAndLines(qContext2D, qGraphRange) {
		qContext2D.strokeStyle = "rgba(0, 255, 0, .5)";
		qContext2D.lineWidth = "1";
		// Draw the line through the two points
		var daaPixels = qGraphRange.FindLineBoundaryIntersectionsInPixels(this.mqLine);
		if (daaPixels != null) {
			qContext2D.beginPath();
			qContext2D.moveTo(daaPixels[0][0], daaPixels[0][1]);
			qContext2D.lineTo(daaPixels[1][0], daaPixels[1][1]);
			qContext2D.stroke();
			qContext2D.font = "20px Times";
			qContext2D.fillStyle = qContext2D.strokeStyle;
			qContext2D.fillText(this.mqLine.mdA.toFixed(2).replace(/\.?0+$/, "")
				+"x + "+this.mqLine.mdB.toFixed(2).replace(/\.?0+$/, "")+"y + "
				+this.mqLine.mdC.toFixed(2).replace(/\.?0+$/, ""), 0, 590);
		}
		// Draw the perpendicular bisector
		qContext2D.strokeStyle = "rgba(255, 0, 0, .5)";
		daaPixels = qGraphRange.FindLineBoundaryIntersectionsInPixels(this.mqPerp);
		if (daaPixels != null) {
			qContext2D.beginPath();
			qContext2D.moveTo(daaPixels[0][0], daaPixels[0][1]);
			qContext2D.lineTo(daaPixels[1][0], daaPixels[1][1]);
			qContext2D.stroke();
			qContext2D.font = "20px Times";
			qContext2D.fillStyle = qContext2D.strokeStyle;
			qContext2D.fillText(this.mqPerp.mdA.toFixed(2).replace(/\.?0+$/, "")
				+"x + "+this.mqPerp.mdB.toFixed(2).replace(/\.?0+$/, "")+"y + "
				+this.mqPerp.mdC.toFixed(2).replace(/\.?0+$/, ""), 400, 590);
		}
		// Draw the two points
		qContext2D.fillStyle = "rgb(0, 128, 0)";
		var dX = qGraphRange.XtoPixelX(this.mqP1.GetX());
		var dY = qGraphRange.YtoPixelY(this.mqP1.GetY());
		qContext2D.beginPath();
		qContext2D.arc(dX, dY, 4, 0, 2.0*Math.PI, true);
		qContext2D.fill();
		qContext2D.fillText("("+this.mqP1.GetX().toFixed(2).replace(/\.?0+$/, "")
				+", "+this.mqP1.GetY().toFixed(2).replace(/\.?0+$/, "")+")", dX-50, dY+20);
		dX = qGraphRange.XtoPixelX(this.mqP2.GetX());
		dY = qGraphRange.YtoPixelY(this.mqP2.GetY());
		qContext2D.beginPath();
		qContext2D.arc(dX, dY, 4, 0, 2.0*Math.PI, true);
		qContext2D.fill();
		qContext2D.fillText("("+this.mqP2.GetX().toFixed(2).replace(/\.?0+$/, "")
				+", "+this.mqP2.GetY().toFixed(2).replace(/\.?0+$/, "")+")", dX-50, dY+20);
	}
}

class GraphRange2D {
	constructor(dLowX, dHighX, dLowY, dHighY, iCanvasW, iCanvasH) {
		this.mdLowX = dLowX;
		this.mdHighX = dHighX;
		this.mdLowY = dLowY;
		this.mdHighY = dHighY;
		this.miCanvasW = iCanvasW;
		this.miCanvasH = iCanvasH;
	}
	Scale(dScale, dZoomPixelX, dZoomPixelY) {
		var dZoomX = gqGraphRange.PixelXToX(dZoomPixelX);
		var dZoomY = gqGraphRange.PixelYToY(dZoomPixelY);
		// Scale around the zoom center point, usually defined by the cursor
		this.mdLowX = dZoomX - (dZoomX - this.mdLowX)*dScale;
		this.mdHighX = dZoomX + (this.mdHighX - dZoomX)*dScale;
		this.mdLowY = dZoomY - (dZoomY - this.mdLowY)*dScale;
		this.mdHighY = dZoomY + (this.mdHighY - dZoomY)*dScale;
	}
	Pan(iDeltaPixelX, iDeltaPixelY) {
		// Get the pixel sizes
		var dPixelWidth = (this.mdHighX - this.mdLowX)/this.miCanvasW;
		var dPixelHeight = (this.mdHighY - this.mdLowY)/this.miCanvasH;
		this.mdLowX -= dPixelWidth*iDeltaPixelX;
		this.mdHighX -= dPixelWidth*iDeltaPixelX;
		this.mdLowY += dPixelHeight*iDeltaPixelY;
		this.mdHighY += dPixelHeight*iDeltaPixelY;
	}
	PixelW() {
		return (this.mdHighX - this.mdLowX)/this.miCanvasW;
	}
	PixelH() {
		return (this.mdHighY - this.mdLowY)/this.miCanvasH;
	}
	// The pixel x range is [-.5, CanvasW - .5]
	XtoPixelX(dX) {
		var dT = (dX - this.mdLowX)/(this.mdHighX - this.mdLowX);
		return (dT*this.miCanvasW - .5);
	}
	// The pixel y range is [-.5, CanvasH - .5], but the orientation is reversed
	YtoPixelY(dY) {
		var dT = (dY - this.mdLowY)/(this.mdHighY - this.mdLowY);
		return (1.0 - dT)*(this.miCanvasH - .5);
	}
	PixelXToX(dPixelX) {
		var dT = (dPixelX - .5)/(this.miCanvasW - 1.0);
		return (dT*(this.mdHighX - this.mdLowX) + this.mdLowX);
	}
	PixelYToY(dPixelY) {
		var dT = (dPixelY - .5)/(this.miCanvasH - 1.0);
		return ((1.0 - dT)*(this.mdHighY - this.mdLowY) + this.mdLowY);
	}
	// Check whether the point is within the bounds of the graph
	IsYInBounds(dY) {
		if ((dY >= this.mdLowY) && (dY <= this.mdHighY)) {
			return true;
		}
		return false;
	}
	IsPixelYInBounds(dPixelY) {
		if ((dPixelY >= 0) && (dPixelY <= this.miCanvasH - 1)) {
			return true;
		}
		return false;
	}
	
	// Find the smallest power greater than x
	FindSmallestGreaterPowerOfTwo(dX) {
		var dPow = 1.0;
		while (dPow < dX) {
			dPow *= 2;
		}
		if (dPow == 1.0) {
			while (dPow >= dX) {
				dPow /= 2;
			}
		}
		return dPow;
	}
	FindLeastMultipleBelow(dX, dLowX) {
		Math.floor(dLowX/dX)*dX;
	}
	
	DrawGridLinesAndAxes(qContext) {
		// Draw grid lines of two types
		// Get an estimate for a value that will be drawn about every 100 pixels and every 20
		var dThickFreqX = this.FindSmallestGreaterPowerOfTwo(this.PixelXToX(100) - this.PixelXToX(0));
		var dThinFreqX = dThickFreqX/4;
		var dThickFreqY = this.FindSmallestGreaterPowerOfTwo(this.PixelYToY(0) - this.PixelYToY(100));
		var dThinFreqY = dThickFreqY/4;

		// Draw the thin lines first, then the thick ones, and the axes.
		// Draw the thin lines first
		// Draw vertical grid lines
		// Find the least frequency value below the low x (greatest lower multiple GLM)
		var dGLM = Math.floor(this.mdLowX/dThickFreqX)*dThickFreqX;
		var dGridValue = dGLM + dThinFreqX;
		qContext.strokeStyle = "#E8E8E8";
		var i = 1;
		while (dGridValue < this.mdHighX) {
			if ((i % 4) != 0) {
				var dGridX = this.XtoPixelX(dGridValue);
				qContext.beginPath();
				qContext.moveTo(dGridX, 0);
				qContext.lineTo(dGridX, this.miCanvasH);
				qContext.stroke();
			}
			dGridValue += dThinFreqX;
			++i;
		}
		// Draw horizontal grid lines
		dGLM = Math.floor(this.mdLowY/dThickFreqY)*dThickFreqY;
		dGridValue = dGLM + dThinFreqY;
		i = 1;
		while (dGridValue < this.mdHighY) {
			if ((i % 4) != 0) {
				var dGridY = this.YtoPixelY(dGridValue);
				qContext.beginPath();
				qContext.moveTo(0, dGridY);
				qContext.lineTo(this.miCanvasW, dGridY);
				qContext.stroke();
			}
			dGridValue += dThinFreqY;
			++i;
		}
		
		// Draw the thick lines
		// Draw vertical grid lines
		qContext.strokeStyle = "#DDDDDD";
		dGLM = Math.floor(this.mdLowX/dThickFreqX)*dThickFreqX;
		dGridValue = dGLM + dThickFreqX;
		while (dGridValue < this.mdHighX) {
			var dGridX = this.XtoPixelX(dGridValue);
			qContext.beginPath();
			qContext.moveTo(dGridX, 0);
			qContext.lineTo(dGridX, this.miCanvasH);
			qContext.stroke();
			dGridValue += dThickFreqX;
		}
		// Draw horizontal grid lines
		dGLM = Math.floor(this.mdLowY/dThickFreqY)*dThickFreqY;
		dGridValue = dGLM + dThickFreqY;
		while (dGridValue < this.mdHighY) {
			var dGridY = this.YtoPixelY(dGridValue);
			qContext.beginPath();
			qContext.moveTo(0, dGridY);
			qContext.lineTo(this.miCanvasW, dGridY);
			qContext.stroke();
			dGridValue += dThickFreqY;
		}

		// Lastly, check whether zero is within each range in order to draw the axes.
		// Draw the Y-Axis
		qContext.strokeStyle = "gray";
		qContext.fillStyle = "gray";
		if (this.mdLowX <= 0 && this.mdHighX >= 0) {
			var dPixelX = this.XtoPixelX(0);
			DrawArrow(qContext, dPixelX, this.miCanvasH, dPixelX, 0);
		}
		// Draw the X-Axis
		if (this.mdLowY <= 0 && this.mdHighY >= 0) {
			var dPixelY = this.YtoPixelY(0);
			DrawArrow(qContext, 0, dPixelY, this.miCanvasW, dPixelY);
		}
	}
	
	FillInRanges(qLowX, qHighX, qLowY, qHighY) {
		// Remove any trailing zeroes and decimal points
		qLowX.value = this.mdLowX.toFixed(4).replace(/\.?0+$/, "");
		qHighX.value = this.mdHighX.toFixed(4).replace(/\.?0+$/, "");
		qLowY.value = this.mdLowY.toFixed(4).replace(/\.?0+$/, "");
		qHighY.value = this.mdHighY.toFixed(4).replace(/\.?0+$/, "");
	}
	// The line is in the coordinate space, not pixels
	FindLineBoundaryIntersectionsInPixels(qLine) {
		// Allocate the two pixel endpoints as array of 2 coordinates
		var qaPixels = new Array(2);
		// First pixel
		qaPixels[0] = new Array(2);
		// Second pixel
		qaPixels[1] = new Array(2);
		var iIntersectionCount = 0;
		// Get each intersection and check whether it is in bounds.
		var dLowX = this.mdLowX;
		var dHighX = this.mdHighX;
		var dLowY = this.mdLowY;
		var dHighY = this.mdHighY;
		var dX = qLine.CalculateXGivenY(dLowY);
		// If the x value is within bounds, the point is in the range and we will graph it
		if (dX >= dLowX && dX <= dHighX) {
			qaPixels[iIntersectionCount][0] = this.XtoPixelX(dX);
			qaPixels[iIntersectionCount][1] = this.YtoPixelY(dLowY);
			++iIntersectionCount;
		}
		dX = qLine.CalculateXGivenY(dHighY);
		if (dX >= dLowX && dX <= dHighX) {
			qaPixels[iIntersectionCount][0] = this.XtoPixelX(dX);
			qaPixels[iIntersectionCount][1] = this.YtoPixelY(dHighY);
			++iIntersectionCount;
		}
		// We should have at most 2 intersections unless the points are exactly at a corner, which must be the third and fourth points.
		var dY = qLine.CalculateYGivenX(dLowX);
		if (dY >= dLowY && dY <= dHighY && iIntersectionCount < 2) {
			qaPixels[iIntersectionCount][0] = this.XtoPixelX(dLowX);
			qaPixels[iIntersectionCount][1] = this.YtoPixelY(dY);
			++iIntersectionCount;
		}
		dY = qLine.CalculateYGivenX(dHighX);
		if (dY >= dLowY && dY <= dHighY && iIntersectionCount < 2) {
			qaPixels[iIntersectionCount][0] = this.XtoPixelX(dHighX);
			qaPixels[iIntersectionCount][1] = this.YtoPixelY(dY);
			++iIntersectionCount;
		}
		if (iIntersectionCount > 1) {
			return qaPixels;
		} else { // If there are no intersections or 1, return null
			return null;
		}
	}
}

var gqGraphRange = null;
var gqContext = null;
var gbIsDragging = false;
var giCursorX = 0;
var giCursorY = 0;
var gqTwoPoints = null;

function Initialize() {
	var qCanvas = document.getElementById("idCanvas");
	qCanvas.addEventListener("wheel", fnZoom, { passive: false });
	qCanvas.addEventListener("mousedown", MouseDown);
	qCanvas.addEventListener("mouseup", MouseUp);
	qCanvas.addEventListener("mousemove", MouseMove);
	gqContext = qCanvas.getContext("2d");
	
	// Get the canvas size
	var dLowX = -4.0;
	var dHighX = 4.0;
	var dLowY = -4.0;
	var dHighY = 4.0;
	gqGraphRange = new GraphRange2D(dLowX, dHighX, dLowY, dHighY, qCanvas.width, qCanvas.height);
	gqGraphRange.DrawGridLinesAndAxes(gqContext);
	
	var qLowX = document.getElementById("idLowX");
	var qHighX = document.getElementById("idHighX");
	var qLowY = document.getElementById("idLowY");
	var qHighY = document.getElementById("idHighY");
	gqGraphRange.FillInRanges(qLowX, qHighX, qLowY, qHighY);
	qCanvas.addEventListener("wheel", fnZoom, { passive: false });

	// Get the two points and draw their lines
	var dDeltaX = dHighX - dLowX;
	var dDeltaY = dHighY - dLowY;
	var dX = Math.random()*dDeltaX + dLowX;
	var dY = Math.random()*dDeltaY + dLowY;
	var qP1 = new CPoint2D(dX, dY);
	dX = Math.random()*dDeltaX + dLowX;
	dY = Math.random()*dDeltaY + dLowY;
	var qP2 = new CPoint2D(dX, dY);
	gqTwoPoints = new CTwoPoints(qP1, qP2);
	// Draw
	gqTwoPoints.DrawPointsAndLines(gqContext, gqGraphRange);
}

function DrawArrow(qContext, dX1, dY1, dX2, dY2) {
	// Draw the line portion
	qContext.beginPath();
	qContext.moveTo(dX1, dY1);
	qContext.lineTo(dX2, dY2);
	qContext.stroke();
	// Draw the head portion
	DrawArrowhead(qContext, dX1, dY1, dX2, dY2);
}

function DrawArrowhead(qContext, dX1, dY1, dX2, dY2) {
	var dTheta = Math.atan2(dY2-dY1,dX2-dX1);
	var dThicknessAngle = Math.PI/6;
	var dHeadLength = 10;
	qContext.beginPath();
	qContext.moveTo(dX2, dY2);
	qContext.lineTo(dX2 - dHeadLength*Math.cos(dTheta-dThicknessAngle), dY2 - dHeadLength*Math.sin(dTheta-dThicknessAngle));
	qContext.lineTo(dX2 - dHeadLength*Math.cos(dTheta+dThicknessAngle), dY2 - dHeadLength*Math.sin(dTheta+dThicknessAngle));
	qContext.closePath();
	qContext.fill();
}

function fnZoom(e) {
  e.preventDefault();
  var dFactor = (e.deltaY > 0.0) ? 1.1: .9090909090909;
  SetCursorCoordinates(e);
  // Zoom centered on the pixel location
  gqGraphRange.Scale(dFactor, giCursorX, giCursorY);
  
  // Draw the graph
  gqContext.clearRect(0, 0, 600, 600);
  gqGraphRange.DrawGridLinesAndAxes(gqContext);
	gqTwoPoints.DrawPointsAndLines(gqContext, gqGraphRange);

	var qLowX = document.getElementById("idLowX");
	var qHighX = document.getElementById("idHighX");
	var qLowY = document.getElementById("idLowY");
	var qHighY = document.getElementById("idHighY");
	gqGraphRange.FillInRanges(qLowX, qHighX, qLowY, qHighY);
}

function MouseMove(e) {
	if (gbIsDragging) {
		var iLastCursorX = giCursorX;
		var iLastCursorY = giCursorY;
		SetCursorCoordinates(e);
		var iCurrCursorX = giCursorX;
		var iCurrCursorY = giCursorY;
		gqGraphRange.Pan(iCurrCursorX - iLastCursorX, iCurrCursorY - iLastCursorY);
		
		// Draw the graph
		gqContext.clearRect(0, 0, 600, 600);
		gqGraphRange.DrawGridLinesAndAxes(gqContext);
		gqTwoPoints.DrawPointsAndLines(gqContext, gqGraphRange);
		
		var qLowX = document.getElementById("idLowX");
		var qHighX = document.getElementById("idHighX");
		var qLowY = document.getElementById("idLowY");
		var qHighY = document.getElementById("idHighY");
		gqGraphRange.FillInRanges(qLowX, qHighX, qLowY, qHighY);
	}
}

function MouseDown(e) {
	SetCursorCoordinates(e);
	gbIsDragging = true;
}

function MouseUp(e) {
	gbIsDragging = false;
}

function SetCursorCoordinates(e) {
	var dElementOffsetX = 0;
	var dElementOffsetY = 0;
	// Get the element that triggered the event
	var qElement = e.target;

	do{
		dElementOffsetX += qElement.offsetLeft - qElement.scrollLeft;
		dElementOffsetY += qElement.offsetTop - qElement.scrollTop;
		qElement = qElement.offsetParent;
	} while(qElement != document.body)
	
	giCursorX = e.pageX - dElementOffsetX;
	giCursorY = e.pageY - dElementOffsetY;
}
 

Output

 
 

© 2007–2025 XoaX.net LLC. All rights reserved.