UNPKG

@openhps/core

Version:

Open Hybrid Positioning System - Core component

691 lines (631 loc) 20.5 kB
import { WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, LoopPingPong, LoopOnce, LoopRepeat, NormalAnimationBlendMode, AdditiveAnimationBlendMode } from '../constants.js'; /** * An instance of `AnimationAction` schedules the playback of an animation which is * stored in {@link AnimationClip}. */ class AnimationAction { /** * Constructs a new animation action. * * @param {AnimationMixer} mixer - The mixer that is controlled by this action. * @param {AnimationClip} clip - The animation clip that holds the actual keyframes. * @param {?Object3D} [localRoot=null] - The root object on which this action is performed. * @param {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)} [blendMode] - The blend mode. */ constructor(mixer, clip, localRoot = null, blendMode = clip.blendMode) { this._mixer = mixer; this._clip = clip; this._localRoot = localRoot; /** * Defines how the animation is blended/combined when two or more animations * are simultaneously played. * * @type {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)} */ 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; /** * The loop mode, set via {@link AnimationAction#setLoop}. * * @type {(LoopRepeat|LoopOnce|LoopPingPong)} * @default LoopRepeat */ 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; /** * The local time of this action (in seconds, starting with `0`). * * The value gets clamped or wrapped to `[0,clip.duration]` (according to the * loop state). * * @type {number} * @default Infinity */ this.time = 0; /** * Scaling factor for the {@link AnimationAction#time}. A value of `0` causes the * animation to pause. Negative values cause the animation to play backwards. * * @type {number} * @default 1 */ this.timeScale = 1; this._effectiveTimeScale = 1; /** * The degree of influence of this action (in the interval `[0, 1]`). Values * between `0` (no impact) and `1` (full impact) can be used to blend between * several actions. * * @type {number} * @default 1 */ this.weight = 1; this._effectiveWeight = 1; /** * The number of repetitions of the performed clip over the course of this action. * Can be set via {@link AnimationAction#setLoop}. * * Setting this number has no effect if {@link AnimationAction#loop} is set to * `THREE:LoopOnce`. * * @type {number} * @default Infinity */ this.repetitions = Infinity; /** * If set to `true`, the playback of the action is paused. * * @type {boolean} * @default false */ this.paused = false; /** * If set to `false`, the action is disabled so it has no impact. * * When the action is re-enabled, the animation continues from its current * time (setting `enabled` to `false` doesn't reset the action). * * @type {boolean} * @default true */ this.enabled = true; /** * If set to true the animation will automatically be paused on its last frame. * * If set to false, {@link AnimationAction#enabled} will automatically be switched * to `false` when the last loop of the action has finished, so that this action has * no further impact. * * Note: This member has no impact if the action is interrupted (it * has only an effect if its last loop has really finished). * * @type {boolean} * @default false */ this.clampWhenFinished = false; /** * Enables smooth interpolation without separate clips for start, loop and end. * * @type {boolean} * @default true */ this.zeroSlopeAtStart = true; /** * Enables smooth interpolation without separate clips for start, loop and end. * * @type {boolean} * @default true */ this.zeroSlopeAtEnd = true; } /** * Starts the playback of the animation. * * @return {AnimationAction} A reference to this animation action. */ play() { this._mixer._activateAction(this); return this; } /** * Stops the playback of the animation. * * @return {AnimationAction} A reference to this animation action. */ stop() { this._mixer._deactivateAction(this); return this.reset(); } /** * Resets the playback of the animation. * * @return {AnimationAction} A reference to this animation action. */ 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(); } /** * Returns `true` if the animation is running. * * @return {boolean} Whether the animation is running or not. */ isRunning() { return this.enabled && !this.paused && this.timeScale !== 0 && this._startTime === null && this._mixer._isActiveAction(this); } /** * Returns `true` when {@link AnimationAction#play} has been called. * * @return {boolean} Whether the animation is scheduled or not. */ isScheduled() { return this._mixer._isActiveAction(this); } /** * Defines the time when the animation should start. * * @param {number} time - The start time in seconds. * @return {AnimationAction} A reference to this animation action. */ startAt(time) { this._startTime = time; return this; } /** * Configures the loop settings for this action. * * @param {(LoopRepeat|LoopOnce|LoopPingPong)} mode - The loop mode. * @param {number} repetitions - The number of repetitions. * @return {AnimationAction} A reference to this animation action. */ setLoop(mode, repetitions) { this.loop = mode; this.repetitions = repetitions; return this; } /** * Sets the effective weight of this action. * * An action has no effect and thus an effective weight of zero when the * action is disabled. * * @param {number} weight - The weight to set. * @return {AnimationAction} A reference to this animation action. */ setEffectiveWeight(weight) { this.weight = weight; // note: same logic as when updated at runtime this._effectiveWeight = this.enabled ? weight : 0; return this.stopFading(); } /** * Returns the effective weight of this action. * * @return {number} The effective weight. */ getEffectiveWeight() { return this._effectiveWeight; } /** * Fades the animation in by increasing its weight gradually from `0` to `1`, * within the passed time interval. * * @param {number} duration - The duration of the fade. * @return {AnimationAction} A reference to this animation action. */ fadeIn(duration) { return this._scheduleFading(duration, 0, 1); } /** * Fades the animation out by decreasing its weight gradually from `1` to `0`, * within the passed time interval. * * @param {number} duration - The duration of the fade. * @return {AnimationAction} A reference to this animation action. */ fadeOut(duration) { return this._scheduleFading(duration, 1, 0); } /** * Causes this action to fade in and the given action to fade out, * within the passed time interval. * * @param {AnimationAction} fadeOutAction - The animation action to fade out. * @param {number} duration - The duration of the fade. * @param {boolean} [warp=false] - Whether warping should be used or not. * @return {AnimationAction} A reference to this animation action. */ crossFadeFrom(fadeOutAction, duration, warp = false) { fadeOutAction.fadeOut(duration); this.fadeIn(duration); if (warp === true) { 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; } /** * Causes this action to fade out and the given action to fade in, * within the passed time interval. * * @param {AnimationAction} fadeInAction - The animation action to fade in. * @param {number} duration - The duration of the fade. * @param {boolean} [warp=false] - Whether warping should be used or not. * @return {AnimationAction} A reference to this animation action. */ crossFadeTo(fadeInAction, duration, warp = false) { return fadeInAction.crossFadeFrom(this, duration, warp); } /** * Stops any fading which is applied to this action. * * @return {AnimationAction} A reference to this animation action. */ stopFading() { const weightInterpolant = this._weightInterpolant; if (weightInterpolant !== null) { this._weightInterpolant = null; this._mixer._takeBackControlInterpolant(weightInterpolant); } return this; } /** * Sets the effective time scale of this action. * * An action has no effect and thus an effective time scale of zero when the * action is paused. * * @param {number} timeScale - The time scale to set. * @return {AnimationAction} A reference to this animation action. */ setEffectiveTimeScale(timeScale) { this.timeScale = timeScale; this._effectiveTimeScale = this.paused ? 0 : timeScale; return this.stopWarping(); } /** * Returns the effective time scale of this action. * * @return {number} The effective time scale. */ getEffectiveTimeScale() { return this._effectiveTimeScale; } /** * Sets the duration for a single loop of this action. * * @param {number} duration - The duration to set. * @return {AnimationAction} A reference to this animation action. */ setDuration(duration) { this.timeScale = this._clip.duration / duration; return this.stopWarping(); } /** * Synchronizes this action with the passed other action. * * @param {AnimationAction} action - The action to sync with. * @return {AnimationAction} A reference to this animation action. */ syncWith(action) { this.time = action.time; this.timeScale = action.timeScale; return this.stopWarping(); } /** * Decelerates this animation's speed to `0` within the passed time interval. * * @param {number} duration - The duration. * @return {AnimationAction} A reference to this animation action. */ halt(duration) { return this.warp(this._effectiveTimeScale, 0, duration); } /** * Changes the playback speed, within the passed time interval, by modifying * {@link AnimationAction#timeScale} gradually from `startTimeScale` to * `endTimeScale`. * * @param {number} startTimeScale - The start time scale. * @param {number} endTimeScale - The end time scale. * @param {number} duration - The duration. * @return {AnimationAction} A reference to this animation action. */ 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; } /** * Stops any scheduled warping which is applied to this action. * * @return {AnimationAction} A reference to this animation action. */ stopWarping() { const timeScaleInterpolant = this._timeScaleInterpolant; if (timeScaleInterpolant !== null) { this._timeScaleInterpolant = null; this._mixer._takeBackControlInterpolant(timeScaleInterpolant); } return this; } /** * Returns the animation mixer of this animation action. * * @return {AnimationMixer} The animation mixer. */ getMixer() { return this._mixer; } /** * Returns the animation clip of this animation action. * * @return {AnimationClip} The animation clip. */ getClip() { return this._clip; } /** * Returns the root object of this animation action. * * @return {Object3D} The root object. */ 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) { deltaTime = 0; } else { 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 };