Files
vesta/components/sections/CloudRevealSection.tsx
T

384 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useRef, useState } from 'react'
import { useTranslations } from 'next-intl'
import {
motion,
useScroll,
useTransform,
useMotionValueEvent,
useReducedMotion,
} from 'framer-motion'
import UnitRevealOverlay from './UnitRevealOverlay'
const CDN = 'https://media.ayris.tech/t'
const HERO_VIDEO = `${CDN}/vesta/hero/hero.mp4`
// Scroll aralığına göre opacity döndüren helper
function usePhaseOpacity(
scrollYProgress: ReturnType<typeof useScroll>['scrollYProgress'],
inStart: number,
inEnd: number,
outStart: number,
outEnd: number
) {
return useTransform(
scrollYProgress,
[inStart, inEnd, outStart, outEnd],
[0, 1, 1, 0]
)
}
export default function CloudRevealSection() {
const t = useTranslations('hero')
const prefersReducedMotion = useReducedMotion()
const sectionRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const [loaded, setLoaded] = useState(false)
const [activeUnit, setActiveUnit] = useState<{ id: string; label: string; sub: string } | null>(null)
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ['start start', 'end end'],
})
// Progress bar
const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1])
// Logo: 08% 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: tagline (sol alt)
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: orta metin
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: stat rakamları
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: CTA
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])
// Video → canvas kurulumu
useEffect(() => {
if (prefersReducedMotion) return
const video = videoRef.current
const canvas = canvasRef.current
if (!video || !canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const draw = () => {
if (canvas.width === 0) {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
if (video.readyState >= 2) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
}
}
const onReady = () => { setLoaded(true); draw() }
video.addEventListener('seeked', draw)
video.addEventListener('loadeddata', onReady)
video.addEventListener('canplay', onReady)
// Yüklemeyi tetikle
video.load()
return () => {
video.removeEventListener('seeked', draw)
video.removeEventListener('loadeddata', onReady)
video.removeEventListener('canplay', onReady)
}
}, [prefersReducedMotion])
// Scroll → video currentTime
useMotionValueEvent(scrollYProgress, 'change', (progress) => {
const video = videoRef.current
if (!video || !video.duration) return
video.currentTime = progress * video.duration
})
// 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)
}, [])
// 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)
}, [])
if (prefersReducedMotion) {
return (
<div className="relative min-h-[100dvh] w-full overflow-hidden bg-black flex items-end">
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent" />
<div className="relative z-10 px-10 pb-24">
<p className="text-white/50 text-xs tracking-[0.4em] uppercase mb-4">Vesta Muğla</p>
<h1 className="text-white text-5xl md:text-7xl font-serif font-light leading-tight">{t('tagline')}</h1>
<p className="text-white/50 mt-4 text-base font-light max-w-md">{t('subtitle')}</p>
</div>
</div>
)
}
return (
<>
<div ref={sectionRef} style={{ height: '500vh' }}>
<div className="sticky top-0 min-h-[100dvh] w-full overflow-hidden bg-black">
{/* Video kaynağı — görünmez ama yüklenebilir */}
<video
ref={videoRef}
src={HERO_VIDEO}
preload="auto"
muted
playsInline
style={{ position: 'absolute', width: 1, height: 1, opacity: 0, pointerEvents: 'none' }}
/>
{/* Canvas */}
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
{/* Alt gradient */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none" />
{/* Loading */}
{!loaded && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black z-20">
<div className="w-48 h-px bg-white/10 mb-3">
<div className="h-full bg-white/60 transition-all duration-150 w-0 animate-pulse" />
</div>
<p className="text-white/30 text-xs tracking-widest uppercase">Yükleniyor</p>
</div>
)}
{/* LOGO — merkez, ilk frame'de görünür, scroll ile kaybolur */}
<motion.div
style={{ opacity: logoOpacity, scale: logoScale }}
className="absolute inset-0 flex flex-col items-center justify-center z-10 pointer-events-none"
>
<p className="text-white text-5xl md:text-7xl font-serif font-light tracking-[0.25em] drop-shadow-2xl">
VESTA
</p>
<div className="mt-3 w-16 h-px bg-white/40" />
<p className="text-white/60 text-xs tracking-[0.5em] uppercase mt-3">Muğla</p>
</motion.div>
{/* PHASE 1 — Sol alt: Ana tagline */}
<motion.div
style={{ opacity: phase1Opacity, y: phase1Y }}
className="absolute bottom-20 left-10 z-10 max-w-sm"
>
<p className="text-white/50 text-[10px] tracking-[0.4em] uppercase mb-3">Vesta Muğla</p>
<h1 className="text-white text-4xl md:text-5xl font-serif font-light leading-tight drop-shadow-2xl">
{t('tagline')}
</h1>
<p className="text-white/50 mt-3 text-sm font-light">{t('subtitle')}</p>
</motion.div>
{/* PHASE 2 — Merkez: Konum & kimlik */}
<motion.div
style={{ opacity: phase2Opacity, y: phase2Y }}
className="absolute inset-0 flex items-center justify-center z-10 pointer-events-none"
>
<div className="text-center px-6">
<p className="text-white/40 text-[10px] tracking-[0.5em] uppercase mb-5">Muğla · Türkiye</p>
<p className="text-white text-3xl md:text-5xl font-serif font-light leading-snug drop-shadow-2xl">
Bulutların altında,<br />
<span className="italic">ormanın içinde</span>
</p>
<div className="mt-6 w-12 h-px bg-white/30 mx-auto" />
</div>
</motion.div>
{/* PHASE 3 — Sağ: Stat rakamları */}
<motion.div
style={{ opacity: phase3Opacity, y: phase3Y }}
className="absolute right-10 top-1/2 -translate-y-1/2 z-10 flex flex-col gap-8 text-right"
>
{[
{ value: '120', label: 'Konut' },
{ value: '15.000', label: 'm² Alan' },
{ value: '%60', label: 'Yeşil Alan' },
].map((stat) => (
<div key={stat.label}>
<p className="text-white text-4xl md:text-5xl font-serif font-light drop-shadow-xl">{stat.value}</p>
<p className="text-white/40 text-xs tracking-widest uppercase mt-1">{stat.label}</p>
</div>
))}
</motion.div>
{/* PHASE 4 — Merkez alt: CTA */}
<motion.div
style={{ opacity: phase4Opacity, y: phase4Y }}
className="absolute bottom-24 left-0 right-0 z-10 flex flex-col items-center gap-4"
>
<p className="text-white/40 text-[10px] tracking-[0.5em] uppercase">Projeyi Keşfet</p>
<p className="text-white text-2xl md:text-3xl font-serif font-light text-center drop-shadow-xl">
Hayalinizdeki yaşam<br />bir adım uzağınızda
</p>
<div className="mt-2 w-px h-10 bg-white/30 animate-pulse" />
</motion.div>
{/* HOTSPOTS — son frame'de görünür */}
<UnitHotspots
scrollYProgress={scrollYProgress}
onSelect={(unit) => setActiveUnit(unit)}
/>
{/* Progress bar */}
<div className="absolute bottom-8 left-10 right-10 z-10 h-[2px] bg-white/10">
<motion.div className="h-full bg-white/60 origin-left" style={{ scaleX: progressScaleX }} />
</div>
</div>
</div>
{/* Unit reveal overlay */}
<UnitRevealOverlay
unitId={activeUnit?.id ?? null}
unitLabel={activeUnit?.label ?? ''}
unitSub={activeUnit?.sub ?? ''}
onClose={() => setActiveUnit(null)}
/>
</>
)
}
// ─── Hotspot bileşeni ────────────────────────────────────────────────────────
const HOTSPOTS: {
id: string
label: string
sub: string
x: number
y: number
dir: 'left' | 'right'
}[] = [
{ id: 'blok-a', label: '2+1 Tip A', sub: 'Blok A · 83.35 m²', x: 52, y: 28, dir: 'left' },
{ id: 'blok-b', label: '2+1 Tip B', sub: 'Blok B · 84 m²', x: 93, y: 30, dir: 'left' },
{ id: 'blok-c', label: '1+1', sub: 'Blok C · 60 m²', x: 11, y: 42, dir: 'right' },
]
// Zigzag SVG çizgisi — dot'tan karta uzanır
function ZigzagLine({ dir, delay }: { dir: 'left' | 'right'; delay: number }) {
// sağa giden: sol→sağ, sola giden: sağ→sol
const w = 72
const h = 24
// zigzag path: 3 segment, orta kısmı yukarı/aşağı kayar
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 (
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} fill="none" className="shrink-0">
<motion.path
d={path}
stroke="rgba(255,255,255,0.65)"
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ delay, duration: 0.7, ease: 'easeInOut' }}
/>
</svg>
)
}
function UnitHotspots({
scrollYProgress,
onSelect,
}: {
scrollYProgress: ReturnType<typeof useScroll>['scrollYProgress']
onSelect: (unit: { id: string; label: string; sub: string }) => void
}) {
const containerOpacity = useTransform(scrollYProgress, [0.92, 0.98], [0, 1])
return (
<motion.div
style={{ opacity: containerOpacity }}
className="absolute inset-0 z-20 pointer-events-none"
>
{HOTSPOTS.map((spot, i) => (
<div
key={spot.id}
className="absolute"
style={{ left: `${spot.x}%`, top: `${spot.y}%` }}
>
<div className={`flex items-center gap-0 ${spot.dir === 'left' ? 'flex-row-reverse' : 'flex-row'}`}>
{/* Pulsing dot */}
<span className="relative flex h-3 w-3 shrink-0">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-70" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-white" />
</span>
{/* Zigzag çizgi */}
<ZigzagLine dir={spot.dir} delay={i * 0.25 + 0.1} />
{/* Ok ucu */}
<motion.div
initial={{ opacity: 0, x: spot.dir === 'right' ? -4 : 4 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.25 + 0.7, duration: 0.2 }}
className="shrink-0"
>
{spot.dir === 'right'
? <svg width="6" height="10" viewBox="0 0 6 10" fill="none"><path d="M1 1l4 4-4 4" stroke="white" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>
: <svg width="6" height="10" viewBox="0 0 6 10" fill="none"><path d="M5 1L1 5l4 4" stroke="white" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>
}
</motion.div>
{/* Kart */}
<motion.button
initial={{ opacity: 0, x: spot.dir === 'right' ? -8 : 8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.25 + 0.75, duration: 0.3, ease: 'easeOut' }}
className="pointer-events-auto bg-black/60 backdrop-blur-md border border-white/20 rounded-lg px-3 py-2 text-left hover:bg-black/80 hover:border-white/40 transition-all duration-200 active:scale-95 cursor-pointer"
onClick={() => onSelect({ id: spot.id, label: spot.label, sub: spot.sub })}
>
<p className="text-white text-xs font-medium tracking-wide whitespace-nowrap">{spot.label}</p>
<p className="text-white/50 text-[10px] mt-0.5 whitespace-nowrap">{spot.sub}</p>
<div className="mt-1.5 flex items-center gap-1 text-white/40 text-[10px] tracking-widest uppercase">
<span>İncele</span>
<svg width="8" height="8" viewBox="0 0 8 8" fill="none">
<path d="M1 4h6M4 1l3 3-3 3" stroke="currentColor" strokeWidth="1" strokeLinecap="round"/>
</svg>
</div>
</motion.button>
</div>
</div>
))}
</motion.div>
)
}