From a33658f0ee3e48772a81eb9e71d4f72d7353c15e Mon Sep 17 00:00:00 2001 From: Mustafa Date: Thu, 30 Jul 2026 22:52:55 +0300 Subject: [PATCH] perf: video-driven hero animation instead of 299 frame preload --- components/sections/CloudRevealSection.tsx | 97 ++++++++++------------ 1 file changed, 43 insertions(+), 54 deletions(-) diff --git a/components/sections/CloudRevealSection.tsx b/components/sections/CloudRevealSection.tsx index 17f0ef5..5112e85 100644 --- a/components/sections/CloudRevealSection.tsx +++ b/components/sections/CloudRevealSection.tsx @@ -11,10 +11,8 @@ import { } from 'framer-motion' import UnitRevealOverlay from './UnitRevealOverlay' -const TOTAL_FRAMES = 299 const CDN = 'https://media.ayris.tech/t' -const FRAME_PATH = (n: number) => - `${CDN}/vesta/frames/ezgif-frame-${String(n).padStart(3, '0')}.jpg` +const HERO_VIDEO = `${CDN}/vesta/hero/hero.mp4` // Scroll aralığına göre opacity döndüren helper function usePhaseOpacity( @@ -36,11 +34,8 @@ export default function CloudRevealSection() { const prefersReducedMotion = useReducedMotion() const sectionRef = useRef(null) const canvasRef = useRef(null) - const framesRef = useRef([]) - const currentFrameRef = useRef(0) + const videoRef = useRef(null) 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 { scrollYProgress } = useScroll({ @@ -51,78 +46,62 @@ export default function CloudRevealSection() { // Progress bar const progressScaleX = useTransform(scrollYProgress, [0, 1], [0, 1]) - // --- 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: 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: 20–45% → orta metin (merkez) + // Phase 2: orta metin 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: 50–72% → stat kartları (sağ) + // Phase 3: stat rakamları 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: 78–100% → CTA (merkez alt) + // Phase 4: CTA 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 + // Video → canvas kurulumu 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 video = videoRef.current const canvas = canvasRef.current - if (!canvas) return + if (!video || !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 - const frameIndex = Math.min(Math.floor(progress * (TOTAL_FRAMES - 1)), TOTAL_FRAMES - 1) - if (frameIndex !== currentFrameRef.current) { - currentFrameRef.current = frameIndex - drawFrame(frameIndex) + const drawVideoFrame = () => { + if (video.readyState >= 2) { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height) + } } + + 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(() => { const lockScroll = () => { const section = sectionRef.current if (!section) return - // Hero'nun scroll edebileceği maksimum pozisyon const maxY = section.offsetTop + section.offsetHeight - window.innerHeight if (window.scrollY > maxY) { window.scrollTo({ top: maxY, behavior: 'instant' }) @@ -132,18 +111,18 @@ 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 - drawFrame(currentFrameRef.current) } resize() window.addEventListener('resize', resize) return () => window.removeEventListener('resize', resize) - }, [loaded]) + }, []) if (prefersReducedMotion) { return ( @@ -163,6 +142,16 @@ export default function CloudRevealSection() {
+ {/* Gizli video — sadece canvas'a kaynak */} +