diff --git a/components/sections/CloudRevealSection.tsx b/components/sections/CloudRevealSection.tsx index c2f1dfe..31abc46 100644 --- a/components/sections/CloudRevealSection.tsx +++ b/components/sections/CloudRevealSection.tsx @@ -6,40 +6,22 @@ import { motion, useScroll, useTransform, - useMotionValueEvent, useReducedMotion, } from 'framer-motion' import UnitRevealOverlay from './UnitRevealOverlay' -const TOTAL_FRAMES = 150 -const FRAME_PATH = (n: number) => - `/frames/frame-${String(n).padStart(3, '0')}.webp` - -// Scroll aralığına göre opacity döndüren helper -function usePhaseOpacity( - scrollYProgress: ReturnType['scrollYProgress'], - inStart: number, - inEnd: number, - outStart: number, - outEnd: number -) { - return useTransform( - scrollYProgress, - [inStart, inEnd, outStart, outEnd], - [0, 1, 1, 0] - ) -} +const HERO_VIDEO = '/hero/hero.mp4' export default function CloudRevealSection() { const t = useTranslations('hero') const prefersReducedMotion = useReducedMotion() const sectionRef = useRef(null) const canvasRef = useRef(null) - const framesRef = useRef([]) - const currentFrameRef = useRef(0) + const videoRef = useRef(null) + const curTimeRef = useRef(0) // lerp'in anlık değeri + const targetTimeRef = useRef(0) // scroll'un hedef değeri const [loaded, setLoaded] = useState(false) const [loadProgress, setLoadProgress] = useState(0) - const hasSnapped = useRef(false) const [activeUnit, setActiveUnit] = useState<{ id: string; label: string; sub: string } | null>(null) const { scrollYProgress } = useScroll({ @@ -47,103 +29,118 @@ export default function CloudRevealSection() { offset: ['start start', 'end end'], }) - // Progress bar const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1]) - - // --- Text phase opacities --- - // Logo: 0–8% görünür, sonra kaybolur const logoOpacity = useTransform(scrollYProgress, [0, 0.02, 0.08, 0.14], [0, 1, 1, 0]) - const logoScale = useTransform(scrollYProgress, [0, 0.02], [0.92, 1]) - - // Phase 1: 0–15% → tagline (sol alt) + const logoScale = useTransform(scrollYProgress, [0, 0.02], [0.92, 1]) const phase1Opacity = useTransform(scrollYProgress, [0, 0.05, 0.12, 0.18], [0, 1, 1, 0]) - const phase1Y = useTransform(scrollYProgress, [0, 0.05], [20, 0]) - - // Phase 2: 20–45% → orta metin (merkez) + const phase1Y = useTransform(scrollYProgress, [0, 0.05], [20, 0]) const phase2Opacity = useTransform(scrollYProgress, [0.20, 0.28, 0.40, 0.48], [0, 1, 1, 0]) - const phase2Y = useTransform(scrollYProgress, [0.20, 0.28], [30, 0]) - - // Phase 3: 50–72% → stat kartları (sağ) + const phase2Y = useTransform(scrollYProgress, [0.20, 0.28], [30, 0]) const phase3Opacity = useTransform(scrollYProgress, [0.50, 0.57, 0.68, 0.75], [0, 1, 1, 0]) - const phase3Y = useTransform(scrollYProgress, [0.50, 0.57], [30, 0]) - - // Phase 4: 78–100% → CTA (merkez alt) + const phase3Y = useTransform(scrollYProgress, [0.50, 0.57], [30, 0]) const phase4Opacity = useTransform(scrollYProgress, [0.78, 0.85, 0.95, 1], [0, 1, 1, 0]) - const phase4Y = useTransform(scrollYProgress, [0.78, 0.85], [30, 0]) + const phase4Y = useTransform(scrollYProgress, [0.78, 0.85], [30, 0]) - // Frame preload + // Canvas boyutu 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) - drawFrame(0, images) - } - } - images[i] = img - } - return () => { cancelled = true } - }, [prefersReducedMotion]) - - function drawFrame(index: number, images?: HTMLImageElement[]) { const canvas = canvasRef.current if (!canvas) return - const ctx = canvas.getContext('2d') - if (!ctx) return - const imgs = images || framesRef.current - const img = imgs[index] - if (!img || !img.complete || img.naturalWidth === 0) return - ctx.drawImage(img, 0, 0, canvas.width, canvas.height) - } + const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight } + resize() + window.addEventListener('resize', resize) + return () => window.removeEventListener('resize', resize) + }, []) - useMotionValueEvent(scrollYProgress, 'change', (progress) => { - if (!loaded) return - const frameIndex = Math.min(Math.floor(progress * (TOTAL_FRAMES - 1)), TOTAL_FRAMES - 1) - if (frameIndex !== currentFrameRef.current) { - currentFrameRef.current = frameIndex - drawFrame(frameIndex) + // Blob yükle → video → RAF loop + useEffect(() => { + if (prefersReducedMotion) return + let cancelled = false + let rafId: number + + const canvas = canvasRef.current + const ctx = canvas?.getContext('2d') + if (!canvas || !ctx) return + + // Blob fetch — progress ile + fetch(HERO_VIDEO) + .then(res => { + if (!res.ok) throw new Error('fetch failed') + const total = Number(res.headers.get('content-length') || 0) + const reader = res.body!.getReader() + const chunks: Uint8Array[] = [] + let received = 0 + + const pump = (): Promise => + reader.read().then(({ done, value }) => { + if (done) return new Blob(chunks, { type: 'video/mp4' }) + chunks.push(value!) + received += value!.length + if (total > 0) setLoadProgress(Math.round((received / total) * 100)) + return pump() + }) + return pump() + }) + .then(blob => { + if (cancelled) return + const url = URL.createObjectURL(blob) + const v = document.createElement('video') + v.muted = true + v.playsInline = true + v.preload = 'auto' + v.src = url + + v.addEventListener('loadedmetadata', () => { + if (cancelled) return + videoRef.current = v + setLoaded(true) + + // RAF loop: lerp curTime → targetTime, canvas'a çiz + const loop = () => { + if (v.readyState >= 2) { + const eps = 0.008 + curTimeRef.current += (targetTimeRef.current - curTimeRef.current) * 0.18 + const t = Math.max(0, Math.min(curTimeRef.current, v.duration * 0.999)) + if (!v.seeking && Math.abs(v.currentTime - t) > eps) { + try { v.currentTime = t } catch (_) {} + } + ctx.drawImage(v, 0, 0, canvas.width, canvas.height) + } + rafId = requestAnimationFrame(loop) + } + rafId = requestAnimationFrame(loop) + }) + }) + .catch(() => {}) // sessizce yoksay + + return () => { + cancelled = true + cancelAnimationFrame(rafId) + if (videoRef.current?.src?.startsWith('blob:')) URL.revokeObjectURL(videoRef.current.src) + videoRef.current = null } - }) + }, [prefersReducedMotion]) - // Scroll pozisyonunu hero sonunda kilitle + // Scroll → targetTime + useEffect(() => { + return scrollYProgress.on('change', (progress) => { + const v = videoRef.current + if (!v || !v.duration) return + targetTimeRef.current = progress * v.duration + }) + }, [scrollYProgress]) + + // Scroll kilidi useEffect(() => { const lockScroll = () => { const section = sectionRef.current if (!section) return - // Hero'nun scroll edebileceği maksimum pozisyon const maxY = section.offsetTop + section.offsetHeight - window.innerHeight - if (window.scrollY > maxY) { - window.scrollTo({ top: maxY, behavior: 'instant' }) - } + if (window.scrollY > maxY) window.scrollTo({ top: maxY, behavior: 'instant' }) } window.addEventListener('scroll', lockScroll, { passive: true }) return () => window.removeEventListener('scroll', lockScroll) }, []) - useEffect(() => { - const canvas = canvasRef.current - if (!canvas) return - const resize = () => { - canvas.width = window.innerWidth - canvas.height = window.innerHeight - drawFrame(currentFrameRef.current) - } - resize() - window.addEventListener('resize', resize) - return () => window.removeEventListener('resize', resize) - }, [loaded]) - if (prefersReducedMotion) { return (
@@ -162,10 +159,7 @@ export default function CloudRevealSection() {
- {/* Canvas */} - - {/* Alt gradient */}
{/* Loading */} @@ -178,55 +172,37 @@ export default function CloudRevealSection() {
)} - {/* LOGO — merkez, ilk frame'de görünür, scroll ile kaybolur */} + {/* LOGO */} -

- VESTA -

+

VESTA

Muğla

- {/* PHASE 1 — Sol alt: Ana tagline */} - + {/* PHASE 1 */} +

Vesta Muğla

-

- {t('tagline')} -

+

{t('tagline')}

{t('subtitle')}

- {/* PHASE 2 — Merkez: Konum & kimlik */} - + {/* PHASE 2 */} +

Muğla · Türkiye

- Bulutların altında,
- ormanın içinde + Bulutların altında,
ormanın içinde

- {/* PHASE 3 — Sağ: Stat rakamları */} - - {[ - { value: '120', label: 'Konut' }, - { value: '15.000', label: 'm² Alan' }, - { value: '%60', label: 'Yeşil Alan' }, - ].map((stat) => ( + {/* PHASE 3 */} + + {[{ value: '120', label: 'Konut' }, { value: '15.000', label: 'm² Alan' }, { value: '%60', label: 'Yeşil Alan' }].map(stat => (

{stat.value}

{stat.label}

@@ -234,11 +210,8 @@ export default function CloudRevealSection() { ))} - {/* PHASE 4 — Merkez alt: CTA */} - + {/* PHASE 4 */} +

Projeyi Keşfet

Hayalinizdeki yaşam
bir adım uzağınızda @@ -246,21 +219,14 @@ export default function CloudRevealSection() {

- {/* HOTSPOTS — son frame'de görünür */} - setActiveUnit(unit)} - /> + setActiveUnit(unit)} /> - {/* Progress bar */}
-
- {/* Unit reveal overlay */} - + ) } -function UnitHotspots({ - scrollYProgress, - onSelect, -}: { +function UnitHotspots({ scrollYProgress, onSelect }: { scrollYProgress: ReturnType['scrollYProgress'] onSelect: (unit: { id: string; label: string; sub: string }) => void }) { const containerOpacity = useTransform(scrollYProgress, [0.92, 0.98], [0, 1]) - return ( - + {HOTSPOTS.map((spot, i) => ( -
+
- - {/* Pulsing dot */} - - {/* Zigzag çizgi */} - - {/* Ok ucu */} - + {spot.dir === 'right' ? : } - - {/* Kart */} - onSelect({ id: spot.id, label: spot.label, sub: spot.sub })} - > + onClick={() => onSelect({ id: spot.id, label: spot.label, sub: spot.sub })}>

{spot.label}

{spot.sub}

İncele - - - +
-
))} diff --git a/public/hero/hero.mp4 b/public/hero/hero.mp4 new file mode 100644 index 0000000..b784959 Binary files /dev/null and b/public/hero/hero.mp4 differ