patx/afterdarklabs
Replace scroll animations with CSS
Commit a0fc069 · patx · 2026-09-07T15:53:52-04:00
Comments
No comments yet.
Diff
diff --git a/docs/index.html b/docs/index.html
index 07d766d..179afdc 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -240,18 +240,6 @@
transform-origin: left center;
}
- .nav-brand-ready .nav-brand {
- visibility: hidden;
- opacity: 1;
- pointer-events: none;
- transform: none;
- }
-
- .nav-brand-ready .nav-brand.is-visible {
- visibility: visible;
- pointer-events: auto;
- }
-
.nav-cta {
display: inline-flex;
flex-shrink: 0;
@@ -290,19 +278,7 @@
.nav-brand-letter {
display: inline-block;
- opacity: var(--nav-letter-opacity, 1);
- filter: blur(var(--nav-letter-blur, 0));
- transform:
- translate3d(var(--nav-letter-x, 0), var(--nav-letter-y, 0), 0)
- rotate(var(--nav-letter-rotate, 0))
- skewX(var(--nav-letter-skew, 0))
- scale(var(--nav-letter-scale-x, 1), var(--nav-letter-scale-y, 1));
transform-origin: center;
- will-change: opacity, filter, transform;
- }
-
- .nav-brand-ready .nav-brand-letter {
- opacity: var(--nav-letter-opacity, 0);
}
@media (min-width: 1024px) {
@@ -364,11 +340,7 @@
.hero-letter {
display: inline-block;
- opacity: var(--hero-letter-opacity, 1);
- filter: blur(var(--hero-letter-blur, 0));
- transform: scale(var(--hero-letter-scale, 1));
transform-origin: center;
- will-change: opacity, filter, transform;
}
.hero-word {
@@ -513,65 +485,6 @@
padding-bottom: 56px;
}
- .work-intro.is-scroll-ready .work-heading-word {
- opacity: 0;
- filter: blur(0.18em);
- transform: translate3d(0, 0.42em, 0) rotate(1.5deg);
- transform-origin: left bottom;
- will-change: opacity, filter, transform;
- }
-
- .work-intro.is-scroll-ready.is-motion-ready .work-heading-word {
- transition:
- opacity 220ms ease,
- filter 360ms ease,
- transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
- }
-
- .work-intro.is-scroll-ready .work-heading-word.is-revealed {
- opacity: 1;
- filter: none;
- transform: none;
- }
-
- @media (prefers-reduced-motion: reduce) {
- .nav-brand-letter {
- filter: none;
- transform: none;
- will-change: auto;
- }
-
- .hero-letter {
- opacity: 1;
- filter: none;
- transform: none;
- will-change: auto;
- }
-
- .hero-scroll-hint::after {
- animation: none;
- }
-
- .work-intro {
- height: auto;
- }
-
- .work-intro-stage {
- position: relative;
- top: auto;
- height: auto;
- transform: none;
- will-change: auto;
- }
-
- .work-intro.is-scroll-ready .work-heading-word {
- opacity: 1;
- filter: none;
- transform: none;
- transition: none;
- }
- }
-
.work-stage {
box-sizing: border-box;
padding: clamp(0rem, 0.5vw, 0.5rem) clamp(1rem, 2vw, 2rem);
@@ -647,20 +560,18 @@
backdrop-filter: blur(0.35rem);
}
- #work-scroller > article:focus-within > .portfolio-details,
- #work-scroller > article.is-description-visible > .portfolio-details {
+ #work-scroller > article:focus-within > .portfolio-details {
opacity: 1;
clip-path: inset(0);
pointer-events: auto;
transform: none;
}
- #work-scroller > article:focus-within > .portfolio-media > img,
- #work-scroller > article.is-description-visible > .portfolio-media > img {
+ #work-scroller > article:focus-within > .portfolio-media > img {
transform: scale(1.025);
}
- #work-scroller > article:focus-within {
+ #work-scroller > article:focus-visible {
outline: 2px solid #fff;
outline-offset: -2px;
}
@@ -739,22 +650,6 @@
outline-offset: 0.08em;
}
- .portfolio-scroll-ready #work-scroller > article {
- opacity: var(--card-opacity, 0);
- filter: blur(var(--card-blur, 0.4rem));
- transform:
- translate3d(var(--card-x, 0), var(--card-y, 5rem), 0)
- rotate(var(--card-rotate, 0deg))
- skewY(var(--card-skew, 0deg))
- scale(var(--card-scale-x, 0.94), var(--card-scale-y, 1.04));
- transform-origin: center bottom;
- will-change: auto;
- }
-
- .portfolio-scroll-ready #work-scroller > article.is-card-animating {
- will-change: opacity, filter, transform;
- }
-
@media (min-width: 768px) {
#work-scroller {
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -788,27 +683,284 @@
cursor: pointer;
}
- #work-scroller > article.is-description-visible {
- cursor: default;
+ #work-scroller > article > .portfolio-details {
+ -webkit-backdrop-filter: none;
+ backdrop-filter: none;
}
+ }
- .portfolio-scroll-ready #work-scroller > article {
- filter: none;
+ @supports (animation-timeline: scroll()) {
+ .hero-letter,
+ .nav-brand-letter {
+ animation-duration: auto;
+ animation-fill-mode: both;
+ animation-timeline: scroll(root block);
+ animation-timing-function: linear;
+ animation-range-start: var(--hero-range-start);
+ animation-range-end: var(--hero-range-end);
}
- #work-scroller > article > .portfolio-details {
- -webkit-backdrop-filter: none;
- backdrop-filter: none;
+ .hero-letter {
+ animation-name: hero-letter-out;
+ will-change: opacity, filter, transform;
+ }
+
+ .nav-brand-letter {
+ animation-name: nav-letter-in;
+ will-change: opacity, filter, transform;
+ }
+
+ .hero-letter:nth-child(1),
+ .nav-brand-letter:nth-child(1) {
+ --hero-range-start: 4.5svh;
+ --hero-range-end: 31svh;
+ }
+
+ .hero-letter:nth-child(2),
+ .nav-brand-letter:nth-child(2) {
+ --hero-range-start: 25svh;
+ --hero-range-end: 51.5svh;
+ }
+
+ .hero-letter:nth-child(3),
+ .nav-brand-letter:nth-child(3) {
+ --hero-range-start: 45.5svh;
+ --hero-range-end: 72svh;
+ }
+
+ .nav-brand {
+ animation-name: nav-brand-enable;
+ animation-duration: auto;
+ animation-fill-mode: both;
+ animation-timeline: scroll(root block);
+ animation-timing-function: steps(1, end);
+ animation-range: 4.5svh calc(4.5svh + 1px);
+ }
+
+ .hero-scroll-hint {
+ animation-name: scroll-hint-out;
+ animation-duration: auto;
+ animation-fill-mode: both;
+ animation-timeline: scroll(root block);
+ animation-timing-function: linear;
+ animation-range: 45.5svh 72svh;
+ }
+
+ .work-intro:not(.work-intro--closing) {
+ view-timeline-name: --work-intro;
+ view-timeline-axis: block;
+ }
+
+ .work-intro:not(.work-intro--closing) .work-heading-word {
+ animation-name: heading-word-in;
+ animation-duration: auto;
+ animation-fill-mode: both;
+ animation-timeline: --work-intro;
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
+ animation-range-start: var(--word-range-start);
+ animation-range-end: var(--word-range-end);
+ transform-origin: left bottom;
+ }
+
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(1) { --word-range-start: cover 38%; --word-range-end: cover 44%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(2) { --word-range-start: cover 39.2%; --word-range-end: cover 45.2%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(3) { --word-range-start: cover 40.4%; --word-range-end: cover 46.4%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(4) { --word-range-start: cover 41.6%; --word-range-end: cover 47.6%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(5) { --word-range-start: cover 42.8%; --word-range-end: cover 48.8%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(6) { --word-range-start: cover 44%; --word-range-end: cover 50%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(7) { --word-range-start: cover 45.2%; --word-range-end: cover 51.2%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(8) { --word-range-start: cover 46.4%; --word-range-end: cover 52.4%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(9) { --word-range-start: cover 47.6%; --word-range-end: cover 53.6%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(10) { --word-range-start: cover 48.8%; --word-range-end: cover 54.8%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(11) { --word-range-start: cover 50%; --word-range-end: cover 56%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(12) { --word-range-start: cover 51.2%; --word-range-end: cover 57.2%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(13) { --word-range-start: cover 52.4%; --word-range-end: cover 58.4%; }
+ .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(14) { --word-range-start: cover 53.6%; --word-range-end: cover 59.6%; }
+
+ #work-scroller > article {
+ --card-start-blur: 7px;
+ --card-mid-blur: 3.5px;
+ --card-end-blur: 1.25px;
+ animation-name: portfolio-card-in-left;
+ animation-duration: auto;
+ animation-fill-mode: both;
+ animation-timeline: view(block);
+ animation-timing-function: linear;
+ animation-range: cover 4% cover 45%;
+ transform-origin: center bottom;
+ }
+
+ #work-scroller > article:nth-child(even) {
+ animation-name: portfolio-card-in-right;
+ }
+
+ .work-intro--closing {
+ view-timeline-name: --closing-intro;
+ view-timeline-axis: block;
+ }
+
+ .work-intro--closing .work-heading-email,
+ .work-intro--closing .work-heading-email > .work-heading-word {
+ animation-name: heading-word-in;
+ animation-duration: auto;
+ animation-fill-mode: both;
+ animation-timeline: --closing-intro;
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
+ animation-range-start: var(--closing-range-start);
+ animation-range-end: var(--closing-range-end);
+ transform-origin: left bottom;
+ }
+
+ .work-intro--closing .work-heading-email { --closing-range-start: entry 4%; --closing-range-end: entry 20%; }
+ .work-intro--closing .work-heading-email > .work-heading-word:nth-child(1) { --closing-range-start: entry 18%; --closing-range-end: entry 34%; }
+ .work-intro--closing .work-heading-email > .work-heading-word:nth-child(2) { --closing-range-start: entry 32%; --closing-range-end: entry 48%; }
+ .work-intro--closing .work-heading-email > .work-heading-word:nth-child(3) { --closing-range-start: entry 46%; --closing-range-end: entry 62%; }
+ .work-intro--closing .work-heading-email > .work-heading-word:nth-child(4) { --closing-range-start: entry 60%; --closing-range-end: entry 76%; }
+ .work-intro--closing .work-heading-email > .work-heading-word:nth-child(5) { --closing-range-start: entry 74%; --closing-range-end: entry 90%; }
+ .work-intro--closing .work-heading-email > .work-heading-word:nth-child(6) { --closing-range-start: entry 84%; --closing-range-end: entry 100%; }
+
+ @media (orientation: portrait) {
+ #work-scroller > article:last-child {
+ animation-range-end: cover 30%;
+ }
+ }
+
+ @media (min-width: 768px) and (orientation: portrait) {
+ #work-scroller > article:nth-last-child(2) {
+ animation-range-end: cover 30%;
+ }
+ }
+ }
+
+ @keyframes hero-letter-out {
+ 0% { opacity: 1; filter: blur(0); transform: scale(1); }
+ 35% { opacity: 0.72; filter: blur(0.005em); transform: scale(0.984); }
+ 65% { opacity: 0.28; filter: blur(0.013em); transform: scale(0.961); }
+ 100% { opacity: 0; filter: blur(0.018em); transform: scale(0.945); }
+ }
+
+ @keyframes nav-letter-in {
+ 0% {
+ opacity: 0;
+ filter: blur(0.32rem);
+ transform: translate3d(-1.35rem, 0, 0) rotate(-12deg) skewX(-18deg) scale(0.38, 1.28);
+ }
+ 35% {
+ opacity: 0.28;
+ filter: blur(0.22rem);
+ transform: translate3d(-0.82rem, 0.08rem, 0) rotate(-6deg) skewX(-10deg) scale(0.61, 1.15);
+ }
+ 65% {
+ opacity: 0.72;
+ filter: blur(0.09rem);
+ transform: translate3d(-0.18rem, -0.04rem, 0) rotate(1.5deg) skewX(2deg) scale(0.9, 1.04);
+ }
+ 82% {
+ opacity: 0.92;
+ filter: blur(0.03rem);
+ transform: translate3d(0.05rem, 0.02rem, 0) rotate(-0.8deg) skewX(-1deg) scale(1.02, 0.99);
+ }
+ 100% { opacity: 1; filter: blur(0); transform: none; }
+ }
+
+ @keyframes nav-brand-enable {
+ from { visibility: hidden; pointer-events: none; }
+ to { visibility: visible; pointer-events: auto; }
+ }
+
+ @keyframes scroll-hint-out {
+ from { opacity: 1; visibility: visible; pointer-events: auto; }
+ to { opacity: 0; visibility: hidden; pointer-events: none; }
+ }
+
+ @keyframes heading-word-in {
+ from {
+ opacity: 0;
+ filter: blur(0.18em);
+ transform: translate3d(0, 0.42em, 0) rotate(1.5deg);
+ }
+ to { opacity: 1; filter: blur(0); transform: none; }
+ }
+
+ @keyframes portfolio-card-in-left {
+ 0% {
+ opacity: 0;
+ filter: blur(var(--card-start-blur));
+ transform: translate3d(-6vw, 96px, 0) rotate(-5deg) skewY(0deg) scale(0.93, 1.065);
+ }
+ 50% {
+ opacity: 0.82;
+ filter: blur(var(--card-mid-blur));
+ transform: translate3d(-2vw, 42px, 0) rotate(-1.5deg) skewY(-0.8deg) scale(0.975, 1.025);
+ }
+ 72% {
+ opacity: 1;
+ filter: blur(var(--card-end-blur));
+ transform: translate3d(0.35vw, 8px, 0) rotate(0.7deg) skewY(0.25deg) scale(1.006, 0.996);
+ }
+ 100% { opacity: 1; filter: blur(0); transform: none; }
+ }
+
+ @keyframes portfolio-card-in-right {
+ 0% {
+ opacity: 0;
+ filter: blur(var(--card-start-blur));
+ transform: translate3d(6vw, 96px, 0) rotate(5deg) skewY(0deg) scale(0.93, 1.065);
+ }
+ 50% {
+ opacity: 0.82;
+ filter: blur(var(--card-mid-blur));
+ transform: translate3d(2vw, 42px, 0) rotate(1.5deg) skewY(0.8deg) scale(0.975, 1.025);
+ }
+ 72% {
+ opacity: 1;
+ filter: blur(var(--card-end-blur));
+ transform: translate3d(-0.35vw, 8px, 0) rotate(-0.7deg) skewY(-0.25deg) scale(1.006, 0.996);
+ }
+ 100% { opacity: 1; filter: blur(0); transform: none; }
+ }
+
+ @media (hover: none), (pointer: coarse) {
+ #work-scroller > article {
+ --card-start-blur: 0px;
+ --card-mid-blur: 0px;
+ --card-end-blur: 0px;
}
}
@media (prefers-reduced-motion: reduce) {
- .portfolio-scroll-ready #work-scroller > article {
+ .nav-brand,
+ .nav-brand-letter,
+ .hero-letter,
+ .hero-scroll-hint,
+ .work-heading-word,
+ #work-scroller > article {
+ animation: none !important;
opacity: 1;
filter: none;
+ visibility: visible;
transform: none;
}
+ .nav-brand,
+ .hero-scroll-hint {
+ pointer-events: auto;
+ }
+
+ .hero-scroll-hint::after {
+ animation: none;
+ }
+
+ .work-intro {
+ height: auto;
+ }
+
+ .work-intro-stage {
+ position: relative;
+ top: auto;
+ height: auto;
+ }
+
#work-scroller > article > .portfolio-media > img,
#work-scroller > article > .portfolio-details {
transition: none;
@@ -970,7 +1122,7 @@
<div class="work-stage">
<div class="work-stage-content">
<div id="work-scroller">
- <article>
+ <article tabindex="0">
<div class="portfolio-media">
<img src="https://afterdarklabs.io/gardenfresh.webp?v=2" width="1200" height="702" loading="lazy" decoding="async" alt="Garden Fresh Wholesale ecommerce web app project by After Dark Labs">
</div>
@@ -980,7 +1132,7 @@
</p>
</div>
</article>
- <article>
+ <article tabindex="0">
<div class="portfolio-media">
<img src="https://afterdarklabs.io/miadruck.webp?v=3" width="1200" height="681" loading="lazy" decoding="async" alt="Mia Druck real estate website project by After Dark Labs">
</div>
@@ -990,7 +1142,7 @@
</p>
</div>
</article>
- <article>
+ <article tabindex="0">
<div class="portfolio-media">
<img src="https://afterdarklabs.io/coralvibe.webp?v=1" width="1200" height="678" loading="lazy" decoding="async" alt="Shopify store for Coral Vibe Florida project by After Dark Labs">
</div>
@@ -1000,7 +1152,7 @@
</p>
</div>
</article>
- <article>
+ <article tabindex="0">
<div class="portfolio-media">
<img src="https://afterdarklabs.io/frontlineboats.webp?v=1" width="1200" height="700" loading="lazy" decoding="async" alt="Frontline Boat Services marine services website project by After Dark Labs">
</div>
@@ -1010,7 +1162,7 @@
</p>
</div>
</article>
- <article>
+ <article tabindex="0">
<div class="portfolio-media">
<img src="https://afterdarklabs.io/voldservices.webp" width="1200" height="702" loading="lazy" decoding="async" alt="Vold Services exterior cleaning website with a scroll-animated hero, designed by After Dark Labs">
</div>
@@ -1020,7 +1172,7 @@
</p>
</div>
</article>
- <article>
+ <article tabindex="0">
<div class="portfolio-media">
<img src="https://afterdarklabs.io/vegy.webp?v=1" width="1200" height="702" loading="lazy" decoding="async" alt="Vegy wholesale ordering platform project by After Dark Labs">
</div>
@@ -1030,7 +1182,7 @@
</p>
</div>
</article>
- <article>
+ <article tabindex="0">
<div class="portfolio-media">
<img src="https://afterdarklabs.io/prima.webp?v=1" width="1200" height="702" loading="lazy" decoding="async" alt="Prima Produce business website project by After Dark Labs">
</div>
@@ -1040,7 +1192,7 @@
</p>
</div>
</article>
- <article>
+ <article tabindex="0">
<div class="portfolio-media">
<img src="https://afterdarklabs.io/invoiceotter.webp?v=1" width="1200" height="673" loading="lazy" decoding="async" alt="Invoice Otter SaaS project by After Dark Labs">
</div>
@@ -1078,487 +1230,5 @@
</main>
- <script>
- (function () {
- const hero = document.getElementById('hero');
- if (!hero) return;
-
- const nav = document.querySelector('nav');
- const mobileLayout = window.matchMedia('(max-width: 767px)');
- let lockedWidth = 0;
- let resizeFrame = 0;
-
- function lockMobileHeroHeight() {
- resizeFrame = 0;
-
- if (!mobileLayout.matches) {
- hero.style.removeProperty('--mobile-hero-height');
- hero.style.removeProperty('--mobile-nav-height');
- lockedWidth = 0;
- return;
- }
-
- const navHeight = Math.round(nav?.getBoundingClientRect().height || 0);
- if (navHeight) hero.style.setProperty('--mobile-nav-height', `${navHeight}px`);
-
- const layoutWidth = Math.round(document.documentElement.clientWidth);
- if (Math.abs(layoutWidth - lockedWidth) < 2) return;
-
- const visibleHeight = Math.round(window.visualViewport?.height || window.innerHeight);
- if (!visibleHeight) return;
-
- lockedWidth = layoutWidth;
- hero.style.setProperty('--mobile-hero-height', `${visibleHeight}px`);
- }
-
- function scheduleHeroHeightLock() {
- if (!resizeFrame) resizeFrame = window.requestAnimationFrame(lockMobileHeroHeight);
- }
-
- window.addEventListener('resize', scheduleHeroHeightLock);
- window.visualViewport?.addEventListener('resize', scheduleHeroHeightLock);
- mobileLayout.addEventListener?.('change', scheduleHeroHeightLock);
- lockMobileHeroHeight();
- })();
-
- (function () {
- const hero = document.getElementById('hero');
- const heading = hero?.querySelector('.hero-content h1');
- const letters = Array.from(heading?.querySelectorAll('.hero-letter') || []);
- const scrollHint = hero?.querySelector('.hero-scroll-hint');
- const nav = document.querySelector('nav');
- const brands = Array.from(document.querySelectorAll('.nav-brand'));
- const navLetterGroups = brands.map(brand => Array.from(brand.querySelectorAll('.nav-brand-letter')));
- if (!hero || !heading || !letters.length || !nav || !brands.length) return;
-
- const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
- let heroTop = 0;
- let fadeDistance = 1;
- let headingSize = 1;
- let updateFrame = 0;
- let resizeFrame = 0;
-
- function clamp(value, minimum = 0, maximum = 1) {
- return Math.min(maximum, Math.max(minimum, value));
- }
-
- function updateHeroLetters() {
- updateFrame = 0;
-
- if (reduceMotion.matches) {
- const revealBrand = hero.getBoundingClientRect().bottom <= nav.getBoundingClientRect().bottom + 1;
- letters.forEach(letter => {
- letter.style.removeProperty('--hero-letter-opacity');
- letter.style.removeProperty('--hero-letter-blur');
- letter.style.removeProperty('--hero-letter-scale');
- });
- navLetterGroups.forEach(group => group.forEach(letter => {
- letter.style.setProperty('--nav-letter-opacity', revealBrand ? '1' : '0');
- letter.style.removeProperty('--nav-letter-blur');
- letter.style.removeProperty('--nav-letter-x');
- letter.style.removeProperty('--nav-letter-y');
- letter.style.removeProperty('--nav-letter-rotate');
- letter.style.removeProperty('--nav-letter-skew');
- letter.style.removeProperty('--nav-letter-scale-x');
- letter.style.removeProperty('--nav-letter-scale-y');
- }));
- brands.forEach(brand => brand.classList.toggle('is-visible', revealBrand));
- if (scrollHint) {
- scrollHint.style.opacity = revealBrand ? '0' : '1';
- scrollHint.style.pointerEvents = revealBrand ? 'none' : 'auto';
- scrollHint.toggleAttribute('inert', revealBrand);
- }
- return;
- }
-
- const progress = clamp((window.scrollY - heroTop) / fadeDistance);
- let visibleNavLetters = 0;
-
- letters.forEach((letter, index) => {
- const start = 0.05 + index * 0.23;
- const localProgress = clamp((progress - start) / 0.3);
- const eased = localProgress * localProgress * (3 - 2 * localProgress);
- const remaining = 1 - eased;
- const wobble = Math.sin(localProgress * Math.PI * 3 + index * 0.65) * remaining;
-
- letter.style.setProperty('--hero-letter-opacity', (1 - eased).toFixed(4));
- letter.style.setProperty('--hero-letter-blur', `${(eased * headingSize * 0.018).toFixed(2)}px`);
- letter.style.setProperty('--hero-letter-scale', (1 - eased * 0.055).toFixed(4));
-
- if (scrollHint && index === letters.length - 1) {
- const hintVisible = remaining > 0.02;
- scrollHint.style.opacity = remaining.toFixed(4);
- scrollHint.style.pointerEvents = hintVisible ? 'auto' : 'none';
- scrollHint.toggleAttribute('inert', !hintVisible);
- }
-
- navLetterGroups.forEach(group => {
- const navLetter = group[index];
- if (!navLetter) return;
-
- navLetter.style.setProperty('--nav-letter-opacity', eased.toFixed(4));
- navLetter.style.setProperty('--nav-letter-blur', `${(remaining * 0.32).toFixed(3)}rem`);
- navLetter.style.setProperty('--nav-letter-x', `${(-1.35 * remaining + wobble * 0.42).toFixed(3)}rem`);
- navLetter.style.setProperty('--nav-letter-y', `${(wobble * 0.18).toFixed(3)}rem`);
- navLetter.style.setProperty('--nav-letter-rotate', `${(-12 * remaining + wobble * 7).toFixed(2)}deg`);
- navLetter.style.setProperty('--nav-letter-skew', `${(-18 * remaining + wobble * 9).toFixed(2)}deg`);
- navLetter.style.setProperty('--nav-letter-scale-x', `${(0.38 + eased * 0.62 + wobble * 0.08).toFixed(4)}`);
- navLetter.style.setProperty('--nav-letter-scale-y', `${(1.28 - eased * 0.28 - wobble * 0.06).toFixed(4)}`);
- });
-
- if (eased > 0.02) visibleNavLetters += 1;
- });
-
- brands.forEach(brand => brand.classList.toggle('is-visible', visibleNavLetters > 0));
- }
-
- function measureHeroLetters() {
- resizeFrame = 0;
- heroTop = hero.getBoundingClientRect().top + window.scrollY;
- fadeDistance = Math.max(1, hero.offsetHeight - (nav?.getBoundingClientRect().height || 0));
- headingSize = parseFloat(window.getComputedStyle(heading).fontSize) || 1;
- updateHeroLetters();
- }
-
- function scheduleHeroLetterUpdate() {
- if (!updateFrame) updateFrame = window.requestAnimationFrame(updateHeroLetters);
- }
-
- function scheduleHeroLetterMeasure() {
- if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureHeroLetters);
- }
-
- window.addEventListener('scroll', scheduleHeroLetterUpdate, { passive: true });
- window.addEventListener('resize', scheduleHeroLetterMeasure);
- window.visualViewport?.addEventListener('resize', scheduleHeroLetterMeasure);
- window.addEventListener('pageshow', event => {
- if (event.persisted) scheduleHeroLetterMeasure();
- });
- reduceMotion.addEventListener('change', scheduleHeroLetterMeasure);
- document.fonts?.ready.then(scheduleHeroLetterMeasure);
- document.documentElement.classList.add('nav-brand-ready');
- measureHeroLetters();
- })();
-
- (function () {
- const intro = document.querySelector('.work-intro:not(.work-intro--closing)');
- const heading = intro?.querySelector('.work-heading');
- const firstCard = document.querySelector('#work-scroller > article');
- const words = Array.from(intro?.querySelectorAll('.work-heading-word') || []);
- if (!intro || !heading || !words.length) return;
-
- const nav = document.querySelector('nav');
- const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
- let revealStart = 0;
- let revealDistance = 1;
- let revealedWords = -1;
- let updateFrame = 0;
- let resizeFrame = 0;
-
- function clamp(value, minimum = 0, maximum = 1) {
- return Math.min(maximum, Math.max(minimum, value));
- }
-
- function getDocumentTop(element) {
- let top = 0;
- let current = element;
-
- while (current) {
- top += current.offsetTop;
- current = current.offsetParent;
- }
-
- return top;
- }
-
- function updateWorkIntro() {
- updateFrame = 0;
-
- if (reduceMotion.matches) {
- words.forEach(word => word.classList.add('is-revealed'));
- revealedWords = words.length;
- return;
- }
-
- const progress = window.scrollY <= 1
- ? 0
- : clamp((window.scrollY - revealStart) / revealDistance);
- const nextRevealedWords = Math.ceil(progress * words.length);
- if (nextRevealedWords === revealedWords) return;
-
- revealedWords = nextRevealedWords;
- words.forEach((word, index) => {
- word.classList.toggle('is-revealed', index < revealedWords);
- });
- }
-
- function measureWorkIntro() {
- resizeFrame = 0;
- intro.classList.toggle('is-scroll-ready', !reduceMotion.matches);
- intro.classList.remove('is-motion-ready');
-
- const navHeight = nav?.getBoundingClientRect().height || 0;
- const viewportHeight = window.visualViewport?.height || window.innerHeight;
- const headingCenter = getDocumentTop(heading) + heading.offsetHeight / 2;
- const visualCenter = navHeight + (viewportHeight - navHeight) * 0.58;
- const cardRevealStart = firstCard
- ? getDocumentTop(firstCard) - viewportHeight * 0.94
- : headingCenter - visualCenter + viewportHeight * 0.32;
- revealStart = headingCenter - visualCenter;
- const revealEnd = Math.max(
- cardRevealStart + viewportHeight * 0.06,
- revealStart + viewportHeight * 0.24
- );
- revealDistance = Math.max(1, revealEnd - revealStart);
- revealedWords = -1;
- updateWorkIntro();
-
- if (!reduceMotion.matches) {
- window.requestAnimationFrame(() => {
- window.requestAnimationFrame(() => intro.classList.add('is-motion-ready'));
- });
- }
- }
-
- function scheduleWorkIntroUpdate() {
- if (!updateFrame) updateFrame = window.requestAnimationFrame(updateWorkIntro);
- }
-
- function scheduleWorkIntroMeasure() {
- if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureWorkIntro);
- }
-
- window.addEventListener('scroll', scheduleWorkIntroUpdate, { passive: true });
- window.addEventListener('resize', scheduleWorkIntroMeasure);
- window.addEventListener('pageshow', event => {
- if (event.persisted) scheduleWorkIntroMeasure();
- });
- reduceMotion.addEventListener('change', scheduleWorkIntroMeasure);
- document.fonts?.ready.then(scheduleWorkIntroMeasure);
- measureWorkIntro();
- })();
-
- (function () {
- const section = document.getElementById('work');
- const cards = Array.from(section?.querySelectorAll('#work-scroller > article') || []);
- if (!cards.length) return;
-
- const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
- const touchDisclosure = window.matchMedia('(hover: none), (pointer: coarse)');
- let cardTops = [];
- let cardStates = cards.map(() => -1);
- let layoutWidth = 0;
- let layoutHeight = 0;
- let lastScrollY = -1;
- let updateFrame = 0;
- let resizeFrame = 0;
-
- function clamp(value, minimum = 0, maximum = 1) {
- return Math.min(maximum, Math.max(minimum, value));
- }
-
- function getDocumentTop(element) {
- let top = 0;
- let current = element;
-
- while (current) {
- top += current.offsetTop;
- current = current.offsetParent;
- }
-
- return top;
- }
-
- function updateCards() {
- updateFrame = 0;
- if (reduceMotion.matches) {
- cards.forEach(card => card.classList.remove('is-card-animating'));
- return;
- }
-
- const scrollY = Math.max(0, window.scrollY);
- const viewportHeight = layoutHeight || document.documentElement.clientHeight || window.innerHeight;
- if (scrollY === lastScrollY) return;
-
- lastScrollY = scrollY;
- const revealDistance = Math.max(240, viewportHeight * 0.58);
- const travelX = Math.min(72, document.documentElement.clientWidth * 0.06);
-
- cards.forEach((card, index) => {
- const progress = clamp((scrollY + viewportHeight * 0.94 - cardTops[index]) / revealDistance);
- const state = progress <= 0 ? 0 : progress >= 1 ? 1 : 2;
- if (cardStates[index] !== state) {
- card.classList.toggle('is-card-animating', state === 2);
- }
- if (state !== 2 && cardStates[index] === state) return;
-
- cardStates[index] = state;
- const eased = 1 - Math.pow(1 - progress, 3);
- const remaining = 1 - eased;
- const direction = index % 2 === 0 ? -1 : 1;
- const wave = Math.sin(progress * Math.PI * 3) * remaining;
- const x = direction * (remaining * travelX + wave * 20);
- const y = remaining * 96 - Math.sin(progress * Math.PI) * 10;
- const rotation = direction * (remaining * 5 + wave * 2.4);
- const skew = direction * wave * 1.6;
- const scaleX = 1 - remaining * 0.07 + Math.abs(wave) * 0.025;
- const scaleY = 1 + remaining * 0.065 - wave * 0.02;
- const opacity = clamp(progress * 1.65);
- const blur = remaining * 7;
-
- card.style.setProperty('--card-x', `${x.toFixed(2)}px`);
- card.style.setProperty('--card-y', `${y.toFixed(2)}px`);
- card.style.setProperty('--card-rotate', `${rotation.toFixed(2)}deg`);
- card.style.setProperty('--card-skew', `${skew.toFixed(2)}deg`);
- card.style.setProperty('--card-scale-x', scaleX.toFixed(4));
- card.style.setProperty('--card-scale-y', scaleY.toFixed(4));
- card.style.setProperty('--card-opacity', opacity.toFixed(4));
- card.style.setProperty('--card-blur', `${blur.toFixed(2)}px`);
- });
- }
-
- function measureCards() {
- resizeFrame = 0;
- const nextLayoutWidth = document.documentElement.clientWidth;
- const nextLayoutHeight = document.documentElement.clientHeight || window.innerHeight;
- const widthChanged = Math.abs(nextLayoutWidth - layoutWidth) >= 2;
- const desktopHeightChanged = !touchDisclosure.matches && Math.abs(nextLayoutHeight - layoutHeight) >= 2;
- if (layoutWidth && !widthChanged && !desktopHeightChanged) {
- scheduleCardUpdate();
- return;
- }
-
- layoutWidth = nextLayoutWidth;
- layoutHeight = nextLayoutHeight;
- cardTops = cards.map(getDocumentTop);
- cardStates = cards.map(() => -1);
- lastScrollY = -1;
- updateCards();
- }
-
- function scheduleCardUpdate() {
- if (!updateFrame) updateFrame = window.requestAnimationFrame(updateCards);
- }
-
- function scheduleCardMeasure() {
- if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureCards);
- }
-
- function scheduleCardRemeasure() {
- layoutWidth = 0;
- scheduleCardMeasure();
- }
-
- function closeDescriptions(exceptCard = null) {
- cards.forEach(card => {
- if (card !== exceptCard) card.classList.remove('is-description-visible');
- });
- }
-
- cards.forEach(card => {
- card.addEventListener('click', event => {
- if (!touchDisclosure.matches || event.target.closest('.portfolio-live-link')) return;
-
- closeDescriptions(card);
- card.classList.add('is-description-visible');
- });
- });
-
- document.addEventListener('pointerdown', event => {
- if (!touchDisclosure.matches || event.target.closest('#work-scroller > article')) return;
- closeDescriptions();
- const activeCard = document.activeElement?.closest?.('#work-scroller > article');
- if (activeCard) document.activeElement.blur();
- }, { passive: true });
-
- touchDisclosure.addEventListener('change', () => closeDescriptions());
-
- section.classList.add('portfolio-scroll-ready');
- window.addEventListener('scroll', scheduleCardUpdate, { passive: true });
- window.addEventListener('resize', scheduleCardMeasure);
- window.visualViewport?.addEventListener('resize', scheduleCardMeasure);
- window.addEventListener('pageshow', event => {
- if (event.persisted) scheduleCardRemeasure();
- });
- reduceMotion.addEventListener('change', scheduleCardRemeasure);
- measureCards();
- })();
-
- (function () {
- const closingStatement = document.querySelector('.work-intro--closing');
- const words = Array.from(closingStatement?.querySelectorAll('.work-heading-word') || []);
- if (!closingStatement || !words.length) return;
-
- const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
- let revealStart = 0;
- let revealDistance = 1;
- let revealedWords = -1;
- let updateFrame = 0;
- let resizeFrame = 0;
-
- function clamp(value, minimum = 0, maximum = 1) {
- return Math.min(maximum, Math.max(minimum, value));
- }
-
- function updateClosingIntro() {
- updateFrame = 0;
-
- if (reduceMotion.matches) {
- words.forEach(word => word.classList.add('is-revealed'));
- revealedWords = words.length;
- return;
- }
-
- const progress = clamp((window.scrollY - revealStart) / revealDistance);
- const nextRevealedWords = Math.ceil(progress * words.length);
- if (nextRevealedWords === revealedWords) return;
-
- revealedWords = nextRevealedWords;
- words.forEach((word, index) => {
- word.classList.toggle('is-revealed', index < revealedWords);
- });
- }
-
- function measureClosingIntro() {
- resizeFrame = 0;
- closingStatement.classList.toggle('is-scroll-ready', !reduceMotion.matches);
- closingStatement.classList.remove('is-motion-ready');
-
- const statementTop = closingStatement.getBoundingClientRect().top + window.scrollY;
- const viewportHeight = window.visualViewport?.height || window.innerHeight;
- const statementHeight = closingStatement.offsetHeight;
- const maximumScroll = Math.max(0, document.documentElement.scrollHeight - viewportHeight);
- revealStart = Math.min(maximumScroll, statementTop - viewportHeight + statementHeight * 0.05);
- revealDistance = Math.max(1, maximumScroll - revealStart);
- revealedWords = -1;
- updateClosingIntro();
-
- if (!reduceMotion.matches) {
- window.requestAnimationFrame(() => {
- window.requestAnimationFrame(() => closingStatement.classList.add('is-motion-ready'));
- });
- }
- }
-
- function scheduleClosingIntroUpdate() {
- if (!updateFrame) updateFrame = window.requestAnimationFrame(updateClosingIntro);
- }
-
- function scheduleClosingIntroMeasure() {
- if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureClosingIntro);
- }
-
- window.addEventListener('scroll', scheduleClosingIntroUpdate, { passive: true });
- window.addEventListener('resize', scheduleClosingIntroMeasure);
- window.visualViewport?.addEventListener('resize', scheduleClosingIntroMeasure);
- window.addEventListener('pageshow', event => {
- if (event.persisted) scheduleClosingIntroMeasure();
- });
- reduceMotion.addEventListener('change', scheduleClosingIntroMeasure);
- document.fonts?.ready.then(scheduleClosingIntroMeasure);
- measureClosingIntro();
- })();
-
- </script>
</body>
</html>