hero: 299 frame scroll animation, openinary CDN transform w_1920,f_webp,q_65

This commit is contained in:
2026-07-30 23:07:41 +03:00
parent 9fd1873cc5
commit 08efae2b97
+65 -77
View File
@@ -11,31 +11,24 @@ import {
} 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]
)
const TOTAL_FRAMES = 299
const OPENINARY_BASE = process.env.NEXT_PUBLIC_OPENINARY_URL ?? 'https://media.ayris.tech'
function frameSrc(i: number) {
const path = `vesta/frames/ezgif-frame-${String(i + 1).padStart(3, '0')}.jpg`
return `${OPENINARY_BASE}/t/w_1920,f_webp,q_65/${path}`
}
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 framesRef = useRef<(HTMLImageElement | null)[]>([])
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({
@@ -66,46 +59,68 @@ export default function CloudRevealSection() {
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
// Canvas boyutu
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) {
if (!canvas) return
const resize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
if (video.readyState >= 2) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
resize()
window.addEventListener('resize', resize)
return () => window.removeEventListener('resize', resize)
}, [])
// Frame preload
useEffect(() => {
if (prefersReducedMotion) return
framesRef.current = new Array(TOTAL_FRAMES).fill(null)
let loadedCount = 0
let cancelled = false
for (let i = 0; i < TOTAL_FRAMES; i++) {
const img = new Image()
img.src = frameSrc(i)
img.onload = () => {
if (cancelled) return
framesRef.current[i] = img
loadedCount++
setLoadProgress(Math.round((loadedCount / TOTAL_FRAMES) * 100))
// İlk frame yüklenince ekranı
if (i === 0 && !loaded) {
const canvas = canvasRef.current
const ctx = canvas?.getContext('2d')
if (canvas && ctx) {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
}
setLoaded(true)
}
}
img.onerror = () => {
if (cancelled) return
loadedCount++
if (loadedCount === TOTAL_FRAMES) setLoadProgress(100)
}
}
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)
}
return () => { cancelled = true }
}, [prefersReducedMotion])
// Scroll → video currentTime
// Scroll → canvas frame çiz
useMotionValueEvent(scrollYProgress, 'change', (progress) => {
const video = videoRef.current
if (!video || !video.duration) return
video.currentTime = progress * video.duration
const frameIdx = Math.min(
Math.floor(progress * (TOTAL_FRAMES - 1)),
TOTAL_FRAMES - 1
)
const img = framesRef.current[frameIdx]
const canvas = canvasRef.current
const ctx = canvas?.getContext('2d')
if (img && canvas && ctx) {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
}
})
// Scroll kilidi
@@ -122,19 +137,6 @@ export default function CloudRevealSection() {
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">
@@ -153,16 +155,6 @@ export default function CloudRevealSection() {
<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" />
@@ -173,9 +165,12 @@ export default function CloudRevealSection() {
{!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
className="h-full bg-white/60 transition-all duration-150"
style={{ width: `${loadProgress}%` }}
/>
</div>
<p className="text-white/30 text-xs tracking-widest uppercase">Yükleniyor</p>
<p className="text-white/30 text-xs tracking-widest uppercase">{loadProgress}%</p>
</div>
)}
@@ -287,12 +282,9 @@ const HOTSPOTS: {
{ 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}`
@@ -335,16 +327,13 @@ function UnitHotspots({
>
<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 }}
@@ -357,7 +346,6 @@ function UnitHotspots({
}
</motion.div>
{/* Kart */}
<motion.button
initial={{ opacity: 0, x: spot.dir === 'right' ? -8 : 8 }}
animate={{ opacity: 1, x: 0 }}