Spaces:
Sleeping
Sleeping
File size: 5,681 Bytes
7d3b88b f7dede2 7d3b88b f7dede2 7d3b88b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | import { useEffect, useRef, useCallback } from 'react'
import { motion, useMotionValue, useSpring } from 'framer-motion'
/*
* Cursor-following blob + floating background shapes
*
* 1. Cursor blob: A large, soft, low-opacity radial gradient that
* follows the mouse with elastic spring lag. Never snaps β always
* trails behind, giving a "weightless" feel.
*
* 2. Background shapes: 6 pastel circles with very slow sinusoidal
* drift + gentle parallax from mouse offset. All GPU-composited.
*/
/* ββ Spring config: elastic, slow settling ββββββββββββββββ */
const CURSOR_SPRING = { damping: 25, stiffness: 50, mass: 1.5 }
const PARALLAX_SPRING = { damping: 80, stiffness: 15, mass: 3 }
/* ββ Background shapes β high-visibility distinct colors ββ */
const SHAPES = [
{ size: 500, x: '5%', y: '8%', color: 'rgba(56,189,248,0.40)', driftDur: 30, xAmp: 25, yAmp: 18, pFactor: 0.012 }, /* sky blue */
{ size: 400, x: '74%', y: '5%', color: 'rgba(251,146,60,0.32)', driftDur: 36, xAmp: 20, yAmp: 30, pFactor: 0.018 }, /* warm orange */
{ size: 380, x: '58%', y: '58%', color: 'rgba(167,139,250,0.38)', driftDur: 34, xAmp: 30, yAmp: 22, pFactor: 0.010 }, /* lavender */
{ size: 300, x: '12%', y: '65%', color: 'rgba(52,211,153,0.32)', driftDur: 28, xAmp: 18, yAmp: 25, pFactor: 0.022 }, /* mint */
{ size: 260, x: '84%', y: '38%', color: 'rgba(251,113,133,0.30)', driftDur: 32, xAmp: 14, yAmp: 18, pFactor: 0.015 }, /* rose */
{ size: 420, x: '38%', y: '24%', color: 'rgba(56,189,248,0.28)', driftDur: 38, xAmp: 22, yAmp: 16, pFactor: 0.008 }, /* light cyan */
]
/* ββ Single floating shape βββββββββββββββββββββββββββββββ */
function Shape({ shape, mouseX, mouseY }) {
const offsetX = useSpring(0, PARALLAX_SPRING)
const offsetY = useSpring(0, PARALLAX_SPRING)
useEffect(() => {
const unX = mouseX.on('change', (v) => offsetX.set(v * shape.pFactor))
const unY = mouseY.on('change', (v) => offsetY.set(v * shape.pFactor))
return () => { unX(); unY() }
}, [mouseX, mouseY, offsetX, offsetY, shape.pFactor])
return (
<motion.div
className="absolute rounded-full pointer-events-none"
style={{
width: shape.size,
height: shape.size,
left: shape.x,
top: shape.y,
background: `radial-gradient(circle, ${shape.color} 0%, transparent 70%)`,
filter: 'blur(40px)',
x: offsetX,
y: offsetY,
willChange: 'transform',
}}
animate={{
x: [0, shape.xAmp, -shape.xAmp * 0.5, 0],
y: [0, -shape.yAmp, shape.yAmp * 0.7, 0],
}}
transition={{
duration: shape.driftDur,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
)
}
/* ββ Cursor-following blob βββββββββββββββββββββββββββββββ */
function CursorBlob({ mouseX, mouseY }) {
const blobX = useSpring(0, CURSOR_SPRING)
const blobY = useSpring(0, CURSOR_SPRING)
useEffect(() => {
const unX = mouseX.on('change', (v) => blobX.set(v))
const unY = mouseY.on('change', (v) => blobY.set(v))
return () => { unX(); unY() }
}, [mouseX, mouseY, blobX, blobY])
return (
<motion.div
className="fixed pointer-events-none rounded-full"
style={{
width: 300,
height: 300,
x: blobX,
y: blobY,
left: -150,
top: -150,
background: 'radial-gradient(circle, rgba(251,146,60,0.42) 0%, rgba(251,113,133,0.24) 40%, transparent 70%)',
filter: 'blur(35px)',
willChange: 'transform',
zIndex: 1,
}}
/>
)
}
/* ββ Main export βββββββββββββββββββββββββββββββββββββββββ */
export default function FloatingBackground() {
const mouseX = useMotionValue(0)
const mouseY = useMotionValue(0)
const rafRef = useRef(null)
const handleMouseMove = useCallback(
(e) => {
if (rafRef.current) return
rafRef.current = requestAnimationFrame(() => {
mouseX.set(e.clientX)
mouseY.set(e.clientY)
rafRef.current = null
})
},
[mouseX, mouseY]
)
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove, { passive: true })
return () => {
window.removeEventListener('mousemove', handleMouseMove)
if (rafRef.current) cancelAnimationFrame(rafRef.current)
}
}, [handleMouseMove])
/* For background parallax, center-relative coords */
const parallaxX = useMotionValue(0)
const parallaxY = useMotionValue(0)
useEffect(() => {
const unX = mouseX.on('change', (v) => parallaxX.set(v - window.innerWidth / 2))
const unY = mouseY.on('change', (v) => parallaxY.set(v - window.innerHeight / 2))
return () => { unX(); unY() }
}, [mouseX, mouseY, parallaxX, parallaxY])
return (
<>
{/* Cursor-following blob */}
<CursorBlob mouseX={mouseX} mouseY={mouseY} />
{/* Background shapes */}
<div
className="fixed inset-0 overflow-hidden pointer-events-none"
style={{ zIndex: 0 }}
aria-hidden="true"
>
{/* Very subtle top vignette */}
<div
className="absolute inset-0"
style={{
background: 'radial-gradient(ellipse 80% 50% at 50% 30%, rgba(99,102,241,0.02) 0%, transparent 70%)',
}}
/>
{SHAPES.map((shape, i) => (
<Shape key={i} shape={shape} mouseX={parallaxX} mouseY={parallaxY} />
))}
</div>
</>
)
}
|