From d9366c033e71ce685596a998f7d37918c62144a5 Mon Sep 17 00:00:00 2001 From: Mustafa Date: Thu, 30 Jul 2026 12:48:31 +0300 Subject: [PATCH] feat: scroll progress bar + tagline text on hero --- components/sections/CloudRevealSection.tsx | 153 ++++++++++++--------- 1 file changed, 85 insertions(+), 68 deletions(-) diff --git a/components/sections/CloudRevealSection.tsx b/components/sections/CloudRevealSection.tsx index 1d8c4e0..30725b0 100644 --- a/components/sections/CloudRevealSection.tsx +++ b/components/sections/CloudRevealSection.tsx @@ -2,6 +2,13 @@ import { useEffect, useRef, useState } from 'react' import { useTranslations } from 'next-intl' +import { + motion, + useScroll, + useTransform, + useMotionValueEvent, + useReducedMotion, +} from 'framer-motion' const TOTAL_FRAMES = 298 const FRAME_PATH = (n: number) => @@ -9,36 +16,52 @@ const FRAME_PATH = (n: number) => export default function CloudRevealSection() { const t = useTranslations('hero') + const prefersReducedMotion = useReducedMotion() const sectionRef = useRef(null) const canvasRef = useRef(null) - const textRef = useRef(null) const framesRef = useRef([]) const currentFrameRef = useRef(0) - const rafRef = useRef(null) const [loaded, setLoaded] = useState(false) const [loadProgress, setLoadProgress] = useState(0) - // Frame'leri sıralı preload et + const { scrollYProgress } = useScroll({ + target: sectionRef, + offset: ['start start', 'end end'], + }) + + // Tagline: ilk %15'te solar, aşağı kayar + const taglineOpacity = useTransform(scrollYProgress, [0, 0.15], [1, 0]) + const taglineY = useTransform(scrollYProgress, [0, 0.15], [0, 40]) + + // Progress bar: scroll'a göre genişler (0 → 100%) + const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1]) + + // Frame preload useEffect(() => { + if (prefersReducedMotion) return + const images: HTMLImageElement[] = new Array(TOTAL_FRAMES) let loadedCount = 0 + let cancelled = false for (let i = 0; i < TOTAL_FRAMES; i++) { const img = new Image() img.src = FRAME_PATH(i + 1) img.onload = () => { + if (cancelled) return loadedCount++ setLoadProgress(Math.round((loadedCount / TOTAL_FRAMES) * 100)) if (loadedCount === TOTAL_FRAMES) { framesRef.current = images setLoaded(true) - // İlk frame'i hemen çiz drawFrame(0, images) } } images[i] = img } - }, []) + + return () => { cancelled = true } + }, [prefersReducedMotion]) function drawFrame(index: number, images?: HTMLImageElement[]) { const canvas = canvasRef.current @@ -51,61 +74,24 @@ export default function CloudRevealSection() { ctx.drawImage(img, 0, 0, canvas.width, canvas.height) } - // Scroll handler - useEffect(() => { + useMotionValueEvent(scrollYProgress, 'change', (progress) => { if (!loaded) return - - const section = sectionRef.current - const text = textRef.current - if (!section || !text) return - - const handleScroll = () => { - const rect = section.getBoundingClientRect() - const sectionHeight = section.offsetHeight - const windowH = window.innerHeight - - const scrolled = -rect.top - const total = sectionHeight - windowH - const progress = Math.min(Math.max(scrolled / total, 0), 1) - - // Hangi frame'i göstereceğimizi hesapla - const frameIndex = Math.min( - Math.floor(progress * (TOTAL_FRAMES - 1)), - TOTAL_FRAMES - 1 - ) - - // Sadece frame değiştiyse çiz (RAF ile) - if (frameIndex !== currentFrameRef.current) { - currentFrameRef.current = frameIndex - if (rafRef.current) cancelAnimationFrame(rafRef.current) - rafRef.current = requestAnimationFrame(() => drawFrame(frameIndex)) - } - - // Tagline: ilk %15'te görünür, sonra solar - if (progress < 0.15) { - const opacity = 1 - progress / 0.15 - text.style.opacity = String(opacity) - text.style.transform = `translateY(${progress * 40}px)` - } else { - text.style.opacity = '0' - } + const frameIndex = Math.min( + Math.floor(progress * (TOTAL_FRAMES - 1)), + TOTAL_FRAMES - 1 + ) + if (frameIndex !== currentFrameRef.current) { + currentFrameRef.current = frameIndex + drawFrame(frameIndex) } + }) - window.addEventListener('scroll', handleScroll, { passive: true }) - return () => { - window.removeEventListener('scroll', handleScroll) - if (rafRef.current) cancelAnimationFrame(rafRef.current) - } - }, [loaded]) - - // Canvas boyutunu ekrana sığdır useEffect(() => { const canvas = canvasRef.current if (!canvas) return const resize = () => { canvas.width = window.innerWidth canvas.height = window.innerHeight - // Resize sonrası mevcut frame'i yeniden çiz drawFrame(currentFrameRef.current) } resize() @@ -113,18 +99,39 @@ export default function CloudRevealSection() { return () => window.removeEventListener('resize', resize) }, [loaded]) + if (prefersReducedMotion) { + return ( +
+
+
+

+ Vesta Muğla +

+

+ {t('tagline')} +

+

+ {t('subtitle')} +

+
+
+ ) + } + return (
-
+
- {/* Canvas — frame'ler buraya çizilir */} + {/* Canvas */} - {/* Loading ekranı */} + {/* Gradient — alt kısım okunabilir olsun */} +
+ + {/* Loading */} {!loaded && (
@@ -139,32 +146,42 @@ export default function CloudRevealSection() {
)} - {/* Tagline */} -
-

+

Vesta Muğla

{t('tagline')}

-

+

{t('subtitle')}

-
+ - {/* Scroll indicator */} + {/* Sağ alt — scroll göstergesi */} {loaded && ( -
- + + {t('scroll')} -
-
+
)} + + {/* Alt progress bar */} +
+ +
+
)