Spaces:
Running
Running
| import { WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, LoopPingPong, LoopOnce, LoopRepeat } from '../constants.js'; | |
| /** | |
| * | |
| * Action provided by AnimationMixer for scheduling clip playback on specific | |
| * objects. | |
| * | |
| * @author Ben Houston / http://clara.io/ | |
| * @author David Sarno / http://lighthaus.us/ | |
| * @author tschw | |
| * | |
| */ | |
| function AnimationAction( mixer, clip, localRoot ) { | |
| this._mixer = mixer; | |
| this._clip = clip; | |
| this._localRoot = localRoot || null; | |
| var tracks = clip.tracks, | |
| nTracks = tracks.length, | |
| interpolants = new Array( nTracks ); | |
| var interpolantSettings = { | |
| endingStart: ZeroCurvatureEnding, | |
| endingEnd: ZeroCurvatureEnding | |
| }; | |
| for ( var i = 0; i !== nTracks; ++ i ) { | |
| var 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 | |
| } | |
| Object.assign( AnimationAction.prototype, { | |
| // State & Scheduling | |
| play: function () { | |
| this._mixer._activateAction( this ); | |
| return this; | |
| }, | |
| stop: function () { | |
| this._mixer._deactivateAction( this ); | |
| return this.reset(); | |
| }, | |
| reset: function () { | |
| 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: function () { | |
| return this.enabled && ! this.paused && this.timeScale !== 0 && | |
| this._startTime === null && this._mixer._isActiveAction( this ); | |
| }, | |
| // return true when play has been called | |
| isScheduled: function () { | |
| return this._mixer._isActiveAction( this ); | |
| }, | |
| startAt: function ( time ) { | |
| this._startTime = time; | |
| return this; | |
| }, | |
| setLoop: function ( 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: function ( 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: function () { | |
| return this._effectiveWeight; | |
| }, | |
| fadeIn: function ( duration ) { | |
| return this._scheduleFading( duration, 0, 1 ); | |
| }, | |
| fadeOut: function ( duration ) { | |
| return this._scheduleFading( duration, 1, 0 ); | |
| }, | |
| crossFadeFrom: function ( fadeOutAction, duration, warp ) { | |
| fadeOutAction.fadeOut( duration ); | |
| this.fadeIn( duration ); | |
| if ( warp ) { | |
| var 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: function ( fadeInAction, duration, warp ) { | |
| return fadeInAction.crossFadeFrom( this, duration, warp ); | |
| }, | |
| stopFading: function () { | |
| var 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: function ( timeScale ) { | |
| this.timeScale = timeScale; | |
| this._effectiveTimeScale = this.paused ? 0 : timeScale; | |
| return this.stopWarping(); | |
| }, | |
| // return the time scale considering warping and .paused | |
| getEffectiveTimeScale: function () { | |
| return this._effectiveTimeScale; | |
| }, | |
| setDuration: function ( duration ) { | |
| this.timeScale = this._clip.duration / duration; | |
| return this.stopWarping(); | |
| }, | |
| syncWith: function ( action ) { | |
| this.time = action.time; | |
| this.timeScale = action.timeScale; | |
| return this.stopWarping(); | |
| }, | |
| halt: function ( duration ) { | |
| return this.warp( this._effectiveTimeScale, 0, duration ); | |
| }, | |
| warp: function ( startTimeScale, endTimeScale, duration ) { | |
| var mixer = this._mixer, now = mixer.time, | |
| interpolant = this._timeScaleInterpolant, | |
| timeScale = this.timeScale; | |
| if ( interpolant === null ) { | |
| interpolant = mixer._lendControlInterpolant(); | |
| this._timeScaleInterpolant = interpolant; | |
| } | |
| var times = interpolant.parameterPositions, | |
| values = interpolant.sampleValues; | |
| times[ 0 ] = now; | |
| times[ 1 ] = now + duration; | |
| values[ 0 ] = startTimeScale / timeScale; | |
| values[ 1 ] = endTimeScale / timeScale; | |
| return this; | |
| }, | |
| stopWarping: function () { | |
| var timeScaleInterpolant = this._timeScaleInterpolant; | |
| if ( timeScaleInterpolant !== null ) { | |
| this._timeScaleInterpolant = null; | |
| this._mixer._takeBackControlInterpolant( timeScaleInterpolant ); | |
| } | |
| return this; | |
| }, | |
| // Object Accessors | |
| getMixer: function () { | |
| return this._mixer; | |
| }, | |
| getClip: function () { | |
| return this._clip; | |
| }, | |
| getRoot: function () { | |
| return this._localRoot || this._mixer._root; | |
| }, | |
| // Interna | |
| _update: function ( time, deltaTime, timeDirection, accuIndex ) { | |
| // called by the mixer | |
| if ( ! this.enabled ) { | |
| // call ._updateWeight() to update ._effectiveWeight | |
| this._updateWeight( time ); | |
| return; | |
| } | |
| var startTime = this._startTime; | |
| if ( startTime !== null ) { | |
| // check for scheduled start of action | |
| var 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 ); | |
| var clipTime = this._updateTime( deltaTime ); | |
| // note: _updateTime may disable the action resulting in | |
| // an effective weight of 0 | |
| var weight = this._updateWeight( time ); | |
| if ( weight > 0 ) { | |
| var interpolants = this._interpolants; | |
| var propertyMixers = this._propertyBindings; | |
| for ( var j = 0, m = interpolants.length; j !== m; ++ j ) { | |
| interpolants[ j ].evaluate( clipTime ); | |
| propertyMixers[ j ].accumulate( accuIndex, weight ); | |
| } | |
| } | |
| }, | |
| _updateWeight: function ( time ) { | |
| var weight = 0; | |
| if ( this.enabled ) { | |
| weight = this.weight; | |
| var interpolant = this._weightInterpolant; | |
| if ( interpolant !== null ) { | |
| var 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: function ( time ) { | |
| var timeScale = 0; | |
| if ( ! this.paused ) { | |
| timeScale = this.timeScale; | |
| var interpolant = this._timeScaleInterpolant; | |
| if ( interpolant !== null ) { | |
| var 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: function ( deltaTime ) { | |
| var time = this.time + deltaTime; | |
| var duration = this._clip.duration; | |
| var loop = this.loop; | |
| var loopCount = this._loopCount; | |
| var 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 break handle_stop; | |
| if ( this.clampWhenFinished ) this.paused = true; | |
| else this.enabled = false; | |
| 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 | |
| var loopDelta = Math.floor( time / duration ); // signed | |
| time -= duration * loopDelta; | |
| loopCount += Math.abs( loopDelta ); | |
| var 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._mixer.dispatchEvent( { | |
| type: 'finished', action: this, | |
| direction: deltaTime > 0 ? 1 : - 1 | |
| } ); | |
| } else { | |
| // keep running | |
| if ( pending === 1 ) { | |
| // entering the last round | |
| var atStart = deltaTime < 0; | |
| this._setEndings( atStart, ! atStart, pingPong ); | |
| } else { | |
| this._setEndings( false, false, pingPong ); | |
| } | |
| this._loopCount = loopCount; | |
| this._mixer.dispatchEvent( { | |
| type: 'loop', action: this, loopDelta: loopDelta | |
| } ); | |
| } | |
| } | |
| if ( pingPong && ( loopCount & 1 ) === 1 ) { | |
| // invert time for the "pong round" | |
| this.time = time; | |
| return duration - time; | |
| } | |
| } | |
| this.time = time; | |
| return time; | |
| }, | |
| _setEndings: function ( atStart, atEnd, pingPong ) { | |
| var 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: function ( duration, weightNow, weightThen ) { | |
| var mixer = this._mixer, now = mixer.time, | |
| interpolant = this._weightInterpolant; | |
| if ( interpolant === null ) { | |
| interpolant = mixer._lendControlInterpolant(); | |
| this._weightInterpolant = interpolant; | |
| } | |
| var times = interpolant.parameterPositions, | |
| values = interpolant.sampleValues; | |
| times[ 0 ] = now; | |
| values[ 0 ] = weightNow; | |
| times[ 1 ] = now + duration; | |
| values[ 1 ] = weightThen; | |
| return this; | |
| } | |
| } ); | |
| export { AnimationAction }; | |