This JavaScript program demonstrates how to draw an antialiased polynomial function and its derivative on a grid of a 2D graph with adaptive scaling. 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. This can be used as the basis for a 2D graph control.
<!DOCTYPE html> <html> <head> <title>XoaX.net's Javascript</title> <script type="text/javascript" src="CAffineTransformation2D.js"></script> <script type="text/javascript" src="CGraphOnImage2D.js"></script> <script type="text/javascript" src="CCanvasGraph2D.js"></script> <script type="text/javascript" src="Main.js"></script> </head> <body onload="Initialize()"> </body> </html>
// Scaling followed by a Translation // [ Sx 0 Tx ] [ 1 0 Tx ][ Sx 0 0 ] // [ 0 Sy Ty ] = [ 0 1 Ty ][ 0 Sy 0 ] // [ 0 0 1 ] [ 0 0 1 ][ 0 0 1 ] // Inverse // [ 1/Sx 0 -Tx/Sx ] [1/Sx 0 0 ][ 1 0 -Tx ] // [ 0 1/Sy -Ty/Sy ] = [ 0 1/Sy 0 ][ 0 1 -Ty ] // [ 0 0 1 ] [ 0 0 1 ][ 0 0 1 ] class CAffineTransformation2D { constructor(dSx, dSy, dTx, dTy) { this.mdaM = [dSx, 0.0, dTx, 0.0, dSy, dTy]; } Transform(daInP) { let mdaOutP = [0.0, 0.0]; mdaOutP[0] = this.mdaM[0]*daInP[0] + this.mdaM[2]; mdaOutP[1] = this.mdaM[4]*daInP[1] + this.mdaM[5]; return mdaOutP; } Invert(daInP) { let mdaOutP = [0.0, 0.0]; mdaOutP[0] = daInP[0]/this.mdaM[0] - this.mdaM[2]/this.mdaM[0]; mdaOutP[1] = daInP[1]/this.mdaM[4] - this.mdaM[5]/this.mdaM[4]; return mdaOutP; } // Scale the transformation and scale around the center point. This means that the translation must be adjusted ScaleAround(dScaleFactor, daCenter) { this.mdaM[0] *= dScaleFactor; this.mdaM[4] *= dScaleFactor; // This is LowX = CX - (CX - LowX)*ScalingFactor = CX*(1 - dScaleFactor) + LowX*dScaleFactor this.mdaM[2] = daCenter[0] - (daCenter[0] - this.mdaM[2])*dScaleFactor; // This is LowY = CY - (CY - LowY)*ScalingFactor = CY*(1 - dScaleFactor) + LowY*dScaleFactor this.mdaM[5] = daCenter[1] - (daCenter[1] - this.mdaM[5])*dScaleFactor; } TranslateBy(daDelta) { this.mdaM[2] += daDelta[0]; this.mdaM[5] += daDelta[1]; } }
// Use the affine transformation to transform from spatial coordinates to pixels class CGraphOnImage2D { constructor(dLowX, dHighX, dLowY, dHighY, qCanvas) { this.mqCanvas = qCanvas; let iCanvasW = qCanvas.width; let iCanvasH = qCanvas.height; let dScaleX = (dHighX - dLowX)/iCanvasW; // The y-axis is flipped let dScaleY = -(dHighY - dLowY)/iCanvasH; // Add .5 for the pixels to get the locations correct for half pixel offsets // [ Sx 0 (Tx + .5Sx) ] [ 1 0 Tx ][ Sx 0 0 ][ 1 0 .5 ] // [ 0 Sy (Ty + .5Sy) ] = [ 0 1 Ty ][ 0 Sy 0 ][ 0 1 .5 ] // [ 0 0 1 ] [ 0 0 1 ][ 0 0 1 ][ 0 0 1 ] this.mqPixelToPoint = new CAffineTransformation2D(dScaleX, dScaleY, dLowX + .5*dScaleX, dHighY + .5*dScaleY); } // Use this to scale the space for mouse events Zoom(dScaleFactor, daPixel) { // Transform the pixel to spatial coordinates let daCenter = this.mqPixelToPoint.Transform(daPixel); this.mqPixelToPoint.ScaleAround(dScaleFactor, daCenter); } Pan(daDeltaPixels) { // Get the x and y sizes of a single pixel in spatial coordiantes let daOriginImage = this.mqPixelToPoint.Transform([0.0, 0.0]); let daOneOneImage = this.mqPixelToPoint.Transform([1.0, 1.0]); let dPixelWidth = (daOneOneImage[0] - daOriginImage[0]); let dPixelHeight = (daOneOneImage[1] - daOriginImage[1]); // Adjust the transformation this.mqPixelToPoint.TranslateBy([daDeltaPixels[0]*dPixelWidth, daDeltaPixels[1]*dPixelHeight]); } LowerLeft() { return this.mqPixelToPoint.Transform([-.5, this.mqCanvas.height -.5]); } UpperRight() { return this.mqPixelToPoint.Transform([this.mqCanvas.width - .5, -.5]); } // 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; } async DrawGraphOver(qContext, fnF, iSamplesPerPixelX, iSamplesPerPixelY, dLineWidth, iR, iG, iB) { let iCanvasW = this.mqCanvas.width; let iCanvasH = this.mqCanvas.height; const kdR = dLineWidth; // Create two arrays of values for the lowest and highest x values colored at each subsample const kiTotalSamplesX = iCanvasW*iSamplesPerPixelX; const kiTotalSamplesY = iCanvasH*iSamplesPerPixelY; var daLowX = new Array(kiTotalSamplesX); var daHighX = new Array(kiTotalSamplesX); // Initialize the lowest values to infinity and the highest values to negative infinity for (var i = 0; i < kiTotalSamplesX; ++i) { daLowX[i] = kiTotalSamplesY; daHighX[i] = 0; } let dSampleWidth = 1/iSamplesPerPixelX; let dSampleHeight = 1/iSamplesPerPixelY; let iSamplesPerRadiusX = Math.floor(kdR/dSampleWidth); // Initialize the lowest values to infinity and the highest values to negative infinity for (var i = 0; i < kiTotalSamplesX; ++i) { // Take the x-value in pixel coordinates let dPixelX = (i + .5)*dSampleWidth; // Convert it to the coordinate space as the center of the pixel circle let daC = this.mqPixelToPoint.Transform([dPixelX, 0.0]); let dCx = daC[0];//this.PixelToCoordX(dPixelX); let dCy = fnF(dCx); let daPixel = this.mqPixelToPoint.Invert([dCx, dCy]);//this.CoordToPixelY(dCy); let dPixelY = daPixel[1]; if (dPixelY > -4*kdR && dPixelY < iCanvasH + 4*kdR) { // The circle is (y - Cy)^2 + (x - Cx)^2 - r^2 = 0, where r = line width let dMinIndexX = ((i - iSamplesPerRadiusX) > 0) ? (i - iSamplesPerRadiusX) : 0; let dMaxIndexX = ((i + iSamplesPerRadiusX) < kiTotalSamplesX - 1) ? (i + iSamplesPerRadiusX) : (kiTotalSamplesX - 1); // Loop over the x subsample range for (let j = dMinIndexX; j <= dMaxIndexX; ++j) { // Get the low and high y values for each x subsample let dCurrX = (j + .5)*dSampleWidth; // Calculate the lowest and highest y values for each subpixel x value // y = Cy +- sqrt(r^2 - (kdX - Cx)^2) let dLowY = dPixelY - Math.sqrt(kdR*kdR - (dCurrX - dPixelX)*(dCurrX - dPixelX)); let dHighY = dPixelY + Math.sqrt(kdR*kdR - (dCurrX - dPixelX)*(dCurrX - dPixelX)); let iLowSubpixelY = Math.floor((dLowY - dSampleHeight*.5)/dSampleHeight); let iHighSubpixelY = Math.ceil((dHighY - dSampleHeight*.5)/dSampleHeight); // Make sure that we have range of values before we bother to add them if (iLowSubpixelY <= iHighSubpixelY) { if (iLowSubpixelY < daLowX[j]) { daLowX[j] = iLowSubpixelY; } if (iHighSubpixelY > daHighX[j]) { daHighX[j] = iHighSubpixelY; } } } } } // Create an alternative image array of samples to fill and initialize all of the values to zero. // Fill this array as we check the subsamples that are within a pixel by going through the array let daaPercentFull = new Array(iCanvasW); for (let i = 0; i < iCanvasW; ++i) { daaPercentFull[i] = new Array(iCanvasH); for (let j = 0; j < iCanvasH; ++j) { daaPercentFull[i][j] = 0.0; } } // Fill each pixel value according to the high and low subsamples const kiSamplesPerPixel = iSamplesPerPixelX*iSamplesPerPixelY; for (var i = 0; i < kiTotalSamplesX; ++i) { let iPixelX = Math.floor(i/iSamplesPerPixelX); for (let j = daLowX[i]; j <= daHighX[i]; ++j) { let iPixelY = Math.floor(j/iSamplesPerPixelY); daaPercentFull[iPixelX][iPixelY] += (1.0/kiSamplesPerPixel); } } // Draw the pixel for the graph var qImageData = qContext.createImageData(iCanvasW, iCanvasH); for (let i = 0; i < iCanvasW; ++i) { for (let j = 0; j < iCanvasH; ++j) { let iIndex = (j*iCanvasW + i)*4; qImageData.data[iIndex] = iR; qImageData.data[iIndex+1] = iG; qImageData.data[iIndex+2] = iB; qImageData.data[iIndex+3] = daaPercentFull[i][j]*255; } } const qBitmap = await createImageBitmap(qImageData); qContext.drawImage(qBitmap, 0, 0); } DrawGridLinesAndAxes() { let daLowerLeft = this.LowerLeft(); let daUpperRight = this.UpperRight(); let qContext = this.mqCanvas.getContext("2d"); // This for calculating a range for grid spacing let daZero = this.mqPixelToPoint.Transform([0.0, 0.0]); let daHundred = this.mqPixelToPoint.Transform([100.0, 100.0]); // Draw grid lines of two types // Get an estimate for a value that will be drawn about every 100 pixels and every 25 var dThickFreqX = this.FindSmallestGreaterPowerOfTwo(Math.abs(daHundred[0] - daZero[0])); var dThinFreqX = dThickFreqX/4; // Reverse the order for the y-coordinates var dThickFreqY = this.FindSmallestGreaterPowerOfTwo(Math.abs(daZero[1] - daHundred[1])); 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(daLowerLeft[0]/dThickFreqX)*dThickFreqX; var dGridValue = dGLM + dThinFreqX; qContext.strokeStyle = "#E8E8E8"; var i = 1; while (dGridValue < daUpperRight[0]) { if ((i % 4) != 0) { let daGridPt = this.mqPixelToPoint.Invert([dGridValue, 0]); qContext.beginPath(); qContext.moveTo(daGridPt[0], 0); qContext.lineTo(daGridPt[0], this.mqCanvas.width); qContext.stroke(); } dGridValue += dThinFreqX; ++i; } // Draw horizontal grid lines dGLM = Math.floor(daLowerLeft[1]/dThickFreqY)*dThickFreqY; dGridValue = dGLM + dThinFreqY; i = 1; while (dGridValue < daUpperRight[1]) { if ((i % 4) != 0) { let daGridPt = this.mqPixelToPoint.Invert([0, dGridValue]); qContext.beginPath(); qContext.moveTo(0, daGridPt[1]); qContext.lineTo(this.mqCanvas.width, daGridPt[1]); qContext.stroke(); } dGridValue += dThinFreqY; ++i; } // Draw the thick lines // Draw vertical grid lines qContext.strokeStyle = "#DDDDDD"; dGLM = Math.floor(daLowerLeft[0]/dThickFreqX)*dThickFreqX; dGridValue = dGLM + dThickFreqX; while (dGridValue < daUpperRight[0]) { let daGridPt = this.mqPixelToPoint.Invert([dGridValue, 0]); qContext.beginPath(); qContext.moveTo(daGridPt[0], 0); qContext.lineTo(daGridPt[0], this.mqCanvas.height); qContext.stroke(); dGridValue += dThickFreqX; } // Draw horizontal grid lines dGLM = Math.floor(daLowerLeft[1]/dThickFreqY)*dThickFreqY; dGridValue = dGLM + dThickFreqY; while (dGridValue < daUpperRight[1]) { let daGridPt = this.mqPixelToPoint.Invert([0, dGridValue]); qContext.beginPath(); qContext.moveTo(0, daGridPt[1]); qContext.lineTo(this.mqCanvas.width, daGridPt[1]); 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"; // Find the pixel location of the origin var daOriginPixel = this.mqPixelToPoint.Invert([0, 0]); if (-.5 <= daOriginPixel[0] && (this.mqCanvas.width - .5) >= daOriginPixel[0]) { this.DrawArrow(qContext, daOriginPixel[0], this.mqCanvas.height, daOriginPixel[0], 0); } // Draw the X-Axis if (-.5 <= daOriginPixel[1] && (this.mqCanvas.height - .5) >= daOriginPixel[1]) { this.DrawArrow(qContext, 0, daOriginPixel[1], this.mqCanvas.width, daOriginPixel[1]); } } // Arrow drawing function to draw the axes 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 this.DrawArrowhead(qContext, dX1, dY1, dX2, dY2); } DrawArrowhead(qContext, dX1, dY1, dX2, dY2) { // The angle direction of the arrow 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(); } }
// This is the external container for the HTML elements class CCanvasGraph2D { constructor(iCanvasWidth, iCanvasHeight) { // Get the body element and create the other elements let qBodyElement = document.getElementsByTagName("body")[0]; let qInstructions = document.createElement("p"); qInstructions.innerHTML = "Hold the left mouse button and move the mouse to pan. Use the mouse wheel to zoom in and out at the cursor."; qBodyElement.appendChild(qInstructions); // Create the external div with width = (iCanvasWidth + 108) and height = (iCanvasHeight + 22) and add it to the body let qContainerDiv = document.createElement("div"); qContainerDiv.style.width = (iCanvasWidth + 108) + "px"; qContainerDiv.style.height = (iCanvasHeight + 22) + "px"; qBodyElement.appendChild(qContainerDiv); // Add a div for the y-range labels let qLabelYDiv = document.createElement("div"); qLabelYDiv.style.width = "108px"; qLabelYDiv.style.height = iCanvasHeight + "px"; qLabelYDiv.style.float = "left"; qContainerDiv.appendChild(qLabelYDiv); this.mqHighY = document.createElement("input"); this.mqHighY.style.width = "100px"; this.mqHighY.style.height = "16px"; this.mqHighY.id = "idHighY"; this.mqHighY.type = "text"; this.mqHighY.size = "9"; qLabelYDiv.appendChild(this.mqHighY); let qSpaceYDiv = document.createElement("div"); qSpaceYDiv.style.width = "108px"; qSpaceYDiv.style.height = (iCanvasHeight - 44) + "px"; qSpaceYDiv.style.float = "left"; qLabelYDiv.appendChild(qSpaceYDiv); this.mqLowY = document.createElement("input"); this.mqLowY.style.width = "100px"; this.mqLowY.style.height = "16px"; this.mqLowY.id = "idLowY"; this.mqLowY.type = "text"; this.mqLowY.size = "9"; qLabelYDiv.appendChild(this.mqLowY); // Create the canvas and add it to that div let qCanvas = document.createElement("canvas"); qCanvas.id = "idCanvas"; qCanvas.width = iCanvasWidth + ""; qCanvas.height = iCanvasHeight + ""; qCanvas.style.backgroundColor = "#F0F0F0"; qCanvas.style.float = "left"; qContainerDiv.appendChild(qCanvas); // Add a div for the x-range labels let qLabelXDiv = document.createElement("div"); qLabelXDiv.style.width = iCanvasWidth + "px"; qLabelXDiv.style.height = "22px"; qLabelXDiv.style.float = "right"; qContainerDiv.appendChild(qLabelXDiv); this.mqLowX = document.createElement("input"); this.mqLowX.style.width = "100px"; this.mqLowX.style.height = "16px"; this.mqLowX.style.float = "left"; this.mqLowX.id = "idLowX"; this.mqLowX.type = "text"; this.mqLowX.size = "9"; qLabelXDiv.appendChild(this.mqLowX); let qSpaceXDiv = document.createElement("div"); qSpaceXDiv.style.width = (iCanvasWidth - 216) + "px"; qSpaceXDiv.style.height = "22px"; qSpaceXDiv.style.float = "left"; qLabelXDiv.appendChild(qSpaceXDiv); this.mqHighX = document.createElement("input"); this.mqHighX.style.width = "100px"; this.mqHighX.style.height = "16px"; this.mqHighX.id = "idHighX"; this.mqHighX.type = "text"; this.mqHighX.size = "9"; qLabelXDiv.appendChild(this.mqHighX); // Create the graphing object this.mqGraph2D = new CGraphOnImage2D(-4.0, 4.0, -4.0, 4.0, qCanvas); } DrawTheGraphAndBounds() { let qCanvas = document.getElementById("idCanvas"); let qContext = qCanvas.getContext("2d"); qContext.clearRect(0, 0, qCanvas.width, qCanvas.height); this.mqGraph2D.DrawGridLinesAndAxes(); this.mqGraph2D.DrawGraphOver(qContext, Polynomial, 16, 16, 1, 128, 255, 128); this.mqGraph2D.DrawGraphOver(qContext, PolyDeriative, 16, 16, 1, 255, 128, 128); // Fill in the range bounds let daLowerLeft = this.mqGraph2D.LowerLeft(); let daUpperRight = this.mqGraph2D.UpperRight(); // Remove any trailing zeroes and decimal points this.mqLowX.value = daLowerLeft[0].toFixed(4).replace(/\.?0+$/, ""); this.mqHighX.value = daUpperRight[0].toFixed(4).replace(/\.?0+$/, ""); this.mqLowY.value = daLowerLeft[1].toFixed(4).replace(/\.?0+$/, ""); this.mqHighY.value = daUpperRight[1].toFixed(4).replace(/\.?0+$/, ""); } GetGraph() { return this.mqGraph2D; } } function Polynomial(dX) { return (dX - 1.5)*(dX - 1)*(dX)*(dX + 1)*(dX + 1.5); } function PolyDeriative(dX) { return (dX - 1)*(dX)*(dX + 1)*(dX + 1.5) + (dX - 1.5)*(dX)*(dX + 1)*(dX + 1.5) + (dX - 1.5)*(dX - 1)*(dX + 1)*(dX + 1.5) + (dX - 1.5)*(dX - 1)*(dX)*(dX + 1.5) + (dX - 1.5)*(dX - 1)*(dX)*(dX + 1); }
// Global variables var gqCanvasGraph = null; var gbIsDragging = false; var giCursorX = 0; var giCursorY = 0; function Initialize() { gqCanvasGraph = new CCanvasGraph2D(600, 600); let qCanvas = document.getElementById("idCanvas"); // With the elements created, add the event listeners to the canvas qCanvas.addEventListener("wheel", fnZoom, { passive: false }); qCanvas.addEventListener("mousedown", MouseDown); qCanvas.addEventListener("mouseup", MouseUp); qCanvas.addEventListener("mousemove", MouseMove); gqCanvasGraph.DrawTheGraphAndBounds(); } // Global mouse event handler functions function fnZoom(e) { e.preventDefault(); var dFactor = (e.deltaY > 0.0) ? 1.1: .9090909090909; SetCursorCoordinates(e); // Zoom centered on the pixel location gqCanvasGraph.GetGraph().Zoom(dFactor, [giCursorX, giCursorY]); gqCanvasGraph.DrawTheGraphAndBounds(); } function MouseMove(e) { if (gbIsDragging) { var iLastCursorX = giCursorX; var iLastCursorY = giCursorY; SetCursorCoordinates(e); var iCurrCursorX = giCursorX; var iCurrCursorY = giCursorY; gqCanvasGraph.GetGraph().Pan([iLastCursorX - iCurrCursorX, iLastCursorY - iCurrCursorY]); gqCanvasGraph.DrawTheGraphAndBounds(); } } 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.