'use client' import { useEffect, useRef, useState } from 'react' import { useTranslations } from 'next-intl' import { motion, useScroll, useTransform, useReducedMotion, } from 'framer-motion' import UnitRevealOverlay from './UnitRevealOverlay' 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 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 [activeUnit, setActiveUnit] = useState<{ id: string; label: string; sub: string } | null>(null) const { scrollYProgress } = useScroll({ target: sectionRef, offset: ['start start', 'end end'], }) const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1]) 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]) const phase1Opacity = useTransform(scrollYProgress, [0, 0.05, 0.12, 0.18], [0, 1, 1, 0]) 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]) 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]) 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]) // Canvas boyutu useEffect(() => { const canvas = canvasRef.current if (!canvas) return const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight } resize() window.addEventListener('resize', resize) return () => window.removeEventListener('resize', resize) }, []) // 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: BlobPart[] = [] 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 → 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 const maxY = section.offsetTop + section.offsetHeight - window.innerHeight if (window.scrollY > maxY) window.scrollTo({ top: maxY, behavior: 'instant' }) } window.addEventListener('scroll', lockScroll, { passive: true }) return () => window.removeEventListener('scroll', lockScroll) }, []) if (prefersReducedMotion) { return (

Vesta Muğla

{t('tagline')}

{t('subtitle')}

) } return ( <>
{/* Loading */} {!loaded && (

{loadProgress}%

)} {/* LOGO */}

VESTA

Muğla

{/* PHASE 1 */}

Vesta Muğla

{t('tagline')}

{t('subtitle')}

{/* PHASE 2 */}

Muğla · Türkiye

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

{/* PHASE 3 */} {[{ value: '120', label: 'Konut' }, { value: '15.000', label: 'm² Alan' }, { value: '%60', label: 'Yeşil Alan' }].map(stat => (

{stat.value}

{stat.label}

))}
{/* PHASE 4 */}

Projeyi Keşfet

Hayalinizdeki yaşam
bir adım uzağınızda

setActiveUnit(unit)} />
setActiveUnit(null)} /> ) } // ─── Hotspot bileşeni ──────────────────────────────────────────────────────── const HOTSPOTS = [ { id: 'blok-a', label: '2+1 Tip A', sub: 'Blok A · 83.35 m²', x: 52, y: 28, dir: 'left' as const }, { id: 'blok-b', label: '2+1 Tip B', sub: 'Blok B · 84 m²', x: 93, y: 30, dir: 'left' as const }, { id: 'blok-c', label: '1+1', sub: 'Blok C · 60 m²', x: 11, y: 42, dir: 'right' as const }, ] function ZigzagLine({ dir, delay }: { dir: 'left' | 'right'; delay: number }) { const w = 72, h = 24 const path = dir === 'right' ? `M0,${h/2} L${w*0.3},${h/2} L${w*0.45},4 L${w*0.6},${h-4} L${w*0.75},${h/2} L${w},${h/2}` : `M${w},${h/2} L${w*0.7},${h/2} L${w*0.55},4 L${w*0.4},${h-4} L${w*0.25},${h/2} L0,${h/2}` return ( ) } 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) => (
{spot.dir === 'right' ? : } onSelect({ id: spot.id, label: spot.label, sub: spot.sub })}>

{spot.label}

{spot.sub}

İncele
))}
) }