Rewrite Globe with pure refs, remove framer-motion dependency
- Remove useMotionValue/useSpring which caused effect re-runs and globe destroy/recreate cycles (source of glitches) - All state tracked via refs (phi, drag offset, pointer position) - Effect only re-runs on theme change, not on every spring tick - Direct delta tracking for drag instead of spring physics - Simpler, more stable WebGL lifecycle
This commit is contained in:
@@ -1,13 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef, useMemo } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import createGlobe, { type COBEOptions } from 'cobe'
|
import createGlobe from 'cobe'
|
||||||
import { useMotionValue, useSpring } from 'framer-motion'
|
|
||||||
import { useTheme } from '@ciphera-net/ui'
|
import { useTheme } from '@ciphera-net/ui'
|
||||||
import { countryCentroids } from '@/lib/country-centroids'
|
import { countryCentroids } from '@/lib/country-centroids'
|
||||||
|
|
||||||
const MOVEMENT_DAMPING = 3000
|
|
||||||
|
|
||||||
interface GlobeProps {
|
interface GlobeProps {
|
||||||
data: Array<{ country: string; pageviews: number }>
|
data: Array<{ country: string; pageviews: number }>
|
||||||
className?: string
|
className?: string
|
||||||
@@ -16,58 +13,38 @@ interface GlobeProps {
|
|||||||
export default function Globe({ data, className }: GlobeProps) {
|
export default function Globe({ data, className }: GlobeProps) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const phiRef = useRef(0)
|
const phiRef = useRef(0)
|
||||||
const pointerInteracting = useRef<number | null>(null)
|
const dragRef = useRef(0)
|
||||||
const pointerInteractionMovement = useRef(0)
|
const pointerRef = useRef<number | null>(null)
|
||||||
const { resolvedTheme } = useTheme()
|
const { resolvedTheme } = useTheme()
|
||||||
|
const isDarkRef = useRef(resolvedTheme === 'dark')
|
||||||
|
const markersRef = useRef<Array<{ location: [number, number]; size: number }>>([])
|
||||||
|
|
||||||
const isDark = resolvedTheme === 'dark'
|
// Update refs without causing effect re-runs
|
||||||
|
isDarkRef.current = resolvedTheme === 'dark'
|
||||||
|
|
||||||
const markers = useMemo(() => {
|
// Compute markers into ref
|
||||||
if (!data.length) return []
|
const max = data.length ? Math.max(...data.map((d) => d.pageviews)) : 0
|
||||||
const max = Math.max(...data.map((d) => d.pageviews))
|
markersRef.current = max > 0
|
||||||
if (max === 0) return []
|
? data
|
||||||
|
.filter((d) => d.country && d.country !== 'Unknown' && countryCentroids[d.country])
|
||||||
return data
|
.map((d) => ({
|
||||||
.filter((d) => d.country && d.country !== 'Unknown' && countryCentroids[d.country])
|
location: [countryCentroids[d.country].lat, countryCentroids[d.country].lng] as [number, number],
|
||||||
.map((d) => ({
|
size: 0.03 + (d.pageviews / max) * 0.12,
|
||||||
location: [countryCentroids[d.country].lat, countryCentroids[d.country].lng] as [number, number],
|
}))
|
||||||
size: 0.03 + (d.pageviews / max) * 0.12,
|
: []
|
||||||
}))
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
const r = useMotionValue(0)
|
|
||||||
const rs = useSpring(r, {
|
|
||||||
mass: 1,
|
|
||||||
damping: 60,
|
|
||||||
stiffness: 60,
|
|
||||||
})
|
|
||||||
|
|
||||||
const updatePointerInteraction = (value: number | null) => {
|
|
||||||
pointerInteracting.current = value
|
|
||||||
if (canvasRef.current) {
|
|
||||||
canvasRef.current.style.cursor = value !== null ? 'grabbing' : 'grab'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateMovement = (clientX: number) => {
|
|
||||||
if (pointerInteracting.current !== null) {
|
|
||||||
const delta = clientX - pointerInteracting.current
|
|
||||||
pointerInteractionMovement.current = delta
|
|
||||||
r.set(r.get() + delta / MOVEMENT_DAMPING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvasRef.current) return
|
if (!canvasRef.current) return
|
||||||
|
|
||||||
const size = canvasRef.current.offsetWidth
|
const size = canvasRef.current.offsetWidth
|
||||||
const pixelRatio = Math.min(window.devicePixelRatio, 2)
|
const pixelRatio = Math.min(window.devicePixelRatio, 2)
|
||||||
|
const isDark = isDarkRef.current
|
||||||
|
|
||||||
const globe = createGlobe(canvasRef.current, {
|
const globe = createGlobe(canvasRef.current, {
|
||||||
width: size * pixelRatio,
|
width: size * pixelRatio,
|
||||||
height: size * pixelRatio,
|
height: size * pixelRatio,
|
||||||
devicePixelRatio: pixelRatio,
|
devicePixelRatio: pixelRatio,
|
||||||
phi: 0,
|
phi: phiRef.current,
|
||||||
theta: 0.3,
|
theta: 0.3,
|
||||||
dark: isDark ? 1 : 0,
|
dark: isDark ? 1 : 0,
|
||||||
diffuse: isDark ? 2 : 0.4,
|
diffuse: isDark ? 2 : 0.4,
|
||||||
@@ -76,21 +53,21 @@ export default function Globe({ data, className }: GlobeProps) {
|
|||||||
baseColor: isDark ? [0.5, 0.5, 0.5] : [1, 1, 1],
|
baseColor: isDark ? [0.5, 0.5, 0.5] : [1, 1, 1],
|
||||||
markerColor: [253 / 255, 94 / 255, 15 / 255],
|
markerColor: [253 / 255, 94 / 255, 15 / 255],
|
||||||
glowColor: isDark ? [0.15, 0.15, 0.15] : [1, 1, 1],
|
glowColor: isDark ? [0.15, 0.15, 0.15] : [1, 1, 1],
|
||||||
markers,
|
markers: markersRef.current,
|
||||||
onRender: (state) => {
|
onRender: (state) => {
|
||||||
if (!pointerInteracting.current) phiRef.current += 0.002
|
if (!pointerRef.current) phiRef.current += 0.002
|
||||||
state.phi = phiRef.current + rs.get()
|
state.phi = phiRef.current + dragRef.current
|
||||||
},
|
},
|
||||||
} as COBEOptions)
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (canvasRef.current) canvasRef.current.style.opacity = '1'
|
if (canvasRef.current) canvasRef.current.style.opacity = '1'
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
return () => {
|
return () => { globe.destroy() }
|
||||||
globe.destroy()
|
// Only recreate on theme change
|
||||||
}
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [rs, markers, isDark])
|
}, [resolvedTheme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full h-full overflow-hidden ${className ?? ''}`}>
|
<div className={`relative w-full h-full overflow-hidden ${className ?? ''}`}>
|
||||||
@@ -100,15 +77,31 @@ export default function Globe({ data, className }: GlobeProps) {
|
|||||||
style={{ contain: 'layout paint size' }}
|
style={{ contain: 'layout paint size' }}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
pointerInteracting.current = e.clientX
|
pointerRef.current = e.clientX
|
||||||
updatePointerInteraction(e.clientX)
|
canvasRef.current!.style.cursor = 'grabbing'
|
||||||
|
}}
|
||||||
|
onPointerUp={() => {
|
||||||
|
pointerRef.current = null
|
||||||
|
canvasRef.current!.style.cursor = 'grab'
|
||||||
|
}}
|
||||||
|
onPointerOut={() => {
|
||||||
|
pointerRef.current = null
|
||||||
|
if (canvasRef.current) canvasRef.current.style.cursor = 'grab'
|
||||||
|
}}
|
||||||
|
onMouseMove={(e) => {
|
||||||
|
if (pointerRef.current !== null) {
|
||||||
|
const delta = e.clientX - pointerRef.current
|
||||||
|
dragRef.current += delta / 3000
|
||||||
|
pointerRef.current = e.clientX
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onTouchMove={(e) => {
|
||||||
|
if (pointerRef.current !== null && e.touches[0]) {
|
||||||
|
const delta = e.touches[0].clientX - pointerRef.current
|
||||||
|
dragRef.current += delta / 3000
|
||||||
|
pointerRef.current = e.touches[0].clientX
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onPointerUp={() => updatePointerInteraction(null)}
|
|
||||||
onPointerOut={() => updatePointerInteraction(null)}
|
|
||||||
onMouseMove={(e) => updateMovement(e.clientX)}
|
|
||||||
onTouchMove={(e) =>
|
|
||||||
e.touches[0] && updateMovement(e.touches[0].clientX)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pointer-events-none absolute inset-0 h-full bg-[radial-gradient(circle_at_50%_200%,rgba(0,0,0,0.2),rgba(255,255,255,0))]" />
|
<div className="pointer-events-none absolute inset-0 h-full bg-[radial-gradient(circle_at_50%_200%,rgba(0,0,0,0.2),rgba(255,255,255,0))]" />
|
||||||
|
|||||||
Reference in New Issue
Block a user