AnimationAction.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. import { WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, LoopPingPong, LoopOnce, LoopRepeat, NormalAnimationBlendMode, AdditiveAnimationBlendMode } from '../constants.js';
  2. class AnimationAction {
  3. constructor( mixer, clip, localRoot, blendMode ) {
  4. this._mixer = mixer;
  5. this._clip = clip;
  6. this._localRoot = localRoot || null;
  7. this.blendMode = blendMode || clip.blendMode;
  8. const tracks = clip.tracks,
  9. nTracks = tracks.length,
  10. interpolants = new Array( nTracks );
  11. const interpolantSettings = {
  12. endingStart: ZeroCurvatureEnding,
  13. endingEnd: ZeroCurvatureEnding
  14. };
  15. for ( let i = 0; i !== nTracks; ++ i ) {
  16. const interpolant = tracks[ i ].createInterpolant( null );
  17. interpolants[ i ] = interpolant;
  18. interpolant.settings = interpolantSettings;
  19. }
  20. this._interpolantSettings = interpolantSettings;
  21. this._interpolants = interpolants; // bound by the mixer
  22. // inside: PropertyMixer (managed by the mixer)
  23. this._propertyBindings = new Array( nTracks );
  24. this._cacheIndex = null; // for the memory manager
  25. this._byClipCacheIndex = null; // for the memory manager
  26. this._timeScaleInterpolant = null;
  27. this._weightInterpolant = null;
  28. this.loop = LoopRepeat;
  29. this._loopCount = - 1;
  30. // global mixer time when the action is to be started
  31. // it's set back to 'null' upon start of the action
  32. this._startTime = null;
  33. // scaled local time of the action
  34. // gets clamped or wrapped to 0..clip.duration according to loop
  35. this.time = 0;
  36. this.timeScale = 1;
  37. this._effectiveTimeScale = 1;
  38. this.weight = 1;
  39. this._effectiveWeight = 1;
  40. this.repetitions = Infinity; // no. of repetitions when looping
  41. this.paused = false; // true -> zero effective time scale
  42. this.enabled = true; // false -> zero effective weight
  43. this.clampWhenFinished = false;// keep feeding the last frame?
  44. this.zeroSlopeAtStart = true;// for smooth interpolation w/o separate
  45. this.zeroSlopeAtEnd = true;// clips for start, loop and end
  46. }
  47. // State & Scheduling
  48. play() {
  49. this._mixer._activateAction( this );
  50. return this;
  51. }
  52. stop() {
  53. this._mixer._deactivateAction( this );
  54. return this.reset();
  55. }
  56. reset() {
  57. this.paused = false;
  58. this.enabled = true;
  59. this.time = 0; // restart clip
  60. this._loopCount = - 1;// forget previous loops
  61. this._startTime = null;// forget scheduling
  62. return this.stopFading().stopWarping();
  63. }
  64. isRunning() {
  65. return this.enabled && ! this.paused && this.timeScale !== 0 &&
  66. this._startTime === null && this._mixer._isActiveAction( this );
  67. }
  68. // return true when play has been called
  69. isScheduled() {
  70. return this._mixer._isActiveAction( this );
  71. }
  72. startAt( time ) {
  73. this._startTime = time;
  74. return this;
  75. }
  76. setLoop( mode, repetitions ) {
  77. this.loop = mode;
  78. this.repetitions = repetitions;
  79. return this;
  80. }
  81. // Weight
  82. // set the weight stopping any scheduled fading
  83. // although .enabled = false yields an effective weight of zero, this
  84. // method does *not* change .enabled, because it would be confusing
  85. setEffectiveWeight( weight ) {
  86. this.weight = weight;
  87. // note: same logic as when updated at runtime
  88. this._effectiveWeight = this.enabled ? weight : 0;
  89. return this.stopFading();
  90. }
  91. // return the weight considering fading and .enabled
  92. getEffectiveWeight() {
  93. return this._effectiveWeight;
  94. }
  95. fadeIn( duration ) {
  96. return this._scheduleFading( duration, 0, 1 );
  97. }
  98. fadeOut( duration ) {
  99. return this._scheduleFading( duration, 1, 0 );
  100. }
  101. crossFadeFrom( fadeOutAction, duration, warp ) {
  102. fadeOutAction.fadeOut( duration );
  103. this.fadeIn( duration );
  104. if ( warp ) {
  105. const fadeInDuration = this._clip.duration,
  106. fadeOutDuration = fadeOutAction._clip.duration,
  107. startEndRatio = fadeOutDuration / fadeInDuration,
  108. endStartRatio = fadeInDuration / fadeOutDuration;
  109. fadeOutAction.warp( 1.0, startEndRatio, duration );
  110. this.warp( endStartRatio, 1.0, duration );
  111. }
  112. return this;
  113. }
  114. crossFadeTo( fadeInAction, duration, warp ) {
  115. return fadeInAction.crossFadeFrom( this, duration, warp );
  116. }
  117. stopFading() {
  118. const weightInterpolant = this._weightInterpolant;
  119. if ( weightInterpolant !== null ) {
  120. this._weightInterpolant = null;
  121. this._mixer._takeBackControlInterpolant( weightInterpolant );
  122. }
  123. return this;
  124. }
  125. // Time Scale Control
  126. // set the time scale stopping any scheduled warping
  127. // although .paused = true yields an effective time scale of zero, this
  128. // method does *not* change .paused, because it would be confusing
  129. setEffectiveTimeScale( timeScale ) {
  130. this.timeScale = timeScale;
  131. this._effectiveTimeScale = this.paused ? 0 : timeScale;
  132. return this.stopWarping();
  133. }
  134. // return the time scale considering warping and .paused
  135. getEffectiveTimeScale() {
  136. return this._effectiveTimeScale;
  137. }
  138. setDuration( duration ) {
  139. this.timeScale = this._clip.duration / duration;
  140. return this.stopWarping();
  141. }
  142. syncWith( action ) {
  143. this.time = action.time;
  144. this.timeScale = action.timeScale;
  145. return this.stopWarping();
  146. }
  147. halt( duration ) {
  148. return this.warp( this._effectiveTimeScale, 0, duration );
  149. }
  150. warp( startTimeScale, endTimeScale, duration ) {
  151. const mixer = this._mixer,
  152. now = mixer.time,
  153. timeScale = this.timeScale;
  154. let interpolant = this._timeScaleInterpolant;
  155. if ( interpolant === null ) {
  156. interpolant = mixer._lendControlInterpolant();
  157. this._timeScaleInterpolant = interpolant;
  158. }
  159. const times = interpolant.parameterPositions,
  160. values = interpolant.sampleValues;
  161. times[ 0 ] = now;
  162. times[ 1 ] = now + duration;
  163. values[ 0 ] = startTimeScale / timeScale;
  164. values[ 1 ] = endTimeScale / timeScale;
  165. return this;
  166. }
  167. stopWarping() {
  168. const timeScaleInterpolant = this._timeScaleInterpolant;
  169. if ( timeScaleInterpolant !== null ) {
  170. this._timeScaleInterpolant = null;
  171. this._mixer._takeBackControlInterpolant( timeScaleInterpolant );
  172. }
  173. return this;
  174. }
  175. // Object Accessors
  176. getMixer() {
  177. return this._mixer;
  178. }
  179. getClip() {
  180. return this._clip;
  181. }
  182. getRoot() {
  183. return this._localRoot || this._mixer._root;
  184. }
  185. // Interna
  186. _update( time, deltaTime, timeDirection, accuIndex ) {
  187. // called by the mixer
  188. if ( ! this.enabled ) {
  189. // call ._updateWeight() to update ._effectiveWeight
  190. this._updateWeight( time );
  191. return;
  192. }
  193. const startTime = this._startTime;
  194. if ( startTime !== null ) {
  195. // check for scheduled start of action
  196. const timeRunning = ( time - startTime ) * timeDirection;
  197. if ( timeRunning < 0 || timeDirection === 0 ) {
  198. return; // yet to come / don't decide when delta = 0
  199. }
  200. // start
  201. this._startTime = null; // unschedule
  202. deltaTime = timeDirection * timeRunning;
  203. }
  204. // apply time scale and advance time
  205. deltaTime *= this._updateTimeScale( time );
  206. const clipTime = this._updateTime( deltaTime );
  207. // note: _updateTime may disable the action resulting in
  208. // an effective weight of 0
  209. const weight = this._updateWeight( time );
  210. if ( weight > 0 ) {
  211. const interpolants = this._interpolants;
  212. const propertyMixers = this._propertyBindings;
  213. switch ( this.blendMode ) {
  214. case AdditiveAnimationBlendMode:
  215. for ( let j = 0, m = interpolants.length; j !== m; ++ j ) {
  216. interpolants[ j ].evaluate( clipTime );
  217. propertyMixers[ j ].accumulateAdditive( weight );
  218. }
  219. break;
  220. case NormalAnimationBlendMode:
  221. default:
  222. for ( let j = 0, m = interpolants.length; j !== m; ++ j ) {
  223. interpolants[ j ].evaluate( clipTime );
  224. propertyMixers[ j ].accumulate( accuIndex, weight );
  225. }
  226. }
  227. }
  228. }
  229. _updateWeight( time ) {
  230. let weight = 0;
  231. if ( this.enabled ) {
  232. weight = this.weight;
  233. const interpolant = this._weightInterpolant;
  234. if ( interpolant !== null ) {
  235. const interpolantValue = interpolant.evaluate( time )[ 0 ];
  236. weight *= interpolantValue;
  237. if ( time > interpolant.parameterPositions[ 1 ] ) {
  238. this.stopFading();
  239. if ( interpolantValue === 0 ) {
  240. // faded out, disable
  241. this.enabled = false;
  242. }
  243. }
  244. }
  245. }
  246. this._effectiveWeight = weight;
  247. return weight;
  248. }
  249. _updateTimeScale( time ) {
  250. let timeScale = 0;
  251. if ( ! this.paused ) {
  252. timeScale = this.timeScale;
  253. const interpolant = this._timeScaleInterpolant;
  254. if ( interpolant !== null ) {
  255. const interpolantValue = interpolant.evaluate( time )[ 0 ];
  256. timeScale *= interpolantValue;
  257. if ( time > interpolant.parameterPositions[ 1 ] ) {
  258. this.stopWarping();
  259. if ( timeScale === 0 ) {
  260. // motion has halted, pause
  261. this.paused = true;
  262. } else {
  263. // warp done - apply final time scale
  264. this.timeScale = timeScale;
  265. }
  266. }
  267. }
  268. }
  269. this._effectiveTimeScale = timeScale;
  270. return timeScale;
  271. }
  272. _updateTime( deltaTime ) {
  273. const duration = this._clip.duration;
  274. const loop = this.loop;
  275. let time = this.time + deltaTime;
  276. let loopCount = this._loopCount;
  277. const pingPong = ( loop === LoopPingPong );
  278. if ( deltaTime === 0 ) {
  279. if ( loopCount === - 1 ) return time;
  280. return ( pingPong && ( loopCount & 1 ) === 1 ) ? duration - time : time;
  281. }
  282. if ( loop === LoopOnce ) {
  283. if ( loopCount === - 1 ) {
  284. // just started
  285. this._loopCount = 0;
  286. this._setEndings( true, true, false );
  287. }
  288. handle_stop: {
  289. if ( time >= duration ) {
  290. time = duration;
  291. } else if ( time < 0 ) {
  292. time = 0;
  293. } else {
  294. this.time = time;
  295. break handle_stop;
  296. }
  297. if ( this.clampWhenFinished ) this.paused = true;
  298. else this.enabled = false;
  299. this.time = time;
  300. this._mixer.dispatchEvent( {
  301. type: 'finished', action: this,
  302. direction: deltaTime < 0 ? - 1 : 1
  303. } );
  304. }
  305. } else { // repetitive Repeat or PingPong
  306. if ( loopCount === - 1 ) {
  307. // just started
  308. if ( deltaTime >= 0 ) {
  309. loopCount = 0;
  310. this._setEndings( true, this.repetitions === 0, pingPong );
  311. } else {
  312. // when looping in reverse direction, the initial
  313. // transition through zero counts as a repetition,
  314. // so leave loopCount at -1
  315. this._setEndings( this.repetitions === 0, true, pingPong );
  316. }
  317. }
  318. if ( time >= duration || time < 0 ) {
  319. // wrap around
  320. const loopDelta = Math.floor( time / duration ); // signed
  321. time -= duration * loopDelta;
  322. loopCount += Math.abs( loopDelta );
  323. const pending = this.repetitions - loopCount;
  324. if ( pending <= 0 ) {
  325. // have to stop (switch state, clamp time, fire event)
  326. if ( this.clampWhenFinished ) this.paused = true;
  327. else this.enabled = false;
  328. time = deltaTime > 0 ? duration : 0;
  329. this.time = time;
  330. this._mixer.dispatchEvent( {
  331. type: 'finished', action: this,
  332. direction: deltaTime > 0 ? 1 : - 1
  333. } );
  334. } else {
  335. // keep running
  336. if ( pending === 1 ) {
  337. // entering the last round
  338. const atStart = deltaTime < 0;
  339. this._setEndings( atStart, ! atStart, pingPong );
  340. } else {
  341. this._setEndings( false, false, pingPong );
  342. }
  343. this._loopCount = loopCount;
  344. this.time = time;
  345. this._mixer.dispatchEvent( {
  346. type: 'loop', action: this, loopDelta: loopDelta
  347. } );
  348. }
  349. } else {
  350. this.time = time;
  351. }
  352. if ( pingPong && ( loopCount & 1 ) === 1 ) {
  353. // invert time for the "pong round"
  354. return duration - time;
  355. }
  356. }
  357. return time;
  358. }
  359. _setEndings( atStart, atEnd, pingPong ) {
  360. const settings = this._interpolantSettings;
  361. if ( pingPong ) {
  362. settings.endingStart = ZeroSlopeEnding;
  363. settings.endingEnd = ZeroSlopeEnding;
  364. } else {
  365. // assuming for LoopOnce atStart == atEnd == true
  366. if ( atStart ) {
  367. settings.endingStart = this.zeroSlopeAtStart ? ZeroSlopeEnding : ZeroCurvatureEnding;
  368. } else {
  369. settings.endingStart = WrapAroundEnding;
  370. }
  371. if ( atEnd ) {
  372. settings.endingEnd = this.zeroSlopeAtEnd ? ZeroSlopeEnding : ZeroCurvatureEnding;
  373. } else {
  374. settings.endingEnd = WrapAroundEnding;
  375. }
  376. }
  377. }
  378. _scheduleFading( duration, weightNow, weightThen ) {
  379. const mixer = this._mixer, now = mixer.time;
  380. let interpolant = this._weightInterpolant;
  381. if ( interpolant === null ) {
  382. interpolant = mixer._lendControlInterpolant();
  383. this._weightInterpolant = interpolant;
  384. }
  385. const times = interpolant.parameterPositions,
  386. values = interpolant.sampleValues;
  387. times[ 0 ] = now;
  388. values[ 0 ] = weightNow;
  389. times[ 1 ] = now + duration;
  390. values[ 1 ] = weightThen;
  391. return this;
  392. }
  393. }
  394. export { AnimationAction };