123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699 |
- import { WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, LoopPingPong, LoopOnce, LoopRepeat, NormalAnimationBlendMode, AdditiveAnimationBlendMode } from '../constants.js';
- class AnimationAction {
- constructor( mixer, clip, localRoot, blendMode ) {
- this._mixer = mixer;
- this._clip = clip;
- this._localRoot = localRoot || null;
- this.blendMode = blendMode || clip.blendMode;
- const tracks = clip.tracks,
- nTracks = tracks.length,
- interpolants = new Array( nTracks );
- const interpolantSettings = {
- endingStart: ZeroCurvatureEnding,
- endingEnd: ZeroCurvatureEnding
- };
- for ( let i = 0; i !== nTracks; ++ i ) {
- const interpolant = tracks[ i ].createInterpolant( null );
- interpolants[ i ] = interpolant;
- interpolant.settings = interpolantSettings;
- }
- this._interpolantSettings = interpolantSettings;
- this._interpolants = interpolants; // bound by the mixer
- // inside: PropertyMixer (managed by the mixer)
- this._propertyBindings = new Array( nTracks );
- this._cacheIndex = null; // for the memory manager
- this._byClipCacheIndex = null; // for the memory manager
- this._timeScaleInterpolant = null;
- this._weightInterpolant = null;
- this.loop = LoopRepeat;
- this._loopCount = - 1;
- // global mixer time when the action is to be started
- // it's set back to 'null' upon start of the action
- this._startTime = null;
- // scaled local time of the action
- // gets clamped or wrapped to 0..clip.duration according to loop
- this.time = 0;
- this.timeScale = 1;
- this._effectiveTimeScale = 1;
- this.weight = 1;
- this._effectiveWeight = 1;
- this.repetitions = Infinity; // no. of repetitions when looping
- this.paused = false; // true -> zero effective time scale
- this.enabled = true; // false -> zero effective weight
- this.clampWhenFinished = false;// keep feeding the last frame?
- this.zeroSlopeAtStart = true;// for smooth interpolation w/o separate
- this.zeroSlopeAtEnd = true;// clips for start, loop and end
- }
- // State & Scheduling
- play() {
- this._mixer._activateAction( this );
- return this;
- }
- stop() {
- this._mixer._deactivateAction( this );
- return this.reset();
- }
- reset() {
- this.paused = false;
- this.enabled = true;
- this.time = 0; // restart clip
- this._loopCount = - 1;// forget previous loops
- this._startTime = null;// forget scheduling
- return this.stopFading().stopWarping();
- }
- isRunning() {
- return this.enabled && ! this.paused && this.timeScale !== 0 &&
- this._startTime === null && this._mixer._isActiveAction( this );
- }
- // return true when play has been called
- isScheduled() {
- return this._mixer._isActiveAction( this );
- }
- startAt( time ) {
- this._startTime = time;
- return this;
- }
- setLoop( mode, repetitions ) {
- this.loop = mode;
- this.repetitions = repetitions;
- return this;
- }
- // Weight
- // set the weight stopping any scheduled fading
- // although .enabled = false yields an effective weight of zero, this
- // method does *not* change .enabled, because it would be confusing
- setEffectiveWeight( weight ) {
- this.weight = weight;
- // note: same logic as when updated at runtime
- this._effectiveWeight = this.enabled ? weight : 0;
- return this.stopFading();
- }
- // return the weight considering fading and .enabled
- getEffectiveWeight() {
- return this._effectiveWeight;
- }
- fadeIn( duration ) {
- return this._scheduleFading( duration, 0, 1 );
- }
- fadeOut( duration ) {
- return this._scheduleFading( duration, 1, 0 );
- }
- crossFadeFrom( fadeOutAction, duration, warp ) {
- fadeOutAction.fadeOut( duration );
- this.fadeIn( duration );
- if ( warp ) {
- const fadeInDuration = this._clip.duration,
- fadeOutDuration = fadeOutAction._clip.duration,
- startEndRatio = fadeOutDuration / fadeInDuration,
- endStartRatio = fadeInDuration / fadeOutDuration;
- fadeOutAction.warp( 1.0, startEndRatio, duration );
- this.warp( endStartRatio, 1.0, duration );
- }
- return this;
- }
- crossFadeTo( fadeInAction, duration, warp ) {
- return fadeInAction.crossFadeFrom( this, duration, warp );
- }
- stopFading() {
- const weightInterpolant = this._weightInterpolant;
- if ( weightInterpolant !== null ) {
- this._weightInterpolant = null;
- this._mixer._takeBackControlInterpolant( weightInterpolant );
- }
- return this;
- }
- // Time Scale Control
- // set the time scale stopping any scheduled warping
- // although .paused = true yields an effective time scale of zero, this
- // method does *not* change .paused, because it would be confusing
- setEffectiveTimeScale( timeScale ) {
- this.timeScale = timeScale;
- this._effectiveTimeScale = this.paused ? 0 : timeScale;
- return this.stopWarping();
- }
- // return the time scale considering warping and .paused
- getEffectiveTimeScale() {
- return this._effectiveTimeScale;
- }
- setDuration( duration ) {
- this.timeScale = this._clip.duration / duration;
- return this.stopWarping();
- }
- syncWith( action ) {
- this.time = action.time;
- this.timeScale = action.timeScale;
- return this.stopWarping();
- }
- halt( duration ) {
- return this.warp( this._effectiveTimeScale, 0, duration );
- }
- warp( startTimeScale, endTimeScale, duration ) {
- const mixer = this._mixer,
- now = mixer.time,
- timeScale = this.timeScale;
- let interpolant = this._timeScaleInterpolant;
- if ( interpolant === null ) {
- interpolant = mixer._lendControlInterpolant();
- this._timeScaleInterpolant = interpolant;
- }
- const times = interpolant.parameterPositions,
- values = interpolant.sampleValues;
- times[ 0 ] = now;
- times[ 1 ] = now + duration;
- values[ 0 ] = startTimeScale / timeScale;
- values[ 1 ] = endTimeScale / timeScale;
- return this;
- }
- stopWarping() {
- const timeScaleInterpolant = this._timeScaleInterpolant;
- if ( timeScaleInterpolant !== null ) {
- this._timeScaleInterpolant = null;
- this._mixer._takeBackControlInterpolant( timeScaleInterpolant );
- }
- return this;
- }
- // Object Accessors
- getMixer() {
- return this._mixer;
- }
- getClip() {
- return this._clip;
- }
- getRoot() {
- return this._localRoot || this._mixer._root;
- }
- // Interna
- _update( time, deltaTime, timeDirection, accuIndex ) {
- // called by the mixer
- if ( ! this.enabled ) {
- // call ._updateWeight() to update ._effectiveWeight
- this._updateWeight( time );
- return;
- }
- const startTime = this._startTime;
- if ( startTime !== null ) {
- // check for scheduled start of action
- const timeRunning = ( time - startTime ) * timeDirection;
- if ( timeRunning < 0 || timeDirection === 0 ) {
- return; // yet to come / don't decide when delta = 0
- }
- // start
- this._startTime = null; // unschedule
- deltaTime = timeDirection * timeRunning;
- }
- // apply time scale and advance time
- deltaTime *= this._updateTimeScale( time );
- const clipTime = this._updateTime( deltaTime );
- // note: _updateTime may disable the action resulting in
- // an effective weight of 0
- const weight = this._updateWeight( time );
- if ( weight > 0 ) {
- const interpolants = this._interpolants;
- const propertyMixers = this._propertyBindings;
- switch ( this.blendMode ) {
- case AdditiveAnimationBlendMode:
- for ( let j = 0, m = interpolants.length; j !== m; ++ j ) {
- interpolants[ j ].evaluate( clipTime );
- propertyMixers[ j ].accumulateAdditive( weight );
- }
- break;
- case NormalAnimationBlendMode:
- default:
- for ( let j = 0, m = interpolants.length; j !== m; ++ j ) {
- interpolants[ j ].evaluate( clipTime );
- propertyMixers[ j ].accumulate( accuIndex, weight );
- }
- }
- }
- }
- _updateWeight( time ) {
- let weight = 0;
- if ( this.enabled ) {
- weight = this.weight;
- const interpolant = this._weightInterpolant;
- if ( interpolant !== null ) {
- const interpolantValue = interpolant.evaluate( time )[ 0 ];
- weight *= interpolantValue;
- if ( time > interpolant.parameterPositions[ 1 ] ) {
- this.stopFading();
- if ( interpolantValue === 0 ) {
- // faded out, disable
- this.enabled = false;
- }
- }
- }
- }
- this._effectiveWeight = weight;
- return weight;
- }
- _updateTimeScale( time ) {
- let timeScale = 0;
- if ( ! this.paused ) {
- timeScale = this.timeScale;
- const interpolant = this._timeScaleInterpolant;
- if ( interpolant !== null ) {
- const interpolantValue = interpolant.evaluate( time )[ 0 ];
- timeScale *= interpolantValue;
- if ( time > interpolant.parameterPositions[ 1 ] ) {
- this.stopWarping();
- if ( timeScale === 0 ) {
- // motion has halted, pause
- this.paused = true;
- } else {
- // warp done - apply final time scale
- this.timeScale = timeScale;
- }
- }
- }
- }
- this._effectiveTimeScale = timeScale;
- return timeScale;
- }
- _updateTime( deltaTime ) {
- const duration = this._clip.duration;
- const loop = this.loop;
- let time = this.time + deltaTime;
- let loopCount = this._loopCount;
- const pingPong = ( loop === LoopPingPong );
- if ( deltaTime === 0 ) {
- if ( loopCount === - 1 ) return time;
- return ( pingPong && ( loopCount & 1 ) === 1 ) ? duration - time : time;
- }
- if ( loop === LoopOnce ) {
- if ( loopCount === - 1 ) {
- // just started
- this._loopCount = 0;
- this._setEndings( true, true, false );
- }
- handle_stop: {
- if ( time >= duration ) {
- time = duration;
- } else if ( time < 0 ) {
- time = 0;
- } else {
- this.time = time;
- break handle_stop;
- }
- if ( this.clampWhenFinished ) this.paused = true;
- else this.enabled = false;
- this.time = time;
- this._mixer.dispatchEvent( {
- type: 'finished', action: this,
- direction: deltaTime < 0 ? - 1 : 1
- } );
- }
- } else { // repetitive Repeat or PingPong
- if ( loopCount === - 1 ) {
- // just started
- if ( deltaTime >= 0 ) {
- loopCount = 0;
- this._setEndings( true, this.repetitions === 0, pingPong );
- } else {
- // when looping in reverse direction, the initial
- // transition through zero counts as a repetition,
- // so leave loopCount at -1
- this._setEndings( this.repetitions === 0, true, pingPong );
- }
- }
- if ( time >= duration || time < 0 ) {
- // wrap around
- const loopDelta = Math.floor( time / duration ); // signed
- time -= duration * loopDelta;
- loopCount += Math.abs( loopDelta );
- const pending = this.repetitions - loopCount;
- if ( pending <= 0 ) {
- // have to stop (switch state, clamp time, fire event)
- if ( this.clampWhenFinished ) this.paused = true;
- else this.enabled = false;
- time = deltaTime > 0 ? duration : 0;
- this.time = time;
- this._mixer.dispatchEvent( {
- type: 'finished', action: this,
- direction: deltaTime > 0 ? 1 : - 1
- } );
- } else {
- // keep running
- if ( pending === 1 ) {
- // entering the last round
- const atStart = deltaTime < 0;
- this._setEndings( atStart, ! atStart, pingPong );
- } else {
- this._setEndings( false, false, pingPong );
- }
- this._loopCount = loopCount;
- this.time = time;
- this._mixer.dispatchEvent( {
- type: 'loop', action: this, loopDelta: loopDelta
- } );
- }
- } else {
- this.time = time;
- }
- if ( pingPong && ( loopCount & 1 ) === 1 ) {
- // invert time for the "pong round"
- return duration - time;
- }
- }
- return time;
- }
- _setEndings( atStart, atEnd, pingPong ) {
- const settings = this._interpolantSettings;
- if ( pingPong ) {
- settings.endingStart = ZeroSlopeEnding;
- settings.endingEnd = ZeroSlopeEnding;
- } else {
- // assuming for LoopOnce atStart == atEnd == true
- if ( atStart ) {
- settings.endingStart = this.zeroSlopeAtStart ? ZeroSlopeEnding : ZeroCurvatureEnding;
- } else {
- settings.endingStart = WrapAroundEnding;
- }
- if ( atEnd ) {
- settings.endingEnd = this.zeroSlopeAtEnd ? ZeroSlopeEnding : ZeroCurvatureEnding;
- } else {
- settings.endingEnd = WrapAroundEnding;
- }
- }
- }
- _scheduleFading( duration, weightNow, weightThen ) {
- const mixer = this._mixer, now = mixer.time;
- let interpolant = this._weightInterpolant;
- if ( interpolant === null ) {
- interpolant = mixer._lendControlInterpolant();
- this._weightInterpolant = interpolant;
- }
- const times = interpolant.parameterPositions,
- values = interpolant.sampleValues;
- times[ 0 ] = now;
- values[ 0 ] = weightNow;
- times[ 1 ] = now + duration;
- values[ 1 ] = weightThen;
- return this;
- }
- }
- export { AnimationAction };
|