diff options
Diffstat (limited to 'o3d/samples/siteswap')
-rw-r--r-- | o3d/samples/siteswap/animation.js | 396 | ||||
-rw-r--r-- | o3d/samples/siteswap/math.js | 2984 | ||||
-rw-r--r-- | o3d/samples/siteswap/siteswap.html | 646 | ||||
-rw-r--r-- | o3d/samples/siteswap/siteswap.js | 830 |
4 files changed, 2428 insertions, 2428 deletions
diff --git a/o3d/samples/siteswap/animation.js b/o3d/samples/siteswap/animation.js index eb20cc8..c9aa80a 100644 --- a/o3d/samples/siteswap/animation.js +++ b/o3d/samples/siteswap/animation.js @@ -1,198 +1,198 @@ -// @@REWRITE(insert js-copyright)
-// @@REWRITE(delete-start)
-// Copyright 2009 Google Inc. All Rights Reserved
-// @@REWRITE(delete-end)
-
-/**
- * This file contains the animation-management code for the siteswap animator.
- * This is encapsulated in the EventQueue and QueueEvent classes, the event
- * handler, and startAnimation, the main external interface to the animation.
- */
-
-/**
- * A record, held in the EventQueue, that describes the curve that should be
- * given to a shape at a time.
- * @constructor
- * @param {!number} time base time at which the event occurs/the curve starts.
- * @param {!o3d.Shape} shape the shape to be updated.
- * @param {Curve} curve the path for the shape to follow.
- */
-function QueueEvent(time, shape, curve) {
- this.time = time;
- this.shape = shape;
- this.curve = curve;
- return this;
-}
-
-/**
- * A circular queue of events that will happen during the course of an animation
- * that's duration beats long. The queue is ordered by the time each curve
- * starts. Note that a curve may start after it ends, since time loops
- * endlessly. The nextEvent field is the index of the next event to occur; it
- * keeps track of how far we've gotten in the queue.
- * @constructor
- * @param {!number} duration the length of the animation in beats.
- */
-function EventQueue(duration) {
- this.events = [];
- this.nextEvent = 0;
- this.duration = duration;
- this.timeCorrection = 0; // Corrects from queue entry time to real time.
- return this;
-}
-
-/**
- * Add an event to the queue, inserting into order by its time field.
- * A heap-based priority queue would be faster, but likely overkill, as this
- * won't ever contain that many items, and isn't likely to be speed-critical.
- * @param {!QueueEvent} event the event to add.
- */
-EventQueue.prototype.addEvent = function(event) {
- var i = 0;
- while (i < this.events.length && event.time > this.events[i].time) {
- ++i;
- }
- this.events.splice(i, 0, event);
-};
-
-/**
- * Pull the next event off the queue.
- * @return {!QueueEvent} the event.
- */
-EventQueue.prototype.shift = function() {
- var e = this.events[this.nextEvent];
- if (++this.nextEvent >= this.events.length) {
- assert(this.nextEvent > 0);
- this.nextEvent = 0;
- this.timeCorrection += this.duration;
- }
- return e;
-};
-
-/**
- * Process all current events, updating all animated objects with their new
- * curves, until we find that the next event in the queue is in the future.
- * @param {!number} time the current time in beats. This number is an absolute,
- * not locked to the range of the duration of the pattern, so getNextTime()
- * returns a doctored number to add in the offset from in-pattern time to real
- * time.
- * @return {!number} the time of the next future event.
- */
-EventQueue.prototype.processEvents = function(time) {
- while (this.getNextTime() <= time) {
- var e = this.shift();
- setParamCurveInfo(e.curve, e.shape, time);
- }
- return this.getNextTime(); // In case you want to set a callback.
-};
-
-/**
- * Look up the initial curve for a shape [the curve that it'll be starting or in
- * the middle of at time 0].
- * @param {!CurveSet} curveSet the complete set of curves for a Shape.
- * @return {!Object} the curve and the time at which it would have started.
- */
-function getInitialCurveInfo(curveSet) {
- var curve = curveSet.getCurveForUnsafeTime(0);
- var curveBaseTime;
- if (!curve.startTime) {
- curveBaseTime = 0;
- } else {
- // If the curve isn't starting now, it must have wrapped around.
- assert(curve.startTime + curve.duration > curveSet.duration);
- // So subtract off one wrap so that its startTime is in the right space of
- // numbers. We assume here that no curve duration is longer than the
- // pattern, which must be guaranteed by the code that generates patterns.
- assert(curve.duration <= curveSet.duration);
- curveBaseTime = curve.startTime - curveSet.duration;
- }
- return { curve: curve, curveBaseTime: curveBaseTime };
-}
-
-/**
- * Set up the event queue with a complete pattern starting at time 0.
- * @param {!Array.CurveSet} curveSets the curve sets for all shapes.
- * @param {!Array.o3d.Shape} shapes all the shapes to animate.
- */
-EventQueue.prototype.setUp = function(curveSets, shapes) {
- assert(curveSets.length == shapes.length);
- for (var i = 0; i < shapes.length; ++i) {
- var curveSet = curveSets[i];
- assert(this.duration % curveSet.duration == 0);
- var shape = shapes[i];
- var record = getInitialCurveInfo(curveSet);
- var curveBaseTime = record.curveBaseTime;
- var curve = record.curve;
- setParamCurveInfo(curve, shape, curveBaseTime);
- do {
- curveBaseTime += curve.duration;
- curve = curveSet.getCurveForTime(curveBaseTime);
- var e = new QueueEvent(curveBaseTime % this.duration, shape, curve);
- this.addEvent(e);
- } while (curveBaseTime + curve.duration <= this.duration);
- }
-};
-
-/**
- * Return the time of the next future event.
- * @return {!number} the time.
- */
-EventQueue.prototype.getNextTime = function() {
- return this.events[this.nextEvent].time + this.timeCorrection;
-};
-
-/**
- * This is the event handler that runs the whole animation. When triggered by
- * the counter, it updates the curves on all objects whose curves have expired.
- *
- * The current time will be some time around when we wanted to be called. It
- * might be exact, but it might be a bit off due to floating point error, or a
- * lot off due to the system getting bogged down somewhere for a second or
- * two. e.g. if we wanted to get a call at time 7, it's likely to be
- * something like 7.04, but might even be 11. We then use 7, not 7.04, as the
- * start time for each of the curves set, so as to remove clock drift. Since
- * the time we wanted to be called is stored in the next item in the queue, we
- * can just pull that out and use it. However, if we then find that we're
- * setting our callback in the past, we repeat the process until our callback
- * is set safely in the future. We may get some visual artifacts, but at
- * least we won't drop any events [leading to stuff drifting endlessly off
- * into the distance].
- */
-function handler() {
- var eventTime = g.eventQueue.getNextTime();
- var trueCurrentTime;
- do {
- g.counter.removeCallback(eventTime);
- eventTime = g.eventQueue.processEvents(eventTime);
- g.counter.addCallback(eventTime, handler);
- trueCurrentTime = g.counter.count;
- } while (eventTime < trueCurrentTime);
-}
-
-/**
- * Given a precomputed juggling pattern, this sets up the O3D objects,
- * EventQueue, and callback necessary to start an animation, then calls
- * updateAnimating to kick it off if enabled.
- *
- * @param {!number} numBalls the number of balls in the animation.
- * @param {!number} numHands the number of hands in the animation.
- * @param {!number} duration the length of the full animation cycle in beats.
- * @param {!Array.CurveSet} ballCurveSets one CurveSet per ball.
- * @param {!Array.CurveSet} handCurveSets one CurveSet per hand.
- */
-function startAnimation(numBalls, numHands, duration, ballCurveSets,
- handCurveSets) {
- g.counter.running = false;
- g.counter.reset();
-
- setNumBalls(numBalls);
- setNumHands(numHands);
-
- g.eventQueue = new EventQueue(duration);
- g.eventQueue.setUp(handCurveSets, g.handShapes);
- g.eventQueue.setUp(ballCurveSets, g.ballShapes);
- g.counter.addCallback(g.eventQueue.getNextTime(), handler);
-
- updateAnimating();
-}
-
+// @@REWRITE(insert js-copyright) +// @@REWRITE(delete-start) +// Copyright 2009 Google Inc. All Rights Reserved +// @@REWRITE(delete-end) + +/** + * This file contains the animation-management code for the siteswap animator. + * This is encapsulated in the EventQueue and QueueEvent classes, the event + * handler, and startAnimation, the main external interface to the animation. + */ + +/** + * A record, held in the EventQueue, that describes the curve that should be + * given to a shape at a time. + * @constructor + * @param {!number} time base time at which the event occurs/the curve starts. + * @param {!o3d.Shape} shape the shape to be updated. + * @param {Curve} curve the path for the shape to follow. + */ +function QueueEvent(time, shape, curve) { + this.time = time; + this.shape = shape; + this.curve = curve; + return this; +} + +/** + * A circular queue of events that will happen during the course of an animation + * that's duration beats long. The queue is ordered by the time each curve + * starts. Note that a curve may start after it ends, since time loops + * endlessly. The nextEvent field is the index of the next event to occur; it + * keeps track of how far we've gotten in the queue. + * @constructor + * @param {!number} duration the length of the animation in beats. + */ +function EventQueue(duration) { + this.events = []; + this.nextEvent = 0; + this.duration = duration; + this.timeCorrection = 0; // Corrects from queue entry time to real time. + return this; +} + +/** + * Add an event to the queue, inserting into order by its time field. + * A heap-based priority queue would be faster, but likely overkill, as this + * won't ever contain that many items, and isn't likely to be speed-critical. + * @param {!QueueEvent} event the event to add. + */ +EventQueue.prototype.addEvent = function(event) { + var i = 0; + while (i < this.events.length && event.time > this.events[i].time) { + ++i; + } + this.events.splice(i, 0, event); +}; + +/** + * Pull the next event off the queue. + * @return {!QueueEvent} the event. + */ +EventQueue.prototype.shift = function() { + var e = this.events[this.nextEvent]; + if (++this.nextEvent >= this.events.length) { + assert(this.nextEvent > 0); + this.nextEvent = 0; + this.timeCorrection += this.duration; + } + return e; +}; + +/** + * Process all current events, updating all animated objects with their new + * curves, until we find that the next event in the queue is in the future. + * @param {!number} time the current time in beats. This number is an absolute, + * not locked to the range of the duration of the pattern, so getNextTime() + * returns a doctored number to add in the offset from in-pattern time to real + * time. + * @return {!number} the time of the next future event. + */ +EventQueue.prototype.processEvents = function(time) { + while (this.getNextTime() <= time) { + var e = this.shift(); + setParamCurveInfo(e.curve, e.shape, time); + } + return this.getNextTime(); // In case you want to set a callback. +}; + +/** + * Look up the initial curve for a shape [the curve that it'll be starting or in + * the middle of at time 0]. + * @param {!CurveSet} curveSet the complete set of curves for a Shape. + * @return {!Object} the curve and the time at which it would have started. + */ +function getInitialCurveInfo(curveSet) { + var curve = curveSet.getCurveForUnsafeTime(0); + var curveBaseTime; + if (!curve.startTime) { + curveBaseTime = 0; + } else { + // If the curve isn't starting now, it must have wrapped around. + assert(curve.startTime + curve.duration > curveSet.duration); + // So subtract off one wrap so that its startTime is in the right space of + // numbers. We assume here that no curve duration is longer than the + // pattern, which must be guaranteed by the code that generates patterns. + assert(curve.duration <= curveSet.duration); + curveBaseTime = curve.startTime - curveSet.duration; + } + return { curve: curve, curveBaseTime: curveBaseTime }; +} + +/** + * Set up the event queue with a complete pattern starting at time 0. + * @param {!Array.CurveSet} curveSets the curve sets for all shapes. + * @param {!Array.o3d.Shape} shapes all the shapes to animate. + */ +EventQueue.prototype.setUp = function(curveSets, shapes) { + assert(curveSets.length == shapes.length); + for (var i = 0; i < shapes.length; ++i) { + var curveSet = curveSets[i]; + assert(this.duration % curveSet.duration == 0); + var shape = shapes[i]; + var record = getInitialCurveInfo(curveSet); + var curveBaseTime = record.curveBaseTime; + var curve = record.curve; + setParamCurveInfo(curve, shape, curveBaseTime); + do { + curveBaseTime += curve.duration; + curve = curveSet.getCurveForTime(curveBaseTime); + var e = new QueueEvent(curveBaseTime % this.duration, shape, curve); + this.addEvent(e); + } while (curveBaseTime + curve.duration <= this.duration); + } +}; + +/** + * Return the time of the next future event. + * @return {!number} the time. + */ +EventQueue.prototype.getNextTime = function() { + return this.events[this.nextEvent].time + this.timeCorrection; +}; + +/** + * This is the event handler that runs the whole animation. When triggered by + * the counter, it updates the curves on all objects whose curves have expired. + * + * The current time will be some time around when we wanted to be called. It + * might be exact, but it might be a bit off due to floating point error, or a + * lot off due to the system getting bogged down somewhere for a second or + * two. e.g. if we wanted to get a call at time 7, it's likely to be + * something like 7.04, but might even be 11. We then use 7, not 7.04, as the + * start time for each of the curves set, so as to remove clock drift. Since + * the time we wanted to be called is stored in the next item in the queue, we + * can just pull that out and use it. However, if we then find that we're + * setting our callback in the past, we repeat the process until our callback + * is set safely in the future. We may get some visual artifacts, but at + * least we won't drop any events [leading to stuff drifting endlessly off + * into the distance]. + */ +function handler() { + var eventTime = g.eventQueue.getNextTime(); + var trueCurrentTime; + do { + g.counter.removeCallback(eventTime); + eventTime = g.eventQueue.processEvents(eventTime); + g.counter.addCallback(eventTime, handler); + trueCurrentTime = g.counter.count; + } while (eventTime < trueCurrentTime); +} + +/** + * Given a precomputed juggling pattern, this sets up the O3D objects, + * EventQueue, and callback necessary to start an animation, then calls + * updateAnimating to kick it off if enabled. + * + * @param {!number} numBalls the number of balls in the animation. + * @param {!number} numHands the number of hands in the animation. + * @param {!number} duration the length of the full animation cycle in beats. + * @param {!Array.CurveSet} ballCurveSets one CurveSet per ball. + * @param {!Array.CurveSet} handCurveSets one CurveSet per hand. + */ +function startAnimation(numBalls, numHands, duration, ballCurveSets, + handCurveSets) { + g.counter.running = false; + g.counter.reset(); + + setNumBalls(numBalls); + setNumHands(numHands); + + g.eventQueue = new EventQueue(duration); + g.eventQueue.setUp(handCurveSets, g.handShapes); + g.eventQueue.setUp(ballCurveSets, g.ballShapes); + g.counter.addCallback(g.eventQueue.getNextTime(), handler); + + updateAnimating(); +} + diff --git a/o3d/samples/siteswap/math.js b/o3d/samples/siteswap/math.js index c5e2f43..e584744 100644 --- a/o3d/samples/siteswap/math.js +++ b/o3d/samples/siteswap/math.js @@ -1,1492 +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
- }
-}
+// @@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 + } +} diff --git a/o3d/samples/siteswap/siteswap.html b/o3d/samples/siteswap/siteswap.html index 15d7497..06656eb 100644 --- a/o3d/samples/siteswap/siteswap.html +++ b/o3d/samples/siteswap/siteswap.html @@ -1,323 +1,323 @@ -<!-- @@REWRITE(insert html-copyright) -->
-<!--
-O3D Siteswap animator
-@@REWRITE(delete-start)
-Author: Eric Uhrhane (ericu@google.com)
-@@REWRITE(delete-end)
--->
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
- "http://www.w3.org/TR/html4/loose.dtd">
-<html>
-<head>
-<meta http-equiv="content-type" content="text/html; charset=UTF-8">
-<title>
- Site Swap Simulator
-</title>
-<style type="text/css">
- html, body {
- height: 100%;
- margin: 0;
- padding: 0;
- border: none;
- }
-</style>
-<!-- Our javascript code -->
-<script type="text/javascript" src="../o3djs/base.js"></script>
-<script type="text/javascript" src="siteswap.js"></script>
-<script type="text/javascript" src="math.js"></script>
-<script type="text/javascript" src="animation.js"></script>
-<script type="text/javascript" id="o3dscript">
-
-o3djs.require('o3djs.util');
-
-// Set up the client area.
-function init() {
- o3djs.util.setMainEngine(o3djs.util.Engine.V8);
- o3djs.util.addScriptUri(''); // Allow V8 to load scripts in the cwd.
- o3djs.util.makeClients(initStep2);
- setUpSelection();
-}
-
-// Select the entire input pattern, so that the user can just type.
-function setUpSelection() {
- var input_pattern = document.getElementById('input_pattern');
- input_pattern.focus();
- input_pattern.selectionStart = 0;
- input_pattern.selectionEnd = input_pattern.value.length;
-}
-
-/*
- * Wrappers to enable button presses to call through to the embedded V8 engine.
- */
-
-// Update whether we're animating or frozen based on the value of the checkbox.
-function updateAnimatingWrapper() {
- g.o3dElement.eval('updateAnimating()');
-}
-
-// Compute a new pattern.
-function onComputePatternWrapper() {
- g.o3dElement.eval('onComputePattern()');
-}
-
-// Adjust the view matrix to the new proportions of the plugin window.
-// TODO: Switch to using the resize event once that's checked in.
-function onResizeWrapper() {
- g.o3dElement.eval('onResize()');
-}
-window.onresize = onResizeWrapper;
-
-// Clean up all our callbacks, etc.
-function cleanupWrapper() {
- if (g && g.o3dElement) {
- g.o3dElement.eval('cleanup()');
- }
-}
-
-// The point of this function is to suppress form submit; we want to use the
-// pattern entered when the user hits return, not submit the page to the
-// webserver.
-function suppressSubmit(event) {
- onComputePatternWrapper(); // TODO: This suppresses form autocomplete storage.
- return false;
-}
-
-</script>
-</head>
-<body onload="init()" onunload="cleanupWrapper()">
-<table width="100%" style="height:100%;">
- <tr>
- <td>
- <h1>Juggler</h1>
- This sample displays a site-swap juggling pattern with animation done by
- a vertex shader.
- <form name="the_form" onsubmit="return suppressSubmit(event)">
- <table>
- <tr>
- <td>
- <table>
- <tr>
- <td>
- <input
- type="radio"
- name="radio_group_hands"
- value="1"
- onclick=onComputePatternWrapper()>
- 1 Hand<br>
- <input
- type="radio"
- name="radio_group_hands"
- value="2"
- onclick=onComputePatternWrapper()
- checked>
- 2 Hands<br>
- <input
- type="radio"
- name="radio_group_hands"
- value="3"
- onclick=onComputePatternWrapper()>
- 3 Hands<br>
- <input
- type="radio"
- name="radio_group_hands"
- value="4"
- onclick=onComputePatternWrapper()>
- 4 Hands<br>
- </td>
- <td>
- <input type="text" name="input_pattern" id="input_pattern"
- value="3 4 5">
- </td>
- <td>
- <input type="checkbox" name="check_box" checked
- onclick=updateAnimatingWrapper()>Animate
- <input
- type="checkbox"
- name="pair_hands"
- onclick=onComputePatternWrapper();
- >Pair Hands
- </td>
- <td>
- <input type="button" name="computePattern"
- value="Compute Pattern"
- onclick=onComputePatternWrapper()>
- </td>
- </tr>
- </table>
- </td>
- <td>
- </td>
- </tr>
- </table>
- </form>
- <table id="container" width="90%" style="height:70%;">
- <tr>
- <td height="100%">
- <!-- Start of g.o3d plugin -->
- <div id="o3d" style="width: 100%; height: 100%;"></div>
- <!-- End of g.o3d plugin -->
- </td>
- </tr>
- </table>
- <!-- a simple way to get a multiline string -->
- <textarea id="shader" name="shader" cols="80" rows="20"
- style="display: none;">
-// the 4x4 world view projection matrix
-float4x4 worldViewProjection : WorldViewProjection;
-
-// positions of the light and camera
-float3 light_pos;
-float3 camera_pos;
-
-// phong lighting properties of the material
-float4 light_ambient;
-float4 light_diffuse;
-float4 light_specular;
-
-// shininess of the material (for specular lighting)
-float shininess;
-
-// time for animation
-float time;
-
-// coefficients for a t^2 + b t + c for each of x, y, z
-float3 coeff_a;
-float3 coeff_b;
-float3 coeff_c;
-
-// flag and coefficient for optional LERP with rate t * coeff_lerp;
-float coeff_lerp;
-
-// coefficients for a t^2 + b t + c for each of x, y, z, for optional LERP
-float3 coeff_l_a;
-float3 coeff_l_b;
-float3 coeff_l_c;
-
-// coefficients for d sin(f t) + e cos(e t) for each of x, y, z
-float3 coeff_d;
-float3 coeff_e;
-float3 coeff_f;
-
-// to be subtracted from time to get the t for the above equation
-float time_base;
-
-// input parameters for our vertex shader
-struct VertexShaderInput {
- float4 position : POSITION;
- float3 normal : NORMAL;
- float4 color : COLOR;
-};
-
-// input parameters for our pixel shader
-// also the output parameters for our vertex shader
-struct PixelShaderInput {
- float4 position : POSITION;
- float3 lightVector : TEXCOORD0;
- float3 normal : TEXCOORD1;
- float3 viewPosition : TEXCOORD2;
- float4 color : COLOR;
-};
-
-/**
- * Vertex Shader - vertex shader for phong illumination
- */
-PixelShaderInput vertexShaderFunction(VertexShaderInput input) {
- /**
- * We use the standard phong illumination equation here.
- * We restrict (clamp) the dot products so that we
- * don't get any negative values.
- * All vectors are normalized for proper calculations.
- *
- * The output color is the summation of the
- * ambient, diffuse, and specular contributions.
- *
- * Note that we have to transform each vertex and normal
- * by the world view projection matrix first.
- */
- PixelShaderInput output;
-
- float t = time - time_base;
- float3 offset = float3(0, 0, 0);
- offset += t * t * coeff_a + t * coeff_b + coeff_c;
- offset += coeff_d * sin(t * coeff_f);
- offset += coeff_e * cos(t * coeff_f);
-
- float3 lerpOffset = t * t * coeff_l_a + t * coeff_l_b + coeff_l_c;
- if (coeff_lerp > 0) {
- float rate = min(coeff_lerp * t, 1);
- float3 lerpVector = float3(rate, rate, rate);
- offset = lerp(offset, lerpOffset, lerpVector);
- } else if (coeff_lerp < 0) {
- float rate = min(-coeff_lerp * t, 1);
- float3 lerpVector = float3(rate, rate, rate);
- offset = lerp(lerpOffset, offset, lerpVector);
- }
-
- input.position = input.position + float4(offset.xyz, 0);
-
- output.position = mul(input.position, worldViewProjection);
-
- /**
- * lightVector - light vector
- * normal - normal vector
- * viewPosition - view vector (from camera)
- */
-
- // NOTE: In this case we do not need to multiply by any matrices since the
- // WORLD transformation matrix is the identity. If you were moving the
- // object such that the WORLD transform matrix was not the identity, you
- // would need to multiply the normal by the WORLDINVERSETTRANSFORM matrix
- // since the normal is in object space. Other values (light_pos, camera_pos)
- // are already in world space.
- float3 lightVector = light_pos - input.position.xyz;
- float3 normal = input.normal;
- float3 viewPosition = camera_pos - input.position.xyz;
-
- output.lightVector = lightVector;
- output.normal = normal;
- output.viewPosition = viewPosition;
- output.color = input.color;
- return output;
-}
-
-/**
- * Pixel Shader
- */
-float4 pixelShaderFunction(PixelShaderInput input): COLOR {
- float3 lightVector = normalize(input.lightVector);
- float3 normal = normalize(input.normal);
- float3 viewPosition = normalize(input.viewPosition);
- float3 halfVector = normalize(lightVector + viewPosition);
-
- // use lit function to calculate phong shading
- // x component contains the ambient coefficient
- // y component contains the diffuse coefficient:
- // max(dot(normal, lightVector),0)
- // z component contains the specular coefficient:
- // dot(normal, lightVector) < 0 || dot(normal, halfVector) < 0 ?
- // 0 : pow(dot(normal, halfVector), shininess)
- // NOTE: This is actually Blinn-Phong shading, not Phong shading
- // which would use the reflection vector instead of the half vector
-
- float4 phong_coeff = lit(dot(normal, lightVector),
- dot(normal, halfVector), shininess);
-
- float4 ambient = light_ambient * phong_coeff.x * input.color;
- float4 diffuse = light_diffuse * phong_coeff.y * input.color;
- float4 specular = light_specular * phong_coeff.z * input.color;
-
- return ambient + diffuse + specular;
-}
-
-// Here we tell our effect file *which* functions are
-// our vertex and pixel shaders.
-
-// #o3d VertexShaderEntryPoint vertexShaderFunction
-// #o3d PixelShaderEntryPoint pixelShaderFunction
-// #o3d MatrixLoadOrder RowMajor
- </textarea>
- </td>
- </tr>
-</table>
-</body>
-</html>
+<!-- @@REWRITE(insert html-copyright) --> +<!-- +O3D Siteswap animator +@@REWRITE(delete-start) +Author: Eric Uhrhane (ericu@google.com) +@@REWRITE(delete-end) +--> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + "http://www.w3.org/TR/html4/loose.dtd"> +<html> +<head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> +<title> + Site Swap Simulator +</title> +<style type="text/css"> + html, body { + height: 100%; + margin: 0; + padding: 0; + border: none; + } +</style> +<!-- Our javascript code --> +<script type="text/javascript" src="../o3djs/base.js"></script> +<script type="text/javascript" src="siteswap.js"></script> +<script type="text/javascript" src="math.js"></script> +<script type="text/javascript" src="animation.js"></script> +<script type="text/javascript" id="o3dscript"> + +o3djs.require('o3djs.util'); + +// Set up the client area. +function init() { + o3djs.util.setMainEngine(o3djs.util.Engine.V8); + o3djs.util.addScriptUri(''); // Allow V8 to load scripts in the cwd. + o3djs.util.makeClients(initStep2); + setUpSelection(); +} + +// Select the entire input pattern, so that the user can just type. +function setUpSelection() { + var input_pattern = document.getElementById('input_pattern'); + input_pattern.focus(); + input_pattern.selectionStart = 0; + input_pattern.selectionEnd = input_pattern.value.length; +} + +/* + * Wrappers to enable button presses to call through to the embedded V8 engine. + */ + +// Update whether we're animating or frozen based on the value of the checkbox. +function updateAnimatingWrapper() { + g.o3dElement.eval('updateAnimating()'); +} + +// Compute a new pattern. +function onComputePatternWrapper() { + g.o3dElement.eval('onComputePattern()'); +} + +// Adjust the view matrix to the new proportions of the plugin window. +// TODO: Switch to using the resize event once that's checked in. +function onResizeWrapper() { + g.o3dElement.eval('onResize()'); +} +window.onresize = onResizeWrapper; + +// Clean up all our callbacks, etc. +function cleanupWrapper() { + if (g && g.o3dElement) { + g.o3dElement.eval('cleanup()'); + } +} + +// The point of this function is to suppress form submit; we want to use the +// pattern entered when the user hits return, not submit the page to the +// webserver. +function suppressSubmit(event) { + onComputePatternWrapper(); // TODO: This suppresses form autocomplete storage. + return false; +} + +</script> +</head> +<body onload="init()" onunload="cleanupWrapper()"> +<table width="100%" style="height:100%;"> + <tr> + <td> + <h1>Juggler</h1> + This sample displays a site-swap juggling pattern with animation done by + a vertex shader. + <form name="the_form" onsubmit="return suppressSubmit(event)"> + <table> + <tr> + <td> + <table> + <tr> + <td> + <input + type="radio" + name="radio_group_hands" + value="1" + onclick=onComputePatternWrapper()> + 1 Hand<br> + <input + type="radio" + name="radio_group_hands" + value="2" + onclick=onComputePatternWrapper() + checked> + 2 Hands<br> + <input + type="radio" + name="radio_group_hands" + value="3" + onclick=onComputePatternWrapper()> + 3 Hands<br> + <input + type="radio" + name="radio_group_hands" + value="4" + onclick=onComputePatternWrapper()> + 4 Hands<br> + </td> + <td> + <input type="text" name="input_pattern" id="input_pattern" + value="3 4 5"> + </td> + <td> + <input type="checkbox" name="check_box" checked + onclick=updateAnimatingWrapper()>Animate + <input + type="checkbox" + name="pair_hands" + onclick=onComputePatternWrapper(); + >Pair Hands + </td> + <td> + <input type="button" name="computePattern" + value="Compute Pattern" + onclick=onComputePatternWrapper()> + </td> + </tr> + </table> + </td> + <td> + </td> + </tr> + </table> + </form> + <table id="container" width="90%" style="height:70%;"> + <tr> + <td height="100%"> + <!-- Start of g.o3d plugin --> + <div id="o3d" style="width: 100%; height: 100%;"></div> + <!-- End of g.o3d plugin --> + </td> + </tr> + </table> + <!-- a simple way to get a multiline string --> + <textarea id="shader" name="shader" cols="80" rows="20" + style="display: none;"> +// the 4x4 world view projection matrix +float4x4 worldViewProjection : WorldViewProjection; + +// positions of the light and camera +float3 light_pos; +float3 camera_pos; + +// phong lighting properties of the material +float4 light_ambient; +float4 light_diffuse; +float4 light_specular; + +// shininess of the material (for specular lighting) +float shininess; + +// time for animation +float time; + +// coefficients for a t^2 + b t + c for each of x, y, z +float3 coeff_a; +float3 coeff_b; +float3 coeff_c; + +// flag and coefficient for optional LERP with rate t * coeff_lerp; +float coeff_lerp; + +// coefficients for a t^2 + b t + c for each of x, y, z, for optional LERP +float3 coeff_l_a; +float3 coeff_l_b; +float3 coeff_l_c; + +// coefficients for d sin(f t) + e cos(e t) for each of x, y, z +float3 coeff_d; +float3 coeff_e; +float3 coeff_f; + +// to be subtracted from time to get the t for the above equation +float time_base; + +// input parameters for our vertex shader +struct VertexShaderInput { + float4 position : POSITION; + float3 normal : NORMAL; + float4 color : COLOR; +}; + +// input parameters for our pixel shader +// also the output parameters for our vertex shader +struct PixelShaderInput { + float4 position : POSITION; + float3 lightVector : TEXCOORD0; + float3 normal : TEXCOORD1; + float3 viewPosition : TEXCOORD2; + float4 color : COLOR; +}; + +/** + * Vertex Shader - vertex shader for phong illumination + */ +PixelShaderInput vertexShaderFunction(VertexShaderInput input) { + /** + * We use the standard phong illumination equation here. + * We restrict (clamp) the dot products so that we + * don't get any negative values. + * All vectors are normalized for proper calculations. + * + * The output color is the summation of the + * ambient, diffuse, and specular contributions. + * + * Note that we have to transform each vertex and normal + * by the world view projection matrix first. + */ + PixelShaderInput output; + + float t = time - time_base; + float3 offset = float3(0, 0, 0); + offset += t * t * coeff_a + t * coeff_b + coeff_c; + offset += coeff_d * sin(t * coeff_f); + offset += coeff_e * cos(t * coeff_f); + + float3 lerpOffset = t * t * coeff_l_a + t * coeff_l_b + coeff_l_c; + if (coeff_lerp > 0) { + float rate = min(coeff_lerp * t, 1); + float3 lerpVector = float3(rate, rate, rate); + offset = lerp(offset, lerpOffset, lerpVector); + } else if (coeff_lerp < 0) { + float rate = min(-coeff_lerp * t, 1); + float3 lerpVector = float3(rate, rate, rate); + offset = lerp(lerpOffset, offset, lerpVector); + } + + input.position = input.position + float4(offset.xyz, 0); + + output.position = mul(input.position, worldViewProjection); + + /** + * lightVector - light vector + * normal - normal vector + * viewPosition - view vector (from camera) + */ + + // NOTE: In this case we do not need to multiply by any matrices since the + // WORLD transformation matrix is the identity. If you were moving the + // object such that the WORLD transform matrix was not the identity, you + // would need to multiply the normal by the WORLDINVERSETTRANSFORM matrix + // since the normal is in object space. Other values (light_pos, camera_pos) + // are already in world space. + float3 lightVector = light_pos - input.position.xyz; + float3 normal = input.normal; + float3 viewPosition = camera_pos - input.position.xyz; + + output.lightVector = lightVector; + output.normal = normal; + output.viewPosition = viewPosition; + output.color = input.color; + return output; +} + +/** + * Pixel Shader + */ +float4 pixelShaderFunction(PixelShaderInput input): COLOR { + float3 lightVector = normalize(input.lightVector); + float3 normal = normalize(input.normal); + float3 viewPosition = normalize(input.viewPosition); + float3 halfVector = normalize(lightVector + viewPosition); + + // use lit function to calculate phong shading + // x component contains the ambient coefficient + // y component contains the diffuse coefficient: + // max(dot(normal, lightVector),0) + // z component contains the specular coefficient: + // dot(normal, lightVector) < 0 || dot(normal, halfVector) < 0 ? + // 0 : pow(dot(normal, halfVector), shininess) + // NOTE: This is actually Blinn-Phong shading, not Phong shading + // which would use the reflection vector instead of the half vector + + float4 phong_coeff = lit(dot(normal, lightVector), + dot(normal, halfVector), shininess); + + float4 ambient = light_ambient * phong_coeff.x * input.color; + float4 diffuse = light_diffuse * phong_coeff.y * input.color; + float4 specular = light_specular * phong_coeff.z * input.color; + + return ambient + diffuse + specular; +} + +// Here we tell our effect file *which* functions are +// our vertex and pixel shaders. + +// #o3d VertexShaderEntryPoint vertexShaderFunction +// #o3d PixelShaderEntryPoint pixelShaderFunction +// #o3d MatrixLoadOrder RowMajor + </textarea> + </td> + </tr> +</table> +</body> +</html> diff --git a/o3d/samples/siteswap/siteswap.js b/o3d/samples/siteswap/siteswap.js index 24a59a7cd..6b77aff 100644 --- a/o3d/samples/siteswap/siteswap.js +++ b/o3d/samples/siteswap/siteswap.js @@ -1,415 +1,415 @@ -// @@REWRITE(insert js-copyright)
-// @@REWRITE(delete-start)
-// Copyright 2009 Google Inc. All Rights Reserved
-// @@REWRITE(delete-end)
-
-/**
- * This file contains the top-level logic and o3d-related code for the siteswap
- * animator.
- */
-
-o3djs.require('o3djs.rendergraph');
-o3djs.require('o3djs.math');
-o3djs.require('o3djs.primitives');
-o3djs.require('o3djs.dump');
-
-// Global variables are all referenced via g, so that either interpreter can
-// find them easily.
-var g = {};
-
-/**
- * Creates a color based on an input index as a seed.
- * @param {!number} index the seed value to select the color.
- * @return {!Array.number} an [r g b a] color.
- */
-function createColor(index) {
- var N = 12; // Number of distinct colors.
- var root3 = Math.sqrt(3);
- var theta = 2 * Math.PI * index / N;
- var sin = Math.sin(theta);
- var cos = Math.cos(theta);
- return [(1 / 3 + 2 / 3 * cos) + (1 / 3 - cos / 3 - sin / root3),
- (1 / 3 - cos / 3 + sin / root3) + (1 / 3 + 2 / 3 * cos),
- (1 / 3 - cos / 3 - sin / root3) + (1 / 3 - cos / 3 + sin / root3),
- 1];
-}
-
-/**
- * Creates a material, given the index as a seed to make it distinguishable.
- * @param {number} index an integer used to create a distinctive color.
- * @return {!o3d.Material} the material.
- */
-function createMaterial(index) {
- var material = g.pack.createObject('Material');
-
- // Apply our effect to this material. The effect tells the 3D hardware
- // which shader to use.
- material.effect = g.effect;
-
- // Set the material's drawList
- material.drawList = g.viewInfo.performanceDrawList;
-
- // This will create our quadColor parameter on the material.
- g.effect.createUniformParameters(material);
-
- // Set up the individual parameters in our effect file.
-
- // Light position
- var light_pos_param = material.getParam('light_pos');
- light_pos_param.value = [10, 10, 20];
-
- // Phong components of the light source
- var light_ambient_param = material.getParam('light_ambient');
- var light_diffuse_param = material.getParam('light_diffuse');
- var light_specular_param = material.getParam('light_specular');
-
- // White ambient light
- light_ambient_param.value = [0.04, 0.04, 0.04, 1];
-
- light_diffuse_param.value = createColor(index);
- // White specular light
- light_specular_param.value = [0.5, 0.5, 0.5, 1];
-
- // Shininess of the material (for specular lighting)
- var shininess_param = material.getParam('shininess');
- shininess_param.value = 30.0;
-
- // Bind the counter's count to the input of the FunctionEval.
- var paramTime = material.getParam('time');
- paramTime.bind(g.counter.getParam('count'));
-
- material.getParam('camera_pos').value = g.eye;
-
- return material;
-}
-
-/**
- * Gets a material from our cache, creating it if it's not yet been made.
- * Uses index as a seed to make the material distinguishable.
- * @param {number} index an integer used to create/fetch a distinctive color.
- * @return {!o3d.Material} the material.
- */
-function getMaterial(index) {
- g.materials = g.materials || []; // See initStep2 for a comment.
- if (!g.materials[index]) {
- g.materials[index] = createMaterial(index);
- }
- return g.materials[index];
-}
-
-/**
- * Initializes g.o3d.
- * @param {Array} clientElements Array of o3d object elements.
- */
-function initStep2(clientElements) {
- // Initializes global variables and libraries.
- window.g = g;
-
- // Used to tell whether we need to recompute our view on resize.
- g.o3dWidth = -1;
- g.o3dHeight = -1;
-
- // We create a different material for each color of object.
- //g.materials = []; // TODO(ericu): If this is executed, we fail. Why?
-
- // We hold on to all the shapes here so that we can clean them up when we want
- // to change patterns.
- g.ballShapes = [];
- g.handShapes = [];
-
- g.o3dElement = clientElements[0];
- g.o3d = g.o3dElement.o3d;
- g.math = o3djs.math;
- g.client = g.o3dElement.client;
-
- // Initialize client sample libraries.
- o3djs.base.init(g.o3dElement);
-
- // Create a g.pack to manage our resources/assets
- g.pack = g.client.createPack();
-
- // Create the render graph for a view.
- g.viewInfo = o3djs.rendergraph.createBasicView(
- g.pack,
- g.client.root,
- g.client.renderGraphRoot);
-
- // Get the default context to hold view/projection matrices.
- g.context = g.viewInfo.drawContext;
-
- // Load a simple effect from a textarea.
- g.effect = g.pack.createObject('Effect');
- g.effect.loadFromFXString(document.getElementById('shader').value);
-
- // Eye-position: this is where our camera is located.
- // Global because each material we create must also know where it is, so that
- // the shader works properly.
- g.eye = [1, 6, 10];
-
- // Target, this is the point at which our camera is pointed.
- var target = [0, 2, 0];
-
- // Up-vector, this tells the camera which direction is 'up'.
- // We define the positive y-direction to be up in this example.
- var up = [0, 1, 0];
-
- g.context.view = g.math.matrix4.lookAt(g.eye, target, up);
-
- // Make a SecondCounter to provide the time for our animation.
- g.counter = g.pack.createObject('SecondCounter');
- g.counter.multiplier = 3; // Speed up time; this is in throws per second.
-
- // Generate the projection and viewProjection matrices based
- // on the g.o3d plugin size by calling onResize().
- onResize();
-
- // If we don't check the size of the client area every frame we don't get a
- // chance to adjust the perspective matrix fast enough to keep up with the
- // browser resizing us.
- // TODO(ericu): Switch to using the resize event once it's checked in.
- g.client.setRenderCallback(onResize);
-}
-
-/**
- * Stops or starts the animation based on the state of an html checkbox.
- */
-function updateAnimating() {
- var box = document.the_form.check_box;
- g.counter.running = box.checked;
-}
-
-/**
- * Generates the projection matrix based on the size of the g.o3d plugin
- * and calculates the view-projection matrix.
- */
-function onResize() {
- var newWidth = g.client.width;
- var newHeight = g.client.height;
-
- if (newWidth != g.o3dWidth || newHeight != g.o3dHeight) {
- debug('resizing');
- g.o3dWidth = newWidth;
- g.o3dHeight = newHeight;
-
- // Create our projection matrix, with a vertical field of view of 45 degrees
- // a near clipping plane of 0.1 and far clipping plane of 100.
- g.context.projection = g.math.matrix4.perspective(
- 45 * Math.PI / 180,
- g.o3dWidth / g.o3dHeight,
- 0.1,
- 100);
- }
-}
-
-/**
- * Computes and prepares animation of the pattern input via the html form. If
- * the box is checked, this will immediately begin animation as well.
- */
-function onComputePattern() {
- try {
- g.counter.removeAllCallbacks();
- var group = document.the_form.radio_group_hands;
- var numHands = -1;
- for (var i = 0; i < group.length; ++i) {
- if (group[i].checked) {
- numHands = parseInt(group[i].value);
- }
- }
- var style = 'even';
- if (document.the_form.pair_hands.checked) {
- style = 'pairs';
- }
- var patternString = document.getElementById('input_pattern').value;
- var patternData =
- computeFullPatternFromString(patternString, numHands, style);
- startAnimation(
- patternData.numBalls,
- patternData.numHands,
- patternData.duration,
- patternData.ballCurveSets,
- patternData.handCurveSets);
- } catch (ex) {
- popup(stringifyObj(ex));
- throw ex;
- }
- setUpSelection();
-}
-
-/**
- * Removes any callbacks so they don't get called after the page has unloaded.
- */
-function cleanup() {
- g.client.cleanup();
-}
-
-
-/**
- * Dump out a newline-terminated string to the debug console, if available.
- * @param {!string} s the string to output.
- */
-function debug(s) {
- o3djs.dump.dump(s + '\n');
-}
-
-/**
- * Dump out a newline-terminated string to the debug console, if available,
- * then display it via an alert.
- * @param {!string} s the string to output.
- */
-function popup(s) {
- debug(s);
- window.alert(s);
-}
-
-/**
- * If t, throw an exception.
- * @param {!bool} t the value to test.
- */
-function assert(t) {
- if (!t) {
- throw new Error('Assertion failed!');
- }
-}
-
-/**
- * Convert an object to a string containing a full one-level-deep property
- * listing, with values.
- * @param {!Object} o the object to convert.
- * @return {!string} the converted object.
- */
-function stringifyObj(o) {
- var s = '';
- for (var i in o) {
- s += i + ':' + o[i] + '\n';
- }
- return s;
-}
-
-/**
- * Add the information in a curve to the params on a shape, such that the vertex
- * shader will move the shape along the curve at times after timeBase.
- * @param {!Curve} curve the curve the shape should follow.
- * @param {!o3d.Shape} shape the shape being moved.
- * @param {!number} timeBase the base to subtract from the current time when
- * giving the curve calculation its time input.
- */
-function setParamCurveInfo(curve, shape, timeBase) {
- assert(curve);
- assert(shape);
- try {
- shape.elements[0].getParam('time_base').value = timeBase;
- shape.elements[0].getParam('coeff_a').value =
- [curve.xEqn.a, curve.yEqn.a, curve.zEqn.a];
- shape.elements[0].getParam('coeff_b').value =
- [curve.xEqn.b, curve.yEqn.b, curve.zEqn.b];
- shape.elements[0].getParam('coeff_c').value =
- [curve.xEqn.c, curve.yEqn.c, curve.zEqn.c];
- shape.elements[0].getParam('coeff_d').value =
- [curve.xEqn.d, curve.yEqn.d, curve.zEqn.d];
- shape.elements[0].getParam('coeff_e').value =
- [curve.xEqn.e, curve.yEqn.e, curve.zEqn.e];
- shape.elements[0].getParam('coeff_f').value =
- [curve.xEqn.f, curve.yEqn.f, curve.zEqn.f];
-
- assert(curve.xEqn.lerpRate == curve.yEqn.lerpRate);
- assert(curve.xEqn.lerpRate == curve.zEqn.lerpRate);
- shape.elements[0].getParam('coeff_lerp').value = curve.xEqn.lerpRate;
- if (curve.xEqn.lerpRate) {
- shape.elements[0].getParam('coeff_l_a').value =
- [curve.xEqn.lA, curve.yEqn.lA, curve.zEqn.lA];
- shape.elements[0].getParam('coeff_l_b').value =
- [curve.xEqn.lB, curve.yEqn.lB, curve.zEqn.lB];
- shape.elements[0].getParam('coeff_l_c').value =
- [curve.xEqn.lC, curve.yEqn.lC, curve.zEqn.lC];
- }
- } catch (ex) {
- debug(ex);
- throw ex;
- }
-}
-
-/**
- * Create the params that the shader expects on the supplied shape's first
- * element.
- * @param {!o3d.Shape} shape the shape on whose first element to create params.
- */
-function createParams(shape) {
- shape.elements[0].createParam('coeff_a', 'ParamFloat3').value = [0, 0, 0];
- shape.elements[0].createParam('coeff_b', 'ParamFloat3').value = [0, 0, 0];
- shape.elements[0].createParam('coeff_c', 'ParamFloat3').value = [0, 0, 0];
- shape.elements[0].createParam('coeff_d', 'ParamFloat3').value = [0, 0, 0];
- shape.elements[0].createParam('coeff_e', 'ParamFloat3').value = [0, 0, 0];
- shape.elements[0].createParam('coeff_f', 'ParamFloat3').value = [0, 0, 0];
- shape.elements[0].createParam('coeff_l_a', 'ParamFloat3').value = [0, 0, 0];
- shape.elements[0].createParam('coeff_l_b', 'ParamFloat3').value = [0, 0, 0];
- shape.elements[0].createParam('coeff_l_c', 'ParamFloat3').value = [0, 0, 0];
- shape.elements[0].createParam('coeff_lerp', 'ParamFloat').value = 0;
- shape.elements[0].createParam('time_base', 'ParamFloat').value = 0;
-}
-
-/**
- * Adjust the number of ball shapes in g.pack.
- * @param {!number} numBalls the number of balls desired.
- */
-function setNumBalls(numBalls) {
- for (var i = 0; i < g.ballShapes.length; ++i) {
- g.pack.removeObject(g.ballShapes[i]);
- g.client.root.removeShape(g.ballShapes[i]);
- }
- g.ballShapes = [];
-
- for (var i = 0; i < numBalls; ++i) {
- var shape = o3djs.primitives.createSphere(g.pack,
- getMaterial(5 * i),
- 0.10,
- 70,
- 70);
- shape.name = 'Ball ' + i;
-
- // generate the draw elements.
- shape.createDrawElements(g.pack, null);
-
- // Now attach the sphere to the root of the scene graph.
- g.client.root.addShape(shape);
-
- // Create the material params for the shader.
- createParams(shape);
-
- g.ballShapes[i] = shape;
- }
-}
-
-/**
- * Adjust the number of hand shapes in g.pack.
- * @param {!number} numHands the number of hands desired.
- */
-function setNumHands(numHands) {
- g.counter.removeAllCallbacks();
-
- for (var i = 0; i < g.handShapes.length; ++i) {
- g.pack.removeObject(g.handShapes[i]);
- g.client.root.removeShape(g.handShapes[i]);
- }
- g.handShapes = [];
-
- for (var i = 0; i < numHands; ++i) {
- var shape = o3djs.primitives.createBox(g.pack,
- getMaterial(3 * (i + 1)),
- 0.25,
- 0.05,
- 0.25);
- shape.name = 'Hand ' + i;
-
- // generate the draw elements.
- shape.createDrawElements(g.pack, null);
-
- // Now attach the sphere to the root of the scene graph.
- g.client.root.addShape(shape);
-
- // Create the material params for the shader.
- createParams(shape);
-
- g.handShapes[i] = shape;
- }
-}
-
+// @@REWRITE(insert js-copyright) +// @@REWRITE(delete-start) +// Copyright 2009 Google Inc. All Rights Reserved +// @@REWRITE(delete-end) + +/** + * This file contains the top-level logic and o3d-related code for the siteswap + * animator. + */ + +o3djs.require('o3djs.rendergraph'); +o3djs.require('o3djs.math'); +o3djs.require('o3djs.primitives'); +o3djs.require('o3djs.dump'); + +// Global variables are all referenced via g, so that either interpreter can +// find them easily. +var g = {}; + +/** + * Creates a color based on an input index as a seed. + * @param {!number} index the seed value to select the color. + * @return {!Array.number} an [r g b a] color. + */ +function createColor(index) { + var N = 12; // Number of distinct colors. + var root3 = Math.sqrt(3); + var theta = 2 * Math.PI * index / N; + var sin = Math.sin(theta); + var cos = Math.cos(theta); + return [(1 / 3 + 2 / 3 * cos) + (1 / 3 - cos / 3 - sin / root3), + (1 / 3 - cos / 3 + sin / root3) + (1 / 3 + 2 / 3 * cos), + (1 / 3 - cos / 3 - sin / root3) + (1 / 3 - cos / 3 + sin / root3), + 1]; +} + +/** + * Creates a material, given the index as a seed to make it distinguishable. + * @param {number} index an integer used to create a distinctive color. + * @return {!o3d.Material} the material. + */ +function createMaterial(index) { + var material = g.pack.createObject('Material'); + + // Apply our effect to this material. The effect tells the 3D hardware + // which shader to use. + material.effect = g.effect; + + // Set the material's drawList + material.drawList = g.viewInfo.performanceDrawList; + + // This will create our quadColor parameter on the material. + g.effect.createUniformParameters(material); + + // Set up the individual parameters in our effect file. + + // Light position + var light_pos_param = material.getParam('light_pos'); + light_pos_param.value = [10, 10, 20]; + + // Phong components of the light source + var light_ambient_param = material.getParam('light_ambient'); + var light_diffuse_param = material.getParam('light_diffuse'); + var light_specular_param = material.getParam('light_specular'); + + // White ambient light + light_ambient_param.value = [0.04, 0.04, 0.04, 1]; + + light_diffuse_param.value = createColor(index); + // White specular light + light_specular_param.value = [0.5, 0.5, 0.5, 1]; + + // Shininess of the material (for specular lighting) + var shininess_param = material.getParam('shininess'); + shininess_param.value = 30.0; + + // Bind the counter's count to the input of the FunctionEval. + var paramTime = material.getParam('time'); + paramTime.bind(g.counter.getParam('count')); + + material.getParam('camera_pos').value = g.eye; + + return material; +} + +/** + * Gets a material from our cache, creating it if it's not yet been made. + * Uses index as a seed to make the material distinguishable. + * @param {number} index an integer used to create/fetch a distinctive color. + * @return {!o3d.Material} the material. + */ +function getMaterial(index) { + g.materials = g.materials || []; // See initStep2 for a comment. + if (!g.materials[index]) { + g.materials[index] = createMaterial(index); + } + return g.materials[index]; +} + +/** + * Initializes g.o3d. + * @param {Array} clientElements Array of o3d object elements. + */ +function initStep2(clientElements) { + // Initializes global variables and libraries. + window.g = g; + + // Used to tell whether we need to recompute our view on resize. + g.o3dWidth = -1; + g.o3dHeight = -1; + + // We create a different material for each color of object. + //g.materials = []; // TODO(ericu): If this is executed, we fail. Why? + + // We hold on to all the shapes here so that we can clean them up when we want + // to change patterns. + g.ballShapes = []; + g.handShapes = []; + + g.o3dElement = clientElements[0]; + g.o3d = g.o3dElement.o3d; + g.math = o3djs.math; + g.client = g.o3dElement.client; + + // Initialize client sample libraries. + o3djs.base.init(g.o3dElement); + + // Create a g.pack to manage our resources/assets + g.pack = g.client.createPack(); + + // Create the render graph for a view. + g.viewInfo = o3djs.rendergraph.createBasicView( + g.pack, + g.client.root, + g.client.renderGraphRoot); + + // Get the default context to hold view/projection matrices. + g.context = g.viewInfo.drawContext; + + // Load a simple effect from a textarea. + g.effect = g.pack.createObject('Effect'); + g.effect.loadFromFXString(document.getElementById('shader').value); + + // Eye-position: this is where our camera is located. + // Global because each material we create must also know where it is, so that + // the shader works properly. + g.eye = [1, 6, 10]; + + // Target, this is the point at which our camera is pointed. + var target = [0, 2, 0]; + + // Up-vector, this tells the camera which direction is 'up'. + // We define the positive y-direction to be up in this example. + var up = [0, 1, 0]; + + g.context.view = g.math.matrix4.lookAt(g.eye, target, up); + + // Make a SecondCounter to provide the time for our animation. + g.counter = g.pack.createObject('SecondCounter'); + g.counter.multiplier = 3; // Speed up time; this is in throws per second. + + // Generate the projection and viewProjection matrices based + // on the g.o3d plugin size by calling onResize(). + onResize(); + + // If we don't check the size of the client area every frame we don't get a + // chance to adjust the perspective matrix fast enough to keep up with the + // browser resizing us. + // TODO(ericu): Switch to using the resize event once it's checked in. + g.client.setRenderCallback(onResize); +} + +/** + * Stops or starts the animation based on the state of an html checkbox. + */ +function updateAnimating() { + var box = document.the_form.check_box; + g.counter.running = box.checked; +} + +/** + * Generates the projection matrix based on the size of the g.o3d plugin + * and calculates the view-projection matrix. + */ +function onResize() { + var newWidth = g.client.width; + var newHeight = g.client.height; + + if (newWidth != g.o3dWidth || newHeight != g.o3dHeight) { + debug('resizing'); + g.o3dWidth = newWidth; + g.o3dHeight = newHeight; + + // Create our projection matrix, with a vertical field of view of 45 degrees + // a near clipping plane of 0.1 and far clipping plane of 100. + g.context.projection = g.math.matrix4.perspective( + 45 * Math.PI / 180, + g.o3dWidth / g.o3dHeight, + 0.1, + 100); + } +} + +/** + * Computes and prepares animation of the pattern input via the html form. If + * the box is checked, this will immediately begin animation as well. + */ +function onComputePattern() { + try { + g.counter.removeAllCallbacks(); + var group = document.the_form.radio_group_hands; + var numHands = -1; + for (var i = 0; i < group.length; ++i) { + if (group[i].checked) { + numHands = parseInt(group[i].value); + } + } + var style = 'even'; + if (document.the_form.pair_hands.checked) { + style = 'pairs'; + } + var patternString = document.getElementById('input_pattern').value; + var patternData = + computeFullPatternFromString(patternString, numHands, style); + startAnimation( + patternData.numBalls, + patternData.numHands, + patternData.duration, + patternData.ballCurveSets, + patternData.handCurveSets); + } catch (ex) { + popup(stringifyObj(ex)); + throw ex; + } + setUpSelection(); +} + +/** + * Removes any callbacks so they don't get called after the page has unloaded. + */ +function cleanup() { + g.client.cleanup(); +} + + +/** + * Dump out a newline-terminated string to the debug console, if available. + * @param {!string} s the string to output. + */ +function debug(s) { + o3djs.dump.dump(s + '\n'); +} + +/** + * Dump out a newline-terminated string to the debug console, if available, + * then display it via an alert. + * @param {!string} s the string to output. + */ +function popup(s) { + debug(s); + window.alert(s); +} + +/** + * If t, throw an exception. + * @param {!bool} t the value to test. + */ +function assert(t) { + if (!t) { + throw new Error('Assertion failed!'); + } +} + +/** + * Convert an object to a string containing a full one-level-deep property + * listing, with values. + * @param {!Object} o the object to convert. + * @return {!string} the converted object. + */ +function stringifyObj(o) { + var s = ''; + for (var i in o) { + s += i + ':' + o[i] + '\n'; + } + return s; +} + +/** + * Add the information in a curve to the params on a shape, such that the vertex + * shader will move the shape along the curve at times after timeBase. + * @param {!Curve} curve the curve the shape should follow. + * @param {!o3d.Shape} shape the shape being moved. + * @param {!number} timeBase the base to subtract from the current time when + * giving the curve calculation its time input. + */ +function setParamCurveInfo(curve, shape, timeBase) { + assert(curve); + assert(shape); + try { + shape.elements[0].getParam('time_base').value = timeBase; + shape.elements[0].getParam('coeff_a').value = + [curve.xEqn.a, curve.yEqn.a, curve.zEqn.a]; + shape.elements[0].getParam('coeff_b').value = + [curve.xEqn.b, curve.yEqn.b, curve.zEqn.b]; + shape.elements[0].getParam('coeff_c').value = + [curve.xEqn.c, curve.yEqn.c, curve.zEqn.c]; + shape.elements[0].getParam('coeff_d').value = + [curve.xEqn.d, curve.yEqn.d, curve.zEqn.d]; + shape.elements[0].getParam('coeff_e').value = + [curve.xEqn.e, curve.yEqn.e, curve.zEqn.e]; + shape.elements[0].getParam('coeff_f').value = + [curve.xEqn.f, curve.yEqn.f, curve.zEqn.f]; + + assert(curve.xEqn.lerpRate == curve.yEqn.lerpRate); + assert(curve.xEqn.lerpRate == curve.zEqn.lerpRate); + shape.elements[0].getParam('coeff_lerp').value = curve.xEqn.lerpRate; + if (curve.xEqn.lerpRate) { + shape.elements[0].getParam('coeff_l_a').value = + [curve.xEqn.lA, curve.yEqn.lA, curve.zEqn.lA]; + shape.elements[0].getParam('coeff_l_b').value = + [curve.xEqn.lB, curve.yEqn.lB, curve.zEqn.lB]; + shape.elements[0].getParam('coeff_l_c').value = + [curve.xEqn.lC, curve.yEqn.lC, curve.zEqn.lC]; + } + } catch (ex) { + debug(ex); + throw ex; + } +} + +/** + * Create the params that the shader expects on the supplied shape's first + * element. + * @param {!o3d.Shape} shape the shape on whose first element to create params. + */ +function createParams(shape) { + shape.elements[0].createParam('coeff_a', 'ParamFloat3').value = [0, 0, 0]; + shape.elements[0].createParam('coeff_b', 'ParamFloat3').value = [0, 0, 0]; + shape.elements[0].createParam('coeff_c', 'ParamFloat3').value = [0, 0, 0]; + shape.elements[0].createParam('coeff_d', 'ParamFloat3').value = [0, 0, 0]; + shape.elements[0].createParam('coeff_e', 'ParamFloat3').value = [0, 0, 0]; + shape.elements[0].createParam('coeff_f', 'ParamFloat3').value = [0, 0, 0]; + shape.elements[0].createParam('coeff_l_a', 'ParamFloat3').value = [0, 0, 0]; + shape.elements[0].createParam('coeff_l_b', 'ParamFloat3').value = [0, 0, 0]; + shape.elements[0].createParam('coeff_l_c', 'ParamFloat3').value = [0, 0, 0]; + shape.elements[0].createParam('coeff_lerp', 'ParamFloat').value = 0; + shape.elements[0].createParam('time_base', 'ParamFloat').value = 0; +} + +/** + * Adjust the number of ball shapes in g.pack. + * @param {!number} numBalls the number of balls desired. + */ +function setNumBalls(numBalls) { + for (var i = 0; i < g.ballShapes.length; ++i) { + g.pack.removeObject(g.ballShapes[i]); + g.client.root.removeShape(g.ballShapes[i]); + } + g.ballShapes = []; + + for (var i = 0; i < numBalls; ++i) { + var shape = o3djs.primitives.createSphere(g.pack, + getMaterial(5 * i), + 0.10, + 70, + 70); + shape.name = 'Ball ' + i; + + // generate the draw elements. + shape.createDrawElements(g.pack, null); + + // Now attach the sphere to the root of the scene graph. + g.client.root.addShape(shape); + + // Create the material params for the shader. + createParams(shape); + + g.ballShapes[i] = shape; + } +} + +/** + * Adjust the number of hand shapes in g.pack. + * @param {!number} numHands the number of hands desired. + */ +function setNumHands(numHands) { + g.counter.removeAllCallbacks(); + + for (var i = 0; i < g.handShapes.length; ++i) { + g.pack.removeObject(g.handShapes[i]); + g.client.root.removeShape(g.handShapes[i]); + } + g.handShapes = []; + + for (var i = 0; i < numHands; ++i) { + var shape = o3djs.primitives.createBox(g.pack, + getMaterial(3 * (i + 1)), + 0.25, + 0.05, + 0.25); + shape.name = 'Hand ' + i; + + // generate the draw elements. + shape.createDrawElements(g.pack, null); + + // Now attach the sphere to the root of the scene graph. + g.client.root.addShape(shape); + + // Create the material params for the shader. + createParams(shape); + + g.handShapes[i] = shape; + } +} + |