@@ -98,11 +98,13 @@ import RotatingCoil from './RotatingCoil.astro';
9898 animation-delay: 0.3s;
9999 }
100100
101- /* Parallax layers */
101+ /* Parallax layers - optimized for GPU compositing */
102102 .hero-coil,
103103 .hero-content,
104104 .hero-scroll {
105105 will-change: transform;
106+ backface-visibility: hidden;
107+ -webkit-backface-visibility: hidden;
106108 }
107109
108110 /* Reduced Motion */
@@ -120,6 +122,7 @@ import RotatingCoil from './RotatingCoil.astro';
120122 .hero-content,
121123 .hero-scroll {
122124 will-change: auto;
125+ transform: none !important;
123126 }
124127 }
125128</style >
@@ -223,7 +226,7 @@ import RotatingCoil from './RotatingCoil.astro';
223226 }
224227
225228 function initParallax() {
226- const hero = document.querySelector('header.relative.min-h-screen ') as HTMLElement;
229+ const hero = document.querySelector('header#hero ') as HTMLElement;
227230 if (!hero) return;
228231
229232 const coil = hero.querySelector('.hero-coil') as HTMLElement;
@@ -233,54 +236,110 @@ import RotatingCoil from './RotatingCoil.astro';
233236 // Check for reduced motion preference
234237 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
235238
236- let ticking = false;
239+ // Cache heroHeight
240+ let heroHeight = hero.offsetHeight;
241+
242+ // Update on resize with debounce
243+ let resizeTimeout: number;
244+ window.addEventListener('resize', () => {
245+ clearTimeout(resizeTimeout);
246+ resizeTimeout = window.setTimeout(() => {
247+ heroHeight = hero.offsetHeight;
248+ }, 100);
249+ }, { passive: true });
250+
251+ let isScrolling = false;
252+ let lastScrollY = window.scrollY;
237253
238254 function updateParallax() {
239- const scrollY = window.scrollY;
240- const heroHeight = hero.offsetHeight;
255+ if (!isScrolling) return;
241256
242- // Only apply parallax when hero is in view
243- if (scrollY < heroHeight) {
244- // Calculate fade-out opacity (starts fading at 20% scroll, fully faded at 60%)
245- const fadeStart = heroHeight * 0.2;
246- const fadeEnd = heroHeight * 0.6;
247- const opacity = Math.max(0, Math.min(1, 1 - (scrollY - fadeStart) / (fadeEnd - fadeStart)));
257+ const scrollY = window.scrollY;
248258
249- // Coil moves slower (0.3x scroll speed) - feels further back
259+ // Early exit when off-screen
260+ if (scrollY >= heroHeight) {
250261 if (coil) {
251- coil.style.transform = `translateY(${scrollY * 0.3}px)` ;
252- coil.style.opacity = String(opacity) ;
262+ coil.style.transform = 'translate3d(0, 0, 0)' ;
263+ coil.style.opacity = '0' ;
253264 }
254-
255- // Content moves faster (0.6x scroll speed) - feels closer
256265 if (content) {
257- content.style.transform = `translateY(${scrollY * 0.6}px)` ;
258- content.style.opacity = String(opacity) ;
266+ content.style.transform = 'translate3d(0, 0, 0)' ;
267+ content.style.opacity = '0' ;
259268 }
260-
261- // Scroll indicator fades out faster
262269 if (scrollIndicator) {
263- const scrollIndicatorOpacity = Math.max(0, 1 - scrollY / (heroHeight * 0.3));
264- scrollIndicator.style.transform = `translateY(${scrollY * 0.5}px)`;
265- scrollIndicator.style.opacity = String(scrollIndicatorOpacity);
270+ scrollIndicator.style.transform = 'translate3d(0, 0, 0)';
271+ scrollIndicator.style.opacity = '0';
266272 }
273+ isScrolling = false;
274+ return;
275+ }
276+
277+ // Calculate scroll progress (0 to 1)
278+ const scrollProgress = scrollY / heroHeight;
279+
280+ // Use integer pixel values to avoid sub-pixel rendering
281+ const coilY = Math.round(scrollY * 0.3);
282+ const contentY = Math.round(scrollY * 0.6);
283+ const scrollY_val = Math.round(scrollY * 0.5);
284+
285+ // Calculate fade-out opacity (starts fading at 20% scroll, fully faded at 60%)
286+ const fadeStart = 0.2;
287+ const fadeEnd = 0.6;
288+ const opacity = Math.max(0, Math.min(1, 1 - (scrollProgress - fadeStart) / (fadeEnd - fadeStart)));
289+
290+ // Scroll indicator fades faster (fully faded at 30%)
291+ const scrollIndicatorOpacity = Math.max(0, 1 - scrollProgress / 0.3);
292+
293+ // Update transforms and opacity
294+ if (coil) {
295+ coil.style.transform = `translate3d(0, ${coilY}px, 0)`;
296+ coil.style.opacity = String(opacity);
297+ }
298+ if (content) {
299+ content.style.transform = `translate3d(0, ${contentY}px, 0)`;
300+ content.style.opacity = String(opacity);
267301 }
302+ if (scrollIndicator) {
303+ scrollIndicator.style.transform = `translate3d(0, ${scrollY_val}px, 0)`;
304+ scrollIndicator.style.opacity = String(scrollIndicatorOpacity);
305+ }
306+
307+ lastScrollY = scrollY;
268308
269- ticking = false;
309+ // Continue loop while scrolling
310+ if (isScrolling) {
311+ requestAnimationFrame(updateParallax);
312+ }
270313 }
271314
272315 function onScroll() {
273- if (!ticking) {
316+ if (!isScrolling) {
317+ isScrolling = true;
274318 requestAnimationFrame(updateParallax);
275- ticking = true;
276319 }
277320 }
278321
279- window.addEventListener('scroll', onScroll, { passive: true });
322+ // Stop scrolling flag after scroll ends
323+ let scrollTimeout: number;
324+ function onScrollEnd() {
325+ clearTimeout(scrollTimeout);
326+ scrollTimeout = window.setTimeout(() => {
327+ isScrolling = false;
328+ }, 50);
329+ }
330+
331+ // Initial call
332+ updateParallax();
333+
334+ window.addEventListener('scroll', () => {
335+ onScroll();
336+ onScrollEnd();
337+ }, { passive: true });
280338
281- // Cleanup on page transition
339+ // Cleanup
282340 document.addEventListener('astro:before-swap', () => {
283341 window.removeEventListener('scroll', onScroll);
342+ clearTimeout(scrollTimeout);
284343 }, { once: true });
285344 }
286345
0 commit comments