Posing and Animating a 3D Stick Figure in JavaScript

The program given below, when run, displays a real-time animation of a stick figure.

The figure is made up of a tree of “bones”, each of which represents a rigid rod of fixed length that can change the orientation of its distal end relative to the distal end of its parent. The animation itself consists of a series of “keyframes”, each of which indicates the relative orientations of the bones at a single point in time. The program interpolates, or “tweens”, the poses described by a pair of adjacent keyframes to determine the actual pose of the skeleton at a particular moment.

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 http://thiscouldbebetter.neocities.org/skeleton.html.

I’d like to make it a bit more polished, but I figure this is good enough for now.

StickFigureWalking

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

// main

function main()
{
	var skeleton = new Skeleton
	(
		"Skeleton0",
		new Bone
		(
			"Root", 
			36, 
			new Orientation(new Coords(0, 0, -1), new Coords(0, 1, 0)),
			[
				new Bone
				(
					"Hip.R", 
					6, 
					new Orientation(new Coords(-1, 0, 0), new Coords(0, 0, 1)),
					[
						new Bone
						(
							"Thigh.R", 
							18,
							new Orientation(new Coords(0, 0, 1), new Coords(-1, 0, 0)),
							[
								new Bone
								(
									"Shin.R",
									18, 
									new Orientation(new Coords(0, 0, 1), new Coords(1, 0, 0)),
									[
										new Bone("Foot.R", 10, new Orientation(new Coords(0, 1, 0), new Coords(1, 0, 0)), [])
									]
								),
							]
						)
					]
				),

				new Bone
				(
					"Hip.L", 
					6, 
					new Orientation(new Coords(1, 0, 0), new Coords(0, 0, 1)),
					[
						new Bone
						(
							"Thigh.L", 
							18,
							new Orientation(new Coords(0, 0, 1), new Coords(-1, 0, 0)),
							[
								new Bone
								(
									"Shin.L",
									18, 
									new Orientation(new Coords(0, 0, 1), new Coords(1, 0, 0)),
									[
										new Bone("Foot.L", 10, new Orientation(new Coords(0, 1, 0), new Coords(1, 0, 0)), [])
									]
								),
							]
						)
					]
				),

				new Bone
				(
					"Spine.1",
					24, 
					new Orientation(new Coords(0, .1, -1), new Coords(0, 1, 0)),
					[
						new Bone
						(
							"Neck",
							6, 
							new Orientation(new Coords(0, 0, -1), new Coords(0, 1, 0)),
							[
								new Bone
								(
									"Head.Back", 
									4, 
									new Orientation(new Coords(0, 0, -1), new Coords(0, 1, 0)),
									[
										new Bone("Head.Front", 8, new Orientation(new Coords(0, 1, 0), new Coords(0, 1, 0)), []),
									] 
								)
							]
						),
						new Bone
						(
							"Shoulder.L",
							12, 
							new Orientation(new Coords(1, 0, 0), new Coords(0, 0, 1)),
							[
								new Bone
								(
									"Bicep.L",
									12, 
									new Orientation(new Coords(0, -.1, 1), new Coords(-1, 0, 0)),
									[
										new Bone("Forearm.L", 12, new Orientation(new Coords(0, .1, 1), new Coords(-1, 0, 0)), [])
									]
								)
							]
						),

						new Bone
						(
							"Shoulder.R",
							12,
							new Orientation(new Coords(-1, 0, 0), new Coords(0, 0, 1)),
							[
								new Bone
								(
									"Bicep.R",
									12, 
									new Orientation(new Coords(0, -.1, 1), new Coords(-1, 0, 0)),
									[
										new Bone("Forearm.R", 12, new Orientation(new Coords(0, .1, 1), new Coords(-1, 0, 0)), [])
									]
								)
							]
						)

					]
				) // end spine
			],

			false // isVisible - hide the root bone
		)
	);

	var cyclesToRotateBones = .1;

	var animationDefn = new AnimationDefn
	(
		"SkeletonRun",
		[
			new AnimationKeyframe
			(
				0, 
				[
					new TransformBonePose("Bicep.L", 	[ -.1 ]),
					new TransformBonePose("Forearm.L", 	[ 0 ]),
					new TransformBonePose("Thigh.L", 	[ .1 ]),
					new TransformBonePose("Shin.L", 	[ 0 ]),

					new TransformBonePose("Bicep.R", 	[ .1 ]),
					new TransformBonePose("Forearm.R", 	[ .1 ]),
					new TransformBonePose("Thigh.R", 	[ -.05 ]),
					new TransformBonePose("Shin.R", 	[ 0 ]),
				]
			),
			
			new AnimationKeyframe
			(
				5, 
				[
					new TransformBonePose("Thigh.L", 	[ .1 ]),
					new TransformBonePose("Shin.L", 	[ .1 ]),

					new TransformBonePose("Thigh.R", 	[ -.1 ]),
					new TransformBonePose("Shin.R", 	[ 0 ]),
				]
			),

			new AnimationKeyframe
			(
				10, 
				[
					new TransformBonePose("Thigh.L", 	[ 0 ]),
					new TransformBonePose("Shin.L", 	[ 0 ]),

					new TransformBonePose("Thigh.R", 	[ 0 ]),
					new TransformBonePose("Shin.R", 	[ .1 ]),
				]
			),

			new AnimationKeyframe
			(
				15, 
				[
					new TransformBonePose("Bicep.L", 	[ .1 ]),
					new TransformBonePose("Forearm.L", 	[ .1 ]),
					new TransformBonePose("Thigh.L", 	[ -.05 ]),

					new TransformBonePose("Bicep.R", 	[ -.1 ]),
					new TransformBonePose("Forearm.R", 	[ 0 ]),
					new TransformBonePose("Thigh.R", 	[ .1 ]),
					new TransformBonePose("Shin.R", 	[ 0 ]),
				]
			),

			new AnimationKeyframe
			(
				20, 
				[
					new TransformBonePose("Thigh.L", 	[ -.1 ]),
					new TransformBonePose("Shin.L", 	[ 0 ]),

					new TransformBonePose("Thigh.R", 	[ .1 ]),
					new TransformBonePose("Shin.R", 	[ .1 ]),
				]
			),

			new AnimationKeyframe
			(
				25, 
				[
					new TransformBonePose("Thigh.L", 	[ 0 ]),
					new TransformBonePose("Shin.L", 	[ .1 ]),

					new TransformBonePose("Thigh.R", 	[ 0 ]),
					new TransformBonePose("Shin.R", 	[ 0 ]),
				]
			),

			new AnimationKeyframe
			(
				30, 
				[
					new TransformBonePose("Bicep.L", 	[ -.1 ]),
					new TransformBonePose("Forearm.L", 	[ 0 ]),
					new TransformBonePose("Thigh.L", 	[ .1 ]),
					new TransformBonePose("Shin.L", 	[ 0 ]),

					new TransformBonePose("Bicep.R", 	[ .1 ]),
					new TransformBonePose("Forearm.R", 	[ .1 ]),
					new TransformBonePose("Thigh.R", 	[ -.05 ]),
					new TransformBonePose("Shin.R", 	[ 0 ]),
				]
			),
		]
	);

	var camera = new Camera
	(
		"Camera0",
		50, // focalLength
		new Coords(100, 100, 100), // viewSize
		new Location
		(
			new Coords(100, 100, -136),
			new Orientation
			(
				new Coords(-1, -1, 1), // forward
				new Coords(0, 0, 1) // down
			)
		)
	);

	var world = new World
	(
		skeleton, 
		animationDefn,
		camera, 
		1 // secondsPerRunCycle
	);

	Globals.Instance.initialize
	(
		world,
		20 // timerTicksPerSecond 
	);
}

// classes

function AnimationDefn(name, keyframes)
{
	this.name = name;
	this.keyframes = keyframes;

	this.numberOfFramesTotal = 
		this.keyframes[this.keyframes.length - 1].frameIndex
		- this.keyframes[0].frameIndex;

	this.propagateTransformsToAllKeyframes();
}
{
	AnimationDefn.prototype.propagateTransformsToAllKeyframes = function()
	{
		var propertyNamesAll = [];

		for (var f = 0; f < this.keyframes.length; f++)
		{
			var keyframe = this.keyframes[f];
			var transforms = keyframe.transforms;

			for (var t = 0; t < transforms.length; t++)
			{
				var transform = transforms[t];
				var propertyName = transform.propertyName;
				if (propertyNamesAll[propertyName] == null)
				{
					propertyNamesAll[propertyName] = propertyName;
					propertyNamesAll.push(propertyName);
				}
			}
		}

		var keyframe = null;
		var keyframePrev = null;

		for (var f = 0; f < this.keyframes.length; f++)
		{
			keyframePrev = keyframe;
			keyframe = this.keyframes[f];

			var transforms = keyframe.transforms;

			for (var p = 0; p < propertyNamesAll.length; p++)
			{
				var propertyName = propertyNamesAll[p];
				if (transforms[propertyName] == null)
				{
					var keyframeNext = null;

					for (var g = f + 1; g < this.keyframes.length; g++)
					{
						var keyframeFuture = this.keyframes[g];
						var transformFuture = keyframeFuture.transforms[propertyName];
						if (transformFuture != null)
						{
							keyframeNext = keyframeFuture;
							break;
						}
					}

					if (keyframePrev != null && keyframeNext != null)
					{
						var transformPrev = keyframePrev.transforms[propertyName];
						var transformNext = keyframeNext.transforms[propertyName];

						var numberOfFramesFromPrevToNext = 
							keyframeNext.frameIndex
							- keyframePrev.frameIndex;

						var numberOfFramesFromPrevToCurrent = 
							keyframe.frameIndex
							- keyframePrev.frameIndex;

						var fractionOfProgressFromPrevToNext = 
							numberOfFramesFromPrevToCurrent 
							/ numberOfFramesFromPrevToNext;

						var transformNew = transformPrev.interpolateWith
						(
							transformNext,
							fractionOfProgressFromPrevToNext
						);
						transforms[propertyName] = transformNew;
						transforms.push(transformNew);
					}
				}
			}
		}		
	}
}

function AnimationKeyframe(frameIndex, transforms)
{
	this.frameIndex = frameIndex;
	this.transforms = transforms;

	ArrayHelper.addLookupsToArray(this.transforms, "propertyName");
}
{
	AnimationKeyframe.prototype.interpolateWith = function(other, fractionOfProgressTowardOther)
	{
		var transformsInterpolated = [];

		for (var i = 0; i < this.transforms.length; i++)
		{
			var transformThis = this.transforms[i];
			var transformOther = other.transforms[transformThis.propertyName];

			var transformInterpolated = transformThis.interpolateWith
			(
				transformOther,
				fractionOfProgressTowardOther
			);

			transformsInterpolated.push(transformInterpolated);
		}

		var returnValue = new AnimationKeyframe
		(
			null, // frameIndex
			transformsInterpolated
		);

		return returnValue;
	}
}

function AnimationRun(defn, transformable)
{
	this.defn = defn;	
	this.transformable = transformable;
}
{
	AnimationRun.prototype.frameCurrent = function()
	{
		var returnValue = null;

		var framesSinceBeginningOfCycle = 
			Globals.Instance.timerTicksSoFar 
			% this.defn.numberOfFramesTotal;

		var i;

		for (i = this.defn.keyframes.length - 1; i >= 0; i--)
		{
			keyframe = this.defn.keyframes[i];

			if (keyframe.frameIndex <= framesSinceBeginningOfCycle)
			{
				break;
			}
		}
		
		var keyframe = this.defn.keyframes[i];
		var framesSinceKeyframe = framesSinceBeginningOfCycle - keyframe.frameIndex;

		var keyframeNext = this.defn.keyframes[i + 1];
		var numberOfFrames = keyframeNext.frameIndex - keyframe.frameIndex;
		var fractionOfProgressFromKeyframeToNext = framesSinceKeyframe / numberOfFrames;

		returnValue = keyframe.interpolateWith
		(
			keyframeNext, 
			fractionOfProgressFromKeyframeToNext
		);

		return returnValue;
	}

	AnimationRun.prototype.updateForTimerTick = function()
	{
		var frameCurrent = this.frameCurrent();

		var transforms = frameCurrent.transforms;

		for (var i = 0; i < transforms.length; i++)
		{
			transforms[i].transform(this.transformable);
		}
	}
}

function ArrayHelper()
{
	// do nothing
}
{
	ArrayHelper.addLookupsToArray = function(arrayToAddLookupsTo, propertyPathForKey)
	{
		var propertyNameAncestryForKey = propertyPathForKey.split(".");

		for (var i = 0; i < arrayToAddLookupsTo.length; i++)
		{
			var item = arrayToAddLookupsTo[i];

			var keyValue = item[propertyNameAncestryForKey[0]];

			for (var p = 1; p < propertyNameAncestryForKey.length; p++)
			{
				var propertyNameForKey = propertyNameAncestryForKey[p];
				keyValue = keyValue[propertyNameForKey];
			}

			arrayToAddLookupsTo[keyValue] = item;
		}
	}
}

function Bone(name, length, orientation, children, isVisible)
{
	this.name = name;
	this.length = length;
	this.orientation = orientation;
	this.children = children;
	this.isVisible = (isVisible == null ? true : isVisible);

	for (var i = 0; i < this.children.length; i++)
	{
		var child = this.children[i];
		child.parent = this;
	}
}
{
	// instance methods

	Bone.prototype.pos = function(returnValue)
	{
		if (returnValue == null)
		{
			returnValue = new Coords(0, 0, 0);
		}
		else
		{
			returnValue.overwriteWithXYZ(0, 0, 0);
		}

		var bone = this.parent;

		while (bone != null)
		{
			returnValue.add
			(
				bone.orientation.forward.clone().multiplyScalar
				(
					bone.length
				)
			);

			bone = bone.parent;
		}

		return returnValue;
	}

	// cloneable

	Bone.prototype.clone = function()
	{
		return new Bone
		(
			this.name,
			this.length,
			this.orientation.clone(),
			Cloneable.cloneMany(this.children),
			this.isVisible
		);
	}

	Bone.prototype.overwriteWith = function(other)
	{
		this.orientation.overwriteWith(other.orientation);
		Cloneable.overwriteManyWithOthers(this.children, other.children);
	}
}

function Camera(name, focalLength, viewSize, loc)
{
	this.name = name;
	this.focalLength = focalLength;
	this.viewSize = viewSize;
	this.loc = loc;

	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
}
{
	Camera.prototype.convertWorldCoordsToViewCoords = function(coordsToConvert)
	{
		var orientation = this.loc.orientation;

		coordsToConvert.subtract
		(
			this.loc.pos
		).overwriteWithXYZ
		(
			coordsToConvert.dotProduct(orientation.right),
			coordsToConvert.dotProduct(orientation.down),
			coordsToConvert.dotProduct(orientation.forward)
		).add
		(
			this.viewSizeHalf
		);	
	}
}

function Cloneable()
{}
{
	Cloneable.cloneMany = function(cloneablesToClone)
	{
		var returnValues = [];

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

		return returnValues;
	}

	Cloneable.overwriteManyWithOthers = function(cloneablesToOverwrite, cloneablesToOverwriteWith)
	{
		for (var i = 0; i < cloneablesToOverwrite.length; i++)
		{
			cloneablesToOverwrite[i].overwriteWith(cloneablesToOverwriteWith[i]);
		}
	}
}

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.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		this.z /= other.z;

		return this;
	}

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

		return this;
	}

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

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

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

		return this;
	}

	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.overwriteWithXYZ = function(x, y, z)
	{
		this.x = x;
		this.y = y;
		this.z = z;

		return this;
	}

	Coords.prototype.rotateXYZ = function()
	{
		var temp = this.x;
		this.x = this.y;
		this.y = this.z;
		this.z = temp;

		return this;	
	}

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

		return this;
	}
}

function DisplayHelper()
{
	this.tempPos = new Coords(0, 0, 0);
	this.drawPos = new Coords(0, 0, 0);
}
{
	DisplayHelper.prototype.drawBoneForCamera = function(bone, camera)
	{
		var bonePos = bone.pos(this.tempPos);

		if (bone.isVisible == true)
		{
			this.graphics.beginPath();

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

			this.graphics.moveTo(this.drawPos.x, this.drawPos.y);

			bonePos.add
			(
				bone.orientation.forward.clone().multiplyScalar
				(
					bone.length
				)
			);

			camera.convertWorldCoordsToViewCoords
			(
				this.drawPos.overwriteWith(bonePos)
			);
	
			this.graphics.lineTo(this.drawPos.x, this.drawPos.y);
	
			this.graphics.stroke();
		}

		for (var i = 0; i < bone.children.length; i++)
		{
			var boneChild = bone.children[i];

			this.drawBoneForCamera
			(
				boneChild, 
				camera
			)
		}
	}

	DisplayHelper.prototype.drawSkeletonForCamera = function(skeleton, camera)
	{
		var viewSize = camera.viewSize;

		this.graphics.fillStyle = "Black";
		this.graphics.fillRect(0, 0, viewSize.x, viewSize.y);

		this.graphics.strokeStyle = "Blue";

		this.drawBoneForCamera
		(
			skeleton.boneRoot, 
			camera
		);
	}

	DisplayHelper.prototype.drawWorld = function(world)
	{
		this.drawSkeletonForCamera(world.skeleton, world.camera);
	}

	DisplayHelper.prototype.initialize = function(viewSize)
	{
		this.canvas = document.createElement("canvas");

		this.canvas.width = viewSize.x;
		this.canvas.height = viewSize.y;

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

		document.body.appendChild(this.canvas);
	}
}

function Globals()
{
	// do nothing	
}
{
	Globals.Instance = new Globals();

	Globals.prototype.handleEventTimerTick = function()
	{
		this.timerTicksSoFar++;

		this.world.updateForTimerTick();
		this.displayHelper.drawWorld(this.world);
	}

	Globals.prototype.initialize = function(world, timerTicksPerSecond)
	{
		this.world = world;

		this.displayHelper = new DisplayHelper();
		this.inputHelper = new InputHelper();

		this.displayHelper.initialize(this.world.camera.viewSize);
		this.inputHelper.initialize();

		this.timerTicksPerSecond = timerTicksPerSecond;
		this.millisecondsPerTimerTick = 1000 / this.timerTicksPerSecond;
		this.timerTicksSoFar = 0;
		
		setInterval
		(
			"Globals.Instance.handleEventTimerTick()", 
			this.millisecondsPerTimerTick
		)	
	}
}

function InputHelper()
{}
{
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var keyCode = event.keyCode;		
		var directionToRotate = 0;

		if (keyCode == 65) // a
		{
			directionToRotate = -1;	
		}
		else if (keyCode == 68) // d
		{
			directionToRotate = 1;			
		}

		if (directionToRotate != 0)
		{
			// hack
			// Camera rotation could be handled more elegantly
			// using motion constraints of some kind,
			// but this gets the job done for our purposes.

			var camera = Globals.Instance.world.camera;
			var cameraLoc = camera.loc;
			var cameraPos = cameraLoc.pos;
			var cameraZOriginal = cameraPos.z;

			var cyclesToRotate = .02;

			var quaternionForRotation = Quaternion.fromAxisAndCyclesToRotate
			(
				new Coords(0, 0, 1),
				cyclesToRotate * directionToRotate
			);

			quaternionForRotation.applyToCoordsAsRotation(cameraPos);
		
			camera.loc.orientation = new Orientation
			(
				cameraPos.clone().add
				(
					new Coords(0, 0, 0 - cameraZOriginal / 4)
				).multiplyScalar
				(
					-1
				),

				new Coords(0, 0, 1) // down
			);
		}
	}

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

function Location(pos, orientation)
{
	this.pos = pos;
	this.orientation = orientation;
}

function Mesh(name, vertices, vertexIndicesForFaces)
{
	this.name = name;
	this.vertices = vertices;
	this.vertexIndicesForFaces = vertexIndicesForFaces;
}
{
	Mesh.prototype.clone = function()
	{
		return new Mesh
		(
			this.name,
			Cloneable.cloneMany(this.vertices),
			this.vertexIndicesForFaces.slice()
		);
	}
}

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

	this.down = this.forward.crossProduct(this.right).normalize();

	this.axes = 
	[
		this.forward,
		this.right,
		this.down,
	];
}
{
	Orientation.prototype.clone = function()
	{
		return new Orientation
		(
			this.forward,
			this.down
		);
	}

	Orientation.prototype.overwriteWith = function(other)
	{
		this.forward.overwriteWith(other.forward);
		this.right.overwriteWith(other.right);
		this.down.overwriteWith(other.down);

		return this;
	}
}

function Quaternion(w, x, y, z)
{
	this.w = w;
	this.x = x;
	this.y = y;
	this.z = z;
}
{
	// static methods

	Quaternion.fromAxisAndCyclesToRotate = function(axisToRotateAround, cyclesToRotate)
	{
		var radiansToRotateHalf = cyclesToRotate * Math.PI;		

		var sineOfRadiansToRotateHalf = Math.sin(radiansToRotateHalf);
	
		var w = Math.cos(radiansToRotateHalf);
		var x = axisToRotateAround.x * sineOfRadiansToRotateHalf;
		var y = axisToRotateAround.y * sineOfRadiansToRotateHalf;
		var z = axisToRotateAround.z * sineOfRadiansToRotateHalf;

		var returnValue = new Quaternion(w, x, y, z).normalize();

		return returnValue;
	}


	// instance methods

	Quaternion.prototype.applyToCoordsAsRotation = function(coordsToRotate)
	{
		var coordsToRotateAsQuaternion = new Quaternion
		(
			0,
			coordsToRotate.x,
			coordsToRotate.y,
			coordsToRotate.z
		);

		var result = this.clone().multiply
		(
			coordsToRotateAsQuaternion	
		).multiply
		(
			this.clone().invert()
		);

		coordsToRotate.overwriteWithXYZ(result.x, result.y, result.z);

		return coordsToRotate;
	}	

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

	Quaternion.prototype.divide = function(divisor)
	{
		this.w /= divisor;
		this.x /= divisor;
		this.y /= divisor;
		this.z /= divisor;

		return this;
	}

	Quaternion.prototype.invert = function()
	{
		var magnitude = this.magnitude();

		this.divide(magnitude * magnitude);

		this.x *= -1;
		this.y *= -1;
		this.z *= -1;

		return this;
	}

	Quaternion.prototype.multiply = function(other)
	{
		return this.overwriteWithWXYZ
		(
			this.w * other.w - this.x * other.x - this.y * other.y - this.z * other.z,
			this.w * other.x + this.x * other.w + this.y * other.z - this.z * other.y,
			this.w * other.y - this.x * other.z + this.y * other.w + this.z * other.x,
			this.w * other.z + this.x * other.y - this.y * other.x + this.z * other.w
		);
	}

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

	Quaternion.prototype.normalize = function()
	{
		return this.divide(this.magnitude());
	}

	Quaternion.prototype.overwriteWith = function(other)
	{
		this.overwriteWithWXYZ(other.w, other.x, other.y, other.z);

		return this;
	}

	Quaternion.prototype.overwriteWithWXYZ = function(w,  x,  y,  z)
	{
		this.w = w;
		this.x = x;
		this.y = y;
		this.z = z;

		return this;
	}
}

function Skeleton(name, boneRoot)
{
	this.name = name;
	this.boneRoot = boneRoot;

	this.bonesAll = [];
	this.bonesAll = TreeHelper.addNodeAndAllDescendantsToList
	(
		this.boneRoot, []
	);
	ArrayHelper.addLookupsToArray(this.bonesAll, "name");
}
{
	Skeleton.prototype.clone = function(other)
	{
		return new Skeleton
		(
			this.name,
			this.boneRoot.clone()
		);
	}

	Skeleton.prototype.overwriteWith = function(other)
	{
		for (var i = 0; i < this.bonesAll.length; i++)
		{
			this.bonesAll[i].overwriteWith(other.bonesAll[i]);
		}
	}
}

function TransformBonePose(boneName, cyclesToRotateAroundAxesDownRightForward)
{
	this.boneName = boneName;
	this.cyclesToRotateAroundAxesDownRightForward = cyclesToRotateAroundAxesDownRightForward;

	this.propertyName = this.boneName;
}
{
	// instance methods

	TransformBonePose.prototype.clone = function()
	{
		return new TransformBonePose
		(
			this.boneName, 
			this.cyclesToRotateAroundAxesDownRightForward
		);
	}

	TransformBonePose.prototype.interpolateWith = function(other, fractionOfProgressTowardOther)
	{
		var cyclesToRotateAroundAxesDownRightForwardInterpolated = [];

		for (var i = 0; i < this.cyclesToRotateAroundAxesDownRightForward.length; i++)
		{
			var cyclesToRotateInterpolated = 
				(1 - fractionOfProgressTowardOther) * this.cyclesToRotateAroundAxesDownRightForward[i]
				+ fractionOfProgressTowardOther * other.cyclesToRotateAroundAxesDownRightForward[i];

			cyclesToRotateAroundAxesDownRightForwardInterpolated[i] = cyclesToRotateInterpolated;
		}

		var returnValue = new TransformBonePose
		(
			this.boneName,
			cyclesToRotateAroundAxesDownRightForwardInterpolated
		);	

		return returnValue;
	}

	TransformBonePose.prototype.transform = function(transformableToTransform)
	{
		var skeletonToTransform = transformableToTransform;

		var boneToTransform = skeletonToTransform.bonesAll[this.boneName];
		var boneOrientation = boneToTransform.orientation;	

		var axesToRotateAround = 
		[
			boneOrientation.down,
			boneOrientation.right,
			boneOrientation.forward,
		];

		var quaternionsForRotation = [];

		for (var i = 0; i < this.cyclesToRotateAroundAxesDownRightForward.length; i++)
		{
			var axisToRotateAround = axesToRotateAround[i];
			var cyclesToRotateAroundAxis = this.cyclesToRotateAroundAxesDownRightForward[i];

			if (cyclesToRotateAroundAxis != 0)
			{
				var quaternionForRotation = Quaternion.fromAxisAndCyclesToRotate
				(
					axisToRotateAround, 
					cyclesToRotateAroundAxis
				);

				quaternionsForRotation.push(quaternionForRotation);
			}
		}

		this.transform_Bone(quaternionsForRotation, boneToTransform)
	}

	TransformBonePose.prototype.transform_Bone = function(quaternionsForRotation, boneToTransform)
	{	
		var axesToTransform = boneToTransform.orientation.axes;

		for (var i = 0; i < quaternionsForRotation.length; i++)
		{
			for (var a = 0; a < axesToTransform.length; a++)
			{
				var axisToTransform = axesToTransform[a];

				quaternionsForRotation[i].applyToCoordsAsRotation
				(
					axisToTransform		
				);
			}
		}

		for (var i = 0; i < boneToTransform.children.length; i++)
		{
			var childBone = boneToTransform.children[i];
			this.transform_Bone(quaternionsForRotation, childBone);
		}
	}
}

function TransformCamera(camera)
{
	this.camera = camera;
}
{
	TransformCamera.prototype.transformCoords = function(coordsToTransform)
	{
		this.camera.convertWorldCoordsToViewCoords(coordsToTransform);
	}
}

function TreeHelper()
{}
{
	TreeHelper.addNodeAndAllDescendantsToList = function(node, listToAddTo)
	{
		listToAddTo.push(node);

		for (var i = 0; i < node.children.length; i++)
		{
			var nodeChild = node.children[i];
			TreeHelper.addNodeAndAllDescendantsToList
			(
				nodeChild, 
				listToAddTo
			);
		}

		return listToAddTo;
	}
}

function World(skeleton, animationDefn, camera, secondsPerRunCycle)
{
	this.skeletonBase = skeleton;
	this.skeleton = this.skeletonBase.clone();
	this.animationDefn = animationDefn;
	this.camera = camera;
	this.secondsPerRunCycle = secondsPerRunCycle;

	this.animationRun = new AnimationRun
	(
		this.animationDefn,
		this.skeleton
	);
}
{
	World.prototype.updateForTimerTick = function()
	{
		this.skeleton.overwriteWith(this.skeletonBase);
		this.animationRun.updateForTimerTick();
	}
}

// run

main();


</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