This commit is contained in:
saiminh 2023-08-18 19:38:00 +02:00
parent 5faecf4f25
commit 1eafc280bd
27 changed files with 683 additions and 439 deletions

View file

@ -60,12 +60,32 @@ onMount(()=>{
let center = [0.5, 0.5]; let center = [0.5, 0.5];
let bulgefilter = new BulgePinchFilter(); let bulgefilter = new BulgePinchFilter();
bulgefilter.radius = xFrac(0.45); bulgefilter.radius = xFrac(0.45);
bulgefilter.strength = 0.5; bulgefilter.strength = 0;
bulgefilter.center = center; bulgefilter.center = center;
bulgefilter.resolution = 2; bulgefilter.resolution = 2;
app.stage.filters = [bulgefilter]; app.stage.filters = [bulgefilter];
gsap.to(bulgefilter, {
strength: 0.5,
duration: 2,
delay: 1,
ease: 'elastic.out',
});
window.addEventListener('mousedown', ()=>{
gsap.to(bulgefilter, {
strength: 0,
duration: 1,
ease: 'elastic.out',
});
})
window.addEventListener('mouseup', ()=>{
gsap.to(bulgefilter, {
strength: 0.5,
duration: 2,
ease: 'elastic.out',
});
})
/*---------------------------------- /*----------------------------------
* Convert text to canvas using * Convert text to canvas using
@ -134,13 +154,27 @@ onMount(()=>{
/*---------------------------------- /*----------------------------------
* Mousemove events * Mousemove events
*----------------------------------*/ *----------------------------------*/
let tween = {
x: 0.5,
y: 0.5,
};
window.addEventListener('mousemove', (e) => { window.addEventListener('mousemove', (e) => {
const pointerX = e.clientX / window.innerWidth; const pointerX = e.clientX / window.innerWidth;
const pointerY = e.clientY / window.innerHeight; const pointerY = e.clientY / window.innerHeight;
const pointerXfrac = pointerX - 0.5; const pointerXfrac = pointerX - 0.5;
const pointerYfrac = pointerY - 0.5; const pointerYfrac = pointerY - 0.5;
center = [(0.5 + pointerXfrac/10),(0.5 + pointerYfrac/10)]; // center = [(0.5 + pointerXfrac/10),(0.5 + pointerYfrac/10)];
gsap.to(tween, {
duration: .5,
ease: 'power3.out',
overwrite: true,
x: 0.5 + pointerXfrac/10,
y: 0.5 + pointerYfrac/10,
})
}) })
@ -151,7 +185,7 @@ onMount(()=>{
app.ticker.add((delta) => { app.ticker.add((delta) => {
elapsed += delta; elapsed += delta;
bulgefilter.center = [(center[0] + Math.sin(elapsed/200)/20 ),(center[1] + Math.cos(elapsed/200)/20 )]; bulgefilter.center = [(tween.x + Math.sin(elapsed/200)/20 ),(tween.y + Math.cos(elapsed/200)/20 )];
updateImgs(); updateImgs();
updateText(); updateText();
}) })

View file

@ -1,4 +1,4 @@
<svg id="floter-logo" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 287.4 83.5"> <svg id="floter-logo" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 287.4 83.5" width="290" height="85">
<path id="r" d="M259.9 82.4h-24.2l11.6-54.9h22l-2.7 12.9c4.5-8.9 11-14 18.1-14 1 0 2.1 0 2.7.2l-4.7 22.1c-1.4-.4-3.3-.9-6.4-.9-5.4 0-9.7 2.3-10.8 7.7l-5.6 26.9z"/> <path id="r" d="M259.9 82.4h-24.2l11.6-54.9h22l-2.7 12.9c4.5-8.9 11-14 18.1-14 1 0 2.1 0 2.7.2l-4.7 22.1c-1.4-.4-3.3-.9-6.4-.9-5.4 0-9.7 2.3-10.8 7.7l-5.6 26.9z"/>
<path id="e" d="M237.9 59.3h-35.7c.3 5.2 2.2 7.5 6.5 7.5 2.9 0 5.1-1.5 5.9-4.2h22.3c-4.3 13.2-14.5 20.9-30.4 20.9-16.3 0-26.5-9.7-26.5-25.2 0-17.8 13.4-32 32.3-32 16.3 0 26.5 9.7 26.5 25.2 0 2.7-.4 5.3-.9 7.8zM203.3 50h13.5c-.2-4-1.8-6.5-6-6.5-3.4 0-6.1 2.1-7.5 6.5z"/> <path id="e" d="M237.9 59.3h-35.7c.3 5.2 2.2 7.5 6.5 7.5 2.9 0 5.1-1.5 5.9-4.2h22.3c-4.3 13.2-14.5 20.9-30.4 20.9-16.3 0-26.5-9.7-26.5-25.2 0-17.8 13.4-32 32.3-32 16.3 0 26.5 9.7 26.5 25.2 0 2.7-.4 5.3-.9 7.8zM203.3 50h13.5c-.2-4-1.8-6.5-6-6.5-3.4 0-6.1 2.1-7.5 6.5z"/>
<path id="t" d="M171.5 65.9c1.9 0 4.3-.3 6.3-1L174 82.2c-2.5.8-7.1 1.3-11.1 1.3-14 0-23.2-5.1-19.6-22.3l3.5-16.7h-5.5l3.6-17h5.5l3.3-15.4H178l-3.3 15.4h11l-3.6 17h-11l-3.3 15.8c-.9 3.9.3 5.6 3.7 5.6z"/> <path id="t" d="M171.5 65.9c1.9 0 4.3-.3 6.3-1L174 82.2c-2.5.8-7.1 1.3-11.1 1.3-14 0-23.2-5.1-19.6-22.3l3.5-16.7h-5.5l3.6-17h5.5l3.3-15.4H178l-3.3 15.4h11l-3.6 17h-11l-3.3 15.8c-.9 3.9.3 5.6 3.7 5.6z"/>
@ -13,5 +13,6 @@
fill: currentColor; fill: currentColor;
height: 100%; height: 100%;
width: auto; width: auto;
display: block;
} }
</style> </style>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,128 @@
<script lang='ts'>
import { onMount } from "svelte";
import { gsap } from "gsap";
onMount(() => {
const navBlockElems: Array<HTMLElement> = Array.from(document.querySelectorAll('#nav'));
navBlockElems.forEach((el) => {
el.style.display = 'block';
});
gsap.set('#nav', { autoAlpha: 0, y: '100%'});
gsap.set('nav a', { autoAlpha: 0, y: 200 });
//Nav links close menu when clicked
let navlinks = document.querySelectorAll('nav a');
navlinks.forEach((link, index) => {
link.addEventListener('click', (e)=> {
const checkbox = document.querySelector('input[type="checkbox"]') as HTMLInputElement;
checkbox ? checkbox.checked = false : null;
gsap.to('.content > *', {duration: 0.5, y: '100%', autoAlpha: 1, ease: 'power4.out'});
gsap.to('#nav', {duration: 0.5, autoAlpha: 1, y: '100%', ease: 'power4.out', delay: 0.1});
gsap.to('nav a', {duration: 0.5, autoAlpha: 1, y: 200, stagger: 0.05, ease: 'power4.out' , delay: 0.2});
})
});
//Toggle menu
let checkbox = document.getElementById('menustate') as HTMLInputElement;
checkbox.addEventListener('change', (e)=> {
console.log(checkbox.checked);
if (checkbox.checked) {
gsap.to('.content > *', {duration: 0.75, y: -200, autoAlpha: 0.5, ease: 'power2.inOut'});
gsap.to('#nav', {duration: 0.5, autoAlpha: 1, y: 0, ease: 'power4.out', delay: 0.1});
gsap.to('nav a', {duration: 0.5, autoAlpha: 1, y: 0, stagger: 0.1, ease: 'power4.out' , delay: 0.2});
} else {
gsap.to('.content > *', {duration: 0.5, y: 0, autoAlpha: 1, ease: 'power4.out'});
gsap.to('#nav', {duration: 0.5, autoAlpha: 1, y: '100%', ease: 'power4.out', delay: 0.1});
gsap.to('nav a', {duration: 0.75, autoAlpha: 1, y: 200, stagger: 0.05, ease: 'power4.out' , delay: 0.2});
}
})
});
</script>
<header>
<input aria-hidden="true" type="checkbox" id="menustate" />
<label for="menustate" aria-hidden="true">
<span class="open"></span>
<span class="close">×</span>
</label>
<nav id="nav">
<a href="/">Home</a>
<a href="/work">About</a>
<a href="/work">Hire</a>
</nav>
</header>
<style lang="scss">
header {
position: fixed;
bottom: 0;
right: 0;
z-index: 3;
padding: var(--spacing-nav);
display: flex;
gap: .75em;
justify-content: flex-end;
align-items: flex-end;
box-sizing: border-box;
}
label {
height: 36px;
}
.open, .close {
font-size: 4em;
line-height: 0.3;
position: relative;
z-index: 2;
cursor: url('/pointer.svg'), auto;
}
.close {
top: 0.033em;
color: #FFF;
}
#menustate, #nav, .close {
/* Hide the checkbox, menu and close button by default */
display: none;
}
#menustate:checked ~ #nav ,
#menustate:checked ~ label .close {
/*
Show the menu and close button when the menu is open
(when the #menustate input field is checked)
*/
display: block;
}
#menustate:checked ~ label .open {
/* Hide the open button when the menu is open */
display: none;
}
#nav {
background-color: var(--color-bg);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: var(--spacing-outer);
}
#nav a {
display: block;
line-height: 1.3;
font-size: 3em;
font-weight: 800;
font-style: italic;
text-transform: lowercase;
text-decoration: none;
color: var(--color-text);
@media screen and (min-width: 768px) {
font-size: 5.5em;
}
&:first-child {
margin-top: 1em;
}
}
</style>

View file

@ -61,16 +61,16 @@ onMount(()=>{
let center = [0.5, 0.5]; let center = [0.5, 0.5];
let bulgefilter = new BulgePinchFilter(); let bulgefilter = new BulgePinchFilter();
bulgefilter.radius = xFrac(0.6); bulgefilter.radius = xFrac(0.5);
bulgefilter.strength = bulgeFactor; bulgefilter.strength = bulgeFactor;
bulgefilter.center = center; bulgefilter.center = center;
bulgefilter.resolution = 2; bulgefilter.resolution = 2;
// app.stage.filters = [bulgefilter]; // app.stage.filters = [bulgefilter];
let rgbFilter = new RGBSplitFilter(); // let rgbFilter = new RGBSplitFilter();
rgbFilter.red = [0, 0]; // rgbFilter.red = [0, 0];
rgbFilter.green = [0, 0]; // rgbFilter.green = [0, 0];
rgbFilter.blue = [0, 0]; // rgbFilter.blue = [0, 0];
rgbFilter.resolution = 2; // rgbFilter.resolution = 2;
app.stage.filters = [bulgefilter]; app.stage.filters = [bulgefilter];
@ -130,7 +130,7 @@ onMount(()=>{
image.position.set(imagePosition.x, imagePosition.y); image.position.set(imagePosition.x, imagePosition.y);
image.width = imagePosition.width; image.width = imagePosition.width;
image.height = imagePosition.height; image.height = imagePosition.height;
// image.alpha = imgElems[index].style.opacity as unknown as number; image.zIndex = imgElems[index].style.zIndex as unknown as number;
image.alpha = window.getComputedStyle(imgElems[index]).opacity as unknown as number; image.alpha = window.getComputedStyle(imgElems[index]).opacity as unknown as number;
}) })
} }
@ -146,17 +146,17 @@ onMount(()=>{
/*---------------------------------- /*----------------------------------
* Mousemove events * Mousemove events
*----------------------------------*/ *----------------------------------*/
let tween = { let tween = {
x: 0, x: 0,
y: 0, y: 0,
}; };
window.addEventListener('mousemove', (e) => { window.addEventListener('mousemove', (e) => {
const pointerX = e.clientX / window.innerWidth; const pointerX = e.clientX / window.innerWidth;
const pointerY = e.clientY / window.innerHeight; const pointerY = e.clientY / window.innerHeight;
const pointerXfrac = pointerX - 0.5; const pointerXfrac = pointerX - 0.5;
const pointerYfrac = pointerY - 0.5; const pointerYfrac = pointerY - 0.5;
rgbFilter.red = [pointerXfrac * 10, pointerYfrac * 10]; // rgbFilter.red = [pointerXfrac * 10, pointerYfrac * 10];
rgbFilter.green = [pointerXfrac * -10, pointerYfrac * -10]; // rgbFilter.green = [pointerXfrac * -10, pointerYfrac * -10];
gsap.to(tween, { gsap.to(tween, {
duration: .5, duration: .5,

View file

@ -15,13 +15,13 @@ type Post = {
export const fetchMarkdownPosts = async () => { export const fetchMarkdownPosts = async () => {
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const allPostFiles = import.meta.glob('/src/routes/work/\*.md') const allPostFiles = import.meta.glob('/src/routes/work/md/\*.md')
const iterablePostFiles = Object.entries(allPostFiles) const iterablePostFiles = Object.entries(allPostFiles)
const allPosts = await Promise.all( const allPosts = await Promise.all(
iterablePostFiles.map(async ([path, resolver]) => { iterablePostFiles.map(async ([path, resolver]) => {
const postPath = path.slice(11, -3) const postPath = path.slice(11, -3).replace('work/md/','work/')
const data: unknown = await resolver() const data: unknown = await resolver()
const postData = data as Post const postData = data as Post
const content = postData.default.render() as unknown as { html: string } const content = postData.default.render() as unknown as { html: string }

View file

@ -1,11 +1,27 @@
:root { :root {
--spacing-outer: 5vw; --spacing-outer: 5vw;
--spacing-nav: 5vw;
// --color-bg: #FFF;
// --color-text: #000;
--color-bg: #00117f; --color-bg: #00117f;
--color-text: #FF9494; --color-text: #FF9494;
--aspect-ratio-heroes: 1.5;
--font-size-p: clamp(20px, 1.6vw, 1.6vw);
@media screen and (min-width: 768px) {
--spacing-outer: 5vw;
--spacing-nav: 2.5vw;
}
}
body {
cursor: url('/cursor.svg'), auto;
}
a:hover, input:hover, button:hover {
cursor: url('/pointer.svg'), auto;
} }
body { body {
font-family: stratos, sans-serif; font-family: stratos, sans-serif;
font-size: 20px; font-size: var(--font-size-p);
padding: 0; padding: 0;
margin: 0; margin: 0;
background-color: var(--color-bg); background-color: var(--color-bg);
@ -16,12 +32,21 @@ body * {
box-sizing: border-box; box-sizing: border-box;
} }
h1, h2, h3, h4, h5 { h1, h2, h3, h4, h5 {
font-size: 1em; font-size: 2em;
line-height: 1.2;
font-weight: 900; font-weight: 900;
font-style: italic;
letter-spacing: -0.02em;
} }
p { ul, ol {
padding-left: 0;
}
ul {
list-style: '▪︎ ';
}
p, li {
font-weight: 400; font-weight: 400;
font-size: clamp(20px, 1.6vw, 1.6vw); font-size: var(--font-size-p);
line-height: 1.3; line-height: 1.3;
} }
a { a {
@ -89,3 +114,21 @@ a {
} }
} }
} }
.infobox {
// border-top: 2px solid var(--color-text);
// border-bottom: 2px solid var(--color-text);
padding: 1.5em 0;
font-size: var(--font-size-p);
& li {
border-bottom: 1px solid;
padding: 0.5em 0;
}
& > :first-child {
margin-top: 0;
}
& > :last-child {
margin-bottom: 0;
}
}

3
src/lib/utils/stores.ts Normal file
View file

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const workbulge = writable(0.25);

View file

@ -1,139 +1,33 @@
<script lang='ts'> <script lang='ts'>
import '$lib/styles/global.scss'; import '$lib/styles/global.scss';
import Logo from '$lib/components/Logo.svelte'; import Logo from '$lib/components/Logo.svelte';
import gsap from 'gsap'; import MainNav from '$lib/components/MainNav.svelte';
import { onMount } from 'svelte'; import { fade } from 'svelte/transition';
export let data;
onMount(() => {
let navlinks = document.querySelectorAll('nav a');
navlinks.forEach((link, index) => {
link.addEventListener('click', (e)=> {
const checkbox = document.querySelector('input[type="checkbox"]') as HTMLInputElement;
checkbox ? checkbox.checked = false : null;
gsap.to('.content > *', {duration: 0.5, y: '0%', autoAlpha: 1, ease: 'power4.out'});
})
});
let checkbox = document.getElementById('menustate') as HTMLInputElement;
checkbox.addEventListener('change', (e)=> {
if (checkbox.checked) {
gsap.to('.content > *', {duration: 0.75, y: -200, autoAlpha: 0.5, ease: 'power2.inOut'});
gsap.from('#nav', {duration: 0.5, autoAlpha: 0, y: '100%', ease: 'power4.out', delay: 0.1});
gsap.from('nav a', {duration: 0.5, autoAlpha: 0, y: 20, stagger: 0.1, ease: 'power4.out' , delay: 0.2});
} else {
gsap.to('.content > *', {duration: 0.5, y: '0%', autoAlpha: 1, ease: 'power4.out'});
}
})
});
</script> </script>
<a href='/' class="logo">
<Logo />
</a>
<header>
<input aria-hidden="true" type="checkbox" id="menustate" />
<label for="menustate" aria-hidden="true">
<span class="open"></span>
<span class="close">×</span>
</label>
<!-- <a href="#nav" class="logo"><Logo /></a> -->
<nav id="nav">
<a href="/">Home</a>
<a href="/work">About</a>
<a href="/work">Hire</a>
</nav>
</header>
<div class="content"> <a href='/' class="logo"><Logo /></a>
<slot /> <MainNav />
</div>
{#key data.pathname}
<div
class="content"
in:fade={{ duration: 100, delay: 0 }}
out:fade={{ duration: 100, delay: 100 }}
>
<slot />
</div>
{/key}
<style lang="scss"> <style lang="scss">
header {
position: fixed;
bottom: 0;
right: 0;
z-index: 3;
padding: var(--spacing-outer);
display: flex;
gap: .75em;
justify-content: flex-end;
align-items: flex-end;
box-sizing: border-box;
// width: 100%;
// &:before {
// content: '';
// position: absolute;
// bottom: 0;
// left: 0;
// width: 100%;
// height: 100%;
// background: linear-gradient(0deg, #00117fFF 0%, #00117fFF 30%, #00117f00 100%);
// }
}
label {
height: 36px;
}
.open, .close {
font-size: 4em;
line-height: 0.3;
position: relative;
z-index: 2;
cursor: pointer;
}
.close {
top: 0.033em;
color: #FFF;
}
#menustate, #nav, .close {
/* Hide the checkbox, menu and close button by default */
display: none;
}
#menustate:checked ~ nav ,
#menustate:checked ~ label .close {
/*
Show the menu and close button when the menu is open
(when the #menustate input field is checked)
*/
display: block;
}
#menustate:checked ~ label .open {
/* Hide the open button when the menu is open */
display: none;
}
.logo { .logo {
height: 36px; height: 36px;
width: auto; width: auto;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
z-index: 4; z-index: 4;
display: flex;
margin: var(--spacing-outer);
}
nav {
background-color: var(--color-bg);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: var(--spacing-outer);
}
nav a {
display: block; display: block;
line-height: 1.3; margin: var(--spacing-nav);
font-size: 5.5em; cursor: url('/pointer.svg'), auto;
font-weight: 800;
font-style: italic;
text-transform: lowercase;
text-decoration: none;
color: var(--color-text);
&:first-child {
margin-top: 1em;
}
} }
</style> </style>

7
src/routes/+layout.ts Normal file
View file

@ -0,0 +1,7 @@
export const load = ({ url }) => {
const { pathname } = url
return {
pathname
}
}

View file

@ -3,63 +3,65 @@
import { gsap } from 'gsap'; import { gsap } from 'gsap';
import ScrollTrigger from 'gsap/dist/ScrollTrigger'; import ScrollTrigger from 'gsap/dist/ScrollTrigger';
import SplitText from 'gsap/dist/SplitText'; import SplitText from 'gsap/dist/SplitText';
import { onMount } from 'svelte'; import { onMount, onDestroy } from 'svelte';
let canvasElems: Array<HTMLElement>; let canvasElems: Array<HTMLElement>;
onMount(() => { onMount(() => {
gsap.registerPlugin( ScrollTrigger, SplitText ); gsap.registerPlugin( ScrollTrigger, SplitText );
const sections = document.querySelectorAll('section'); const sections = document.querySelectorAll('section');
sections.forEach( (section) => { sections.forEach( (section) => {
if ( section.classList.contains('splash')){ if ( section.classList.contains('splash')){
let split = new SplitText(section.querySelectorAll('h1'), { type: 'lines', linesClass: 'lineChildren' }); let split = new SplitText(section.querySelectorAll('h1'), { type: 'lines', linesClass: 'lineChildren' });
gsap.set(split.lines, { gsap.set(split.lines, {
opacity: 0, opacity: 1,
y: window.innerHeight * 0.25, y: window.innerHeight,
}) })
gsap.to(split.lines, { gsap.to(split.lines, {
duration: 1, duration: 1,
opacity: 1, opacity: 1,
y: 0, y: 0,
stagger: 0.075, stagger: 0.075,
ease: 'power4.out', ease: 'power4.out',
})
}
else {
let split = new SplitText(section.querySelectorAll('h2'), { type: 'lines', linesClass: 'lineChildren' });
gsap.set(split.lines, { transformOrigin: '0% 100%' });
gsap.set(split.lines, { yPercent: 100, opacity: 0 });
gsap.to(split.lines, { duration: 1, yPercent: 0, opacity: 1, stagger: 0.1, ease: 'power4.out',
scrollTrigger: { scroller: '.scroller', trigger: split.lines, start: 'top 90%', end: 'bottom 70%', scrub: true, id: 'sth2' }
})
if (section.querySelector('p')) {
let splitp = new SplitText(section.querySelectorAll('p'), { type: 'lines', linesClass: 'lineChildren' });
gsap.set(splitp.lines, { transformOrigin: '0% 100%' });
gsap.set(splitp.lines, { yPercent: 100, autoAlpha: 0 });
gsap.to(splitp.lines, { duration: 1, yPercent: 0, autoAlpha: 1, stagger: 0.05, ease: 'power4.out',
scrollTrigger: { scroller: '.scroller', trigger: splitp.lines, start: 'top 80%', end: 'bottom 40%', scrub: true }
}) })
} }
else { }
let split = new SplitText(section.querySelectorAll('h2'), { type: 'lines', linesClass: 'lineChildren' }); if (section.querySelector('.cols-2')){
gsap.set(split.lines, { transformOrigin: '0% 100%' }); gsap.set(section.querySelector('.cols-2'), { autoAlpha: 0 });
gsap.set(split.lines, { yPercent: 100, opacity: 0 }); gsap.to(section.querySelector('.cols-2'), { duration: 1, autoAlpha: 1, ease: 'power4.out',
gsap.to(split.lines, { duration: 1, yPercent: 0, opacity: 1, stagger: 0.1, ease: 'power4.out', scrollTrigger: { scroller: '.scroller', trigger: section.querySelector('.cols-2'), start: 'top 80%', end: 'bottom 40%', scrub: true }
scrollTrigger: { trigger: split.lines, start: 'top 90%', end: 'bottom 70%', scrub: true } })
}) }
if (section.querySelector('p')) { })
let splitp = new SplitText(section.querySelectorAll('p'), { type: 'lines', linesClass: 'lineChildren' }); canvasElems = Array.from(document.querySelectorAll('.lineChildren'));
gsap.set(splitp.lines, { transformOrigin: '0% 100%' });
gsap.set(splitp.lines, { yPercent: 100, autoAlpha: 0 });
gsap.to(splitp.lines, { duration: 1, yPercent: 0, autoAlpha: 1, stagger: 0.05, ease: 'power4.out',
scrollTrigger: { trigger: splitp.lines, start: 'top 80%', end: 'bottom 40%', scrub: true }
})
}
}
if (section.querySelector('.cols-2')){
gsap.set(section.querySelector('.cols-2'), { autoAlpha: 0 });
gsap.to(section.querySelector('.cols-2'), { duration: 1, autoAlpha: 1, ease: 'power4.out',
scrollTrigger: { trigger: section.querySelector('.cols-2'), start: 'top 80%', end: 'bottom 40%', scrub: true }
})
}
})
canvasElems = Array.from(document.querySelectorAll('.lineChildren'));
}); });
</script> </script>
<article> <article class="scroller">
<section class="splash"> <section class="splash">
<h1>Simon Flöter creates products that stand out.</h1> <h1>Simon Flöter creates products that stand out.</h1>
</section> </section>
<section class="intro"> <section class="intro">
<h2>As a Creative Frontend Developer ...</h2> <h2>As a Creative Web Developer ...</h2>
<div class="cols-2"> <div class="cols-2">
<div> <div>
<p>I specialise in delivering beautifully crafted bespoke websites.</p> <p>I specialise in delivering beautifully crafted bespoke websites.</p>
@ -100,8 +102,14 @@
<HomeCanvas textsToCanvas={canvasElems}/> <HomeCanvas textsToCanvas={canvasElems}/>
<style lang="scss"> <style lang="scss">
article { .scroller {
font-size: clamp(32px, 4.5vw, 4.5vw); font-size: clamp(32px, 4.5vw, 4.5vw);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow-y: scroll;
} }
section { section {
scroll-snap-align: start; scroll-snap-align: start;
@ -129,6 +137,7 @@
line-height: 1.1; line-height: 1.1;
letter-spacing: -0.025em; letter-spacing: -0.025em;
font-weight: 400; font-weight: 400;
font-style: normal;
opacity: 0; opacity: 0;
margin: 0; margin: 0;
} }

View file

@ -1,222 +1,58 @@
<script lang="ts"> <script lang="ts">
import WorkCanvas from '$lib/components/WorkCanvas.svelte'; import WorkCanvas from '$lib/components/WorkCanvas.svelte';
import { onMount, onDestroy } from 'svelte'; import { onMount } from 'svelte';
import gsap from 'gsap'; import { workClickHandler, initWorkPage } from './workUtils.js';
import ScrollTrigger from 'gsap/dist/ScrollTrigger'; import { workbulge } from '$lib/utils/stores.js';
import SplitText from 'gsap/dist/SplitText';
export let data; export let data;
let canvasTextElems: Array<HTMLElement>; let canvasTextElems: Array<HTMLElement>;
let canvasImgElems: Array<HTMLElement>; let canvasImgElems: Array<HTMLElement>;
let bulge = { factor: 0.15 }; let bulge = {factor: 0};
onMount(() => { workbulge.subscribe((val) => {
bulge.factor = val;
gsap.registerPlugin( ScrollTrigger, SplitText );
let h1 = document.querySelector('h1') as HTMLElement;
let h1Text = h1?.innerHTML || '';
h1.innerHTML = h1Text + h1Text + h1Text + h1Text || '';
h1.style.overflow = 'hidden';
h1.style.whiteSpace = 'nowrap';
h1.querySelectorAll('span').forEach((span) => {
gsap.to(span, {xPercent: -100, duration: 4, ease: 'none', repeat: -1 })
});
const split = new SplitText('h1', { type: 'words', wordsClass: 'words' });
gsap.set(split.words, { opacity: 0 });
canvasTextElems = Array.from(document.querySelectorAll('.words'));
gsap.set(canvasTextElems, {
opacity: 0, yPercent: -10
})
gsap.to(canvasTextElems, {
duration: 1,
opacity: 1,
yPercent: 0,
stagger: 0.1,
ease: 'power4.out',
})
canvasImgElems = Array.from(document.querySelectorAll('.workhero'));
gsap.set(canvasImgElems, {
opacity: 0, yPercent: -10
})
gsap.to(canvasImgElems, {
duration: 1,
opacity: 1,
yPercent: 0,
stagger: 0.1,
ease: 'power4.out',
})
}); });
function workClickHandler(e:Event){ onMount(() => {
e.preventDefault(); const headline: HTMLElement = document.querySelector('.headline') as HTMLElement;
const target = e.target as HTMLElement; const images: Array<HTMLElement> = Array.from(document.querySelectorAll('.workhero'));
const originalLink = target.getAttribute('href') as string; let canvasElems = initWorkPage( headline, images );
const targetImg = target.querySelector('.workhero') as HTMLElement; canvasTextElems = canvasElems.text as Array<HTMLElement>;
const targetImgRect = targetImg.getBoundingClientRect(); canvasImgElems = canvasElems.images;
const targetImgAspectRatio = targetImgRect.width / targetImgRect.height; });
target.classList.add('active');
targetImg.style.width = `${targetImgRect.width}px`;
targetImg.style.height = `${targetImgRect.height}px`;
targetImg.style.top = `${targetImgRect.top}px`;
targetImg.style.left = `${targetImgRect.left}px`;
targetImg.style.position = 'fixed';
gsap.to('.work:not(.active) .workhero', {
duration: .3,
opacity: 0,
yPercent: 10,
stagger: 0.1,
ease: 'power4.out',
})
gsap.set(targetImg, {
zIndex: 100,
})
gsap.to(targetImg, {
duration: .6,
top: 0,
left: 0,
width: window.innerWidth,
height: window.innerWidth / targetImgAspectRatio,
ease: 'circ.inOut',
delay: .3,
})
gsap.to(bulge, {
duration: .3,
factor: 0,
ease: 'circ.inOut',
onUpdate: () => {
bulge.factor = bulge.factor;
}
})
gsap.to('h1', {
duration: .3,
yPercent: -200,
ease: 'circ.inOut',
})
// after a timeout of 1000ms (1s), navigate to the original link
setTimeout(() => {
window.location.href = originalLink;
}, 1000);
}
</script> </script>
<h1><span>Casestudies</span></h1> <h1 class="headline"><span>Casestudies</span></h1>
<div class="works"> <div class="works">
{#each data.posts as work} {#each data.posts as work}
<a href="{work.path}" class="work" on:click={ workClickHandler }> <a href="{work.path}" class="work" on:click={ (e) => workClickHandler(e) }>
<img class="workhero" src="{work.meta.header_bg_image}" alt="{work.meta.title}"/> <img class="workhero" src="{work.meta.header_bg_image}" alt="{work.meta.title}"/>
<h2>{work.meta.title}</h2> <h2>{work.meta.title}</h2>
</a> </a>
{/each} {/each}
{#each data.posts as work} {#each data.posts as work}
<a href="{work.path}" class="work" on:click={ workClickHandler }> <a href="{work.path}" class="work" on:click={ (e) => workClickHandler(e) }>
<img class="workhero" src="{work.meta.header_bg_image}" alt="{work.meta.title}"/> <img class="workhero" src="{work.meta.header_bg_image}" alt="{work.meta.title}"/>
<h2>{work.meta.title}</h2> <h2>{work.meta.title}</h2>
</a> </a>
{/each} {/each}
{#each data.posts as work} {#each data.posts as work}
<a href="{work.path}" class="work" on:click={ workClickHandler }> <a href="{work.path}" class="work" on:click={ (e) => workClickHandler(e) }>
<img class="workhero" src="{work.meta.header_bg_image}" alt="{work.meta.title}"/> <img class="workhero" src="{work.meta.header_bg_image}" alt="{work.meta.title}"/>
<h2>{work.meta.title}</h2> <h2>{work.meta.title}</h2>
</a> </a>
{/each} {/each}
{#each data.posts as work} {#each data.posts as work}
<a href="{work.path}" class="work" on:click={ workClickHandler }> <a href="{work.path}" class="work" on:click={ (e) => workClickHandler(e) }>
<img class="workhero" src="{work.meta.header_bg_image}" alt="{work.meta.title}"/> <img class="workhero" src="{work.meta.header_bg_image}" alt="{work.meta.title}"/>
<h2>{work.meta.title}</h2> <h2>{work.meta.title}</h2>
</a> </a>
{/each} {/each}
</div> </div>
<WorkCanvas textsToCanvas={canvasTextElems} imgsToCanvas={canvasImgElems} bulgeFactor={bulge.factor} /> <WorkCanvas
textsToCanvas={canvasTextElems}
imgsToCanvas={canvasImgElems}
bulgeFactor={bulge.factor}
/>
<style src="./work.scss" lang="scss"></style>
<style lang="scss">
h1 {
font-size: 12vw;
font-style: italic;
margin: 0.5em 0 0.5em 0;
letter-spacing: -0.04em;
text-align: center;
position: fixed;
top: 0;
width: 100%;
visibility: hidden;
@media screen and (min-width: 768px) {
font-size: 7vw;
}
& span {
display: inline-block;
padding: 0 0.25em;
}
}
h2 {
line-height: 1.1;
letter-spacing: -0.025em;
font-weight: 400;
font-size: 2vw;
// visibility: hidden;
opacity: 0;
position: absolute;
right: 0%;
top: 0%;
transform: scale(0.8);
margin: 0;
padding: 0.1em 0.4em 0.2em 0.4em;
border-radius: .25em;
color: var(--color-bg);
text-align: center;
transform-origin: center center;
width: auto;
margin: 0;
background-color: var(--color-text);
transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
.works .work:hover & {
opacity: 1;
transform: scale(1);
}
}
.works {
padding: 24vw 0.5em 0.5em 0.5em;
@media screen and (min-width: 768px) {
padding: 14vw 0.5em 0.5em 0.5em;
display: flex;
flex-wrap: wrap;
gap: 0.25em;
width: 100%;
padding-bottom: 20svh;
}
}
.work {
flex: 0 0 calc(33% - .125em);
display: block;
position: relative;
padding: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
text-decoration: none;
overflow: hidden;
// visibility: hidden;
}
.workhero {
width: 100%;
// aspect-ratio: 4/3;
object-fit: cover;
visibility: hidden;
display: block;
}
</style>

View file

@ -1,14 +1,16 @@
export async function load({ params }){ export async function load({ params }){
try { try {
const post = await import(`../${params.slug}.md`) const post = await import(`../md/${params.slug}.md`)
const { title, date, header_bg_image } = post.metadata const { title, date, header_bg_image, svg, video } = post.metadata
const Content = post.default.render() const Content = post.default.render()
return { return {
title, title,
date, date,
header_bg_image, header_bg_image,
Content svg,
video,
Content,
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View file

@ -1,45 +1,160 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { gsap } from 'gsap';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
export let data; export let data;
let visible = false; let visible = false;
function animForDesktop() {
gsap.to('.hero', { duration: .6, x: "-10%", ease: "cubic.inOut" })
gsap.fromTo('.heromask', {
clipPath: "polygon(0 0, 100% 0, 100% 100%, 0% 100%)",
},{
clipPath: "polygon(0 0, 50% 0, 25% 100%, 0% 100%)",
duration: .6,
ease: "cubic.inOut"
})
}
function animForMobile() {
gsap.to('.hero', { duration: .6, x: 0, ease: "cubic.inOut" })
gsap.to('.heromask', {
clipPath: "polygon(0 0, 100% 0, 100% 90%, 0% 100%)",
duration: .6,
ease: "cubic.inOut"
})
}
function animForSize(){
if ( window.matchMedia("(min-width: 768px) and (orientation: landscape)").matches ) {
animForDesktop();
} else {
animForMobile();
}
}
onMount(() => { onMount(() => {
let workheros = document.querySelector('.hero'); let workheros = document.querySelector('.hero');
console.log(workheros); console.log(workheros);
visible = true; visible = true;
animForSize();
window.addEventListener('resize', () => {
animForSize();
})
}) })
</script> </script>
<article> <div class="heromask">
<img class="hero" src="{data.header_bg_image}" alt="{data.title}" /> <img class="hero" src="{data.header_bg_image}" alt="{data.title}" />
<!-- <video class="herovid" autoplay muted loop playsinline>
<source src="{data.video}" type="video/mp4">
</video> -->
</div>
<div class="subnav">
<a href="/work" class="subnav-item">← Back</a>
</div>
<article>
{#if visible} {#if visible}
<h1 in:fly>{data.title}</h1> <div class="work-content" in:fly={{ x: '10vw', duration: 400, delay: 400, opacity:0 }}>
<p in:fly>{@html data.Content.html}</p> <h1>{@html data.svg}</h1>
<div class="work-content-text">
{@html data.Content.html}
</div>
</div>
{/if} {/if}
</article> </article>
<style lang="scss"> <style lang="scss">
article { article:after {
content: '';
position: fixed;
right: 0; bottom: 0; left: 0;
height: calc(36px + 2 * var(--spacing-outer));
z-index: 1;
border-top: 1px solid var(--color-text);
background: #00117f;
@media screen and (min-width: 768px) {
content: none;
}
}
.subnav {
position: fixed;
top: 0;
right: 0;
z-index: 4;
padding: var(--spacing-outer); padding: var(--spacing-outer);
} }
.hero { .heromask {
position: fixed; position: fixed;
top: 0; top: 0;
left:0; left:0;
width: 100%; width: 100%;
z-index: 0; z-index: 2;
height: auto; height: auto;
// object-fit: cover; aspect-ratio: var(--aspect-ratio-heroes);
clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
@media screen and (min-width: 768px) {
position: fixed;
}
} }
h1, p { .hero {
width: 100%;
height: 100%;
object-fit: fill;
z-index: 0;
display: block;
position: relative;
perspective: 400px;
}
.herovid{
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
display: block;
position: absolute;
top: -10%;
left: -10%;
perspective: 300px;
animation: fadein 2s .5s both;
}
@keyframes fadein {
from { opacity: 0; transform: rotateX(0deg); }
to { opacity: 1; transform: rotateX(45deg);}
}
.work-content {
padding: var(--spacing-outer);
padding-top: calc(100vw / var(--aspect-ratio-heroes) + 1.5em);
position: relative;
z-index: 1;
margin-top: 0;
color: var(--color-text);
@media screen and (min-width: 768px) {
margin-left: 40vw;
max-width: 60vw;
padding-top: calc( 3 * var(--spacing-outer) );
padding-left: calc(var(--spacing-outer) * 1.5);
padding-right: calc(var(--spacing-outer) * 2.5);
}
}
.work-content-text {
border-top: 1px solid var(--color-text);
}
h1 {
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
@media (max-width: 767px){ h1 {
h1 { max-width: 200px;
margin-top: calc(100vw / 1.333); margin: 0;
padding: 0 0 1em 0;
@media screen and (min-width: 768px) {
max-width: 400px;
} }
} }
</style> </style>

View file

@ -1,21 +0,0 @@
---
title: Adidas
header_bg_image: /work/adidas/adidas_hero.jpg
order: 12
description: Adidas, as you might have heard, is an international sports and lifestyle clothing brand. I worked on several digital campaigns as a graphic designer and illustrator.
svg: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 1440 900" viewBox="0 0 1440 900">
<path d="M1440 900v-.5l-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.5-14.3-14.2-8.2.5-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.2.5-14.3-14.2-8.3.5-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.2.5-14.3-14.2-8.3.5-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.2.4-14.3-14.1-8.3.5-14.2-14.2-8.3.5-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.3.5-14.2-14.2-8.3.5-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.2.5-14.3-14.2-8.3.5-14.3-14.2-8.2.5-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.3-14.1-8.2.4-14.3-14.1-8.3.4-14.3-14.1-8.3.5-14.2-14.2-8.3.5-14.3-14.2-8.3.4-11.9H707.9l-.1.6 14.1 8.3-.4 14.3 14.1 8.2-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.5 14.3 14.2 8.2-.5 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.2-.5 14.3 14.2 8.3-.5 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.2-.5 14.3 14.2 8.3-.5 14.2 14.2 8.3-.5 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.5 14.3 14.2 8.2-.5 14.3 14.1 8.3-.4 14.3 14.1 8.2-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.5 14.2 14.2 8.3-.5 14.3 14.2 8.3-.5 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.5 14.3 14.2 8.3-.5 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.3 14.1 8.2V900z" style="fill:#ffb7ab"/>
</svg>'
---
<div class="infobox">
<p>Client: <a href="https://www.adidas-group.com/en/">Adidas E-commerce dept</a></p>
Tasks:
<ul>
<li>Visual Design</li>
<li>Campaign asset production</li>
<li>Illustration</li>
</ul>
</div>
<div>
<p><span class="drop_cap">A</span>didas, as you might have heard, is an international sports and lifestyle clothing brand. I worked on several digital campaigns as a graphic designer and illustrator.</p>
</div>

View file

@ -1,21 +0,0 @@
---
title: Formo.bio
header_bg_image: /work/formo/formo_hero.png
order: 12
description: Formo, as you might have heard, is an international sports and lifestyle clothing brand. I worked on several digital campaigns as a graphic designer and illustrator.
svg: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 1440 900" viewBox="0 0 1440 900">
<path d="M1440 900v-.5l-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.5-14.3-14.2-8.2.5-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.2.5-14.3-14.2-8.3.5-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.2.5-14.3-14.2-8.3.5-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.2.4-14.3-14.1-8.3.5-14.2-14.2-8.3.5-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.3.5-14.2-14.2-8.3.5-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.2.5-14.3-14.2-8.3.5-14.3-14.2-8.2.5-14.3-14.1-8.3.4-14.2-14.1-8.3.4-14.3-14.1-8.3.4-14.3-14.1-8.2.4-14.3-14.1-8.3.4-14.3-14.1-8.3.5-14.2-14.2-8.3.5-14.3-14.2-8.3.4-11.9H707.9l-.1.6 14.1 8.3-.4 14.3 14.1 8.2-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.5 14.3 14.2 8.2-.5 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.2-.5 14.3 14.2 8.3-.5 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.2-.5 14.3 14.2 8.3-.5 14.2 14.2 8.3-.5 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.5 14.3 14.2 8.2-.5 14.3 14.1 8.3-.4 14.3 14.1 8.2-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.5 14.2 14.2 8.3-.5 14.3 14.2 8.3-.5 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.5 14.3 14.2 8.3-.5 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.2 14.1 8.3-.4 14.3 14.1 8.3-.4 14.3 14.1 8.2V900z" style="fill:#ffb7ab"/>
</svg>'
---
<div class="infobox">
<p>Client: <a href="https://www.adidas-group.com/en/">Adidas E-commerce dept</a></p>
Tasks:
<ul>
<li>Visual Design</li>
<li>Campaign asset production</li>
<li>Illustration</li>
</ul>
</div>
<div>
<p><span class="drop_cap">A</span>didas, as you might have heard, is an international sports and lifestyle clothing brand. I worked on several digital campaigns as a graphic designer and illustrator.</p>
</div>

View file

@ -0,0 +1,9 @@
---
title: Adidas
header_bg_image: /work/adidas/adidas_hero.jpg
order: 12
description: Adidas, as you might have heard, is an international sports and lifestyle clothing brand. I worked on several digital campaigns as a graphic designer and illustrator.
svg: '<svg id="adidas-logo" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 1000 674.2" style="enable-background:new 0 0 1000 674.2; fill: currentColor" xml:space="preserve"><path d="M654.5 442.2 448.9 84.8 596.6 0l255.7 442.2H654.5"/><path d="m106.8 392.1 147.7-85.4 78.2 135.5H135.5l-28.7-50.1"/><path d="M396.7 670.2h42V500.1h-42v170.1z"/><path d="M923.6 674.2c-47 0-75.3-24.3-76.8-58.5h44.3c0 10.7 6.7 26.4 35.4 26.9 19.1 0 28.1-11.3 28.1-19.7-1.1-13.4-18-14.5-35.9-17.4-18-2.9-33.3-6.1-44.3-11.8-14.1-7.3-23.7-22.9-23.7-40.9 0-30.4 26.4-54.5 70.3-54.5 42.6 0 69.6 22.4 72.4 55.6h-42.8c-.4-9-2.1-23.1-27.3-23.1-17 0-28.3 3.4-29.2 15.3 0 17.4 35.4 16.2 62.9 23.5 26.4 6.7 43.2 23.1 43.2 46.1-.2 42.2-34.4 58.5-76.6 58.5"/><path d="m280 240.4 147.7-85.2 165.7 287H438.8v42h-42V442L280 240.4"/><path class="st0" d="M283.8 674.2c-48.9 0-88.7-39.9-88.7-88.3 0-48.9 39.7-87.5 88.7-87.5 18.5 0 35.4 5 50.1 15.1v-71.3h42v228h-42v-11.3c-14.8 9.6-31.6 15.3-50.1 15.3zm-48.4-88.3c0 26.4 22.5 48.3 49.5 48.3 26.4 0 48.9-22 48.9-48.3 0-26.4-22.5-48.9-48.9-48.9-26.9 0-49.5 22.5-49.5 48.9"/><path class="st0" d="M594.5 442.2H636v228h-41.5v-11.3c-14.1 9.6-31.5 15.3-50.6 15.3-48.3 0-88.1-39.9-88.1-88.3 0-48.9 39.7-87.5 88.1-87.5 19.1 0 35.9 5 50.6 15.1v-71.3zm-97.8 143.7c0 26.4 22.5 48.3 48.3 48.3 26.9 0 49.5-22 49.5-48.3 0-26.4-22.5-48.9-49.5-48.9-25.8 0-48.3 22.5-48.3 48.9"/><path class="st0" d="M738.2 674.2c-48.2 0-88.1-39.9-88.1-88.3 0-48.9 39.9-87.5 88.1-87.5 18.5 0 35.9 5 50.1 15.1v-13.6h42v170.3h-42v-11.3c-14.2 9.6-31 15.3-50.1 15.3zM691 585.9c0 26.4 22.5 48.3 48.9 48.3s48.3-22 48.3-48.3c0-26.4-22-48.9-48.3-48.9-26.4 0-48.9 22.5-48.9 48.9"/><path class="st0" d="M40.5 585.9c0 26.4 22.5 48.3 48.9 48.3 26.9 0 49.5-22 49.5-48.3 0-26.4-22.5-48.9-49.5-48.9-26.3 0-48.9 22.5-48.9 48.9zm47.8 88.3c-48.4 0-88.3-40-88.3-88.3 0-48.9 39.9-87.5 88.3-87.5 18.5 0 35.9 5 50.6 15.1v-13.6h41.5v170.3h-41.5v-11.3c-14.1 9.6-31.5 15.3-50.6 15.3"/></svg>'
---
Adidas, as you might have heard, is an international sports and lifestyle clothing brand. I worked on several digital campaigns as a graphic designer and illustrator.

View file

@ -0,0 +1,15 @@
---
title: Formo.bio
header_bg_image: /work/formo/formo_hero.png
video: /work/formo/formo-rec.webm
order: 12
description: Formo, as you might have heard, is an international sports and lifestyle clothing brand. I worked on several digital campaigns as a graphic designer and illustrator.
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 20" style="enable-background:new 0 0 70 20" xml:space="preserve"><path d="m3.4 5.6 7.9-3.3c.1 0 .1-.1.1-.2l-.9-2c-.1-.1-.2-.1-.2-.1L2.4 3.3C.9 3.9 0 5.3 0 6.9v12.3c0 .1.1.1.1.1h2.2c.1 0 .1-.1.1-.1v-8.7c.2.1.3.1.5.2l7.2 1.8c.1 0 .1 0 .1-.1l.6-2.1c0-.1 0-.1-.1-.1L3.6 8.3C2.7 8 2.5 7.2 2.5 7s0-1 .9-1.4zm59.7.7c-3.8 0-6.9 3-6.9 6.8s3.1 6.8 6.9 6.8 6.9-3 6.9-6.8-3.1-6.8-6.9-6.8zm0 11.1c-2.4 0-4.4-1.9-4.4-4.3s2-4.3 4.4-4.3 4.4 1.9 4.4 4.3-2 4.3-4.4 4.3zM18.8 6.3c-3.8 0-6.9 3-6.9 6.8s3.1 6.8 6.9 6.8 6.9-3 6.9-6.8-3.1-6.8-6.9-6.8zm0 11.1c-2.4 0-4.4-1.9-4.4-4.3s2-4.3 4.4-4.3 4.4 1.9 4.4 4.3-2 4.3-4.4 4.3zM53.1 7.1c-.9-.7-2-.9-3-.6l-3.2.9c-.3.1-.6.2-.9.4-.2-.3-.4-.5-.7-.7-.9-.7-2-.9-3-.6l-3.3.9c-1.6.4-2.7 1.9-2.7 3.5v8.3c0 .1.1.1.1.1h2.2c.1 0 .1-.1.1-.1v-8.3c0-.5.4-1 .9-1.1l3.2-.9c.4-.1.7.1.8.2.3.2.4.5.4.8v9.4c0 .1.1.1.1.1h2.2c.1 0 .1-.1.1-.1V11c0-.5.4-1 .9-1.1l3.2-.9c.4-.1.7.1.8.2.3.2.4.5.4.8v9.4c0 .1.1.1.1.1H54c.1 0 .1 0 .1-.1V9.9c.4-1.1-.1-2.1-1-2.8zm-23 .3c-1.6.4-2.7 1.9-2.7 3.5v8.3c0 .1.1.1.1.1h2.2c.1 0 .1-.1.1-.1v-8.3c0-.5.4-1 .9-1.1l4.6-1.3c.1 0 .1-.1.1-.1l-.6-2.1c0-.1-.1-.1-.1-.1l-4.6 1.2z" style="fill:currentColor"/></svg>'
---
## A website for the future of dairy.
Formo is using precision fermentation instead of cows to make dairy products and save the world.
Formo's previous website was lacking an easy way for the team to create new, visually engaging content without technical knowledge. After consulting with the team, we decided to use the latest version of WordPress which offers full support for the powerful Gutenberg editor.
We built custom content blocks for the team to use and combine when creating new pages and articles.

80
src/routes/work/work.scss Normal file
View file

@ -0,0 +1,80 @@
h1 {
font-size: 12vw;
font-style: italic;
margin: 0.5em 0 0.5em 0;
letter-spacing: -0.04em;
text-align: center;
position: fixed;
top: 0;
width: 100%;
visibility: hidden;
@media screen and (min-width: 768px) {
font-size: 7vw;
}
& span {
display: inline-block;
padding: 0 0.25em;
}
}
h2 {
line-height: 1.1;
letter-spacing: -0.025em;
// font-weight: 400;
font-size: 2.5vw;
// visibility: hidden;
opacity: 0;
position: absolute;
left: .5em;
bottom: .5em;
transform: scale(0.8) translateY(100%);
margin: 0;
padding: 0.1em 0.4em 0.2em 0.4em;
// border-radius: .25em;
text-align: center;
transform-origin: center center;
width: auto;
margin: 0;
background-color: var(--color-bg);
color: var(--color-text);
transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
.works .work:not(.active):hover & {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.works {
padding: 15vw 0.5em 0.5em 0.5em;
display: flex;
gap: 0.5em;
flex-wrap: wrap;
@media screen and (min-width: 768px) {
gap: 0.25em;
padding: 8.5vw 0.5em 0.5em 0.5em;
width: 100%;
padding-bottom: 20svh;
}
}
.work {
flex: 0 0 100%;
display: block;
position: relative;
padding: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
text-decoration: none;
overflow: hidden;
@media screen and (min-width: 768px) {
flex: 0 0 calc(33% - .125em);
}
}
.workhero {
width: 100%;
aspect-ratio: var(--aspect-ratio-heroes);
object-fit: fill;
visibility: hidden;
display: block;
}

View file

@ -0,0 +1,106 @@
import { goto } from '$app/navigation';
import gsap from 'gsap';
import SplitText from 'gsap/dist/SplitText';
import { workbulge } from '$lib/utils/stores';
const bulge = {factor: 0};
workbulge.subscribe(value => {
bulge.factor = value;
})
export function workClickHandler(e:Event){
e.preventDefault();
const target = e.target as HTMLElement;
const originalLink = target.getAttribute('href') as string;
const targetImg = target.querySelector('.workhero') as HTMLElement;
const targetImgRect = targetImg.getBoundingClientRect();
const targetImgAspectRatio = targetImgRect.width / targetImgRect.height;
target.classList.add('active');
targetImg.style.width = `${targetImgRect.width}px`;
targetImg.style.height = `${targetImgRect.height}px`;
targetImg.style.top = `${targetImgRect.top}px`;
targetImg.style.left = `${targetImgRect.left}px`;
targetImg.style.position = 'fixed';
gsap.to('.work:not(.active) .workhero', {
duration: .3,
opacity: 0,
yPercent: 10,
ease: 'power4.out',
})
gsap.set(targetImg, {
zIndex: 100,
})
gsap.to(targetImg, {
duration: .3,
top: 0,
left: 0,
width: window.innerWidth,
height: window.innerWidth / targetImgAspectRatio,
ease: 'circ.inOut',
delay: 0.3,
})
gsap.to(bulge, {
duration: .3,
factor: 0,
ease: 'circ.inOut',
onUpdate: () => {
workbulge.set(bulge.factor);
}
})
gsap.to('h1', {
duration: .3,
yPercent: -200,
ease: 'circ.inOut',
})
setTimeout(() => {
goto(originalLink);
}, 600);
}
export function initWorkPage( h1: HTMLElement, canvasImgElems: Array<HTMLElement>) {
workbulge.set(0.25);
gsap.registerPlugin( SplitText );
console.log('initWorkPage:', h1);
const h1Text = h1?.innerHTML || '';
h1.innerHTML = h1Text + h1Text + h1Text + h1Text || '';
h1.style.overflow = 'hidden';
h1.style.whiteSpace = 'nowrap';
h1.querySelectorAll('span').forEach((span) => {
gsap.to(span, {xPercent: -100, duration: 4, ease: 'none', repeat: -1 })
});
const split = new SplitText(h1, { type: 'words', wordsClass: 'words' });
gsap.set(split.words, {
opacity: 0, yPercent: -10
})
gsap.to(split.words, {
duration: 1,
opacity: 1,
yPercent: 0,
stagger: 0.1,
ease: 'power4.out',
})
gsap.set(canvasImgElems, {
opacity: 1, yPercent: 100
})
gsap.to(canvasImgElems, {
duration: 1,
opacity: 1,
yPercent: 0,
stagger: 0.05,
ease: 'power4.out',
})
return {
text: Array.from(split.words),
images: canvasImgElems
}
}

BIN
static/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

1
static/cursor.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 50" style="enable-background:new 0 0 40 50" xml:space="preserve" width="40" height="50"><path style="fill:#ff9494;stroke:#00117f;stroke-width:3;stroke-miterlimit:10" d="M5.4 5 5 44.6l13.1-10.9L35 31.2z"/></svg>

After

Width:  |  Height:  |  Size: 266 B

BIN
static/pointer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

1
static/pointer.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 50" style="enable-background:new 0 0 40 50" xml:space="preserve" width="40" height="50"><path d="m17.2 12.2-5.7-8.1c-1.3-1.9-3.9-2.3-5.7-1l-.2.2C3.7 4.6 3.3 7.2 4.6 9l6.3 8.9c-3 1.7-2.4 4.8-2.4 4.8-2.2.8-3.2 2.4-3 4.8 0 0-3.9 2.1-3.1 6.1l6.4 9.1c3.5 5 10.4 6.2 15.3 2.7l4.6-3.3c3-2.1 4.7-5.5 4.7-9.1-.1-6.1 1-8.2-2-13.5l-4-9.6c-.6-1.1-1.8-1.6-3-1.3l-.7.2c-1.2.3-2 1.4-2 2.6l.4 7.6-4.9-6.8z" style="fill:#ff9494;stroke:#00117f;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:10"/></svg>

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

Binary file not shown.

View file

@ -1,5 +1,6 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite'; import { vitePreprocess } from '@sveltejs/kit/vite';
import preprocess from 'svelte-preprocess';
import { mdsvex } from 'mdsvex' import { mdsvex } from 'mdsvex'
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
@ -7,6 +8,7 @@ const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: [ preprocess: [
preprocess(),
vitePreprocess(), vitePreprocess(),
mdsvex({ mdsvex({
extensions: ['.md'] extensions: ['.md']