From 0148236b361e6e7d40644f84556399c366b534fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fe=CC=81lix=20Pe=CC=81ault?= Date: Sun, 21 Nov 2021 13:39:56 +0100 Subject: [PATCH] Add reveal function for animating elements --- src/animations/RevealQueue.ts | 65 +++++++ src/animations/index.ts | 327 ++++++++++++++++++++++++++++++++++ 2 files changed, 392 insertions(+) create mode 100644 src/animations/RevealQueue.ts create mode 100644 src/animations/index.ts diff --git a/src/animations/RevealQueue.ts b/src/animations/RevealQueue.ts new file mode 100644 index 0000000..53ca03e --- /dev/null +++ b/src/animations/RevealQueue.ts @@ -0,0 +1,65 @@ +interface AnimationsQueueItem { + node: Node + animation: Function + delay: number +} + +export class RevealQueue { + items: AnimationsQueueItem[] = [] + queuedItems: AnimationsQueueItem[] = [] + timer = null + observer = null + + constructor () { + if (typeof IntersectionObserver === 'undefined') return + + this.observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + this.observer.unobserve(entry.target) + const item = this.findItemFromNode(entry.target) + this.queuedItems.push(item) + + if (this.timer === null) { + this.run() + } + } + }) + }) + } + + // Add an animation in queue + add (node: Node, animation: Function, delay: number) { + this.items.push({ + node, + animation, + delay, + }) + this.observer.observe(node) + } + + // Remove node from queue and unobserve from IO + remove (node: Node) { + this.observer.unobserve(node) + this.items = this.items.filter(v => v.node !== node) + this.queuedItems = this.queuedItems.filter(v => v.node !== node) + } + + // Run animation + run () { + if (this.queuedItems.length === 0) { + this.timer = null + return + } + + const item = this.queuedItems[0] + item.animation() + this.remove(item.node) + this.timer = window.setTimeout(this.run.bind(this), item.delay) + } + + // Find item from node + findItemFromNode (node: Node) { + return this.items.find(i => i.node === node) + } +} \ No newline at end of file diff --git a/src/animations/index.ts b/src/animations/index.ts new file mode 100644 index 0000000..68feb24 --- /dev/null +++ b/src/animations/index.ts @@ -0,0 +1,327 @@ +import anime from 'animejs' +import type { AnimeParams } from 'animejs' +import type { TransitionConfig } from 'svelte/transition' + + +// Options interface +interface TransitionOptions { + direct?: boolean + children?: string + targets?: Element + from?: number | string + to?: number | string + opacity?: boolean + rotate?: any + rotateX?: number + rotateRandom?: boolean + duration?: number + stagger?: number + scale?: number[] + delay?: number + easing?: string + clear?: boolean +} + + +/** + * Effect: Fly + * @returns Anime.js animation + */ +export const fly = ( + node: Element, + { + direct = false, + targets = node, + children, + from = 16, + to = 0, + opacity = true, + rotate = false, + rotateX = 0, + rotateRandom = false, + duration = 1600, + stagger, + scale = null, + delay = 0, + easing = 'easeOutQuart', + clear = false + }: TransitionOptions +): TransitionConfig => { + const anim = anime({ + autoplay: !direct, + targets: children ? node.querySelectorAll(children) : targets, + ...(opacity && { opacity: [0, 1] }), + ...(scale && { scale }), + ...(rotate && { + rotate: + // If Array, use it, otherwise use the value up to 0 + Array.isArray(rotate) + ? rotate + : [rotateRandom ? anime.random(-rotate, rotate) : rotate, 0] + }), + ...(rotateX && { rotateX: [rotateX, 0] }), + translateY: [from, to], + translateZ: 0, + duration, + easing, + delay: stagger ? anime.stagger(stagger, { start: delay }) : delay, + complete: ({ animatables }) => { + // Remove styles on end + if (clear) { + animatables.forEach((el: AnimeParams) => { + el.target.style.transform = '' + opacity && (el.target.style.opacity = '') + }) + } + } + }) + + return direct ? anim : { + tick: (t: number, u: number) => anim + } +} + + +/** + * Effect: Fade + * @returns Anime.js animation + */ +export const fade = ( + node: Element, + { + direct = false, + targets = node, + children, + from = 0, + to = 1, + duration = 1600, + stagger, + delay = 0, + easing = 'easeInOutQuart', + clear = false + }: TransitionOptions +): TransitionConfig => { + + const anim = anime({ + autoplay: !direct, + targets: children ? node.querySelectorAll(children) : targets, + opacity: [from, to], + duration, + easing, + delay: stagger ? anime.stagger(stagger, { start: delay }) : delay, + complete: ({ animatables }) => { + // Remove styles on end + if (clear) { + animatables.forEach((el: AnimeParams) => { + el.target.style.opacity = '' + }) + } + } + }) + + return direct ? anim : { + tick: (t: number, u: number) => anim + } +} + + +/** + * Effect: Scale + * @returns Anime.js animation + */ +export const scale = ( + node: Element, + { + direct = false, + from = 0, + to = 1, + duration = 1200, + delay = 0, + easing = 'easeOutQuart', + clear = false + }: TransitionOptions +): TransitionConfig => { + const anim = anime({ + autoplay: !direct, + targets: node, + scaleY: [from, to], + translateZ: 0, + duration, + easing, + delay, + complete: ({ animatables }) => { + // Remove styles on end + if (clear) { + animatables.forEach((el: AnimeParams) => { + el.target.style.transform = '' + }) + } + } + }) + + return direct ? anim : { + tick: (t: number, u: number) => anim + } +} + + +/** + * Effect: Words reveal + * @description Anime.js animation + */ +export const words = ( + node: Element, + { + direct = false, + children = 'span', + from = '45%', + to = 0, + duration = 1200, + stagger = 60, + rotate = 0, + delay = 0, + opacity = true, + easing = 'easeOutQuart', + clear = false + }: TransitionOptions +): TransitionConfig => { + const anim = anime({ + autoplay: !direct, + targets: node.querySelectorAll(children), + ...(opacity && { opacity: [0, 1] }), + translateY: [from, to], + ...(rotate && { rotateX: [rotate, 0] }), + translateZ: 0, + duration, + easing, + delay: stagger ? anime.stagger(stagger, { start: delay }) : delay, + complete: ({ animatables }) => { + // Remove styles on end + if (clear) { + animatables.forEach((el: AnimeParams) => { + el.target.style.transform = '' + opacity && (el.target.style.opacity = '') + }) + } + } + }) + + return direct ? anim : { + tick: (t: number, u: number) => anim + } +} + + + +/** + * Run animation on reveal + * @description IntersectionObserver triggering an animation function or a callback + */ +export const reveal = ( + node: Element | any, + { + enable = true, + targets = node, + animation, + options = {}, + callback, + callbackTrigger, + once = true, + threshold = 0.2, + rootMargin = '0px 0px 0px', + queue = null, + queueDelay = 0, + }: revealOptions +) => { + let observer: IntersectionObserver + + // Kill if IntersectionObserver is not supported + if (typeof IntersectionObserver === 'undefined' || !enable) return + + // Use animation with provided node selector + if (animation) { + const anim = animation(node, { + ...options, + direct: true, + autoplay: false + }) + + // If a queue exists, let it run animations + if (queue) { + queue.add(node, anim.play, queueDelay) + + return { + destroy () { + queue.remove(node) + } + } + } + + observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + anim && anim.play() + once && observer.unobserve(entry.target) + } + }) + }, { threshold, rootMargin }) + + observer.observe(node) + } + + // Custom callback + else if (callback) { + observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + const cb = callback(entry) + + if (entry.isIntersecting) { + // Run callback + callbackTrigger && callbackTrigger(cb) + + // Run IntersectionObserver only once + once && observer.unobserve(entry.target) + } + }) + }, { threshold }) + + const elements = typeof targets === 'string' ? node.querySelectorAll(targets) : targets + + if (elements) { + // Observe each element + if (elements.length > 0) { + elements.forEach((target: Element) => { + observer.observe(target) + }) + } + // Directly observe + else { + observer.observe(elements) + } + } + } + + // Methods + return { + // Destroy + destroy () { + observer && observer.disconnect() + } + } +} + +interface revealOptions { + animation?: Function + options?: TransitionOptions + queue?: any + callback?: Function + callbackTrigger?: any + targets?: string | Element + enable?: boolean + once?: boolean + threshold?: number, + rootMargin?: string, + queueDelay?: number +} + +export { RevealQueue } from './RevealQueue'