362 lines
No EOL
10 KiB
Svelte
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> |