| Index: samples/siteswap/math.js
|
| ===================================================================
|
| --- samples/siteswap/math.js (revision 0)
|
| +++ samples/siteswap/math.js (revision 0)
|
| @@ -0,0 +1,1492 @@
|
| +// @@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
|
| + }
|
| +}
|
|
|