This JavaScript program demonstrates how to draw a 2d graph of a function with anti-aliased subsampling.
<!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>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);
}
© 20072025 XoaX.net LLC. All rights reserved.