215 lines
6.5 KiB
TypeScript
215 lines
6.5 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
import { X } from 'lucide-react'
|
|
|
|
const UNIT_FRAMES = 120
|
|
const CDN = 'https://media.ayris.tech/t'
|
|
|
|
// Video kullanan birimler
|
|
const VIDEO_UNITS: Record<string, string> = {
|
|
'blok-a': `${CDN}/vesta/unit-videos/blok-a.mp4`,
|
|
}
|
|
|
|
interface UnitRevealOverlayProps {
|
|
unitId: string | null
|
|
unitLabel: string
|
|
unitSub: string
|
|
onClose: () => void
|
|
}
|
|
|
|
export default function UnitRevealOverlay({
|
|
unitId,
|
|
unitLabel,
|
|
unitSub,
|
|
onClose,
|
|
}: UnitRevealOverlayProps) {
|
|
const isVideo = unitId ? !!VIDEO_UNITS[unitId] : false
|
|
|
|
// ── Video mode ──────────────────────────────────────────────────
|
|
const videoRef = useRef<HTMLVideoElement>(null)
|
|
const [videoFinished, setVideoFinished] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!unitId || !isVideo) return
|
|
setVideoFinished(false)
|
|
const v = videoRef.current
|
|
if (!v) return
|
|
v.currentTime = 0
|
|
v.play().catch(() => {})
|
|
}, [unitId, isVideo])
|
|
|
|
// ── Frame mode ───────────────────────────────────────────────────
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
const framesRef = useRef<HTMLImageElement[]>([])
|
|
const rafRef = useRef<number | null>(null)
|
|
const [loadProgress, setLoadProgress] = useState(0)
|
|
const [loaded, setLoaded] = useState(false)
|
|
const [finished, setFinished] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!unitId || isVideo) return
|
|
|
|
setLoaded(false)
|
|
setFinished(false)
|
|
setLoadProgress(0)
|
|
framesRef.current = []
|
|
|
|
const images: HTMLImageElement[] = new Array(UNIT_FRAMES)
|
|
let loadedCount = 0
|
|
let cancelled = false
|
|
|
|
for (let i = 0; i < UNIT_FRAMES; i++) {
|
|
const img = new Image()
|
|
img.src = `${CDN}/vesta/unit-frames/${unitId}/ezgif-frame-${String(i + 1).padStart(3, '0')}.jpg`
|
|
img.onload = () => {
|
|
if (cancelled) return
|
|
loadedCount++
|
|
setLoadProgress(Math.round((loadedCount / UNIT_FRAMES) * 100))
|
|
if (loadedCount === UNIT_FRAMES) {
|
|
framesRef.current = images
|
|
setLoaded(true)
|
|
}
|
|
}
|
|
images[i] = img
|
|
}
|
|
|
|
return () => { cancelled = true }
|
|
}, [unitId, isVideo])
|
|
|
|
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)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!loaded || isVideo) return
|
|
|
|
const canvas = canvasRef.current
|
|
const ctx = canvas?.getContext('2d')
|
|
if (!canvas || !ctx) return
|
|
|
|
let frame = 0
|
|
const fps = 24
|
|
const interval = 1000 / fps
|
|
let last = 0
|
|
|
|
const animate = (ts: number) => {
|
|
if (ts - last >= interval) {
|
|
const img = framesRef.current[frame]
|
|
if (img?.complete && img.naturalWidth > 0) {
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
}
|
|
last = ts
|
|
if (frame < UNIT_FRAMES - 1) {
|
|
frame++
|
|
} else {
|
|
setFinished(true)
|
|
return
|
|
}
|
|
}
|
|
rafRef.current = requestAnimationFrame(animate)
|
|
}
|
|
|
|
rafRef.current = requestAnimationFrame(animate)
|
|
return () => {
|
|
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
|
}
|
|
}, [loaded, isVideo])
|
|
|
|
// ESC kapat
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose()
|
|
}
|
|
window.addEventListener('keydown', onKey)
|
|
return () => window.removeEventListener('keydown', onKey)
|
|
}, [onClose])
|
|
|
|
const showInfo = isVideo ? videoFinished : finished
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{unitId && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.4 }}
|
|
className="fixed inset-0 z-50 bg-black"
|
|
>
|
|
{/* Video modu */}
|
|
{isVideo && (
|
|
<video
|
|
ref={videoRef}
|
|
src={VIDEO_UNITS[unitId!]}
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
playsInline
|
|
muted
|
|
onEnded={() => setVideoFinished(true)}
|
|
/>
|
|
)}
|
|
|
|
{/* Frame modu */}
|
|
{!isVideo && (
|
|
<>
|
|
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
|
{!loaded && (
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black z-10">
|
|
<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>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Kapat */}
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute top-6 right-6 z-20 flex items-center gap-2 bg-black/40 backdrop-blur-sm border border-white/20 text-white/70 hover:text-white hover:bg-black/60 transition-all rounded-full px-4 py-2 text-sm"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
Geri
|
|
</button>
|
|
|
|
{/* Bitiş bilgisi */}
|
|
<AnimatePresence>
|
|
{showInfo && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
className="absolute bottom-16 left-10 z-20"
|
|
>
|
|
<p className="text-white/50 text-[10px] tracking-[0.4em] uppercase mb-2">Vesta Muğla</p>
|
|
<h2 className="text-white text-3xl md:text-4xl font-serif font-light">{unitLabel}</h2>
|
|
<p className="text-white/50 mt-1 text-sm">{unitSub}</p>
|
|
<button
|
|
onClick={onClose}
|
|
className="mt-5 flex items-center gap-2 text-white/60 hover:text-white text-xs tracking-widest uppercase transition-colors"
|
|
>
|
|
<span>← Projeye Dön</span>
|
|
</button>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|