packages/scenejs/src/Scene.ts

import Animator from "./Animator";
import SceneItem from "./SceneItem";
import { SELECTOR, DURATION, DELAY, RUNNING, NAME_SEPARATOR } from "./consts";
import {
    playCSS, getRealId, isPausedCSS,
    isEndedCSS, setPlayCSS, isScene, flatSceneObject,
    isSceneItem,
} from "./utils";
import { isFunction, IS_WINDOW, IObject, $, IArrayFormat } from "@daybrush/utils";
import {
    AnimateElement, SceneState, SceneOptions, EasingType,
    AnimatorState, SceneItemOptions, PlayCondition,
    NameType, SceneEvents, SelectorAllType
} from "./types";
import Frame from "./Frame";
import OrderMap from "order-map";
import styled, { InjectResult, StyledInjector } from "css-styled";
/**
 * manage sceneItems and play Scene.
 * @extends Animator
 * @sort 1
 */
class Scene extends Animator<SceneOptions, SceneState, SceneEvents> {
    /**
    * version info
    * @type {string}
    * @example
    * Scene.VERSION // #__VERSION__#
    */
    public static VERSION: string = "#__VERSION__#";
    public items: IObject<Scene | SceneItem> = {};
    public orderMap = new OrderMap(NAME_SEPARATOR);
    public styled: StyledInjector;
    public styledInjector: InjectResult;
    public temp: IObject<Frame>;
    /**
    * @param - properties
    * @param - options
    * @example
    const scene = new Scene({
      item1: {
        0: {
          display: "none",
        },
        1: {
          display: "block",
          opacity: 0,
        },
        2: {
          opacity: 1,
        },
      },
      item2: {
        2: {
          opacity: 1,
        },
      }
    });
      */
    constructor(properties?: { options?: Partial<SceneOptions> } & IObject<any>, options?: Partial<SceneOptions>) {
        super();
        this.load(properties, options);
    }
    public getDuration() {
        let time = 0;

        this.forEach(item => {
            time = Math.max(time, item.getTotalDuration() / item.getPlaySpeed());
        });
        return time || this.state[DURATION];
    }
    public setDuration(duration: number) {
        const items = this.items;
        const sceneDuration = this.getDuration();

        if (duration === 0 || !isFinite(sceneDuration)) {
            return this;
        }
        if (sceneDuration === 0) {
            this.forEach(item => {
                item.setDuration(duration);
            });
        } else {
            const ratio = duration / sceneDuration;

            this.forEach(item => {
                item.setDelay(item.getDelay() * ratio);
                item.setDuration(item.getDuration() * ratio);
            });
        }
        super.setDuration(duration);
        return this;
    }
    public getItem<T extends (Scene | SceneItem) = Scene | SceneItem>(name: number | string): T;
    /**
    * get item in scene by name
    * @param - The item's name
    * @return {Scene | SceneItem} item
    * @example
    const item = scene.getItem("item1")
    */
    public getItem(name: number | string) {
        return this.items[name];
    }
    /**
    * create item in scene
    * @param {} name - name of item to create
    * @param {} options - The option object of SceneItem
    * @return {} Newly created item
    * @example
    const item = scene.newItem("item1")
    */
    public newItem(name: number | string, options: Partial<SceneItemOptions> = {}): Scene | SceneItem {
        if (this.items[name]) {
            return this.items[name];
        }
        const item = new SceneItem();

        this.setItem(name, item);
        item.setOptions(options);

        return item;
    }
    /**
    * remove item in scene
    * @param - name of item to remove
    * @return  An instance itself
    * @example
    const item = scene.newItem("item1")

    scene.removeItem("item1");
    */
    public removeItem(name: number | string): this {
        delete this.items[name];

        this.orderMap.remove([name]);
        return this;
    }
    /**
    * add a sceneItem to the scene
    * @param - name of item to create
    * @param - sceneItem
    * @example
    const item = scene.newItem("item1")
    */
    public setItem(name: number | string, item: Scene | SceneItem) {
        item.setId(name);
        this.items[name] = item;

        this.orderMap.add([name]);
        return this;
    }
    /**
    * Get the current computed frames.
    * (If needUpdate is true, get a new computed frames, not the temp that has already been saved.)
    */
    public getCurrentFrames(needUpdate?: boolean, parentEasing?: EasingType) {
        const easing = this.getEasing() || parentEasing;
        const frames: IObject<any> = {};

        this.forEach(item => {
            const id = item.getId();

            if (isScene(item)) {
                frames[id] = item.getCurrentFrames(needUpdate, easing);
            } else {
                frames[id] = item.getCurrentFrame(needUpdate, easing);
            }
        });
        this.temp = frames;

        return frames;
    }
    /**
   * Get the current flatted computed frames.
   * (If needUpdate is true, get a new computed frames, not the temp that has already been saved.)
   * If there is a scene in the scene, you can get a flatted frame map.
   * @example
   * import Scene, { NAME_SEPARATOR } from "scenejs";
   *
   * {
   *   "a": Frame,
   *   "b": {
   *     "b1": Frame,
   *     "b2": Frame,
   *   },
   * }
   * const frames = scene.getCurrentFrames();
   * {
   *   "a": Frame,
   *   "b_///_b1": Frame,
   *   "b_///_b2": Frame,
   * }
   * const frames = scene.getCurrentFlattedFrames();
   *
   */
    public getCurrentFlattedFrames(needUpdate?: boolean, parentEasing?: EasingType): Record<string, Frame> {
        const frames = this.getCurrentFrames(needUpdate, parentEasing);

        return flatSceneObject(frames, NAME_SEPARATOR);
    }
    public setTime(time: number | string, isTick?: boolean, isParent?: boolean, parentEasing?: EasingType) {
        super.setTime(time, isTick, isParent, () => {
            const iterationTime = this.getIterationTime();
            const easing = this.getEasing() || parentEasing;

            this.forEach(item => {
                item.setTime(iterationTime * item.getPlaySpeed() - item.getDelay(), isTick, true, easing);
            });

            const frames = this.getCurrentFrames(false, parentEasing);

            /**
             * This event is fired when timeupdate and animate.
             * @event Scene#animate
             * @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 {object} param.frames frames of that time.
             * @example
const scene = new Scene({
    a: {
        0: {
            opacity: 0,
        },
        1: {
            opacity: 1,
        }
    },
    b: {
        0: {
            opacity: 0,
        },
        1: {
            opacity: 1,
        }
    }
}).on("animate", e => {
    console.log(e.frames);
    // {a: Frame, b: Frame}
    console.log(e.frames.a.get("opacity"));
});
                 */
            this.trigger("animate", {
                frames,
                currentTime: this.getTime(),
                time: iterationTime,
            });
        });
        return this;
    }
    /**
     * executes a provided function once for each scene item.
     * @param - Function to execute for each element, taking three arguments
     * @return {Scene} An instance itself
     */
    public forEach(
        func: (
            item: Scene | SceneItem,
            id: string | number,
            index: number,
            items: IObject<Scene | SceneItem>,
        ) => void,
    ) {
        const items = this.items;
        this.getOrders().forEach((id, index) => {
            func(items[id], id, index, items);
        });
        return this;
    }
    public toCSS(
        playCondition?: PlayCondition,
        duration: number = this.getDuration(), parentStates: AnimatorState[] = []) {
        const totalDuration = !duration || !isFinite(duration) ? 0 : duration;
        const styles: string[] = [];
        const state = this.state;

        state[DURATION] = this.getDuration();

        this.forEach(item => {
            styles.push(item.toCSS(playCondition, totalDuration, parentStates.concat(state)));
        });
        return styles.join("");
    }
    /**
     * Export the CSS of the items to the style.
     * @param - Add a selector or className to play.
     * @return {Scene} An instance itself
     */
    public exportCSS(
        playCondition?: PlayCondition, duration?: number, parentStates?: AnimatorState[]) {
        const css = this.toCSS(playCondition, duration, parentStates);

        if (!parentStates || !parentStates.length) {
            if (this.styledInjector) {
                this.styledInjector.destroy();
                this.styledInjector = null;
            }
            this.styled = styled(css);
            this.styledInjector = this.styled.inject(this.getAnimationElement(), { original: true });
            // && exportCSS(getRealId(this), css);
        }
        return this;
    }
    public append(item: SceneItem | Scene) {
        item.setDelay(item.getDelay() + this.getDuration());
        this.setItem(getRealId(item), item);
    }
    public pauseCSS() {
        return this.forEach(item => {
            item.pauseCSS();
        });
    }
    public pause() {
        super.pause();

        isPausedCSS(this) && this.pauseCSS();
        this.forEach(item => {
            item.pause();
        });
        return this;
    }
    public endCSS() {
        this.forEach(item => {
            item.endCSS();
        });
        setPlayCSS(this, false);
    }
    public end() {
        isEndedCSS(this) && this.endCSS();
        super.end();
        return this;
    }
    /**
  * get item orders
  * @example
  scene.getOrders() // => ["item1", "item2"]
  */
    public getOrders(): NameType[] {
        return this.orderMap.get([]) || [];
    }
    /**
      * set item orders
      * @param - orders
      * @example
      frame.setOrders(["item2", "item1"]) // => ["item2", "item1"]
      */
    public setOrders(orders: NameType[]): NameType[] {
        return this.orderMap.set([], orders);
    }
    public getAnimationElement() {
        let animtionElement: AnimateElement;

        this.forEach(item => {
            const el = item.getAnimationElement();

            !animtionElement && (animtionElement = el);
        });
        return animtionElement;
    }
    public addPlayClass(isPaused: boolean, playClassName?: string, properties: object = {}) {
        let animtionElement: AnimateElement;

        this.forEach(item => {
            const el = item.addPlayClass(isPaused, playClassName, properties);

            !animtionElement && (animtionElement = el);
        });
        return animtionElement;
    }
    /**
    * Play using the css animation and keyframes.
    * @param - Check if you want to export css.
    * @param [playClassName="startAnimation"] - Add a class name to play.
    * @param - The shorthand properties for six of the animation properties.
    * @return {Scene} An instance itself
    * @see {@link https://www.w3schools.com/cssref/css3_pr_animation.asp}
    * @example
    scene.playCSS();
    scene.playCSS(false, {
    direction: "reverse",
    fillMode: "forwards",
    });
    */
    public playCSS(isExportCSS = true, playClassName?: string, properties: Partial<AnimatorState> = {}) {
        playCSS(this, isExportCSS, playClassName, properties);
        return this;
    }
    public set(properties: any, ...args: any[]): this;
    /**
      * Set properties to the Scene.
      * @param - properties
      * @return An instance itself
      * @example
scene.set({
    ".a": {
        0: {
            opacity: 0,
        },
        1: {
            opacity: 1,
        },
    },
});
// 0
console.log(scene.getItem(".a").get(0, "opacity"));
// 1
console.log(scene.getItem(".a").get(1, "opacity"));
      */
    public set(properties: any) {
        this.load(properties);
        return this;
    }
    /**
      * Clear All Items
      * @return {Scene} An instance itself
      */
    public clear() {
        this.finish();
        this.items = {};
        this.orderMap = new OrderMap(NAME_SEPARATOR);

        if (this.styledInjector) {
            this.styledInjector.destroy();
        }
        this.styled = null;
        this.styledInjector = null;
    }
    public load(properties: any = {}, options = properties.options) {
        if (!properties) {
            return this;
        }
        this.setOptions(options);

        const selector = options && options[SELECTOR] || this.state[SELECTOR];
        for (const name in properties) {
            if (name === "options") {
                continue;
            }
            const object = properties[name];
            let item;

            if (isScene(object) || isSceneItem(object)) {
                this.setItem(name, object);
                item = object;
            } else if (isFunction(object)) {
                let elements: IArrayFormat<AnimateElement> = [];

                if (selector && IS_WINDOW) {
                    if (!this.state.noRegisterElement) {
                        elements = $(
                            `${isFunction(selector) ? selector(name) : name}`,
                            true,
                        );
                    }
                }
                const elementsLength = elements.length;
                const length = elementsLength || (object as SelectorAllType).defaultCount || 0;
                const scene = new Scene();

                const ids: Array<string | number> = [];
                for (let i = 0; i < length; ++i) {
                    const element = elements[i];
                    const subItem = scene.newItem(i) as SceneItem;

                    subItem.setId().load(object(i, elements[i]));

                    ids.push(subItem.getId());
                    if (element) {
                        subItem.setElement(element);
                    }
                }
                if (!elementsLength) {
                    let subElements: IArrayFormat<AnimateElement> = [];

                    scene.state[SELECTOR] = (id: number) => {
                        if (!subElements.length) {
                            subElements = $(`${isFunction(selector) ? selector(name) : name}`, true);
                        }

                        return subElements[ids.indexOf(id)];
                    };
                }
                this.setItem(name, scene);
                continue;
            } else {
                item = this.newItem(name, {
                    noRegisterElement: true,
                });
                item.load(object);
            }
            if (!this.state.noRegisterElement) {
                selector && item.setSelector(selector);
            }
        }
    }
    public setOptions(options: Partial<SceneState> = {}): this {
        super.setOptions(options);

        const selector = options.selector;

        if (selector) {
            this.state[SELECTOR] = selector;
        }
        return this;
    }
    public setSelector(target?: string | boolean | ((id: number | string) => string | AnimateElement)) {
        const state = this.state;
        const selector = target === true ? state[SELECTOR] || true : target;

        state[SELECTOR] = selector;
        const isItFunction = isFunction(target);
        if (selector) {
            this.forEach((item, name) => {
                item.setSelector(isItFunction ? (target as (id: number | string) => string)(name) : selector);
            });
        }
        return this;
    }
    public start(delay: number = this.state[DELAY]): boolean {
        const result = super.start(delay);

        if (result) {
            this.forEach(item => {
                item.start(0);
            });
        } else {
            this.forEach(item => {
                item.setPlayState(RUNNING);
            });
        }
        return result;
    }
}

export default Scene;