import { THRESHOLD, ALTERNATE, ALTERNATE_REVERSE, REVERSE, INFINITE, NORMAL, ITERATION_COUNT, DELAY, FILL_MODE, DIRECTION, PLAY_SPEED, DURATION, EASING, ITERATION_TIME, EASING_NAME, PAUSED, RUNNING, PLAY, TIMEUPDATE, ENDED, PLAY_STATE, PREV_TIME, TICK_TIME, CURRENT_TIME, ITERATION, OPTIONS} from "./consts"; import { toFixed, getEasing } from "./utils"; import { splitUnit, isString, camelize, requestAnimationFrame, cancelAnimationFrame } from "@daybrush/utils"; import { IterationCountType, DirectionType, AnimatorState, EasingFunction, FillModeType, PlayStateType, EasingType, AnimatorOptions, AnimatorEvents, } from "./types"; import EventEmitter from "@scena/event-emitter"; import { reactive } from "@cfcs/core"; function GetterSetter {}>( getter: string[], setter: string[], parent: string) { return (constructor: T) => { const prototype = constructor.prototype; getter.forEach(name => { prototype[camelize(`get ${name}`)] = function() { return this[parent][name]; }; }); setter.forEach(name => { prototype[camelize(`set ${name}`)] = function(value: any) { this[parent][name] = value; return this; }; }); }; } export function isDirectionReverse(iteration: number, iteraiontCount: IterationCountType, direction: DirectionType) { if (direction === REVERSE) { return true; } else if (iteraiontCount !== INFINITE && iteration === iteraiontCount && iteraiontCount % 1 === 0) { return direction === (iteration % 2 >= 1 ? ALTERNATE_REVERSE : ALTERNATE); } return direction === (iteration % 2 >= 1 ? ALTERNATE : ALTERNATE_REVERSE); } /** * @typedef {Object} AnimatorState The Animator options. Properties used in css animation. * @property {number} [duration] The duration property defines how long an animation should take to complete one cycle. * @property {"none"|"forwards"|"backwards"|"both"} [fillMode] The fillMode property specifies a style for the element when the animation is not playing (before it starts, after it ends, or both). * @property {"infinite"|number} [iterationCount] The iterationCount property specifies the number of times an animation should be played. * @property {array|function} [easing] The easing(timing-function) specifies the speed curve of an animation. * @property {number} [delay] The delay property specifies a delay for the start of an animation. * @property {"normal"|"reverse"|"alternate"|"alternate-reverse"} [direction] The direction property defines whether an animation should be played forwards, backwards or in alternate cycles. */ export const ANIMATOR_SETTERS = [ "id", ITERATION_COUNT, DELAY, FILL_MODE, DIRECTION, PLAY_SPEED, DURATION, PLAY_SPEED, ITERATION_TIME, PLAY_STATE, ]; export const ANIMATOR_GETTERS = [ ...ANIMATOR_SETTERS, EASING, EASING_NAME, ]; /** * play video, animation, the others * @extends EventEmitter * @see {@link https://www.w3schools.com/css/css3_animations.asp CSS3 Animation} */ @GetterSetter(ANIMATOR_GETTERS, ANIMATOR_SETTERS, "state") class Animator < Options extends AnimatorOptions = AnimatorOptions, State extends AnimatorState = AnimatorState, Events extends {} = {}, > extends EventEmitter { public state: State; private timerId: number = 0; /** * @param - animator's options * @example const animator = new Animator({ delay: 2, diretion: "alternate", duration: 2, fillMode: "forwards", iterationCount: 3, easing: Scene.easing.EASE, }); */ constructor(options?: Partial) { super(); this.state = reactive({ id: "", easing: 0, easingName: "linear", iterationCount: 1, delay: 0, fillMode: "forwards", direction: NORMAL, playSpeed: 1, currentTime: 0, iterationTime: -1, iteration: 0, tickTime: 0, prevTime: 0, playState: PAUSED, duration: 0, } as any) as State; this.setOptions(options); } /** * set animator's easing. * @param curverArray - The speed curve of an animation. * @return {Animator} An instance itself. * @example animator.({ delay: 2, diretion: "alternate", duration: 2, fillMode: "forwards", iterationCount: 3, easing: Scene.easing.EASE, }); */ public setEasing(curveArray: string | number[] | EasingFunction): this { const easing: EasingType = getEasing(curveArray); const easingName = easing && easing[EASING_NAME] || "linear"; const state = this.state; state[EASING] = easing; state[EASING_NAME] = easingName; return this; } /** * set animator's options. * @see {@link https://www.w3schools.com/css/css3_animations.asp|CSS3 Animation} * @param - animator's options * @return {Animator} An instance itself. * @example animator.({ delay: 2, diretion: "alternate", duration: 2, fillMode: "forwards", iterationCount: 3, easing: Scene.eaasing.EASE, }); */ public setOptions(options: Partial = {}): this { for (const name in options) { const value = options[name]; if (name === EASING) { this.setEasing(value); continue; } else if (name === DURATION) { value && this.setDuration(value); continue; } if (OPTIONS.indexOf(name as any) > -1) { this.state[name] = value; } } return this; } /** * Get the animator's total duration including delay * @return {number} Total duration * @example animator.getTotalDuration(); */ public getTotalDuration(): number { return this.getActiveDuration(true); } /** * Get the animator's total duration excluding delay * @return {number} Total duration excluding delay * @example animator.getActiveDuration(); */ public getActiveDuration(delay?: boolean): number { const state = this.state; const count = state[ITERATION_COUNT]; if (count === INFINITE) { return Infinity; } return (delay ? state[DELAY] : 0) + this.getDuration() * count; } /** * Check if the animator has reached the end. * @return {boolean} ended * @example animator.isEnded(); // true or false */ public isEnded(): boolean { if (this.state[TICK_TIME] === 0 && this.state[PLAY_STATE] === PAUSED) { return true; } else if (this.getTime() < this.getActiveDuration()) { return false; } return true; } /** *Check if the animator is paused: * @return {boolean} paused * @example animator.isPaused(); // true or false */ public isPaused(): boolean { return this.state[PLAY_STATE] === PAUSED; } public start(delay: number = this.state[DELAY]): boolean { const state = this.state; state[PLAY_STATE] = RUNNING; if (state[TICK_TIME] >= delay) { /** * This event is fired when play animator. * @event Animator#play */ this.trigger<"play", AnimatorEvents["play"]>(PLAY); return true; } return false; } /** * play animator * @return {Animator} An instance itself. */ public play(toTime?: number) { const state = this.state; const delay = state[DELAY]; const currentTime = this.getTime(); state[PLAY_STATE] = RUNNING; if (this.isEnded() && (currentTime === 0 || currentTime >= this.getActiveDuration())) { this.setTime(-delay, true); } this.timerId = requestAnimationFrame((time: number) => { state[PREV_TIME] = time; this.tick(time, toTime); }); this.start(); return this; } /** * pause animator * @return {Animator} An instance itself. */ public pause(): this { const state = this.state; if (state[PLAY_STATE] !== PAUSED) { state[PLAY_STATE] = PAUSED; /** * This event is fired when animator is paused. * @event Animator#paused */ this.trigger<"paused", AnimatorEvents["paused"]>(PAUSED); } cancelAnimationFrame(this.timerId); return this; } /** * end animator * @return {Animator} An instance itself. */ public finish() { this.setTime(0); this.state[TICK_TIME] = 0; this.end(); return this; } /** * end animator * @return {Animator} An instance itself. */ public end() { this.pause(); /** * This event is fired when animator is ended. * @event Animator#ended */ this.trigger<"ended", AnimatorEvents["ended"]>(ENDED); return this; } /** * set currentTime * @param {Number|String} time - currentTime * @return {Animator} An instance itself. * @example animator.setTime("from"); // 0 animator.setTime("to"); // 100% animator.setTime("50%"); animator.setTime(10); animator.getTime() // 10 */ public setTime(time: number | string, isTick?: boolean, isParent?: boolean, exec?: () => void) { const activeDuration = this.getActiveDuration(); const state = this.state; const prevTime = state[TICK_TIME]; const delay = state[DELAY]; let currentTime = isTick ? (time as number) : this.getUnitTime(time); state[TICK_TIME] = delay + currentTime; if (currentTime < 0) { currentTime = 0; } else if (currentTime > activeDuration) { currentTime = activeDuration; } state[CURRENT_TIME] = currentTime; this.calculate(); const isSelfTick = isTick && !isParent; const tickTime = state[TICK_TIME]; const numericTime = isString(time) ? parseFloat(time) : time; if (isSelfTick && prevTime < delay && numericTime >= 0) { this.start(0); } exec?.(); if (isSelfTick && (tickTime < prevTime || this.isEnded())) { this.end(); return this; } if (this.isDelay()) { return this; } /** * This event is fired when the animator updates the time. * @event Animator#timeupdate * @param {Object} param The object of data to be sent to an event. * @param {Number} param.currentTime The total time that the animator is running. * @param {Number} param.time The iteration time during duration that the animator is running. * @param {Number} param.iterationCount The iteration count that the animator is running. */ this.trigger<"timeupdate", AnimatorEvents["timeupdate"]>(TIMEUPDATE, { currentTime, time: this.getIterationTime(), iterationCount: state[ITERATION], }); return this; } /** * Get the animator's current time * @return {number} current time * @example animator.getTime(); */ public getTime(): number { return this.state[CURRENT_TIME]; } public getUnitTime(time: string | number) { if (isString(time)) { const duration = this.getDuration() || 100; if (time === "from") { return 0; } else if (time === "to") { return duration; } const { unit, value } = splitUnit(time); if (unit === "%") { !this.getDuration() && (this.setDuration(duration)); return toFixed(parseFloat(time) / 100 * duration); } else if (unit === ">") { return value + THRESHOLD; } else { return value; } } else { return toFixed(time); } } /** * Check if the current state of animator is delayed. * @return {boolean} check delay state */ public isDelay() { const state = this.state; const delay = state[DELAY]; const tickTime = state[TICK_TIME]; return delay > 0 && (tickTime < delay); } public setIteration(iterationCount: number): this { const state = this.state; const passIterationCount = Math.floor(iterationCount); const maxIterationCount = state[ITERATION_COUNT] === INFINITE ? Infinity : state[ITERATION_COUNT]; if (state[ITERATION] < passIterationCount && passIterationCount < maxIterationCount) { /** * The event is fired when an iteration of an animation ends. * @event Animator#iteration * @param {Object} param The object of data to be sent to an event. * @param {Number} param.currentTime The total time that the animator is running. * @param {Number} param.iterationCount The iteration count that the animator is running. */ this.trigger<"iteration", AnimatorEvents["iteration"]>(ITERATION, { currentTime: state[CURRENT_TIME], iterationCount: passIterationCount, }); } state[ITERATION] = iterationCount; return this; } protected calculate() { const state = this.state; const iterationCount = state[ITERATION_COUNT]; const fillMode = state[FILL_MODE]; const direction = state[DIRECTION]; const duration = this.getDuration(); const time = this.getTime(); const iteration = duration === 0 ? 0 : time / duration; let currentIterationTime = duration ? time % duration : 0; if (!duration) { this.setIterationTime(0); return this; } this.setIteration(iteration); // direction : normal, reverse, alternate, alternate-reverse // fillMode : forwards, backwards, both, none const isReverse = isDirectionReverse(iteration, iterationCount, direction); const isFiniteDuration = isFinite(duration); if (isFiniteDuration && isReverse) { currentIterationTime = duration - currentIterationTime; } if (isFiniteDuration && iterationCount !== INFINITE) { const isForwards = fillMode === "both" || fillMode === "forwards"; // fill forwards if (iteration >= iterationCount) { currentIterationTime = duration * (isForwards ? (iterationCount % 1) || 1 : 0); isReverse && (currentIterationTime = duration - currentIterationTime); } } this.setIterationTime(currentIterationTime); return this; } private tick(now: number, to?: number) { if (this.isPaused()) { return; } const state = this.state; const playSpeed = state[PLAY_SPEED]; const prevTime = state[PREV_TIME]; const delay = state[DELAY]; const tickTime = state[TICK_TIME]; const currentTime = tickTime + Math.min(1000, now - prevTime) / 1000 * playSpeed; state[PREV_TIME] = now; if (to && to >= currentTime) { this.setTime(to - delay, true); this.pause(); } else { this.setTime(currentTime - delay, true); } if (state[PLAY_STATE] === PAUSED) { return; } this.timerId = requestAnimationFrame((time: number) => { this.tick(time, to); }); } } /** * Specifies the unique indicator of the animator * @method Animator#setId * @param {number | string} - String or number of id to be set in the animator * @return {Animator} An instance itself. */ /** * Specifies the unique indicator of the animator * @method Animator#getId * @return {number | string} the indicator of the item. */ /** * Get a delay for the start of an animation. * @method Animator#getDelay * @return {number} delay */ /** * Set a delay for the start of an animation. * @method Animator#setDelay * @param {number} delay - delay * @return {Animator} An instance itself. */ /** * Get fill mode for the item when the animation is not playing (before it starts, after it ends, or both) * @method Animator#getFillMode * @return {FillModeType} fillMode */ /** * Set fill mode for the item when the animation is not playing (before it starts, after it ends, or both) * @method Animator#setFillMode * @param {FillModeType} fillMode - fillMode * @return {Animator} An instance itself. */ /** * Get the number of times an animation should be played. * @method Animator#getIterationCount * @return {IterationCountType} iterationCount */ /** * Set the number of times an animation should be played. * @method Animator#setIterationCount * @param {IterationCountType} iterationCount - iterationCount * @return {Animator} An instance itself. */ /** * Get whether an animation should be played forwards, backwards or in alternate cycles. * @method Animator#getDirection * @return {DirectionType} direction */ /** * Set whether an animation should be played forwards, backwards or in alternate cycles. * @method Animator#setDirection * @param {DirectionType} direction - direction * @return {Animator} An instance itself. */ /** * Get whether the animation is running or paused. * @method Animator#getPlayState * @return {PlayStateType} playState */ /** * Set whether the animation is running or paused. * @method Animator#setPlayState * @param {PlayStateType} playState - playState * @return {Animator} An instance itself. */ /** * Get the animator's play speed * @method Animator#getPlaySpeed * @return {number} playSpeed */ /** * Set the animator's play speed * @method Animator#setPlaySpeed * @param {number} playSpeed - playSpeed * @return {Animator} An instance itself. */ /** * Get how long an animation should take to complete one cycle. * @method Animator#getDuration * @return {number} duration */ /** * Set how long an animation should take to complete one cycle. * @method Animator#setDuration * @param {number} duration - duration * @return {Animator} An instance itself. */ /** * Get the speed curve of an animation. * @method Animator#getEasing * @return {EasingType} easing */ /** * Get the speed curve's name * @method Animator#getEasingName * @return {string} the curve's name. */ /** * Get the animator's current iteration time * @method Animator#getIterationTime * @return {number} current iteration time * @example animator.getIterationTime(); */ // tslint:disable-next-line:interface-name interface Animator < Options extends AnimatorOptions = AnimatorOptions, State extends AnimatorState = AnimatorState, Events extends {} = {}, > extends EventEmitter { setId(id: number | string): this; getId(): number | string; getIterationTime(): number; setIterationTime(time: number): this; setDelay(delay: number): this; getDelay(): number; setFillMode(fillMode: FillModeType): this; getFillMode(): FillModeType; setIterationCount(iterationCount: IterationCountType): this; getIterationCount(): IterationCountType; setDirection(direction: DirectionType): this; getDirection(): DirectionType; setPlayState(playState: PlayStateType): this; getPlayState(): PlayStateType; setPlaySpeed(playSpeed: number): this; getPlaySpeed(): number; setDuration(duration: number): this; getDuration(): number; getEasing(): EasingType; getEasingName(): string; } export default Animator;