Added some animations and tweaked some styles

This commit is contained in:
2026-06-12 20:22:56 -05:00
parent d8d93f4517
commit 3cb4b9b71f
9 changed files with 404 additions and 202 deletions
+34 -3
View File
@@ -25,8 +25,8 @@
.dark-mode { .dark-mode {
--text: #fbfbf7; --text: #fbfbf7;
--bg: #111111; --bg: #0c0c0c;
--muted: #d1d1d1; --muted: #a3a3a3;
--color-white-500: #fbfbf7; --color-white-500: #fbfbf7;
} }
@@ -73,6 +73,34 @@ body {
line-height: 1.6; line-height: 1.6;
} }
/* full-page film grain */
body::after {
content: '';
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
opacity: 0.06;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
mix-blend-mode: multiply;
}
.dark-mode body::after {
mix-blend-mode: screen;
opacity: 0.045;
}
/* quick page transition */
.page-enter-active,
.page-leave-active {
transition: opacity 0.18s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
}
.basic-link { .basic-link {
color: var(--text); color: var(--text);
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
@@ -92,7 +120,10 @@ body {
transition: all 200ms ease; transition: all 200ms ease;
padding: 0 4px; padding: 0 4px;
margin: 0 -4px; margin: 0 -4px;
background-image: linear-gradient(var(--color-white-500), var(--color-white-500)); background-image: linear-gradient(
var(--color-white-500),
var(--color-white-500)
);
background-size: 100% 1px; background-size: 100% 1px;
background-position: bottom; background-position: bottom;
background-repeat: no-repeat; background-repeat: no-repeat;
+106 -30
View File
@@ -1,3 +1,9 @@
<script lang="ts">
// module-scoped so it survives client-side navigation —
// the intro should only ever play once per visit
let introPlayed = false;
</script>
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{
title?: string; title?: string;
@@ -9,6 +15,60 @@ const theme = useColorMode();
const toggleTheme = () => { const toggleTheme = () => {
theme.preference = theme.preference === 'light' ? 'dark' : 'light'; theme.preference = theme.preference === 'light' ? 'dark' : 'light';
}; };
const { $gsap } = useNuxtApp();
const pageEl = ref<HTMLElement | null>(null);
onMounted(() => {
if (!$gsap || !pageEl.value || introPlayed) return;
introPlayed = true;
const reduced = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches;
const ctx = $gsap.context(() => {
if (reduced) return;
const tl = $gsap.timeline({
defaults: { ease: 'power3.out' },
});
// trace lines draw down the page
tl.from('.trace', {
scaleY: 0,
transformOrigin: 'top center',
duration: 1.4,
ease: 'power3.inOut',
})
// header panel fades in as the lines pass it
.from(
'.header-lower',
{ autoAlpha: 0, duration: 0.9, ease: 'power2.out' },
0.3,
)
// masked title reveal
.from('.name', { yPercent: 110, duration: 0.9 }, 0.55)
.from('.title', { yPercent: 120, autoAlpha: 0, duration: 0.8 }, 0.7)
// footer appears as the lines reach the bottom
.from(
['.footer-upper', '.footer-lower'],
{ autoAlpha: 0, duration: 0.7, ease: 'power2.out' },
0.9,
)
// nav chrome fades in last
.from('.header-upper', { autoAlpha: 0, duration: 0.7 }, 0.9);
// body content waits for the intro
tl.from(
'main',
{ autoAlpha: 0, y: 16, duration: 0.8, ease: 'power2.out' },
0.7,
);
}, pageEl.value);
onBeforeUnmount(() => ctx.revert());
});
</script> </script>
<template> <template>
@@ -24,7 +84,7 @@ const toggleTheme = () => {
<feBlend mode="multiply" in="SourceGraphic"></feBlend> <feBlend mode="multiply" in="SourceGraphic"></feBlend>
</filter> </filter>
</svg> </svg>
<div class="page-wrapper"> <div class="page-wrapper" ref="pageEl">
<div class="trace left"></div> <div class="trace left"></div>
<div class="trace right"></div> <div class="trace right"></div>
@@ -41,24 +101,36 @@ const toggleTheme = () => {
{{ theme.value === 'dark' ? '🌚' : '🌝' }} {{ theme.value === 'dark' ? '🌚' : '🌝' }}
</button> </button>
<div class="flex-ff"></div> <div class="flex-ff"></div>
<a
class="basic-link"
href="https://github.com/jaronpate"
>
GitHub
</a>
<!-- <NuxtLink to="/resume" class="basic-link" <!-- <NuxtLink to="/resume" class="basic-link"
>Resume</NuxtLink >Resume</NuxtLink
> --> > -->
<!-- <NuxtLink to="/writing" class="basic-link"> <NuxtLink to="/writing" class="basic-link">
Writing Writing
</NuxtLink> --> </NuxtLink>
</div> </div>
</slot> </slot>
</div> </div>
<div class="header-lower"> <div class="header-lower">
<slot name="header-lower"> <slot name="header-lower">
<h1 class="name">{{ title || "Hello, I'm Jaron" }}</h1> <div class="mask">
<p class="title"> <h1 class="name">
{{ {{ title || "Hello, I'm Jaron" }}
description || </h1>
'Software Engineer by trade — builder by nature.' </div>
}} <div class="mask">
</p> <p class="title">
{{
description ||
'Software Engineer by trade — builder by nature.'
}}
</p>
</div>
</slot> </slot>
</div> </div>
</header> </header>
@@ -69,33 +141,27 @@ const toggleTheme = () => {
</main> </main>
<footer class="footer"> <footer class="footer">
<div class="footer-upper"> <div class="footer-upper flex-center">
<div class="flex-center"> <p>
<p> <a class="punch-link" href="mailto:yo@jp.wtf"
<a class="punch-link" href="mailto:yo@jp.wtf" >Say hi.</a
>Say hi.</a >
> </p>
</p> <div class="flex-ff"></div>
<div class="flex-ff"></div> <p style="text-align: right">Let's make something cool.</p>
<p style="text-align: right">
Let's make something cool.
</p>
</div>
</div> </div>
<div class="footer-lower"> <div class="footer-lower flex-center" style="gap: var(--gap)">
<div class="flex-center" style="gap: var(--gap)"> <p style="text-align: right">Est. 2003</p>
<p style="text-align: right">Est. 2003</p> <!-- <p style="font-family: monospace">🐟🎩.dev</p> -->
<!-- <p style="font-family: monospace">🐟🎩.dev</p> --> <div class="flex-ff"></div>
<div class="flex-ff"></div> <p style="text-align: right">Austin, TX</p>
<p style="text-align: right">Austin, TX</p>
</div>
</div> </div>
</footer> </footer>
</div> </div>
</div> </div>
</template> </template>
<style> <style scoped>
.page-wrapper { .page-wrapper {
--main-width: 50vw; --main-width: 50vw;
width: var(--main-width); width: var(--main-width);
@@ -133,6 +199,10 @@ svg[height='0'] {
position: fixed; position: fixed;
} }
.mask {
overflow: hidden;
}
.name { .name {
font-size: 3rem; font-size: 3rem;
font-weight: 400; font-weight: 400;
@@ -144,6 +214,7 @@ svg[height='0'] {
font-weight: 300; font-weight: 300;
margin: 0; margin: 0;
font-style: italic; font-style: italic;
color: var(--color-white-500);
} }
.header { .header {
@@ -226,12 +297,17 @@ svg[height='0'] {
font-weight: bold; font-weight: bold;
color: var(--text); color: var(--text);
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
padding: 0;
} }
.theme-toggle:hover { .theme-toggle:hover {
opacity: 0.5; opacity: 0.5;
} }
.footer p {
margin: 0;
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.page-wrapper { .page-wrapper {
--main-width: 80vw; --main-width: 80vw;
+192 -128
View File
@@ -1,3 +1,8 @@
<script lang="ts">
// module-scoped so the reveal only plays once per visit
let sectionsRevealed = false;
</script>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
@@ -26,148 +31,188 @@ useHead({
}); });
definePageMeta({ definePageMeta({
layout: 'main', layout: false,
});
const { $gsap } = useNuxtApp();
const pageRoot = ref<HTMLElement | null>(null);
onMounted(() => {
if (!$gsap || !pageRoot.value || sectionsRevealed) return;
sectionsRevealed = true;
const reduced = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches;
const ctx = $gsap.context(() => {
if (reduced) return;
// sections in view on load are revealed by the layout's intro;
// below-the-fold sections rise in as they enter the viewport
$gsap.utils.toArray<HTMLElement>('main section').forEach((el) => {
const inView = el.getBoundingClientRect().top < window.innerHeight;
if (inView) return;
$gsap.from(el, {
autoAlpha: 0,
y: 24,
duration: 0.6,
ease: 'power2.out',
scrollTrigger: {
trigger: el,
start: 'top 92%',
},
});
});
}, pageRoot.value);
onBeforeUnmount(() => ctx.revert());
}); });
</script> </script>
<template> <template>
<NuxtLayout name="main"> <div ref="pageRoot">
<section class="projects"> <NuxtLayout name="main">
<h5>Selected Work</h5> <section class="projects">
<div class="section__content"> <h5>Selected Work</h5>
<a <div class="section__content">
href="https://unicord.hat.fish/" <a
target="_blank" href="https://unicord.hat.fish/"
class="project" target="_blank"
> class="project"
<h3 class="project__title">Unicord</h3> >
<p class="project__description"> <h3 class="project__title">Unicord</h3>
Discord bot SDK inspired by webserver architecture. <p class="project__description">
Discord bot SDK inspired by webserver architecture.
</p>
</a>
<a
href="https://mystwright.com/"
target="_blank"
class="project"
>
<h3 class="project__title">Mystwright</h3>
<p class="project__description">
An AI-driven mystery detective game. Generate,
investigate, solve.
</p>
</a>
<a
href="https://github.com/jaronpate/rift-buddy"
target="_blank"
class="project"
>
<h3 class="project__title">Rune Buddy</h3>
<p class="project__description">
League of Legends runepage manager. Save & organise
your pages for free.
</p>
</a>
</div>
</section>
<section class="about">
<h5>About</h5>
<div class="section__content">
<p>
I enjoy the entire process of building things; backend,
frontend, product, etc.
</p> </p>
</a> <p>
<a I left school and my job at Chick-fil-A in 2022 to write
href="https://mystwright.com/" code full time. It's definitely one of the best
target="_blank" decisions I've ever made. I've learned so much since
class="project" then and
> <em>still</em> have so much more to learn.
<h3 class="project__title">Mystwright</h3>
<p class="project__description">
An AI-driven mystery detective game. Generate,
investigate, solve.
</p> </p>
</a> <p>
<a If you want to build something cool or connect hit me
href="https://github.com/jaronpate/rift-buddy" up! I still dont know enough people to form a band yet
target="_blank" ;)
class="project"
>
<h3 class="project__title">Rune Buddy</h3>
<p class="project__description">
League of Legends runepage manager. Save & organise your
pages for free.
</p> </p>
</a> </div>
</div> </section>
</section> <section class="photos">
<section class="about"> <h5>Photos</h5>
<h5>About</h5> <div class="section__content">
<div class="section__content"> <div class="photo-grid">
<p> <div class="photo-grid__item">
I enjoy the entire process of building things; backend, <div class="photo__image">
frontend, product, etc. <img
</p> src="/photography/athens_red_scooter.jpg"
<p> alt="Red scooter in Athens"
I left school and my job at Chick-fil-A in 2022 to write />
code full time. It's definitely one of the best decisions </div>
I've ever made. I've learned so much since then and <div class="photo__overlay">
<em>still</em> have so much more to learn. <p>Athens</p>
</p> <p>2026</p>
<p> </div>
If you want to build something cool or connect hit me up! I
still dont know enough people to form a band yet ;)
</p>
</div>
</section>
<!-- <section class="photos">
<h5>Photos</h5>
<div class="section__content">
<div class="photo-grid">
<div class="photo-grid__item">
<div class="photo__image">
<img
src="/photography/athens_red_scooter.jpg"
alt="Red scooter in Athens"
/>
</div> </div>
<div class="photo__overlay"> <div class="photo-grid__item">
<p>Athens</p> <div class="photo__image">
<p>2026</p> <img
src="/photography/athens_subway.jpg"
alt="Athens metro station"
/>
</div>
<div class="photo__overlay">
<p>Athens</p>
<p>2026</p>
</div>
</div> </div>
</div> <div class="photo-grid__item">
<div class="photo-grid__item"> <div class="photo__image">
<div class="photo__image"> <img
<img src="/photography/athens_sunset.jpg"
src="/photography/athens_subway.jpg" alt="Athens at sunset"
alt="Athens metro station" />
/> </div>
<div class="photo__overlay">
<p>Athens</p>
<p>2026</p>
</div>
</div> </div>
<div class="photo__overlay"> <div class="photo-grid__item">
<p>Athens</p> <div class="photo__image">
<p>2026</p> <img
src="/photography/london_porsche.jpg"
alt="Porsche shop in London"
/>
</div>
<div class="photo__overlay">
<p>London</p>
<p>2025</p>
</div>
</div> </div>
</div> <div class="photo-grid__item">
<div class="photo-grid__item"> <div class="photo__image">
<div class="photo__image"> <img
<img src="/photography/underground.jpg"
src="/photography/athens_sunset.jpg" alt="Entrace to the London underground"
alt="Athens at sunset" />
/> </div>
<div class="photo__overlay">
<p>London</p>
<p>2026</p>
</div>
</div> </div>
<div class="photo__overlay"> <div class="photo-grid__item">
<p>Athens</p> <div class="photo__image">
<p>2026</p> <img
</div> src="/photography/austin_parking.jpg"
</div> alt="Parking garage in Austin"
<div class="photo-grid__item"> />
<div class="photo__image"> </div>
<img <div class="photo__overlay">
src="/photography/london_porsche.jpg" <p>Austin</p>
alt="Porsche shop in London" <p>2024</p>
/> </div>
</div>
<div class="photo__overlay">
<p>London</p>
<p>2025</p>
</div>
</div>
<div class="photo-grid__item">
<div class="photo__image">
<img
src="/photography/underground.jpg"
alt="Entrace to the London underground"
/>
</div>
<div class="photo__overlay">
<p>London</p>
<p>2026</p>
</div>
</div>
<div class="photo-grid__item">
<div class="photo__image">
<img
src="/photography/austin_parking.jpg"
alt="Parking garage in Austin"
/>
</div>
<div class="photo__overlay">
<p>Austin</p>
<p>2024</p>
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</section> --> </NuxtLayout>
</NuxtLayout> </div>
</template> </template>
<style scoped> <style scoped>
@@ -244,6 +289,25 @@ section:last-of-type {
font-weight: 400; font-weight: 400;
} }
.project__title::after {
content: '↗';
position: relative;
display: inline-block;
margin-left: 0.4em;
font-size: 0.75em;
opacity: 0;
vertical-align: center;
transform: translate(-6px, 6px);
transition:
opacity 0.25s ease,
transform 0.25s ease;
}
.project:hover .project__title::after {
opacity: 1;
transform: translate(0, 0);
}
.project__description { .project__description {
margin: 0; margin: 0;
font-size: small; font-size: small;
@@ -287,7 +351,7 @@ section:last-of-type {
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
color: var(--bg); color: var(--color-white-500);
z-index: 2; z-index: 2;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
display: flex; display: flex;
+5 -3
View File
@@ -16,12 +16,13 @@ useSeoMeta({
}); });
definePageMeta({ definePageMeta({
layout: 'main', layout: false,
}); });
</script> </script>
<template> <template>
<NuxtLayout <div>
<NuxtLayout
name="main" name="main"
:title="page?.title" :title="page?.title"
:description="page?.description" :description="page?.description"
@@ -39,7 +40,8 @@ definePageMeta({
</div> </div>
</template> </template>
<ContentRenderer class="writing__content" v-if="page" :value="page" /> <ContentRenderer class="writing__content" v-if="page" :value="page" />
</NuxtLayout> </NuxtLayout>
</div>
</template> </template>
<style scoped> <style scoped>
+49 -38
View File
@@ -1,55 +1,66 @@
<script setup lang="ts"> <script setup lang="ts">
const { data: pages } = await useAsyncData('writing-pages', () => // const { data: pages } = await useAsyncData('writing-pages', () =>
queryCollection('writing').all(), // queryCollection('writing').all(),
); // );
const pages: any[] = [];
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
// ex. May 5, 2026 // ex. May 5, 2026
return `${date.toLocaleDateString('en-US', { month: 'short' })} ${date.getDate()}, ${date.getFullYear()}`; return `${date.toLocaleDateString('en-US', { month: 'short' })} ${date.getDate()}, ${date.getFullYear()}`;
}; };
definePageMeta({
layout: false,
});
</script> </script>
<template> <template>
<NuxtLayout <div>
name="main" <NuxtLayout
title="Writing" name="main"
description="Thoughts I've had here and there. Maybe you'll find something helpful or interesting here." title="Writing"
> description="Thoughts I've had here and there. Maybe you'll find something helpful or interesting here."
<template #header-upper> >
<div class="header-upper-inner"> <template #header-upper>
<NuxtLink to="/" class="back-link" aria-label="Back to Home"> <div class="header-upper-inner">
<span class="arrow">←</span>
<span>Home</span>
</NuxtLink>
</div>
</template>
<section>
<div class="section__content">
<template v-if="pages?.length">
<NuxtLink <NuxtLink
v-for="page in pages" to="/"
:to="'/writing/' + page.path" class="back-link"
:key="page.path" aria-label="Back to Home"
class="article"
> >
<h3 class="article__title">{{ page.title }}</h3> <span class="arrow">←</span>
<!-- <p class="article__date"> <span>Home</span>
</NuxtLink>
</div>
</template>
<section>
<div class="section__content">
<template v-if="pages?.length">
<NuxtLink
v-for="page in pages"
:to="'/writing/' + page.path"
:key="page.path"
class="article"
>
<h3 class="article__title">{{ page.title }}</h3>
<!-- <p class="article__date">
{{ formatDate(page.date) }} {{ formatDate(page.date) }}
</p> --> </p> -->
<p class="article__description"> <p class="article__description">
{{ page.description }} {{ page.description }}
</p> </p>
</NuxtLink> </NuxtLink>
</template> </template>
<template v-else> <template v-else>
<div class="empty-state"> <div class="empty-state">
<p>Nothing here yet.</p> <p>No thoughts...head empty</p>
</div> </div>
</template> </template>
</div> </div>
</section> </section>
</NuxtLayout> </NuxtLayout>
</div>
</template> </template>
<style scoped> <style scoped>
+13
View File
@@ -0,0 +1,13 @@
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
export default defineNuxtPlugin(() => {
gsap.registerPlugin(ScrollTrigger);
return {
provide: {
gsap,
ScrollTrigger,
},
};
});
+3
View File
@@ -7,6 +7,7 @@
"dependencies": { "dependencies": {
"@nuxt/content": "3.13.0", "@nuxt/content": "3.13.0",
"better-sqlite3": "^12.9.0", "better-sqlite3": "^12.9.0",
"gsap": "^3.15.0",
"nuxt": "^4.4.2", "nuxt": "^4.4.2",
"vue": "^3.5.33", "vue": "^3.5.33",
"vue-router": "^5.0.6", "vue-router": "^5.0.6",
@@ -981,6 +982,8 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"gsap": ["gsap@3.15.0", "", {}, "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A=="],
"gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="], "gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="],
"h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
+1
View File
@@ -30,6 +30,7 @@ export default defineNuxtConfig({
}, },
app: { app: {
pageTransition: { name: 'page', mode: 'out-in' },
head: { head: {
htmlAttrs: { lang: 'en' }, htmlAttrs: { lang: 'en' },
charset: 'utf-8', charset: 'utf-8',
+1
View File
@@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@nuxt/content": "3.13.0", "@nuxt/content": "3.13.0",
"better-sqlite3": "^12.9.0", "better-sqlite3": "^12.9.0",
"gsap": "^3.15.0",
"nuxt": "^4.4.2", "nuxt": "^4.4.2",
"vue": "^3.5.33", "vue": "^3.5.33",
"vue-router": "^5.0.6", "vue-router": "^5.0.6",