floter.design/src/routes/work/+page.svelte
2026-02-10 18:06:34 +01:00

362 lines
No EOL
10 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import gsap from 'gsap';
import { SplitText } from 'gsap/SplitText';
import { goto } from '$app/navigation';
let { data } = $props();
const orderedPosts = $derived.by(() => (data.posts || []).sort((a, b) => {
return a.meta.order - b.meta.order;
}));
function getColorValue(cssValue: string): string {
// If it's a CSS variable reference, resolve it
if (cssValue.trim().startsWith('var(')) {
// Extract the variable name and resolve it
const varName = cssValue.match(/var\(([^)]+)\)/)?.[1];
if (varName) {
return getComputedStyle(document.documentElement).getPropertyValue(varName.trim()).trim() || cssValue;
}
}
return cssValue.trim();
}
onMount(() => {
document.querySelectorAll('.workclone')?.forEach(clone => {
clone.remove();
})
gsap.registerPlugin(SplitText);
let isZoomed = false;
// const currentBgColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg');
// const currentHighlightColor = getComputedStyle(document.documentElement).getPropertyValue('--color-highlight');
// Read from data.theme if available, otherwise from CSS
let currentBgColor: string;
let currentHighlightColor: string;
if (data.theme) {
currentBgColor = getColorValue(data.theme.bg);
currentHighlightColor = getColorValue(data.theme.highlight);
} else {
currentBgColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg').trim();
currentHighlightColor = getComputedStyle(document.documentElement).getPropertyValue('--color-highlight').trim();
}
let works: Array<HTMLElement> = Array.from(document.querySelectorAll('.work'));
for ( let i = 0; i < 40; i++ ){
let clone = works[i].cloneNode(true) as HTMLElement;
works[i].parentNode?.appendChild(clone);
works.push(clone);
}
works.forEach((work, index) => {
let workrect = work.getBoundingClientRect();
let worksrect = document.querySelector('.works')?.getBoundingClientRect() || {x: 0, y: 0, width: 1, height: 1};
let distanceXinPercent = (workrect.x - worksrect?.x || 0) / (worksrect?.width || 1) * 100 ;
let distanceYinPercent = (workrect.y - worksrect?.y || 0) / (worksrect?.height || 1) * 100 ;
work.addEventListener('click', (e) => {
isZoomed = !isZoomed;
e.preventDefault();
let href = work.getAttribute('href');
if (isZoomed) {
gsap.to('.works', {
rotate: 0,
xPercent: -distanceXinPercent,
yPercent: -distanceYinPercent,
rotateX: 0,
rotateY: 0,
scale: 1,
duration: .75,
ease: 'power2.inOut',
onComplete: () => {
let workclone = work.cloneNode(true) as HTMLElement;
workclone.classList.add('workclone');
workclone.style.position = 'fixed';
workclone.style.top = `0`;
workclone.style.left = `0`;
document.body.appendChild(workclone);
// window.location.href = href || ''
goto(href || '');
}
})
gsap.fromTo(split.chars, {
scaleY: 1,
opacity: 1,
}, {
scaleY: 0,
opacity: 1,
duration: .3,
ease: 'power3.out',
})
} else {
gsap.to('.works', {
rotate: -5,
transformOrigin: '0 0',
xPercent: -3,
yPercent: 0,
rotateX: 0,
rotateY: 0,
scale: .25,
duration: 1,
ease: 'power4.inOut'
})
}
})
});
// set initial positions AFTER the individual rects have been read
let split = new SplitText('.headline', { type: 'chars', charsClass: 'charChildren' });
gsap.set('.works, .headline', {
rotate: -5,
xPercent: -5,
yPercent: -2,
rotateX: 0,
rotateY: 0,
scale: .25,
})
gsap.set('.work, .works, .headline', {
transformOrigin: '0 0',
})
gsap.set('.work',{
autoAlpha: 0,
yPercent: 50,
scaleY: 0,
})
gsap.to('.work',{
autoAlpha:1,
yPercent: 0,
scaleY: 1,
duration: .75,
stagger: 0.01,
ease: 'power4.inOut',
})
gsap.fromTo(split.chars, {
yPercent: -300
}, {
yPercent: 0,
duration: 1,
stagger: 0.0075,
ease: 'elastic.out(1, .8)',
})
// Move the works around on mousemove
let mouseMoveHandler = (e: MouseEvent) => {
let x = e.clientX;
let y = e.clientY;
if (isZoomed) return;
gsap.to('.works, .headline', {
xPercent: -x/window.innerWidth * 4 - 3,
yPercent: -y/window.innerHeight * 4,
rotateX: x/window.innerWidth * 10 - 5,
rotateY: x/window.innerWidth * 10 - 5,
duration: 1,
ease: 'power4.out',
overwrite: true,
})
}
window.addEventListener('mousemove', mouseMoveHandler);
// Create handler functions outside the loop for proper cleanup
const handleMouseEnter = (work: HTMLElement) => {
if (isZoomed) return;
gsap.killTweensOf([work, work.querySelector('.work-logo')]);
gsap.to(work, {
backgroundColor: currentBgColor,
duration: .125,
ease: 'power1.inOut',
})
const logo = work.querySelector('.work-logo');
if (logo) {
gsap.to(logo, {
color: currentHighlightColor,
duration: .125,
ease: 'power1.inOut',
})
}
}
const handleMouseLeave = (work: HTMLElement) => {
if (isZoomed) return;
gsap.killTweensOf([work, work.querySelector('.work-logo')]);
gsap.to(work, {
backgroundColor: currentHighlightColor,
duration: .125,
ease: 'power1.inOut',
})
const logo = work.querySelector('.work-logo');
if (logo) {
gsap.to(logo, {
color: currentBgColor,
duration: .125,
ease: 'power1.inOut',
})
}
}
// Hover states of .work
works.forEach((work) => {
work.addEventListener('mouseenter', () => handleMouseEnter(work));
work.addEventListener('mouseleave', () => handleMouseLeave(work));
})
// Move the works the same way on touch drag
let touchMoveHandler = (e: TouchEvent) => {
let x = e.touches[0].clientX;
let y = e.touches[0].clientY;
if (isZoomed) return;
gsap.to('.works, .headline', {
xPercent: -x/window.innerWidth * 4 - 3.33,
yPercent: -y/window.innerHeight * 4,
rotateX: x/window.innerWidth * 10 - 5,
rotateY: x/window.innerWidth * 10 - 5,
duration: 1,
ease: 'power4.out',
overwrite: true,
})
}
window.addEventListener('touchmove', touchMoveHandler, { passive: true });
document.querySelector('html')?.style.setProperty('overflow', 'hidden');
document.querySelector('html')?.style.setProperty('overscroll-behavior', 'none');
return () => {
document.querySelector('html')?.style.setProperty('overflow', 'visible');
document.querySelector('html')?.style.setProperty('overscroll-behavior', 'auto');
window.removeEventListener('mousemove', mouseMoveHandler);
window.removeEventListener('touchmove', touchMoveHandler);
gsap.killTweensOf('.works, .work');
works.forEach((work) => {
work.removeEventListener('mouseenter', () => handleMouseEnter(work));
work.removeEventListener('mouseleave', () => handleMouseLeave(work));
work.removeEventListener('click', (e) => {});
})
}
});
</script>
<div class="works-wrapper">
<h1 class="headline"><span>Work References</span></h1>
<div class="works">
{#each orderedPosts as work, i}
<a
data-sveltekit-preload-data
href="{work.path}"
class="work"
>
<div class="work-logo">
{@html work.meta.svg}
</div>
<div class="work-info">
<h2 class="title"><span class="title-words">{work.meta.title}</span></h2>
{#if work.meta.tags}
<ul class="tags">
{#each work.meta.tags as tag, index}
<li class="tag">{tag}{index < work.meta.tags.length-1 ? ' | ' : ''}</li>
{/each}
</ul>
{/if}
</div>
</a>
{/each}
</div>
</div>
<style>
:global(html) {
overflow: hidden;
overscroll-behavior: none;
}
.works-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
perspective: 700px;
transform-origin: 0 0;
will-change: transform;
}
.headline {
position: fixed;
pointer-events: none;
display: flex;
width: 100%;
height: 100%;
text-align: center;
flex-direction: column;
justify-content: center;
align-content: center;
letter-spacing: -0.05em;
z-index: 10;
transform: translateZ(500px);
margin-left: 40.5vw;
margin-top: 37.25vh;
font-size: 14vw;
}
@media screen and (min-width: 768px) {
.headline {
margin-left: 41.66vw;
margin-top: 40.5vh;
font-size: 7vw;
transform: translateZ(600px);
}
}
.works {
margin: 0 auto;
position: absolute;
top: 0;
left: 0;
width: 600vw;
height: 600vw;
transform: scale(.333);
transform-origin: 0 0;
display: grid;
grid-template-columns: repeat(6, 100vw);
grid-auto-rows: 100vh;
gap: 6px;
will-change: transform;
}
.work {
background-color: var(--color-highlight);
text-decoration: none;
text-align: center;
width: 100vw;
height: 100vh;
opacity: 0;
visibility: hidden;
transform: translateZ(700px);
will-change: transform;
}
:global(.work-logo) {
width: 100%;
height: 100%;
color: var(--color-bg);
padding: 3em;
margin: auto;
display: flex;
flex-direction: column;
justify-content: center;
will-change: transform;
}
@media screen and (min-width: 768px) {
:global(.work-logo) { width: 60%; }
}
:global(.work-logo svg) {
object-fit: fill;
width: 100%;
height: 100%;
}
.work-info {
display: none;
}
.work-info .tags {
display: flex;
list-style: none;
gap: .25em;
}
</style>