perf: video-driven hero animation instead of 299 frame preload

This commit is contained in:
2026-07-30 22:52:55 +03:00
parent ae82ad5bc3
commit a33658f0ee
+43 -54
View File
@@ -11,10 +11,8 @@ import {
} from 'framer-motion' } from 'framer-motion'
import UnitRevealOverlay from './UnitRevealOverlay' import UnitRevealOverlay from './UnitRevealOverlay'
const TOTAL_FRAMES = 299
const CDN = 'https://media.ayris.tech/t' const CDN = 'https://media.ayris.tech/t'
const FRAME_PATH = (n: number) => const HERO_VIDEO = `${CDN}/vesta/hero/hero.mp4`
`${CDN}/vesta/frames/ezgif-frame-${String(n).padStart(3, '0')}.jpg`
// Scroll aralığına göre opacity döndüren helper // Scroll aralığına göre opacity döndüren helper
function usePhaseOpacity( function usePhaseOpacity(
@@ -36,11 +34,8 @@ export default function CloudRevealSection() {
const prefersReducedMotion = useReducedMotion() const prefersReducedMotion = useReducedMotion()
const sectionRef = useRef<HTMLDivElement>(null) const sectionRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null)
const framesRef = useRef<HTMLImageElement[]>([]) const videoRef = useRef<HTMLVideoElement>(null)
const currentFrameRef = useRef(0)
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
const [loadProgress, setLoadProgress] = useState(0)
const hasSnapped = useRef(false)
const [activeUnit, setActiveUnit] = useState<{ id: string; label: string; sub: string } | null>(null) const [activeUnit, setActiveUnit] = useState<{ id: string; label: string; sub: string } | null>(null)
const { scrollYProgress } = useScroll({ const { scrollYProgress } = useScroll({
@@ -51,78 +46,62 @@ export default function CloudRevealSection() {
// Progress bar // Progress bar
const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1]) const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1])
// --- Text phase opacities ---
// Logo: 08% görünür, sonra kaybolur // Logo: 08% görünür, sonra kaybolur
const logoOpacity = useTransform(scrollYProgress, [0, 0.02, 0.08, 0.14], [0, 1, 1, 0]) 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]) const logoScale = useTransform(scrollYProgress, [0, 0.02], [0.92, 1])
// Phase 1: 015% → tagline (sol alt) // Phase 1: 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])
// Phase 2: 2045% → orta metin (merkez) // Phase 2: orta metin
const phase2Opacity = useTransform(scrollYProgress, [0.20, 0.28, 0.40, 0.48], [0, 1, 1, 0]) 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]) const phase2Y = useTransform(scrollYProgress, [0.20, 0.28], [30, 0])
// Phase 3: 5072% → stat kartları (sağ) // Phase 3: stat rakamları
const phase3Opacity = useTransform(scrollYProgress, [0.50, 0.57, 0.68, 0.75], [0, 1, 1, 0]) 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]) const phase3Y = useTransform(scrollYProgress, [0.50, 0.57], [30, 0])
// Phase 4: 78100% → CTA (merkez alt) // Phase 4: CTA
const phase4Opacity = useTransform(scrollYProgress, [0.78, 0.85, 0.95, 1], [0, 1, 1, 0]) 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]) const phase4Y = useTransform(scrollYProgress, [0.78, 0.85], [30, 0])
// Frame preload // Video → canvas kurulumu
useEffect(() => { useEffect(() => {
if (prefersReducedMotion) return if (prefersReducedMotion) return
const images: HTMLImageElement[] = new Array(TOTAL_FRAMES) const video = videoRef.current
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 const canvas = canvasRef.current
if (!canvas) return if (!video || !canvas) return
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
if (!ctx) return 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) => { const drawVideoFrame = () => {
if (!loaded) return if (video.readyState >= 2) {
const frameIndex = Math.min(Math.floor(progress * (TOTAL_FRAMES - 1)), TOTAL_FRAMES - 1) ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
if (frameIndex !== currentFrameRef.current) { }
currentFrameRef.current = frameIndex
drawFrame(frameIndex)
} }
video.addEventListener('seeked', drawVideoFrame)
video.addEventListener('loadeddata', () => {
setLoaded(true)
drawVideoFrame()
})
return () => video.removeEventListener('seeked', drawVideoFrame)
}, [prefersReducedMotion])
// Scroll → video currentTime
useMotionValueEvent(scrollYProgress, 'change', (progress) => {
const video = videoRef.current
if (!video || !video.duration) return
video.currentTime = progress * video.duration
}) })
// Scroll pozisyonunu hero sonunda kilitle // Scroll kilidi
useEffect(() => { useEffect(() => {
const lockScroll = () => { const lockScroll = () => {
const section = sectionRef.current const section = sectionRef.current
if (!section) return if (!section) return
// Hero'nun scroll edebileceği maksimum pozisyon
const maxY = section.offsetTop + section.offsetHeight - window.innerHeight const maxY = section.offsetTop + section.offsetHeight - window.innerHeight
if (window.scrollY > maxY) { if (window.scrollY > maxY) {
window.scrollTo({ top: maxY, behavior: 'instant' }) window.scrollTo({ top: maxY, behavior: 'instant' })
@@ -132,18 +111,18 @@ export default function CloudRevealSection() {
return () => window.removeEventListener('scroll', lockScroll) return () => window.removeEventListener('scroll', lockScroll)
}, []) }, [])
// Canvas boyutu
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current const canvas = canvasRef.current
if (!canvas) return if (!canvas) return
const resize = () => { const resize = () => {
canvas.width = window.innerWidth canvas.width = window.innerWidth
canvas.height = window.innerHeight canvas.height = window.innerHeight
drawFrame(currentFrameRef.current)
} }
resize() resize()
window.addEventListener('resize', resize) window.addEventListener('resize', resize)
return () => window.removeEventListener('resize', resize) return () => window.removeEventListener('resize', resize)
}, [loaded]) }, [])
if (prefersReducedMotion) { if (prefersReducedMotion) {
return ( return (
@@ -163,6 +142,16 @@ export default function CloudRevealSection() {
<div ref={sectionRef} style={{ height: '500vh' }}> <div ref={sectionRef} style={{ height: '500vh' }}>
<div className="sticky top-0 min-h-[100dvh] w-full overflow-hidden bg-black"> <div className="sticky top-0 min-h-[100dvh] w-full overflow-hidden bg-black">
{/* Gizli video — sadece canvas'a kaynak */}
<video
ref={videoRef}
src={HERO_VIDEO}
preload="auto"
muted
playsInline
className="hidden"
/>
{/* Canvas */} {/* Canvas */}
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" /> <canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
@@ -173,9 +162,9 @@ export default function CloudRevealSection() {
{!loaded && ( {!loaded && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black z-20"> <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="w-48 h-px bg-white/10 mb-3">
<div className="h-full bg-white/60 transition-all duration-150" style={{ width: `${loadProgress}%` }} /> <div className="h-full bg-white/60 transition-all duration-150 w-0 animate-pulse" />
</div> </div>
<p className="text-white/30 text-xs tracking-widest uppercase">{loadProgress}%</p> <p className="text-white/30 text-xs tracking-widest uppercase">Yükleniyor</p>
</div> </div>
)} )}