OLD | NEW |
(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 } |
OLD | NEW |