import { WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, LoopPingPong, LoopOnce, LoopRepeat, NormalAnimationBlendMode, AdditiveAnimationBlendMode } from '../constants.js'; class AnimationAction { constructor( mixer, clip, localRoot = null, blendMode = clip.blendMode ) { this._mixer = mixer; this._clip = clip; this._localRoot = localRoot; this.blendMode = 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 };