upgrades and updates

This commit is contained in:
saiminh 2026-01-22 17:24:51 +13:00
parent 2149372530
commit d7cab5d526
11 changed files with 163 additions and 73 deletions

2
package-lock.json generated
View file

@ -11,7 +11,7 @@
"gsap": "^3.13.0", "gsap": "^3.13.0",
"mdsvex": "^0.11.0", "mdsvex": "^0.11.0",
"pixi-filters": "^6.1.5", "pixi-filters": "^6.1.5",
"pixi.js": "^8.0.0", "pixi.js": "^8.15.0",
"superjson": "^1.13.1", "superjson": "^1.13.1",
"svelte-cloudinary": "^1.1.0" "svelte-cloudinary": "^1.1.0"
}, },

View file

@ -40,8 +40,8 @@
"dependencies": { "dependencies": {
"gsap": "^3.13.0", "gsap": "^3.13.0",
"mdsvex": "^0.11.0", "mdsvex": "^0.11.0",
"pixi.js": "^8.15.0",
"pixi-filters": "^6.1.5", "pixi-filters": "^6.1.5",
"pixi.js": "^8.0.0",
"superjson": "^1.13.1", "superjson": "^1.13.1",
"svelte-cloudinary": "^1.1.0" "svelte-cloudinary": "^1.1.0"
} }

View file

@ -72,6 +72,7 @@
<HomeIlluShape /> <HomeIlluShape />
<HomeIlluShape /> <HomeIlluShape />
<HomeIlluShape /> <HomeIlluShape />
<!-- <HomeIlluShape />
<HomeIlluShape /> <HomeIlluShape />
<HomeIlluShape /> <HomeIlluShape />
<HomeIlluShape /> <HomeIlluShape />
@ -79,8 +80,7 @@
<HomeIlluShape /> <HomeIlluShape />
<HomeIlluShape /> <HomeIlluShape />
<HomeIlluShape /> <HomeIlluShape />
<HomeIlluShape /> <HomeIlluShape /> -->
<HomeIlluShape />
</div> </div>
<style> <style>
@ -90,7 +90,7 @@
gap: clamp(1px, 0.3vw, 5px); gap: clamp(1px, 0.3vw, 5px);
} }
:global(.home-illu-shapes > *) { :global(.home-illu-shapes > *) {
flex-basis: calc(20% - clamp(1px, 0.3vw, 5px)); flex-basis: calc(33.333% - clamp(1px, 0.3vw, 5px));
opacity: 0; opacity: 0;
} }
</style> </style>

View file

@ -1,7 +1,7 @@
<script lang='ts'> <script lang='ts'>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { gsap } from "gsap"; import { gsap } from "gsap";
import { page } from "$app/stores"; import { page } from "$app/state";
onMount(() => { onMount(() => {
@ -16,7 +16,7 @@
let navlinks = document.querySelectorAll('nav a'); let navlinks = document.querySelectorAll('nav a');
navlinks.forEach((link, index) => { navlinks.forEach((link, index) => {
link.addEventListener('click', (e)=> { link.addEventListener('click', (e)=> {
if (e.target?.toString() === $page.url.toString()) { if (e.target?.toString() === page.url.toString()) {
return; return;
} }
const checkbox = document.querySelector('input[type="checkbox"]') as HTMLInputElement; const checkbox = document.querySelector('input[type="checkbox"]') as HTMLInputElement;
@ -52,11 +52,11 @@
<span class="close">×</span> <span class="close">×</span>
</label> </label>
<nav id="nav"> <nav id="nav">
<a href="/" class="navlink {$page.route.id === '/' ? 'current' : ''}">Home</a> <a href="/" class="navlink {page.route.id === '/' ? 'current' : ''}">Home</a>
<a href="/work" class="navlink {$page.route.id?.includes('/work') ? 'current' : ''}">Work</a> <a href="/work" class="navlink {page.route.id?.includes('/work') ? 'current' : ''}">Work</a>
<a href="/service" class="navlink {$page.route.id === '/service' ? 'current' : ''}">Services</a> <a href="/service" class="navlink {page.route.id === '/service' ? 'current' : ''}">Services</a>
<!-- <a href="/about" class="navlink {$page.route.id === '/about' ? 'current' : ''}">About</a> --> <!-- <a href="/about" class="navlink {page.route.id === '/about' ? 'current' : ''}">About</a> -->
<a href="/contact" class="navlink {$page.route.id === '/contact' ? 'current' : ''}">Contact</a> <a href="/contact" class="navlink {page.route.id === '/contact' ? 'current' : ''}">Contact</a>
</nav> </nav>
</header> </header>

View file

@ -2,6 +2,10 @@
--spacing-outer: 5vw; --spacing-outer: 5vw;
--spacing-nav: 5vw; --spacing-nav: 5vw;
--color-bg: rgb(63, 111, 207); --color-bg: rgb(63, 111, 207);
--color-bg-variant-1: rgb(63, 111, 207);
--color-bg-variant-2: rgb(207, 63, 70);
--color-bg-variant-3: rgb(63, 207, 80);
--color-bg-variant-4: rgb(255, 174, 0);
--color-text: rgb(255, 234, 217); --color-text: rgb(255, 234, 217);
--color-highlight: rgb(29, 12, 18); --color-highlight: rgb(29, 12, 18);
--aspect-ratio-heroes: 1.5; --aspect-ratio-heroes: 1.5;

View file

@ -1,23 +1,50 @@
<script lang='ts'> <script lang="ts">
import '$lib/styles/global.scss'; import '$lib/styles/global.scss';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import Loader from '$lib/components/Loader.svelte'; import Loader from '$lib/components/Loader.svelte';
import { navigating, page } from '$app/stores'; import { page } from '$app/state';
import { onMount } from 'svelte'; import { beforeNavigate } from '$app/navigation';
import { browser } from '$app/environment';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
interface LayoutData { interface LayoutData {
pathname: string; pathname: string;
} }
let { data, children }: { data: LayoutData, children: Snippet } = $props();
let { data, children }: { data: LayoutData; children: Snippet } = $props();
// Track pathname changes - if pathname changes but navigating is still true,
// navigation might be stuck (though this shouldn't happen normally) let showLoader = $state(false);
let lastPathname = $derived(data.pathname); let timer: ReturnType<typeof setTimeout> | null = null;
$effect(() => { let navToken = 0;
if (data.pathname !== lastPathname) {
lastPathname = data.pathname; function clearTimer() {
if (timer) {
clearTimeout(timer);
timer = null;
} }
}); }
if (browser) {
beforeNavigate((nav) => {
// token to avoid races between overlapping navigations
const token = ++navToken;
clearTimer();
showLoader = false;
// only show if navigation takes >150ms
timer = setTimeout(() => {
if (token === navToken) showLoader = true;
}, 150);
// hide loader when THIS navigation completes (even if afterNavigate timing is odd)
nav.complete.finally(() => {
if (token !== navToken) return;
clearTimer();
showLoader = false;
});
});
}
</script> </script>
<svelte:head> <svelte:head>
@ -25,14 +52,14 @@
<meta property="og:url" content={`https://floter.design`+data.pathname} /> <meta property="og:url" content={`https://floter.design`+data.pathname} />
<meta property="og:image" content="https://floter.design/ogimage.png"> <meta property="og:image" content="https://floter.design/ogimage.png">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="description" content={$page.data.description ? $page.data.description : 'Simon Flöter is a designer and creative developer'} /> <meta name="description" content={page.data.description ? page.data.description : 'Simon Flöter is a designer and creative developer'} />
<title>{$page.data.title ? $page.data.title : 'Simon Flöter, Designer and Creative Developer'}</title> <title>{page.data.title ? page.data.title : 'Simon Flöter, Designer and Creative Developer'}</title>
</svelte:head> </svelte:head>
<Header /> <Header />
{#key data.pathname} {#key data.pathname}
<div class="content"> <div class="content">
{#if $navigating} {#if showLoader}
<Loader /> <Loader />
{/if} {/if}
{@render children()} {@render children()}

View file

@ -93,9 +93,6 @@
</script> </script>
{#if !mounted}
<Loader />
{/if}
<article class="scroller"> <article class="scroller">
<section class="canvasized splash"> <section class="canvasized splash">
<h1 class="align-middle">I create digital experiences that <em>stand out</em> from the rest.</h1> <h1 class="align-middle">I create digital experiences that <em>stand out</em> from the rest.</h1>

View file

@ -0,0 +1,43 @@
<article>
<div class="eyebrow">
Research &amp; Thoughts
</div>
<h1>Ethical Web Development</h1>
<p>
What does that mean? For me, it means building websites and applications that prioritize user privacy, accessibility, and sustainability.
</p>
</article>
<style>
article {
max-width: 1200px;
padding: 0
var(--spacing-outer)
100px
var(--spacing-outer);
@media screen and (min-width: 768px) {
margin: 0
var(--spacing-outer)
0
150px;
padding: calc(50vh - var(--spacing-outer) - 6vw)
var(--spacing-outer)
var(--spacing-outer)
var(--spacing-outer);
width: calc(100% - 150px - var(--spacing-outer));
}
}
.eyebrow {
font-size: 1.25em;
text-transform: uppercase;
letter-spacing: 0.01em;
margin-bottom: 0;
}
h1 {
font-size: clamp(2.5rem, 7vw, 7rem);
letter-spacing: -0.04em;
margin-top: 0;
margin-bottom: 0.5em;
}
</style>

View file

@ -94,12 +94,12 @@
transformOrigin: '0 0', transformOrigin: '0 0',
}) })
gsap.set('.work',{ gsap.set('.work',{
opacity: 0, autoAlpha: 0,
yPercent: 50, yPercent: 50,
scaleY: 0, scaleY: 0,
}) })
gsap.to('.work',{ gsap.to('.work',{
opacity:1, autoAlpha:1,
yPercent: 0, yPercent: 0,
scaleY: 1, scaleY: 1,
duration: .75, duration: .75,
@ -129,36 +129,50 @@
overwrite: true, overwrite: true,
}) })
} }
// Hover states of .work
works.forEach((work, index) => {
work.addEventListener('mouseenter', (e) => {
if (isZoomed) return;
gsap.to(work, {
backgroundColor: 'var(--color-bg)',
duration: .125,
ease: 'power1.inOut',
})
gsap.to(work.querySelector('.work-logo'), {
color: 'var(--color-highlight)',
duration: .125,
ease: 'power1.inOut',
})
})
work.addEventListener('mouseleave', (e) => {
if (isZoomed) return;
gsap.to(work, {
backgroundColor: 'var(--color-highlight)',
duration: .125,
ease: 'power1.inOut',
})
gsap.to(work.querySelector('.work-logo'), {
color: 'var(--color-bg)',
duration: .125,
ease: 'power1.inOut',
})
})
})
window.addEventListener('mousemove', mouseMoveHandler); 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: 'rgb(63, 111, 207)',
duration: .125,
ease: 'power1.inOut',
})
const logo = work.querySelector('.work-logo');
if (logo) {
gsap.to(logo, {
color: 'rgb(29, 12, 18)',
duration: .125,
ease: 'power1.inOut',
})
}
}
const handleMouseLeave = (work: HTMLElement) => {
if (isZoomed) return;
gsap.killTweensOf([work, work.querySelector('.work-logo')]);
gsap.to(work, {
backgroundColor: 'rgb(29, 12, 18)',
duration: .125,
ease: 'power1.inOut',
})
const logo = work.querySelector('.work-logo');
if (logo) {
gsap.to(logo, {
color: 'rgb(63, 111, 207)',
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 // Move the works the same way on touch drag
let touchMoveHandler = (e: TouchEvent) => { let touchMoveHandler = (e: TouchEvent) => {
@ -186,9 +200,9 @@
window.removeEventListener('mousemove', mouseMoveHandler); window.removeEventListener('mousemove', mouseMoveHandler);
window.removeEventListener('touchmove', touchMoveHandler); window.removeEventListener('touchmove', touchMoveHandler);
gsap.killTweensOf('.works, .work'); gsap.killTweensOf('.works, .work');
works.forEach((work, index) => { works.forEach((work) => {
work.removeEventListener('mouseenter', (e) => {}); work.removeEventListener('mouseenter', () => handleMouseEnter(work));
work.removeEventListener('mouseleave', (e) => {}); work.removeEventListener('mouseleave', () => handleMouseLeave(work));
work.removeEventListener('click', (e) => {}); work.removeEventListener('click', (e) => {});
}) })
} }

View file

@ -56,8 +56,9 @@
gsap.to('.logo-wrapper', { gsap.to('.logo-wrapper', {
opacity: 0, opacity: 0,
scale: 0.85,
zIndex: -1, zIndex: -1,
duration: 1, duration: .5,
ease: 'power2.out', ease: 'power2.out',
}) })
gsap.from('.work', { gsap.from('.work', {
@ -163,10 +164,10 @@
</div> </div>
<article class="work"> <article class="work">
{#if visible} {#if visible}
<h1>{data.title}</h1>
<div class="description"> <div class="description">
{data.description} {data.description}
</div> </div>
<h1>{data.title}</h1>
<div class="work-content"> <div class="work-content">
<div class="infobox"> <div class="infobox">
<div class="tasks"> <div class="tasks">
@ -245,19 +246,20 @@
} }
} }
h1 { h1 {
margin: .5em 0 0.25em 0; margin: .125em 0 0.5em 0;
font-size: 2.5rem; font-size: 2.5rem;
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
font-size: 5rem; font-size: 6rem;
} }
} }
.description { .description {
margin-bottom: 1.5em; margin-top: 1.5em;
line-height: 1.3; line-height: 1.3;
letter-spacing: -0.0075em; letter-spacing: 0;
text-transform: uppercase;
font-size: 1.25rem; font-size: 1.25rem;
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
font-size: 2.5rem; font-size: 1.75rem;
} }
} }
.infobox { .infobox {

View file

@ -11,6 +11,7 @@
overflow: hidden; overflow: hidden;
perspective: 700px; perspective: 700px;
transform-origin: 0 0; transform-origin: 0 0;
will-change: transform;
} }
.headline { .headline {
position: fixed; position: fixed;
@ -48,9 +49,8 @@
display: grid; display: grid;
grid-template-columns: repeat(6, 100vw); grid-template-columns: repeat(6, 100vw);
grid-auto-rows: 100vh; grid-auto-rows: 100vh;
// grid-template-rows: repeat(6, 100vh);
// grid-auto-flow: column;
gap: 6px; gap: 6px;
will-change: transform;
} }
.work { .work {
background-color: var(--color-highlight); background-color: var(--color-highlight);
@ -59,7 +59,9 @@
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
opacity: 0; opacity: 0;
visibility: hidden;
transform: translateZ(700px); transform: translateZ(700px);
will-change: transform;
} }
:global(.work-logo) { :global(.work-logo) {
width: 100%; width: 100%;
@ -70,6 +72,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
will-change: transform;
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
width: 60%; width: 60%;