Canvas JavaScript

Raytraced Earth with Cities

This JavaScript program demonstrates how to draw a raytraced, earth-textured sphere with lighting using an image data instance to set individual pixels on a canvas element. Use the arrow keys to rotate the view, the less-than and greater than to zoom, and the check-boxes to toggle the display of cities on the globe.

RaytracedEarthTextureWithZoom.html

<!DOCTYPE html>
<html>
	<head>
		<link rel="icon" href="data:,">
		<title>XoaX.net's Javascript</title>
		<script type="text/javascript" src="RaytracedEarthTextureWithZoom.js"></script>
		<style>
			.cFocus { border: 1px red solid; }
			.cBlur { border: none; }
		</style>
	</head>
	<body onload="LoadImage()">
		<canvas id="idCanvas" width="600" height ="600" style="background-color: #F0F0F0;"></canvas>
		<div style="width:600px;"><fieldset id="idCities"><legend>Cities:</legend>
			<input style="accent-color: orange;" type="checkbox" id="idChicago" onclick="Rerender()" checked>Chicago</input>
			<input style="accent-color: red;" type="checkbox" id="idMoscow" onclick="Rerender()" checked>Moscow</input>
			<input style="accent-color: deepskyblue;" type="checkbox" id="idSydney" onclick="Rerender()" checked>Sydney</input>
			<input style="accent-color: yellow;" type="checkbox" id="idTokyo" onclick="Rerender()" checked>Tokyo</input>
			<input style="accent-color: lime;" type="checkbox" id="idLima" onclick="Rerender()" checked>Lima</input>
			<input style="accent-color: aqua;" type="checkbox" id="idJohannesburg" onclick="Rerender()" checked>Johannesburg</input>
			<input style="accent-color: magenta;" type="checkbox" id="idParis" onclick="Rerender()" checked>Paris</input>
		</fieldset></div>
	</body>
</html>

RaytracedEarthTextureWithZoom.js

var qCP = null;
var qImageTexture = null;
var qEarthImageData	= null;
var gqaCities = [];

function LoadImage() {
	qImageTexture = new Image();
	qImageTexture.onload = Initialize;
	qImageTexture.src = "Earth10800x5400.jpg";
}

function Rerender() {
	qCP.DrawScene();
}

function Initialize() {
	gqaCities[0] = new CCity([255, 165, 0], [41.88, 87.66], document.getElementById("idChicago"));						//(41.8819 N, 87.6645 W)
	gqaCities[1] = new CCity([255, 0, 0], [55.75, 360-37.62], document.getElementById("idMoscow"));						//(55.75204 N, 37.61781 E)
	gqaCities[2] = new CCity([0, 191, 255], [-33.87, 360-151.21], document.getElementById("idSydney"));				//(33.8688 S, 151.2093 E)
	gqaCities[3] = new CCity([255, 255, 0], [35.69, 360-139.69], document.getElementById("idTokyo"));					//(35.6895 N, 139.6917 E)
	gqaCities[4] = new CCity([0, 255, 0], [-12.04, 77.03], document.getElementById("idLima"));								//(12.04318 S, 77.02824 W)
	gqaCities[5] = new CCity([0, 255, 255], [-26.20, 360-28.05], document.getElementById("idJohannesburg"));	//(26.2041 S, 28.0473 E)
	gqaCities[6] = new CCity([255, 0, 255], [48.86, 360-2.3522], document.getElementById("idParis"));					//(48.8566 N, 2.3522 E)

	let iImageWidth = qImageTexture.width;
	let iImageHeight = qImageTexture.height;
	
	var qEarthCanvas = new OffscreenCanvas(iImageWidth, iImageHeight);
	var qEarthContext	= qEarthCanvas.getContext("2d");
	qEarthContext.drawImage(qImageTexture, 0, 0);
	qEarthImageData = qEarthContext.getImageData(0, 0, iImageWidth, iImageHeight);
	
	
	qCP = new CCanvasPlane("idCanvas", 2.0, 2.0);
	window.onkeydown=KeyDownFunction;
	window.addEventListener("focus", FocusFunction);
	window.addEventListener("blur", BlurFunction);
	window.focus();
	FocusFunction();
	qCP.DrawScene();
}

class CCity {
	constructor(iaColor, daLatLong, qElement) {
		this.miaColor = iaColor;
		this.mdaLatLong = daLatLong;
		this.mqElement = qElement;
	}
}

class CSphere {
	constructor(daC, dR) {
		this.mdaC = [daC[0], daC[1], daC[2]];;
		this.mdR = dR;
	}
	
	// Pass in the position and direction for the ray
	Intersect(daP, daV, daTanDir, iaRGB) {
		// Sphere: (x - cx)^2 + (y - cy)^2 + (z - cz)^2 = r^2
		// Sphere with ray: (px + tvx - cx)^2 + (py + tvy - cy)^2 + (pz + tvz - cz)^2 = r^2
		// Solve for terms: (px^2 - 2pxcx + cx^2) + (py^2 - 2pycy + cy^2) + (pz^2 - 2pzcz + cz^2) - r^2
		// + 2t[(vx(px - cx)) + (vy(py - cy)) + (vz(pz - cz))]
		// + t^2(vx^2 + vy^2 + vz^2)
		var dT = NaN;
		var dC = daP[0]*daP[0] + daP[1]*daP[1] + daP[2]*daP[2] + 
			-2.0*(daP[0]*this.mdaC[0] + daP[1]*this.mdaC[1] + daP[2]*this.mdaC[2]) +
			this.mdaC[0]*this.mdaC[0] + this.mdaC[1]*this.mdaC[1] + this.mdaC[2]*this.mdaC[2] -  this.mdR*this.mdR;
		var dB = 2.0*(daV[0]*(daP[0] - this.mdaC[0]) + daV[1]*(daP[1] - this.mdaC[1]) + daV[2]*(daP[2] - this.mdaC[2]));
		var dA = daV[0]*daV[0] + daV[1]*daV[1] + daV[2]*daV[2];
		var dDisc = dB*dB - 4.0*dA*dC;
		if (dDisc > 0) {
			// T is either 2C/(-B - sqrt(B*B - 4AC)) or (-B - sqrt(B*B - 4AC))/2A
			// The second anser is closer. So, we use that one to give the first intersection.
			dT = (-dB - Math.sqrt(dDisc))/(2.0*dA);
			// The tangent plane direction is [2(x - cx), 2(y - cy), 2(z - cz)]
			// x = px + t*vx, y = py + tvy, z = pz + tvz
			var dMag =  Math.sqrt(2.0*(daP[0] + dT*daV[0] - this.mdaC[0])*2.0*(daP[0] + dT*daV[0]  - this.mdaC[0]) +
				2.0*(daP[1] + dT*daV[1] - this.mdaC[1])*2.0*(daP[1] + dT*daV[1] - this.mdaC[1]) +
				2.0*(daP[2] + dT*daV[2] - this.mdaC[2])*2.0*(daP[2] + dT*daV[2] - this.mdaC[2]));
			daTanDir[0] = 2.0*(daP[0] + dT*daV[0] - this.mdaC[0])/dMag;
			daTanDir[1] = 2.0*(daP[1] + dT*daV[1] - this.mdaC[1])/dMag;
			daTanDir[2] = 2.0*(daP[2] + dT*daV[2] - this.mdaC[2])/dMag;
		}
		// Get the tangent in spherical coordinates.
		var dTheta = Math.atan2(daTanDir[0], daTanDir[1]);
		var dPhi = Math.acos(daTanDir[2]);
		var dPixelX =  Math.floor((qEarthImageData.width - 1.0e-10)*dTheta/(2.0*Math.PI));
		var dPixelY =  Math.floor((qEarthImageData.height - 1.0e-10)*dPhi/Math.PI);
		var iPixelIndex = 4*dPixelX + 4*qEarthImageData.width*dPixelY;
		let dMinDist = 1.0;
		let iaColor = null;
		for(let qCity of gqaCities) {
			dMinDist = 1.0;
			// Scale by the angle phi to make the dots circular
			let dScale = ((dPhi > Math.PI/6) ? 1/Math.sin(dPhi) : 1/Math.sin(Math.PI/6));
			let dPhiDiff = dScale*(dPhi - (90 - qCity.mdaLatLong[0])*Math.PI/180.0);
			let dThetaDiff = (dTheta - (180 - qCity.mdaLatLong[1])*Math.PI/180);
			let dPixelDist = Math.sqrt(dPhiDiff*dPhiDiff + dThetaDiff*dThetaDiff);
			dMinDist = ((dMinDist > dPixelDist) ? dPixelDist : dMinDist);
			if (dMinDist <= .01 && qCity.mqElement.checked) {
				iaColor = qCity.miaColor;
			}
		}
		if (iaColor == null) {
			iaRGB[0] = qEarthImageData.data[iPixelIndex];
			iaRGB[1] = qEarthImageData.data[iPixelIndex + 1];
			iaRGB[2] = qEarthImageData.data[iPixelIndex + 2];
		} else {
			iaRGB[0] = iaColor[0];
			iaRGB[1] = iaColor[1];
			iaRGB[2] = iaColor[2];
		}
		return dT;
	}
}

class CCanvasPlane {
	// Pass in the canvas size in pixels and the size in space
	// The pixels of the canvas will be cenered at the origin
	// So, if the canvas is 600x600 pixels. Pixel (299.5, 299.5) will be at the origin,
	// with pixels starting at 0 and going to 599
	constructor(sCanvasId, dW, dH)  {
		var qCanvas = document.getElementById(sCanvasId);
		this.mqContext = qCanvas.getContext("2d");
		this.miPixelW = qCanvas.width;
		this.miPixelH = qCanvas.height;
		this.mqImData = this.mqContext.createImageData(this.miPixelW, this.miPixelH);
		this.mdW = dW;
		this.mdH = dH;
		// Set some default angles
		this.mqAlpha = Math.PI/6;
		this.mqBeta = -Math.PI/6;
	}
	
	Clear() {
		this.mqContext.clearRect(0, 0, 640, 480);
	}
	
	Left() {
		this.mqAlpha +=  this.mdW*Math.PI/30;
	}
	Right() {
		this.mqAlpha -=  this.mdW*Math.PI/30;
	}
	Up() {
		if (this.mqBeta - this.mdW*Math.PI/30 > -Math.PI/2 + .01) {
			this.mqBeta -=  this.mdW*Math.PI/30;
		}
	}
	Down() {
		if (this.mqBeta + this.mdW*Math.PI/30 < Math.PI/2 +.01) {
			this.mqBeta +=  this.mdW*Math.PI/30;
		}
	}
	
	CreateCoordinateVectors() {
		var daaA = [];
			// This is the x direction of the canvas inside the plane z = 0 in space coordinates
			daaA[0] = [Math.cos(this.mqAlpha), Math.sin(this.mqAlpha), 0];
			// cos(beta)*up + sin(beta)*(vector perp to x pointing forward), since beta is angle between the canvas and the z-axis
			daaA[1] = [-daaA[0][1]*Math.sin(this.mqBeta), daaA[0][0]*Math.sin(this.mqBeta), Math.cos(this.mqBeta)];
			// The vector straigth out of canvas. The cross product of the previous vectors
			daaA[2] = [daaA[0][1]*daaA[1][2] - daaA[0][2]*daaA[1][1],
				daaA[0][2]*daaA[1][0] - daaA[0][0]*daaA[1][2],
				daaA[0][0]*daaA[1][1] - daaA[0][1]*daaA[1][0]];
		return daaA;
	}
	
	DrawScene() {
		// Create a sphere
		var qUnitSphere = new CSphere([0.0, 0.0, 0.0], 1.0);
		
		// The pixel width and height in the coordinates of the space
		var dPixWidth = this.mdW/this.miPixelW;
		var dPixHeight = this.mdH/this.miPixelH;
		var qCanvasInSpace = this.CreateCoordinateVectors();
		var daDirX = qCanvasInSpace[0];
		var daDirY = [-qCanvasInSpace[1][0], -qCanvasInSpace[1][1], -qCanvasInSpace[1][2]];
		var daView = qCanvasInSpace[2];
		var dPixStartX = dPixWidth*.5 - (this.mdW/2);
		var dPixStartY = dPixHeight*.5 - (this.mdH/2);
		// The position in space of the center of the first pixel
		var daPixPosInit = [dPixStartX*daDirX[0]+dPixStartY*daDirY[0], 
			dPixStartX*daDirX[1]+dPixStartY*daDirY[1], 
			dPixStartX*daDirX[2]+dPixStartY*daDirY[2]];
		// The position of the center of the first pixel in the current row.
		var daPixPosRow = [daPixPosInit[0], daPixPosInit[1], daPixPosInit[2]];
		// The center point of the current pixel.
		var daPixPos = [daPixPosInit[0], daPixPosInit[1], daPixPosInit[2]];
		// The translation vector for a pixel in the x-direction
		var daPixDx = [dPixWidth*daDirX[0], dPixWidth*daDirX[1], dPixWidth*daDirX[2]];
		// The translation vector for a pixel in the y-direction
		var daPixDy = [dPixHeight*daDirY[0], dPixHeight*daDirY[1], dPixHeight*daDirY[2]];
		// The current pixel index
		var iPix = 0;
		var daTanDir = [0.0, 0.0, 0.0];
		//var daLightDir = [-1.0/Math.sqrt(14.0), -2.0/Math.sqrt(14.0), -3.0/Math.sqrt(14.0)];
		var daLightDir = [daView[0], daView[1], daView[2]];
		for (var i = 0; i < this.miPixelW; ++i) {
			for (var j = 0; j < this.miPixelH; ++j) {
				var daRGB = [0.0,0.0,0.0];
				var dT = qUnitSphere.Intersect(daPixPos, daView, daTanDir, daRGB);
						if (Number.isNaN(dT)) {
							this.mqImData.data[iPix] = 0;
							this.mqImData.data[iPix+1] = 0;
							this.mqImData.data[iPix+2] = 0;
							this.mqImData.data[iPix+3] = 255;
						} else {
							var dDot = daTanDir[0]*daLightDir[0] + daTanDir[1]*daLightDir[1] + daTanDir[2]*daLightDir[2];
							dDot = (dDot < 0.0) ? -dDot: 0.0;
							this.mqImData.data[iPix] = daRGB[0]*(.2 + .8*dDot);
							this.mqImData.data[iPix+1] = daRGB[1]*(.2 + .8*dDot);
							this.mqImData.data[iPix+2] = daRGB[2]*(.2 + .8*dDot);
							this.mqImData.data[iPix+3] = 255;
						}
				
				daPixPos[0] += daPixDx[0];
				daPixPos[1] += daPixDx[1];
				daPixPos[2] += daPixDx[2];
				iPix += 4;
			}
			daPixPosRow[0] += daPixDy[0];
			daPixPosRow[1] += daPixDy[1];
			daPixPosRow[2] += daPixDy[2];
			daPixPos[0] = daPixPosRow[0];
			daPixPos[1] = daPixPosRow[1];
			daPixPos[2] = daPixPosRow[2];
		}
		// Use the image data to draw the pixels at (0, 0)
		this.mqContext.putImageData(this.mqImData, 0, 0);
	}
}

function FocusFunction() {
	document.getElementById("idCanvas").className ='cFocus';
}

function BlurFunction() {
	document.getElementById("idCanvas").className ='cBlur';
}

function KeyDownFunction(e) {
	var iKeyUp = 38;
	var iKeyLeft = 37;
	var iKeyRight = 39;
	var iKeyDown = 40;
	var iKeyLess = 188;
	var iKeyGreater = 190;
	var iKeyCode = 0;
	if (e) {
		iKeyCode = e.which;
	} else {
		iKeyCode = window.event.keyCode;
	}
	qCP.Clear();
	switch (iKeyCode) {
		case iKeyUp: {
			qCP.Up();
			// Prevent the window scrolling
			e.preventDefault();
			break;
		}
		case iKeyLeft: {
			qCP.Left();
			// Prevent the window scrolling
			e.preventDefault();
			break;
		}
		case iKeyRight: {
			qCP.Right();
			// Prevent the window scrolling
			e.preventDefault();
			break;
		}
		case iKeyDown: {
			qCP.Down();
			// Prevent the window scrolling
			e.preventDefault();
			break;
		}
		case iKeyLess: {
			if (qCP.mdW < 3.6) {
				qCP.mdW *= 1.05;
				qCP.mdH *= 1.05;
			}
			// Prevent the window scrolling
			e.preventDefault();
			break;
		}
		case iKeyGreater: {
			if (qCP.mdW > .2) {
				qCP.mdW *= .95238;
				qCP.mdH *= .95238;
			}
			// Prevent the window scrolling
			e.preventDefault();
			break;
		}
		default: {
			break;
		}
	}
	qCP.DrawScene();
}
 

Output

 
 

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