Skip to content

Commit 6623869

Browse files
authored
Optimize animation performance: parallax scroll and image compression (#6)
- Replace JPG images with WebP format for better compression - Optimize parallax implementation with GPU-accelerated transforms - Use translate3d() and integer pixel values to eliminate jitter - Add continuous RAF loop for smoother scroll tracking - Restore fade-out opacity effect during parallax scroll - Reduce cloud layers from 3 to 2 for better performance - Add backface-visibility hidden for GPU compositing - Cache hero height with debounced resize handler
1 parent 80a233b commit 6623869

File tree

9 files changed

+94
-41
lines changed

9 files changed

+94
-41
lines changed

public/images/cloud-1.jpg

-51.5 KB
Binary file not shown.

public/images/cloud-1.webp

24.1 KB
Loading

public/images/cloud-2.jpg

-101 KB
Binary file not shown.

public/images/cloud-2.webp

32 KB
Loading

public/images/mountain-ranges.jpg

-552 KB
Binary file not shown.

public/images/mountain-ranges.webp

34.3 KB
Loading

src/components/AnimatedClouds.astro

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
height: 100%;
2121
width: 200vw;
2222
background-repeat: repeat-x;
23-
background-size: 100vw 100%; /* Each tile = viewport width */
24-
mix-blend-mode: overlay;
23+
background-size: contain; /* Each tile = viewport width */
24+
/*mix-blend-mode: overlay;*/
2525
will-change: transform;
2626
transform: translateZ(0);
27-
opacity: 0.3;
27+
opacity: 0.5;
2828
}
2929

3030
.animate-fog-slow {
@@ -47,9 +47,9 @@
4747

4848
<div class="absolute inset-0 w-full h-full pointer-events-none overflow-hidden">
4949
<!-- Cloud Layer 1 - Slow -->
50-
<div class="fog-layer animate-fog-slow bg-[url('/images/cloud-1.jpg')]"></div>
50+
<div class="fog-layer animate-fog-slow bg-[url('/images/cloud-1.webp')]"></div>
5151
<!-- Cloud Layer 2 - Fast -->
52-
<div class="fog-layer fog-layer-fast animate-fog-fast bg-[url('/images/cloud-2.jpg')]"></div>
52+
<!-- <div class="fog-layer fog-layer-fast animate-fog-fast bg-[url('/images/cloud-2.jpg')]"></div> -->
5353
<!-- Cloud Layer 3 - Medium -->
54-
<div class="fog-layer fog-layer-medium animate-fog-medium bg-[url('/images/cloud-1.jpg')]"></div>
54+
<div class="fog-layer fog-layer-medium animate-fog-medium bg-[url('/images/cloud-2.webp')]"></div>
5555
</div>

src/components/BackgroundLayers.astro

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,11 @@ import AnimatedGrid from './AnimatedGrid.astro';
66

77
<div class="fixed inset-0 z-0">
88
<!-- Background Image: Pink Mountain Dawn -->
9-
<div class="absolute inset-0 bg-[url('/images/mountain-ranges.jpg')] bg-cover bg-center"></div>
9+
<div class="absolute inset-0 bg-[url('/images/mountain-ranges.webp')] bg-cover bg-center"></div>
1010

1111
<!-- Animated Cloud Layers -->
1212
<AnimatedClouds />
1313

14-
<!-- Gradient Overlay for Contrast -->
15-
<div class="absolute inset-0 bg-linear-to-b from-[#795364] via-[#594848]/60 to-[#321824]/50 mix-blend-multiply"></div>
16-
17-
<!-- Noise Texture for film grain feel - using CSS grain effect instead of SVG filter -->
18-
<div class="absolute inset-0 w-full h-full pointer-events-none" style="background-image: url('data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22><filter id=%22n%22><feTurbulence type=%22fractalNoise%22 baseFrequency=%22.5%22 numOctaves=%221%22 stitchTiles=%22stitch%22/></filter><rect width=%22100%22 height=%22100%22 filter=%22url(%23n)%22 opacity=%22.05%22/></svg>'); opacity: 0.05;"></div>
19-
2014
<!-- Golden Ratio Grid Overlay -->
2115
<AnimatedGrid />
2216
</div>

src/components/Hero.astro

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)