Add reveal function for animating elements

This commit is contained in:
2021-11-21 13:39:56 +01:00
parent cd9734b8a4
commit 0148236b36
2 changed files with 392 additions and 0 deletions

View File

@@ -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)
}
}

327
src/animations/index.ts Normal file
View File

@@ -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'