effects.ts

import { SceneItem, AnimatorState, Frame, SceneItemOptions } from "scenejs";
import { IObject, isArray, splitUnit } from "@daybrush/utils";
import { EffectState, KineticType } from "./types";
import { getKeyframes } from "keyframer";

/**
 * @namespace effects
 */

/**
 * Use the property to create an effect.
 * @memberof effects
 * @private
 * @param - property to set effect
 * @param - values of 100%
 * @example
// import {set, blink} from "@scenejs/effects";
// Scene.set("opacity", [0, 1, 0], {duration: 2});
set("opacity", [0, 1, 0], {duration: 2});

// Same
// Scene.blink({duration: 2});
blink({ duration: 2});

// Same
new SceneItem({
	"0%": {
		opacity: 0,
	},
	"50%": {
		opacity: 1,
	}
	"100%": {
		opacity: 0,
	}
}, {
	duration: 2,
});
 */
function set(property: string | string[], values: any[], options: Partial<AnimatorState>): SceneItem {
    const item = new SceneItem({}, options);
    const length = values.length;

    for (let i = 0; i < length; ++i) {
        item.set(`${i / (length - 1) * 100}%`, property, values[i]);
    }
    return item;
}

/**
 * Make a zoom in effect.
 * @memberof effects
 * @param options
 * @param {number} [options.from = 0] start zoom
 * @param {number}[options.to = 1] end zoom
 * @param {number} options.duration animation's duration
 * @example
import { zoomIn } from "@scenejs/effects";

// Scene.zoomIn({duration: 2});
zoomIn({duration: 2});

// Same
new SceneItem({
	"0%": {
		"transform": "scale(0)",
	},
	"100%": {
		"transform": "scale(1)",
	}
}, {
	duration: 2,
});
 */
export function zoomIn({ from = 0, to = 1 }: Partial<EffectState> = {}): SceneItem {
    return set(["transform", "scale"], [from, to], arguments[0]);
}

/**
 * Make a zoom out effect.
 * @memberof effects
 * @param options
 * @param {number} [options.from = 1] start zoom
 * @param {number}[options.to = 0] end zoom
 * @param {number} options.duration animation's duration
 * @example
import { zoomOut } from "@scenejs/effects";
// Scene.zoomOut({ duration: 2 });
zoomOut({ duration: 2 });

// Same
new SceneItem({
	"0%": {
		"transform": "scale(1)",
	},
	"100%": {
		"transform": "scale(0)",
	}
}, {
	duration: 2,
});
 */
export function zoomOut({ from = 1, to = 0 }: Partial<EffectState> = {}): SceneItem {
    return set(["transform", "scale"], [from, to], arguments[0]);
}

/**
 * Make a wipe in effect.
 * @memberof effects
 * @param options
 * @param {string|string[]} [options.property = "left"] position property
 * @param {number|string} [options.from = "-100%"] start position
 * @param {number|string}[options.to = "0%"] end position
 * @param {number} options.duration animation's duration
 * @example
import { wipeIn } from "@scenejs/effects";

// Scene.wipeIn({ property: "left", duration: 2 });
wipeIn({ property: "left", duration: 2 });
// Same
new SceneItem({
	"0%": {
		"left": "-100%",
	},
	"100%": {
		"left": "0%",
	}
}, {
	duration: 2,
});
 */
export function wipeIn({ from = "-100%", to = "0%", property = "left" }: Partial<EffectState> = {}): SceneItem {
    return set(property, [from, to], arguments[0]);
}

/**
 * Make a wipe out effect.
 * @memberof effects
 * @param options
 * @param {string|string[]} [options.property = "left"] position property
 * @param {number|string} [options.from = "0%"] start position
 * @param {number|string}[options.to = "100%"] end position
 * @param {number} options.duration animation's duration
 * @example
import { wipeOut } from "@scenejs/effects";
// Scene.wipeOut({property: "left", duration: 2});
wipeOut({property: "left", duration: 2});
// Same
new SceneItem({
	"0%": {
		"left": "0%",
	},
	"100%": {
		"left": "100%",
	}
}, {
	duration: 2,
});
 */
export function wipeOut({ from = "0%", to = "100%", property = "left" }: Partial<EffectState> = {}): SceneItem {
    return set(property, [from, to], arguments[0]);
}

/**
 * Switch the scene from `item1` to `item2`.
 * @memberof effects
 * @param - Item that end effect
 * @param - Item that start effect
 * @param -  `transitionItem` or `transitionObject` to switch from `item1` to `item2`
 * @example
import Scene from "scenejs";
import {transition, zoomIn, fadeOut} from "@scenejs/effects";

var transitionScene = new Scene({
  "[data-transition] .target": {},
  "[data-transition] .target2": {},
}, {
  delay: 0.1,
  easing: "ease-in-out",
  selector: true,
});
Scene.transition(
  transitionScene.getItem("[data-transition] .target"),
  transitionScene.getItem("[data-transition] .target2"),
  {
    0:  [
      fadeOut({ duration: 1 }),
      zoomIn({ from: 1, to: 2, duration: 1 }),
      "opacity: 1; transform: rotate(0deg)",
    ],
    1: "opacity: 0; transform: rotate(40deg)",
  }
);
transitionScene.play();
 */
export function transition(
    item1: SceneItem,
    item2: SceneItem,
    transitionObject: SceneItem | IObject<any>,
): void {
    const transitionItem = new SceneItem();

    transitionItem.append(transitionObject);

    const duration = transitionItem.getDuration();
    const transitionTime = Math.max(item1.getDuration() - duration, 0);
    item1.set({
        [transitionTime]: transitionItem,
    });
    transitionItem.setDirection("reverse");
    item2.set({
        0: transitionItem,
    });
}

/**
 * Make a fade in effect.
 * @memberof effects
 * @param options
 * @param {number} [options.from = 0] start opacity
 * @param {number}[options.to = 1] end opacity
 * @param {number} options.duration animation's duration
 * @example
import { fadeIn } from "@scenejs/effects";
// Scene.fadeIn({duration: 2});
fadeIn({duration: 2});
// Same
new SceneItem({
	"0%": {
		opacity: 0,
	},
	"100%": {
		opacity: 1,
	}
}, {
	duration: 2,
});
 */
export function fadeIn({ from = 0, to = 1 }: Partial<EffectState> = {}): SceneItem {
    return set("opacity", [from, to], arguments[0]);
}

/**
 * Make a fade out effect.
 * @memberof effects
 * @param options
 * @param {number} [options.from = 1] start opacity
 * @param {number}[options.to = 0] end opacity
 * @param {number} options.duration animation's duration
 * @example
import { fadeOut } from "@scenejs/effects";
// Scene.fadeOut({duration: 2});
fadeOut({duration: 2});
// Same
new SceneItem({
	"0%": {
		opacity: 1,
	},
	"100%": {
		opacity: 0,
	}
}, {
	duration: 2,
});
 */
export function fadeOut({ from = 1, to = 0 }: Partial<EffectState> = {}): SceneItem {
    return set("opacity", [from, to], arguments[0]);
}
/**
 * Make a blinking effect.
 * @memberof effects
 * @param options
 * @param {number} [options.from = 0] start opacity
 * @param {number}[options.to = 1] end opacity
 * @param {number} options.duration animation's duration
 * @example
import {blink} from "@scenejs/effects";
// Scene.blink({duration: 2});
blink({duration: 2});
// Same
new SceneItem({
	"0%": {
		opacity: 0,
	},
	"50%": {
		opacity: 1,
	},
	"100%": {
		opacity: 0,
	}
}, {
	duration: 2,
});
 */
export function blink({ from = 0, to = 1 }: Partial<EffectState> = {}): SceneItem {
    return set("opacity", [from, to, from], arguments[0]);
}
/**
 * You can create a flip effect horizontally, vertically, or diagonally.
 * @memberof effects
 * @param options
 * @param {number} [options.x=1] - Indicates the direction and amount to be moved by the x-axis.
 * @param {number} [options.y=1] - Indicates the direction and amount to be moved by the y-axis.
 * @param {boolean} [options.backside=false] - Indicates whether to start from the back.
 * @example
import { flip } from "@scenejs/effects";

// flip({ x: 1, y: 1, backside: false })
flip()
  .setDuration(1)
  .setSelector("[data-flip] .target")
  .play();
flip({ backside: true })
  .setDuration(1)
  .setSelector("[data-flip] .target2")
  .play();
 */
export function flip({
    x = 1,
    y = 1,
    backside = false,
}: Partial<EffectState> = {}) {
    const item = new SceneItem({}, arguments[0]);
    let property = "";
    let startValue = "";
    let endValue = "";
    const ratio = (x && y) || x ? x : y;
    const startDeg = (backside ? (ratio > 0 ? 180 : -180) : 0);
    const endDeg = startDeg + ratio * 180;

    if (x && y) {
        const axis = [x > 0 ? 1 : -1, y > 0 ? 1 : -1, 0, ""].join(",");

        property = "rotate3d";
        startValue = axis + startDeg + "deg";
        endValue = axis + endDeg + "deg";
    } else {
        if (x) {
            property = "rotateX";
        } else if (y) {
            property = "rotateY";
        } else {
            return item;
        }
        startValue = startDeg + "deg";
        endValue = endDeg + "deg";
    }
    item.set({
        transform: {
            [property]: [startValue, endValue],
        },
    });
    return item;
}
/**
 * You can create an effect that flips vertically around the x-axis.
 * @memberof effects
 * @param options
 * @param {number} [options.x=1] - Indicates the direction and amount of movement.
 * @param {boolean} [options.backside=false] - Indicates whether to start from the back.
 * @example
import { flip, flipX } from "@scenejs/effects";

// flip({ x: 1, y: 0, backside: false })
// flipX({ x: 1, backside: false })
flipX()
  .setDuration(1)
  .setSelector("[data-flipx] .target")
  .play();
flipX({ backside: true })
  .setDuration(1)
  .setSelector("[data-flipx] .target2")
  .play();
 */
export function flipX({
    x = 1,
    backside = false,
}: Partial<EffectState> = {}): SceneItem {
    const item = flip({ y: 0, x, backside });

    item.setOptions(arguments[0]);

    return item;
}
/**
 * You can create an effect that flips horizontally around the y-axis.
 * @memberof effects
 * @param options
 * @param {number} [options.y=1] - Indicates the direction and amount of movement.
 * @param {boolean} [options.backside=false] - Indicates whether to start from the back.
 * @example
import { flip, flipY } from "@scenejs/effects";

// flip({ x: 0, y: 1, backside: false })
// flipY({ y: 1, backside: false })
flipY()
  .setDuration(1)
  .setSelector("[data-flipy] .target")
  .play();
flipY({ backside: true })
  .setDuration(1)
  .setSelector("[data-flipy] .target2")
  .play();
 */
export function flipY({
    y = 1,
    backside = false,
}: Partial<EffectState> = {}): SceneItem {
    const item = flip({ x: 0, y, backside });

    item.setOptions(arguments[0]);
    return item;
}

/**
 * Make a shake effect.
 * @memberof effects
 * @param options
 * @param {object|string} [options.properties="transform: translateX(5px) translateY (5px) rotate(5deg)"] - The range of properties to be moved.
 * @param {number} [options.frequency=10] - frequency of shakes
 * @example
import { shake } from "@scenejs/effects";

shake()
  .setDuration(0.2)
  .setIterationCount("infinite")
  .setSelector("[data-shake] .target")
  .play();

shake({
    properties: {
      transform: {
        // translateX: ["-5px", "5px"]
        translateX: "5px",
        translateY: ["-5px", "5px"],
        rotate: "5deg",
        // set range
        scale: [0.8, 1],
      },
    },
    frequency: 10,
  })
  .setDuration(0.2)
  .setIterationCount("infinite")
  .setSelector("[data-shake] .target2")
  .play();
 */
export function shake({
    properties = {
        transform: {
            translateX: [`-10px`, `10px`],
            translateY: [`-10px`, `10px`],
            rotate: [`-10deg`, `10deg`],
        },
    },
    frequency = 10,
}: Partial<EffectState> = {}): SceneItem {
    const item = new SceneItem({}, arguments[0]);
    const frame = new Frame(properties);
    const names = frame.getNames();

    names.forEach((propertyNames, i) => {
        const value = frame.get(...propertyNames);
        let start: number = 0;
        let end: number = 0;
        let unit: string = "";

        if (isArray(value)) {
            const { value: startNumber, unit: startUnit } = splitUnit(value[0]);

            unit = startUnit;
            start = startNumber;
            end = splitUnit(value[1]).value;
        } else {
            const { value: valueNumber, unit: valueUnit } = splitUnit(value);

            unit = valueUnit;
            end = Math.abs(valueNumber);
            start = -end;
        }
        item.set(`0%`, ...propertyNames, `${(start + end) / 2}${unit}`);
        item.set(`100%`, ...propertyNames, `${(start + end) / 2}${unit}`);

        for (let j = 1; j <= frequency; ++j) {
            const ratio = Math.random() * (end - start) + start;
            item.set(`${j / (frequency + 1) * 100}%`, ...propertyNames, `${ratio}${unit}`);
        }
    });
    return item;
}

/**
 * Make a horizontal shake effect.
 * @memberof effects
 * @param options
 * @param {string|string[]} [options.x=["-5px", "5px"]] - range of x's movement
 * @param {number} [options.frequency=10] - frequency of shakes
 * @example
import { shake, shakeX } from "@scenejs/effects";

// shakeX({ x: ["-5px", "5px"], frequency: 10 })
shakeX()
  .setDuration(0.2)
  .setIterationCount("infinite")
  .setSelector("[data-shakex] .target")
  .play();

shake({
    properties: {
      transform: {
        // translateX: ["-5px", "5px"]
        translateX: "5px",
      },
    },
    frequency: 10,
  })
  .setDuration(0.2)
  .setIterationCount("infinite")
  .setSelector("[data-shakex] .target2")
  .play();
 */
export function shakeX({
    x = ["-5px", "5px"],
    frequency = 10,
}: Partial<EffectState> = {}) {
    const item = shake({
        properties: {
            transform: {
                translateX: x,
            },
        },
        frequency,
    });
    item.setOptions(arguments[0]);

    return item;
}
/**
 * Make a vertical shake effect.
 * @memberof effects
 * @param options
 * @param {string|string[]} [options.y=["-5px", "5px"]] - range of y's movement
 * @param {number} [options.frequency=10] - frequency of shakes
 * @example
import { shake, shakeY } from "@scenejs/effects";

// shakeY({ y: ["-5px", "5px"], frequency: 10 })
shakeY()
  .setDuration(0.2)
  .setIterationCount("infinite")
  .setSelector("[data-shakey] .target")
  .play();

shake({
    properties: {
      transform: {
        // translateY: ["-5px", "5px"]
        translateY: "5px",
      },
    },
    frequency: 10,
  })
  .setDuration(0.2)
  .setIterationCount("infinite")
  .setSelector("[data-shakey] .target2")
  .play();
 */
export function shakeY({
    y = ["-5px", "5px"],
    frequency = 10,
}: Partial<EffectState> = {}) {
    const item = shake({
        properties: {
            transform: {
                translateY: y,
            },
        },
        frequency,
    });
    item.setOptions(arguments[0]);

    return item;
}
/**
 * Make the CSS Keyframes Playable Animator(SceneItem).
 * @see {@link https://github.com/daybrush/keyframer}
 * @param - The name of the keyframes(`CSSKeyframes​Rule`) in the stylesheet(`CSSStyleSheet`).
 * @param - SceneItem's options
 * @memberof effects
 * @example
`@keyframes keyframes {
    0%, 7.69% {
      border-width:35px;
      transform: translate(-50%, -50%) scale(0);
    }
    84.61% {
      border-width: 0px;
      transform: translate(-50%, -50%) scale(1);
    }
    100% {
      border-width: 0px;
      transform: translate(-50%, -50%) scale(1);
    }
}`

import { keyframer } from "@scenejs/effects";

keyframer("keyframes", {
    duration: 1,
    iterationCount: "infinite",
    selector: ".rect",
}).play();
 */
export function keyframer(name: string, options: Partial<SceneItemOptions>): SceneItem {
    return new SceneItem(getKeyframes(name), options);
}

/**
 * Create a frame that moves the origin in the opposite direction as it moves through the transform.
 * @memberof effects
 * @param options
 * @param {string|string[]} [options.leftProperty=["transform", "translateX"]] - Property name corresponding to left
 * @param {string|string[]} [options.topProperty=["transform", "translateY"]] - Property name corresponding to top
 * @param {string|number} [options.left="0px"] - Numbers to move horizontally
 * @param {string|number} [options.top="0px"] - Numbers to move vertically
 * @example
import { SceneItem } from "scenejs";
import { kineticFrame } from "@scenejs/effects";

new SceneItem({
    0: kineticFrame({ left: "0px", top: "0px" }).set({ transform: "rotate(0deg)"}),
    1: kineticFrame({ left: "50px", top: "0px" }).set({ transform: "rotate(90deg)"}),
    2: kineticFrame({ left: "50px", top: "50px" }).set({ transform: "rotate(180deg)"}),
    3: kineticFrame({ left: "0px", top: "50px" }).set({ transform: "rotate(270deg)"}),
    4: kineticFrame({ left: "0px", top: "0px" }).set({ transform: "rotate(360deg)"}),
}).setSelector(".target").play();
 */
export function kineticFrame({
    leftProperty = ["transform", "translateX"],
    topProperty = ["transform", "translateY"],
    left = "0px",
    top = "0px",
}: Partial<KineticType> = {}): Frame {
    const frame = new Frame();

    frame.set(...[].concat(leftProperty), left);
    frame.set(...[].concat(topProperty), top);
    frame.set("transform-origin", `calc(50% - ${left}) calc(50% - ${top})`);

    return frame;
}

/**
 * Make a typing effect that is typed one character at a time like a typewriter.
 * The `html` property only works with javascript animations.
 * The `content` property of CSS animations works only on desktop Chrome.
 * @memberof effects
 * @param options
 * @param {string|string[]} [options.property=["html"]] - Property to apply the typing animation
 * @param {string} [options.text=""] - Text to type
 * @param {number} [options.start=0] - Index to start typing
 * @param {number} [options.end=0] - Index to end typing
 * @param {number} [options.prefix=""] - The prefix string to be attached before text
 * @param {number} [options.suffix=""] - The suffix string to be attached after text

 * @example
import { typing } from "@scenejs/effects";

typing({ text: "Make a typing effect with Scene.js."})
  .setDuration(7)
  .setSelector(".target")
  .play();
 */
export function typing({
    property = ["html"],
    text = "",
    start = 0,
    end = text.length,
    prefix = "",
    suffix = "",
}: Partial<EffectState> = {}): SceneItem {
    const properties = [].concat(property);
    const item = new SceneItem();
    const length = Math.abs(end - start) + 1;

    if (start < end) {
        for (let i = start; i <= end; ++i) {
            item.set(`${(i - start) / length * 100}%`, ...properties, `${prefix}${text.substring(start, i)}${suffix}`);
        }
    } else {
        for (let i = end; i <= start; ++i) {
            item.set(
                `${(i - end) / length * 100}%`,
                ...properties,
                `${prefix}text.substring(end, start + end - i)${suffix}`,
            );
        }
    }
    item.setOptions(arguments[0]);

    return item;
}