Spaces:
Running
Running
| 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 }; | |