Canvas JavaScript

Anti-Aliased Level Sets

This JavaScript program demonstrates how to draw a graph of 2d level sets with anti-aliased subsampling. The magnitude of the gradient is used to measure the distance to a level value, combined with the pen width and the pixel magnitude.

AntialiasedLevelSetGraphs.html

<!DOCTYPE html>
<html>
  <head>
    <title>XoaX.net's Javascript</title>
    <script type="text/javascript" src="AntialiasedLevelSetGraphs.js"></script>
  </head>
  <body onload="Initialize()">
    <canvas id="idCanvas" width="600" height ="600" style="background-color: #F0F0F0;"></canvas>
  </body>
</html>

AntialiasedLevelSetGraphs.js

class Graph2D {
	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;
	}
	// Map the function a a level curve of the function z(x, y) = -y(x) + ...
	async DrawGraphOver(qContext, fnF, fnGradF, iSamplesPerPixelX, iSamplesPerPixelY, dPenWidth, daLevels) {
		var qImageData = qContext.createImageData(this.miCanvasW, this.miCanvasH);
		const kdDxDp = this.PixelW();
		const kdDyDp = this.PixelH();
		let dMagPixel = Math.sqrt(kdDxDp*kdDxDp + kdDyDp*kdDyDp);
		const kdDxDs = kdDxDp/iSamplesPerPixelX;
		const kdDyDs = kdDyDp/iSamplesPerPixelY;
		//const kdGraphZ = dLineDepth/2;
		const kdSamplesPerPixel = iSamplesPerPixelX*iSamplesPerPixelY;
		let iPixel = 0;
		// Run over the y pixel values
		for (let j = 0; j < this.miCanvasH; ++j) {
			for (let i = 0; i < this.miCanvasW; ++i) {
				// Get the position of the center of the pixel and subtract half of the samples distance.
				let dPixelX = this.mdLowX + (i + .5)*kdDxDp - kdDxDs*(iSamplesPerPixelX - 1)/2;
				let dPixelY = this.mdHighY - (j + .5)*kdDyDp - kdDyDs*(iSamplesPerPixelY - 1)/2;
				let iCount = 0;
				for (let iSx = 0; iSx < iSamplesPerPixelX; ++iSx) {
					let dX = dPixelX + iSx*kdDxDs;
					for (let iSy = 0; iSy < iSamplesPerPixelY; ++iSy) {
						let dY = dPixelY + iSy*kdDyDs;
						let dF = fnF(dX, dY);
						let daGradF = fnGradF(dX, dY);
						let dMagGradF = Math.sqrt(daGradF[0]*daGradF[0] + daGradF[1]*daGradF[1]);
						let dCloseLevel = FindClosestLevel(dF, daLevels);//5*Math.round(dF/5); 
						// Check whether the function value is less than the magnitude of the gradient.
						// This tells us if we reach zero within on unit.
						// This is multiplied by the magnitude of the pixel in spatial unit to make the unit pixels
						// Finally, this is multiple by the pen width in pixels to make the drawing the correct width.
						if (Math.abs(dF - dCloseLevel) < dPenWidth*dMagPixel*dMagGradF) {
							++iCount;
						}
					}
				}
			 	var uiPixelColor = Math.round(255*(1.0 - iCount/kdSamplesPerPixel));
			 	qImageData.data[iPixel] = uiPixelColor;
			 	qImageData.data[iPixel + 1] = uiPixelColor;
			 	qImageData.data[iPixel + 2] = uiPixelColor;
			 	qImageData.data[iPixel + 3] = 255 - uiPixelColor;
				iPixel += 4;
			}
		}
		// This version allows the image data to be alpha composited to allow the axes to show through
		const qBitmap = await createImageBitmap(qImageData);
		qContext.drawImage(qBitmap, 0, 0);
	}
}

// z = (y - x)^2 + (1 - x)^2
function F(x, y) {
	return (y - x)*(y - x) + (1 - x)*(1 - x);
}

function GradF(x, y) {
	return [-2*(y - x) - 2*(1 - x), 2*(y - x)];
}

function FindClosestLevel(dZ, daLevels) {
	let dClosestDistance = Math.abs(dZ - daLevels[0]);
	for (let i = 1; i < daLevels.length; ++i) {
		let dNextDistance = Math.abs(dZ - daLevels[i]);
		if (dNextDistance < dClosestDistance) {
			dClosestDistance = dNextDistance;
			if (i == daLevels.length - 1) {
				return daLevels[daLevels.length-1];
			}
		} else {
			return daLevels[i-1];
		}
	}
	return daLevels[0];
}

function Initialize() {
	var qCanvas = document.getElementById("idCanvas");
	var qContext2D = qCanvas.getContext("2d");
	
	// Draw the axes
	qContext2D.strokeStyle = "lightgray";
	qContext2D.lineWidth = "1";
	qContext2D.beginPath();
	qContext2D.moveTo(0, qCanvas.height/2);
	qContext2D.lineTo(qCanvas.width, qCanvas.height/2);
	qContext2D.moveTo(qCanvas.width/2, 0);
	qContext2D.lineTo(qCanvas.width/2, qCanvas.height);
	qContext2D.stroke();

	var qGraph2D = new Graph2D(-4, 4, -4, 4, qCanvas.width, qCanvas.height);
	var daLevels = [0, .25, .5, 1, 2, 4, 8, 16, 32, 64];
	qGraph2D.DrawGraphOver(qContext2D, F, GradF, 4, 4, .5, daLevels);
}


 

Output

 
 

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