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.
<!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>
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; }
© 20072025 XoaX.net LLC. All rights reserved.