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;
+90 -14
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">
<h1 class="name">
{{ title || "Hello, I'm Jaron" }}
</h1>
</div>
<div class="mask">
<p class="title"> <p class="title">
{{ {{
description || description ||
'Software Engineer by trade — builder by nature.' 'Software Engineer by trade — builder by nature.'
}} }}
</p> </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"> <p style="text-align: right">Let's make something cool.</p>
Let's make something cool.
</p>
</div> </div>
</div> <div class="footer-lower flex-center" style="gap: var(--gap)">
<div class="footer-lower">
<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;
+74 -10
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,11 +31,48 @@ 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>
<div ref="pageRoot">
<NuxtLayout name="main"> <NuxtLayout name="main">
<section class="projects"> <section class="projects">
<h5>Selected Work</h5> <h5>Selected Work</h5>
@@ -63,8 +105,8 @@ definePageMeta({
> >
<h3 class="project__title">Rune Buddy</h3> <h3 class="project__title">Rune Buddy</h3>
<p class="project__description"> <p class="project__description">
League of Legends runepage manager. Save & organise your League of Legends runepage manager. Save & organise
pages for free. your pages for free.
</p> </p>
</a> </a>
</div> </div>
@@ -78,17 +120,19 @@ definePageMeta({
</p> </p>
<p> <p>
I left school and my job at Chick-fil-A in 2022 to write 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 code full time. It's definitely one of the best
I've ever made. I've learned so much since then and decisions I've ever made. I've learned so much since
then and
<em>still</em> have so much more to learn. <em>still</em> have so much more to learn.
</p> </p>
<p> <p>
If you want to build something cool or connect hit me up! I If you want to build something cool or connect hit me
still dont know enough people to form a band yet ;) up! I still dont know enough people to form a band yet
;)
</p> </p>
</div> </div>
</section> </section>
<!-- <section class="photos"> <section class="photos">
<h5>Photos</h5> <h5>Photos</h5>
<div class="section__content"> <div class="section__content">
<div class="photo-grid"> <div class="photo-grid">
@@ -166,8 +210,9 @@ definePageMeta({
</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;
+3 -1
View File
@@ -16,11 +16,12 @@ useSeoMeta({
}); });
definePageMeta({ definePageMeta({
layout: 'main', layout: false,
}); });
</script> </script>
<template> <template>
<div>
<NuxtLayout <NuxtLayout
name="main" name="main"
:title="page?.title" :title="page?.title"
@@ -40,6 +41,7 @@ definePageMeta({
</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>
+16 -5
View File
@@ -1,16 +1,22 @@
<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>
<div>
<NuxtLayout <NuxtLayout
name="main" name="main"
title="Writing" title="Writing"
@@ -18,7 +24,11 @@ const formatDate = (dateString: string) => {
> >
<template #header-upper> <template #header-upper>
<div class="header-upper-inner"> <div class="header-upper-inner">
<NuxtLink to="/" class="back-link" aria-label="Back to Home"> <NuxtLink
to="/"
class="back-link"
aria-label="Back to Home"
>
<span class="arrow">←</span> <span class="arrow">←</span>
<span>Home</span> <span>Home</span>
</NuxtLink> </NuxtLink>
@@ -44,12 +54,13 @@ const formatDate = (dateString: string) => {
</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",