perf: video-driven hero animation instead of 299 frame preload
This commit is contained in:
@@ -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<HTMLDivElement>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const framesRef = useRef<HTMLImageElement[]>([])
|
||||
const currentFrameRef = useRef(0)
|
||||
const videoRef = useRef<HTMLVideoElement>(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() {
|
||||
<div ref={sectionRef} style={{ height: '500vh' }}>
|
||||
<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 ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||
|
||||
@@ -173,9 +162,9 @@ 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" style={{ width: `${loadProgress}%` }} />
|
||||
<div className="h-full bg-white/60 transition-all duration-150 w-0 animate-pulse" />
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user