Finish to replace Anime with Motion One for page animations

Page intro animation and reveal that has now been simplified as Motion One manages an inView option (that uses IntersectionObserver)
This commit is contained in:
2022-08-14 20:24:28 +02:00
parent fa74e5bf7f
commit fd6fc20b13
15 changed files with 157 additions and 558 deletions

View File

@@ -16,7 +16,6 @@
},
"dependencies": {
"@studio-freight/lenis": "^0.1.13",
"animejs": "^3.2.1",
"dayjs": "^1.11.5",
"embla-carousel": "^7.0.0",
"focus-visible": "^5.2.0",
@@ -30,7 +29,6 @@
"@sveltejs/adapter-node": "^1.0.0-next.86",
"@sveltejs/adapter-vercel": "^1.0.0-next.66",
"@sveltejs/kit": "^1.0.0-next.405",
"@types/animejs": "^3.1.5",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"browserslist": "^4.21.3",

12
pnpm-lock.yaml generated
View File

@@ -6,10 +6,8 @@ specifiers:
'@sveltejs/adapter-node': ^1.0.0-next.86
'@sveltejs/adapter-vercel': ^1.0.0-next.66
'@sveltejs/kit': ^1.0.0-next.405
'@types/animejs': ^3.1.5
'@typescript-eslint/eslint-plugin': ^5.33.0
'@typescript-eslint/parser': ^5.33.0
animejs: ^3.2.1
browserslist: ^4.21.3
cssnano: ^5.1.13
dayjs: ^1.11.5
@@ -37,7 +35,6 @@ specifiers:
dependencies:
'@studio-freight/lenis': 0.1.13
animejs: 3.2.1
dayjs: 1.11.5
embla-carousel: 7.0.0
focus-visible: 5.2.0
@@ -51,7 +48,6 @@ devDependencies:
'@sveltejs/adapter-node': 1.0.0-next.86
'@sveltejs/adapter-vercel': 1.0.0-next.66
'@sveltejs/kit': 1.0.0-next.405_svelte@3.49.0+vite@3.0.7
'@types/animejs': 3.1.5
'@typescript-eslint/eslint-plugin': 5.33.0_njno5y7ry2l2lcmiu4tywxkwnq
'@typescript-eslint/parser': 5.33.0_qugx7qdu5zevzvxaiqyxfiwquq
browserslist: 4.21.3
@@ -489,10 +485,6 @@ packages:
engines: {node: '>=10.13.0'}
dev: true
/@types/animejs/3.1.5:
resolution: {integrity: sha512-4i3i1YuNaNEPoHBJY78uzYu8qKIwyx96G04tnVtNhRMQC9I1Xhg6fY9GeWmZAzudaesKKrPkQgTCthT1zSGYyg==}
dev: true
/@types/json-schema/7.0.11:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
@@ -693,10 +685,6 @@ packages:
uri-js: 4.4.1
dev: true
/animejs/3.2.1:
resolution: {integrity: sha512-sWno3ugFryK5nhiDm/2BKeFCpZv7vzerWUcUPyAZLDhMek3+S/p418ldZJbJXo5ZUOpfm2kP2XRO4NJcULMy9A==}
dev: false
/ansi-regex/5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}

View File

@@ -1,65 +0,0 @@
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)
}
}

View File

@@ -14,6 +14,7 @@ export const [send, receive] = crossfade({
const style = getComputedStyle(node)
const transform = style.transform === 'none' ? '' : style.transform
const sd = 1 - start
return {
duration,
easing,

View File

@@ -1,326 +0,0 @@
import anime, { 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'

View File

@@ -8,7 +8,7 @@
<script lang="ts">
import { map } from '$utils/functions'
import { reveal, fly } from '$animations/index'
import reveal from '$animations/reveal'
export let tag: string
export let label: string = undefined
@@ -46,17 +46,15 @@
const revealOptions = animate ? {
animation: fly,
children: '.char',
animation: { y: ['-105%', 0] },
options: {
children: '.char',
stagger: 60,
duration: 1600,
from: '-105%',
opacity: false,
delay: 200,
stagger: 0.06,
duration: 1.6,
delay: 0.2,
threshold: 0.2,
},
threshold: 0.2,
} : {}
} : null
</script>
<svelte:window

View File

@@ -4,7 +4,7 @@
<script lang="ts">
import SplitText from '$components/SplitText.svelte'
import { reveal, fly } from '$animations/index'
import reveal from '$animations/reveal'
import { DURATION } from '$utils/contants'
export let variant: string = 'lines'
@@ -14,16 +14,14 @@
{#if tag === 'h1'}
<h1 class="site-title site-title--{variant}"
use:reveal={{
animation: fly,
children: '.char',
animation: { y: ['105%', 0] },
options: {
children: '.char',
stagger: 40,
duration: 1000,
from: '110%',
opacity: false,
delay: DURATION.PAGE_IN
stagger: 0.04,
duration: 1,
delay: DURATION.PAGE_IN / 1000,
threshold: 0,
},
threshold: 0,
}}
>
<SplitText text="Houses" mode="chars" class="pink mask" />

View File

@@ -5,7 +5,7 @@
<script lang="ts">
import { page } from '$app/stores'
import { getContext } from 'svelte'
import { fly, reveal } from '$animations/index'
import reveal from '$animations/reveal'
// Components
import Icon from '$components/atoms/Icon.svelte'
@@ -42,14 +42,12 @@
class:is-open={isOpen}
class:is-over={isOver}
use:reveal={{
animation: fly,
animation: { y: [24, 0], opacity: [0, 1] },
options: {
from: 24,
to: 0,
duration: 1000,
easing: 'easeOutQuart',
delay: 600,
}
duration: 1,
delay: 0.6,
threshold: 0,
},
}}
>
<button class="switcher__button" title="{!isOpen ? 'Open' : 'Close'} menu" tabindex="0"

View File

@@ -3,10 +3,13 @@
</style>
<script lang="ts">
import { navigating } from '$app/stores'
import { getContext, onMount } from 'svelte'
import anime, { type AnimeTimelineInstance } from 'animejs'
import { stagger, timeline } from 'motion'
import { cartOpen } from '$utils/stores/shop'
import { smoothScroll } from '$utils/functions'
import { DELAY } from '$utils/contants'
import { quartOut } from '$animations/easings'
// Components
import Image from '$components/atoms/Image.svelte'
import ButtonCart from '$components/atoms/ButtonCart.svelte'
@@ -37,46 +40,52 @@
/**
* Animations
*/
// Setup animations
const timeline: AnimeTimelineInstance = anime.timeline({
duration: 1600,
easing: 'easeOutQuart',
autoplay: false,
const animation = timeline([
// Hero image
['.shop-page__background', {
scale: [1.06, 1],
opacity: [0, 1],
}, {
at: 0.4,
duration: 2.4,
}],
// Intro top elements
['.shop-page__intro .top > *', {
y: [-100, 0],
opacity: [0, 1],
}, {
at: 0.4,
delay: stagger(0.25),
}],
// Hero title
['.shop-page__title h1', {
y: [32, 0],
opacity: [0, 1],
}, {
at: 0.5,
}],
// Intro navbar
['.shop-page__nav .container > *, .shop-page__intro .button-cart', {
y: [100, 0],
opacity: [0, 1],
}, {
at: 0.7,
delay: stagger(0.25),
}]
], {
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Hero image
timeline.add({
targets: '.shop-page__background',
scale: [1.06, 1],
opacity: [0, 1],
duration: 2400,
}, 400)
// Hero title
timeline.add({
targets: '.shop-page__title h1',
translateY: [32, 0],
opacity: [0, 1],
}, 500)
// Intro top elements
timeline.add({
targets: '.shop-page__intro .top > *',
translateY: [-100, 0],
delay: anime.stagger(250),
}, 400)
// Intro navbar
timeline.add({
targets: '.shop-page__nav .container > *, .shop-page__intro .button-cart',
opacity: [0, 1],
translateY: [100, 0],
translateZ: 0,
delay: anime.stagger(250),
}, 700)
// Transition in
requestAnimationFrame(timeline.play)
// Run animation
requestAnimationFrame(animation.play)
// Destroy

View File

@@ -4,17 +4,18 @@
<script lang="ts">
import { browser } from '$app/env'
import { page } from '$app/stores'
import { page, navigating } from '$app/stores'
import { goto } from '$app/navigation'
import { onMount, tick } from 'svelte'
import { fade, scale } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import dayjs from 'dayjs'
import { stagger, timeline } from 'motion'
import { fetchAPI, getAssetUrlKey } from '$utils/api'
import { previousPage } from '$utils/stores'
import { DELAY } from '$utils/contants'
import { throttle } from '$utils/functions'
import { swipe } from '$utils/interactions/swipe'
import dayjs from 'dayjs'
import anime, { type AnimeTimelineInstance } from 'animejs'
// Components
import Metas from '$components/Metas.svelte'
import SplitText from '$components/SplitText.svelte'
@@ -219,76 +220,74 @@
/**
* Animations
*/
// Setup animations
const timeline: AnimeTimelineInstance = anime.timeline({
duration: 1600,
easing: 'easeOutQuart',
autoplay: false,
})
const animation = timeline([
// First photo
['.photo-page__picture.is-1', {
y: [24, 0],
opacity: [0, 1],
}, {
duration: 0.9,
}],
// Other photos
['.photo-page__picture:not(.is-1)', {
x: ['-150%', 0],
opacity: [0, 1],
}, {
at: 0.4,
delay: stagger(0.1),
opacity: { duration: 0.25 },
}],
anime.set('.photo-page__picture', {
opacity: 0,
})
anime.set('.photo-page__picture.is-1', {
translateY: 24,
})
// Prev/Next buttons
['.photo-page__controls .prev', {
x: [-16, 0],
opacity: [0, 1],
}, {
at: 0.45,
}],
['.photo-page__controls .next', {
x: [16, 0],
opacity: [0, 1],
}, {
at: 0.45,
}],
// Photos
timeline.add({
targets: '.photo-page__picture.is-1',
opacity: 1,
translateY: 0,
duration: 900,
}, 250)
timeline.add({
targets: '.photo-page__picture:not(.is-1)',
opacity: 1,
translateX (element: HTMLElement) {
const x = getComputedStyle(element).getPropertyValue('--offset-x').trim()
return [`-${x}`, 0]
// Infos
['.photo-page__info > *', {
y: [24, 0],
opacity: [0, 1],
}, {
at: 0.4,
delay: stagger(0.3)
}],
// Index
['.photo-page__index', {
opacity: [0, 1],
}, {
at: 0.6,
delay: stagger(0.2),
duration: 0.9,
}],
// Fly each number
['.photo-page__index .char', {
y: ['300%', 0],
}, {
at: 1.1,
delay: stagger(0.2),
duration: 1,
}],
], {
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
delay: anime.stagger(55)
}, 350)
// Prev/Next buttons
timeline.add({
targets: '.photo-page__controls button',
translateX (item: HTMLElement) {
let direction = item.classList.contains('prev') ? -1 : 1
return [16 * direction, 0]
},
opacity: [0, 1],
}, 450)
// Infos
timeline.add({
targets: '.photo-page__info > *',
translateY: [24, 0],
opacity: [0, 1],
delay: anime.stagger(200)
}, 400)
anime.set('.photo-page__index', {
opacity: 0
})
// Index
timeline.add({
targets: '.photo-page__index',
opacity: 1,
duration: 900,
}, 600)
// Fly each number
timeline.add({
targets: '.photo-page__index .char',
translateY: ['100%', 0],
delay: anime.stagger(200),
duration: 1000,
}, 1100)
animation.stop()
// Transition in
requestAnimationFrame(timeline.play)
// Run animation
requestAnimationFrame(animation.play)
})
</script>

View File

@@ -146,13 +146,12 @@
/**
* Animations
*/
const animationDelay = $navigating ? DURATION.PAGE_IN : 0
const animation = timeline([
// Title word
['.location-page__intro .word', {
y: ['110%', 0],
}, {
at: 0.2 + animationDelay,
at: 0.2,
}],
// Illustration
@@ -160,7 +159,7 @@
scale: [1.06, 1],
opacity: [0, 1],
}, {
at: 0.4 + animationDelay,
at: 0.4,
duration: 2.4,
}],
@@ -168,7 +167,7 @@
['.location-page__intro .of', {
opacity: [0, 1],
}, {
at: 0.95 + animationDelay,
at: 0.95,
duration: 1.2,
}],
@@ -177,11 +176,11 @@
y: ['10%', 0],
opacity: [0, 1],
}, {
at: 0.9 + animationDelay,
at: 0.9,
duration: 1.2,
}]
], {
delay: DELAY.PAGE_LOADING / 1000,
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,

View File

@@ -4,6 +4,7 @@
<script lang="ts">
import { onMount } from 'svelte'
import { navigating } from '$app/stores'
import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/contants'
import { quartOut } from 'svelte/easing'
@@ -45,7 +46,7 @@
delay: stagger(0.35),
}],
], {
delay: DELAY.PAGE_LOADING / 1000,
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,

View File

@@ -3,12 +3,12 @@
</style>
<script lang="ts">
import { page } from '$app/stores'
import { page, navigating } from '$app/stores'
import { getContext, onMount } from 'svelte'
import { timeline, stagger } from 'motion'
import { DELAY } from '$utils/contants'
import { smoothScroll } from '$utils/functions'
import { reveal, fade as animeFade } from '$animations/index'
import reveal from '$animations/reveal'
import { quartOut } from '$animations/easings'
// Components
import Metas from '$components/Metas.svelte'
@@ -57,7 +57,7 @@
delay: stagger(0.075),
}]
], {
delay: DELAY.PAGE_LOADING / 1000,
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,
@@ -81,9 +81,9 @@
<PageTransition name="homepage">
<section class="homepage__intro"
use:reveal={{
animation: animeFade,
animation: { opacity: [0, 1] },
options: {
duration: 1000,
duration: 1,
},
}}
>

View File

@@ -3,12 +3,12 @@
</style>
<script lang="ts">
import { page } from '$app/stores'
import { page, navigating } from '$app/stores'
import { goto } from '$app/navigation'
import { getContext, onMount } from 'svelte'
import { fly } from 'svelte/transition'
import dayjs from 'dayjs'
import { quartOut as quartOutSvelte } from 'svelte/easing'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime.js'
import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/contants'
@@ -294,11 +294,11 @@
y: [16, 0],
opacity: [0, 1],
}, {
at: 0.4,
delay: stagger(0.25),
at: 0.5,
delay: stagger(0.3),
}]
], {
delay: DELAY.PAGE_LOADING / 1000,
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,

View File

@@ -3,9 +3,10 @@
</style>
<script lang="ts">
import { navigating } from '$app/stores'
import { onMount } from 'svelte'
import { stagger, timeline } from 'motion'
import dayjs from 'dayjs'
import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/contants'
import { quartOut } from '$animations/easings'
// Components
@@ -44,7 +45,7 @@
delay: stagger(0.15),
}],
], {
delay: DELAY.PAGE_LOADING / 1000,
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,