Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2)

Side by Side Diff: samples/siteswap/math.js

Issue 155005: Adding unfinished-but-working siteswap demo, copied from an ancient perforce changelist. (Closed) Base URL: svn://chrome-svn/chrome/trunk/src/o3d/
Patch Set: Created 11 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « samples/siteswap/animation.js ('k') | samples/siteswap/siteswap.html » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // @@REWRITE(insert js-copyright)
2 // @@REWRITE(delete-start)
3 // Copyright 2009 Google Inc. All Rights Reserved
4 // @@REWRITE(delete-end)
5
6 /**
7 * @fileoverview This file contains all the math for the siteswap animator. It
8 * handles all of the site-swap-related stuff [converting a sequence of integers
9 * into a more-useful representation of a pattern, pattern validation, etc.] as
10 * well as all the physics used for the simulation.
11 */
12
13 /**
14 * This is a container class that holds the coefficients of an equation
15 * describing the motion of an object.
16 * The basic equation is:
17 * f(x) := a t^2 + b t + c + d sin (f t) + e cos (f t).
18 * However, sometimes we LERP between that function and this one:
19 * g(x) := lA t^2 + lB t + lC
20 * lerpRate [so far] is always either 1 [LERP from f to g over 1 beat] or -1,
21 * [LERP from g to f over one beat].
22 *
23 * Just plug in t to evaluate the equation. There's no JavaScript function to
24 * do this because it's always done on the GPU.
25 *
26 * @constructor
27 */
28 EquationCoefficients = function(a, b, c, d, e, f, lA, lB, lC, lerpRate) {
29 assert(!isNaN(a) && !isNaN(b) && !isNaN(c));
30 d = d || 0;
31 e = e || 0;
32 f = f || 0;
33 assert(!isNaN(d) && !isNaN(e) && !isNaN(f));
34 lA = lA || 0;
35 lB = lB || 0;
36 lC = lC || 0;
37 assert(!isNaN(lA) && !isNaN(lB) && !isNaN(lC));
38 lerpRate = lerpRate || 0;
39 this.a = a;
40 this.b = b;
41 this.c = c;
42 this.d = d;
43 this.e = e;
44 this.f = f;
45 this.lA = lA;
46 this.lB = lB;
47 this.lC = lC;
48 this.lerpRate = lerpRate;
49 }
50
51 /**
52 * Create a new equation that's equivalent to this equation's coefficients a-f
53 * with a LERP to the polynomial portion of the supplied equation.
54 * @param {!EquationCoefficients} eqn the source of coefficients.
55 * @param {!number} lerpRate the rate and direction of the LERP; positive for
56 * "from this equation to the new one" and vice-versa.
57 * @return {!EquationCoefficients} a new set of coefficients.
58 */
59 EquationCoefficients.prototype.lerpIn = function(eqn, lerpRate) {
60 assert(!this.lerpRate);
61 return new EquationCoefficients(this.a, this.b, this.c, this.d, this.e,
62 this.f, eqn.a, eqn.b, eqn.c, lerpRate);
63 };
64
65 /**
66 * Convert the EquationCoefficients to a string for debugging.
67 * @return {String} debugging output.
68 */
69 EquationCoefficients.prototype.toString = function() {
70 return 'F(t) := ' + this.a.toFixed(2) + ' t^2 + ' + this.b.toFixed(2) +
71 ' t + ' + this.c.toFixed(2) + ' + ' +
72 this.d.toFixed(2) + ' sin(' + this.f.toFixed(2) + ' t) + ' +
73 this.e.toFixed(2) + ' cos(' + this.f.toFixed(2) + ' t) + LERP(' +
74 this.lerpRate.toFixed(2) + ') of ' +
75 this.lA.toFixed(2) + ' t^2 + ' + this.lB.toFixed(2) +
76 ' t + ' + this.lC.toFixed(2);
77 };
78
79 /**
80 * A set of equations which describe the motion of an object over time.
81 * The three equations each supply one dimension of the motion, and the curve is
82 * valid from startTime to startTime + duration.
83 * @param {!number} startTime the initial time at which the curve is valid.
84 * @param {!number} duration how long [in beats] the curve is valid.
85 * @param {!EquationCoefficients} xEqn the equation for motion in x.
86 * @param {!EquationCoefficients} yEqn the equation for motion in y.
87 * @param {!EquationCoefficients} zEqn the equation for motion in z.
88 * @constructor
89 */
90 Curve = function(startTime, duration, xEqn, yEqn, zEqn) {
91 this.startTime = startTime;
92 this.duration = duration;
93 this.xEqn = xEqn;
94 this.yEqn = yEqn;
95 this.zEqn = zEqn;
96 }
97
98 /**
99 * Convert the Curve to a string for debugging.
100 * @return {String} debugging output.
101 */
102 Curve.prototype.toString = function() {
103 var s = 'startTime: ' + this.startTime + '\n';
104 s += 'duration: ' + this.duration + '\n';
105 s += this.xEqn + '\n';
106 s += this.yEqn + '\n';
107 s += this.zEqn + '\n';
108 return s;
109 };
110
111 /**
112 * Modify this curve's coefficients to include a LERP to the polynomial
113 * portion of the supplied curve.
114 * @param {!Curve} curve the source of coefficients.
115 * @param {!number} lerpRate the rate and direction of the LERP; positive for
116 * "from this equation to the new one" and vice-versa.
117 * @return {!Curve} a new curve.
118 */
119 Curve.prototype.lerpIn = function(curve, lerpRate) {
120 assert(this.startTime == curve.startTime);
121 assert(this.duration == curve.duration);
122 var xEqn = this.xEqn.lerpIn(curve.xEqn, lerpRate);
123 var yEqn = this.yEqn.lerpIn(curve.yEqn, lerpRate);
124 var zEqn = this.zEqn.lerpIn(curve.zEqn, lerpRate);
125 return new Curve(this.startTime, this.duration, xEqn, yEqn, zEqn);
126 };
127
128 /**
129 * Produce a set of polynomial coefficients that describe linear motion between
130 * two points in 1 dimension.
131 * @param {!number} startPos the starting position.
132 * @param {!number} endPos the ending position.
133 * @param {!number} duration how long the motion takes.
134 * @return {!EquationCoefficients} the equation for the motion.
135 */
136 Curve.computeLinearCoefficients = function(startPos, endPos, duration) {
137 return new EquationCoefficients(
138 0, (endPos - startPos) / duration, startPos);
139 }
140
141 var GRAVITY = 1; // Higher means higher throws for the same duration.
142 /**
143 * Produce a set of polynomial coefficients that describe parabolic motion
144 * between two points in 1 dimension.
145 * @param {!number} startPos the starting position.
146 * @param {!number} endPos the ending position.
147 * @param {!number} duration how long the motion takes.
148 * @return {!EquationCoefficients} the equation for the motion.
149 */
150 Curve.computeParabolicCoefficients = function(startPos, endPos, duration) {
151 var dY = endPos - startPos;
152 return new EquationCoefficients(-GRAVITY / 2,
153 dY / duration + GRAVITY * duration / 2,
154 startPos);
155 }
156
157 /**
158 * Compute the curve taken by a ball given its throw and catch positions, the
159 * time it was thrown, and how long it stayed in the air.
160 *
161 * We use duration rather than throwTime and catchTime because, what
162 * with the modular arithmetic used in our records, catchTime might be before
163 * throwTime, and in some representations the pattern could wrap around a few
164 * times while the ball's in the air. When the parabola computed here is used,
165 * time must be supplied as an offset from the time of the throw, and must of
166 * course not wrap at all. That is, these coefficients work for f(0) ==
167 * throwPos, f(duration) == catchPos.
168 *
169 * We treat the y axis as vertical and thus affected by gravity.
170 *
171 * @param {!EquationCoefficients} throwPos
172 * @param {!EquationCoefficients} catchPos
173 * @param {!number} startTime
174 * @param {!number} duration
175 * @return {!Curve}
176 */
177 Curve.computeThrowCurve = function(throwPos, catchPos, startTime, duration) {
178 var xEqn = Curve.computeLinearCoefficients(throwPos.x, catchPos.x, duration);
179 var yEqn = Curve.computeParabolicCoefficients(throwPos.y, catchPos.y,
180 duration);
181 var zEqn = Curve.computeLinearCoefficients(throwPos.z, catchPos.z, duration);
182 return new Curve(startTime, duration, xEqn, yEqn, zEqn);
183 }
184
185 /**
186 * Compute a straight line Curve given start and end positions, the start time,
187 * and the duration of the motion.
188 *
189 * @param {!EquationCoefficients} startPos
190 * @param {!EquationCoefficients} endPos
191 * @param {!number} startTime
192 * @param {!number} duration
193 * @return {!Curve}
194 */
195 Curve.computeStraightLineCurve =
196 function(startPos, endPos, startTime, duration) {
197 var xEqn = Curve.computeLinearCoefficients(startPos.x, endPos.x, duration);
198 var yEqn = Curve.computeLinearCoefficients(startPos.y, endPos.y, duration);
199 var zEqn = Curve.computeLinearCoefficients(startPos.z, endPos.z, duration);
200 return new Curve(startTime, duration, xEqn, yEqn, zEqn);
201 }
202
203 /**
204 * Threshold horizontal distance below which computeCircularCurve won't bother
205 * trying to approximate a circular curve. See the comment above
206 * computeCircularCurve for more info.
207 * @type {number}
208 */
209 Curve.EPSILON = .0001;
210
211 /**
212 * Compute a circular curve, used as an approximation for the motion of a hand
213 * between a catch and its following throw.
214 *
215 * Assumes a lot of stuff about this looking like a "normal" throw: the catch is
216 * moving roughly the opposite direction as the throw, the throw and catch
217 * aren't at the same place, and such. Otherwise this looks very odd at best.
218 * This is used for the height of the curve.
219 * This produces coefficients for d sin(f t) + e cos(f t) for each of x, y, z.
220 * It produces a vertical-ish circular curve from the start to the end, going
221 * down, then up. So if dV [the distance from the start to finish in the x-z
222 * plane, ignoring y] is less than Curve.EPSILON, it doesn't know which way down
223 * is, and it bails by returning a straight line instead.
224 */
225 Curve.computeCircularCurve = function(startPos, endPos, startTime, duration) {
226 var dX = endPos.x - startPos.x;
227 var dY = endPos.y - startPos.y;
228 var dZ = endPos.z - startPos.z;
229 var dV = Math.sqrt(dX * dX + dZ * dZ);
230 if (dV < Curve.EPSILON) {
231 return Curve.computeStraightLineCurve(startPos, endPos, startTime,
232 duration);
233 }
234 var negHalfdV = -0.5 * dV;
235 var negHalfdY = -0.5 * dY;
236 var f = Math.PI / duration;
237 var yEqn = new EquationCoefficients(
238 0, 0, startPos.y + dY / 2,
239 negHalfdV, negHalfdY, f);
240 var ratio = dX / dV;
241 var xEqn = new EquationCoefficients(
242 0, 0, startPos.x + dX / 2,
243 negHalfdY * ratio, negHalfdV * ratio, f);
244 ratio = dZ / dV;
245 var zEqn = new EquationCoefficients(
246 0, 0, startPos.z + dZ / 2,
247 negHalfdY * ratio, negHalfdV * ratio, f);
248 return new Curve(startTime, duration, xEqn, yEqn, zEqn);
249 }
250
251 /**
252 * This is the abstract base class for an object that describes a throw, catch,
253 * or empty hand [placeholder] in a site-swap pattern.
254 * @constructor
255 */
256 Descriptor = function() {
257 }
258
259 /**
260 * Create an otherwise-identical copy of this descriptor at a given time offset.
261 * Note that offset may put time past patternLength; the caller will have to fix
262 * this up manually.
263 * @param {number} offset how many beats to offset the new descriptor.
264 * Derived classes must override this function.
265 */
266 Descriptor.prototype.clone = function(offset) {
267 throw new Error('Unimplemented.');
268 };
269
270 /**
271 * Generate the Curve implied by this descriptor and the supplied hand
272 * positions.
273 * @param {!Array.HandPositionRecord} handPositions where the hands will be.
274 * Derived classes must override this function.
275 */
276 Descriptor.prototype.generateCurve = function(handPositions) {
277 throw new Error('Unimplemented.');
278 };
279
280 /**
281 * Adjust the start time of this Descriptor to be in [0, pathLength).
282 * @param {!number} pathLength the duration of a path, in beats.
283 * @return {!Descriptor} this.
284 */
285 Descriptor.prototype.fixUpModPathLength = function(pathLength) {
286 this.time = this.time % pathLength;
287 return this;
288 };
289
290 /**
291 * This describes a throw in a site-swap pattern.
292 * @param {!number} throwNum the site-swap number of the throw.
293 * @param {!number} throwTime the time this throw occurs.
294 * @param {!number} sourceHand the index of the throwing hand.
295 * @param {!number} destHand the index of the catching hand.
296 * @constructor
297 */
298 ThrowDescriptor = function(throwNum, throwTime, sourceHand, destHand) {
299 this.throwNum = throwNum;
300 this.sourceHand = sourceHand;
301 this.destHand = destHand;
302 this.time = throwTime;
303 }
304
305 /**
306 * This is a subclass of Descriptor.
307 */
308 ThrowDescriptor.prototype = new Descriptor();
309
310 /**
311 * Set up the constructor, just to be neat.
312 */
313 ThrowDescriptor.prototype.constructor = ThrowDescriptor;
314
315 /**
316 * We label each Descriptor subclass with a type for debugging.
317 */
318 ThrowDescriptor.prototype.type = 'THROW';
319
320 /**
321 * Create an otherwise-identical copy of this descriptor at a given time offset.
322 * Note that offset may put time past patternLength; the caller will have to fix
323 * this up manually.
324 * @param {number} offset how many beats to offset the new descriptor.
325 * @return {!Descriptor} the new copy.
326 */
327 ThrowDescriptor.prototype.clone = function(offset) {
328 offset = offset || 0; // Turn null into 0.
329 return new ThrowDescriptor(this.throwNum, this.time + offset,
330 this.sourceHand, this.destHand);
331 };
332
333 /**
334 * Convert the ThrowDescriptor to a string for debugging.
335 * @return {String} debugging output.
336 */
337 ThrowDescriptor.prototype.toString = function() {
338 return '(' + this.throwNum + ' from hand ' + this.sourceHand + ' to hand ' +
339 this.destHand + ')';
340 };
341
342 /**
343 * Generate the Curve implied by this descriptor and the supplied hand
344 * positions.
345 * @param {!Array.HandPositionRecord} handPositions where the hands will be.
346 * @return {!Curve} the curve.
347 */
348 ThrowDescriptor.prototype.generateCurve = function(handPositions) {
349 var startPos = handPositions[this.sourceHand].throwPositions[this.destHand];
350 var endPos = handPositions[this.destHand].catchPosition;
351 return Curve.computeThrowCurve(startPos, endPos, this.time,
352 this.throwNum - 1); };
353
354 /**
355 * This describes a catch in a site-swap pattern.
356 * @param {!number} hand the index of the catching hand.
357 * @param {!number} sourceThrowNum the site-swap number of the preceeding throw.
358 * @param {!number} destThrowNum the site-swap number of the following throw.
359 * @param {!number} sourceHand the index of the hand throwing the source throw.
360 * @param {!number} destHand the index of the hand catching the following throw.
361 * @param {!number} catchTime the time at which the catch occurs.
362 * @constructor
363 */
364 CarryDescriptor = function(hand, sourceThrowNum, destThrowNum, sourceHand,
365 destHand, catchTime) {
366 this.hand = hand;
367 this.sourceThrowNum = sourceThrowNum;
368 this.destThrowNum = destThrowNum;
369 this.sourceHand = sourceHand;
370 this.destHand = destHand;
371 this.time = catchTime;
372 }
373
374 /**
375 * This is a subclass of Descriptor.
376 */
377 CarryDescriptor.prototype = new Descriptor();
378
379 /**
380 * Set up the constructor, just to be neat.
381 */
382 CarryDescriptor.prototype.constructor = CarryDescriptor;
383
384 /**
385 * We label each Descriptor subclass with a type for debugging.
386 */
387 CarryDescriptor.prototype.type = 'CARRY';
388
389 /**
390 * Since this gets pathLength, not patternLength, we'll have to collapse sets
391 * of CarryDescriptors later, as they may be spread sparsely through the full
392 * animation and we'll only want them to be distributed over the full pattern
393 * length. We may have dupes to throw away as well.
394 * @param {!ThrowDescriptor} inThrowDescriptor
395 * @param {!ThrowDescriptor} outThrowDescriptor
396 * @param {!number} pathLength
397 * @return {!CarryDescriptor}
398 */
399 CarryDescriptor.fromThrowDescriptors = function(inThrowDescriptor,
400 outThrowDescriptor, pathLength) {
401 assert(inThrowDescriptor.destHand == outThrowDescriptor.sourceHand);
402 assert((inThrowDescriptor.time + inThrowDescriptor.throwNum) %
403 pathLength == outThrowDescriptor.time);
404 return new CarryDescriptor(inThrowDescriptor.destHand,
405 inThrowDescriptor.throwNum, outThrowDescriptor.throwNum,
406 inThrowDescriptor.sourceHand, outThrowDescriptor.destHand,
407 (outThrowDescriptor.time + pathLength - 1) % pathLength);
408 };
409
410 /**
411 * Create an otherwise-identical copy of this descriptor at a given time offset.
412 * Note that offset may put time past patternLength; the caller will have to fix
413 * this up manually.
414 * @param {number} offset how many beats to offset the new descriptor.
415 * @return {!Descriptor} the new copy.
416 */
417 CarryDescriptor.prototype.clone = function(offset) {
418 offset = offset || 0; // Turn null into 0.
419 return new CarryDescriptor(this.hand, this.sourceThrowNum,
420 this.destThrowNum, this.sourceHand, this.destHand, this.time + offset);
421 };
422
423 /**
424 * Convert the CarryDescriptor to a string for debugging.
425 * @return {String} debugging output.
426 */
427 CarryDescriptor.prototype.toString = function() {
428 return 'time: ' + this.time + ' (hand ' + this.hand + ' catches ' +
429 this.sourceThrowNum + ' from hand ' + this.sourceHand + ' then throws ' +
430 this.destThrowNum + ' to hand ' + this.destHand + ')';
431 };
432
433 /**
434 * Test if this CarryDescriptor is equivalent to another, mod patternLength.
435 * @param {!CarryDescriptor} cd the other CarryDescriptor.
436 * @param {!number} patternLength the length of the pattern.
437 * @return {!bool}
438 */
439 CarryDescriptor.prototype.equalsWithMod = function(cd, patternLength) {
440 if (!(cd instanceof CarryDescriptor)) {
441 return false;
442 }
443 if (this.hand != cd.hand) {
444 return false;
445 }
446 if (this.sourceThrowNum != cd.sourceThrowNum) {
447 return false;
448 }
449 if (this.destThrowNum != cd.destThrowNum) {
450 return false;
451 }
452 if (this.sourceHand != cd.sourceHand) {
453 return false;
454 }
455 if (this.destHand != cd.destHand) {
456 return false;
457 }
458 if (this.time % patternLength != cd.time % patternLength) {
459 return false;
460 }
461 return true;
462 };
463
464 /**
465 * Generate the Curve implied by this descriptor and the supplied hand
466 * positions.
467 * @param {!Array.HandPositionRecord} handPositions where the hands will be.
468 * @return {!Curve} the curve.
469 */
470 CarryDescriptor.prototype.generateCurve = function(handPositions) {
471 var startPos = handPositions[this.hand].catchPosition;
472 var endPos = handPositions[this.hand].throwPositions[this.destHand];
473 return Curve.computeCircularCurve(startPos, endPos, this.time, 1);
474 };
475
476 /**
477 * This describes a carry of a "1" in a site-swap pattern.
478 * The flags isThrow and isCatch tell whether this is the actual 1 [isThrow] or
479 * the carry that receives the handoff [isCatch]. It's legal for both to be
480 * true, which happens when there are two 1s in a row.
481 * @param {!number} sourceThrowNum the site-swap number of the prev throw
482 * [including this one if isCatch].
483 * @param {!number} sourceHand the index of the hand throwing sourceThrowNum.
484 * @param {!number} destThrowNum the site-swap number of the next throw
485 * [including this one if isThrow].
486 * @param {!number} destHand the index of the hand catching destThrowNum.
487 * @param {!number} hand the index of the hand doing this carry.
488 * @param {!number} time the time at which the carry starts.
489 * @param {!bool} isThrow whether this is a 1.
490 * @param {!bool} isCatch whether this is the carry after a 1.
491 * @constructor
492 */
493 CarryOneDescriptor = function(sourceThrowNum, sourceHand, destThrowNum,
494 destHand, hand, time, isThrow, isCatch) {
495 // It's possible to have !isCatch with sourceThrowNum == 1 temporarily, if we
496 // just haven't handled that 1 yet [we're doing the throw of this one, and
497 // will later get to the previous one, due to wraparound], and vice-versa.
498 assert(isThrow || (sourceThrowNum == 1));
499 assert(isCatch || (destThrowNum == 1));
500 this.sourceThrowNum = sourceThrowNum;
501 this.sourceHand = sourceHand;
502 this.destHand = destHand;
503 this.destThrowNum = destThrowNum;
504 this.hand = hand;
505 this.time = time;
506 this.isThrow = isThrow;
507 this.isCatch = isCatch;
508 return this;
509 }
510
511 /**
512 * This is a subclass of Descriptor.
513 */
514 CarryOneDescriptor.prototype = new Descriptor();
515
516 /**
517 * Set up the constructor, just to be neat.
518 */
519 CarryOneDescriptor.prototype.constructor = CarryOneDescriptor;
520
521 /**
522 * We label each Descriptor subclass with a type for debugging.
523 */
524 CarryOneDescriptor.prototype.type = 'CARRY_ONE';
525
526 /**
527 * Create a pair of CarryOneDescriptors to describe the carry that is a throw of
528 * 1. A 1 spends all its time being carried, so these two carries surrounding
529 * it represent [and therefore don't have] a throw between them.
530 * Prev and post are generally the ordinary CarryDescriptors surrounding the
531 * throw of 1 that we're trying to implement. However, they could each [or
532 * both] independently be CarryOneDescriptors implementing other 1 throws.
533 * @param {!Descriptor} prev the carry descriptor previous to the 1.
534 * @param {!Descriptor} post the carry descriptor subsequent to the 1.
535 * @return {!Array.CarryOneDescriptor} a pair of CarryOneDescriptors.
536 */
537 CarryOneDescriptor.getDescriptorPair = function(prev, post) {
538 assert(prev instanceof CarryDescriptor || prev instanceof CarryOneDescriptor);
539 assert(post instanceof CarryDescriptor || post instanceof CarryOneDescriptor);
540 assert(prev.destHand == post.hand);
541 assert(prev.hand == post.sourceHand);
542 var newPrev;
543 var newPost;
544 if (prev instanceof CarryOneDescriptor) {
545 assert(prev.isCatch && !prev.isThrow);
546 newPrev = prev;
547 newPrev.isThrow = true;
548 assert(newPrev.destHand == post.hand);
549 } else {
550 newPrev = new CarryOneDescriptor(prev.sourceThrowNum, prev.sourceHand, 1,
551 post.hand, prev.hand, prev.time, true, false);
552 }
553 if (post instanceof CarryOneDescriptor) {
554 assert(post.isThrow && !post.isCatch);
555 newPost = post;
556 newPost.isCatch = true;
557 assert(newPost.sourceHand == prev.hand);
558 assert(newPost.sourceThrowNum == 1);
559 } else {
560 newPost = new CarryOneDescriptor(1, prev.hand, post.destThrowNum,
561 post.destHand, post.hand, post.time, false, true);
562 }
563 return [newPrev, newPost];
564 };
565
566 /**
567 * Convert the CarryOneDescriptor to a string for debugging.
568 * @return {String} debugging output.
569 */
570 CarryOneDescriptor.prototype.toString = function() {
571 var s;
572 if (this.isThrow) {
573 s = 'Hand ' + this.hand + ' catches a ' + this.sourceThrowNum + ' from ' +
574 this.sourceHand + ' at time ' + this.time + ' and then passes a 1 to ' +
575 this.destHand + '.';
576 } else {
577 assert(this.isCatch && this.sourceThrowNum == 1);
578 s = 'Hand ' + this.hand + ' catches a 1 from ' + this.sourceHand +
579 ' at time ' + this.time + ' and then passes a ' + this.destThrowNum +
580 ' to ' + this.destHand + '.';
581 }
582 return s;
583 };
584
585 /**
586 * Compute the curve taken by a ball during the carry representing a 1, as long
587 * as it's not both a catch and a throw of a 1, which is handled elsewhere.
588 * It's either a LERP from a circular curve [a catch of a throw > 1] to a
589 * straight line to the handoff point [for isThrow] or a LERP from a straight
590 * line from the handoff to a circular curve for the next throw > 1 [for
591 * isCatch].
592 *
593 * @param {!EquationCoefficients} catchPos
594 * @param {!EquationCoefficients} throwPos
595 * @param {!EquationCoefficients} handoffPos
596 * @param {!number} startTime
597 * @param {!bool} isCatch whether this is the carry after a 1.
598 * @param {!bool} isThrow whether this is a 1.
599 * @return {!Curve}
600 */
601 Curve.computeCarryOneCurve = function(catchPos, throwPos, handoffPos, startTime,
602 isCatch, isThrow) {
603 assert(!isCatch != !isThrow);
604 var curve = Curve.computeCircularCurve(catchPos, throwPos, startTime, 1);
605 var curve2 = Curve.computeStraightLineCurve(handoffPos, handoffPos,
606 startTime, 1);
607 return curve.lerpIn(curve2, isThrow ? 1 : -1);
608 }
609
610 /**
611 * Compute the curve taken by a ball during the carry representing a 1 that is
612 * both the catch of one 1 and the immediately-following throw of another 1.
613 *
614 * @param {!EquationCoefficients} leadingHandoffPos
615 * @param {!EquationCoefficients} trailingHandoffPos
616 * @param {!Array.HandPositionRecord} handPositions where the hands will be.
617 * @param {!number} hand
618 * @param {!number} time the time at which the first 1's catch takes place.
619 * @return {!Curve}
620 */
621 Curve.computeConsecutiveCarryOneCurve = function(leadingHandoffPos,
622 trailingHandoffPos, handPositions, hand, time) {
623 var curve = Curve.computeStraightLineCurve(leadingHandoffPos,
624 handPositions[hand].basePosition, time, 1);
625 var curve2 =
626 Curve.computeStraightLineCurve(handPositions[hand].basePosition,
627 trailingHandoffPos, time, 1);
628 return curve.lerpIn(curve2, 1);
629 }
630
631 /**
632 * Generate the Curve implied by this descriptor and the supplied hand
633 * positions.
634 * @param {!Array.HandPositionRecord} handPositions where the hands will be.
635 * @return {!Curve} the curve.
636 */
637 CarryOneDescriptor.prototype.generateCurve = function(handPositions) {
638 var leadingHandoffPos, trailingHandoffPos;
639 if (this.isCatch) {
640 var p0 = handPositions[this.hand].basePosition;
641 var p1 = handPositions[this.sourceHand].basePosition;
642 handoffPos = leadingHandoffPos = p0.add(p1).scale(0.5);
643 }
644 if (this.isThrow) {
645 var p0 = handPositions[this.hand].basePosition;
646 var p1 = handPositions[this.destHand].basePosition;
647 handoffPos = trailingHandoffPos = p0.add(p1).scale(0.5);
648 }
649 if (!this.isCatch || !this.isThrow) {
650 return Curve.computeCarryOneCurve(handPositions[this.hand].catchPosition,
651 handPositions[this.hand].throwPositions[this.destHand], handoffPos,
652 this.time, this.isCatch, this.isThrow);
653 } else {
654 return Curve.computeConsecutiveCarryOneCurve(leadingHandoffPos,
655 trailingHandoffPos, handPositions, this.hand, this.time);
656 }
657 };
658
659 /**
660 * Create an otherwise-identical copy of this descriptor at a given time offset.
661 * Note that offset may put time past patternLength; the caller will have to fix
662 * this up manually.
663 * @param {number} offset how many beats to offset the new descriptor.
664 * @return {!Descriptor} the new copy.
665 */
666 CarryOneDescriptor.prototype.clone = function(offset) {
667 offset = offset || 0; // Turn null into 0.
668 return new CarryOneDescriptor(this.sourceThrowNum, this.sourceHand,
669 this.destThrowNum, this.destHand, this.hand, this.time + offset,
670 this.isThrow, this.isCatch);
671 };
672
673 /**
674 * Test if this CarryOneDescriptor is equivalent to another, mod patternLength.
675 * @param {!CarryOneDescriptor} cd the other CarryOneDescriptor.
676 * @param {!number} patternLength the length of the pattern.
677 * @return {!bool}
678 */
679 CarryOneDescriptor.prototype.equalsWithMod = function(cd, patternLength) {
680 if (!(cd instanceof CarryOneDescriptor)) {
681 return false;
682 }
683 if (this.hand != cd.hand) {
684 return false;
685 }
686 if (this.sourceThrowNum != cd.sourceThrowNum) {
687 return false;
688 }
689 if (this.destThrowNum != cd.destThrowNum) {
690 return false;
691 }
692 if (this.sourceHand != cd.sourceHand) {
693 return false;
694 }
695 if (this.destHand != cd.destHand) {
696 return false;
697 }
698 if (this.isCatch != cd.isCatch) {
699 return false;
700 }
701 if (this.isThrow != cd.isThrow) {
702 return false;
703 }
704 if (this.time % patternLength != cd.time % patternLength) {
705 return false;
706 }
707 return true;
708 };
709
710 /**
711 * This describes an empty hand in a site-swap pattern.
712 * @param {!Descriptor} cd0 the CarryDescriptor or CarryOneDescriptor describing
713 * this hand immediately before it was emptied.
714 * @param {!Descriptor} cd1 the CarryDescriptor or CarryOneDescriptor describing
715 * this hand immediately after it's done being empty.
716 * @param {!number} patternLength the length of the pattern.
717 * @constructor
718 */
719 EmptyHandDescriptor = function(cd0, cd1, patternLength) {
720 assert(cd0.hand == cd1.hand);
721 this.hand = cd0.hand;
722 this.prevThrowDest = cd0.destHand;
723 this.sourceThrowNum = cd0.destThrowNum;
724 this.nextCatchSource = cd1.sourceHand;
725 this.destThrowNum = cd1.sourceThrowNum;
726 // This code assumes that each CarryDescriptor and CarryOneDescriptor always
727 // has a duration of 1 beat. If we want to be able to allow long-held balls
728 // [instead of thrown twos, for example], we'll have to fix that here and a
729 // number of other places.
730 this.time = (cd0.time + 1) % patternLength;
731 this.duration = cd1.time - this.time;
732 if (this.duration < 0) {
733 this.duration += patternLength;
734 assert(this.duration > 0);
735 }
736 }
737
738 /**
739 * This is a subclass of Descriptor.
740 */
741 EmptyHandDescriptor.prototype = new Descriptor();
742
743 /**
744 * Set up the constructor, just to be neat.
745 */
746 EmptyHandDescriptor.prototype.constructor = EmptyHandDescriptor;
747
748 /**
749 * We label each Descriptor subclass with a type for debugging.
750 */
751 EmptyHandDescriptor.prototype.type = 'EMPTY';
752
753 /**
754 * Convert the EmptyHandDescriptor to a string for debugging.
755 * @return {String} debugging output.
756 */
757 EmptyHandDescriptor.prototype.toString = function() {
758 return 'time: ' + this.time + ' for ' + this.duration + ' (hand ' +
759 this.hand + ', after throwing a ' + this.sourceThrowNum + ' to hand ' +
760 this.prevThrowDest + ' then catches a ' + this.destThrowNum +
761 ' from hand ' + this.nextCatchSource + ')';
762 };
763
764 /**
765 * Generate the Curve implied by this descriptor and the supplied hand
766 * positions.
767 * @param {!Array.HandPositionRecord} handPositions where the hands will be.
768 * @return {!Curve} the curve.
769 */
770 EmptyHandDescriptor.prototype.generateCurve = function(handPositions) {
771 var startPos, endPos;
772 if (this.sourceThrowNum == 1) {
773 var p0 = handPositions[this.hand].basePosition;
774 var p1 = handPositions[this.prevThrowDest].basePosition;
775 startPos = p0.add(p1).scale(0.5);
776 } else {
777 startPos = handPositions[this.hand].throwPositions[this.prevThrowDest];
778 }
779 if (this.destThrowNum == 1) {
780 var p0 = handPositions[this.hand].basePosition;
781 var p1 = handPositions[this.nextCatchSource].basePosition;
782 endPos = p0.add(p1).scale(0.5);
783 } else {
784 endPos = handPositions[this.hand].catchPosition;
785 }
786 // TODO: Replace with a good empty-hand curve.
787 return Curve.computeStraightLineCurve(startPos, endPos, this.time,
788 this.duration);
789 };
790
791 /**
792 * A series of descriptors that describes the full path of an object during a
793 * pattern.
794 * @param {!Array.Descriptor} descriptors all descriptors for the object.
795 * @param {!number} pathLength the length of the path in beats.
796 * @constructor
797 */
798 Path = function(descriptors, pathLength) {
799 this.descriptors = descriptors;
800 this.pathLength = pathLength;
801 }
802
803 /**
804 * Create a Path representing a ball, filling in the gaps between the throws
805 * with carry descriptors. Since it's a ball's path, there are no
806 * EmptyHandDescriptors in the output.
807 * @param {!Array.ThrowDescriptor} throwDescriptors the ball's part of the
808 * pattern.
809 * @param {!number} pathLength the length of the pattern in beats.
810 * @return {!Path} the ball's full path.
811 */
812 Path.ballPathFromThrowDescriptors = function(throwDescriptors, pathLength) {
813 return new Path(
814 Path.createDescriptorList(throwDescriptors, pathLength), pathLength);
815 };
816
817 /**
818 * Create the sequence of ThrowDescriptors, CarryDescriptors, and
819 * CarryOneDescriptor describing the path of a ball through a pattern.
820 * A sequence such as (h j k) generally maps to an alternating series of throw
821 * and carry descriptors [Th Chj Tj Cjk Tk Ck? ...]. However, when j is a 1,
822 * you remove the throw descriptor and modify the previous and subsequent carry
823 * descriptors, since the throw descriptor has zero duration and the carry
824 * descriptors need to take into account the handoff.
825 * @param {!Array.ThrowDescriptor} throwDescriptors the ball's part of the
826 * pattern.
827 * @param {!number} pathLength the length of the pattern in beats.
828 * @return {!Array.Descriptor} the full set of descriptors for the ball.
829 */
830 Path.createDescriptorList = function(throwDescriptors, pathLength) {
831 var descriptors = [];
832 var prevThrow;
833 for (var index in throwDescriptors) {
834 var td = throwDescriptors[index];
835 if (prevThrow) {
836 descriptors.push(
837 CarryDescriptor.fromThrowDescriptors(prevThrow, td, pathLength));
838 } // Else it's handled after the loop.
839 descriptors.push(td);
840 prevThrow = td;
841 }
842 descriptors.push(
843 CarryDescriptor.fromThrowDescriptors(prevThrow, throwDescriptors[0],
844 pathLength));
845 // Now post-process to take care of throws of 1. It's easier to do it here
846 // than during construction since we can now assume that the previous and
847 // subsequent carry descriptors are already in place [modulo pathLength].
848 for (var i = 0; i < descriptors.length; ++i) {
849 var descriptor = descriptors[i];
850 if (descriptor instanceof ThrowDescriptor) {
851 if (descriptor.throwNum == 1) {
852 var prevIndex = (i + descriptors.length - 1) % descriptors.length;
853 var postIndex = (i + 1) % descriptors.length;
854 var replacements = CarryOneDescriptor.getDescriptorPair(
855 descriptors[prevIndex], descriptors[postIndex]);
856 descriptors[prevIndex] = replacements[0];
857 descriptors[postIndex] = replacements[1];
858 descriptors.splice(i, 1);
859 // We've removed a descriptor from the array, but since we can never
860 // have 2 ThrowDescriptors in a row, we don't need to decrement i.
861 }
862 }
863 }
864 return descriptors;
865 };
866
867 /**
868 * Convert the Path to a string for debugging.
869 * @return {String} debugging output.
870 */
871 Path.prototype.toString = function() {
872 var ret = 'pathLength is ' + this.pathLength + '; [';
873 for (var index in this.descriptors) {
874 ret += this.descriptors[index].toString();
875 }
876 ret += ']';
877 return ret;
878 };
879
880 /**
881 * Create an otherwise-identical copy of this path at a given time offset.
882 * Note that offset may put time references in the Path past the length of the
883 * pattern. The caller must fix this up manually.
884 * @param {number} offset how many beats to offset the new Path.
885 * @return {!Path} the new copy.
886 */
887 Path.prototype.clone = function(offset) {
888 offset = offset || 0; // Turn null into 0.
889 var descriptors = [];
890 for (var index in this.descriptors) {
891 descriptors.push(this.descriptors[index].clone(offset));
892 }
893 return new Path(descriptors, this.pathLength);
894 };
895
896 /**
897 * Adjust the start time of all descriptors to be in [0, pathLength) via modular
898 * arithmetic. Reorder the array such that they're sorted in increasing order
899 * of time.
900 * @return {!Path} this.
901 */
902 Path.prototype.fixUpModPathLength = function() {
903 var splitIndex;
904 var prevTime = 0;
905 for (var index in this.descriptors) {
906 var d = this.descriptors[index];
907 d.fixUpModPathLength(this.pathLength);
908 if (d.time < prevTime) {
909 assert(null == splitIndex);
910 splitIndex = index; // From here to the end should move to the start.
911 }
912 prevTime = d.time;
913 }
914 if (null != splitIndex) {
915 var temp = this.descriptors.slice(splitIndex);
916 this.descriptors.length = splitIndex;
917 this.descriptors = temp.concat(this.descriptors);
918 }
919 return this;
920 };
921
922 /**
923 * Take a standard asynch siteswap pattern [expressed as an array of ints] and
924 * a number of hands, and expand it into a 2D grid of ThrowDescriptors with one
925 * row per hand.
926 * Non-asynch patterns are more complicated, since their linear forms aren't
927 * fully-specified, so we don't handle them here.
928 * You'll want to expand your pattern to the LCM of numHands and minimal pattern
929 * length before calling this.
930 * The basic approach doesn't really work for one-handed patterns. It ends up
931 * with catches and throws happening at the same time [having removed all
932 * empty-hand time in between them]. To fix this, we double all throw heights
933 * and space them out, as if doing a two-handed pattern with all zeroes from the
934 * other hand. Yes, this points out that the overall approach we're taking is a
935 * bit odd [since you end up with hands empty for time proportional to the
936 * number of hands], but you have to make some sort of assumptions to generalize
937 * siteswaps to N hands, and that's what I chose.
938 * @param {!Array.number} pattern an asynch siteswap pattern.
939 * @param {!number} numHands the number of hands.
940 * @return {!Array.Array.ThrowDescriptor} the expanded pattern.
941 */
942 function expandPattern(pattern, numHands) {
943 var fullPattern = [];
944 assert(numHands > 0);
945 if (numHands == 1) {
946 numHands = 2;
947 var temp = [];
948 for (var i = 0; i < pattern.length; ++i) {
949 temp[2 * i] = 2 * pattern[i];
950 temp[2 * i + 1] = 0;
951 }
952 pattern = temp;
953 }
954 for (var hand = 0; hand < numHands; ++hand) {
955 fullPattern[hand] = [];
956 }
957 for (var time = 0; time < pattern.length; ++time) {
958 for (var hand = 0; hand < numHands; ++hand) {
959 var t;
960 if (hand == time % numHands) {
961 t = new ThrowDescriptor(pattern[time], time, hand,
962 (hand + pattern[time]) % numHands);
963 } else {
964 // These are ignored during analysis, so they don't appear in BallPaths.
965 t = new ThrowDescriptor(0, time, hand, hand);
966 }
967 fullPattern[hand].push(t);
968 }
969 }
970 return fullPattern;
971 }
972
973 // TODO: Wrap the final pattern in a class, then make the remaining few global
974 // functions be members of that class to clean up the global namespace.
975
976 /**
977 * Given a valid site-swap for a nonzero number of balls, stored as an expanded
978 * pattern array-of-arrays, with pattern length the LCM of hands and minimal
979 * pattern length, produce Paths for all the balls.
980 * @param {!Array.Array.ThrowDescriptor} pattern a valid pattern.
981 * @return {!Array.Path} the paths of all the balls.
982 */
983 function generateBallPaths(pattern) {
984 var numHands = pattern.length;
985 assert(numHands > 0);
986 var patternLength = pattern[0].length;
987 assert(patternLength > 0);
988 var sum = 0;
989 for (var hand in pattern) {
990 for (var time in pattern[hand]) {
991 sum += pattern[hand][time].throwNum;
992 }
993 }
994 var numBalls = sum / patternLength;
995 assert(numBalls == Math.round(numBalls));
996 assert(numBalls > 0);
997
998 var ballsToAllocate = numBalls;
999 var ballPaths = [];
1000 // NOTE: The indices of locationsChecked are reversed from those of pattern
1001 // for simplicity of allocation. This might be worth flipping to match.
1002 var locationsChecked = [];
1003 for (var time = 0; time < patternLength && ballsToAllocate; ++time) {
1004 locationsChecked[time] = locationsChecked[time] || [];
1005 for (var hand = 0; hand < numHands && ballsToAllocate; ++hand) {
1006 if (locationsChecked[time][hand]) {
1007 continue;
1008 }
1009 var curThrowDesc = pattern[hand][time];
1010 var curThrow = curThrowDesc.throwNum;
1011 if (!curThrow) {
1012 assert(curThrow === 0);
1013 continue;
1014 }
1015 var throwDescriptors = [];
1016 var curTime = time;
1017 var curHand = hand;
1018 var wraps = 0;
1019 do {
1020 if (!locationsChecked[curTime]) {
1021 locationsChecked[curTime] = [];
1022 }
1023 assert(!locationsChecked[curTime][curHand]);
1024 locationsChecked[curTime][curHand] = true;
1025 // We copy curThrowDesc here, adding wraps * patternLength, to get
1026 // the true throw time relative to offset. Later we'll add in offset
1027 // when we clone again, then mod by pathLength.
1028 throwDescriptors.push(curThrowDesc.clone(wraps * patternLength));
1029 var nextThrowTime = curThrow + curTime;
1030 wraps += Math.floor(nextThrowTime / patternLength);
1031 curTime = nextThrowTime % patternLength;
1032 assert(curTime >= time); // Else we'd have covered it earlier.
1033 curHand = curThrowDesc.destHand;
1034 var tempThrowDesc = curThrowDesc;
1035 curThrowDesc = pattern[curHand][curTime];
1036 curThrow = curThrowDesc.throwNum;
1037 assert(tempThrowDesc.destHand == curThrowDesc.sourceHand);
1038 assert(curThrowDesc.time ==
1039 (tempThrowDesc.throwNum + tempThrowDesc.time) % patternLength);
1040 } while (curTime != time || curHand != hand);
1041 var pathLength = wraps * patternLength;
1042 var ballPath =
1043 Path.ballPathFromThrowDescriptors(throwDescriptors, pathLength);
1044 for (var i = 0; i < wraps; ++i) {
1045 var offset = i * patternLength % pathLength;
1046 ballPaths.push(ballPath.clone(offset, pathLength).fixUpModPathLength());
1047 }
1048 ballsToAllocate -= wraps;
1049 assert(ballsToAllocate >= 0);
1050 }
1051 }
1052 return ballPaths;
1053 }
1054
1055 /**
1056 * Given an array of ball paths, produce the corresponding set of hand paths.
1057 * @param {!Array.Path} ballPaths the Paths of all the balls in the pattern.
1058 * @param {!number} numHands how many hands to use in the pattern.
1059 * @param {!number} patternLength the length, in beats, of the pattern.
1060 * @return {!Array.Path} the paths of all the hands.
1061 */
1062 function generateHandPaths(ballPaths, numHands, patternLength) {
1063 assert(numHands > 0);
1064 assert(patternLength > 0);
1065 var handRecords = []; // One record per hand.
1066 for (var idxBR in ballPaths) {
1067 var descriptors = ballPaths[idxBR].descriptors;
1068 for (var idxD in descriptors) {
1069 var descriptor = descriptors[idxD];
1070 // TODO: Fix likely needed for throws of 1.
1071 if (!(descriptor instanceof ThrowDescriptor)) {
1072 // It's a CarryDescriptor or a CarryOneDescriptor.
1073 var hand = descriptor.hand;
1074 if (!handRecords[hand]) {
1075 handRecords[hand] = [];
1076 }
1077 // TODO: Should we not shorten stuff here if we're going to lengthen
1078 // everything later anyway? Is there a risk of inconsistency due to
1079 // ball paths of different lengths?
1080 var catchTime = descriptor.time % patternLength;
1081 if (!handRecords[hand][catchTime]) {
1082 // We pass in this offset to set the new descriptor's time to
1083 // catchTime, so as to keep it within [0, patternLength).
1084 handRecords[hand][catchTime] =
1085 descriptor.clone(catchTime - descriptor.time);
1086 } else {
1087 assert(
1088 handRecords[hand][catchTime].equalsWithMod(
1089 descriptor, patternLength));
1090 }
1091 }
1092 }
1093 }
1094 var handPaths = [];
1095 for (var hand in handRecords) {
1096 var outDescriptors = [];
1097 var inDescriptors = handRecords[hand];
1098 var prevDescriptor = null;
1099 var descriptor;
1100 for (var idxD in inDescriptors) {
1101 descriptor = inDescriptors[idxD];
1102 assert(descriptor); // Enumeration should skip array holes.
1103 assert(descriptor.hand == hand);
1104 if (prevDescriptor) {
1105 outDescriptors.push(new EmptyHandDescriptor(prevDescriptor, descriptor,
1106 patternLength));
1107 }
1108 outDescriptors.push(descriptor.clone());
1109 prevDescriptor = descriptor;
1110 }
1111 // Note that this EmptyHandDescriptor that wraps around the end lives at the
1112 // end of the array, not the beginning, despite the fact that it may be the
1113 // active one at time zero. This is the same behavior as with Paths for
1114 // balls.
1115 descriptor = new EmptyHandDescriptor(prevDescriptor, outDescriptors[0],
1116 patternLength);
1117 if (descriptor.time < outDescriptors[0].time) {
1118 assert(descriptor.time + descriptor.duration == outDescriptors[0].time);
1119 outDescriptors.unshift(descriptor);
1120 } else {
1121 assert(descriptor.time ==
1122 outDescriptors[outDescriptors.length - 1].time + 1);
1123 outDescriptors.push(descriptor);
1124 }
1125 handPaths[hand] =
1126 new Path(outDescriptors, patternLength).fixUpModPathLength();
1127 }
1128 return handPaths;
1129 }
1130
1131 // NOTE: All this Vector stuff does lots of object allocations. If that's a
1132 // problem for your browser [e.g. IE6], you'd better stick with the embedded V8.
1133 // This code predates the creation of o3djs/math.js; I should probably switch it
1134 // over at some point, but for now it's not worth the trouble.
1135
1136 /**
1137 * A simple 3-dimensional vector.
1138 * @constructor
1139 */
1140 Vector = function(x, y, z) {
1141 this.x = x;
1142 this.y = y;
1143 this.z = z;
1144 }
1145
1146 Vector.prototype.sub = function(v) {
1147 return new Vector(this.x - v.x, this.y - v.y, this.z - v.z);
1148 };
1149
1150 Vector.prototype.add = function(v) {
1151 return new Vector(this.x + v.x, this.y + v.y, this.z + v.z);
1152 };
1153
1154 Vector.prototype.dot = function(v) {
1155 return this.x * v.x + this.y * v.y + this.z * v.z;
1156 };
1157
1158 Vector.prototype.length = function() {
1159 return Math.sqrt(this.dot(this));
1160 };
1161
1162 Vector.prototype.scale = function(s) {
1163 return new Vector(this.x * s, this.y * s, this.z * s);
1164 };
1165
1166 Vector.prototype.set = function(v) {
1167 this.x = v.x;
1168 this.y = v.y;
1169 this.z = v.z;
1170 };
1171
1172 Vector.prototype.normalize = function() {
1173 var length = this.length();
1174 assert(length);
1175 this.set(this.scale(1 / length));
1176 return this;
1177 };
1178
1179 /**
1180 * Convert the Vector to a string for debugging.
1181 * @return {String} debugging output.
1182 */
1183 Vector.prototype.toString = function() {
1184 return '{' + this.x.toFixed(3) + ', ' + this.y.toFixed(3) + ', ' +
1185 this.z.toFixed(3) + '}';
1186 };
1187
1188 /**
1189 * A container class that holds the positions relevant to a hand: where it is
1190 * when it's not doing anything, where it likes to catch balls, and where it
1191 * likes to throw balls to each of the other hands.
1192 * @param {!Vector} basePosition the centroid of throw and catch positions when
1193 * the hand throws to itself.
1194 * @param {!Vector} catchPosition where the hand likes to catch balls.
1195 * @constructor
1196 */
1197 HandPositionRecord = function(basePosition, catchPosition) {
1198 this.basePosition = basePosition;
1199 this.catchPosition = catchPosition;
1200 this.throwPositions = [];
1201 }
1202
1203 /**
1204 * Convert the HandPositionRecord to a string for debugging.
1205 * @return {String} debugging output.
1206 */
1207 HandPositionRecord.prototype.toString = function() {
1208 var s = 'base: ' + this.basePosition.toString() + ';\n';
1209 s += 'catch: ' + this.catchPosition.toString() + ';\n';
1210 s += 'throws:\n';
1211 for (var i = 0; i < this.throwPositions.length; ++i) {
1212 s += '[' + i + '] ' + this.throwPositions[i].toString() + '\n';
1213 }
1214 return s;
1215 };
1216
1217 /**
1218 * Compute all the hand positions used in a pattern given a number of hands and
1219 * a grouping style ["even" for evenly-spaced hands, "pairs" to group them in
1220 * pairs, as with 2-handed jugglers].
1221 * @param {!number} numHands the number of hands to use.
1222 * @param {!String} style the grouping style.
1223 * @return {!Array.HandPositionRecord} a full set of hand positions.
1224 */
1225 function computeHandPositions(numHands, style) {
1226 assert(numHands > 0);
1227 var majorRadiusScale = 0.75;
1228 var majorRadius = majorRadiusScale * (numHands - 1);
1229 var throwCatchOffset = 0.45;
1230 var catchRadius = majorRadius + throwCatchOffset;
1231 var handPositionRecords = [];
1232 for (var hand = 0; hand < numHands; ++hand) {
1233 var circleFraction;
1234 if (style == 'even') {
1235 circleFraction = hand / numHands;
1236 } else {
1237 assert(style == 'pairs');
1238 circleFraction = (hand + Math.floor(hand / 2)) / (1.5 * numHands);
1239 }
1240 var cos = Math.cos(Math.PI * 2 * circleFraction);
1241 var sin = Math.sin(Math.PI * 2 * circleFraction);
1242 var cX = catchRadius * cos;
1243 var cY = 0;
1244 var cZ = catchRadius * sin;
1245 var bX = majorRadius * cos;
1246 var bY = 0;
1247 var bZ = majorRadius * sin;
1248 handPositionRecords[hand] = new HandPositionRecord(
1249 new Vector(bX, bY, bZ), new Vector(cX, cY, cZ));
1250 }
1251 // Now that we've got all the hands' base and catch positions, we need to
1252 // compute the appropriate throw positions for each hand pair.
1253 for (var source = 0; source < numHands; ++source) {
1254 var throwHand = handPositionRecords[source];
1255 for (var target = 0; target < numHands; ++target) {
1256 var catchHand = handPositionRecords[target];
1257 if (throwHand == catchHand) {
1258 var baseV = throwHand.basePosition;
1259 throwHand.throwPositions[target] =
1260 baseV.add(baseV.sub(throwHand.catchPosition));
1261 } else {
1262 var directionV =
1263 catchHand.catchPosition.sub(throwHand.basePosition).normalize();
1264 var offsetV = directionV.scale(throwCatchOffset);
1265 throwHand.throwPositions[target] =
1266 throwHand.basePosition.add(offsetV);
1267 }
1268 }
1269 }
1270 return handPositionRecords;
1271 }
1272
1273 /**
1274 * Convert an array of HandPositionRecord to a string for debugging.
1275 * @param {!Array.HandPositionRecord} positions the positions to display.
1276 * @return {String} debugging output.
1277 */
1278 function getStringFromHandPositions(positions) {
1279 var s = '';
1280 for (index in positions) {
1281 s += positions[index].toString();
1282 }
1283 return s;
1284 }
1285
1286 /**
1287 * The set of curves an object passes through throughout a full animation cycle.
1288 * @param {!number} duration the length of the animation in beats.
1289 * @param {!Array.Curve} curves the full set of Curves.
1290 * @constructor
1291 */
1292 CurveSet = function(duration, curves) {
1293 this.duration = duration;
1294 this.curves = curves;
1295 }
1296
1297 /**
1298 * Looks up what curve is active at a particular time. This is slower than
1299 * getCurveForTime, but can be used even if no Curve starts precisely at
1300 * unsafeTime % this.duration.
1301 * @param {!number} unsafeTime the time at which to check.
1302 * @return {!Curve} the curve active at unsafeTime.
1303 */
1304 CurveSet.prototype.getCurveForUnsafeTime = function(unsafeTime) {
1305 unsafeTime %= this.duration;
1306 time = Math.floor(unsafeTime);
1307 if (this.curves[time]) {
1308 return this.curves[time];
1309 }
1310 var curve;
1311 for (var i = time; i >= 0; --i) {
1312 curve = this.curves[i];
1313 if (curve) {
1314 assert(i + curve.duration >= unsafeTime);
1315 return curve;
1316 }
1317 }
1318 // We must want the last one. There's always a last one, given how we
1319 // construct the CurveSets; they're sparse, but the length gets set by adding
1320 // elements at the end.
1321 curve = this.curves[this.curves.length - 1];
1322 unsafeTime += this.duration;
1323 assert(curve.startTime <= unsafeTime);
1324 assert(curve.startTime + curve.duration > unsafeTime);
1325 return curve;
1326 };
1327
1328 /**
1329 * Looks up what curve is active at a particular time. This is faster than
1330 * getCurveForUnsafeTime, but can only be used if if a Curve starts precisely at
1331 * unsafeTime % this.duration.
1332 * @param {!number} time the time at which to check.
1333 * @return {!Curve} the curve starting at time.
1334 */
1335 CurveSet.prototype.getCurveForTime = function(time) {
1336 return this.curves[time % this.duration];
1337 };
1338
1339 /**
1340 * Convert the CurveSet to a string for debugging.
1341 * @return {String} debugging output.
1342 */
1343 CurveSet.prototype.toString = function() {
1344 var s = 'Duration: ' + this.duration + '\n';
1345 for (var c in this.curves) {
1346 s += this.curves[c].toString();
1347 }
1348 return s;
1349 };
1350
1351 /**
1352 * Namespace object to hold the pure math functions.
1353 * TODO: Consider just rolling these into the Pattern object, when it gets
1354 * created.
1355 */
1356 var JugglingMath = {};
1357
1358 /**
1359 * Computes the greatest common devisor of integers a and b.
1360 * @param {!number} a an integer.
1361 * @param {!number} b an integer.
1362 * @return {!number} the GCD of a and b.
1363 */
1364 JugglingMath.computeGCD = function(a, b) {
1365 assert(Math.round(a) == a);
1366 assert(Math.round(b) == b);
1367 assert(a >= 0);
1368 assert(b >= 0);
1369 if (!b) {
1370 return a;
1371 } else {
1372 return JugglingMath.computeGCD(b, a % b);
1373 }
1374 }
1375
1376 /**
1377 * Computes the least common multiple of integers a and b, by making use of the
1378 * fact that LCM(a, b) * GCD(a, b) == a * b.
1379 * @param {!number} a an integer.
1380 * @param {!number} b an integer.
1381 * @return {!number} the LCM of a and b.
1382 */
1383 JugglingMath.computeLCM = function(a, b) {
1384 assert(Math.round(a) == a);
1385 assert(Math.round(b) == b);
1386 assert(a >= 0);
1387 assert(b >= 0);
1388 var ret = a * b / JugglingMath.computeGCD(a, b);
1389 assert(Math.round(ret) == ret);
1390 return ret;
1391 }
1392
1393 /**
1394 * Given a Path and a set of hand positions, compute the corresponding set of
1395 * Curves.
1396 * @param {!Path} path the path of an object.
1397 * @param {!Array.HandPositionRecord} handPositions the positions of the hands
1398 * juggling the pattern containing the path.
1399 * @return {!CurveSet} the full set of curves.
1400 */
1401 CurveSet.getCurveSetFromPath = function(path, handPositions) {
1402 var curves = [];
1403 var pathLength = path.pathLength;
1404 for (var index in path.descriptors) {
1405 var descriptor = path.descriptors[index];
1406 var curve = descriptor.generateCurve(handPositions);
1407 assert(!curves[curve.startTime]);
1408 assert(curve.startTime < pathLength);
1409 curves[curve.startTime] = curve;
1410 }
1411 return new CurveSet(pathLength, curves);
1412 }
1413
1414 /**
1415 * Given a set of Paths and a set of hand positions, compute the corresponding
1416 * CurveSets.
1417 * @param {!Array.Path} paths the paths of a number of objects.
1418 * @param {!Array.HandPositionRecord} handPositions the positions of the hands
1419 * juggling the pattern containing the paths.
1420 * @return {!Array.CurveSet} the CurveSets.
1421 */
1422 CurveSet.getCurveSetsFromPaths = function(paths, handPositions) {
1423 var curveSets = [];
1424 for (var index in paths) {
1425 var path = paths[index];
1426 curveSets[index] = CurveSet.getCurveSetFromPath(path, handPositions);
1427 }
1428 return curveSets;
1429 }
1430
1431 /**
1432 * This is a temporary top-level calculation function that converts a standard
1433 * asynchronous siteswap, expressed as a string of digits, into a full
1434 * ready-to-animate set of CurveSets. Later on we'll be using an interface that
1435 * can create a richer set of patterns than those expressable in the traditional
1436 * string-of-ints format.
1437 * @param {!String} patternString the siteswap.
1438 * @param {!number} numHands the number of hands to use for the pattern.
1439 * @param {!String} style how to space the hands ["pairs" or "even"].
1440 * @return {!Object} a fully-analyzed pattern as CurveSets and associated data.
1441 */
1442 function computeFullPatternFromString(patternString, numHands, style) {
1443 var patternAsStrings = patternString.split(/[ ,]+ */);
1444 var patternSegment = [];
1445 for (var index in patternAsStrings) {
1446 if (patternAsStrings[index]) { // Beware extra whitespace at the ends.
1447 patternSegment.push(parseInt(patternAsStrings[index]));
1448 }
1449 }
1450 var pattern = [];
1451 // Now expand the pattern out to the length of the LCM of pattern length and
1452 // number of hands, so that each throw gets done in each of its incarnations.
1453 var multiple = JugglingMath.computeLCM(patternSegment.length, numHands) /
1454 patternSegment.length;
1455 for (var i = 0; i < multiple; ++i) {
1456 pattern = pattern.concat(patternSegment);
1457 }
1458
1459 var fullPattern = expandPattern(pattern, numHands);
1460 var patternLength = fullPattern[0].length;
1461
1462 var ballPaths = generateBallPaths(fullPattern);
1463 var handPaths = generateHandPaths(ballPaths, numHands, patternLength);
1464
1465 var handPositions = computeHandPositions(numHands, style);
1466 var ballCurveSets = CurveSet.getCurveSetsFromPaths(ballPaths, handPositions);
1467 var handCurveSets = CurveSet.getCurveSetsFromPaths(handPaths, handPositions);
1468
1469 // Find the LCM of all the curveSet durations. This will be the length of the
1470 // fully-expanded queue. We could expand to this before computing the
1471 // CurveSets, but this way's probably just a little cheaper.
1472 var lcmDuration = 1;
1473 for (var i in ballCurveSets) {
1474 var duration = ballCurveSets[i].duration;
1475 if (duration > lcmDuration || lcmDuration % duration) {
1476 lcmDuration = JugglingMath.computeLCM(lcmDuration, duration);
1477 }
1478 }
1479 for (var i in handCurveSets) {
1480 var duration = handCurveSets[i].duration;
1481 if (duration > lcmDuration || lcmDuration % duration) {
1482 lcmDuration = JugglingMath.computeLCM(lcmDuration, duration);
1483 }
1484 }
1485 return {
1486 numBalls: ballPaths.length,
1487 numHands: handPaths.length,
1488 duration: lcmDuration,
1489 handCurveSets: handCurveSets,
1490 ballCurveSets: ballCurveSets
1491 }
1492 }
OLDNEW
« no previous file with comments | « samples/siteswap/animation.js ('k') | samples/siteswap/siteswap.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698