Compare commits
7 commits
41cb3d799c
...
e277b33bd8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e277b33bd8 | ||
|
|
d49773134f | ||
|
|
d7cab5d526 | ||
|
|
2149372530 | ||
|
|
01b217a2a5 | ||
|
|
06a5dd0232 | ||
|
|
e024c4258a |
25 changed files with 475 additions and 1763 deletions
1226
package-lock.json
generated
1226
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -38,14 +38,10 @@
|
|||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@pixi/core": "^7.4.3",
|
||||
"@pixi/filter-alpha": "^7.4.3",
|
||||
"@pixi/filter-blur": "^7.4.3",
|
||||
"@pixi/unsafe-eval": "^7.4.3",
|
||||
"gsap": "^3.13.0",
|
||||
"mdsvex": "^0.11.0",
|
||||
"pixi-filters": "^5.2.1",
|
||||
"pixi.js": "^7.2.4",
|
||||
"pixi.js": "^8.15.0",
|
||||
"pixi-filters": "^6.1.5",
|
||||
"superjson": "^1.13.1",
|
||||
"svelte-cloudinary": "^1.1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import '$lib/utils/pixiInit'; // Initialize PixiJS settings before importing filters
|
||||
import * as PIXI from 'pixi.js';
|
||||
import { BulgePinchFilter, TwistFilter } from 'pixi-filters';
|
||||
import * as filters from 'pixi-filters';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { SplitText } from 'gsap/SplitText';
|
||||
|
|
@ -23,6 +23,9 @@ let app: PIXI.Application;
|
|||
let canvas: HTMLCanvasElement;
|
||||
|
||||
onMount(()=>{
|
||||
let cleanup: (() => void) | undefined;
|
||||
|
||||
(async ()=>{
|
||||
|
||||
function xFrac(x: number){
|
||||
return window.innerWidth * x;
|
||||
|
|
@ -39,13 +42,14 @@ onMount(()=>{
|
|||
|
||||
gsap.registerPlugin(PixiPlugin, ScrollTrigger, SplitText);
|
||||
|
||||
app = new PIXI.Application({
|
||||
app = new PIXI.Application();
|
||||
await app.init({
|
||||
canvas: canvas,
|
||||
resizeTo: window,
|
||||
antialias: true,
|
||||
autoDensity: true,
|
||||
resolution: 2,
|
||||
backgroundAlpha: 0,
|
||||
view: canvas,
|
||||
});
|
||||
|
||||
//for debugging but Typescript has an issue with this:
|
||||
|
|
@ -60,9 +64,8 @@ onMount(()=>{
|
|||
app.stage.addChild(bulgegroup);
|
||||
|
||||
let bulgebg = new PIXI.Graphics();
|
||||
bulgebg.beginFill('rgb(0, 0, 0)');
|
||||
bulgebg.drawRect(0, 0, xFrac(1), yFrac(1));
|
||||
bulgebg.endFill();
|
||||
bulgebg.rect(0, 0, xFrac(1), yFrac(1));
|
||||
bulgebg.fill('rgb(0, 0, 0)');
|
||||
bulgebg.alpha = 0;
|
||||
bulgebg.pivot.set(xFrac(.5), yFrac(.5));
|
||||
bulgebg.x = xFrac(0.5);
|
||||
|
|
@ -71,13 +74,13 @@ onMount(()=>{
|
|||
|
||||
let center = [0.5, 0.5];
|
||||
|
||||
let bulgefilter = new BulgePinchFilter();
|
||||
let bulgefilter = new filters.BulgePinchFilter();
|
||||
bulgefilter.radius = is_landscape ? xFrac(0.5) : xFrac(0.55);
|
||||
bulgefilter.strength = 0.5;
|
||||
bulgefilter.center = is_landscape ? center : [0.5, 0];
|
||||
bulgefilter.resolution = 2;
|
||||
|
||||
let twistfilter = new TwistFilter();
|
||||
let twistfilter = new filters.TwistFilter();
|
||||
twistfilter.angle = 0;
|
||||
twistfilter.radius = is_landscape ? window.innerWidth/4 : window.innerWidth/2;
|
||||
twistfilter.offset = new PIXI.Point(window.innerWidth/2, window.innerHeight/3);
|
||||
|
|
@ -207,8 +210,8 @@ onMount(()=>{
|
|||
* ----------------------------------*/
|
||||
let elapsed = 0.0;
|
||||
|
||||
app.ticker.add((delta) => {
|
||||
elapsed += delta;
|
||||
app.ticker.add((ticker) => {
|
||||
elapsed += ticker.deltaTime;
|
||||
if (!is_landscape) {
|
||||
// bulgefilter.center = [(tween.x + Math.sin(elapsed/200)/20 ),(tween.y + Math.cos(elapsed/200)/20 )];
|
||||
let movingCenter = [(0.5 + Math.sin(elapsed/200)/20 ),(0.33 + Math.cos(elapsed/200)/20 )];
|
||||
|
|
@ -229,11 +232,17 @@ onMount(()=>{
|
|||
window.addEventListener('resize', (e) => {
|
||||
tween.y = is_fine ? 0.5 : 250/window.innerHeight;
|
||||
})
|
||||
return () => {
|
||||
|
||||
cleanup = () => {
|
||||
gsap.killTweensOf(imgElems);
|
||||
gsap.killTweensOf(elems);
|
||||
ScrollTrigger.getAll().forEach( instance => instance.kill() );
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
if (cleanup) cleanup();
|
||||
}
|
||||
}) // <- end onMount
|
||||
|
||||
onDestroy(() => {
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@
|
|||
</script>
|
||||
|
||||
<details bind:this={details}>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<summary on:click={summaryClickHandler} on:keyup={summaryClickHandler}>{@html summary}</summary>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<summary onclick={summaryClickHandler} onkeyup={summaryClickHandler}>{@html summary}</summary>
|
||||
<div class="faq-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ let app: PIXI.Application;
|
|||
let canvas: HTMLCanvasElement;
|
||||
|
||||
onMount(()=>{
|
||||
let cleanup: (() => void) | undefined;
|
||||
|
||||
(async ()=>{
|
||||
|
||||
function xFrac(x: number){
|
||||
return window.innerWidth * x;
|
||||
|
|
@ -38,13 +41,14 @@ onMount(()=>{
|
|||
|
||||
gsap.registerPlugin(PixiPlugin, ScrollTrigger, SplitText);
|
||||
|
||||
app = new PIXI.Application({
|
||||
app = new PIXI.Application();
|
||||
await app.init({
|
||||
canvas: canvas,
|
||||
resizeTo: document.querySelector('.canvasResizeToThis') as HTMLElement,
|
||||
antialias: true,
|
||||
autoDensity: true,
|
||||
resolution: 2,
|
||||
backgroundAlpha: 0,
|
||||
view: canvas
|
||||
});
|
||||
|
||||
//for debugging but Typescript has an issue with this:
|
||||
|
|
@ -59,9 +63,8 @@ onMount(()=>{
|
|||
app.stage.addChild(bulgegroup);
|
||||
|
||||
let bulgebg = new PIXI.Graphics();
|
||||
bulgebg.beginFill('rgb(0, 0, 0)');
|
||||
bulgebg.drawRect(0, 0, xFrac(1.2), yFrac(1.2));
|
||||
bulgebg.endFill();
|
||||
bulgebg.rect(0, 0, xFrac(1.2), yFrac(1.2));
|
||||
bulgebg.fill('rgb(0, 0, 0)');
|
||||
bulgebg.alpha = 0;
|
||||
bulgebg.pivot.set(xFrac(.5), yFrac(.5));
|
||||
bulgebg.x = xFrac(0.5);
|
||||
|
|
@ -202,8 +205,8 @@ onMount(()=>{
|
|||
* ----------------------------------*/
|
||||
let elapsed = 0.0;
|
||||
|
||||
app.ticker.add((delta) => {
|
||||
elapsed += delta;
|
||||
app.ticker.add((ticker) => {
|
||||
elapsed += ticker.deltaTime;
|
||||
if (is_portrait) {
|
||||
bulgefilter.center = [(0.5 + Math.sin(elapsed/200)/40 ),(0.45 + Math.cos(elapsed/200)/20 )];
|
||||
// bulgefilter.center = [0.5, 0.5];
|
||||
|
|
@ -218,11 +221,17 @@ onMount(()=>{
|
|||
window.addEventListener('resize', (e) => {
|
||||
tween.y = is_fine ? 0.5 : 250/window.innerHeight;
|
||||
})
|
||||
return () => {
|
||||
|
||||
cleanup = () => {
|
||||
gsap.killTweensOf(imgElems);
|
||||
gsap.killTweensOf(elems);
|
||||
ScrollTrigger.getAll().forEach( instance => instance.kill() );
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
if (cleanup) cleanup();
|
||||
}
|
||||
}) // <- end onMount
|
||||
|
||||
onDestroy(() => {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
<HomeIlluShape />
|
||||
<HomeIlluShape />
|
||||
<HomeIlluShape />
|
||||
<!-- <HomeIlluShape />
|
||||
<HomeIlluShape />
|
||||
<HomeIlluShape />
|
||||
<HomeIlluShape />
|
||||
|
|
@ -79,8 +80,7 @@
|
|||
<HomeIlluShape />
|
||||
<HomeIlluShape />
|
||||
<HomeIlluShape />
|
||||
<HomeIlluShape />
|
||||
<HomeIlluShape />
|
||||
<HomeIlluShape /> -->
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
gap: clamp(1px, 0.3vw, 5px);
|
||||
}
|
||||
: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;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang='ts'>
|
||||
import { onMount } from "svelte";
|
||||
import { gsap } from "gsap";
|
||||
import { page } from "$app/stores";
|
||||
import { page } from "$app/state";
|
||||
|
||||
onMount(() => {
|
||||
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
let navlinks = document.querySelectorAll('nav a');
|
||||
navlinks.forEach((link, index) => {
|
||||
link.addEventListener('click', (e)=> {
|
||||
if (e.target?.toString() === $page.url.toString()) {
|
||||
if (e.target?.toString() === page.url.toString()) {
|
||||
return;
|
||||
}
|
||||
const checkbox = document.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
|
|
@ -52,11 +52,11 @@
|
|||
<span class="close">×</span>
|
||||
</label>
|
||||
<nav id="nav">
|
||||
<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="/service" class="navlink {$page.route.id === '/service' ? 'current' : ''}">Services</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="/" class="navlink {page.route.id === '/' ? 'current' : ''}">Home</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="/about" class="navlink {page.route.id === '/about' ? 'current' : ''}">About</a> -->
|
||||
<a href="/contact" class="navlink {page.route.id === '/contact' ? 'current' : ''}">Contact</a>
|
||||
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ let {
|
|||
let app: PIXI.Application;
|
||||
let canvas: HTMLCanvasElement;
|
||||
|
||||
onMount( () => {
|
||||
onMount(() => {
|
||||
let cleanup: (() => void) | undefined;
|
||||
|
||||
(async () => {
|
||||
|
||||
let is_fine = window.matchMedia('(pointer:fine)').matches
|
||||
let is_landscape = window.matchMedia('(orientation:landscape)').matches
|
||||
|
|
@ -37,14 +40,15 @@ onMount( () => {
|
|||
const highlightColorFromRoot = () => { return getComputedStyle(root).getPropertyValue('--color-highlight') || 'rgb(0, 0, 0)' };
|
||||
const thisElemBgColor = (elem: HTMLElement) => { return getComputedStyle(elem).getPropertyValue('background-color') || 'rgb(255, 255, 255)' };
|
||||
|
||||
app = new PIXI.Application({
|
||||
app = new PIXI.Application();
|
||||
await app.init({
|
||||
canvas: canvas,
|
||||
resizeTo: window,
|
||||
antialias: true,
|
||||
autoDensity: true,
|
||||
resolution: 2,
|
||||
backgroundColor: bgColorFromRoot(),
|
||||
backgroundAlpha: 0,
|
||||
view: canvas,
|
||||
});
|
||||
|
||||
//for debugging but Typescript has an issue with this:
|
||||
|
|
@ -68,9 +72,8 @@ onMount( () => {
|
|||
let group_background = new PIXI.Graphics();
|
||||
function draw_group_background(group_background: PIXI.Graphics) {
|
||||
group_background.clear();
|
||||
group_background.beginFill(bgColorFromRoot());
|
||||
group_background.drawRect(0, 0, xFrac(1), yFrac(1));
|
||||
group_background.endFill();
|
||||
group_background.rect(0, 0, xFrac(1), yFrac(1));
|
||||
group_background.fill(bgColorFromRoot());
|
||||
group_background.alpha = 0;
|
||||
group_background.pivot.set(xFrac(.5), yFrac(.5));
|
||||
group_background.x = xFrac(0.5);
|
||||
|
|
@ -162,9 +165,8 @@ onMount( () => {
|
|||
workinfos.forEach( workinfo => {
|
||||
const workinfoRect = workinfo.getBoundingClientRect();
|
||||
let workinfoGraphic = new PIXI.Graphics();
|
||||
workinfoGraphic.beginFill(textColorFromRoot());
|
||||
workinfoGraphic.drawRect(workinfoRect.x, workinfoRect.y, workinfoRect.width, workinfoRect.height);
|
||||
workinfoGraphic.endFill();
|
||||
workinfoGraphic.rect(workinfoRect.x, workinfoRect.y, workinfoRect.width, workinfoRect.height);
|
||||
workinfoGraphic.fill(textColorFromRoot());
|
||||
workinfoGraphic.pivot.set(workinfoRect.x, workinfoRect.y);
|
||||
workinfoBgs.push(workinfoGraphic);
|
||||
app.stage.addChild(workinfoGraphic);
|
||||
|
|
@ -178,9 +180,8 @@ onMount( () => {
|
|||
workinfos.forEach((workinfo, index) => {
|
||||
const workinfoRect = workinfo.getBoundingClientRect();
|
||||
workinfoBgs[index].clear();
|
||||
workinfoBgs[index].beginFill(thisElemBgColor(workinfo as HTMLElement));
|
||||
workinfoBgs[index].drawRect(workinfoRect.x, workinfoRect.y, workinfoRect.width, workinfoRect.height);
|
||||
workinfoBgs[index].endFill();
|
||||
workinfoBgs[index].rect(workinfoRect.x, workinfoRect.y, workinfoRect.width, workinfoRect.height);
|
||||
workinfoBgs[index].fill(thisElemBgColor(workinfo as HTMLElement));
|
||||
workinfoBgs[index].pivot.set(workinfoRect.x, workinfoRect.y);
|
||||
workinfoBgs[index].position.set(workinfoRect.x - ((tween.x - 0.5) * 50), workinfoRect.y - ((tween.y - 0.5) * 50));
|
||||
workinfoBgs[index].alpha = window.getComputedStyle(workinfo).opacity as unknown as number;
|
||||
|
|
@ -224,8 +225,8 @@ onMount( () => {
|
|||
* ----------------------------------*/
|
||||
let elapsed = 0.0;
|
||||
|
||||
app.ticker.add((delta) => {
|
||||
elapsed += delta;
|
||||
app.ticker.add((ticker) => {
|
||||
elapsed += ticker.deltaTime;
|
||||
// bulgefilter.center = center;
|
||||
// bulgefilter.center = [(center[0] + Math.sin(elapsed/200)/20 ),(center[1] + Math.cos(elapsed/200)/20 )];
|
||||
bulgefilter.center = [tween.x, tween.y];
|
||||
|
|
@ -233,7 +234,19 @@ onMount( () => {
|
|||
updateImgs();
|
||||
updateText();
|
||||
updateWorkInfoBgs();
|
||||
})
|
||||
});
|
||||
|
||||
cleanup = () => {
|
||||
gsap.killTweensOf(imgElems);
|
||||
gsap.killTweensOf(elems);
|
||||
gsap.killTweensOf(tween);
|
||||
ScrollTrigger.getAll().forEach( instance => instance.kill() );
|
||||
};
|
||||
})();
|
||||
|
||||
return () => {
|
||||
if (cleanup) cleanup();
|
||||
};
|
||||
}) // <- end onMount
|
||||
|
||||
onDestroy(() => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,26 @@
|
|||
:root {
|
||||
--spacing-outer: 5vw;
|
||||
--spacing-nav: 5vw;
|
||||
--color-bg: rgb(207, 63, 63);
|
||||
--color-bg: rgb(207, 63, 70);;
|
||||
--color-text: rgb(255, 234, 217);
|
||||
--color-highlight: rgb(29, 12, 18);
|
||||
|
||||
--color-bg-variant-1: rgb(207, 63, 70);
|
||||
--color-text-variant-1: rgb(255, 234, 217);
|
||||
--color-highlight-variant-1: rgb(29, 12, 18);
|
||||
|
||||
--color-bg-variant-2: rgb(63, 111, 207);
|
||||
--color-text-variant-2: rgb(255, 228, 207);
|
||||
--color-highlight-variant-2: rgb(57, 0, 17);
|
||||
|
||||
--color-bg-variant-3: rgb(34, 169, 49);
|
||||
--color-text-variant-3: rgb(255, 232, 168);
|
||||
--color-highlight-variant-3: rgb(36, 0, 0);
|
||||
|
||||
--color-bg-variant-4: rgb(238, 127, 1);
|
||||
--color-text-variant-4: rgb(253, 254, 255);
|
||||
--color-highlight-variant-4: rgb(0, 39, 67);
|
||||
|
||||
--aspect-ratio-heroes: 1.5;
|
||||
--font-size-p: clamp(20px, 1.6vw, 1.6vw);
|
||||
|
||||
|
|
@ -41,6 +58,7 @@ body {
|
|||
min-height: 100svh;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
transition: background-color .5s ease-in-out, color .5s ease-in-out;
|
||||
}
|
||||
body * {
|
||||
box-sizing: border-box;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default function createCanvasText(
|
|||
|
||||
if (elemSrc.includes('.svg')) {
|
||||
scalefactor = Number(elem.attributes.getNamedItem('data-svgscale')?.value);
|
||||
const canvasImgTexture = PIXI.Texture.from(elemSrc, { resourceOptions: { scale: scalefactor } });
|
||||
const canvasImgTexture = PIXI.Texture.from(elemSrc, true as any);
|
||||
|
||||
// Use provided sprite object or create a new one
|
||||
if (spriteObject) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default function createCanvasText(
|
|||
// Use provided text object or create a new one
|
||||
const canvasText = textObject || new Text(elem.textContent as string, {
|
||||
fontFamily: elemFontFamily,
|
||||
fontSize: elemFontSize,
|
||||
fontSize: parseInt(elemFontSize),
|
||||
fontWeight: elemFontWeight as PIXI.TextStyleFontWeight,
|
||||
fontStyle: elemFontStyle as PIXI.TextStyleFontStyle,
|
||||
letterSpacing: elemLetterSpacing,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,4 @@ import * as PIXI from 'pixi.js';
|
|||
|
||||
// Set default resolution early to avoid deprecation warnings
|
||||
// from filter packages that use the deprecated settings.FILTER_RESOLUTION
|
||||
PIXI.Filter.defaultResolution = 2;
|
||||
|
||||
// Also set the deprecated setting for backward compatibility with older packages
|
||||
// This prevents warnings from packages like @pixi/filter-advanced-bloom that
|
||||
// still check the deprecated settings.FILTER_RESOLUTION during module initialization
|
||||
if (PIXI.settings && 'FILTER_RESOLUTION' in PIXI.settings) {
|
||||
(PIXI.settings as any).FILTER_RESOLUTION = 2;
|
||||
}
|
||||
(PIXI.Filter as any).defaultResolution = 2;
|
||||
|
|
|
|||
|
|
@ -1,40 +1,106 @@
|
|||
<script lang='ts'>
|
||||
<script lang="ts">
|
||||
import '$lib/styles/global.scss';
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import Loader from '$lib/components/Loader.svelte';
|
||||
import { navigating, page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface LayoutData {
|
||||
pathname: string;
|
||||
}
|
||||
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 lastPathname = $state(data.pathname);
|
||||
$effect(() => {
|
||||
if (data.pathname !== lastPathname) {
|
||||
lastPathname = data.pathname;
|
||||
}
|
||||
onMount(() => {
|
||||
console.log('layout mounted');
|
||||
// Set initial random background color on first load
|
||||
// setRandomBgColor();
|
||||
});
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
|
||||
let showLoader = $state(false);
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
let navToken = 0;
|
||||
|
||||
function clearTimer() {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setRandomBgColor() {
|
||||
const currentBgColor = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--color-bg');
|
||||
const currentHighlightColor = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--color-highlight');
|
||||
const currentTextColor = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--color-text');
|
||||
|
||||
let variantNumber: number;
|
||||
let variantBgColor: string;
|
||||
let variantHighlightColor: string;
|
||||
let variantTextColor: string;
|
||||
|
||||
// Keep picking a random variant until it's different from the current color
|
||||
do {
|
||||
variantNumber = Math.floor(Math.random() * 4) + 1;
|
||||
variantBgColor = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(`--color-bg-variant-${variantNumber}`);
|
||||
variantHighlightColor = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(`--color-highlight-variant-${variantNumber}`);
|
||||
variantTextColor = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(`--color-text-variant-${variantNumber}`);
|
||||
} while (variantBgColor.trim() === currentBgColor.trim());
|
||||
|
||||
document.documentElement.style.setProperty('--color-bg', variantBgColor);
|
||||
document.documentElement.style.setProperty('--color-highlight', variantHighlightColor);
|
||||
document.documentElement.style.setProperty('--color-text', variantTextColor);
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
beforeNavigate((nav) => {
|
||||
// token to avoid races between overlapping navigations
|
||||
const token = ++navToken;
|
||||
|
||||
clearTimer();
|
||||
showLoader = false;
|
||||
|
||||
// Change background color on route change
|
||||
setRandomBgColor();
|
||||
|
||||
// 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>
|
||||
|
||||
<svelte:head>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={`https://floter.design`+data.pathname} />
|
||||
<meta property="og:url" content={`https://floter.design${page.url.pathname}`} />
|
||||
<meta property="og:image" content="https://floter.design/ogimage.png">
|
||||
<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'} />
|
||||
<title>{$page.data.title ? $page.data.title : 'Simon Flöter, Designer and Creative Developer'}</title>
|
||||
<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>
|
||||
</svelte:head>
|
||||
|
||||
<Header />
|
||||
{#key data.pathname}
|
||||
<!-- {#key data.pathname} -->
|
||||
<div class="content">
|
||||
{#if $navigating}
|
||||
{#if showLoader}
|
||||
<Loader />
|
||||
{/if}
|
||||
{@render children()}
|
||||
</div>
|
||||
{/key}
|
||||
<!-- {/key} -->
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { loading } from '$lib/utils/stores.js'
|
||||
loading.set(true)
|
||||
export const load = async ({ url }) => {
|
||||
const { pathname } = url
|
||||
loading.set(true)
|
||||
// Simulate async operation if needed
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
loading.set(false)
|
||||
return {
|
||||
pathname
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
|
@ -93,9 +93,6 @@
|
|||
|
||||
</script>
|
||||
|
||||
{#if !mounted}
|
||||
<Loader />
|
||||
{/if}
|
||||
<article class="scroller">
|
||||
<section class="canvasized splash">
|
||||
<h1 class="align-middle">I create digital experiences that <em>stand out</em> from the rest.</h1>
|
||||
|
|
|
|||
43
src/routes/ethical-webdevelopment/+page.svelte
Normal file
43
src/routes/ethical-webdevelopment/+page.svelte
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<article>
|
||||
<div class="eyebrow">
|
||||
Research & 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>
|
||||
|
|
@ -13,21 +13,24 @@
|
|||
const textPool: PIXI.Text[] = [];
|
||||
|
||||
onMount(() => {
|
||||
let highLightColor = window.getComputedStyle(document.body).getPropertyValue('--color-highlight');
|
||||
let cleanup: (() => void) | undefined;
|
||||
|
||||
let is_fine = window.matchMedia('(pointer:fine)').matches
|
||||
let is_landscape = window.matchMedia('(orientation:landscape)').matches
|
||||
(async () => {
|
||||
let highLightColor = window.getComputedStyle(document.body).getPropertyValue('--color-highlight');
|
||||
let is_landscape = window.matchMedia('(orientation:landscape)').matches;
|
||||
let isDestroyed = false;
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
let app = new PIXI.Application({
|
||||
let app = new PIXI.Application();
|
||||
await app.init({
|
||||
canvas: canvas,
|
||||
resizeTo: window,
|
||||
antialias: true,
|
||||
autoDensity: true,
|
||||
resolution: 2,
|
||||
backgroundColor: 'rgb(255, 255, 255)',
|
||||
backgroundColor: 'rgb(29, 12, 18)',
|
||||
backgroundAlpha: 0,
|
||||
view: canvas,
|
||||
});
|
||||
|
||||
let textgroup = new PIXI.Container();
|
||||
|
|
@ -134,9 +137,11 @@
|
|||
ease: 'power2.inOut',
|
||||
overwrite: true,
|
||||
onComplete: () => {
|
||||
if (!isDestroyed) {
|
||||
introDone = true;
|
||||
createScrollTrigger();
|
||||
}
|
||||
}
|
||||
})
|
||||
tweens.push(textgroupIn);
|
||||
|
||||
|
|
@ -166,6 +171,10 @@
|
|||
let scrollTriggerTweens: Array<GSAPTween> = [];
|
||||
|
||||
function createScrollTrigger() {
|
||||
|
||||
// Check if component is destroyed before creating ScrollTriggers
|
||||
if (isDestroyed) return;
|
||||
|
||||
const tween1 = gsap.to([textRows[0], textRows[1]], {
|
||||
y: '+=' + -textRows[0][0].height * 0.9,
|
||||
duration: 1,
|
||||
|
|
@ -193,8 +202,10 @@
|
|||
scrollTriggerTweens.push(tween1, tween2);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cleanup = () => {
|
||||
isDestroyed = true; // Flag to prevent further actions
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
|
||||
// IMPORTANT: Kill ScrollTrigger and GSAP animations FIRST,
|
||||
// before destroying PixiJS objects they reference
|
||||
scrollTriggerTweens.forEach(tween => {
|
||||
|
|
@ -205,10 +216,23 @@
|
|||
});
|
||||
// Kill all other tweens
|
||||
tweens.forEach(tween => tween.kill());
|
||||
|
||||
// Kill orphanes ScrollTriggers
|
||||
ScrollTrigger.getAll().forEach((trigger) => {
|
||||
if (trigger.trigger === document.querySelector('article')) {
|
||||
trigger.kill();
|
||||
}
|
||||
});
|
||||
|
||||
// Return objects to pools
|
||||
allTexts.forEach(text => returnTextToPool(text));
|
||||
// Now safe to destroy PixiJS app
|
||||
app.destroy(true, { children: true, texture: true, baseTexture: true });
|
||||
app.destroy(true, { children: true, texture: true });
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
if (cleanup) cleanup();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
backgroundAlpha: 0,
|
||||
resizeTo: window
|
||||
});
|
||||
PIXI.Filter.defaultResolution = window.devicePixelRatio;
|
||||
(PIXI.Filter as any).defaultResolution = window.devicePixelRatio;
|
||||
|
||||
const container = new PIXI.Container();
|
||||
app.stage.addChild(container);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
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) => {
|
||||
|
|
@ -16,6 +17,9 @@
|
|||
|
||||
let isZoomed = false;
|
||||
|
||||
const currentBgColor = getComputedStyle(document.documentElement).getPropertyValue('--color-bg');
|
||||
const currentHighlightColor = getComputedStyle(document.documentElement).getPropertyValue('--color-highlight');
|
||||
|
||||
let works: Array<HTMLElement> = Array.from(document.querySelectorAll('.work'));
|
||||
for ( let i = 0; i < 40; i++ ){
|
||||
let clone = works[i].cloneNode(true) as HTMLElement;
|
||||
|
|
@ -50,7 +54,8 @@
|
|||
workclone.style.top = `0`;
|
||||
workclone.style.left = `0`;
|
||||
document.body.appendChild(workclone);
|
||||
window.location.href = href || ''
|
||||
// window.location.href = href || ''
|
||||
goto(href || '');
|
||||
}
|
||||
})
|
||||
gsap.fromTo(split.chars, {
|
||||
|
|
@ -94,12 +99,12 @@
|
|||
transformOrigin: '0 0',
|
||||
})
|
||||
gsap.set('.work',{
|
||||
opacity: 0,
|
||||
autoAlpha: 0,
|
||||
yPercent: 50,
|
||||
scaleY: 0,
|
||||
})
|
||||
gsap.to('.work',{
|
||||
opacity:1,
|
||||
autoAlpha:1,
|
||||
yPercent: 0,
|
||||
scaleY: 1,
|
||||
duration: .75,
|
||||
|
|
@ -129,37 +134,51 @@
|
|||
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);
|
||||
|
||||
// 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;
|
||||
|
|
@ -186,9 +205,9 @@
|
|||
window.removeEventListener('mousemove', mouseMoveHandler);
|
||||
window.removeEventListener('touchmove', touchMoveHandler);
|
||||
gsap.killTweensOf('.works, .work');
|
||||
works.forEach((work, index) => {
|
||||
work.removeEventListener('mouseenter', (e) => {});
|
||||
work.removeEventListener('mouseleave', (e) => {});
|
||||
works.forEach((work) => {
|
||||
work.removeEventListener('mouseenter', () => handleMouseEnter(work));
|
||||
work.removeEventListener('mouseleave', () => handleMouseLeave(work));
|
||||
work.removeEventListener('click', (e) => {});
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,21 @@
|
|||
export async function load( { params }: { params: { slug: string }} ){
|
||||
try {
|
||||
const post = await import(`../md/${params.slug}.md`)
|
||||
const { title = '', date = '', header_bg_image = '', svg = '', video = '', tags = [], reference = '', referenceName = '', tasks = [], description = [], images = [], agency = '', agencyName = '' } = post.metadata
|
||||
const {
|
||||
title = '',
|
||||
date = '',
|
||||
header_bg_image = '',
|
||||
svg = '',
|
||||
video = '',
|
||||
tags = [],
|
||||
reference = '',
|
||||
referenceName = '',
|
||||
tasks = [],
|
||||
description = [],
|
||||
images = [],
|
||||
agency = '',
|
||||
agencyName = ''
|
||||
} = post.metadata
|
||||
|
||||
// Don't pass the component - it's not serializable
|
||||
// Import it directly in the page component instead
|
||||
|
|
|
|||
|
|
@ -5,23 +5,9 @@
|
|||
import { CldImage } from 'svelte-cloudinary';
|
||||
let { data } = $props();
|
||||
|
||||
let visible = $state(false);
|
||||
let Content = $state<any>(null);
|
||||
|
||||
// Import the markdown component dynamically and set up animations
|
||||
onMount(async () => {
|
||||
// Load the markdown component
|
||||
try {
|
||||
const post = await import(`../md/${data.slug}.md`);
|
||||
Content = post.default;
|
||||
} catch (error) {
|
||||
console.error('Error loading markdown component:', error);
|
||||
}
|
||||
|
||||
gsap.registerPlugin(ScrollToPlugin);
|
||||
|
||||
visible = true;
|
||||
|
||||
onMount(() => {
|
||||
let is_landscape = window.matchMedia('(orientation:landscape)').matches
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
|
|
@ -54,17 +40,18 @@
|
|||
|
||||
gsap.to('.logo-wrapper', {
|
||||
opacity: 0,
|
||||
scale: 0.85,
|
||||
zIndex: -1,
|
||||
duration: 1,
|
||||
duration: .5,
|
||||
ease: 'power2.out',
|
||||
})
|
||||
gsap.from('.work', {
|
||||
y: '100vh',
|
||||
gsap.to('.work', {
|
||||
y: '-100vh',
|
||||
duration: 1,
|
||||
ease: 'power4.out',
|
||||
})
|
||||
gsap.from('.gallery-wrapper', {
|
||||
y: '-100vh',
|
||||
gsap.to('.gallery-wrapper', {
|
||||
y: '100vh',
|
||||
duration: 1,
|
||||
ease: 'power4.out',
|
||||
})
|
||||
|
|
@ -137,8 +124,7 @@
|
|||
</div>
|
||||
<div class="gallery-wrapper">
|
||||
<div class="gallery">
|
||||
{#if visible}
|
||||
{#each data.images as image}
|
||||
{#each data.images as image (image)}
|
||||
<figure>
|
||||
{#if image.includes('/video/')}
|
||||
<video src={image} width="1400" height="840" autoplay muted loop></video>
|
||||
|
|
@ -156,23 +142,21 @@
|
|||
{/if}
|
||||
</figure>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<article class="work">
|
||||
{#if visible}
|
||||
<h1>{data.title}</h1>
|
||||
<div class="description">
|
||||
{data.description}
|
||||
</div>
|
||||
<h1>{data.title}</h1>
|
||||
<div class="work-content">
|
||||
<div class="infobox">
|
||||
<div class="tasks">
|
||||
<div class="tasks-title">What I did:</div>
|
||||
<ul>
|
||||
{#each data.tags as tag }
|
||||
{#each data.tags as tag (tag)}
|
||||
<li>{tag}</li>
|
||||
{ /each }
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="reference">
|
||||
|
|
@ -187,12 +171,13 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="work-content-text">
|
||||
{#if Content}
|
||||
<svelte:component this={Content} />
|
||||
{/if}
|
||||
{#await import(`../md/${data.slug}.md`) then { default: Content }}
|
||||
<Content />
|
||||
{:catch error}
|
||||
<p>Error loading content: {error.message}</p>
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
@ -234,6 +219,8 @@
|
|||
height: 100%;
|
||||
}
|
||||
.work {
|
||||
position: relative;
|
||||
top: 100vh;
|
||||
overflow: hidden;
|
||||
padding: 0 var(--spacing-outer) 100px var(--spacing-outer);
|
||||
@media screen and (min-width: 768px) {
|
||||
|
|
@ -243,19 +230,20 @@
|
|||
}
|
||||
}
|
||||
h1 {
|
||||
margin: .5em 0 0.25em 0;
|
||||
margin: .125em 0 0.5em 0;
|
||||
font-size: 2.5rem;
|
||||
@media screen and (min-width: 768px) {
|
||||
font-size: 5rem;
|
||||
font-size: 6rem;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.0075em;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.25rem;
|
||||
@media screen and (min-width: 768px) {
|
||||
font-size: 2.5rem;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
.infobox {
|
||||
|
|
@ -320,17 +308,17 @@
|
|||
}
|
||||
.gallery-wrapper {
|
||||
position: relative;
|
||||
top: -100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
padding: 0 0 1em 0;
|
||||
z-index: 1;
|
||||
// opacity: 0.75;
|
||||
perspective: 250px;
|
||||
perspective-origin: center bottom;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-bottom: -80px;
|
||||
top: -80px;
|
||||
top: calc(-100vh - 80px);
|
||||
padding: 2em 0 4em 0;
|
||||
margin-bottom: -120px;
|
||||
perspective: 500px;
|
||||
|
|
|
|||
|
|
@ -1,279 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { CldImage } from 'svelte-cloudinary';
|
||||
export let data;
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
let visible = false;
|
||||
|
||||
function animForDesktop() {
|
||||
|
||||
ScrollTrigger.getAll().forEach(t => t.kill());
|
||||
|
||||
gsap.to('.heromask, .coverclone', { duration: .6, x: "-10%", ease: "cubic.inOut" })
|
||||
|
||||
gsap.to('.work', {
|
||||
xPercent: -100,
|
||||
duration: .6,
|
||||
ease: "expo.out",
|
||||
delay: .2
|
||||
})
|
||||
|
||||
let heroheight = document.querySelector('.heromask')?.getBoundingClientRect().height || 100;
|
||||
|
||||
gsap.to('.heromask', {
|
||||
clipPath: "polygon(0 0, 60% 0, 35% 100%, 0% 100%)",
|
||||
duration: 1,
|
||||
ease: "power4.out",
|
||||
onStart: () => {
|
||||
setTimeout(() => {
|
||||
document.querySelector('.coverclone')?.remove();
|
||||
}, 100);
|
||||
},
|
||||
onComplete: () => {
|
||||
gsap.to('.heromask', {
|
||||
ease: "none",
|
||||
clipPath: "polygon(0 0, 50% 0, 50% 100%, 0% 100%)",
|
||||
scrollTrigger: {
|
||||
trigger: '.work',
|
||||
start: 'top top',
|
||||
end: `200px top`,
|
||||
scrub: true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
function animForMobile() {
|
||||
ScrollTrigger.getAll().forEach(t => t.kill());
|
||||
gsap.to('.heromask, .coverclone', { duration: .6, y: -20, ease: "cubic.inOut" })
|
||||
|
||||
gsap.to('.work', {
|
||||
opacity: 1,
|
||||
yPercent: -100,
|
||||
duration: .4,
|
||||
ease: "expo.out",
|
||||
delay: .2,
|
||||
})
|
||||
|
||||
gsap.to('.heromask', {
|
||||
clipPath: "polygon(0 0, 100% 0, 100% 75%, 0% 100%)",
|
||||
duration: .6,
|
||||
ease: "cubic.inOut",
|
||||
onStart: () => {
|
||||
setTimeout(() => {
|
||||
document.querySelector('.coverclone')?.remove();
|
||||
}, 100);
|
||||
},
|
||||
onComplete: () => {
|
||||
gsap.to('.heromask', {
|
||||
ease: "power1.out",
|
||||
scrollTrigger: {
|
||||
trigger: '.work',
|
||||
markers: false,
|
||||
start: '0px -10px',
|
||||
end: `0px -20px`,
|
||||
scrub: false,
|
||||
onEnterBack: () => {
|
||||
gsap.to('.heromask', {
|
||||
clipPath: "polygon(0 0, 100% 0, 100% 75%, 0% 100%)",
|
||||
duration: .6,
|
||||
ease: "expo.out",
|
||||
})
|
||||
},
|
||||
onEnter: () => {
|
||||
gsap.to('.heromask', {
|
||||
clipPath: "polygon(0 0, 100% 0, 100% 0%, 0% 0%)",
|
||||
duration: .6,
|
||||
ease: "expo.out",
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
function animForSize(){
|
||||
if ( window.matchMedia("(min-width: 768px) and (orientation: landscape)").matches ) {
|
||||
animForDesktop();
|
||||
} else {
|
||||
animForMobile();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
||||
visible = true;
|
||||
|
||||
document.querySelector('.heromask img')?.addEventListener('load', () => {
|
||||
animForSize();
|
||||
|
||||
let portrait = window.matchMedia("(orientation: portrait)");
|
||||
|
||||
portrait.addEventListener("change", function(e) {
|
||||
animForSize();
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="heromask">
|
||||
<CldImage
|
||||
src={data.header_bg_image}
|
||||
alt="{data.title}"
|
||||
sizes="100vw"
|
||||
width={2100}
|
||||
height={1400}
|
||||
placeholder="blur"
|
||||
loading="eager"
|
||||
objectFit="fill"
|
||||
/>
|
||||
</div>
|
||||
<div class="subnav">
|
||||
<a href="/work" class="subnav-item">← Back</a>
|
||||
</div>
|
||||
<article class="work">
|
||||
{#if visible}
|
||||
<div class="work-content">
|
||||
{#if data.tags != undefined && data.tags.length > 0 }
|
||||
<div class="tags">
|
||||
{#each data.tags as tag }
|
||||
<div class="tag">{tag}</div>
|
||||
{ /each }
|
||||
</div>
|
||||
{/if}
|
||||
<h1><span class="svg-logo">{@html data.svg}</span><span class="name">{data.title}</span></h1>
|
||||
<div class="work-content-text">
|
||||
{@html data.Content.html}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<style lang="scss">
|
||||
.work {
|
||||
width: 100vw;
|
||||
min-height: 100svh;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transform: translateY(100%);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
.subnav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 4;
|
||||
padding: var(--spacing-outer);
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
}
|
||||
.heromask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left:0;
|
||||
aspect-ratio: var(--aspect-ratio-heroes);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
z-index: 2;
|
||||
clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
|
||||
}
|
||||
:global(.heromask img) {
|
||||
z-index: 0;
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: var(--aspect-ratio-heroes);
|
||||
margin: 0;
|
||||
object-fit: fill;
|
||||
}
|
||||
.work-content {
|
||||
padding: 0 var(--spacing-outer);
|
||||
padding-top: calc(66.6vw + 1em);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--color-text);
|
||||
|
||||
& > :last-child {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
@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);
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
padding: 0 0 1em 0;
|
||||
}
|
||||
& .name {
|
||||
display: none;
|
||||
}
|
||||
& .svg-logo :global(svg) {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 250px;
|
||||
max-height: 80px;
|
||||
margin-bottom: 1em;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: 400px;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tags {
|
||||
padding-bottom: .25em;
|
||||
font-size: 1em;
|
||||
margin-bottom: 2em;
|
||||
line-height: 1.1;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: calc(100% + var(--spacing-outer) * 2.5);
|
||||
height: 1px;
|
||||
background-color: var(--color-text);
|
||||
margin-top: .5em;
|
||||
}
|
||||
}
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin-right: .5em;
|
||||
padding: .125em 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -.005em;
|
||||
|
||||
&:after {
|
||||
content: ','
|
||||
}
|
||||
&:last-child:after {
|
||||
content: none
|
||||
}
|
||||
}
|
||||
|
||||
:global(.header-nav){
|
||||
transition: all .3s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
:global(.work .header-nav){
|
||||
transform: translateY(100%);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
<script lang="ts">
|
||||
import WorkCanvas from '$lib/components/WorkCanvas.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { workClickHandler, initWorkPage } from './workUtils.js';
|
||||
import { workbulge } from '$lib/utils/stores.js';
|
||||
import { CldImage } from 'svelte-cloudinary';
|
||||
export let data;
|
||||
|
||||
const orderedPosts = data.posts.sort((a, b) => {
|
||||
return a.meta.order - b.meta.order;
|
||||
});
|
||||
|
||||
let canvasTextElems: Array<HTMLElement>;
|
||||
let canvasImgElems: Array<HTMLElement>;
|
||||
let bulge = {factor: 0};
|
||||
|
||||
workbulge.subscribe((val) => {
|
||||
bulge.factor = val;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const headline: HTMLElement = document.querySelector('.headline') as HTMLElement;
|
||||
const headlines: Array<HTMLElement> = Array.from(document.querySelectorAll('h2, li'));
|
||||
const images: Array<HTMLElement> = Array.from(document.querySelectorAll('.work img'));
|
||||
let canvasElems = initWorkPage( headlines, images );
|
||||
canvasTextElems = canvasElems.text as Array<HTMLElement>;
|
||||
canvasImgElems = canvasElems.images;
|
||||
});
|
||||
</script>
|
||||
<div class="works-wrapper">
|
||||
|
||||
<h1 class="headline"><span>Outstanding work</span></h1>
|
||||
<div class="works">
|
||||
|
||||
{#each orderedPosts as work, i}
|
||||
<a
|
||||
data-sveltekit-preload-data
|
||||
href="{work.path}"
|
||||
class="work"
|
||||
on:click={ (e) => workClickHandler(e) }
|
||||
>
|
||||
<CldImage
|
||||
src={work.meta.header_bg_image}
|
||||
sizes={ i === 0 ? `(min-width: 768px) 60vw, 50vw` : `(min-width: 768px) 20vw, 50vw`}
|
||||
alt={work.meta.title}
|
||||
width="2100"
|
||||
height="1400"
|
||||
objectFit="fill"
|
||||
loading= "lazy"
|
||||
/>
|
||||
<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>
|
||||
<WorkCanvas
|
||||
textsToCanvas={canvasTextElems}
|
||||
imgsToCanvas={canvasImgElems}
|
||||
bulgeFactor={bulge.factor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style src="./work.scss" lang="scss"></style>
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
.works-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
h1 {
|
||||
font-size: 16vw;
|
||||
margin: var(--spacing-nav) var(--spacing-nav) .25em var(--spacing-nav);
|
||||
padding: 0 0 calc(var(--spacing-nav) / 3) 0;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
border-bottom: 1px solid;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
font-size: 7vw;
|
||||
}
|
||||
}
|
||||
.works {
|
||||
padding: 0.5em;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1px;
|
||||
padding: 0 var(--spacing-nav);
|
||||
margin: 0 auto;
|
||||
padding-bottom: 20svh;
|
||||
}
|
||||
}
|
||||
.work-info,
|
||||
.work:visited .work-info {
|
||||
position: absolute;
|
||||
left: 0.25em;
|
||||
top: 0.25em;
|
||||
padding: 0.5em .75em;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--color-text);
|
||||
visibility: hidden;
|
||||
|
||||
& h2 {
|
||||
text-transform: none;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1.2em;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
visibility: hidden;
|
||||
color: var(--color-bg);
|
||||
}
|
||||
& .tags {
|
||||
visibility: hidden;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 10px 0 0 0 ;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.15em;
|
||||
transition: all .4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
& .tag {
|
||||
font-size: .6em;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.01em;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
// font-weight: 800;
|
||||
// font-style: italic;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
.work:hover .work-info {
|
||||
background-color: var(--color-bg);
|
||||
& h2 {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
.work {
|
||||
flex: 0 0 calc(50% - 0.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;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-column-end: span 1;
|
||||
|
||||
&:first-child {
|
||||
grid-column-end: span 2;
|
||||
grid-row-end: span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
.work .tag {
|
||||
opacity: 0;
|
||||
}
|
||||
:global(.work img) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: var(--aspect-ratio-heroes);
|
||||
object-fit: fill;
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
overflow: hidden;
|
||||
perspective: 700px;
|
||||
transform-origin: 0 0;
|
||||
will-change: transform;
|
||||
}
|
||||
.headline {
|
||||
position: fixed;
|
||||
|
|
@ -48,9 +49,8 @@
|
|||
display: grid;
|
||||
grid-template-columns: repeat(6, 100vw);
|
||||
grid-auto-rows: 100vh;
|
||||
// grid-template-rows: repeat(6, 100vh);
|
||||
// grid-auto-flow: column;
|
||||
gap: 6px;
|
||||
will-change: transform;
|
||||
}
|
||||
.work {
|
||||
background-color: var(--color-highlight);
|
||||
|
|
@ -59,9 +59,11 @@
|
|||
width: 100vw;
|
||||
height: 100vh;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateZ(700px);
|
||||
will-change: transform;
|
||||
}
|
||||
.work-logo {
|
||||
:global(.work-logo) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--color-bg);
|
||||
|
|
@ -70,6 +72,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
will-change: transform;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: 60%;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue