AnimationAction.js 12 KB

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