AnimationAction.js 12 KB

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