KeyframeTrack.js 6.8 KB

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