hero: 299 frame scroll animation, openinary CDN transform w_1920,f_webp,q_65
This commit is contained in:
@@ -11,31 +11,24 @@ import {
|
|||||||
} from 'framer-motion'
|
} from 'framer-motion'
|
||||||
import UnitRevealOverlay from './UnitRevealOverlay'
|
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
|
const TOTAL_FRAMES = 299
|
||||||
function usePhaseOpacity(
|
const OPENINARY_BASE = process.env.NEXT_PUBLIC_OPENINARY_URL ?? 'https://media.ayris.tech'
|
||||||
scrollYProgress: ReturnType<typeof useScroll>['scrollYProgress'],
|
|
||||||
inStart: number,
|
function frameSrc(i: number) {
|
||||||
inEnd: number,
|
const path = `vesta/frames/ezgif-frame-${String(i + 1).padStart(3, '0')}.jpg`
|
||||||
outStart: number,
|
return `${OPENINARY_BASE}/t/w_1920,f_webp,q_65/${path}`
|
||||||
outEnd: number
|
|
||||||
) {
|
|
||||||
return useTransform(
|
|
||||||
scrollYProgress,
|
|
||||||
[inStart, inEnd, outStart, outEnd],
|
|
||||||
[0, 1, 1, 0]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function CloudRevealSection() {
|
export default function CloudRevealSection() {
|
||||||
const t = useTranslations('hero')
|
const t = useTranslations('hero')
|
||||||
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 videoRef = useRef<HTMLVideoElement>(null)
|
const framesRef = useRef<(HTMLImageElement | null)[]>([])
|
||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
const [loadProgress, setLoadProgress] = useState(0)
|
||||||
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({
|
||||||
@@ -66,46 +59,68 @@ export default function CloudRevealSection() {
|
|||||||
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])
|
||||||
|
|
||||||
// Video → canvas kurulumu
|
// Canvas boyutu
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prefersReducedMotion) return
|
|
||||||
const video = videoRef.current
|
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current
|
||||||
if (!video || !canvas) return
|
if (!canvas) return
|
||||||
const ctx = canvas.getContext('2d')
|
const resize = () => {
|
||||||
if (!ctx) return
|
|
||||||
|
|
||||||
const draw = () => {
|
|
||||||
if (canvas.width === 0) {
|
|
||||||
canvas.width = window.innerWidth
|
canvas.width = window.innerWidth
|
||||||
canvas.height = window.innerHeight
|
canvas.height = window.innerHeight
|
||||||
}
|
}
|
||||||
if (video.readyState >= 2) {
|
resize()
|
||||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
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ı aç
|
||||||
|
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() }
|
return () => { cancelled = true }
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}, [prefersReducedMotion])
|
}, [prefersReducedMotion])
|
||||||
|
|
||||||
// Scroll → video currentTime
|
// Scroll → canvas frame çiz
|
||||||
useMotionValueEvent(scrollYProgress, 'change', (progress) => {
|
useMotionValueEvent(scrollYProgress, 'change', (progress) => {
|
||||||
const video = videoRef.current
|
const frameIdx = Math.min(
|
||||||
if (!video || !video.duration) return
|
Math.floor(progress * (TOTAL_FRAMES - 1)),
|
||||||
video.currentTime = progress * video.duration
|
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
|
// Scroll kilidi
|
||||||
@@ -122,19 +137,6 @@ export default function CloudRevealSection() {
|
|||||||
return () => window.removeEventListener('scroll', lockScroll)
|
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) {
|
if (prefersReducedMotion) {
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-[100dvh] w-full overflow-hidden bg-black flex items-end">
|
<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 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">
|
||||||
|
|
||||||
{/* 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 */}
|
||||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||||
|
|
||||||
@@ -173,9 +165,12 @@ 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 w-0 animate-pulse" />
|
<div
|
||||||
|
className="h-full bg-white/60 transition-all duration-150"
|
||||||
|
style={{ width: `${loadProgress}%` }}
|
||||||
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -287,12 +282,9 @@ const HOTSPOTS: {
|
|||||||
{ id: 'blok-c', label: '1+1', sub: 'Blok C · 60 m²', x: 11, y: 42, dir: 'right' },
|
{ 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 }) {
|
function ZigzagLine({ dir, delay }: { dir: 'left' | 'right'; delay: number }) {
|
||||||
// sağa giden: sol→sağ, sola giden: sağ→sol
|
|
||||||
const w = 72
|
const w = 72
|
||||||
const h = 24
|
const h = 24
|
||||||
// zigzag path: 3 segment, orta kısmı yukarı/aşağı kayar
|
|
||||||
const path = dir === 'right'
|
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}`
|
? `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}`
|
: `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'}`}>
|
<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="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="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 className="relative inline-flex rounded-full h-3 w-3 bg-white" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Zigzag çizgi */}
|
|
||||||
<ZigzagLine dir={spot.dir} delay={i * 0.25 + 0.1} />
|
<ZigzagLine dir={spot.dir} delay={i * 0.25 + 0.1} />
|
||||||
|
|
||||||
{/* 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 }}
|
||||||
@@ -357,7 +346,6 @@ function UnitHotspots({
|
|||||||
}
|
}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Kart */}
|
|
||||||
<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 }}
|
||||||
|
|||||||
Reference in New Issue
Block a user