/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/utils/animation.js
380 строк
10 KB
Shihua Zheng
commit (#39097)
06 июн 2023, 02:37
Не верифицирован
06 июн 2023, 02:37
649e4ac
Код
Авторство
О чём код?
import {getCurve} from '#core/data-structures/curve'; import {Deferred} from '#core/data-structures/promise'; /** @typedef {import('#core/types/date').NormTimeDef} NormTimeDef */ /** @typedef {import('#core/types/date').TimestampDef} TimestampDef */ import {Services} from '#service'; import {dev} from '#utils/log'; const TAG_ = 'Animation'; const NOOP_CALLBACK = function () {}; /** * The animation class allows construction of arbitrary animation processes. * The main method is "add" that adds a segment of animation at particular * time offset (delay) and duration. All animation segments are simply functions * of type Transition which are iterated from 0 to 1 in animation frames to * achieve the desired effect. */ export class Animation { /** * Creates and starts animation with a single segment. Returns AnimationPlayer * object that can be used to monitor or control animation. * * @param {!Node} contextNode The context node. * @param {!TransitionDef<?>} transition Transition to animate. * @param {TimestampDef} duration Duration in milliseconds. * @param {(!./core/data-structures/curve.CurveDef|string)=} opt_curve Optional curve to use for * animation. Default is the linear animation. * @return {!AnimationPlayer} */ static animate(contextNode, transition, duration, opt_curve) { return new Animation(contextNode) .setCurve(opt_curve) .add(0, transition, 1) .start(duration); } /** * @param {!Node} contextNode * @param {!./service/vsync-impl.Vsync=} opt_vsync */ constructor(contextNode, opt_vsync) { /** @private @const {!Node} */ this.contextNode_ = contextNode; /** @private @const {!./service/vsync-impl.Vsync} */ this.vsync_ = opt_vsync || Services.vsyncFor(self); /** @private {?./core/data-structures/curve.CurveDef} */ this.curve_ = null; /** * @private @const {!Array<!SegmentDef>} */ this.segments_ = []; } /** * Sets the default curve for the animation. Each segment is allowed to have * its own curve, but this curve will be used if a segment doesn't specify * its own. * @param {!./core/data-structures/curve.CurveDef|string|undefined} curve * @return {!Animation} */ setCurve(curve) { if (curve) { this.curve_ = getCurve(curve); } return this; } /** * Adds a segment to the animation. Each segment starts at offset (delay) and * runs for a portion of the overall animation (duration). Note that both * delay and duration and NormTimeDef types which accept values from 0 to 1. * Optionally, the time is pushed through a curve. If curve is not specified, * the default animation curve will be used. The specified transition is * animated over the specified duration from 0 to 1. * * @param {!NormTimeDef} delay * @param {!TransitionDef<?>} transition * @param {!NormTimeDef} duration * @param {(!./core/data-structures/curve.CurveDef|string)=} opt_curve * @return {!Animation} */ add(delay, transition, duration, opt_curve) { this.segments_.push({ delay, func: transition, duration, curve: getCurve(opt_curve), }); return this; } /** * Starts the animation and returns the AnimationPlayer object that can be * used to monitor and control the animation. * * @param {!TimestampDef} duration Absolute time in milliseconds. * @return {!AnimationPlayer} */ start(duration) { const player = new AnimationPlayer( this.vsync_, this.contextNode_, this.segments_, this.curve_, duration ); return player; } } /** * AnimationPlayer allows tracking and monitoring of the running animation. * Most importantly it exposes methods "then" and "thenAlways" that have the * semantics of a Promise and signal when the animation completed or failed. * Additionally, it exposes the method "halt" which allows to stop/reset the * animation. * // TODO(@cramforce) Actually fully implement. * implements {IThenable} */ class AnimationPlayer { /** * @param {!./service/vsync-impl.Vsync} vsync * @param {!Node} contextNode * @param {!Array<!SegmentDef>} segments * @param {?./core/data-structures/curve.CurveDef} defaultCurve * @param {!TimestampDef} duration */ constructor(vsync, contextNode, segments, defaultCurve, duration) { /** @private @const {!./service/vsync-impl.Vsync} */ this.vsync_ = vsync; /** @private @const {!Node} */ this.contextNode_ = contextNode; /** @private @const {!Array<!SegmentRuntimeDef>} */ this.segments_ = []; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; this.segments_.push({ delay: segment.delay, func: segment.func, duration: segment.duration, curve: segment.curve || defaultCurve, started: false, completed: false, }); } /** @private @const */ this.duration_ = duration; /** @private {!TimestampDef} */ this.startTime_ = Date.now(); /** @private {!NormTimeDef} */ // this.normLinearTime_ = 0; /** @private {!NormTimeDef} */ // this.normTime_ = 0; /** @private {boolean} */ this.running_ = true; /** @private {!{[key: string]: *}} */ this.state_ = {}; const deferred = new Deferred(); /** @const @private */ this.promise_ = deferred.promise; /** @const @private */ this.resolve_ = deferred.resolve; /** @const @private */ this.reject_ = deferred.reject; /** @const */ this.task_ = this.vsync_.createAnimTask(this.contextNode_, { mutate: this.stepMutate_.bind(this), }); if (this.vsync_.canAnimate(this.contextNode_)) { this.task_(this.state_); } else { dev().warn(TAG_, 'cannot animate'); this.complete_(/* success */ false, /* dir */ 0); } } /** * Chains to the animation's promise that will resolve when the animation has * completed or will reject if animation has failed or was interrupted. * @param {!Function=} opt_resolve * @param {!Function=} opt_reject * @return {!Promise} */ then(opt_resolve, opt_reject) { if (!opt_resolve && !opt_reject) { return this.promise_; } return this.promise_.then(opt_resolve, opt_reject); } /** * Callback for regardless whether the animation succeeds or fails. * @param {!Function=} opt_callback * @return {!Promise} */ thenAlways(opt_callback) { const callback = opt_callback || NOOP_CALLBACK; return this.then(callback, callback); } /** * Halts the animation. Depending on the opt_dir value, the following actions * can be performed: * 0: No action. The state will be as at the moment of halting (default) * 1: Final state. Transitionable will be set to state = 1. * -1: Reset state. Transitionable will be reset to state = 0. * The animation's promise will be rejected since the transition has been * interrupted. * @param {number=} opt_dir */ halt(opt_dir) { this.complete_(/* success */ false, /* dir */ opt_dir || 0); } /** * @param {boolean} success * @param {number} dir * @private */ complete_(success, dir) { if (!this.running_) { return; } this.running_ = false; if (dir != 0) { // Sort in the completion order. if (this.segments_.length > 1) { this.segments_.sort((s1, s2) => { return s1.delay + s1.duration - (s2.delay + s2.duration); }); } try { if (dir > 0) { // Natural order - all set to 1. for (let i = 0; i < this.segments_.length; i++) { this.segments_[i].func(1, true); } } else { // Reverse order - all set to 0. for (let i = this.segments_.length - 1; i >= 0; i--) { this.segments_[i].func(0, false); } } } catch (e) { dev().error(TAG_, 'completion failed: ' + e, e); success = false; } } if (success) { this.resolve_(); } else { this.reject_(); } } /** * @param {!{[key: string]: *}} unusedState * @private */ stepMutate_(unusedState) { if (!this.running_) { return; } const currentTime = Date.now(); const normLinearTime = Math.min( (currentTime - this.startTime_) / this.duration_, 1 ); // Start segments due to be started for (let i = 0; i < this.segments_.length; i++) { const segment = this.segments_[i]; if (!segment.started && normLinearTime >= segment.delay) { segment.started = true; } } // Execute all pending segments. for (let i = 0; i < this.segments_.length; i++) { const segment = this.segments_[i]; if (!segment.started || segment.completed) { continue; } this.mutateSegment_(segment, normLinearTime); } // Complete or start next cycle. if (normLinearTime == 1) { this.complete_(/* success */ true, /* dir */ 0); } else { if (this.vsync_.canAnimate(this.contextNode_)) { this.task_(this.state_); } else { dev().warn(TAG_, 'cancel animation'); this.complete_(/* success */ false, /* dir */ 0); } } } /** * @param {!SegmentRuntimeDef} segment * @param {number} totalLinearTime */ mutateSegment_(segment, totalLinearTime) { let normLinearTime; let normTime; if (segment.duration > 0) { normLinearTime = Math.min( (totalLinearTime - segment.delay) / segment.duration, 1 ); normTime = normLinearTime; if (segment.curve && normTime != 1) { try { normTime = segment.curve(normLinearTime); } catch (e) { dev().error(TAG_, 'step curve failed: ' + e, e); this.complete_(/* success */ false, /* dir */ 0); return; } } } else { normLinearTime = 1; normTime = 1; } if (normLinearTime == 1) { segment.completed = true; } try { segment.func(normTime, segment.completed); } catch (e) { dev().error(TAG_, 'step mutate failed: ' + e, e); this.complete_(/* success */ false, /* dir */ 0); return; } } } /** * @typedef {{ * delay: NormTimeDef, * func: !TransitionDef, * duration: NormTimeDef, * curve: ?./core/data-structures/curve.CurveDef * }} */ let SegmentDef; /** * @typedef {{ * delay: NormTimeDef, * func: !TransitionDef, * duration: NormTimeDef, * curve: ?./core/data-structures/curve.CurveDef, * started: boolean, * completed: boolean * }} */ let SegmentRuntimeDef;