Files
vesta/components/sections/CloudRevealSection.tsx
T

239 lines
8.6 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'
const TOTAL_FRAMES = 298
const FRAME_PATH = (n: number) =>
`/frames/ezgif-frame-${String(n).padStart(3, '0')}.jpg`
// 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 framesRef = useRef<HTMLImageElement[]>([])
const currentFrameRef = useRef(0)
const [loaded, setLoaded] = useState(false)
const [loadProgress, setLoadProgress] = useState(0)
const hasSnapped = useRef(false)
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ['start start', 'end end'],
})
// Progress bar
const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1])
// --- Text phase opacities ---
// Phase 1: 015% → 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: 2045% → orta metin (merkez)
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: 5072% → stat kartları (sağ)
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: 78100% → CTA (merkez alt)
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])
// 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)
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)
}
useMotionValueEvent(scrollYProgress, 'change', (progress) => {
if (!loaded) return
// Frame güncelle
const frameIndex = Math.min(Math.floor(progress * (TOTAL_FRAMES - 1)), TOTAL_FRAMES - 1)
if (frameIndex !== currentFrameRef.current) {
currentFrameRef.current = frameIndex
drawFrame(frameIndex)
}
// %100'de bir sonraki bölüme snap et
if (progress >= 0.99 && !hasSnapped.current) {
hasSnapped.current = true
const next = document.getElementById('proje')
if (next) {
next.scrollIntoView({ behavior: 'smooth' })
}
}
// Geri scroll yapılırsa snap'i sıfırla
if (progress < 0.9) {
hasSnapped.current = false
}
})
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 (
<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">
{/* 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" style={{ width: `${loadProgress}%` }} />
</div>
<p className="text-white/30 text-xs tracking-widest uppercase">{loadProgress}%</p>
</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>
{/* 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>
)
}