Visualizing a Network of Nodes on the Surface of Sphere

The JavaScript code shown below generates a random network of nodes distributed across the surface of a sphere, connects them together with links, and then renders them as an image. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/sphericalnetwork.html.  The A, D, S, and W keys can be used to rotate the view.

My initial intention was to use this as a starting point to build and render a system of “stargates” or “jump points” in a space-themed science fiction game. For example, look into the old DOS game Ascendancy.

As it stands, the program could probably stand some enhancement. Among other things, right now I’m not sure the nodes are being distributed and/or connected in the most efficient and/or aesthetically pleasing manner.

SphericalNetwork

<html>
<body>
<script type='text/javascript'>

// main

function NetworkVisualizer()
{
	this.main = function()
	{
		var network = Network.generateRandom
		(
			"Test Network",
			NodeDefn.Instances._All,
			64, // numberOfNodes
			[250, 250] // minAndMaxDistanceOfNodesFromOrigin
		);

		var camera = new Camera
		(
			new Coords(600, 600, 600), // viewSize, 
			600, // focalLength, 
			new Coords(-600, 0, 0), //pos, 
			new Orientation
			(
				new Coords(1, 0, 0), // forward
				new Coords(0, 0, 1) // down
			)
		);

		Globals.Instance.initialize
		(
			1000, // millisecondsPerTick
			network, 
			camera
		);
	}
}

// classes

function ArrayHelper()
{}
{
	ArrayHelper.addLookupsToArray = function(arrayToAddLookupsTo, propertyNameForKey)
	{
		for (var i = 0; i < arrayToAddLookupsTo.length; i++)
		{
			var arrayItem = arrayToAddLookupsTo[i];
			var key = arrayItem[propertyNameForKey];
			arrayToAddLookupsTo[key] = arrayItem;
		}
	}

	ArrayHelper.getPropertyValueForEachItemInArray = function(propertyName, arrayToGetFrom)
	{
		var returnValues = [];

		for (var i = 0; i < arrayToGetFrom.length; i++)
		{
			returnValues.push(arrayToGetFrom[i][propertyName]);
		}

		return returnValues;
	}
}

function Camera(viewSize, focalLength, pos, orientation)
{
	this.viewSize = viewSize;
	this.focalLength = focalLength;
	this.pos = pos;
	this.orientation = orientation;
}
{
	Camera.prototype.convertWorldCoordsToViewCoords = function(coordsToConvert)
	{
		coordsToConvert.subtract
		(
			this.pos
		);

		coordsToConvert.overwriteWithDimensions
		(
			this.orientation.right.dotProduct(coordsToConvert),
			this.orientation.down.dotProduct(coordsToConvert),
			this.orientation.forward.dotProduct(coordsToConvert)
		);

		var distanceForwardInFocalLengths = coordsToConvert.z / this.focalLength;

		coordsToConvert.x /= distanceForwardInFocalLengths;
		coordsToConvert.y /= distanceForwardInFocalLengths;

		coordsToConvert.x += this.viewSize.x / 2;
		coordsToConvert.y += this.viewSize.y / 2;

		return coordsToConvert;
	}
}

function Color(name, systemColor)
{
	this.name = name;
	this.systemColor = systemColor;
}
{
	function Color_Instances()
	{
		this.Black = new Color("Black", "rgb(0, 0, 0)");
		this.Blue = new Color("Blue", "rgb(0, 0, 255)");
		this.CyanHalfTranslucent = new Color("CyanHalfTranslucent", "rgba(0, 128, 128, .5)");
		this.Gray = new Color("Gray", "rgb(128, 128, 128)");
		this.Green = new Color("Green", "rgb(0, 255, 0)");
		this.Red = new Color("Red", "rgb(255, 0, 0)");
		this.White = new Color("White", "rgb(255, 255, 255)");
	}

	Color.Instances = new Color_Instances();
}

function Constants()
{}
{
	Constants.Tau = Math.PI * 2;
}

function Coords(x, y, z)
{
	this.x = x;
	this.y = y;
	this.z = z;
}
{
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		this.z += other.z;

		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y, this.z);
	}

	Coords.prototype.crossProduct = function(other)
	{
		return new Coords
		(
			this.y * other.z - this.z * other.y,
			this.z * other.x - this.x * other.z,
			this.x * other.y - this.y * other.x
		);
	}

	Coords.prototype.dotProduct = function(other)
	{
		var returnValue = 
			this.x * other.x 
			+ this.y * other.y
			+ this.z * other.z;

		return returnValue;
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		this.z /= scalar;

		return this;
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt
		(
			this.x * this.x
			+ this.y * this.y
			+ this.z * this.z
		);
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		this.z *= scalar;

		return this;
	}

	Coords.prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		this.z = other.z;

		return this;
	}

	Coords.prototype.overwriteWithDimensions = function(x, y, z)
	{
		this.x = x;
		this.y = y;
		this.z = z;

		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		this.z -= other.z;

		return this;
	}
}

function Globals()
{
	this.inputHelper = new InputHelper();
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function
	(
		millisecondsPerTimerTick, 
		network,
		camera
	)
	{
		this.inputHelper.initialize();

		this.network = network;

		this.networkViewer = new NetworkViewer
		(
			this.network, camera
		);

		setInterval
		(
			"Globals.Instance.handleTimerTick()", 
			millisecondsPerTimerTick
		);

		this.networkViewer.draw();
	}

	Globals.prototype.handleTimerTick = function()
	{
		// todo
	}
}

function InputHelper()
{}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleKeyDownEvent.bind(this);
	}

	InputHelper.prototype.handleKeyDownEvent = function(event)
	{
		var keyCode = event.keyCode;

		var networkViewer = Globals.Instance.networkViewer;
		var network = networkViewer.network;
		var camera = networkViewer.camera;

		var cameraSpeed = 20;

		var displacementToMoveCamera = null;

		if (keyCode == 65) // A
		{
			displacementToMoveCamera = camera.orientation.right.clone().multiplyScalar
			(
				0 - cameraSpeed
			);
		}
		else if (keyCode == 68) // D
		{
			displacementToMoveCamera = camera.orientation.right.clone().multiplyScalar
			(
				cameraSpeed
			);
		}
		else if (keyCode == 83) // S
		{
			displacementToMoveCamera = camera.orientation.down.clone().multiplyScalar
			(
				cameraSpeed
			);
		}
		else if (keyCode == 87) // W
		{
			displacementToMoveCamera = camera.orientation.down.clone().multiplyScalar
			(
				0 - cameraSpeed
			);
		}

		if (displacementToMoveCamera != null)
		{
			camera.pos.add(displacementToMoveCamera);
			var cameraPosAsPolar = Polar.fromCoords(camera.pos);
			cameraPosAsPolar.radius = camera.focalLength;
			camera.pos = cameraPosAsPolar.toCoords();

			var cameraOrientationForward = camera.pos.clone().multiplyScalar(-1);

			camera.orientation = new Orientation
			(
				cameraOrientationForward,
				camera.orientation.down	
			);

			networkViewer.draw();
		}
	}
}

function Link(namesOfNodesLinked)
{
	this.namesOfNodesLinked = namesOfNodesLinked;
}
{
	Link.prototype.nodesLinked = function(network)
	{
		var returnValue = 
		[
			network.nodes[this.namesOfNodesLinked[0]],
			network.nodes[this.namesOfNodesLinked[1]],
		];

		return returnValue;
	}
}

function Network(name, nodes, links)
{
	this.name = name;
	this.nodes = nodes;
	this.links = links;

	ArrayHelper.addLookupsToArray(this.nodes, "name");
}
{
	Network.generateRandom = function(name, nodeDefns, numberOfNodes, minAndMaxDistanceOfNodesFromOrigin)
	{
		var nodesNotYetLinked = [];

		var radiusRange = 
			minAndMaxDistanceOfNodesFromOrigin[1] 
			- minAndMaxDistanceOfNodesFromOrigin[0];

		for (var i = 0; i < numberOfNodes; i++)
		{
			var polarRandom = Polar.random();
			polarRandom.radius = 
				minAndMaxDistanceOfNodesFromOrigin[0]
				+ Math.random() * radiusRange;

			var nodeDefnIndexRandom = Math.floor(nodeDefns.length * Math.random());
			var nodeDefn = nodeDefns[nodeDefnIndexRandom];

			var node = new Node
			(
				"" + i,
				nodeDefn,
				polarRandom.toCoords()
			);

			nodesNotYetLinked.push(node);
		}

		var nodesLinked = [ nodesNotYetLinked[0] ];
		nodesNotYetLinked.splice(0, 1);
		var links = [];

		var tempPos = new Coords(0, 0, 0);

		while (nodesLinked.length < numberOfNodes)
		{
			var nodePairClosestSoFar = null;
			var distanceBetweenNodePairClosestSoFar = minAndMaxDistanceOfNodesFromOrigin[1] * 4;

			for (var i = 0; i < nodesLinked.length; i++)
			{
				var nodeLinked = nodesLinked[i];

				for (var j = 0; j < nodesNotYetLinked.length; j++)
				{
					var nodeToLink = nodesNotYetLinked[j];

					var distanceBetweenNodes = tempPos.overwriteWith
					(
						nodeLinked.pos
					).subtract
					(
						nodeToLink.pos
					).magnitude();

					if (distanceBetweenNodes <= distanceBetweenNodePairClosestSoFar)
					{
						distanceBetweenNodePairClosestSoFar = distanceBetweenNodes;
						nodePairClosestSoFar = [nodeToLink, nodeLinked];
					}
				}
			}

			var nodeToLink = nodePairClosestSoFar[0];
			var nodeLinked = nodePairClosestSoFar[1];

			var link = new Link
			([ 
				nodeToLink.name,
				nodeLinked.name
			]);

			links.push(link);

			nodesLinked.push(nodeToLink);
			nodesNotYetLinked.splice(nodesNotYetLinked.indexOf(nodeToLink), 1);
		}

		var returnValue = new Network
		(
			name,
			nodesLinked,
			links
		);

		return returnValue;
	}
}

function NetworkViewer(network, camera)
{
	this.network = network;
	this.camera = camera;
}
{
	NetworkViewer.prototype.draw = function()
	{
		var canvas = this.htmlElement;

		if (canvas == null)
		{
			canvas = document.createElement("canvas");
			canvas.style.border = "1px solid";
			canvas.width = this.camera.viewSize.x;
			canvas.height = this.camera.viewSize.y;

			this.graphics = canvas.getContext("2d");

			document.body.appendChild(canvas);

			this.htmlElement = canvas;
		}

		this.graphics.fillStyle = Color.Instances.Black.systemColor;
		this.graphics.fillRect(0, 0, this.camera.viewSize.x, this.camera.viewSize.y);

		var drawPos = new Coords(0, 0, 0);
		var drawPosFrom = new Coords(0, 0, 0);
		var drawPosTo = new Coords(0, 0, 0);

		var cameraPos = this.camera.pos;

		var networkNodes = this.network.nodes;
		var numberOfNetworkNodes = networkNodes.length;

		var networkLinks = this.network.links;
		var numberOfNetworkLinks = networkLinks.length;

		var nodeRadiusActual = 4;


		this.graphics.fillStyle = Color.Instances.CyanHalfTranslucent.systemColor;

		for (var i = 0; i < numberOfNetworkLinks; i++)
		{
			var link = networkLinks[i];

			var nodesLinked = link.nodesLinked(this.network);

			drawPosFrom.overwriteWith(nodesLinked[0].pos);
			this.camera.convertWorldCoordsToViewCoords(drawPosFrom);
			drawPosTo.overwriteWith(nodesLinked[1].pos);
			this.camera.convertWorldCoordsToViewCoords(drawPosTo);

			var directionFromNode0To1InView = drawPosTo.clone().subtract
			(
				drawPosFrom
			).normalize();

			var perpendicular = new Coords
			(
				0 - directionFromNode0To1InView.y, 
				directionFromNode0To1InView.x,
				0
			);

			var radiusApparentFrom = nodeRadiusActual * this.camera.focalLength / drawPosFrom.z;
			var radiusApparentTo = nodeRadiusActual * this.camera.focalLength / drawPosTo.z;

			this.graphics.beginPath();
			this.graphics.moveTo(drawPosFrom.x, drawPosFrom.y);
			this.graphics.lineTo(drawPosTo.x, drawPosTo.y);
			this.graphics.lineTo
			(
				drawPosTo.x + perpendicular.x * radiusApparentTo,
				drawPosTo.y + perpendicular.y * radiusApparentTo
			);
			this.graphics.lineTo
			(
				drawPosFrom.x + perpendicular.x * radiusApparentFrom, 
				drawPosFrom.y + perpendicular.y * radiusApparentFrom
			);
			this.graphics.fill();
		}

		this.graphics.font = "16px Arial";

		for (var i = 0; i < numberOfNetworkNodes; i++)
		{
			var node = networkNodes[i];
			var nodePos = node.pos;

			drawPos.overwriteWith(nodePos);
			this.camera.convertWorldCoordsToViewCoords(drawPos);

			var radiusApparent = nodeRadiusActual * this.camera.focalLength / drawPos.z;

			this.graphics.strokeStyle = node.defn.color.systemColor;
			this.graphics.fillStyle = node.defn.color.systemColor;

			this.graphics.beginPath();
			this.graphics.arc
			(
				drawPos.x, drawPos.y, 
				radiusApparent, 
				0, 2 * Math.PI, // start and stop angles 
				false // counterClockwise
			);
			this.graphics.stroke();

			this.graphics.fillText
			(
				node.name, drawPos.x, drawPos.y
			);
		}
	
	}
}

function Node(name, defn, pos)
{
	this.name = name;
	this.defn = defn;
	this.pos = pos;
}

function NodeDefn(name, color)
{
	this.name = name;
	this.color = color;
}
{
	function NodeDefn_Instances()
	{
		this.Blue = new NodeDefn("Blue", Color.Instances.Blue);
		this.Green = new NodeDefn("Green", Color.Instances.Green);
		this.Red = new NodeDefn("Red", Color.Instances.Red);

		this._All = 
		[
			this.Blue,
			this.Green,
			this.Red,			
		];
	}

	NodeDefn.Instances = new NodeDefn_Instances();
}

function Orientation(forward, down)
{
	this.forward = forward.clone().normalize();
	this.right = down.crossProduct(this.forward).normalize();
	this.down = this.forward.crossProduct(this.right).normalize();

}

function Polar(azimuth, elevation, radius)
{
	// values in radians

	this.azimuth = azimuth;
	this.elevation = elevation;
	this.radius = radius;
}
{
	// static methods

	Polar.fromCoords = function(coordsToConvert)
	{
		var azimuth = Math.atan2(coordsToConvert.y, coordsToConvert.x);
		if (azimuth < 0)
		{
			azimuth += Constants.Tau;
		}

		var radius = coordsToConvert.magnitude();

		var elevation = Math.asin(coordsToConvert.z / radius);

		var returnValue = new Polar
		(
			azimuth,
			elevation,
			radius
		);

		return returnValue;
	}

	// instance methods

	Polar.random = function()
	{
		return new Polar
		(
			Math.random() * Constants.Tau,
			Math.random() * Constants.Tau,
			Math.random()
		);
	}

	Polar.prototype.toCoords = function()
	{
		var cosineOfElevation = Math.cos(this.elevation);

		var returnValue = new Coords
		(
			Math.cos(this.azimuth) * cosineOfElevation,
			Math.sin(this.azimuth) * cosineOfElevation,
			Math.sin(this.elevation)
		).multiplyScalar(this.radius);

		return returnValue;
	}
}

// run

new NetworkVisualizer().main();

// notes - for later enhancements

	/*
	var gradient = this.graphics.createLinearGradient
	(
		drawPosFrom.x, drawPosFrom.y,
		drawPosTo.x, drawPosTo.y
	);
	gradient.addColorStop(0,"red");
	gradient.addColorStop(1,"green");
	this.graphics.strokeStyle = gradient;
	*/

</script>
</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s