[wip] Switch from Anime to Motion One for page animations

This commit is contained in:
2022-08-14 00:45:44 +02:00
parent a044cf3939
commit f771a73b67
13 changed files with 295 additions and 222 deletions

View File

@@ -1,7 +1,10 @@
import type { Easing } from 'motion'
/** /**
* Ease: Quart Out Array * Ease: Quart Out Array
*/ */
export const quartOut = [.165, .84, .44, 1] export const quartOut: Easing = [.165, .84, .44, 1]
/** /**

45
src/animations/reveal.ts Normal file
View File

@@ -0,0 +1,45 @@
import { animate, inView, stagger } from 'motion'
import { quartOut } from '$animations/easings'
const defaultOptions = {
stagger: null,
delay: 0,
duration: 1.6,
easing: quartOut,
}
export default (node: Element | any, {
enable = true,
children = undefined,
animation = [],
options = defaultOptions,
}: RevealOptions) => {
if (!enable) return
// Define targets from children, if empty get node
const targets = children ? node.querySelectorAll(children) : [node]
// If animation has opacity starting with 0, hide it first
if (animation.opacity && animation.opacity[0] === 0) {
targets.forEach((el: HTMLElement) => el.style.opacity = '0')
}
// Create inView instance
inView(node, ({ isIntersecting }) => {
const anim = animate(
targets,
animation,
{
delay: options.stagger ? stagger(options.stagger, { start: options.delay }) : options.delay,
duration: options.duration,
easing: options.easing,
}
)
anim.stop()
// Run animation if in view and tab is active
isIntersecting && requestAnimationFrame(anim.play)
}, {
amount: options.threshold,
})
}

11
src/app.d.ts vendored
View File

@@ -1,5 +1,6 @@
/// <reference types="@sveltejs/kit" /> /// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
declare namespace App { declare namespace App {
@@ -25,7 +26,7 @@ declare namespace svelte.JSX {
/** /**
* Custom Types * Custom Types
*/ */
declare type PhotoGridAbout = { declare interface PhotoGridAbout {
id: string id: string
title: string title: string
slug: string slug: string
@@ -62,7 +63,7 @@ declare interface smoothScrollOptions {
/** /**
* Swipe options * Swipe options
*/ */
interface SwipeOptions { declare interface SwipeOptions {
travelX?: number travelX?: number
travelY?: number travelY?: number
timeframe?: number timeframe?: number
@@ -72,17 +73,17 @@ interface SwipeOptions {
/** /**
* Reveal Animation * Reveal Animation
*/ */
declare interface RevealOptions { declare type RevealOptions = {
enable?: boolean enable?: boolean
options?: TransitionOptions options?: TransitionOptions
children?: string | HTMLElement children?: string | HTMLElement
animation: any animation: any
} }
// Options interface // Options interface
declare interface TransitionOptions { declare type TransitionOptions = {
threshold?: number threshold?: number
duration?: number duration?: number
stagger?: number stagger?: number
delay?: number delay?: number
easing?: any easing?: string | Easing
} }

View File

@@ -33,6 +33,7 @@
on:blur={() => sendHover(false)} on:blur={() => sendHover(false)}
> >
{#if url} {#if url}
<div class="photo-card__content">
<a href={url} sveltekit:noscroll> <a href={url} sveltekit:noscroll>
<Image <Image
{id} {id}
@@ -55,6 +56,7 @@
</div> </div>
{/if} {/if}
</a> </a>
</div>
{:else} {:else}
<Image <Image
{id} {id}

View File

@@ -6,7 +6,7 @@
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { flip } from 'svelte/animate' import { flip } from 'svelte/animate'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { reveal, fly } from '$animations/index' import reveal from '$animations/reveal'
import { send, receive } from '$animations/crossfade' import { send, receive } from '$animations/crossfade'
import { throttle } from '$utils/functions' import { throttle } from '$utils/functions'
import { sendEvent } from '$utils/analytics' import { sendEvent } from '$utils/analytics'
@@ -67,14 +67,13 @@
<ul class="browse__locations" <ul class="browse__locations"
use:reveal={{ use:reveal={{
animation: fly, children: '.location',
animation: { y: ['20%', 0], opacity: [0, 1] },
options: { options: {
children: 'li', stagger: 0.105,
stagger: 100, duration: 1,
duration: 1200,
from: '20%',
},
threshold: 0.3, threshold: 0.3,
},
}} }}
> >
{#each filteredLocations as location (location)} {#each filteredLocations as location (location)}

View File

@@ -3,14 +3,15 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { navigating, page } from '$app/stores' import { page, navigating } from '$app/stores'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import anime, { type AnimeTimelineInstance } from 'animejs' import { timeline } from 'motion'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime.js' import relativeTime from 'dayjs/plugin/relativeTime.js'
import { getAssetUrlKey } from '$utils/helpers' import { getAssetUrlKey } from '$utils/helpers'
import { quartOut } from '$animations/easings'
import { fetchAPI } from '$utils/api' import { fetchAPI } from '$utils/api'
import { DURATION } from '$utils/contants' import { DELAY, DURATION } from '$utils/contants'
import { photoFields } from '.' import { photoFields } from '.'
// Components // Components
import Metas from '$components/Metas.svelte' import Metas from '$components/Metas.svelte'
@@ -146,43 +147,51 @@
/** /**
* Animations * Animations
*/ */
// Transition in const animationDelay = $navigating ? DURATION.PAGE_IN : 0
const timeline: AnimeTimelineInstance = anime.timeline({ const animation = timeline([
duration: 1600,
easing: 'easeOutQuart',
autoplay: false,
})
// Title word // Title word
timeline.add({ ['.location-page__intro .word', {
targets: '.location-page__intro .word', y: ['110%', 0],
translateY: ['110%', 0], }, {
delay: anime.stagger(200) at: 0.2 + animationDelay,
}, 200 + ($navigating ? DURATION.PAGE_IN : 0)) }],
// Illustration // Illustration
timeline.add({ ['.location-page__illustration', {
targets: '.location-page__illustration',
scale: [1.06, 1], scale: [1.06, 1],
opacity: [0, 1], opacity: [0, 1],
duration: 2400, }, {
}, 400 + ($navigating ? DURATION.PAGE_IN : 0)) at: 0.4 + animationDelay,
duration: 2.4,
}],
// Title of // Title of
timeline.add({ ['.location-page__intro .of', {
targets: '.location-page__intro .of',
opacity: [0, 1], opacity: [0, 1],
duration: 1200, }, {
}, 950 + ($navigating ? DURATION.PAGE_IN : 0)) at: 0.95 + animationDelay,
duration: 1.2,
}],
// Description // Description
timeline.add({ ['.location-page__description', {
targets: '.location-page__description', y: ['10%', 0],
translateY: ['10%', 0],
opacity: [0, 1], opacity: [0, 1],
}, 900 + ($navigating ? DURATION.PAGE_IN : 0)) }, {
at: 0.9 + animationDelay,
duration: 1.2,
}]
], {
delay: DELAY.PAGE_LOADING / 1000,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
requestAnimationFrame(timeline.play) // Run animation
requestAnimationFrame(animation.play)
// Destroy // Destroy

View File

@@ -4,7 +4,9 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import { onMount } from 'svelte'
import anime, { type AnimeTimelineInstance } from 'animejs' import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/contants'
import { quartOut } from 'svelte/easing'
// Components // Components
import Metas from '$components/Metas.svelte' import Metas from '$components/Metas.svelte'
import PageTransition from '$components/PageTransition.svelte' import PageTransition from '$components/PageTransition.svelte'
@@ -19,39 +21,40 @@
/** /**
* Animations * Animations
*/ */
// Setup animations const animation = timeline([
const timeline: AnimeTimelineInstance = anime.timeline({ // Heading
duration: 1600, ['.heading .text', {
easing: 'easeOutQuart', y: [24, 0],
autoplay: false, opacity: [0, 1],
}) }],
anime.set('.heading .text, .credits__category > ul > li', { // Categories
opacity: 0, ['.credits__category', {
translateY: 24, opacity: [0, 1],
}) }, {
anime.set('.credits__category', { at: 0,
opacity: 0, delay: stagger(0.35, { start: 0.5 }),
}) }],
// Elements
timeline.add({
targets: '.heading .text, .credits__category',
opacity: 1,
translateY: 0,
delay: anime.stagger(350),
}, 500)
// Names // Names
timeline.add({ ['.credits__category > ul > li', {
targets: '.credits__category > ul > li', y: [24, 0],
opacity: 1, opacity: [0, 1],
translateY: 0, }, {
delay: anime.stagger(350), at: 1.1,
}, 1100) delay: stagger(0.35),
}],
], {
delay: DELAY.PAGE_LOADING / 1000,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Transition in // Run animation
requestAnimationFrame(timeline.play) requestAnimationFrame(animation.play)
}) })
</script> </script>

View File

@@ -5,10 +5,11 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores' import { page } from '$app/stores'
import { getContext, onMount } from 'svelte' import { getContext, onMount } from 'svelte'
import anime, { type AnimeTimelineInstance } from 'animejs' import { timeline, stagger } from 'motion'
import { DELAY } from '$utils/contants' import { DELAY } from '$utils/contants'
import { sleep, smoothScroll } from '$utils/functions' import { smoothScroll } from '$utils/functions'
import { reveal, fade as animeFade } from '$animations/index' import { reveal, fade as animeFade } from '$animations/index'
import { quartOut } from '$animations/easings'
// Components // Components
import Metas from '$components/Metas.svelte' import Metas from '$components/Metas.svelte'
import PageTransition from '$components/PageTransition.svelte' import PageTransition from '$components/PageTransition.svelte'
@@ -30,36 +31,42 @@
const { settings, locations }: any = getContext('global') const { settings, locations }: any = getContext('global')
let scrollY: number, innerHeight: number let scrollY: number, innerHeight: number
let timeline: AnimeTimelineInstance
onMount(() => { onMount(() => {
timeline = anime.timeline({ /**
duration: 1600, * Animations
easing: 'easeOutQuart', */
autoplay: false, const animation = timeline([
})
// Reveal text // Reveal text
timeline.add({ ['.homepage__headline', {
targets: '.homepage__headline', y: [16, 0],
translateY: [16, 0],
opacity: [0, 1], opacity: [0, 1],
}, 750) }, {
at: 0.75,
}],
// Animate collage photos // Animate collage photos
timeline.add({ ['.collage .photo-card', {
targets: '.collage .photo-card', y: ['33.33%', 0],
translateY: ['33.33%', 0], rotate: [-4, 0],
rotate (item: HTMLElement) {
return [-4, getComputedStyle(item).getPropertyValue('--rotation')]
},
opacity: [0, 1], opacity: [0, 1],
duration: 1200, }, {
delay: anime.stagger(75), at: 0,
}, 0) duration: 1.2,
delay: stagger(0.075),
}]
], {
delay: DELAY.PAGE_LOADING / 1000,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
sleep(DELAY.PAGE_LOADING).then(timeline.play) // Run animation
requestAnimationFrame(animation.play)
}) })
</script> </script>

View File

@@ -7,11 +7,13 @@
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { getContext, onMount } from 'svelte' import { getContext, onMount } from 'svelte'
import { fly } from 'svelte/transition' import { fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { quartOut as quartOutSvelte } from 'svelte/easing'
import relativeTime from 'dayjs/plugin/relativeTime.js' import relativeTime from 'dayjs/plugin/relativeTime.js'
import anime, { type AnimeTimelineInstance } from 'animejs' import { stagger, timeline } from 'motion'
import { DELAY } from '$utils/contants'
import { fetchAPI } from '$utils/api' import { fetchAPI } from '$utils/api'
import { quartOut } from '$animations/easings'
import { map, lerp, throttle } from '$utils/functions' import { map, lerp, throttle } from '$utils/functions'
// Components // Components
import Metas from '$components/Metas.svelte' import Metas from '$components/Metas.svelte'
@@ -282,37 +284,30 @@
const existingPhotos = photosGridEl.querySelectorAll('.photo') const existingPhotos = photosGridEl.querySelectorAll('.photo')
existingPhotos.forEach(el => observerPhotos.observe(el)) existingPhotos.forEach(el => observerPhotos.observe(el))
/** /**
* Animations * Animations
*/ */
// Transition in const animation = timeline([
const timeline: AnimeTimelineInstance = anime.timeline({
duration: 1600,
easing: 'easeOutQuart',
autoplay: false,
})
// Reveal text // Reveal text
timeline.add({ ['.photos-page__intro .discover, .photos-page__intro .filters__bar', {
targets: '.photos-page__intro .discover', y: [16, 0],
translateY: [16, 0],
opacity: [0, 1], opacity: [0, 1],
}, 900) }, {
at: 0.4,
delay: stagger(0.25),
}]
], {
delay: DELAY.PAGE_LOADING / 1000,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Filters // Run animation
timeline.add({ requestAnimationFrame(animation.play)
targets: '.photos-page__intro .filters',
translateY: [16, 0],
opacity: [0, 1],
complete ({ animatables }) {
const element = animatables[0].target
// Remove style to not interfere with CSS when scrolling back up over photos
element.removeAttribute('style')
}
}, 1300)
// Play animation
requestAnimationFrame(timeline.play)
// Destroy // Destroy
@@ -351,9 +346,8 @@
class:is-transitioning={filtersTransitioning} class:is-transitioning={filtersTransitioning}
class:is-visible={filtersVisible} class:is-visible={filtersVisible}
> >
<span class="text-label filters__label">Filter photos</span>
<div class="filters__bar"> <div class="filters__bar">
<span class="text-label filters__label">Filter photos</span>
<ul> <ul>
<li> <li>
<Select <Select
@@ -418,7 +412,7 @@
{#if filtered} {#if filtered}
<button class="reset button-link" <button class="reset button-link"
on:click={resetFiltered} on:click={resetFiltered}
transition:fly={{ y: 4, duration: 600, easing: quartOut }} transition:fly={{ y: 4, duration: 600, easing: quartOutSvelte }}
> >
Reset Reset
</button> </button>

View File

@@ -4,8 +4,10 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { stagger, timeline } from 'motion'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import anime, { type AnimeTimelineInstance } from 'animejs' import { DELAY } from '$utils/contants'
import { quartOut } from '$animations/easings'
// Components // Components
import Metas from '$components/Metas.svelte' import Metas from '$components/Metas.svelte'
import PageTransition from '$components/PageTransition.svelte' import PageTransition from '$components/PageTransition.svelte'
@@ -22,37 +24,36 @@
/** /**
* Animations * Animations
*/ */
// Setup animations const animation = timeline([
const timeline: AnimeTimelineInstance = anime.timeline({
duration: 1600,
easing: 'easeOutQuart',
autoplay: false,
})
anime.set('.heading .text, .subscribe__top .newsletter-form, .subscribe__issues', {
opacity: 0,
translateY: 24,
})
// Elements // Elements
timeline.add({ ['.heading .text, .subscribe__top .newsletter-form, .subscribe__issues', {
targets: '.heading .text, .subscribe__top .newsletter-form, .subscribe__issues', y: [24, 0],
opacity: 1, opacity: [0, 1],
translateY: 0, }, {
delay: anime.stagger(200), at: 0.5,
}, 500) delay: stagger(0.35),
}],
// Reveal each issue // Reveal each issue
timeline.add({ ['.subscribe__issues > ul > li', {
targets: '.subscribe__issues .issue', y: [16, 0],
opacity: [0, 1], opacity: [0, 1],
translateY: [16, 0], }, {
delay: anime.stagger(150), duration: 1,
duration: 1000, at: 1.5,
}, 1000) delay: stagger(0.15),
}],
], {
delay: DELAY.PAGE_LOADING / 1000,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Transition in // Run animation
requestAnimationFrame(timeline.play) requestAnimationFrame(animation.play)
}) })
</script> </script>
@@ -75,7 +76,8 @@
<h2 class="title-small">Past Issues</h2> <h2 class="title-small">Past Issues</h2>
<ul> <ul>
{#each issues as { issue, title, date_sent, link, thumbnail: { id } }} {#each issues as { issue, title, date_sent, link, thumbnail: { id } }}
<li class="issue"> <li>
<div class="issue">
<a href={link} target="_blank" rel="external noreferrer noopener" tabindex="0"> <a href={link} target="_blank" rel="external noreferrer noopener" tabindex="0">
<Image <Image
id={id} id={id}
@@ -91,6 +93,7 @@
</dd> </dd>
</dl> </dl>
</a> </a>
</div>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@@ -1,20 +1,21 @@
.photo-card { .photo-card {
& > * {
border-radius: 8px;
}
a { a {
display: block; display: block;
overflow: hidden; overflow: hidden;
cursor: zoom-in; cursor: zoom-in;
border-radius: 8px;
box-shadow: 0 16px 12px rgba(#000, 0.15), 0 26px 52px rgba(#000, 0.2); box-shadow: 0 16px 12px rgba(#000, 0.15), 0 26px 52px rgba(#000, 0.2);
transition: transform 0.7s var(--ease-quart); transition: transform 0.7s var(--ease-quart);
transform: translateZ(0);
will-change: transform; will-change: transform;
} }
// Image
:global(picture) { :global(picture) {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: $color-primary-tertiary20; background: $color-primary-tertiary20;
}
:global(img) { :global(img) {
display: block; display: block;
width: calc(100% + 2px); width: calc(100% + 2px);
@@ -22,6 +23,7 @@
object-fit: cover; object-fit: cover;
transition: opacity 0.7s var(--ease-quart); transition: opacity 0.7s var(--ease-quart);
} }
}
// Informations // Informations
&__info { &__info {
@@ -92,7 +94,8 @@
transition: opacity 0.8s var(--ease-quart); transition: opacity 0.8s var(--ease-quart);
} }
// Slightly zoom in and show info on hover
// Slightly zoom in, show gradient and info on hover
@media (hover: hover) { @media (hover: hover) {
a:hover { a:hover {
transform: scale(1.0375) rotate(2deg) translateZ(0); transform: scale(1.0375) rotate(2deg) translateZ(0);
@@ -109,6 +112,7 @@
transition-delay: 180ms; transition-delay: 180ms;
} }
} }
&:after { &:after {
opacity: 1; opacity: 1;
} }

View File

@@ -20,12 +20,15 @@
height: 100%; height: 100%;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
transform: rotate(var(--rotation)) translateZ(0);
@include bp (sm) { @include bp (sm) {
height: clamp(156px, 18vw, 400px); height: clamp(156px, 18vw, 400px);
} }
& > :global(*) {
transform: rotate(var(--rotation)) translateZ(0);
}
// First row // First row
// Mobile: Top left // Mobile: Top left
&:nth-child(1) { &:nth-child(1) {

View File

@@ -254,7 +254,6 @@
** Filters ** Filters
*/ */
.filters { .filters {
position: relative;
max-width: 982px; max-width: 982px;
margin: 0 auto; margin: 0 auto;
padding: 0 16px; padding: 0 16px;
@@ -265,6 +264,7 @@
// Bar // Bar
&__bar { &__bar {
position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -364,7 +364,7 @@
@include bp (sm) { @include bp (sm) {
position: absolute; position: absolute;
left: 64px; left: 32px;
top: 52%; top: 52%;
transform: translateY(-50%); transform: translateY(-50%);
margin-bottom: 0; margin-bottom: 0;