WIP Animations all over site

- Run a transition In for each page
- Involve a "loader" panel on page change
- TODO: tweak the animations and finish the concept
This commit is contained in:
2020-03-06 14:22:51 +01:00
parent cd1033f97b
commit 9ffc210c02
27 changed files with 827 additions and 296 deletions

View File

@@ -0,0 +1,56 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import { animDuration } from '../utils/store'
/*
** Transition In
*/
export const animateIn = () => {
// Card: Active
const cardActive = ScrollOut({
once: true,
targets: '.gallery__photo--active',
onShown (el) {
anime({
targets: el,
top: [32, 0],
duration: 1200,
delay: 650,
easing: 'easeOutQuart'
})
}
})
// Card: Prev
const cardPrev = ScrollOut({
once: true,
targets: '.gallery__photo--prev',
onShown (el) {
anime({
targets: el,
top: [5, 0],
left: [-64, 0],
duration: 1200,
delay: 690,
easing: 'easeOutQuart'
})
}
})
// Card: Prev
const cardNext = ScrollOut({
once: true,
targets: '.gallery__photo--next',
onShown (el) {
anime({
targets: el,
top: [5, 0],
left: [48, 0],
duration: 1200,
delay: 710,
easing: 'easeOutQuart'
})
}
})
}

View File

@@ -0,0 +1,47 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import { animDuration } from '../utils/store'
/*
** Transition In
*/
export const animateIn = () => {
// Each location
const locations = ScrollOut({
targets: '#locations_list .location',
onShown (el) {
// Timeline
const tl = anime.timeline({
autoplay: false,
duration: 600,
delay: anime.stagger(200),
easing: 'easeOutQuart'
})
// Image
tl.add({
targets: el.querySelector('img'),
scale: [1.3, 1],
opacity: [0, 1],
duration: 1800,
delay: 100
})
// Name
tl.add({
targets: el.querySelector('h3'),
translateY: ['100%', 0]
}, 300)
// Country
tl.add({
targets: el.querySelector('p'),
translateY: ['100%', 0]
}, 550)
// Play
tl.play()
}
})
}

View File

@@ -0,0 +1,34 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import { animDuration } from '../utils/store'
/*
** Transition In
*/
export const animateIn = callback => {
// On scroll animation
const title = ScrollOut({
once: true,
targets: '.title-location',
onShown (el) {
// Each letters
anime({
targets: el.querySelectorAll('span'),
translateY: ['100%', 0],
delay: anime.stagger(40),
duration: 1000,
easing: 'easeOutQuart'
})
// Word in between
anime({
targets: el.querySelectorAll('em span'),
opacity: [0, 1],
delay: anime.stagger(80, { start: 400 }),
duration: 1000,
easing: 'easeOutQuart'
})
}
})
}

View File

@@ -0,0 +1,61 @@
import anime from 'animejs'
import { animDuration, animDurationLong } from '../utils/store'
/*
** Transition In
*/
export const animateIn = () => {
// Panel itself
const transition = anime({
targets: '#transition',
height: ['100%', '100%'],
opacity: [0, 1],
duration: 200,
delay: 0,
easing: 'easeInOutQuart'
})
// Globe icon
const globe = anime({
targets: '#transition svg',
opacity: [0, 1],
duration: 200,
delay: 0,
easing: 'easeInOutQuart'
})
}
/*
** Transition Out
*/
export const animateOut = callback => {
// Panel itself
const transition = anime({
targets: '#transition',
height: ['100%', 0],
duration: animDurationLong,
delay: 800,
easing: 'easeInOutQuart',
complete: callback
})
// Title
const title = anime({
targets: '#transition .title-location',
opacity: 0,
duration: 600,
delay: 1400,
easing: 'easeInOutQuart'
})
// Globe icon
const globe = anime({
targets: '#transition svg',
opacity: 0,
duration: 600,
delay: 1400,
easing: 'easeInOutQuart'
})
}

View File

@@ -1,10 +1,8 @@
// import anime from 'animejs'
import { crossfade } from 'svelte/transition' import { crossfade } from 'svelte/transition'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
// Crossfade transition // Crossfade transition
export const [crossfadeSend, crossfadeReceive] = crossfade({ export const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200), duration: d => Math.sqrt(d * 200),
fallback(node, params) { fallback(node, params) {

121
src/animations/index.js Normal file
View File

@@ -0,0 +1,121 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import { animDuration } from '../utils/store'
import { debounce } from '../utils/functions'
/*
** Transition In
*/
export const animateIn = () => {
// Title: Houses
const titleHouses = ScrollOut({
once: true,
targets: '#title-houses',
onShown (el) {
// Reveal
anime({
targets: el.querySelectorAll('span'),
translateY: ['-70%', 0],
delay: anime.stagger(80),
duration: animDuration,
easing: 'easeOutQuart'
})
// Parallax on scroll
const translate = anime.timeline({
autoplay: false,
duration: animDuration
})
translate.add({
targets: el,
translateX: ['-3%', '-17%'],
easing: 'easeOutQuart',
duration: animDuration
})
window.addEventListener('scroll', fn.debounce(event => {
translate.seek(translate.duration * (window.scrollY / 1000))
}), 50)
}
})
// Intro: Description
const introDescription = ScrollOut({
once: true,
targets: '#intro-description',
onShown (el) {
anime({
targets: el.querySelectorAll('p, a'),
opacity: [0, 1],
translateY: [8, 0],
duration: animDuration,
delay: anime.stagger(200, { start: 400 }),
easing: 'easeOutQuart'
})
}
})
// Intro: Carousel
const introCarousel = ScrollOut({
once: true,
targets: '#intro-carousel',
onShown(el) {
anime({
targets: el,
opacity: [0, 1],
translateY: [24, 0],
duration: animDuration,
delay: 650,
easing: 'easeOutQuart'
})
}
})
// Title: Of
const titleOf = ScrollOut({
once: true,
targets: '#title-of',
onShown (el) {
anime({
targets: el.querySelectorAll('span'),
translateY: ['100%', 0],
delay: anime.stagger(70),
duration: animDuration,
easing: 'easeOutQuart'
})
}
})
// Title: World
const titleWorld = ScrollOut({
once: true,
targets: '#title-world',
onShown (el, ctx) {
anime({
targets: el.querySelectorAll('span'),
translateY: ['100%', 0],
delay: anime.stagger(70),
duration: animDuration,
easing: 'easeOutQuart'
})
// Parallax on scroll
const translate = anime.timeline({
autoplay: false,
duration: animDuration
})
translate.add({
targets: el,
translateX: ['4%', '-4%'],
easing: 'easeOutQuart',
duration: animDuration
})
if (ctx.visible) {
window.addEventListener('scroll', debounce(event => {
translate.seek(translate.duration * (window.scrollY / 1000))
}), 35)
}
}
})
}

25
src/animations/page.js Normal file
View File

@@ -0,0 +1,25 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import { animDuration, animDurationLong } from '../utils/store'
/*
** Transition In
*/
export const animateIn = () => {
// Simple fade
const page = ScrollOut({
once: true,
targets: '.page',
onShown (el) {
anime({
targets: '.page__part',
opacity: [0, 1],
translateY: [8, 0],
duration: 1800,
delay: anime.stagger(220, { start: animDurationLong * 0.3 }),
easing: 'easeOutQuart'
})
}
})
}

60
src/animations/place.js Normal file
View File

@@ -0,0 +1,60 @@
import anime from 'animejs'
// import ScrollOut from 'scroll-out'
import { animDuration } from '../utils/store'
/*
** Transition In
*/
export const animateIn = () => {
const tl = anime.timeline({
duration: 1800,
easing: 'easeOutQuart'
})
// Title: Houses
tl.add({
targets: '.place__title_houses',
translateY: ['150%', 0],
duration: animDuration
})
// Title: Of
tl.add({
targets: '.place__title_of',
opacity: [0, 1],
duration: 800
}, 550)
// Title: Place name
tl.add({
targets: '.place__title_name',
translateY: ['150%', 0],
duration: animDuration
}, 200)
// Switcher link
tl.add({
targets: '.place__title .button-control',
scale: [0.95, 1],
opacity: [0, 1],
duration: animDuration
}, 700)
// Illustration
tl.add({
targets: '.place__illustration',
scale: [1.075, 1],
opacity: [0, 1],
duration: animDuration
}, 200)
// Description
tl.add({
targets: '.place__description',
opacity: [0, 1],
translateY: [24, 0],
duration: animDuration
}, 800)
// Play
tl.play()
}

View File

@@ -1,17 +1,33 @@
<script> <script>
import { onMount } from 'svelte'
import { lettersToSpan } from '../utils/functions' import { lettersToSpan } from '../utils/functions'
// Animations
import { animateIn } from '../animations/TitleSite.js'
/*
** Run code on component mount
*/
onMount(() => {
// Entering transition
animateIn()
})
</script> </script>
<div class="title-location title-location--inline"> <div class="title-location title-location--inline">
<div role="heading" aria-level="1" aria-label="Houses" data-aos="letters-translate-bottom"> <div role="heading" aria-level="1" aria-label="Houses">
<div class="anim-mask"> <div class="anim-mask">
{@html lettersToSpan('Houses')} {@html lettersToSpan('Houses')}
</div> </div>
</div> </div>
<em>of the</em> <em>
<span>of</span>
<span>the</span>
</em>
<div aria-label="World" data-aos="letters-translate-bottom"> <div aria-label="World">
<div class="anim-mask"> <div class="anim-mask">
{@html lettersToSpan('World')} {@html lettersToSpan('World')}
</div> </div>

View File

@@ -40,7 +40,7 @@
// const globe = document.querySelector('.globe img') // const globe = document.querySelector('.globe img')
// const places = document.querySelectorAll('.globe .pin--place') // const places = document.querySelectorAll('.globe .pin--place')
// // Init function // Init function
// init = () => { // init = () => {
// mapWidth = globe.getBoundingClientRect().width // mapWidth = globe.getBoundingClientRect().width
// mapHeight = globe.getBoundingClientRect().height // mapHeight = globe.getBoundingClientRect().height
@@ -59,7 +59,7 @@
// init() // init()
// } // }
// // init() // init()
}) })
</script> </script>

View File

@@ -3,7 +3,7 @@
export let location export let location
</script> </script>
<div class="location" data-aos="location"> <div class="location">
<a href="/location/{location.country.slug}/{location.slug}" rel="prefetch" sapper-noscroll> <a href="/location/{location.country.slug}/{location.slug}" rel="prefetch" sapper-noscroll>
<img src={location.country.flag.full_url} alt="Flag of {location.country.name}"> <img src={location.country.flag.full_url} alt="Flag of {location.country.name}">
<div class="anim-mask mask-city"> <div class="anim-mask mask-city">

View File

@@ -3,9 +3,6 @@
import { fly } from 'svelte/transition' import { fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing' import { quartOut } from 'svelte/easing'
import { site, currentLocation } from '../utils/store' import { site, currentLocation } from '../utils/store'
// Dependencies
import * as basicScroll from 'basicscroll'
import { getThumbnail, formatDate } from '../utils/functions' import { getThumbnail, formatDate } from '../utils/functions'
// Props and variables // Props and variables
@@ -29,27 +26,27 @@
*/ */
onMount(() => { onMount(() => {
// Parallax on photo when the image has been loaded // Parallax on photo when the image has been loaded
const parallaxNumber = basicScroll.default.create({ // const parallaxNumber = basicScroll.default.create({
elem: photoElement.querySelector('.photo__number'), // elem: photoElement.querySelector('.photo__number'),
direct: photoElement, // direct: photoElement,
from: 'top-bottom', // from: 'top-bottom',
to: 'bottom-top', // to: 'bottom-top',
props: { // props: {
'--translate': { // '--translate': {
from: '-75%', // from: '-75%',
to: '-25%' // to: '-25%'
} // }
} // }
}) // })
parallaxNumber.start() // parallaxNumber.start()
parallaxNumber.calculate() // parallaxNumber.calculate()
parallaxNumber.update() // parallaxNumber.update()
}) })
</script> </script>
<div class="photo" <div class="photo"
bind:this={photoElement} bind:this={photoElement}
transition:fly="{{ y: 40, duration: 1000, easing: quartOut }}" in:fly="{{ y: 40, duration: 1000, easing: quartOut }}"
> >
<div class="photo__location wrap"> <div class="photo__location wrap">
<div class="wrapper"> <div class="wrapper">

View File

@@ -6,6 +6,9 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const { page } = stores() const { page } = stores()
// Animations
import { animateIn } from '../animations/carousel'
// Components // Components
import IconArrow from '../atoms/IconArrow' import IconArrow from '../atoms/IconArrow'
@@ -89,6 +92,9 @@
** Run code on browser only ** Run code on browser only
*/ */
onMount(() => { onMount(() => {
// Entering transition
animateIn()
// Hover function // Hover function
hover = event => { hover = event => {
const button = event.currentTarget.querySelector('button') const button = event.currentTarget.querySelector('button')

View File

@@ -1,17 +1,17 @@
<script> <script>
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { flip } from 'svelte/animate' import { flip } from 'svelte/animate'
import { crossfadeReceive, crossfadeSend } from '../utils/animations' import { receive, send } from '../animations/crossfade'
import { locations, countries, continents } from '../utils/store' import { locations, countries, continents } from '../utils/store'
// Dependencies
import AOS from 'aos'
import { throttle } from '../utils/functions' import { throttle } from '../utils/functions'
// Components // Components
import Button from '../atoms/Button' import Button from '../atoms/Button'
import Location from '../molecules/Location' import Location from '../molecules/Location'
// Animations
import { animateIn } from '../animations/Locations'
// Variables // Variables
const transitionDuration = 800 const transitionDuration = 800
let filterLocations let filterLocations
@@ -31,13 +31,11 @@
/* /*
** Run code on browser only ** Run code on component mount
*/ */
onMount(() => { onMount(() => {
// Scroll apparitions // Entering transition
if (process.browser) { animateIn()
AOS.init()
}
}) })
</script> </script>
@@ -62,8 +60,8 @@
<div class="browse__locations" id="locations_list"> <div class="browse__locations" id="locations_list">
{#each filteredLocations as location (location.id)} {#each filteredLocations as location (location.id)}
<div animate:flip="{{ duration: transitionDuration }}" <div animate:flip="{{ duration: transitionDuration }}"
in:crossfadeReceive="{{ key: location.id }}" in:receive="{{ key: location.id }}"
out:crossfadeSend="{{ key: location.id }}" out:send="{{ key: location.id }}"
> >
<Location {location} /> <Location {location} />
</div> </div>

View File

@@ -2,9 +2,6 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { site } from '../utils/store' import { site } from '../utils/store'
// Dependencies
import AOS from 'aos'
// Components // Components
import IconArrow from '../atoms/IconArrow' import IconArrow from '../atoms/IconArrow'
import TitleSite from '../atoms/TitleSite' import TitleSite from '../atoms/TitleSite'
@@ -21,10 +18,7 @@
** Run code on browser only ** Run code on browser only
*/ */
onMount(() => { onMount(() => {
// Scroll apparitions
if (process.browser) {
AOS.init()
}
}) })
</script> </script>

View File

@@ -1,11 +1,5 @@
<script context="module"> <script context="module">
import { import { apiEndpoints, } from '../utils/store'
apiEndpoints,
site,
continents,
countries,
locations
} from '../utils/store'
export async function preload (page, session) { export async function preload (page, session) {
const req = await this.fetch(apiEndpoints.gql, { const req = await this.fetch(apiEndpoints.gql, {
@@ -66,11 +60,27 @@
</script> </script>
<script> <script>
// Manipulate countries data import { onMount } from 'svelte'
import {
site,
continents,
countries,
locations
} from '../utils/store'
// Components
import Transition from '../utils/Transition'
// Props
export const segment = null
/*
** Manipulate data
*/
// Replace each countrie's continent by the database // Replace each countrie's continent by the database
$countries.forEach(count => count.continent = $continents.find(cont => cont.id === count.continent.id)) $countries.forEach(count => count.continent = $continents.find(cont => cont.id === count.continent.id))
// Manipulate continents data
// Push each country to its continent // Push each country to its continent
$countries.forEach(country => { $countries.forEach(country => {
const continent = $continents.find(cont => cont.id === country.continent.id) const continent = $continents.find(cont => cont.id === country.continent.id)
@@ -78,13 +88,15 @@
!continent.countries.includes(country) && continent.countries.push(country) !continent.countries.includes(country) && continent.countries.push(country)
}) })
// Manipulate locations data
// Replace each location's country by the database // Replace each location's country by the database
$locations.forEach(loc => loc.country = $countries.find(cont => cont.id === loc.country.id)) $locations.forEach(loc => loc.country = $countries.find(cont => cont.id === loc.country.id))
</script> </script>
<style lang="scss" global> <style lang="scss" global>
@import "../style/style.scss"; @import "../style/style.scss";
</style> </style>
<slot></slot> <slot></slot>
<Transition />

View File

@@ -3,12 +3,11 @@
import { import {
site, site,
currentLocation, currentLocation,
currentPhotos currentPhotos,
pageReady,
pageTransition
} from '../utils/store' } from '../utils/store'
// Depencencies
import AOS from 'aos'
// Components // Components
import IconArrow from '../atoms/IconArrow' import IconArrow from '../atoms/IconArrow'
import TitleSite from '../atoms/TitleSite' import TitleSite from '../atoms/TitleSite'
@@ -17,6 +16,11 @@
import Footer from '../organisms/Footer' import Footer from '../organisms/Footer'
import SocialMetas from '../utils/SocialMetas' import SocialMetas from '../utils/SocialMetas'
// Animations
import { animateIn } from '../animations/page'
pageTransition.onAnimationEnd = animateIn
// Reset current location if existing // Reset current location if existing
$: { $: {
if ($currentLocation) currentLocation.set() if ($currentLocation) currentLocation.set()
@@ -25,13 +29,11 @@
/* /*
** Run code on browser only ** Run code when mounted
*/ */
onMount(() => { onMount(() => {
// Scroll apparitions // Page is loaded
if (process.browser) { pageReady.set(true)
AOS.init()
}
}) })
</script> </script>
@@ -57,7 +59,7 @@
<TitleSite /> <TitleSite />
</div> </div>
<div class="page__description style-description"> <div class="page__description page__part style-description">
<p>{$site.explore_globe}</p> <p>{$site.explore_globe}</p>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,6 @@
<script> <script>
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { site } from '../utils/store' import { site, pageReady } from '../utils/store'
// Dependencies
import AOS from 'aos'
// Components // Components
import IconArrow from '../atoms/IconArrow' import IconArrow from '../atoms/IconArrow'
@@ -13,18 +10,17 @@
import Footer from '../organisms/Footer' import Footer from '../organisms/Footer'
import SocialMetas from '../utils/SocialMetas' import SocialMetas from '../utils/SocialMetas'
// Categories // Animations
$: credits = $site.credits_list import { animateIn } from '../animations/page'
pageTransition.onAnimationEnd = animateIn
/* /*
** Run code on browser only ** Run code when mounted
*/ */
onMount(() => { onMount(() => {
// Scroll apparitions // Page is loaded
if (process.browser) { pageReady.set(true)
AOS.init()
}
}) })
</script> </script>
@@ -41,7 +37,7 @@
<section class="page"> <section class="page">
<div class="wrap"> <div class="wrap">
<div class="page__top"> <div class="page__top page__part">
<a href="/" class="button-control button-control--pink dir-left"> <a href="/" class="button-control button-control--pink dir-left">
<IconArrow direction="left" color="#fff" class="icon" /> <IconArrow direction="left" color="#fff" class="icon" />
<IconArrow direction="left" color="#fff" class="icon" hidden="true" /> <IconArrow direction="left" color="#fff" class="icon" hidden="true" />
@@ -50,12 +46,12 @@
<TitleSite /> <TitleSite />
</div> </div>
<div class="page__description style-description"> <div class="page__description page__part style-description">
<p>{$site.credits_text}</p> <p>{$site.credits_text}</p>
</div> </div>
<div class="page__list"> <div class="page__list page__part">
{#each credits as category} {#each $site.credits_list as category}
<div class="page__category"> <div class="page__category">
<h2 class="title-category">{category.name}</h2> <h2 class="title-category">{category.name}</h2>
{#each category.credits as person} {#each category.credits as person}

View File

@@ -1,7 +1,5 @@
<script context="module"> <script context="module">
import { import { apiEndpoints } from '../utils/store'
apiEndpoints,
} from '../utils/store'
// Preload data // Preload data
export async function preload (page, session) { export async function preload (page, session) {
@@ -18,12 +16,16 @@
<script> <script>
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { site, currentLocation, currentPhotos } from '../utils/store' import {
site,
currentLocation,
currentPhotos,
pageReady,
pageTransition
} from '../utils/store'
import { lettersToSpan } from '../utils/functions' import { lettersToSpan } from '../utils/functions'
// Dependencies // Dependencies
import * as basicScroll from 'basicscroll'
import AOS from 'aos'
import zenscroll from 'zenscroll' import zenscroll from 'zenscroll'
// Components // Components
@@ -35,6 +37,14 @@
import Locations from '../organisms/Locations' import Locations from '../organisms/Locations'
import Footer from '../organisms/Footer' import Footer from '../organisms/Footer'
import SocialMetas from '../utils/SocialMetas' import SocialMetas from '../utils/SocialMetas'
import Transition from '../utils/Transition'
// Animations
import { animateIn } from '../animations/index'
pageTransition.onAnimationEnd = animateIn
// Props and variables
export let photos
// Reset current location if existing // Reset current location if existing
$: { $: {
@@ -42,48 +52,13 @@
if ($currentPhotos) currentPhotos.set() if ($currentPhotos) currentPhotos.set()
} }
// Props and variables
export let photos
let appearing
/* /*
** Run code on browser only ** Run code when mounted
*/ */
onMount(() => { onMount(() => {
if (process.browser) { // Page is loaded
// Scroll apparitions pageReady.set(true)
AOS.init()
// Parallax titles
const titleHouses = document.getElementById('title-houses')
const scrollTitleHouses = basicScroll.default.create({
elem: titleHouses,
direct: titleHouses,
from: 0,
to: window.innerHeight * 0.6,
props: {
'--translateX': {
from: '-3%',
to: '-20%'
}
}
}).start()
const titleWorld = document.getElementById('title-world')
const scrollTitleWorld = basicScroll.default.create({
elem: titleWorld,
direct: titleWorld,
from: document.querySelector('.explore__description').getBoundingClientRect().top,
to: document.querySelector('#title-world').getBoundingClientRect().bottom * 1.1,
props: {
'--translateX': {
from: '4%',
to: '-4%'
}
}
}).start()
}
}) })
</script> </script>
@@ -100,14 +75,14 @@
<section class="intro"> <section class="intro">
<div class="anim-mask"> <div class="anim-mask">
<div class="anim title-parallax" id="title-houses" data-aos="letters-translate-top" data-aos-once="true"> <div class="anim title-parallax" id="title-houses">
<h1 class="title-massive" aria-label="Houses"> <h1 class="title-massive" aria-label="Houses">
{@html lettersToSpan('Houses')} {@html lettersToSpan('Houses')}
</h1> </h1>
</div> </div>
</div> </div>
<div class="wrap"> <div class="wrap" id="intro-description">
<div class="intro__description style-description"> <div class="intro__description style-description">
<p>{$site.description}</p> <p>{$site.description}</p>
@@ -117,11 +92,13 @@
</div> </div>
</div> </div>
<div id="intro-carousel">
<Carousel {photos} /> <Carousel {photos} />
</div>
</section> </section>
<section class="explore explore--homepage"> <section class="explore explore--homepage">
<div class="of" aria-label="of" data-aos="letters-translate-bottom" data-aos-once="true"> <div class="of" id="title-of" aria-label="of">
<div class="anim-mask"> <div class="anim-mask">
{@html lettersToSpan('of')} {@html lettersToSpan('of')}
</div> </div>
@@ -134,7 +111,7 @@
<InteractiveGlobe /> <InteractiveGlobe />
<div class="anim-mask anim-title"> <div class="anim-mask anim-title">
<h1 class="title-massive title-parallax" id="title-world" aria-label="World" data-aos="letters-translate-bottom" data-aos-once="true"> <h1 class="title-massive title-parallax" id="title-world" aria-label="World">
{@html lettersToSpan('World')} {@html lettersToSpan('World')}
</h1> </h1>
</div> </div>

View File

@@ -1,12 +1,5 @@
<script context="module"> <script context="module">
import { import { apiEndpoints } from '../../../utils/store'
apiEndpoints,
site,
locations,
currentLocation,
currentPhotos
} from '../../../utils/store'
import { stores } from '@sapper/app'
// Preload data // Preload data
export async function preload (page, session) { export async function preload (page, session) {
@@ -22,11 +15,20 @@
<script> <script>
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { stores } from '@sapper/app'
import {
site,
locations,
currentLocation,
currentPhotos,
pageReady,
pageTransition
} from '../../../utils/store'
import { formatDate, relativeTime, getThumbnail } from '../../../utils/functions' import { formatDate, relativeTime, getThumbnail } from '../../../utils/functions'
const { page } = stores() const { page } = stores()
// Dependencies // Dependencies
import AOS from 'aos' import lazySizes from 'lazysizes'
// Components // Components
import IconGlobe from '../../../atoms/IconGlobe' import IconGlobe from '../../../atoms/IconGlobe'
@@ -39,6 +41,10 @@
import Footer from '../../../organisms/Footer' import Footer from '../../../organisms/Footer'
import SocialMetas from '../../../utils/SocialMetas' import SocialMetas from '../../../utils/SocialMetas'
// Animations
import { animateIn } from '../../../animations/place'
pageTransition.onAnimationEnd = animateIn
// Props and variables // Props and variables
export let photos export let photos
let layoutSetting let layoutSetting
@@ -70,18 +76,15 @@
/* /*
** Run code on browser only ** Run code when mounted
*/ */
onMount(() => { onMount(() => {
// Page is loaded
pageReady.set(true)
// Get layout setting from storage // Get layout setting from storage
layoutSetting = localStorage.getItem('photosLayout') layoutSetting = localStorage.getItem('photosLayout')
if (process.browser) {
// Scroll apparitions
AOS.init()
}
}) })
</script> </script>
<svelte:head> <svelte:head>
@@ -95,6 +98,7 @@
/> />
</svelte:head> </svelte:head>
<section class="place"> <section class="place">
<div class="place__title"> <div class="place__title">
<h1 class="title-location title-location--big" aria-label="Houses of {location.name}"> <h1 class="title-location title-location--big" aria-label="Houses of {location.name}">

View File

@@ -1,11 +1,6 @@
<script context="module"> <script context="module">
import { stores } from '@sapper/app' import { stores } from '@sapper/app'
import { import { apiEndpoints } from '../../../../utils/store'
apiEndpoints,
locations,
currentLocation,
currentPhotos
} from '../../../../utils/store'
// Define either to preload data or use the store // Define either to preload data or use the store
let preloaded let preloaded
@@ -16,7 +11,7 @@
export async function preload (page, session) { export async function preload (page, session) {
// Load the photos if not loaded // Load the photos if not loaded
if (!preloaded) { if (!preloaded) {
const req = await this.fetch(`${apiEndpoints.rest}/items/photos?fields=id,name,slug,image.*,location.*,location.country.*,created_on,modified_on&filter[location.slug][rlike]=%${page.params.location}%`) const req = await this.fetch(`${apiEndpoints.rest}/items/photos?fields=id,name,slug,date,image.*,location.*,location.country.*,created_on,modified_on&filter[location.slug][rlike]=%${page.params.location}%`)
const photos = await req.json() const photos = await req.json()
return { return {
photos: photos.data photos: photos.data
@@ -31,7 +26,14 @@
<script> <script>
import { onMount, createEventDispatcher } from 'svelte' import { onMount, createEventDispatcher } from 'svelte'
import { goto } from '@sapper/app' import {
site,
locations,
currentLocation,
currentPhotos,
pageReady,
pageTransition
} from '../../../../utils/store'
import { getThumbnail } from '../../../../utils/functions' import { getThumbnail } from '../../../../utils/functions'
const { page } = stores() const { page } = stores()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@@ -42,24 +44,25 @@
import Carousel from '../../../../organisms/Carousel' import Carousel from '../../../../organisms/Carousel'
import SocialMetas from '../../../../utils/SocialMetas' import SocialMetas from '../../../../utils/SocialMetas'
// Animations
// import { animateIn } from '../../../animations/photoPage'
// pageTransition.onAnimationEnd = animateIn
// Props // Props
export let photos export let photos
// Variables // Variables
let windowWidth let windowWidth
let currentPhoto let currentPhoto = photos.find(photo => photo.slug === $page.params.photo)
// Define current location // Update store current location
const location = (!$currentLocation) ? $locations.find(loc => loc.slug === $page.params.location) : $currentLocation if (!$currentLocation) currentLocation.set($locations.find(loc => loc.slug === $page.params.location))
// Update store current location informations
if (!$currentLocation) currentLocation.set(location)
if (!$currentPhotos) currentPhotos.set(photos) if (!$currentPhotos) currentPhotos.set(photos)
// The photo has changed from the carousel // The photo has changed from the carousel
const photoChanged = event => { const photoChanged = event => {
currentPhoto = event.detail.currentPhoto currentPhoto = event.detail.currentPhoto
console.log('photoChanged [photo] event: ', currentPhoto.slug)
// Change the URL to the current photo // Change the URL to the current photo
if (!event.detail.init) { if (!event.detail.init) {
@@ -82,22 +85,16 @@
/* /*
!!! TODO: ** Run code when mounted
- URL navigation on photo change (from Carousel)
- If coming to direct URL, show the proper photo (i.e currentIndex from photo.slug in URL)
- Get currentPhoto from Carousel to update title and metas
*/
/*
** Run code on browser only
*/ */
onMount(() => { onMount(() => {
// Page is loaded
pageReady.set(true)
/* /*
!!! TODO: !!! TODO:
- Change the title with the current photo name and update Metas (with window location url) - Change the title with the current photo name and update Metas (with window location url)
*/ */
// console.log(currentPhoto)
dispatch('changeUrl', { dispatch('changeUrl', {
page: $page page: $page
@@ -106,14 +103,14 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Houses Of Photos of {location.name}, {location.country.name}</title> <title>{$site.seo_name} Photos of {$currentLocation.name}, {$currentLocation.country.name}</title>
<meta name="description" content={location.description}> <meta name="description" content="{$site.seo_name} {$currentLocation.name} {$currentLocation.description}">
<SocialMetas <SocialMetas
title="Houses Of - Beautiful houses of {location.name}, {location.country.name}" title="{$site.seo_name} - Beautiful homes of {$currentLocation.name}, {$currentLocation.country.name}"
description="Houses Of {location.name} {location.description}" description="{$site.seo_name} {$currentLocation.name} {$currentLocation.description}"
image={getThumbnail(currentPhoto.image.private_hash, 1200, 630)}
url="//{$page.host.split(':3000')[0]}/viewer/{currentPhoto.location.country.slug}/{currentPhoto.location.slug}/{currentPhoto.slug}"
/> />
<!-- url="//{$page.host.split(':3000')[0]}/viewer/{currentPhoto.location.country.slug}/{currentPhoto.location.slug}/{currentPhoto.slug}" -->
<!-- image={currentPhoto ? fn.getThumbnail(currentPhoto.image.private_hash, 1200, 630) : null} -->
</svelte:head> </svelte:head>
<svelte:window bind:innerWidth={windowWidth} /> <svelte:window bind:innerWidth={windowWidth} />
@@ -129,7 +126,7 @@
<circle cx="50%" cy="50%" r="{windowWidth >= 768 ? 32 : 24}px"></circle> <circle cx="50%" cy="50%" r="{windowWidth >= 768 ? 32 : 24}px"></circle>
</svg> </svg>
</a> </a>
<a href="/location/{location.country.slug}/{location.slug}" class="button-control button-control--pink dir-bottom" aria-label="Close"> <a href="/location/{$currentLocation.country.slug}/{$currentLocation.slug}" class="button-control button-control--pink dir-bottom" aria-label="Close">
<IconCross color="#fff" width="18" class="icon" /> <IconCross color="#fff" width="18" class="icon" />
<IconCross color="#fff" width="18" class="icon" hidden="true" /> <IconCross color="#fff" width="18" class="icon" hidden="true" />
</a> </a>
@@ -137,7 +134,7 @@
</div> </div>
<Carousel <Carousel
{photos} photos={photos}
viewer={true} viewer={true}
init={$page} init={$page}
on:photoChange={photoChanged} on:photoChange={photoChanged}

View File

@@ -1,10 +1,19 @@
/* ========================================================================== /* ==========================================================================
PARALLAX EFFECTS PAGE TRANSITION
========================================================================== */ ========================================================================== */
// Parallax title: Translate X .transition {
.title-parallax { position: fixed;
transform: translateX(var(--translateX)); top: 0;
will-change: transform; left: 0;
width: 100%;
height: 100%;
z-index: 1000;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: $color-primary;
will-change: height, transform, padding-top, padding-bottom;
} }
@@ -15,124 +24,164 @@
.anim-mask { .anim-mask {
display: block; display: block;
overflow: hidden; overflow: hidden;
}
// Translate each letter from a direction
[data-aos="letters-translate-top"],
[data-aos="letters-translate-bottom"] {
white-space: nowrap; white-space: nowrap;
span { span {
display: inline-block; display: inline-block;
transition: transform 1000ms $ease-quart;
will-change: transform;
@for $i from 1 to 8 {
&:nth-child(#{$i}) {
transition-delay: $i * 40ms;
}
}
}
&.aos-animate {
span {
transform: translate(0, 0);
}
}
}
[data-aos="letters-translate-top"] {
span {
transform: translate(0, -100%);
}
}
[data-aos="letters-translate-bottom"] {
span {
transform: translate(0, 100%);
} }
} }
// Carousel prev/active/next photos // Translate each letter from a direction
[data-aos="carousel-prev"] { // [data-aos="letters-translate-top"],
transform: translate(-16%, -4%) rotate(-3deg) scale(0.8); // [data-aos="letters-translate-bottom"] {
opacity: 0; // white-space: nowrap;
transition-duration: 1.6s;
transition-delay: 450ms;
&.aos-animate { // span {
transform: translate(-9%, -1%) rotate(-1deg) scale(0.9); // display: inline-block;
opacity: 1; // transition: transform 1s $ease-quart;
} // will-change: transform;
}
[data-aos="carousel-active"] {
transform: scale(0.8);
opacity: 0;
transition-delay: 400ms;
&.aos-animate {
transform: scale(1);
opacity: 1;
}
}
[data-aos="carousel-next"] {
transform: translate(16%, -4%) rotate(3deg) scale(0.8);
opacity: 0;
transition-duration: 1.6s;
transition-delay: 500ms;
&.aos-animate {
transform: translate(9%, -1%) rotate(1deg) scale(0.9);
opacity: 1;
}
}
// Scale down and Fade in // @for $i from 1 to 8 {
[data-aos="scale-down-fade-in"] { // &:nth-child(#{$i}) {
transform: scale(1.1); // transition-delay: $i * 40ms;
opacity: 0; // }
transition: transform 2s $ease-quart, opacity 2s $ease-quart; // }
will-change: transform, opacity; // }
&.aos-animate { // &.aos-animate {
transform: scale(1); // span {
opacity: 1; // transform: translate(0, 0);
} // }
} // }
// }
// [data-aos="letters-translate-top"] {
// span {
// transform: translate(0, -100%);
// }
// }
// [data-aos="letters-translate-bottom"] {
// span {
// transform: translate(0, 100%);
// }
// }
// // Carousel prev/active/next photos
// [data-aos="carousel-prev"] {
// transform: translate(-16%, -4%) rotate(-3deg) scale(0.8);
// opacity: 0;
// transition-duration: 1.6s;
// transition-delay: 0.45s;
// &.aos-animate {
// transform: translate(-9%, -1%) rotate(-1deg) scale(0.9);
// opacity: 1;
// }
// }
// [data-aos="carousel-active"] {
// transform: scale(0.8);
// opacity: 0;
// transition-delay: 0.4s;
// &.aos-animate {
// transform: scale(1);
// opacity: 1;
// }
// }
// [data-aos="carousel-next"] {
// transform: translate(16%, -4%) rotate(3deg) scale(0.8);
// opacity: 0;
// transition-duration: 1.6s;
// transition-delay: 0.5s;
// &.aos-animate {
// transform: translate(9%, -1%) rotate(1deg) scale(0.9);
// opacity: 1;
// }
// }
// Location reveal // // Scale down and Fade in
[data-aos="location"] { // [data-aos="scale-down-fade-in"] {
img { // transform: scale(1.1);
opacity: 0; // opacity: 0;
transform: scale(1.15); // transition: transform 2s $ease-quart, opacity 2s $ease-quart;
transition: opacity 0.6s $ease-quart, transform 0.6s $ease-quart; // will-change: transform, opacity;
will-change: opacity, transform;
}
h3, p { // &.aos-animate {
transform: translateY(150%); // transform: scale(1);
transition: transform 0.6s $ease-quart; // opacity: 1;
will-change: transform; // }
} // }
h3 {
transition: all 0.6s $ease-quart;
transition-delay: 100ms;
}
p {
transition-delay: 200ms;
}
&.aos-animate {
img { // // Place title reveal
opacity: 1; // [data-aos="place-title"] {
transform: scale(1); // .anim-fade {
} // opacity: 0;
h3, p { // transition: opacity 0.9s $ease-inout;
transform: translateY(0); // transition-delay: 1.8s;
} // will-change: opacity;
} // }
} // .anim-translate {
// transform: translateY(120%);
// transition: transform 0.9s $ease-quart;
// transition-delay: 1.8s;
// will-change: transform;
// }
// .place__title_of {
// transition-delay: 2.0s;
// }
// .place__title_name {
// transition-delay: 2.1s;
// }
// .button-control {
// transition-delay: 2.2s;
// transition-duration: 0.6s;
// }
// &.aos-animate {
// .anim-fade {
// opacity: 1;
// }
// .anim-translate {
// transform: translateY(0);
// }
// }
// }
// // Location reveal
// [data-aos="location"] {
// img {
// opacity: 0;
// transform: scale(1.15);
// transition: opacity 0.6s $ease-quart, transform 0.6s $ease-quart;
// will-change: opacity, transform;
// }
// h3, p {
// transform: translateY(150%);
// transition: transform 0.6s $ease-quart;
// will-change: transform;
// }
// h3 {
// transition: all 0.6s $ease-quart;
// transition-delay: 0.1s;
// }
// p {
// transition-delay: 0.2s;
// }
// &.aos-animate {
// img {
// opacity: 1;
// transform: scale(1);
// }
// h3, p {
// transform: translateY(0);
// }
// }
// }

View File

@@ -59,6 +59,7 @@ button {
letter-spacing: -2vw; letter-spacing: -2vw;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
will-change: transform;
@include breakpoint (lg) { @include breakpoint (lg) {
font-size: pxVW(700); font-size: pxVW(700);

View File

@@ -57,8 +57,8 @@
height: 100%; height: 100%;
transform: scale($scale); transform: scale($scale);
box-shadow: 0 pxVW(15) pxVW(60) rgba(#000, 0.3); box-shadow: 0 pxVW(15) pxVW(60) rgba(#000, 0.3);
transition: transform $duration $ease-quart, opacity $duration / 2 $ease-quart; transition: transform $duration $ease-quart, opacity ($duration / 2) $ease-quart;
will-change: transform, opacity; will-change: transform, opacity, top, left;
@include breakpoint (sm) { @include breakpoint (sm) {
border-radius: $radius; border-radius: $radius;

View File

@@ -32,9 +32,9 @@
flex-flow: row wrap; flex-flow: row wrap;
@include breakpoint (sm) { @include breakpoint (sm) {
position: static; // position: static;
justify-content: center; justify-content: center;
margin: 24px 0 0; margin: 24px 0;
} }
// Tip message // Tip message
@@ -56,6 +56,10 @@
a { a {
margin-left: 16px; margin-left: 16px;
&:first-child {
margin-left: 0;
}
} }
} }
} }
@@ -73,10 +77,12 @@
margin: 0; margin: 0;
@include breakpoint (sm) { @include breakpoint (sm) {
position: static; // position: static;
transform: none; // transform: none;
margin-top: auto; // margin-top: auto;
margin-bottom: auto; // margin-bottom: auto;
left: 50%;
transform: translate(-50%, -45%);
width: 85%; width: 85%;
min-height: 500px; min-height: 500px;
} }

View File

@@ -0,0 +1,63 @@
<script>
import { stores } from '@sapper/app'
import { pageReady, animDurationLong, pageTransition } from './store'
const { page } = stores()
// Components
import TitleSite from '../atoms/TitleSite'
import IconGlobe from '../atoms/IconGlobe'
// Animations
import { animateIn, animateOut } from '../animations/Transition'
/*
** PAGE LOADING PROCESS
** 1. Set pageReady to false
** 1. Runs the Loader transition In
** ?. The next page changes the value of pageReady when mounted (or later)
** 2. The Loader detects the value change of pageReady
** 3. Hide the loader with transition Out
** 4. The Loader runs the page transition In via pageTransition
*/
let firstLoad = true
// 1. Watch page change
page.subscribe(() => {
// Run the loader animation (only after first load)
if (!firstLoad && process.browser) {
animateIn()
}
// Set pageReady to false (?)
pageReady.set(false)
})
// 2. Watch when loaded changes
pageReady.subscribe(loaded => {
if (loaded) {
setTimeout(() => {
// Scroll back to top of page
window.scrollTo(0,0)
// 3. Hide the loader
// Also sets firstLoad to false in order to show the second icon afterwards
animateOut(() => firstLoad = false)
}, 200) // This duration allows to not come over the transition In
// [OU ALORS] les pages changent la valeur de loaded plus tard
// 4. Run the page's transition in, but a little bit before the end of the loader
setTimeout(() => {
pageTransition.onAnimationEnd()
}, animDurationLong * 0.666 + 200)
}
})
</script>
<div class="transition" id="transition" aria-hidden="true">
<div class="loader">
{#if firstLoad}
<TitleSite />
{:else}
<IconGlobe width="44" color="#fff" animated="true" />
{/if}
</div>
</div>

View File

@@ -25,4 +25,15 @@ export let currentLocation = writable()
export let currentPhotos = writable() export let currentPhotos = writable()
// State // State
export let loaded = writable(false) // export let ready = writable(false)
export let pageReady = writable(false)
export const pageTransition = {
onAnimationEnd () {}
}
/* ==========================================================================
Animation related
========================================================================== */
export const animDuration = 1400
export const animDurationLong = 1800