Canvas JavaScript

Anti-Aliased Graphs

This JavaScript program demonstrates how to draw a 2d graph of a function with anti-aliased subsampling.

AntialiasedGraphs.html

<!DOCTYPE html>
<html>
  <head>
    <title>XoaX.net's Javascript</title>
    <script type="text/javascript" src="AntialiasedGraphs.js"></script>
  </head>
  <body onload="Initialize()">
    <canvas id="idCanvas" width="800" height ="800" style="background-color: white;border: 1px solid black;"></canvas>
  </body>
</html>

AntialiasedGraphs.js

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;
	}
	PixelW() {
		return (this.mdHighX - this.mdLowX)/this.miCanvasW;
	}
	PixelH() {
		return (this.mdHighY - this.mdLowY)/this.miCanvasH;
	}
	PixelToCoordX(dPixelValueX) {
		// The pixel values run from 0 to miCanvasW - 1
		// The pixel locations run from .5 to miCanvasW - .5
		// Pixel space goes from 0 to miCanvasW.
		let dPixelLocX = (dPixelValueX + .5);
		let dPixelFracX = dPixelLocX/this.miCanvasW;
		let dCoordX = dPixelFracX*(this.mdHighX - this.mdLowX) + this.mdLowX;
		return dCoordX;
	}
	PixelToCoordY(dPixelValueY) {
		// The pixel values run from 0 to miCanvasH - 1
		// The pixel locations run from miCanvasH - .5 to .5
		// Pixel space goes from miCanvasW to 0.
		let dPixelLocY = this.miCanvasH - .5 - dPixelValueY;
		let dPixelFracY = dPixelLocY/this.miCanvasH;
		let dCoordY = dPixelFracY*(this.mdHighY - this.mdLowY) + this.mdLowY;
		return dCoordY;
	}
	CoordToPixelX(dCoordX) {
		let dFracX = (dCoordX - this.mdLowX)/(this.mdHighX - this.mdLowX);
		let dPixelX = dFracX*this.miCanvasW - .5;
		return dPixelX;
	}
	CoordToPixelY(dCoordY) {
		let dFracY = (dCoordY - this.mdLowY)/(this.mdHighY - this.mdLowY);
		let dPixelY = (1 - dFracY)*this.miCanvasH + .5;
		return dPixelY;
	}
	
	async DrawGraphOver(qContext, fnF, iSamplesPerPixelX, iSamplesPerPixelY, dLineWidth, iR, iG, iB) {
		const kdR = dLineWidth;
		// Create two arrays of values for the lowest and highest x values colored at each subsample
		const kiTotalSamplesX = this.miCanvasW*iSamplesPerPixelX;
		const kiTotalSamplesY = this.miCanvasH*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 dCx = this.PixelToCoordX(dPixelX);
			let dCy = fnF(dCx);
			let dPixelY = this.CoordToPixelY(dCy);
			if (dPixelY > -4*kdR && dPixelY < this.miCanvasH + 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;
  					}
  				} else {
  					console.log("iLowSubpixelY: " + iLowSubpixelY + "  iHighSubpixelY: " + 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(this.miCanvasW);
		for (let i = 0; i < this.miCanvasW; ++i) {
			daaPercentFull[i] = new Array(this.miCanvasH);
			for (let j = 0; j < this.miCanvasH; ++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(this.miCanvasW, this.miCanvasH);
		for (let i = 0; i < this.miCanvasW; ++i) {
			for (let j = 0; j < this.miCanvasH; ++j) {
				let iIndex = (j*this.miCanvasW + 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);
	}
}

function Initialize() {
	var qCanvas = document.getElementById("idCanvas");
	var qContext = qCanvas.getContext("2d");

	let qGraph = new GraphRange2D(-6, 6, -6, 6, 800, 800);
	
	qContext.strokeStyle = "#E0E0E0";
	qContext.lineWidth = "1";
	qContext.beginPath();
	
	// Draw the vertical grid lines
	let iLowX = Math.ceil(qGraph.PixelToCoordX(0));
	let iHighX = Math.floor(qGraph.PixelToCoordX(qCanvas.width));

	for (let i = iLowX; i <= iHighX; ++i) {
		if (i != 0) {
			let dX = qGraph.CoordToPixelX(i);
			qContext.moveTo(dX, 0);
			qContext.lineTo(dX, qCanvas.height);
		}
	}

	// Draw the horizontal grid lines
	let iLowY = Math.ceil(qGraph.PixelToCoordY(qCanvas.height));
	let iHighY = Math.floor(qGraph.PixelToCoordY(0));

	for (let i = iLowY; i <= iHighY; ++i) {
		if (i != 0) {
			let dY = qGraph.CoordToPixelY(i);
			qContext.moveTo(0, dY);
			qContext.lineTo(qCanvas.width, dY);
		}
	}
	// Finally, draw all of the grid lines
	qContext.stroke();
	
	// Draw the axes
	qContext.strokeStyle = "black";
	qContext.lineWidth = "1";
	qContext.beginPath();
	qContext.moveTo(0, qCanvas.height/2);
	qContext.lineTo(qCanvas.width, qCanvas.height/2);
	qContext.moveTo(qCanvas.width/2, 0);
	qContext.lineTo(qCanvas.width/2, qCanvas.height);
	qContext.stroke();
	
	// Get the canvas size
	qGraph.DrawGraphOver(qContext, Math.tan, 16, 16, 1, 128, 255, 128);
	qGraph.DrawGraphOver(qContext, Math.sin, 16, 16, 1, 255, 128, 128);
	qGraph.DrawGraphOver(qContext, Math.cos, 16, 16, 1, 128, 128, 255);
}

 

Output

 
 

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