feat: blok-a video, zigzag hotspots, logo intro, updated positions
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
|||||||
} from 'framer-motion'
|
} from 'framer-motion'
|
||||||
import UnitRevealOverlay from './UnitRevealOverlay'
|
import UnitRevealOverlay from './UnitRevealOverlay'
|
||||||
|
|
||||||
const TOTAL_FRAMES = 299
|
const TOTAL_FRAMES = 298
|
||||||
const FRAME_PATH = (n: number) =>
|
const FRAME_PATH = (n: number) =>
|
||||||
`/frames/ezgif-frame-${String(n).padStart(3, '0')}.jpg`
|
`/frames/ezgif-frame-${String(n).padStart(3, '0')}.jpg`
|
||||||
|
|
||||||
@@ -51,6 +51,10 @@ export default function CloudRevealSection() {
|
|||||||
const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1])
|
const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1])
|
||||||
|
|
||||||
// --- Text phase opacities ---
|
// --- 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)
|
// Phase 1: 0–15% → tagline (sol alt)
|
||||||
const phase1Opacity = useTransform(scrollYProgress, [0, 0.05, 0.12, 0.18], [0, 1, 1, 0])
|
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 phase1Y = useTransform(scrollYProgress, [0, 0.05], [20, 0])
|
||||||
@@ -174,6 +178,18 @@ export default function CloudRevealSection() {
|
|||||||
</div>
|
</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 */}
|
{/* PHASE 1 — Sol alt: Ana tagline */}
|
||||||
<motion.div
|
<motion.div
|
||||||
style={{ opacity: phase1Opacity, y: phase1Y }}
|
style={{ opacity: phase1Opacity, y: phase1Y }}
|
||||||
@@ -261,16 +277,41 @@ const HOTSPOTS: {
|
|||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
sub: string
|
sub: string
|
||||||
x: number // dot pozisyonu — ekranın % yatay
|
x: number
|
||||||
y: number // dot pozisyonu — ekranın % dikey
|
y: number
|
||||||
dir: 'left' | 'right' // kartın hangi tarafa uzayacağı
|
dir: 'left' | 'right'
|
||||||
}[] = [
|
}[] = [
|
||||||
{ id: 'blok-a', label: '2+1 Daire', sub: 'Blok A · 89 m²', x: 34, y: 22, dir: 'left' },
|
{ 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 Daire', sub: 'Blok B · 84 m²', x: 70, y: 19, dir: 'right' },
|
{ 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 Daire', sub: 'Blok C · 60 m²', x: 50, y: 55, dir: 'right' },
|
{ id: 'blok-c', label: '1+1', sub: 'Blok C · 60 m²', x: 11, y: 42, dir: 'right' },
|
||||||
{ id: 'blok-d', label: '1+1 Daire', sub: 'Blok D · 47 m²', x: 20, y: 65, dir: 'left' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 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({
|
function UnitHotspots({
|
||||||
scrollYProgress,
|
scrollYProgress,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -291,7 +332,6 @@ function UnitHotspots({
|
|||||||
className="absolute"
|
className="absolute"
|
||||||
style={{ left: `${spot.x}%`, top: `${spot.y}%` }}
|
style={{ left: `${spot.x}%`, top: `${spot.y}%` }}
|
||||||
>
|
>
|
||||||
{/* Dot + çizgi + kart — yatay flex */}
|
|
||||||
<div className={`flex items-center gap-0 ${spot.dir === 'left' ? 'flex-row-reverse' : 'flex-row'}`}>
|
<div className={`flex items-center gap-0 ${spot.dir === 'left' ? 'flex-row-reverse' : 'flex-row'}`}>
|
||||||
|
|
||||||
{/* Pulsing dot */}
|
{/* Pulsing dot */}
|
||||||
@@ -300,24 +340,19 @@ function UnitHotspots({
|
|||||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-white" />
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-white" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Animated çizgi */}
|
{/* Zigzag çizgi */}
|
||||||
<motion.div
|
<ZigzagLine dir={spot.dir} delay={i * 0.25 + 0.1} />
|
||||||
className="h-px bg-white/60 shrink-0"
|
|
||||||
initial={{ width: 0 }}
|
|
||||||
animate={{ width: 48 }}
|
|
||||||
transition={{ delay: i * 0.2 + 0.1, duration: 0.4, ease: 'easeOut' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Ok ucu */}
|
{/* Ok ucu */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: spot.dir === 'right' ? -4 : 4 }}
|
initial={{ opacity: 0, x: spot.dir === 'right' ? -4 : 4 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: i * 0.2 + 0.4, duration: 0.2 }}
|
transition={{ delay: i * 0.25 + 0.7, duration: 0.2 }}
|
||||||
className="shrink-0 text-white/60"
|
className="shrink-0"
|
||||||
>
|
>
|
||||||
{spot.dir === 'right'
|
{spot.dir === 'right'
|
||||||
? <svg width="6" height="8" viewBox="0 0 6 8" fill="none"><path d="M1 1l4 3-4 3" stroke="white" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
? <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="8" viewBox="0 0 6 8" fill="none"><path d="M5 1L1 4l4 3" stroke="white" strokeWidth="1.2" 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>
|
</motion.div>
|
||||||
|
|
||||||
@@ -325,7 +360,7 @@ function UnitHotspots({
|
|||||||
<motion.button
|
<motion.button
|
||||||
initial={{ opacity: 0, x: spot.dir === 'right' ? -8 : 8 }}
|
initial={{ opacity: 0, x: spot.dir === 'right' ? -8 : 8 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: i * 0.2 + 0.45, duration: 0.3, ease: 'easeOut' }}
|
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"
|
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 })}
|
onClick={() => onSelect({ id: spot.id, label: spot.label, sub: spot.sub })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import { X } from 'lucide-react'
|
|||||||
|
|
||||||
const UNIT_FRAMES = 120
|
const UNIT_FRAMES = 120
|
||||||
|
|
||||||
|
// Video kullanan birimler
|
||||||
|
const VIDEO_UNITS: Record<string, string> = {
|
||||||
|
'blok-a': '/unit-videos/blok-a.mp4',
|
||||||
|
}
|
||||||
|
|
||||||
interface UnitRevealOverlayProps {
|
interface UnitRevealOverlayProps {
|
||||||
unitId: string | null
|
unitId: string | null
|
||||||
unitLabel: string
|
unitLabel: string
|
||||||
@@ -19,6 +24,22 @@ export default function UnitRevealOverlay({
|
|||||||
unitSub,
|
unitSub,
|
||||||
onClose,
|
onClose,
|
||||||
}: UnitRevealOverlayProps) {
|
}: 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 canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const framesRef = useRef<HTMLImageElement[]>([])
|
const framesRef = useRef<HTMLImageElement[]>([])
|
||||||
const rafRef = useRef<number | null>(null)
|
const rafRef = useRef<number | null>(null)
|
||||||
@@ -26,9 +47,8 @@ export default function UnitRevealOverlay({
|
|||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
const [finished, setFinished] = useState(false)
|
const [finished, setFinished] = useState(false)
|
||||||
|
|
||||||
// Frame'leri preload et
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!unitId) return
|
if (!unitId || isVideo) return
|
||||||
|
|
||||||
setLoaded(false)
|
setLoaded(false)
|
||||||
setFinished(false)
|
setFinished(false)
|
||||||
@@ -55,9 +75,8 @@ export default function UnitRevealOverlay({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [unitId])
|
}, [unitId, isVideo])
|
||||||
|
|
||||||
// Canvas boyutu
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current
|
||||||
if (!canvas) return
|
if (!canvas) return
|
||||||
@@ -70,9 +89,8 @@ export default function UnitRevealOverlay({
|
|||||||
return () => window.removeEventListener('resize', resize)
|
return () => window.removeEventListener('resize', resize)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Yüklenince otomatik oynat
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loaded) return
|
if (!loaded || isVideo) return
|
||||||
|
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current
|
||||||
const ctx = canvas?.getContext('2d')
|
const ctx = canvas?.getContext('2d')
|
||||||
@@ -83,22 +101,18 @@ export default function UnitRevealOverlay({
|
|||||||
const interval = 1000 / fps
|
const interval = 1000 / fps
|
||||||
let last = 0
|
let last = 0
|
||||||
|
|
||||||
const draw = (imgs: HTMLImageElement[], index: number) => {
|
|
||||||
const img = imgs[index]
|
|
||||||
if (img?.complete && img.naturalWidth > 0) {
|
|
||||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const animate = (ts: number) => {
|
const animate = (ts: number) => {
|
||||||
if (ts - last >= interval) {
|
if (ts - last >= interval) {
|
||||||
draw(framesRef.current, frame)
|
const img = framesRef.current[frame]
|
||||||
|
if (img?.complete && img.naturalWidth > 0) {
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||||
|
}
|
||||||
last = ts
|
last = ts
|
||||||
if (frame < UNIT_FRAMES - 1) {
|
if (frame < UNIT_FRAMES - 1) {
|
||||||
frame++
|
frame++
|
||||||
} else {
|
} else {
|
||||||
setFinished(true)
|
setFinished(true)
|
||||||
return // son frame'de dur
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rafRef.current = requestAnimationFrame(animate)
|
rafRef.current = requestAnimationFrame(animate)
|
||||||
@@ -108,9 +122,9 @@ export default function UnitRevealOverlay({
|
|||||||
return () => {
|
return () => {
|
||||||
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
||||||
}
|
}
|
||||||
}, [loaded])
|
}, [loaded, isVideo])
|
||||||
|
|
||||||
// ESC ile kapat
|
// ESC kapat
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') onClose()
|
if (e.key === 'Escape') onClose()
|
||||||
@@ -119,6 +133,8 @@ export default function UnitRevealOverlay({
|
|||||||
return () => window.removeEventListener('keydown', onKey)
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
}, [onClose])
|
}, [onClose])
|
||||||
|
|
||||||
|
const showInfo = isVideo ? videoFinished : finished
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{unitId && (
|
{unitId && (
|
||||||
@@ -129,20 +145,34 @@ export default function UnitRevealOverlay({
|
|||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
className="fixed inset-0 z-50 bg-black"
|
className="fixed inset-0 z-50 bg-black"
|
||||||
>
|
>
|
||||||
{/* Canvas */}
|
{/* Video modu */}
|
||||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
{isVideo && (
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={VIDEO_UNITS[unitId!]}
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
|
playsInline
|
||||||
|
muted
|
||||||
|
onEnded={() => setVideoFinished(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Loading */}
|
{/* Frame modu */}
|
||||||
{!loaded && (
|
{!isVideo && (
|
||||||
<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">
|
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||||
<div
|
{!loaded && (
|
||||||
className="h-full bg-white/60 transition-all duration-150"
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black z-10">
|
||||||
style={{ width: `${loadProgress}%` }}
|
<div className="w-48 h-px bg-white/10 mb-3">
|
||||||
/>
|
<div
|
||||||
</div>
|
className="h-full bg-white/60 transition-all duration-150"
|
||||||
<p className="text-white/30 text-xs tracking-widest uppercase">{loadProgress}%</p>
|
style={{ width: `${loadProgress}%` }}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-white/30 text-xs tracking-widest uppercase">{loadProgress}%</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Kapat */}
|
{/* Kapat */}
|
||||||
@@ -154,9 +184,9 @@ export default function UnitRevealOverlay({
|
|||||||
Geri
|
Geri
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Bitiş bilgisi — son frame'de */}
|
{/* Bitiş bilgisi */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{finished && (
|
{showInfo && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user