KeyframeTrack.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. /**
  2. *
  3. * A Track that returns a keyframe interpolated value.
  4. *
  5. * TODO: Add cubic in addition to linear interpolation.
  6. *
  7. * @author Ben Houston / http://clara.io/
  8. * @author David Sarno / http://lighthaus.us/
  9. */
  10. THREE.KeyframeTrack = function ( name, keys ) {
  11. this.name = name;
  12. this.keys = keys || []; // time in seconds, value as value
  13. this.sort();
  14. this.validate();
  15. //this.optimize();
  16. };
  17. THREE.KeyframeTrack.prototype = {
  18. constructor: THREE.KeyframeTrack,
  19. getAt: function( time ) {
  20. //console.log( 'KeyframeTrack[' + this.name + '].getAt( ' + time + ' )' );
  21. if( this.keys.length == 0 ) throw new Error( "no keys in track named " + this.name );
  22. //console.log( "keys", this.keys );
  23. // before the start of the track, return the first key value
  24. if( this.keys[0].time >= time ) {
  25. //console.log( ' before: ' + this.keys[0].value );
  26. return this.keys[0].value;
  27. }
  28. // past the end of the track, return the last key value
  29. if( this.keys[ this.keys.length - 1 ].time <= time ) {
  30. //console.log( ' after: ' + this.keys[ this.keys.length - 1 ].value );
  31. return this.keys[ this.keys.length - 1 ].value;
  32. }
  33. // interpolate to the required time
  34. for( var i = 1; i < this.keys.length; i ++ ) {
  35. if( time <= this.keys[ i ].time ) {
  36. // linear interpolation to start with
  37. var alpha = ( time - this.keys[ i - 1 ].time ) / ( this.keys[ i ].time - this.keys[ i - 1 ].time );
  38. var interpolatedValue = this.lerp( this.keys[ i - 1 ].value, this.keys[ i ].value, alpha );
  39. /*console.log( ' interpolated: ', {
  40. value: interpolatedValue,
  41. alpha: alpha,
  42. time0: this.keys[ i - 1 ].time,
  43. time1: this.keys[ i ].time,
  44. value0: this.keys[ i - 1 ].value,
  45. value1: this.keys[ i ].value
  46. } );*/
  47. return interpolatedValue;
  48. }
  49. }
  50. throw new Error( "should never get here." );
  51. },
  52. // memoization of the lerp function for speed.
  53. // NOTE: Do not optimize as a prototype initialization closure, as value0 will be different on a per class basis.
  54. lerp: function( value0, value1, alpha ) {
  55. this.lerp = THREE.AnimationUtils.getLerpFunc( value0, false );
  56. return this.lerp( value0, value1, alpha );
  57. },
  58. // sort in ascending order
  59. sort: function() {
  60. var keyComparator = function(key0, key1) {
  61. return key0.time - key1.time;
  62. };
  63. return function() {
  64. this.keys.sort( keyComparator );
  65. }
  66. }(),
  67. // ensure we do not get a GarbageInGarbageOut situation, make sure tracks are at least minimally viable
  68. // TODO: ensure that all key.values in a track are all of the same type (otherwise interpolation makes no sense.)
  69. validate: function() {
  70. var prevKey = null;
  71. if( this.keys.length === 0 ) {
  72. console.error( " track is empty, no keys", this );
  73. return;
  74. }
  75. for( var i = 0; i < this.keys.length; i ++ ) {
  76. var currKey = this.keys[i];
  77. if( ! currKey ) {
  78. console.error( " key is null in track", this, i );
  79. return;
  80. }
  81. if( ( typeof currKey.time ) !== 'number' || Number.isNaN( currKey.time ) ) {
  82. console.error( " key.time is not a valid number", this, i, currKey );
  83. return;
  84. }
  85. if( currKey.value === undefined || currKey.value === null) {
  86. console.error( " key.value is null in track", this, i, currKey );
  87. return;
  88. }
  89. if( prevKey && prevKey.time > currKey.time ) {
  90. console.error( " key.time is less than previous key time, out of order keys", this, i, currKey, prevKey );
  91. return;
  92. }
  93. prevKey = currKey;
  94. }
  95. },
  96. // currently only removes equivalent sequential keys (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0), which are common in morph target animations
  97. // TODO: linear based interpolation optimization with an error threshold.
  98. optimize: function() {
  99. var newKeys = [];
  100. var prevKey = this.keys[0];
  101. newKeys.push( prevKey );
  102. var equalsFunc = THREE.AnimationUtils.equalsFunc( prevKey.value );
  103. for( var i = 1; i < this.keys.length - 1; i ++ ) {
  104. var currKey = this.keys[i];
  105. var nextKey = this.keys[i+1];
  106. //console.log( prevKey, currKey, nextKey );
  107. // if prevKey & currKey are the same time, remove currKey. If you want immediate adjacent keys, use an epsilon offset
  108. // it is not possible to have two keys at the same time as we sort them. The sort is not stable on keys with the same time.
  109. if( ( prevKey.time === currKey.time ) ) {
  110. console.log( 'removing key at the same time', currKey );
  111. continue;
  112. }
  113. // remove completely unnecessary keyframes that are the same as their prev and next keys
  114. if( equalsFunc( prevKey.value, currKey.value ) && equalsFunc( currKey.value, nextKey.value ) ) {
  115. console.log( 'removing key identical to prev and next', currKey );
  116. continue;
  117. }
  118. // TODO:add here a check for linear interpolation optimization.
  119. newKeys.push( currKey );
  120. prevKey = currKey;
  121. }
  122. newKeys.push( this.keys[ this.keys.length - 1 ] );
  123. if( ( this.keys.length - newKeys.length ) > 0 ) {
  124. console.log( ' optimizing removed keys:', ( this.keys.length - newKeys.length ), this.name );
  125. }
  126. this.keys = newKeys;
  127. },
  128. // removes keyframes before and after animation without changing any values within the range [0,duration].
  129. // IMPORTANT: We do not shift around keys to the start of the track time, because for interpolated keys this will change their values
  130. trim: function( duration ) {
  131. var firstKeysToRemove = 0;
  132. for( var i = 1; i < this.keys.length; i ++ ) {
  133. if( this.keys[i] <= 0 ) {
  134. firstKeysToRemove ++;
  135. }
  136. }
  137. var lastKeysToRemove = 0;
  138. for( var i = this.keys.length - 2; i > 0; i ++ ) {
  139. if( this.keys[i] >= duration ) {
  140. lastKeysToRemove ++;
  141. }
  142. else {
  143. break;
  144. }
  145. }
  146. // remove last keys first because it doesn't affect the position of the first keys (the otherway around doesn't work as easily)
  147. // TODO: Figure out if there is an array subarray function... might be faster
  148. if( ( firstKeysToRemove + lastKeysToRemove ) > 0 ) {
  149. console.log( ' triming removed keys: first and last', firstKeysToRemove, lastKeysToRemove, this.keys );
  150. this.keys = this.keys.splice( firstKeysToRemove, this.keys.length - lastKeysToRemove - firstKeysToRemove );;
  151. console.log( ' result', this.keys );
  152. }
  153. }
  154. };