349 lines
12 KiB
Svelte
349 lines
12 KiB
Svelte
<style lang="scss">
|
||
@import "../../style/pages/about";
|
||
</style>
|
||
|
||
<script lang="ts">
|
||
import { navigating, page } from '$app/stores'
|
||
import { onMount, afterUpdate } from 'svelte'
|
||
import type { PageData } from './$types'
|
||
import { scroll, animate, inView, timeline } from 'motion'
|
||
import { map } from '$utils/functions'
|
||
import { getAssetUrlKey } from '$utils/api'
|
||
import { DELAY } from '$utils/contants'
|
||
import { quartOut } from '$animations/easings'
|
||
// Components
|
||
import Metas from '$components/Metas.svelte'
|
||
import PageTransition from '$components/PageTransition.svelte'
|
||
import Image from '$components/atoms/Image.svelte'
|
||
import Button from '$components/atoms/Button.svelte'
|
||
import AboutGridPhoto from '$components/atoms/AboutGridPhoto.svelte'
|
||
import Heading from '$components/molecules/Heading.svelte'
|
||
import ProcessStep from '$components/molecules/ProcessStep.svelte'
|
||
import InteractiveGlobe2 from '$components/organisms/InteractiveGlobe2.svelte'
|
||
import ShopModule from '$components/organisms/ShopModule.svelte'
|
||
import NewsletterModule from '$components/organisms/NewsletterModule.svelte'
|
||
|
||
export let data: PageData
|
||
const { about, photos } = data
|
||
|
||
let scrollY: number, innerWidth: number, innerHeight: number
|
||
let purposeEl: HTMLElement, photosGridEl: HTMLElement
|
||
let currentStep: number = 0
|
||
let photoFirstEl: HTMLElement, photoUsEl: HTMLElement
|
||
let photosGridOffset: number = photosGridEl && photosGridEl.offsetTop
|
||
|
||
$: currentStep = $page.url.hash ? Number($page.url.hash.split('#step-')[1]) - 1 : 0
|
||
$: parallaxPhotos = photosGridEl && map(scrollY, photosGridOffset - innerHeight, photosGridOffset + innerHeight / 1.5, 0, innerHeight * 0.15, true)
|
||
$: fadedPhotosIndexes = innerWidth > 768
|
||
? [0, 2, 5, 7, 9, 12, 17, 20, 22, 26, 30, 32, 34]
|
||
: [1, 4, 5, 7, 11, 14, 17, 20, 24, 27, 30, 33, 34, 36, 40, 43]
|
||
|
||
const introText = about.intro_text
|
||
.replace('<strong>',
|
||
`<a href="/${about.intro_firstlocation.country.slug}/${about.intro_firstlocation.slug}" data-sveltekit-noscroll data-sveltekit-prefetch>
|
||
<img src="${getAssetUrlKey(about.intro_firstlocation.country.flag.id, 'square-small-jpg')}" width="32" height="32" alt="${about.intro_firstlocation.country.flag.title}">
|
||
<strong>
|
||
`)
|
||
.replace('</strong>', '</strong></a>')
|
||
|
||
|
||
onMount(() => {
|
||
/**
|
||
* Animations
|
||
*/
|
||
const animation = timeline([
|
||
// Heading
|
||
['.heading .text', {
|
||
y: [24, 0],
|
||
opacity: [0, 1],
|
||
z: 0,
|
||
}, {
|
||
at: 0.5,
|
||
}],
|
||
|
||
// First photo
|
||
[photoFirstEl, {
|
||
y: ['10%', 0],
|
||
rotate: [0, getComputedStyle(photoFirstEl).getPropertyValue('--rotate')],
|
||
opacity: [0, 1],
|
||
z: 0,
|
||
}, {
|
||
at: 0.75,
|
||
opacity: {
|
||
duration: 1
|
||
}
|
||
}],
|
||
|
||
// Portrait photo
|
||
[photoUsEl, {
|
||
y: ['10%', 0],
|
||
x: [0, '5%'],
|
||
rotate: [0, 5],
|
||
opacity: [0, 1],
|
||
z: 0,
|
||
}, {
|
||
at: 1,
|
||
opacity: {
|
||
duration: 1
|
||
}
|
||
}],
|
||
|
||
// Text
|
||
['.about__introduction .text', {
|
||
y: [32, 0],
|
||
opacity: [0, 1],
|
||
z: 0,
|
||
}, {
|
||
at: 1.2,
|
||
}],
|
||
], {
|
||
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
|
||
defaultOptions: {
|
||
duration: 1.6,
|
||
easing: quartOut,
|
||
},
|
||
})
|
||
animation.stop()
|
||
|
||
// Run animation
|
||
requestAnimationFrame(animation.play)
|
||
|
||
|
||
/**
|
||
* Intro parallax
|
||
*/
|
||
// First photo
|
||
scroll(animate(photoFirstEl.querySelector('figure'), {
|
||
y: ['-3%', '6%'],
|
||
x: [0, '-4%'],
|
||
rotate: [-1.5, 0],
|
||
z: 0,
|
||
}), {
|
||
target: photoFirstEl,
|
||
offset: ["-200%", "150%"]
|
||
})
|
||
// Portrait photo
|
||
scroll(animate(photoUsEl.querySelector('figure'), {
|
||
y: [0, '-6%'],
|
||
x: [0, '-3%'],
|
||
rotate: [-2, 0],
|
||
z: 0,
|
||
}), {
|
||
target: photoUsEl,
|
||
offset: ["-250%", "150%"]
|
||
})
|
||
|
||
|
||
/**
|
||
* Purpose reveal
|
||
*/
|
||
inView(purposeEl, ({ target, isIntersecting }) => {
|
||
target.classList.toggle('is-visible', isIntersecting)
|
||
}, { amount: 0.6 })
|
||
|
||
// Parallax
|
||
scroll(animate(purposeEl.querySelector('picture img'), {
|
||
y: [0, '40%'],
|
||
}))
|
||
|
||
|
||
/**
|
||
* Steps scroll animation
|
||
*/
|
||
const cards = stepsEl.querySelectorAll('.step')
|
||
const cardsAmount = about.process_steps.length
|
||
|
||
cards.forEach((card: HTMLElement, i: number) => {
|
||
const index = i + 1
|
||
const reverseIndex = cardsAmount - index
|
||
const overlay = card.querySelector('.overlay')
|
||
const scrollOptions: ScrollOptions = {
|
||
target: stepsEl,
|
||
offset: [`${i / cardsAmount * 100}%`, `${index / cardsAmount * 100}%`],
|
||
}
|
||
|
||
// Card scale
|
||
scroll(animate(card, {
|
||
scale: [1, 1 - (0.02 * reverseIndex)]
|
||
}), scrollOptions)
|
||
|
||
// Overlay opacity
|
||
scroll(animate(overlay, {
|
||
opacity: [0, 0.2 + (0.05 * reverseIndex)]
|
||
}), scrollOptions)
|
||
})
|
||
|
||
|
||
/**
|
||
* Play videos only when visible
|
||
*/
|
||
const videos = stepsEl.querySelectorAll('video')
|
||
if (videos) {
|
||
videosObserver = new IntersectionObserver(entries => {
|
||
entries.forEach(({ target, isIntersecting }) => {
|
||
// @ts-ignore
|
||
isIntersecting ? target.play() : target.pause()
|
||
})
|
||
}, {
|
||
threshold: 0,
|
||
})
|
||
videos.forEach(video => videosObserver.observe(video))
|
||
}
|
||
|
||
|
||
// Destroy
|
||
return () => {
|
||
videosObserver.disconnect()
|
||
}
|
||
})
|
||
|
||
|
||
afterUpdate(() => {
|
||
// Update photos grid top offset
|
||
photosGridOffset = photosGridEl.offsetTop
|
||
})
|
||
</script>
|
||
|
||
<svelte:window bind:scrollY bind:innerWidth bind:innerHeight />
|
||
|
||
<Metas
|
||
title="About the project – Houses Of"
|
||
description={about.description}
|
||
image=""
|
||
/>
|
||
|
||
|
||
<PageTransition name="about">
|
||
<Heading
|
||
text={about.description}
|
||
/>
|
||
|
||
<section class="about__introduction">
|
||
<div class="container grid">
|
||
<div class="photo-first" bind:this={photoFirstEl}>
|
||
<figure>
|
||
<Image
|
||
class="shadow-box-dark"
|
||
id={about.intro_firstphoto.id}
|
||
alt={about.intro_firstphoto.title}
|
||
sizeKey="photo-list"
|
||
sizes={{
|
||
small: { width: 400 },
|
||
medium: { width: 600 },
|
||
large: { width: 800 },
|
||
}}
|
||
ratio={1.5}
|
||
/>
|
||
<figcaption class="text-info">{about.intro_firstphoto_caption}</figcaption>
|
||
</figure>
|
||
</div>
|
||
|
||
<div class="photo-us" bind:this={photoUsEl}>
|
||
<figure>
|
||
<Image
|
||
class="shadow-box-dark"
|
||
id={about.intro_portraits.id}
|
||
alt={about.intro_portraits.title}
|
||
sizeKey="square"
|
||
sizes={{
|
||
small: { width: 250 }
|
||
}}
|
||
ratio={1}
|
||
/>
|
||
</figure>
|
||
</div>
|
||
|
||
<div class="text text-normal">
|
||
{@html introText}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="about__purpose grid" bind:this={purposeEl}>
|
||
<div class="container">
|
||
<div class="text title-xl">
|
||
{@html about.purpose_text}
|
||
</div>
|
||
|
||
<div class="background">
|
||
<picture class="background__illustration">
|
||
<source media="(min-width: 1200px)" srcset={getAssetUrlKey(about.intro_firstlocation.illustration_desktop_2x.id, 'illustration-desktop-2x')}>
|
||
<source media="(min-width: 768px)" srcset={getAssetUrlKey(about.intro_firstlocation.illustration_desktop.id, 'illustration-desktop-1x')}>
|
||
<img
|
||
src={getAssetUrlKey(about.intro_firstlocation.illustration_mobile.id, 'illustration-mobile')}
|
||
width={320}
|
||
height={824}
|
||
alt="Illustration for {about.intro_firstlocation.name}"
|
||
decoding="async"
|
||
/>
|
||
</picture>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="about__process">
|
||
<div class="container grid">
|
||
<aside>
|
||
<div class="heading">
|
||
<h2 class="title-medium">{about.process_title}</h2>
|
||
<p class="text-normal">{about.process_subtitle}</p>
|
||
</div>
|
||
|
||
<ol>
|
||
{#each about.process_steps as { title }, index}
|
||
<li class:is-active={index === currentStep}>
|
||
<a href="#step-{index + 1}" class="title-big">
|
||
<span>{title}</span>
|
||
</a>
|
||
</li>
|
||
{/each}
|
||
</ol>
|
||
</aside>
|
||
|
||
<div class="steps">
|
||
{#each about.process_steps as { text, image, video_mp4, video_webm }, index}
|
||
<ProcessStep
|
||
{index} {text}
|
||
image={image ?? undefined}
|
||
video={video_mp4 && video_webm ? {
|
||
mp4: video_mp4.id,
|
||
webm: video_webm.id
|
||
} : undefined}
|
||
visible={index === currentStep}
|
||
/>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="about__photos" bind:this={photosGridEl}>
|
||
<div class="container-wide">
|
||
<div class="photos-grid" style:--parallax-y="{parallaxPhotos}px">
|
||
{#each photos as { image: { id }, title }, index}
|
||
<AboutGridPhoto class="grid-photo"
|
||
{id}
|
||
alt={title}
|
||
disabled={fadedPhotosIndexes.includes(index)}
|
||
/>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="about__interest grid">
|
||
<div class="container grid">
|
||
<h2 class="title-xl">{about.contact_title}</h2>
|
||
<div class="blocks">
|
||
{#each about.contact_blocks as { title, text, link, button }}
|
||
<div class="block">
|
||
<h3 class="text-label">{title}</h3>
|
||
<p class="text-normal">{text}</p>
|
||
{#if link}
|
||
<Button size="small" url={link} text={button} />
|
||
{/if}
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</PageTransition> |