text: //
  //  walkApi.js
  //  version 1.3
  //
  //  Created by David Wooldridge, June 2015
  //  Copyright © 2014 - 2015 High Fidelity, Inc.
  //
  //  Exposes API for use by walk.js version 1.2+.
  //
  //  Editing tools for animation data files available here: https://github.com/DaveDubUK/walkTools
  //
  //  Distributed under the Apache License, Version 2.0.
  //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
  //
  
  // included here to ensure walkApi.js can be used as an API, separate from walk.js
  Script.include("walkConstants.js");
  
  Avatar = function() {
      // if Hydras are connected, the only way to enable use is to never set any arm joint rotation
      this.hydraCheck = function () {
          return Controller.Hardware.Hydra !== undefined;
      }
      // settings
      this.headFree = true;
      this.armsFree = this.hydraCheck(); // automatically sets true to enable Hydra support - temporary fix
      this.makesFootStepSounds = false;
      this.blenderPreRotations = false; // temporary fix
      this.animationSet = undefined; // currently just one animation set
      this.setAnimationSet = function(animationSet) {
          this.animationSet = animationSet;
          switch (animationSet) {
              case 'standardMale':
                  this.selectedIdle = walkAssets.getAnimationDataFile("MaleIdle");
                  this.selectedWalk = walkAssets.getAnimationDataFile("MaleWalk");
                  this.selectedWalkBackwards = walkAssets.getAnimationDataFile("MaleWalkBackwards");
                  this.selectedSideStepLeft = walkAssets.getAnimationDataFile("MaleSideStepLeft");
                  this.selectedSideStepRight = walkAssets.getAnimationDataFile("MaleSideStepRight");
                  this.selectedWalkBlend = walkAssets.getAnimationDataFile("WalkBlend");
                  this.selectedHover = walkAssets.getAnimationDataFile("MaleHover");
                  this.selectedFly = walkAssets.getAnimationDataFile("MaleFly");
                  this.selectedFlyBackwards = walkAssets.getAnimationDataFile("MaleFlyBackwards");
                  this.selectedFlyDown = walkAssets.getAnimationDataFile("MaleFlyDown");
                  this.selectedFlyUp = walkAssets.getAnimationDataFile("MaleFlyUp");
                  this.selectedFlyBlend = walkAssets.getAnimationDataFile("FlyBlend");
                  this.currentAnimation = this.selectedIdle;
                  return;
          }
      }
      this.setAnimationSet('standardMale');
  
      // calibration
      this.calibration = {
          hipsToFeet: 1,
          strideLength: this.selectedWalk.calibration.strideLength
      }
      this.distanceFromSurface = 0;
      this.calibrate = function() {
          // Triple check: measurements are taken three times to ensure accuracy - the first result is often too large
          const MAX_ATTEMPTS = 3;
          var attempts = MAX_ATTEMPTS;
          var extraAttempts = 0;
          do {
              for (joint in walkAssets.animationReference.joints) {
                  var IKChain = walkAssets.animationReference.joints[joint].IKChain;
  
                  // only need to zero right leg IK chain and hips
                  if (IKChain === "RightLeg" || joint === "Hips" ) {
                      MyAvatar.setJointRotation(joint, Quat.fromPitchYawRollDegrees(0, 0, 0));
                  }
              }
              this.calibration.hipsToFeet = MyAvatar.getJointPosition("Hips").y - MyAvatar.getJointPosition("RightToeBase").y;
  
              // maybe measuring before Blender pre-rotations have been applied?
              if (this.calibration.hipsToFeet < 0 && this.blenderPreRotations) {
                  this.calibration.hipsToFeet *= -1;
              }
  
              if (this.calibration.hipsToFeet === 0 && extraAttempts < 100) {
                  attempts++;
                  extraAttempts++;// Interface can sometimes report zero for hips to feet. if so, we try again.
              }
          } while (attempts-- > 1)
  
          // just in case
          if (this.calibration.hipsToFeet <= 0 || isNaN(this.calibration.hipsToFeet)) {
              this.calibration.hipsToFeet = 1;
              print('walk.js error: Unable to get a non-zero measurement for the avatar hips to feet measure. Hips to feet set to default value ('+
                    this.calibration.hipsToFeet.toFixed(3)+'m). This will cause some foot sliding. If your avatar has only just appeared, it is recommended that you re-load the walk script.');
          } else {
              print('walk.js info: Hips to feet calibrated to '+this.calibration.hipsToFeet.toFixed(3)+'m');
          }
      }
  
      // pose the fingers
      this.poseFingers = function() {
          for (knuckle in walkAssets.animationReference.leftHand) {
              if (walkAssets.animationReference.leftHand[knuckle].IKChain === "LeftHandThumb") {
                  MyAvatar.setJointRotation(knuckle, Quat.fromPitchYawRollDegrees(0, 0, -4));
              } else {
                  MyAvatar.setJointRotation(knuckle, Quat.fromPitchYawRollDegrees(16, 0, 5));
              }
          }
          for (knuckle in walkAssets.animationReference.rightHand) {
              if (walkAssets.animationReference.rightHand[knuckle].IKChain === "RightHandThumb") {
                  MyAvatar.setJointRotation(knuckle, Quat.fromPitchYawRollDegrees(0, 0, 4));
              } else {
                  MyAvatar.setJointRotation(knuckle, Quat.fromPitchYawRollDegrees(16, 0, -5));
              }
          }
      };
      this.calibrate();
      this.poseFingers();
  
      // footsteps
      this.nextStep = RIGHT; // the first step is right, because the waveforms say so
      this.leftAudioInjector = null;
      this.rightAudioInjector = null;
      this.makeFootStepSound = function() {
          // correlate footstep volume with avatar speed. place the audio source at the feet, not the hips
          const SPEED_THRESHOLD = 0.4;
          const VOLUME_ATTENUATION = 0.8;
          const MIN_VOLUME = 0.5;
          var volume = Vec3.length(motion.velocity) > SPEED_THRESHOLD ?
                       VOLUME_ATTENUATION * Vec3.length(motion.velocity) / MAX_WALK_SPEED : MIN_VOLUME;
          volume = volume > 1 ? 1 : volume; // occurs when landing at speed - can walk faster than max walking speed
          var options = {
              position: Vec3.sum(MyAvatar.position, {x:0, y: -this.calibration.hipsToFeet, z:0}),
              volume: volume
          };
          if (this.nextStep === RIGHT) {
              if (this.rightAudioInjector === null) {
                  this.rightAudioInjector = Audio.playSound(walkAssets.footsteps[0], options);
              } else {
                  this.rightAudioInjector.setOptions(options);
                  this.rightAudioInjector.restart();
              }
              this.nextStep = LEFT;
          } else if (this.nextStep === LEFT) {
              if (this.leftAudioInjector === null) {
                  this.leftAudioInjector = Audio.playSound(walkAssets.footsteps[1], options);
              } else {
                  this.leftAudioInjector.setOptions(options);
                  this.leftAudioInjector.restart();
              }
              this.nextStep = RIGHT;
          }
      }
  };
  
  // constructor for the Motion object
  Motion = function() {
      this.isLive = true;
      // locomotion status
      this.state = STATIC;
      this.nextState = STATIC;
      this.isMoving = false;
      this.isWalkingSpeed = false;
      this.isFlyingSpeed = false;
      this.isAccelerating = false;
      this.isDecelerating = false;
      this.isDeceleratingFast = false;
      this.isComingToHalt = false;
      this.directedAcceleration = 0;
  
      // used to make sure at least one step has been taken when transitioning from a walk cycle
      this.elapsedFTDegrees = 0;
  
      // the current transition (any layered transitions are nested within this transition)
      this.currentTransition = null;
  
      // orientation, locomotion and timing
      this.velocity = {x:0, y:0, z:0};
      this.acceleration = {x:0, y:0, z:0};
      this.yaw = Quat.safeEulerAngles(MyAvatar.orientation).y;
      this.yawDelta = 0;
      this.yawDeltaAcceleration = 0;
      this.direction = FORWARDS;
      this.deltaTime = 0;
  
      // historical orientation, locomotion and timing
      this.lastDirection = FORWARDS;
      this.lastVelocity = {x:0, y:0, z:0};
      this.lastYaw = Quat.safeEulerAngles(MyAvatar.orientation).y;
      this.lastYawDelta = 0;
      this.lastYawDeltaAcceleration = 0;
  
      // Quat.safeEulerAngles(MyAvatar.orientation).y tends to repeat values between frames, so values are filtered
      var YAW_SMOOTHING = 22;
      this.yawFilter = filter.createAveragingFilter(YAW_SMOOTHING);
      this.deltaTimeFilter = filter.createAveragingFilter(YAW_SMOOTHING);
      this.yawDeltaAccelerationFilter = filter.createAveragingFilter(YAW_SMOOTHING);
  
      // assess locomotion state
      this.assess = function(deltaTime) {
          // calculate avatar frame speed, velocity and acceleration
          this.deltaTime = deltaTime;
          this.velocity = Vec3.multiplyQbyV(Quat.inverse(MyAvatar.orientation), MyAvatar.getVelocity());
          var lateralVelocity = Math.sqrt(Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.z, 2));
  
          // MyAvatar.getAcceleration() currently not working. bug report submitted: https://worklist.net/20527
          var acceleration = {x:0, y:0, z:0};
          this.acceleration.x = (this.velocity.x - this.lastVelocity.x) / deltaTime;
          this.acceleration.y = (this.velocity.y - this.lastVelocity.y) / deltaTime;
          this.acceleration.z = (this.velocity.z - this.lastVelocity.z) / deltaTime;
  
          // MyAvatar.getAngularVelocity and MyAvatar.getAngularAcceleration currently not working. bug report submitted
          this.yaw = Quat.safeEulerAngles(MyAvatar.orientation).y;
          if (this.lastYaw < 0 && this.yaw > 0 || this.lastYaw > 0 && this.yaw < 0) {
              this.lastYaw *= -1;
          }
          var timeDelta = this.deltaTimeFilter.process(deltaTime);
          this.yawDelta = filter.degToRad(this.yawFilter.process(this.lastYaw - this.yaw)) / timeDelta;
          this.yawDeltaAcceleration = this.yawDeltaAccelerationFilter.process(this.lastYawDelta - this.yawDelta) / timeDelta;
  
          // how far above the surface is the avatar? (for testing / validation purposes)
          var pickRay = {origin: MyAvatar.position, direction: {x:0, y:-1, z:0}};
          var distanceFromSurface = Entities.findRayIntersectionBlocking(pickRay).distance;
          avatar.distanceFromSurface = distanceFromSurface - avatar.calibration.hipsToFeet;
  
          // determine principle direction of locomotion
          var FWD_BACK_BIAS = 100; // helps prevent false sidestep condition detection when banking hard
          if (Math.abs(this.velocity.x) > Math.abs(this.velocity.y) &&
              Math.abs(this.velocity.x) > FWD_BACK_BIAS * Math.abs(this.velocity.z)) {
              if (this.velocity.x < 0) {
                  this.directedAcceleration = -this.acceleration.x;
                  this.direction = LEFT;
              } else if (this.velocity.x > 0){
                  this.directedAcceleration = this.acceleration.x;
                  this.direction = RIGHT;
              }
          } else if (Math.abs(this.velocity.y) > Math.abs(this.velocity.x) &&
                     Math.abs(this.velocity.y) > Math.abs(this.velocity.z)) {
              if (this.velocity.y > 0) {
                  this.directedAcceleration = this.acceleration.y;
                  this.direction = UP;
              } else if (this.velocity.y < 0) {
                  this.directedAcceleration = -this.acceleration.y;
                  this.direction = DOWN;
              }
          } else if (FWD_BACK_BIAS * Math.abs(this.velocity.z) > Math.abs(this.velocity.x) &&
                     Math.abs(this.velocity.z) > Math.abs(this.velocity.y)) {
              if (this.velocity.z < 0) {
                  this.direction = FORWARDS;
                  this.directedAcceleration = -this.acceleration.z;
              } else if (this.velocity.z > 0) {
                  this.directedAcceleration = this.acceleration.z;
                  this.direction = BACKWARDS;
              }
          } else {
              this.direction = NONE;
              this.directedAcceleration = 0;
          }
  
          // set speed flags
          if (Vec3.length(this.velocity) < MOVE_THRESHOLD) {
              this.isMoving = false;
              this.isWalkingSpeed = false;
              this.isFlyingSpeed = false;
              this.isComingToHalt = false;
          } else if (Vec3.length(this.velocity) < MAX_WALK_SPEED) {
              this.isMoving = true;
              this.isWalkingSpeed = true;
              this.isFlyingSpeed = false;
          } else {
              this.isMoving = true;
              this.isWalkingSpeed = false;
              this.isFlyingSpeed = true;
          }
  
          // set acceleration flags
          if (this.directedAcceleration > ACCELERATION_THRESHOLD) {
              this.isAccelerating = true;
              this.isDecelerating = false;
              this.isDeceleratingFast = false;
              this.isComingToHalt = false;
          } else if (this.directedAcceleration < DECELERATION_THRESHOLD) {
              this.isAccelerating = false;
              this.isDecelerating = true;
              this.isDeceleratingFast = (this.directedAcceleration < FAST_DECELERATION_THRESHOLD);
          } else {
              this.isAccelerating = false;
              this.isDecelerating = false;
              this.isDeceleratingFast = false;
          }
  
          // use the gathered information to build up some spatial awareness
          var isOnSurface = (avatar.distanceFromSurface < ON_SURFACE_THRESHOLD);
          var isUnderGravity = (avatar.distanceFromSurface < GRAVITY_THRESHOLD);
          var isTakingOff = (isUnderGravity && this.velocity.y > OVERCOME_GRAVITY_SPEED);
          var isComingInToLand = (isUnderGravity && this.velocity.y < -OVERCOME_GRAVITY_SPEED);
          var aboutToLand = isComingInToLand && avatar.distanceFromSurface < LANDING_THRESHOLD;
          var surfaceMotion = isOnSurface && this.isMoving;
          var acceleratingAndAirborne = this.isAccelerating && !isOnSurface;
          var goingTooFastToWalk = !this.isDecelerating && this.isFlyingSpeed;
          var movingDirectlyUpOrDown = (this.direction === UP || this.direction === DOWN)
          var maybeBouncing = Math.abs(this.acceleration.y > BOUNCE_ACCELERATION_THRESHOLD) ? true : false;
  
          // we now have enough information to set the appropriate locomotion mode
          switch (this.state) {
              case STATIC:
                  var staticToAirMotion = this.isMoving && (acceleratingAndAirborne || goingTooFastToWalk ||
                                                             (movingDirectlyUpOrDown && !isOnSurface));
                  var staticToSurfaceMotion = surfaceMotion && !motion.isComingToHalt && !movingDirectlyUpOrDown &&
                                              !this.isDecelerating && lateralVelocity > MOVE_THRESHOLD;
  
                  if (staticToAirMotion) {
                      this.nextState = AIR_MOTION;
                  } else if (staticToSurfaceMotion) {
                      this.nextState = SURFACE_MOTION;
                  } else {
                      this.nextState = STATIC;
                  }
                  break;
  
              case SURFACE_MOTION:
                  var surfaceMotionToStatic = !this.isMoving ||
                                              (this.isDecelerating && motion.lastDirection !== DOWN && surfaceMotion &&
                                              !maybeBouncing && Vec3.length(this.velocity) < MAX_WALK_SPEED);
                  var surfaceMotionToAirMotion = (acceleratingAndAirborne || goingTooFastToWalk || movingDirectlyUpOrDown) &&
                                                 (!surfaceMotion && isTakingOff) ||
                                                 (!surfaceMotion && this.isMoving && !isComingInToLand);
  
                  if (surfaceMotionToStatic) {
                      // working on the assumption that stopping is now inevitable
                      if (!motion.isComingToHalt && isOnSurface) {
                          motion.isComingToHalt = true;
                      }
                      this.nextState = STATIC;
                  } else if (surfaceMotionToAirMotion) {
                      this.nextState = AIR_MOTION;
                  } else {
                      this.nextState = SURFACE_MOTION;
                  }
                  break;
  
              case AIR_MOTION:
                  var airMotionToSurfaceMotion = (surfaceMotion || aboutToLand) && !movingDirectlyUpOrDown;
                  var airMotionToStatic = !this.isMoving && this.direction === this.lastDirection;
  
                  if (airMotionToSurfaceMotion){
                      this.nextState = SURFACE_MOTION;
                  } else if (airMotionToStatic) {
                      this.nextState = STATIC;
                  } else {
                      this.nextState = AIR_MOTION;
                  }
                  break;
          }
      }
  
      // frequency time wheel (foot / ground speed matching)
      const DEFAULT_HIPS_TO_FEET = 1;
      this.frequencyTimeWheelPos = 0;
      this.frequencyTimeWheelRadius = DEFAULT_HIPS_TO_FEET / 2;
      this.recentFrequencyTimeIncrements = [];
      const FT_WHEEL_HISTORY_LENGTH = 8;
      for (var i = 0; i < FT_WHEEL_HISTORY_LENGTH; i++) {
          this.recentFrequencyTimeIncrements.push(0);
      }
      this.averageFrequencyTimeIncrement = 0;
  
      this.advanceFrequencyTimeWheel = function(angle){
          this.elapsedFTDegrees += angle;
          // keep a running average of increments for use in transitions (used during transitioning)
          this.recentFrequencyTimeIncrements.push(angle);
          this.recentFrequencyTimeIncrements.shift();
          for (increment in this.recentFrequencyTimeIncrements) {
              this.averageFrequencyTimeIncrement += this.recentFrequencyTimeIncrements[increment];
          }
          this.averageFrequencyTimeIncrement /= this.recentFrequencyTimeIncrements.length;
          this.frequencyTimeWheelPos += angle;
          const FULL_CIRCLE = 360;
          if (this.frequencyTimeWheelPos >= FULL_CIRCLE) {
              this.frequencyTimeWheelPos = this.frequencyTimeWheelPos % FULL_CIRCLE;
          }
      }
  
      this.saveHistory = function() {
          this.lastDirection = this.direction;
          this.lastVelocity = this.velocity;
          this.lastYaw = this.yaw;
          this.lastYawDelta = this.yawDelta;
          this.lastYawDeltaAcceleration = this.yawDeltaAcceleration;
      }
  };  // end Motion constructor
  
  // animation manipulation object
  animationOperations = (function() {
  
      return {
  
          // helper function for renderMotion(). calculate joint translations based on animation file settings and frequency * time
          calculateTranslations: function(animation, ft, direction) {
              var jointName = "Hips";
              var joint = animation.joints[jointName];
              var jointTranslations = {x:0, y:0, z:0};
  
              // gather modifiers and multipliers
              modifiers = new FrequencyMultipliers(joint, direction);
  
              // calculate translations. Use synthesis filters where specified by the animation data file.
  
              // sway (oscillation on the x-axis)
              if (animation.filters.hasOwnProperty(jointName) && 'swayFilter' in animation.filters[jointName]) {
                  jointTranslations.x = joint.sway * animation.filters[jointName].swayFilter.calculate
                      (filter.degToRad(modifiers.swayFrequencyMultiplier * ft + joint.swayPhase)) + joint.swayOffset;
              } else {
                  jointTranslations.x = joint.sway * Math.sin
                      (filter.degToRad(modifiers.swayFrequencyMultiplier * ft + joint.swayPhase)) + joint.swayOffset;
              }
              // bob (oscillation on the y-axis)
              if (animation.filters.hasOwnProperty(jointName) && 'bobFilter' in animation.filters[jointName]) {
                  jointTranslations.y = joint.bob * animation.filters[jointName].bobFilter.calculate
                      (filter.degToRad(modifiers.bobFrequencyMultiplier * ft + joint.bobPhase)) + joint.bobOffset;
              } else {
                  jointTranslations.y = joint.bob * Math.sin
                      (filter.degToRad(modifiers.bobFrequencyMultiplier * ft + joint.bobPhase)) + joint.bobOffset;
  
                  if (animation.filters.hasOwnProperty(jointName) && 'bobLPFilter' in animation.filters[jointName]) {
                      jointTranslations.y = filter.clipTrough(jointTranslations.y, joint, 2);
                      jointTranslations.y = animation.filters[jointName].bobLPFilter.process(jointTranslations.y);
                  }
              }
              // thrust (oscillation on the z-axis)
              if (animation.filters.hasOwnProperty(jointName) && 'thrustFilter' in animation.filters[jointName]) {
                  jointTranslations.z = joint.thrust * animation.filters[jointName].thrustFilter.calculate
                      (filter.degToRad(modifiers.thrustFrequencyMultiplier * ft + joint.thrustPhase)) + joint.thrustOffset;
              } else {
                  jointTranslations.z = joint.thrust * Math.sin
                      (filter.degToRad(modifiers.thrustFrequencyMultiplier * ft + joint.thrustPhase)) + joint.thrustOffset;
              }
              return jointTranslations;
          },
  
          // helper function for renderMotion(). calculate joint rotations based on animation file settings and frequency * time
          calculateRotations: function(jointName, animation, ft, direction) {
              var joint = animation.joints[jointName];
              var jointRotations = {x:0, y:0, z:0};
  
              if (avatar.blenderPreRotations) {
                  jointRotations = Vec3.sum(jointRotations, walkAssets.blenderPreRotations.joints[jointName]);
              }
  
              // gather frequency multipliers for this joint
              modifiers = new FrequencyMultipliers(joint, direction);
  
              // calculate rotations. Use synthesis filters where specified by the animation data file.
  
              // calculate pitch
              if (animation.filters.hasOwnProperty(jointName) &&
                 'pitchFilter' in animation.filters[jointName]) {
                  jointRotations.x += joint.pitch * animation.filters[jointName].pitchFilter.calculate
                      (filter.degToRad(ft * modifiers.pitchFrequencyMultiplier + joint.pitchPhase)) + joint.pitchOffset;
              } else {
                  jointRotations.x += joint.pitch * Math.sin
                      (filter.degToRad(ft * modifiers.pitchFrequencyMultiplier + joint.pitchPhase)) + joint.pitchOffset;
              }
              // calculate yaw
              if (animation.filters.hasOwnProperty(jointName) &&
                 'yawFilter' in animation.filters[jointName]) {
                  jointRotations.y += joint.yaw * animation.filters[jointName].yawFilter.calculate
                      (filter.degToRad(ft * modifiers.yawFrequencyMultiplier + joint.yawPhase)) + joint.yawOffset;
              } else {
                  jointRotations.y += joint.yaw * Math.sin
                      (filter.degToRad(ft * modifiers.yawFrequencyMultiplier + joint.yawPhase)) + joint.yawOffset;
              }
              // calculate roll
              if (animation.filters.hasOwnProperty(jointName) &&
                 'rollFilter' in animation.filters[jointName]) {
                  jointRotations.z += joint.roll * animation.filters[jointName].rollFilter.calculate
                      (filter.degToRad(ft * modifiers.rollFrequencyMultiplier + joint.rollPhase)) + joint.rollOffset;
              } else {
                  jointRotations.z += joint.roll * Math.sin
                      (filter.degToRad(ft * modifiers.rollFrequencyMultiplier + joint.rollPhase)) + joint.rollOffset;
              }
              return jointRotations;
          },
  
          zeroAnimation: function(animation) {
              for (i in animation.joints) {
                  for (j in animation.joints[i]) {
                      animation.joints[i][j] = 0;
                  }
              }
          },
  
          blendAnimation: function(sourceAnimation, targetAnimation, percent) {
              for (i in targetAnimation.joints) {
                  targetAnimation.joints[i].pitch += percent * sourceAnimation.joints[i].pitch;
                  targetAnimation.joints[i].yaw += percent * sourceAnimation.joints[i].yaw;
                  targetAnimation.joints[i].roll += percent * sourceAnimation.joints[i].roll;
                  targetAnimation.joints[i].pitchPhase += percent * sourceAnimation.joints[i].pitchPhase;
                  targetAnimation.joints[i].yawPhase += percent * sourceAnimation.joints[i].yawPhase;
                  targetAnimation.joints[i].rollPhase += percent * sourceAnimation.joints[i].rollPhase;
                  targetAnimation.joints[i].pitchOffset += percent * sourceAnimation.joints[i].pitchOffset;
                  targetAnimation.joints[i].yawOffset += percent * sourceAnimation.joints[i].yawOffset;
                  targetAnimation.joints[i].rollOffset += percent * sourceAnimation.joints[i].rollOffset;
                  if (i === "Hips") {
                      // Hips only
                      targetAnimation.joints[i].thrust += percent * sourceAnimation.joints[i].thrust;
                      targetAnimation.joints[i].sway += percent * sourceAnimation.joints[i].sway;
                      targetAnimation.joints[i].bob += percent * sourceAnimation.joints[i].bob;
                      targetAnimation.joints[i].thrustPhase += percent * sourceAnimation.joints[i].thrustPhase;
                      targetAnimation.joints[i].swayPhase += percent * sourceAnimation.joints[i].swayPhase;
                      targetAnimation.joints[i].bobPhase += percent * sourceAnimation.joints[i].bobPhase;
                      targetAnimation.joints[i].thrustOffset += percent * sourceAnimation.joints[i].thrustOffset;
                      targetAnimation.joints[i].swayOffset += percent * sourceAnimation.joints[i].swayOffset;
                      targetAnimation.joints[i].bobOffset += percent * sourceAnimation.joints[i].bobOffset;
                  }
              }
          },
  
          deepCopy: function(sourceAnimation, targetAnimation) {
              // calibration
              targetAnimation.calibration = JSON.parse(JSON.stringify(sourceAnimation.calibration));
  
              // harmonics
              targetAnimation.harmonics = {};
              if (sourceAnimation.harmonics) {
                  targetAnimation.harmonics = JSON.parse(JSON.stringify(sourceAnimation.harmonics));
              }
  
              // filters
              targetAnimation.filters = {};
              for (i in sourceAnimation.filters) {
                  // are any filters specified for this joint?
                  if (sourceAnimation.filters[i]) {
                      targetAnimation.filters[i] = sourceAnimation.filters[i];
                      // wave shapers
                      if (sourceAnimation.filters[i].pitchFilter) {
                          targetAnimation.filters[i].pitchFilter = sourceAnimation.filters[i].pitchFilter;
                      }
                      if (sourceAnimation.filters[i].yawFilter) {
                          targetAnimation.filters[i].yawFilter = sourceAnimation.filters[i].yawFilter;
                      }
                      if (sourceAnimation.filters[i].rollFilter) {
                          targetAnimation.filters[i].rollFilter = sourceAnimation.filters[i].rollFilter;
                      }
                      // LP filters
                      if (sourceAnimation.filters[i].swayLPFilter) {
                          targetAnimation.filters[i].swayLPFilter = sourceAnimation.filters[i].swayLPFilter;
                      }
                      if (sourceAnimation.filters[i].bobLPFilter) {
                          targetAnimation.filters[i].bobLPFilter = sourceAnimation.filters[i].bobLPFilter;
                      }
                      if (sourceAnimation.filters[i].thrustLPFilter) {
                          targetAnimation.filters[i].thrustLPFilter = sourceAnimation.filters[i].thrustLPFilter;
                      }
                  }
              }
              // joints
              targetAnimation.joints = JSON.parse(JSON.stringify(sourceAnimation.joints));
          }
      }
  
  })(); // end animation object literal
  
  // ReachPose datafile wrapper object
  ReachPose = function(reachPoseName) {
      this.name = reachPoseName;
      this.reachPoseParameters = walkAssets.getReachPoseParameters(reachPoseName);
      this.reachPoseDataFile = walkAssets.getReachPoseDataFile(reachPoseName);
      this.progress = 0;
      this.smoothingFilter = filter.createAveragingFilter(this.reachPoseParameters.smoothing);
      this.currentStrength = function() {
          // apply optionally smoothed (D)ASDR envelope to reach pose's strength / influence whilst active
          var segmentProgress = undefined; // progress through chosen segment
          var segmentTimeDelta = undefined; // total change in time over chosen segment
          var segmentStrengthDelta = undefined; // total change in strength over chosen segment
          var lastStrength = undefined; // the last value the previous segment held
          var currentStrength = undefined; // return value
  
          // select parameters based on segment (a segment being one of (D),A,S,D or R)
          if (this.progress >= this.reachPoseParameters.sustain.timing) {
              // release segment
              segmentProgress = this.progress - this.reachPoseParameters.sustain.timing;
              segmentTimeDelta = this.reachPoseParameters.release.timing - this.reachPoseParameters.sustain.timing;
              segmentStrengthDelta = this.reachPoseParameters.release.strength - this.reachPoseParameters.sustain.strength;
              lastStrength = this.reachPoseParameters.sustain.strength;
          } else if (this.progress >= this.reachPoseParameters.decay.timing) {
              // sustain phase
              segmentProgress = this.progress - this.reachPoseParameters.decay.timing;
              segmentTimeDelta = this.reachPoseParameters.sustain.timing - this.reachPoseParameters.decay.timing;
              segmentStrengthDelta = this.reachPoseParameters.sustain.strength - this.reachPoseParameters.decay.strength;
              lastStrength = this.reachPoseParameters.decay.strength;
          } else if (this.progress >= this.reachPoseParameters.attack.timing) {
              // decay phase
              segmentProgress = this.progress - this.reachPoseParameters.attack.timing;
              segmentTimeDelta = this.reachPoseParameters.decay.timing - this.reachPoseParameters.attack.timing;
              segmentStrengthDelta = this.reachPoseParameters.decay.strength - this.reachPoseParameters.attack.strength;
              lastStrength = this.reachPoseParameters.attack.strength;
          } else if (this.progress >= this.reachPoseParameters.delay.timing) {
              // attack phase
              segmentProgress = this.progress - this.reachPoseParameters.delay.timing;
              segmentTimeDelta = this.reachPoseParameters.attack.timing - this.reachPoseParameters.delay.timing;
              segmentStrengthDelta = this.reachPoseParameters.attack.strength - this.reachPoseParameters.delay.strength;
              lastStrength = 0; //this.delay.strength;
          } else {
              // delay phase
              segmentProgress = this.progress;
              segmentTimeDelta = this.reachPoseParameters.delay.timing;
              segmentStrengthDelta = this.reachPoseParameters.delay.strength;
              lastStrength = 0;
          }
          currentStrength = segmentTimeDelta > 0 ? lastStrength + segmentStrengthDelta * segmentProgress / segmentTimeDelta
                                                 : lastStrength;
          // smooth off the response curve
          currentStrength = this.smoothingFilter.process(currentStrength);
          return currentStrength;
      }
  };
  
  // constructor with default parameters
  TransitionParameters = function() {
      this.duration = 0.5;
      this.easingLower = {x:0.25, y:0.75};
      this.easingUpper = {x:0.75, y:0.25};
      this.reachPoses = [];
  }
  
  const QUARTER_CYCLE = 90;
  const HALF_CYCLE = 180;
  const THREE_QUARTER_CYCLE = 270;
  const FULL_CYCLE = 360;
  
  // constructor for animation Transition
  Transition = function(nextAnimation, lastAnimation, lastTransition, playTransitionReachPoses) {
  
      if (playTransitionReachPoses === undefined) {
          playTransitionReachPoses = true;
      }
  
      // record the current state of animation
      this.nextAnimation = nextAnimation;
      this.lastAnimation = lastAnimation;
      this.lastTransition = lastTransition;
  
      // collect information about the currently playing animation
      this.direction = motion.direction;
      this.lastDirection = motion.lastDirection;
      this.lastFrequencyTimeWheelPos = motion.frequencyTimeWheelPos;
      this.lastFrequencyTimeIncrement = motion.averageFrequencyTimeIncrement;
      this.lastFrequencyTimeWheelRadius = motion.frequencyTimeWheelRadius;
      this.degreesToTurn = 0; // total degrees to turn the ft wheel before the avatar stops (walk only)
      this.degreesRemaining = 0; // remaining degrees to turn the ft wheel before the avatar stops (walk only)
      this.lastElapsedFTDegrees = motion.elapsedFTDegrees; // degrees elapsed since last transition start
      motion.elapsedFTDegrees = 0; // reset ready for the next transition
      motion.frequencyTimeWheelPos = 0; // start the next animation's frequency time wheel from zero
  
      // set parameters for the transition
      this.parameters = new TransitionParameters();
      this.liveReachPoses = [];
      if (walkAssets && lastAnimation && nextAnimation) {
          // overwrite this.parameters with any transition parameters specified for this particular transition
          walkAssets.getTransitionParameters(lastAnimation, nextAnimation, this.parameters);
          // fire up any reach poses for this transition
          if (playTransitionReachPoses) {
              for (poseName in this.parameters.reachPoses) {
                  this.liveReachPoses.push(new ReachPose(this.parameters.reachPoses[poseName]));
              }
          }
      }
      this.startTime = new Date().getTime(); // Starting timestamp (seconds)
      this.progress = 0; // how far are we through the transition?
      this.filteredProgress = 0;
  
      // coming to a halt whilst walking? if so, will need a clean stopping point defined
      if (motion.isComingToHalt) {
  
          const FULL_CYCLE_THRESHOLD = 320;
          const HALF_CYCLE_THRESHOLD = 140;
          const CYCLE_COMMIT_THRESHOLD = 5;
  
          // how many degrees do we need to turn the walk wheel to finish walking with both feet on the ground?
          if (this.lastElapsedFTDegrees < CYCLE_COMMIT_THRESHOLD) {
              // just stop the walk cycle right here and blend to idle
              this.degreesToTurn = 0;
          } else if (this.lastElapsedFTDegrees < HALF_CYCLE) {
              // we have not taken a complete step yet, so we advance to the second stop angle
              this.degreesToTurn = HALF_CYCLE  - this.lastFrequencyTimeWheelPos;
          } else if (this.lastFrequencyTimeWheelPos > 0 && this.lastFrequencyTimeWheelPos <= HALF_CYCLE_THRESHOLD) {
              // complete the step and stop at 180
              this.degreesToTurn = HALF_CYCLE - this.lastFrequencyTimeWheelPos;
          } else if (this.lastFrequencyTimeWheelPos > HALF_CYCLE_THRESHOLD && this.lastFrequencyTimeWheelPos <= HALF_CYCLE) {
              // complete the step and next then stop at 0
              this.degreesToTurn = HALF_CYCLE - this.lastFrequencyTimeWheelPos + HALF_CYCLE;
          } else if (this.lastFrequencyTimeWheelPos > HALF_CYCLE && this.lastFrequencyTimeWheelPos <= FULL_CYCLE_THRESHOLD) {
              // complete the step and stop at 0
              this.degreesToTurn = FULL_CYCLE - this.lastFrequencyTimeWheelPos;
          } else {
              // complete the step and the next then stop at 180
              this.degreesToTurn = FULL_CYCLE - this.lastFrequencyTimeWheelPos + HALF_CYCLE;
          }
  
          // transition length in this case should be directly proportional to the remaining degrees to turn
          var MIN_FT_INCREMENT = 5.0; // degrees per frame
          var MIN_TRANSITION_DURATION = 0.4;
          const TWO_THIRDS = 0.6667;
          this.lastFrequencyTimeIncrement *= TWO_THIRDS; // help ease the transition
          var lastFrequencyTimeIncrement = this.lastFrequencyTimeIncrement > MIN_FT_INCREMENT ?
                                           this.lastFrequencyTimeIncrement : MIN_FT_INCREMENT;
          var timeToFinish = Math.max(motion.deltaTime * this.degreesToTurn / lastFrequencyTimeIncrement,
                                      MIN_TRANSITION_DURATION);
          this.parameters.duration = timeToFinish;
          this.degreesRemaining = this.degreesToTurn;
      }
  
      // deal with transition recursion (overlapping transitions)
      this.recursionDepth = 0;
      this.incrementRecursion = function() {
          this.recursionDepth += 1;
  
          // cancel any continued motion
          this.degreesToTurn = 0;
  
          // limit the number of layered / nested transitions
          if (this.lastTransition !== nullTransition) {
              this.lastTransition.incrementRecursion();
              if (this.lastTransition.recursionDepth > MAX_TRANSITION_RECURSION) {
                  this.lastTransition = nullTransition;
              }
          }
      };
      if (this.lastTransition !== nullTransition) {
          this.lastTransition.incrementRecursion();
      }
  
      // end of transition initialisation. begin Transition public methods
  
      // keep up the pace for the frequency time wheel for the last animation
      this.advancePreviousFrequencyTimeWheel = function(deltaTime) {
          var wheelAdvance = undefined;
  
          if (this.lastAnimation === avatar.selectedWalkBlend &&
              this.nextAnimation === avatar.selectedIdle) {
              if (this.degreesRemaining <= 0) {
                  // stop continued motion
                  wheelAdvance = 0;
                  if (motion.isComingToHalt) {
                      if (this.lastFrequencyTimeWheelPos < QUARTER_CYCLE) {
                          this.lastFrequencyTimeWheelPos = 0;
                      } else {
                          this.lastFrequencyTimeWheelPos = HALF_CYCLE;
                      }
                  }
              } else {
                  wheelAdvance = this.lastFrequencyTimeIncrement;
                  var distanceToTravel = avatar.calibration.strideLength * wheelAdvance / HALF_CYCLE;
                  if (this.degreesRemaining <= 0) {
                      distanceToTravel = 0;
                      this.degreesRemaining = 0;
                  } else {
                      this.degreesRemaining -= wheelAdvance;
                  }
              }
          } else {
              wheelAdvance = this.lastFrequencyTimeIncrement;
          }
  
          // advance the ft wheel
          this.lastFrequencyTimeWheelPos += wheelAdvance;
          if (this.lastFrequencyTimeWheelPos >= FULL_CYCLE) {
              this.lastFrequencyTimeWheelPos = this.lastFrequencyTimeWheelPos % FULL_CYCLE;
          }
  
          // advance ft wheel for the nested (previous) Transition
          if (this.lastTransition !== nullTransition) {
              this.lastTransition.advancePreviousFrequencyTimeWheel(deltaTime);
          }
          // update the lastElapsedFTDegrees for short stepping
          this.lastElapsedFTDegrees += wheelAdvance;
          this.degreesTurned += wheelAdvance;
      };
  
      this.updateProgress = function() {
          const MILLISECONDS_CONVERT = 1000;
          const ACCURACY_INCREASER = 1000;
          var elapasedTime = (new Date().getTime() - this.startTime) / MILLISECONDS_CONVERT;
          this.progress = elapasedTime / this.parameters.duration;
          this.progress = Math.round(this.progress * ACCURACY_INCREASER) / ACCURACY_INCREASER;
  
          // updated nested transition/s
          if (this.lastTransition !== nullTransition) {
              if (this.lastTransition.updateProgress() === TRANSITION_COMPLETE) {
                  // the previous transition is now complete
                  this.lastTransition = nullTransition;
              }
          }
  
          // update any reachPoses
          for (pose in this.liveReachPoses) {
              // use independent timing for reachPoses
              this.liveReachPoses[pose].progress += (motion.deltaTime / this.liveReachPoses[pose].reachPoseParameters.duration);
              if (this.liveReachPoses[pose].progress >= 1) {
                  // time to kill off this reach pose
                  this.liveReachPoses.splice(pose, 1);
              }
          }
  
          // update transition progress
          this.filteredProgress = filter.bezier(this.progress, this.parameters.easingLower, this.parameters.easingUpper);
          return this.progress >= 1 ? TRANSITION_COMPLETE : false;
      };
  
      this.blendTranslations = function(frequencyTimeWheelPos, direction) {
          var lastTranslations = {x:0, y:0, z:0};
          var nextTranslations = animationOperations.calculateTranslations(this.nextAnimation,
                                                                           frequencyTimeWheelPos,
                                                                           direction);
          // are we blending with a previous, still live transition?
          if (this.lastTransition !== nullTransition) {
              lastTranslations = this.lastTransition.blendTranslations(this.lastFrequencyTimeWheelPos,
                                                                       this.lastDirection);
          } else {
              lastTranslations = animationOperations.calculateTranslations(this.lastAnimation,
                                                       this.lastFrequencyTimeWheelPos,
                                                       this.lastDirection);
          }
  
          // blend last / next translations
          nextTranslations = Vec3.multiply(this.filteredProgress, nextTranslations);
          lastTranslations = Vec3.multiply((1 - this.filteredProgress), lastTranslations);
          nextTranslations = Vec3.sum(nextTranslations, lastTranslations);
  
          if (this.liveReachPoses.length > 0) {
              for (pose in this.liveReachPoses) {
                  var reachPoseStrength = this.liveReachPoses[pose].currentStrength();
                  var poseTranslations = animationOperations.calculateTranslations(
                                                               this.liveReachPoses[pose].reachPoseDataFile,
                                                               frequencyTimeWheelPos,
                                                               direction);
  
                  // can't use Vec3 operations here, as if x,y or z is zero, the reachPose should have no influence at all
                  if (Math.abs(poseTranslations.x) > 0) {
                      nextTranslations.x = reachPoseStrength * poseTranslations.x + (1 - reachPoseStrength) * nextTranslations.x;
                  }
                  if (Math.abs(poseTranslations.y) > 0) {
                      nextTranslations.y = reachPoseStrength * poseTranslations.y + (1 - reachPoseStrength) * nextTranslations.y;
                  }
                  if (Math.abs(poseTranslations.z) > 0) {
                      nextTranslations.z = reachPoseStrength * poseTranslations.z + (1 - reachPoseStrength) * nextTranslations.z;
                  }
              }
          }
          return nextTranslations;
      };
  
      this.blendRotations = function(jointName, frequencyTimeWheelPos, direction) {
          var lastRotations = {x:0, y:0, z:0};
          var nextRotations = animationOperations.calculateRotations(jointName,
                                                 this.nextAnimation,
                                                 frequencyTimeWheelPos,
                                                 direction);
  
          // are we blending with a previous, still live transition?
          if (this.lastTransition !== nullTransition) {
              lastRotations = this.lastTransition.blendRotations(jointName,
                                                                 this.lastFrequencyTimeWheelPos,
                                                                 this.lastDirection);
          } else {
              lastRotations = animationOperations.calculateRotations(jointName,
                                                 this.lastAnimation,
                                                 this.lastFrequencyTimeWheelPos,
                                                 this.lastDirection);
          }
          // blend last / next translations
          nextRotations = Vec3.multiply(this.filteredProgress, nextRotations);
          lastRotations = Vec3.multiply((1 - this.filteredProgress), lastRotations);
          nextRotations = Vec3.sum(nextRotations, lastRotations);
  
          // are there reachPoses defined for this transition?
          if (this.liveReachPoses.length > 0) {
              for (pose in this.liveReachPoses) {
                  var reachPoseStrength = this.liveReachPoses[pose].currentStrength();
                  var poseRotations = animationOperations.calculateRotations(jointName,
                                                         this.liveReachPoses[pose].reachPoseDataFile,
                                                         frequencyTimeWheelPos,
                                                         direction);
  
                  // don't use Vec3 operations here, as if x,y or z is zero, the reach pose should have no influence at all
                  if (Math.abs(poseRotations.x) > 0) {
                      nextRotations.x = reachPoseStrength * poseRotations.x + (1 - reachPoseStrength) * nextRotations.x;
                  }
                  if (Math.abs(poseRotations.y) > 0) {
                      nextRotations.y = reachPoseStrength * poseRotations.y + (1 - reachPoseStrength) * nextRotations.y;
                  }
                  if (Math.abs(poseRotations.z) > 0) {
                      nextRotations.z = reachPoseStrength * poseRotations.z + (1 - reachPoseStrength) * nextRotations.z;
                  }
              }
          }
          return nextRotations;
      };
  }; // end Transition constructor
  
  // individual joint modifiers
  FrequencyMultipliers = function(joint, direction) {
      // gather multipliers
      this.pitchFrequencyMultiplier = 1;
      this.yawFrequencyMultiplier = 1;
      this.rollFrequencyMultiplier = 1;
      this.swayFrequencyMultiplier = 1;
      this.bobFrequencyMultiplier = 1;
      this.thrustFrequencyMultiplier = 1;
  
      if (joint) {
          if (joint.pitchFrequencyMultiplier) {
              this.pitchFrequencyMultiplier = joint.pitchFrequencyMultiplier;
          }
          if (joint.yawFrequencyMultiplier) {
              this.yawFrequencyMultiplier = joint.yawFrequencyMultiplier;
          }
          if (joint.rollFrequencyMultiplier) {
              this.rollFrequencyMultiplier = joint.rollFrequencyMultiplier;
          }
          if (joint.swayFrequencyMultiplier) {
              this.swayFrequencyMultiplier = joint.swayFrequencyMultiplier;
          }
          if (joint.bobFrequencyMultiplier) {
              this.bobFrequencyMultiplier = joint.bobFrequencyMultiplier;
          }
          if (joint.thrustFrequencyMultiplier) {
              this.thrustFrequencyMultiplier = joint.thrustFrequencyMultiplier;
          }
      }
  };
  