// @@REWRITE(insert js-copyright) // @@REWRITE(delete-start) // Copyright 2009 Google Inc. All Rights Reserved // @@REWRITE(delete-end) /** * @fileoverview This file contains all the math for the siteswap animator. It * handles all of the site-swap-related stuff [converting a sequence of integers * into a more-useful representation of a pattern, pattern validation, etc.] as * well as all the physics used for the simulation. */ /** * This is a container class that holds the coefficients of an equation * describing the motion of an object. * The basic equation is: * f(x) := a t^2 + b t + c + d sin (f t) + e cos (f t). * However, sometimes we LERP between that function and this one: * g(x) := lA t^2 + lB t + lC * lerpRate [so far] is always either 1 [LERP from f to g over 1 beat] or -1, * [LERP from g to f over one beat]. * * Just plug in t to evaluate the equation. There's no JavaScript function to * do this because it's always done on the GPU. * * @constructor */ EquationCoefficients = function(a, b, c, d, e, f, lA, lB, lC, lerpRate) { assert(!isNaN(a) && !isNaN(b) && !isNaN(c)); d = d || 0; e = e || 0; f = f || 0; assert(!isNaN(d) && !isNaN(e) && !isNaN(f)); lA = lA || 0; lB = lB || 0; lC = lC || 0; assert(!isNaN(lA) && !isNaN(lB) && !isNaN(lC)); lerpRate = lerpRate || 0; this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f; this.lA = lA; this.lB = lB; this.lC = lC; this.lerpRate = lerpRate; } /** * Create a new equation that's equivalent to this equation's coefficients a-f * with a LERP to the polynomial portion of the supplied equation. * @param {!EquationCoefficients} eqn the source of coefficients. * @param {!number} lerpRate the rate and direction of the LERP; positive for * "from this equation to the new one" and vice-versa. * @return {!EquationCoefficients} a new set of coefficients. */ EquationCoefficients.prototype.lerpIn = function(eqn, lerpRate) { assert(!this.lerpRate); return new EquationCoefficients(this.a, this.b, this.c, this.d, this.e, this.f, eqn.a, eqn.b, eqn.c, lerpRate); }; /** * Convert the EquationCoefficients to a string for debugging. * @return {String} debugging output. */ EquationCoefficients.prototype.toString = function() { return 'F(t) := ' + this.a.toFixed(2) + ' t^2 + ' + this.b.toFixed(2) + ' t + ' + this.c.toFixed(2) + ' + ' + this.d.toFixed(2) + ' sin(' + this.f.toFixed(2) + ' t) + ' + this.e.toFixed(2) + ' cos(' + this.f.toFixed(2) + ' t) + LERP(' + this.lerpRate.toFixed(2) + ') of ' + this.lA.toFixed(2) + ' t^2 + ' + this.lB.toFixed(2) + ' t + ' + this.lC.toFixed(2); }; /** * A set of equations which describe the motion of an object over time. * The three equations each supply one dimension of the motion, and the curve is * valid from startTime to startTime + duration. * @param {!number} startTime the initial time at which the curve is valid. * @param {!number} duration how long [in beats] the curve is valid. * @param {!EquationCoefficients} xEqn the equation for motion in x. * @param {!EquationCoefficients} yEqn the equation for motion in y. * @param {!EquationCoefficients} zEqn the equation for motion in z. * @constructor */ Curve = function(startTime, duration, xEqn, yEqn, zEqn) { this.startTime = startTime; this.duration = duration; this.xEqn = xEqn; this.yEqn = yEqn; this.zEqn = zEqn; } /** * Convert the Curve to a string for debugging. * @return {String} debugging output. */ Curve.prototype.toString = function() { var s = 'startTime: ' + this.startTime + '\n'; s += 'duration: ' + this.duration + '\n'; s += this.xEqn + '\n'; s += this.yEqn + '\n'; s += this.zEqn + '\n'; return s; }; /** * Modify this curve's coefficients to include a LERP to the polynomial * portion of the supplied curve. * @param {!Curve} curve the source of coefficients. * @param {!number} lerpRate the rate and direction of the LERP; positive for * "from this equation to the new one" and vice-versa. * @return {!Curve} a new curve. */ Curve.prototype.lerpIn = function(curve, lerpRate) { assert(this.startTime == curve.startTime); assert(this.duration == curve.duration); var xEqn = this.xEqn.lerpIn(curve.xEqn, lerpRate); var yEqn = this.yEqn.lerpIn(curve.yEqn, lerpRate); var zEqn = this.zEqn.lerpIn(curve.zEqn, lerpRate); return new Curve(this.startTime, this.duration, xEqn, yEqn, zEqn); }; /** * Produce a set of polynomial coefficients that describe linear motion between * two points in 1 dimension. * @param {!number} startPos the starting position. * @param {!number} endPos the ending position. * @param {!number} duration how long the motion takes. * @return {!EquationCoefficients} the equation for the motion. */ Curve.computeLinearCoefficients = function(startPos, endPos, duration) { return new EquationCoefficients( 0, (endPos - startPos) / duration, startPos); } var GRAVITY = 1; // Higher means higher throws for the same duration. /** * Produce a set of polynomial coefficients that describe parabolic motion * between two points in 1 dimension. * @param {!number} startPos the starting position. * @param {!number} endPos the ending position. * @param {!number} duration how long the motion takes. * @return {!EquationCoefficients} the equation for the motion. */ Curve.computeParabolicCoefficients = function(startPos, endPos, duration) { var dY = endPos - startPos; return new EquationCoefficients(-GRAVITY / 2, dY / duration + GRAVITY * duration / 2, startPos); } /** * Compute the curve taken by a ball given its throw and catch positions, the * time it was thrown, and how long it stayed in the air. * * We use duration rather than throwTime and catchTime because, what * with the modular arithmetic used in our records, catchTime might be before * throwTime, and in some representations the pattern could wrap around a few * times while the ball's in the air. When the parabola computed here is used, * time must be supplied as an offset from the time of the throw, and must of * course not wrap at all. That is, these coefficients work for f(0) == * throwPos, f(duration) == catchPos. * * We treat the y axis as vertical and thus affected by gravity. * * @param {!EquationCoefficients} throwPos * @param {!EquationCoefficients} catchPos * @param {!number} startTime * @param {!number} duration * @return {!Curve} */ Curve.computeThrowCurve = function(throwPos, catchPos, startTime, duration) { var xEqn = Curve.computeLinearCoefficients(throwPos.x, catchPos.x, duration); var yEqn = Curve.computeParabolicCoefficients(throwPos.y, catchPos.y, duration); var zEqn = Curve.computeLinearCoefficients(throwPos.z, catchPos.z, duration); return new Curve(startTime, duration, xEqn, yEqn, zEqn); } /** * Compute a straight line Curve given start and end positions, the start time, * and the duration of the motion. * * @param {!EquationCoefficients} startPos * @param {!EquationCoefficients} endPos * @param {!number} startTime * @param {!number} duration * @return {!Curve} */ Curve.computeStraightLineCurve = function(startPos, endPos, startTime, duration) { var xEqn = Curve.computeLinearCoefficients(startPos.x, endPos.x, duration); var yEqn = Curve.computeLinearCoefficients(startPos.y, endPos.y, duration); var zEqn = Curve.computeLinearCoefficients(startPos.z, endPos.z, duration); return new Curve(startTime, duration, xEqn, yEqn, zEqn); } /** * Threshold horizontal distance below which computeCircularCurve won't bother * trying to approximate a circular curve. See the comment above * computeCircularCurve for more info. * @type {number} */ Curve.EPSILON = .0001; /** * Compute a circular curve, used as an approximation for the motion of a hand * between a catch and its following throw. * * Assumes a lot of stuff about this looking like a "normal" throw: the catch is * moving roughly the opposite direction as the throw, the throw and catch * aren't at the same place, and such. Otherwise this looks very odd at best. * This is used for the height of the curve. * This produces coefficients for d sin(f t) + e cos(f t) for each of x, y, z. * It produces a vertical-ish circular curve from the start to the end, going * down, then up. So if dV [the distance from the start to finish in the x-z * plane, ignoring y] is less than Curve.EPSILON, it doesn't know which way down * is, and it bails by returning a straight line instead. */ Curve.computeCircularCurve = function(startPos, endPos, startTime, duration) { var dX = endPos.x - startPos.x; var dY = endPos.y - startPos.y; var dZ = endPos.z - startPos.z; var dV = Math.sqrt(dX * dX + dZ * dZ); if (dV < Curve.EPSILON) { return Curve.computeStraightLineCurve(startPos, endPos, startTime, duration); } var negHalfdV = -0.5 * dV; var negHalfdY = -0.5 * dY; var f = Math.PI / duration; var yEqn = new EquationCoefficients( 0, 0, startPos.y + dY / 2, negHalfdV, negHalfdY, f); var ratio = dX / dV; var xEqn = new EquationCoefficients( 0, 0, startPos.x + dX / 2, negHalfdY * ratio, negHalfdV * ratio, f); ratio = dZ / dV; var zEqn = new EquationCoefficients( 0, 0, startPos.z + dZ / 2, negHalfdY * ratio, negHalfdV * ratio, f); return new Curve(startTime, duration, xEqn, yEqn, zEqn); } /** * This is the abstract base class for an object that describes a throw, catch, * or empty hand [placeholder] in a site-swap pattern. * @constructor */ Descriptor = function() { } /** * Create an otherwise-identical copy of this descriptor at a given time offset. * Note that offset may put time past patternLength; the caller will have to fix * this up manually. * @param {number} offset how many beats to offset the new descriptor. * Derived classes must override this function. */ Descriptor.prototype.clone = function(offset) { throw new Error('Unimplemented.'); }; /** * Generate the Curve implied by this descriptor and the supplied hand * positions. * @param {!Array.HandPositionRecord} handPositions where the hands will be. * Derived classes must override this function. */ Descriptor.prototype.generateCurve = function(handPositions) { throw new Error('Unimplemented.'); }; /** * Adjust the start time of this Descriptor to be in [0, pathLength). * @param {!number} pathLength the duration of a path, in beats. * @return {!Descriptor} this. */ Descriptor.prototype.fixUpModPathLength = function(pathLength) { this.time = this.time % pathLength; return this; }; /** * This describes a throw in a site-swap pattern. * @param {!number} throwNum the site-swap number of the throw. * @param {!number} throwTime the time this throw occurs. * @param {!number} sourceHand the index of the throwing hand. * @param {!number} destHand the index of the catching hand. * @constructor */ ThrowDescriptor = function(throwNum, throwTime, sourceHand, destHand) { this.throwNum = throwNum; this.sourceHand = sourceHand; this.destHand = destHand; this.time = throwTime; } /** * This is a subclass of Descriptor. */ ThrowDescriptor.prototype = new Descriptor(); /** * Set up the constructor, just to be neat. */ ThrowDescriptor.prototype.constructor = ThrowDescriptor; /** * We label each Descriptor subclass with a type for debugging. */ ThrowDescriptor.prototype.type = 'THROW'; /** * Create an otherwise-identical copy of this descriptor at a given time offset. * Note that offset may put time past patternLength; the caller will have to fix * this up manually. * @param {number} offset how many beats to offset the new descriptor. * @return {!Descriptor} the new copy. */ ThrowDescriptor.prototype.clone = function(offset) { offset = offset || 0; // Turn null into 0. return new ThrowDescriptor(this.throwNum, this.time + offset, this.sourceHand, this.destHand); }; /** * Convert the ThrowDescriptor to a string for debugging. * @return {String} debugging output. */ ThrowDescriptor.prototype.toString = function() { return '(' + this.throwNum + ' from hand ' + this.sourceHand + ' to hand ' + this.destHand + ')'; }; /** * Generate the Curve implied by this descriptor and the supplied hand * positions. * @param {!Array.HandPositionRecord} handPositions where the hands will be. * @return {!Curve} the curve. */ ThrowDescriptor.prototype.generateCurve = function(handPositions) { var startPos = handPositions[this.sourceHand].throwPositions[this.destHand]; var endPos = handPositions[this.destHand].catchPosition; return Curve.computeThrowCurve(startPos, endPos, this.time, this.throwNum - 1); }; /** * This describes a catch in a site-swap pattern. * @param {!number} hand the index of the catching hand. * @param {!number} sourceThrowNum the site-swap number of the preceeding throw. * @param {!number} destThrowNum the site-swap number of the following throw. * @param {!number} sourceHand the index of the hand throwing the source throw. * @param {!number} destHand the index of the hand catching the following throw. * @param {!number} catchTime the time at which the catch occurs. * @constructor */ CarryDescriptor = function(hand, sourceThrowNum, destThrowNum, sourceHand, destHand, catchTime) { this.hand = hand; this.sourceThrowNum = sourceThrowNum; this.destThrowNum = destThrowNum; this.sourceHand = sourceHand; this.destHand = destHand; this.time = catchTime; } /** * This is a subclass of Descriptor. */ CarryDescriptor.prototype = new Descriptor(); /** * Set up the constructor, just to be neat. */ CarryDescriptor.prototype.constructor = CarryDescriptor; /** * We label each Descriptor subclass with a type for debugging. */ CarryDescriptor.prototype.type = 'CARRY'; /** * Since this gets pathLength, not patternLength, we'll have to collapse sets * of CarryDescriptors later, as they may be spread sparsely through the full * animation and we'll only want them to be distributed over the full pattern * length. We may have dupes to throw away as well. * @param {!ThrowDescriptor} inThrowDescriptor * @param {!ThrowDescriptor} outThrowDescriptor * @param {!number} pathLength * @return {!CarryDescriptor} */ CarryDescriptor.fromThrowDescriptors = function(inThrowDescriptor, outThrowDescriptor, pathLength) { assert(inThrowDescriptor.destHand == outThrowDescriptor.sourceHand); assert((inThrowDescriptor.time + inThrowDescriptor.throwNum) % pathLength == outThrowDescriptor.time); return new CarryDescriptor(inThrowDescriptor.destHand, inThrowDescriptor.throwNum, outThrowDescriptor.throwNum, inThrowDescriptor.sourceHand, outThrowDescriptor.destHand, (outThrowDescriptor.time + pathLength - 1) % pathLength); }; /** * Create an otherwise-identical copy of this descriptor at a given time offset. * Note that offset may put time past patternLength; the caller will have to fix * this up manually. * @param {number} offset how many beats to offset the new descriptor. * @return {!Descriptor} the new copy. */ CarryDescriptor.prototype.clone = function(offset) { offset = offset || 0; // Turn null into 0. return new CarryDescriptor(this.hand, this.sourceThrowNum, this.destThrowNum, this.sourceHand, this.destHand, this.time + offset); }; /** * Convert the CarryDescriptor to a string for debugging. * @return {String} debugging output. */ CarryDescriptor.prototype.toString = function() { return 'time: ' + this.time + ' (hand ' + this.hand + ' catches ' + this.sourceThrowNum + ' from hand ' + this.sourceHand + ' then throws ' + this.destThrowNum + ' to hand ' + this.destHand + ')'; }; /** * Test if this CarryDescriptor is equivalent to another, mod patternLength. * @param {!CarryDescriptor} cd the other CarryDescriptor. * @param {!number} patternLength the length of the pattern. * @return {!bool} */ CarryDescriptor.prototype.equalsWithMod = function(cd, patternLength) { if (!(cd instanceof CarryDescriptor)) { return false; } if (this.hand != cd.hand) { return false; } if (this.sourceThrowNum != cd.sourceThrowNum) { return false; } if (this.destThrowNum != cd.destThrowNum) { return false; } if (this.sourceHand != cd.sourceHand) { return false; } if (this.destHand != cd.destHand) { return false; } if (this.time % patternLength != cd.time % patternLength) { return false; } return true; }; /** * Generate the Curve implied by this descriptor and the supplied hand * positions. * @param {!Array.HandPositionRecord} handPositions where the hands will be. * @return {!Curve} the curve. */ CarryDescriptor.prototype.generateCurve = function(handPositions) { var startPos = handPositions[this.hand].catchPosition; var endPos = handPositions[this.hand].throwPositions[this.destHand]; return Curve.computeCircularCurve(startPos, endPos, this.time, 1); }; /** * This describes a carry of a "1" in a site-swap pattern. * The flags isThrow and isCatch tell whether this is the actual 1 [isThrow] or * the carry that receives the handoff [isCatch]. It's legal for both to be * true, which happens when there are two 1s in a row. * @param {!number} sourceThrowNum the site-swap number of the prev throw * [including this one if isCatch]. * @param {!number} sourceHand the index of the hand throwing sourceThrowNum. * @param {!number} destThrowNum the site-swap number of the next throw * [including this one if isThrow]. * @param {!number} destHand the index of the hand catching destThrowNum. * @param {!number} hand the index of the hand doing this carry. * @param {!number} time the time at which the carry starts. * @param {!bool} isThrow whether this is a 1. * @param {!bool} isCatch whether this is the carry after a 1. * @constructor */ CarryOneDescriptor = function(sourceThrowNum, sourceHand, destThrowNum, destHand, hand, time, isThrow, isCatch) { // It's possible to have !isCatch with sourceThrowNum == 1 temporarily, if we // just haven't handled that 1 yet [we're doing the throw of this one, and // will later get to the previous one, due to wraparound], and vice-versa. assert(isThrow || (sourceThrowNum == 1)); assert(isCatch || (destThrowNum == 1)); this.sourceThrowNum = sourceThrowNum; this.sourceHand = sourceHand; this.destHand = destHand; this.destThrowNum = destThrowNum; this.hand = hand; this.time = time; this.isThrow = isThrow; this.isCatch = isCatch; return this; } /** * This is a subclass of Descriptor. */ CarryOneDescriptor.prototype = new Descriptor(); /** * Set up the constructor, just to be neat. */ CarryOneDescriptor.prototype.constructor = CarryOneDescriptor; /** * We label each Descriptor subclass with a type for debugging. */ CarryOneDescriptor.prototype.type = 'CARRY_ONE'; /** * Create a pair of CarryOneDescriptors to describe the carry that is a throw of * 1. A 1 spends all its time being carried, so these two carries surrounding * it represent [and therefore don't have] a throw between them. * Prev and post are generally the ordinary CarryDescriptors surrounding the * throw of 1 that we're trying to implement. However, they could each [or * both] independently be CarryOneDescriptors implementing other 1 throws. * @param {!Descriptor} prev the carry descriptor previous to the 1. * @param {!Descriptor} post the carry descriptor subsequent to the 1. * @return {!Array.CarryOneDescriptor} a pair of CarryOneDescriptors. */ CarryOneDescriptor.getDescriptorPair = function(prev, post) { assert(prev instanceof CarryDescriptor || prev instanceof CarryOneDescriptor); assert(post instanceof CarryDescriptor || post instanceof CarryOneDescriptor); assert(prev.destHand == post.hand); assert(prev.hand == post.sourceHand); var newPrev; var newPost; if (prev instanceof CarryOneDescriptor) { assert(prev.isCatch && !prev.isThrow); newPrev = prev; newPrev.isThrow = true; assert(newPrev.destHand == post.hand); } else { newPrev = new CarryOneDescriptor(prev.sourceThrowNum, prev.sourceHand, 1, post.hand, prev.hand, prev.time, true, false); } if (post instanceof CarryOneDescriptor) { assert(post.isThrow && !post.isCatch); newPost = post; newPost.isCatch = true; assert(newPost.sourceHand == prev.hand); assert(newPost.sourceThrowNum == 1); } else { newPost = new CarryOneDescriptor(1, prev.hand, post.destThrowNum, post.destHand, post.hand, post.time, false, true); } return [newPrev, newPost]; }; /** * Convert the CarryOneDescriptor to a string for debugging. * @return {String} debugging output. */ CarryOneDescriptor.prototype.toString = function() { var s; if (this.isThrow) { s = 'Hand ' + this.hand + ' catches a ' + this.sourceThrowNum + ' from ' + this.sourceHand + ' at time ' + this.time + ' and then passes a 1 to ' + this.destHand + '.'; } else { assert(this.isCatch && this.sourceThrowNum == 1); s = 'Hand ' + this.hand + ' catches a 1 from ' + this.sourceHand + ' at time ' + this.time + ' and then passes a ' + this.destThrowNum + ' to ' + this.destHand + '.'; } return s; }; /** * Compute the curve taken by a ball during the carry representing a 1, as long * as it's not both a catch and a throw of a 1, which is handled elsewhere. * It's either a LERP from a circular curve [a catch of a throw > 1] to a * straight line to the handoff point [for isThrow] or a LERP from a straight * line from the handoff to a circular curve for the next throw > 1 [for * isCatch]. * * @param {!EquationCoefficients} catchPos * @param {!EquationCoefficients} throwPos * @param {!EquationCoefficients} handoffPos * @param {!number} startTime * @param {!bool} isCatch whether this is the carry after a 1. * @param {!bool} isThrow whether this is a 1. * @return {!Curve} */ Curve.computeCarryOneCurve = function(catchPos, throwPos, handoffPos, startTime, isCatch, isThrow) { assert(!isCatch != !isThrow); var curve = Curve.computeCircularCurve(catchPos, throwPos, startTime, 1); var curve2 = Curve.computeStraightLineCurve(handoffPos, handoffPos, startTime, 1); return curve.lerpIn(curve2, isThrow ? 1 : -1); } /** * Compute the curve taken by a ball during the carry representing a 1 that is * both the catch of one 1 and the immediately-following throw of another 1. * * @param {!EquationCoefficients} leadingHandoffPos * @param {!EquationCoefficients} trailingHandoffPos * @param {!Array.HandPositionRecord} handPositions where the hands will be. * @param {!number} hand * @param {!number} time the time at which the first 1's catch takes place. * @return {!Curve} */ Curve.computeConsecutiveCarryOneCurve = function(leadingHandoffPos, trailingHandoffPos, handPositions, hand, time) { var curve = Curve.computeStraightLineCurve(leadingHandoffPos, handPositions[hand].basePosition, time, 1); var curve2 = Curve.computeStraightLineCurve(handPositions[hand].basePosition, trailingHandoffPos, time, 1); return curve.lerpIn(curve2, 1); } /** * Generate the Curve implied by this descriptor and the supplied hand * positions. * @param {!Array.HandPositionRecord} handPositions where the hands will be. * @return {!Curve} the curve. */ CarryOneDescriptor.prototype.generateCurve = function(handPositions) { var leadingHandoffPos, trailingHandoffPos; if (this.isCatch) { var p0 = handPositions[this.hand].basePosition; var p1 = handPositions[this.sourceHand].basePosition; handoffPos = leadingHandoffPos = p0.add(p1).scale(0.5); } if (this.isThrow) { var p0 = handPositions[this.hand].basePosition; var p1 = handPositions[this.destHand].basePosition; handoffPos = trailingHandoffPos = p0.add(p1).scale(0.5); } if (!this.isCatch || !this.isThrow) { return Curve.computeCarryOneCurve(handPositions[this.hand].catchPosition, handPositions[this.hand].throwPositions[this.destHand], handoffPos, this.time, this.isCatch, this.isThrow); } else { return Curve.computeConsecutiveCarryOneCurve(leadingHandoffPos, trailingHandoffPos, handPositions, this.hand, this.time); } }; /** * Create an otherwise-identical copy of this descriptor at a given time offset. * Note that offset may put time past patternLength; the caller will have to fix * this up manually. * @param {number} offset how many beats to offset the new descriptor. * @return {!Descriptor} the new copy. */ CarryOneDescriptor.prototype.clone = function(offset) { offset = offset || 0; // Turn null into 0. return new CarryOneDescriptor(this.sourceThrowNum, this.sourceHand, this.destThrowNum, this.destHand, this.hand, this.time + offset, this.isThrow, this.isCatch); }; /** * Test if this CarryOneDescriptor is equivalent to another, mod patternLength. * @param {!CarryOneDescriptor} cd the other CarryOneDescriptor. * @param {!number} patternLength the length of the pattern. * @return {!bool} */ CarryOneDescriptor.prototype.equalsWithMod = function(cd, patternLength) { if (!(cd instanceof CarryOneDescriptor)) { return false; } if (this.hand != cd.hand) { return false; } if (this.sourceThrowNum != cd.sourceThrowNum) { return false; } if (this.destThrowNum != cd.destThrowNum) { return false; } if (this.sourceHand != cd.sourceHand) { return false; } if (this.destHand != cd.destHand) { return false; } if (this.isCatch != cd.isCatch) { return false; } if (this.isThrow != cd.isThrow) { return false; } if (this.time % patternLength != cd.time % patternLength) { return false; } return true; }; /** * This describes an empty hand in a site-swap pattern. * @param {!Descriptor} cd0 the CarryDescriptor or CarryOneDescriptor describing * this hand immediately before it was emptied. * @param {!Descriptor} cd1 the CarryDescriptor or CarryOneDescriptor describing * this hand immediately after it's done being empty. * @param {!number} patternLength the length of the pattern. * @constructor */ EmptyHandDescriptor = function(cd0, cd1, patternLength) { assert(cd0.hand == cd1.hand); this.hand = cd0.hand; this.prevThrowDest = cd0.destHand; this.sourceThrowNum = cd0.destThrowNum; this.nextCatchSource = cd1.sourceHand; this.destThrowNum = cd1.sourceThrowNum; // This code assumes that each CarryDescriptor and CarryOneDescriptor always // has a duration of 1 beat. If we want to be able to allow long-held balls // [instead of thrown twos, for example], we'll have to fix that here and a // number of other places. this.time = (cd0.time + 1) % patternLength; this.duration = cd1.time - this.time; if (this.duration < 0) { this.duration += patternLength; assert(this.duration > 0); } } /** * This is a subclass of Descriptor. */ EmptyHandDescriptor.prototype = new Descriptor(); /** * Set up the constructor, just to be neat. */ EmptyHandDescriptor.prototype.constructor = EmptyHandDescriptor; /** * We label each Descriptor subclass with a type for debugging. */ EmptyHandDescriptor.prototype.type = 'EMPTY'; /** * Convert the EmptyHandDescriptor to a string for debugging. * @return {String} debugging output. */ EmptyHandDescriptor.prototype.toString = function() { return 'time: ' + this.time + ' for ' + this.duration + ' (hand ' + this.hand + ', after throwing a ' + this.sourceThrowNum + ' to hand ' + this.prevThrowDest + ' then catches a ' + this.destThrowNum + ' from hand ' + this.nextCatchSource + ')'; }; /** * Generate the Curve implied by this descriptor and the supplied hand * positions. * @param {!Array.HandPositionRecord} handPositions where the hands will be. * @return {!Curve} the curve. */ EmptyHandDescriptor.prototype.generateCurve = function(handPositions) { var startPos, endPos; if (this.sourceThrowNum == 1) { var p0 = handPositions[this.hand].basePosition; var p1 = handPositions[this.prevThrowDest].basePosition; startPos = p0.add(p1).scale(0.5); } else { startPos = handPositions[this.hand].throwPositions[this.prevThrowDest]; } if (this.destThrowNum == 1) { var p0 = handPositions[this.hand].basePosition; var p1 = handPositions[this.nextCatchSource].basePosition; endPos = p0.add(p1).scale(0.5); } else { endPos = handPositions[this.hand].catchPosition; } // TODO: Replace with a good empty-hand curve. return Curve.computeStraightLineCurve(startPos, endPos, this.time, this.duration); }; /** * A series of descriptors that describes the full path of an object during a * pattern. * @param {!Array.Descriptor} descriptors all descriptors for the object. * @param {!number} pathLength the length of the path in beats. * @constructor */ Path = function(descriptors, pathLength) { this.descriptors = descriptors; this.pathLength = pathLength; } /** * Create a Path representing a ball, filling in the gaps between the throws * with carry descriptors. Since it's a ball's path, there are no * EmptyHandDescriptors in the output. * @param {!Array.ThrowDescriptor} throwDescriptors the ball's part of the * pattern. * @param {!number} pathLength the length of the pattern in beats. * @return {!Path} the ball's full path. */ Path.ballPathFromThrowDescriptors = function(throwDescriptors, pathLength) { return new Path( Path.createDescriptorList(throwDescriptors, pathLength), pathLength); }; /** * Create the sequence of ThrowDescriptors, CarryDescriptors, and * CarryOneDescriptor describing the path of a ball through a pattern. * A sequence such as (h j k) generally maps to an alternating series of throw * and carry descriptors [Th Chj Tj Cjk Tk Ck? ...]. However, when j is a 1, * you remove the throw descriptor and modify the previous and subsequent carry * descriptors, since the throw descriptor has zero duration and the carry * descriptors need to take into account the handoff. * @param {!Array.ThrowDescriptor} throwDescriptors the ball's part of the * pattern. * @param {!number} pathLength the length of the pattern in beats. * @return {!Array.Descriptor} the full set of descriptors for the ball. */ Path.createDescriptorList = function(throwDescriptors, pathLength) { var descriptors = []; var prevThrow; for (var index in throwDescriptors) { var td = throwDescriptors[index]; if (prevThrow) { descriptors.push( CarryDescriptor.fromThrowDescriptors(prevThrow, td, pathLength)); } // Else it's handled after the loop. descriptors.push(td); prevThrow = td; } descriptors.push( CarryDescriptor.fromThrowDescriptors(prevThrow, throwDescriptors[0], pathLength)); // Now post-process to take care of throws of 1. It's easier to do it here // than during construction since we can now assume that the previous and // subsequent carry descriptors are already in place [modulo pathLength]. for (var i = 0; i < descriptors.length; ++i) { var descriptor = descriptors[i]; if (descriptor instanceof ThrowDescriptor) { if (descriptor.throwNum == 1) { var prevIndex = (i + descriptors.length - 1) % descriptors.length; var postIndex = (i + 1) % descriptors.length; var replacements = CarryOneDescriptor.getDescriptorPair( descriptors[prevIndex], descriptors[postIndex]); descriptors[prevIndex] = replacements[0]; descriptors[postIndex] = replacements[1]; descriptors.splice(i, 1); // We've removed a descriptor from the array, but since we can never // have 2 ThrowDescriptors in a row, we don't need to decrement i. } } } return descriptors; }; /** * Convert the Path to a string for debugging. * @return {String} debugging output. */ Path.prototype.toString = function() { var ret = 'pathLength is ' + this.pathLength + '; ['; for (var index in this.descriptors) { ret += this.descriptors[index].toString(); } ret += ']'; return ret; }; /** * Create an otherwise-identical copy of this path at a given time offset. * Note that offset may put time references in the Path past the length of the * pattern. The caller must fix this up manually. * @param {number} offset how many beats to offset the new Path. * @return {!Path} the new copy. */ Path.prototype.clone = function(offset) { offset = offset || 0; // Turn null into 0. var descriptors = []; for (var index in this.descriptors) { descriptors.push(this.descriptors[index].clone(offset)); } return new Path(descriptors, this.pathLength); }; /** * Adjust the start time of all descriptors to be in [0, pathLength) via modular * arithmetic. Reorder the array such that they're sorted in increasing order * of time. * @return {!Path} this. */ Path.prototype.fixUpModPathLength = function() { var splitIndex; var prevTime = 0; for (var index in this.descriptors) { var d = this.descriptors[index]; d.fixUpModPathLength(this.pathLength); if (d.time < prevTime) { assert(null == splitIndex); splitIndex = index; // From here to the end should move to the start. } prevTime = d.time; } if (null != splitIndex) { var temp = this.descriptors.slice(splitIndex); this.descriptors.length = splitIndex; this.descriptors = temp.concat(this.descriptors); } return this; }; /** * Take a standard asynch siteswap pattern [expressed as an array of ints] and * a number of hands, and expand it into a 2D grid of ThrowDescriptors with one * row per hand. * Non-asynch patterns are more complicated, since their linear forms aren't * fully-specified, so we don't handle them here. * You'll want to expand your pattern to the LCM of numHands and minimal pattern * length before calling this. * The basic approach doesn't really work for one-handed patterns. It ends up * with catches and throws happening at the same time [having removed all * empty-hand time in between them]. To fix this, we double all throw heights * and space them out, as if doing a two-handed pattern with all zeroes from the * other hand. Yes, this points out that the overall approach we're taking is a * bit odd [since you end up with hands empty for time proportional to the * number of hands], but you have to make some sort of assumptions to generalize * siteswaps to N hands, and that's what I chose. * @param {!Array.number} pattern an asynch siteswap pattern. * @param {!number} numHands the number of hands. * @return {!Array.Array.ThrowDescriptor} the expanded pattern. */ function expandPattern(pattern, numHands) { var fullPattern = []; assert(numHands > 0); if (numHands == 1) { numHands = 2; var temp = []; for (var i = 0; i < pattern.length; ++i) { temp[2 * i] = 2 * pattern[i]; temp[2 * i + 1] = 0; } pattern = temp; } for (var hand = 0; hand < numHands; ++hand) { fullPattern[hand] = []; } for (var time = 0; time < pattern.length; ++time) { for (var hand = 0; hand < numHands; ++hand) { var t; if (hand == time % numHands) { t = new ThrowDescriptor(pattern[time], time, hand, (hand + pattern[time]) % numHands); } else { // These are ignored during analysis, so they don't appear in BallPaths. t = new ThrowDescriptor(0, time, hand, hand); } fullPattern[hand].push(t); } } return fullPattern; } // TODO: Wrap the final pattern in a class, then make the remaining few global // functions be members of that class to clean up the global namespace. /** * Given a valid site-swap for a nonzero number of balls, stored as an expanded * pattern array-of-arrays, with pattern length the LCM of hands and minimal * pattern length, produce Paths for all the balls. * @param {!Array.Array.ThrowDescriptor} pattern a valid pattern. * @return {!Array.Path} the paths of all the balls. */ function generateBallPaths(pattern) { var numHands = pattern.length; assert(numHands > 0); var patternLength = pattern[0].length; assert(patternLength > 0); var sum = 0; for (var hand in pattern) { for (var time in pattern[hand]) { sum += pattern[hand][time].throwNum; } } var numBalls = sum / patternLength; assert(numBalls == Math.round(numBalls)); assert(numBalls > 0); var ballsToAllocate = numBalls; var ballPaths = []; // NOTE: The indices of locationsChecked are reversed from those of pattern // for simplicity of allocation. This might be worth flipping to match. var locationsChecked = []; for (var time = 0; time < patternLength && ballsToAllocate; ++time) { locationsChecked[time] = locationsChecked[time] || []; for (var hand = 0; hand < numHands && ballsToAllocate; ++hand) { if (locationsChecked[time][hand]) { continue; } var curThrowDesc = pattern[hand][time]; var curThrow = curThrowDesc.throwNum; if (!curThrow) { assert(curThrow === 0); continue; } var throwDescriptors = []; var curTime = time; var curHand = hand; var wraps = 0; do { if (!locationsChecked[curTime]) { locationsChecked[curTime] = []; } assert(!locationsChecked[curTime][curHand]); locationsChecked[curTime][curHand] = true; // We copy curThrowDesc here, adding wraps * patternLength, to get // the true throw time relative to offset. Later we'll add in offset // when we clone again, then mod by pathLength. throwDescriptors.push(curThrowDesc.clone(wraps * patternLength)); var nextThrowTime = curThrow + curTime; wraps += Math.floor(nextThrowTime / patternLength); curTime = nextThrowTime % patternLength; assert(curTime >= time); // Else we'd have covered it earlier. curHand = curThrowDesc.destHand; var tempThrowDesc = curThrowDesc; curThrowDesc = pattern[curHand][curTime]; curThrow = curThrowDesc.throwNum; assert(tempThrowDesc.destHand == curThrowDesc.sourceHand); assert(curThrowDesc.time == (tempThrowDesc.throwNum + tempThrowDesc.time) % patternLength); } while (curTime != time || curHand != hand); var pathLength = wraps * patternLength; var ballPath = Path.ballPathFromThrowDescriptors(throwDescriptors, pathLength); for (var i = 0; i < wraps; ++i) { var offset = i * patternLength % pathLength; ballPaths.push(ballPath.clone(offset, pathLength).fixUpModPathLength()); } ballsToAllocate -= wraps; assert(ballsToAllocate >= 0); } } return ballPaths; } /** * Given an array of ball paths, produce the corresponding set of hand paths. * @param {!Array.Path} ballPaths the Paths of all the balls in the pattern. * @param {!number} numHands how many hands to use in the pattern. * @param {!number} patternLength the length, in beats, of the pattern. * @return {!Array.Path} the paths of all the hands. */ function generateHandPaths(ballPaths, numHands, patternLength) { assert(numHands > 0); assert(patternLength > 0); var handRecords = []; // One record per hand. for (var idxBR in ballPaths) { var descriptors = ballPaths[idxBR].descriptors; for (var idxD in descriptors) { var descriptor = descriptors[idxD]; // TODO: Fix likely needed for throws of 1. if (!(descriptor instanceof ThrowDescriptor)) { // It's a CarryDescriptor or a CarryOneDescriptor. var hand = descriptor.hand; if (!handRecords[hand]) { handRecords[hand] = []; } // TODO: Should we not shorten stuff here if we're going to lengthen // everything later anyway? Is there a risk of inconsistency due to // ball paths of different lengths? var catchTime = descriptor.time % patternLength; if (!handRecords[hand][catchTime]) { // We pass in this offset to set the new descriptor's time to // catchTime, so as to keep it within [0, patternLength). handRecords[hand][catchTime] = descriptor.clone(catchTime - descriptor.time); } else { assert( handRecords[hand][catchTime].equalsWithMod( descriptor, patternLength)); } } } } var handPaths = []; for (var hand in handRecords) { var outDescriptors = []; var inDescriptors = handRecords[hand]; var prevDescriptor = null; var descriptor; for (var idxD in inDescriptors) { descriptor = inDescriptors[idxD]; assert(descriptor); // Enumeration should skip array holes. assert(descriptor.hand == hand); if (prevDescriptor) { outDescriptors.push(new EmptyHandDescriptor(prevDescriptor, descriptor, patternLength)); } outDescriptors.push(descriptor.clone()); prevDescriptor = descriptor; } // Note that this EmptyHandDescriptor that wraps around the end lives at the // end of the array, not the beginning, despite the fact that it may be the // active one at time zero. This is the same behavior as with Paths for // balls. descriptor = new EmptyHandDescriptor(prevDescriptor, outDescriptors[0], patternLength); if (descriptor.time < outDescriptors[0].time) { assert(descriptor.time + descriptor.duration == outDescriptors[0].time); outDescriptors.unshift(descriptor); } else { assert(descriptor.time == outDescriptors[outDescriptors.length - 1].time + 1); outDescriptors.push(descriptor); } handPaths[hand] = new Path(outDescriptors, patternLength).fixUpModPathLength(); } return handPaths; } // NOTE: All this Vector stuff does lots of object allocations. If that's a // problem for your browser [e.g. IE6], you'd better stick with the embedded V8. // This code predates the creation of o3djs/math.js; I should probably switch it // over at some point, but for now it's not worth the trouble. /** * A simple 3-dimensional vector. * @constructor */ Vector = function(x, y, z) { this.x = x; this.y = y; this.z = z; } Vector.prototype.sub = function(v) { return new Vector(this.x - v.x, this.y - v.y, this.z - v.z); }; Vector.prototype.add = function(v) { return new Vector(this.x + v.x, this.y + v.y, this.z + v.z); }; Vector.prototype.dot = function(v) { return this.x * v.x + this.y * v.y + this.z * v.z; }; Vector.prototype.length = function() { return Math.sqrt(this.dot(this)); }; Vector.prototype.scale = function(s) { return new Vector(this.x * s, this.y * s, this.z * s); }; Vector.prototype.set = function(v) { this.x = v.x; this.y = v.y; this.z = v.z; }; Vector.prototype.normalize = function() { var length = this.length(); assert(length); this.set(this.scale(1 / length)); return this; }; /** * Convert the Vector to a string for debugging. * @return {String} debugging output. */ Vector.prototype.toString = function() { return '{' + this.x.toFixed(3) + ', ' + this.y.toFixed(3) + ', ' + this.z.toFixed(3) + '}'; }; /** * A container class that holds the positions relevant to a hand: where it is * when it's not doing anything, where it likes to catch balls, and where it * likes to throw balls to each of the other hands. * @param {!Vector} basePosition the centroid of throw and catch positions when * the hand throws to itself. * @param {!Vector} catchPosition where the hand likes to catch balls. * @constructor */ HandPositionRecord = function(basePosition, catchPosition) { this.basePosition = basePosition; this.catchPosition = catchPosition; this.throwPositions = []; } /** * Convert the HandPositionRecord to a string for debugging. * @return {String} debugging output. */ HandPositionRecord.prototype.toString = function() { var s = 'base: ' + this.basePosition.toString() + ';\n'; s += 'catch: ' + this.catchPosition.toString() + ';\n'; s += 'throws:\n'; for (var i = 0; i < this.throwPositions.length; ++i) { s += '[' + i + '] ' + this.throwPositions[i].toString() + '\n'; } return s; }; /** * Compute all the hand positions used in a pattern given a number of hands and * a grouping style ["even" for evenly-spaced hands, "pairs" to group them in * pairs, as with 2-handed jugglers]. * @param {!number} numHands the number of hands to use. * @param {!String} style the grouping style. * @return {!Array.HandPositionRecord} a full set of hand positions. */ function computeHandPositions(numHands, style) { assert(numHands > 0); var majorRadiusScale = 0.75; var majorRadius = majorRadiusScale * (numHands - 1); var throwCatchOffset = 0.45; var catchRadius = majorRadius + throwCatchOffset; var handPositionRecords = []; for (var hand = 0; hand < numHands; ++hand) { var circleFraction; if (style == 'even') { circleFraction = hand / numHands; } else { assert(style == 'pairs'); circleFraction = (hand + Math.floor(hand / 2)) / (1.5 * numHands); } var cos = Math.cos(Math.PI * 2 * circleFraction); var sin = Math.sin(Math.PI * 2 * circleFraction); var cX = catchRadius * cos; var cY = 0; var cZ = catchRadius * sin; var bX = majorRadius * cos; var bY = 0; var bZ = majorRadius * sin; handPositionRecords[hand] = new HandPositionRecord( new Vector(bX, bY, bZ), new Vector(cX, cY, cZ)); } // Now that we've got all the hands' base and catch positions, we need to // compute the appropriate throw positions for each hand pair. for (var source = 0; source < numHands; ++source) { var throwHand = handPositionRecords[source]; for (var target = 0; target < numHands; ++target) { var catchHand = handPositionRecords[target]; if (throwHand == catchHand) { var baseV = throwHand.basePosition; throwHand.throwPositions[target] = baseV.add(baseV.sub(throwHand.catchPosition)); } else { var directionV = catchHand.catchPosition.sub(throwHand.basePosition).normalize(); var offsetV = directionV.scale(throwCatchOffset); throwHand.throwPositions[target] = throwHand.basePosition.add(offsetV); } } } return handPositionRecords; } /** * Convert an array of HandPositionRecord to a string for debugging. * @param {!Array.HandPositionRecord} positions the positions to display. * @return {String} debugging output. */ function getStringFromHandPositions(positions) { var s = ''; for (index in positions) { s += positions[index].toString(); } return s; } /** * The set of curves an object passes through throughout a full animation cycle. * @param {!number} duration the length of the animation in beats. * @param {!Array.Curve} curves the full set of Curves. * @constructor */ CurveSet = function(duration, curves) { this.duration = duration; this.curves = curves; } /** * Looks up what curve is active at a particular time. This is slower than * getCurveForTime, but can be used even if no Curve starts precisely at * unsafeTime % this.duration. * @param {!number} unsafeTime the time at which to check. * @return {!Curve} the curve active at unsafeTime. */ CurveSet.prototype.getCurveForUnsafeTime = function(unsafeTime) { unsafeTime %= this.duration; time = Math.floor(unsafeTime); if (this.curves[time]) { return this.curves[time]; } var curve; for (var i = time; i >= 0; --i) { curve = this.curves[i]; if (curve) { assert(i + curve.duration >= unsafeTime); return curve; } } // We must want the last one. There's always a last one, given how we // construct the CurveSets; they're sparse, but the length gets set by adding // elements at the end. curve = this.curves[this.curves.length - 1]; unsafeTime += this.duration; assert(curve.startTime <= unsafeTime); assert(curve.startTime + curve.duration > unsafeTime); return curve; }; /** * Looks up what curve is active at a particular time. This is faster than * getCurveForUnsafeTime, but can only be used if if a Curve starts precisely at * unsafeTime % this.duration. * @param {!number} time the time at which to check. * @return {!Curve} the curve starting at time. */ CurveSet.prototype.getCurveForTime = function(time) { return this.curves[time % this.duration]; }; /** * Convert the CurveSet to a string for debugging. * @return {String} debugging output. */ CurveSet.prototype.toString = function() { var s = 'Duration: ' + this.duration + '\n'; for (var c in this.curves) { s += this.curves[c].toString(); } return s; }; /** * Namespace object to hold the pure math functions. * TODO: Consider just rolling these into the Pattern object, when it gets * created. */ var JugglingMath = {}; /** * Computes the greatest common devisor of integers a and b. * @param {!number} a an integer. * @param {!number} b an integer. * @return {!number} the GCD of a and b. */ JugglingMath.computeGCD = function(a, b) { assert(Math.round(a) == a); assert(Math.round(b) == b); assert(a >= 0); assert(b >= 0); if (!b) { return a; } else { return JugglingMath.computeGCD(b, a % b); } } /** * Computes the least common multiple of integers a and b, by making use of the * fact that LCM(a, b) * GCD(a, b) == a * b. * @param {!number} a an integer. * @param {!number} b an integer. * @return {!number} the LCM of a and b. */ JugglingMath.computeLCM = function(a, b) { assert(Math.round(a) == a); assert(Math.round(b) == b); assert(a >= 0); assert(b >= 0); var ret = a * b / JugglingMath.computeGCD(a, b); assert(Math.round(ret) == ret); return ret; } /** * Given a Path and a set of hand positions, compute the corresponding set of * Curves. * @param {!Path} path the path of an object. * @param {!Array.HandPositionRecord} handPositions the positions of the hands * juggling the pattern containing the path. * @return {!CurveSet} the full set of curves. */ CurveSet.getCurveSetFromPath = function(path, handPositions) { var curves = []; var pathLength = path.pathLength; for (var index in path.descriptors) { var descriptor = path.descriptors[index]; var curve = descriptor.generateCurve(handPositions); assert(!curves[curve.startTime]); assert(curve.startTime < pathLength); curves[curve.startTime] = curve; } return new CurveSet(pathLength, curves); } /** * Given a set of Paths and a set of hand positions, compute the corresponding * CurveSets. * @param {!Array.Path} paths the paths of a number of objects. * @param {!Array.HandPositionRecord} handPositions the positions of the hands * juggling the pattern containing the paths. * @return {!Array.CurveSet} the CurveSets. */ CurveSet.getCurveSetsFromPaths = function(paths, handPositions) { var curveSets = []; for (var index in paths) { var path = paths[index]; curveSets[index] = CurveSet.getCurveSetFromPath(path, handPositions); } return curveSets; } /** * This is a temporary top-level calculation function that converts a standard * asynchronous siteswap, expressed as a string of digits, into a full * ready-to-animate set of CurveSets. Later on we'll be using an interface that * can create a richer set of patterns than those expressable in the traditional * string-of-ints format. * @param {!String} patternString the siteswap. * @param {!number} numHands the number of hands to use for the pattern. * @param {!String} style how to space the hands ["pairs" or "even"]. * @return {!Object} a fully-analyzed pattern as CurveSets and associated data. */ function computeFullPatternFromString(patternString, numHands, style) { var patternAsStrings = patternString.split(/[ ,]+ */); var patternSegment = []; for (var index in patternAsStrings) { if (patternAsStrings[index]) { // Beware extra whitespace at the ends. patternSegment.push(parseInt(patternAsStrings[index])); } } var pattern = []; // Now expand the pattern out to the length of the LCM of pattern length and // number of hands, so that each throw gets done in each of its incarnations. var multiple = JugglingMath.computeLCM(patternSegment.length, numHands) / patternSegment.length; for (var i = 0; i < multiple; ++i) { pattern = pattern.concat(patternSegment); } var fullPattern = expandPattern(pattern, numHands); var patternLength = fullPattern[0].length; var ballPaths = generateBallPaths(fullPattern); var handPaths = generateHandPaths(ballPaths, numHands, patternLength); var handPositions = computeHandPositions(numHands, style); var ballCurveSets = CurveSet.getCurveSetsFromPaths(ballPaths, handPositions); var handCurveSets = CurveSet.getCurveSetsFromPaths(handPaths, handPositions); // Find the LCM of all the curveSet durations. This will be the length of the // fully-expanded queue. We could expand to this before computing the // CurveSets, but this way's probably just a little cheaper. var lcmDuration = 1; for (var i in ballCurveSets) { var duration = ballCurveSets[i].duration; if (duration > lcmDuration || lcmDuration % duration) { lcmDuration = JugglingMath.computeLCM(lcmDuration, duration); } } for (var i in handCurveSets) { var duration = handCurveSets[i].duration; if (duration > lcmDuration || lcmDuration % duration) { lcmDuration = JugglingMath.computeLCM(lcmDuration, duration); } } return { numBalls: ballPaths.length, numHands: handPaths.length, duration: lcmDuration, handCurveSets: handCurveSets, ballCurveSets: ballCurveSets } }