Add reveal function for animating elements
This commit is contained in:
65
src/animations/RevealQueue.ts
Normal file
65
src/animations/RevealQueue.ts
Normal 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
327
src/animations/index.ts
Normal 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'
|
||||||
Reference in New Issue
Block a user