world-game/src/App.svelte

261 lines
7.2 KiB
Svelte
Raw Normal View History

<script>
import { onMount } from 'svelte'
import { slides } from '@state/slides.svelte'
import { slideTo } from '@router'
import Header from '@components/layout/Header.svelte'
import Cursor from '@components/layout/Cursor.svelte'
import Footer from '@components/layout/Footer.svelte'
import LanguageSwitcher from '@components/ui/LanguageSwitcher.svelte'
import Home from '@views/Home.svelte'
import About from '@views/About.svelte'
import Expertise from '@views/Expertise.svelte'
import Portfolio from '@views/Portfolio.svelte'
import Project from '@views/Project.svelte'
import Play from '@views/Play.svelte'
import Game from '@views/Game.svelte'
import Blog from '@views/Blog.svelte'
import Article from '@views/Article.svelte'
import WhitePapers from '@views/WhitePapers.svelte'
import Privacy from '@views/Privacy.svelte'
import Default from '@views/Default.svelte'
const templates = {
home: Home,
about: About,
expertise: Expertise,
portfolio: Portfolio,
project: Project,
play: Play,
game: Game,
blog: Blog,
article: Article,
'white-papers': WhitePapers,
privacy: Privacy,
default: Default
}
const wrapperWidth = $derived(`${slides.all.length * 100}vw`)
const wrapperTransform = $derived(`translateX(-${slides.activeIndex * 100}vw)`)
const linesBySlide = {
home: [6, 11, 16],
expertise: [6, 8, 11, 14, 16],
about: [6, 8, 11, 14, 16],
portfolio: [11, 16],
privacy: [6, 8, 11, 14, 16],
}
const ALL_COLS = [6, 8, 11, 14, 16]
const activeTemplate = $derived(slides.standalone?.template ?? slides.active?.template)
const activeLines = $derived(new Set(linesBySlide[activeTemplate] ?? []))
let isReady = $state(false)
let isResizing = $state(false)
let resizeTimer = null
// Active la transition seulement après le premier paint à la bonne position.
// Double rAF : le premier laisse passer un paint avec le bon translateX,
// le second active is-animated — évite l'animation parasite au chargement.
$effect(() => {
if (slides.all.length > 0 && !isReady) {
requestAnimationFrame(() => requestAnimationFrame(() => { isReady = true }))
}
})
onMount(() => {
const handleResize = () => {
isResizing = true
clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => { isResizing = false }, 150)
}
window.addEventListener('resize', handleResize)
const handleKeydown = (e) => {
if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return
// Si on est sur une sous-page (ex: /livres-blancs/slug), ne pas changer de slide
const activePath = slides.active?.path ?? ''
const currentPath = window.location.pathname.replace(/^\/en/, '') || '/'
const isSubPage = activePath && currentPath.startsWith(activePath + '/')
if (isSubPage) {
if (e.key === 'ArrowLeft') history.back()
return
}
if (e.key === 'ArrowRight') {
const next = slides.all[slides.activeIndex + 1]
if (next) slideTo(next.path)
} else {
const prev = slides.all[slides.activeIndex - 1]
if (prev) slideTo(prev.path)
}
}
window.addEventListener('keydown', handleKeydown)
// Swipe gauche/droite → même comportement que les touches clavier
const TOUCH_THRESHOLD = 50
let touchStartX = 0
let touchStartY = 0
const handleTouchStart = (e) => {
touchStartX = e.touches[0].clientX
touchStartY = e.touches[0].clientY
}
const handleTouchEnd = (e) => {
const deltaX = touchStartX - e.changedTouches[0].clientX
const deltaY = touchStartY - e.changedTouches[0].clientY
if (Math.abs(deltaX) < TOUCH_THRESHOLD) return
if (Math.abs(deltaY) > Math.abs(deltaX)) return
const activePath = slides.active?.path ?? ''
const currentPath = window.location.pathname.replace(/^\/en/, '') || '/'
const isSubPage = activePath && currentPath.startsWith(activePath + '/')
if (isSubPage) return
if (deltaX > 0) {
const next = slides.all[slides.activeIndex + 1]
if (next) slideTo(next.path)
} else {
const prev = slides.all[slides.activeIndex - 1]
if (prev) slideTo(prev.path)
}
}
window.addEventListener('touchstart', handleTouchStart, { passive: true })
window.addEventListener('touchend', handleTouchEnd, { passive: true })
return () => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('keydown', handleKeydown)
window.removeEventListener('touchstart', handleTouchStart)
window.removeEventListener('touchend', handleTouchEnd)
clearTimeout(resizeTimer)
}
})
</script>
<Cursor />
<Header />
<div class="bg-fixed" style="background-image: url('/assets/img/scrollable-page-background.png')"></div>
<div class="vertical-lines" aria-hidden="true">
{#each ALL_COLS as col}
<div class="vl vl-col{col}" class:visible={activeLines.has(col)}></div>
{/each}
</div>
<main class="main">
{#if slides.standalone}
<svelte:component
this={templates[slides.standalone.template] ?? Default}
data={slides.standalone}
/>
{:else}
<div
class="slides-wrapper"
class:is-animated={isReady && !isResizing}
style="width: {wrapperWidth}; transform: {wrapperTransform}"
>
{#each slides.all as slide, i}
<section class="slide" class:active={i === slides.activeIndex} data-slide={slide.id} inert={i !== slides.activeIndex}>
{#if slide.loaded}
<svelte:component
this={templates[slide.template] ?? Default}
data={slide.data}
/>
{/if}
</section>
{/each}
</div>
{/if}
</main>
<LanguageSwitcher />
<Footer />
<style>
:global(#app) {
height: 100vh;
}
:global(*) {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:global(body) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #000;
color: #fff;
overflow: hidden;
}
:global(a) {
color: inherit;
text-decoration: none;
}
:global(img) {
max-width: 100%;
height: auto;
}
.main {
position: relative;
overflow: hidden;
height: 100vh;
}
.slides-wrapper {
display: flex;
height: 100%;
}
.slides-wrapper.is-animated {
transition: transform 1000ms cubic-bezier(0.77, 0, 0.175, 1);
}
.slide {
width: 100vw;
height: 100vh;
flex-shrink: 0;
}
.bg-fixed {
position: fixed;
inset: 0;
z-index: -1;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.vertical-lines {
position: fixed;
inset: 0;
z-index: var(--z-base);
pointer-events: none;
}
.vl {
position: absolute;
top: 0;
height: 0;
border-left: 0.1px solid rgba(238, 238, 238, 0.2);
overflow: hidden;
transition: height 600ms cubic-bezier(0.77, 0, 0.175, 1);
}
.vl.visible { height: 100vh; }
.vl-col6 { left: calc(10.66 / 63.96 * 100%); }
.vl-col8 { left: calc(21.32 / 63.96 * 100%); }
.vl-col11 { left: calc(31.98 / 63.96 * 100%); }
.vl-col14 { left: calc(42.64 / 63.96 * 100%); }
.vl-col16 { left: calc(53.30 / 63.96 * 100%); }
</style>