effects
Antigravity Particles
A gorgeous React Three Fiber canvas component rendering magnetic floating particles that orbit the mouse cursor on hover.
particlesmagnetorbitwaveinteractivefiberwebgl
▶Preview
React Three Fiber Instanced Field
Adjust Particle Instancing Props
Particle Geometry
Particles Count250
Magnet Radius10
Orbit Radius8
Particle Scale2
Enable Multi-Color Particles
Particle Color (Single color disabled)
🖲Usage
example-usage.tsx
import { Antigravity } from "@/components/kanso/antigravity"
export default function AntigravityDemo() {
return (
<div className="w-full h-[350px] border border-zinc-200 dark:border-zinc-800 bg-zinc-950 rounded-xl overflow-hidden">
<Antigravity
count={250}
magnetRadius={8}
ringRadius={8}
color="#c084fc"
particleShape="capsule"
/>
</div>
)
}↓Installation
1
Create folder & copy source
Create a folder named kanso inside your project's components directory (i.e. components/kanso/). Copy the source code shown in the next section, and paste it into a file named antigravity.tsx inside it.
2
Install dependencies
pnpm add three @react-three/fiber3
Required helper files
Ensure your project has the following helper files configured:
- →
lib/utils
<>Source Code
antigravity.tsx
'use client';
import * as React from 'react';
import * as THREE from 'three';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { cn } from '@/lib/utils';
export interface AntigravityProps {
count?: number;
magnetRadius?: number;
ringRadius?: number;
waveSpeed?: number;
waveAmplitude?: number;
particleSize?: number;
lerpSpeed?: number;
color?: string;
colors?: string[];
autoAnimate?: boolean;
particleVariance?: number;
rotationSpeed?: number;
depthFactor?: number;
pulseSpeed?: number;
particleShape?: 'capsule' | 'sphere' | 'box' | 'tetrahedron';
fieldStrength?: number;
}
function createRandom(seed: number) {
let s = seed;
return () => {
const x = Math.sin(s++) * 10000;
return x - Math.floor(x);
};
}
const AntigravityInner: React.FC<AntigravityProps> = ({
count = 300,
magnetRadius = 10,
ringRadius = 10,
waveSpeed = 0.4,
waveAmplitude = 1,
particleSize = 2,
lerpSpeed = 0.1,
color = '#FF9FFC',
colors,
autoAnimate = false,
particleVariance = 1,
rotationSpeed = 0,
depthFactor = 1,
pulseSpeed = 3,
particleShape = 'capsule',
fieldStrength = 10,
}) => {
const meshRef = React.useRef<THREE.InstancedMesh>(null);
const { viewport } = useThree();
const dummy = React.useMemo(() => new THREE.Object3D(), []);
const hasColors = colors && colors.length > 0;
const lastMousePos = React.useRef({ x: 0, y: 0 });
const lastMouseMoveTime = React.useRef(0);
const virtualMouse = React.useRef({ x: 0, y: 0 });
const particles = React.useMemo(() => {
const temp = [];
const width = viewport.width || 100;
const height = viewport.height || 100;
const random = createRandom(42);
for (let i = 0; i < count; i++) {
const t = random() * 100;
const factor = 20 + random() * 100;
const speed = 0.01 + random() / 200;
const xFactor = -50 + random() * 100;
const yFactor = -50 + random() * 100;
const zFactor = -50 + random() * 100;
const x = (random() - 0.5) * width;
const y = (random() - 0.5) * height;
const z = (random() - 0.5) * 20;
const randomRadiusOffset = (random() - 0.5) * 2;
temp.push({
t,
factor,
speed,
xFactor,
yFactor,
zFactor,
mx: x,
my: y,
mz: z,
cx: x,
cy: y,
cz: z,
vx: 0,
vy: 0,
vz: 0,
randomRadiusOffset,
});
}
return temp;
}, [count, viewport.width, viewport.height]);
useFrame((state) => {
const mesh = meshRef.current;
if (!mesh) return;
const { viewport: v, pointer: m } = state;
const mouseDist = Math.sqrt(
Math.pow(m.x - lastMousePos.current.x, 2) +
Math.pow(m.y - lastMousePos.current.y, 2)
);
if (mouseDist > 0.001) {
lastMouseMoveTime.current = Date.now();
lastMousePos.current = { x: m.x, y: m.y };
}
let destX = (m.x * v.width) / 2;
let destY = (m.y * v.height) / 2;
if (autoAnimate && Date.now() - lastMouseMoveTime.current > 2000) {
const time = state.clock.getElapsedTime();
destX = Math.sin(time * 0.5) * (v.width / 4);
destY = Math.cos(time * 0.5 * 2) * (v.height / 4);
}
const smoothFactor = 0.05;
virtualMouse.current.x += (destX - virtualMouse.current.x) * smoothFactor;
virtualMouse.current.y += (destY - virtualMouse.current.y) * smoothFactor;
const targetX = virtualMouse.current.x;
const targetY = virtualMouse.current.y;
const globalRotation = state.clock.getElapsedTime() * rotationSpeed;
const colorObj = new THREE.Color();
particles.forEach((particle, i) => {
let { t } = particle;
const { speed, mx, my, mz, cz, randomRadiusOffset } = particle;
t = particle.t += speed / 2;
const projectionFactor = 1 - cz / 50;
const projectedTargetX = targetX * projectionFactor;
const projectedTargetY = targetY * projectionFactor;
const dx = mx - projectedTargetX;
const dy = my - projectedTargetY;
const dist = Math.sqrt(dx * dx + dy * dy);
const targetPos = { x: mx, y: my, z: mz * depthFactor };
if (dist < magnetRadius) {
const angle = Math.atan2(dy, dx) + globalRotation;
const wave = Math.sin(t * waveSpeed + angle) * (0.5 * waveAmplitude);
const deviation = randomRadiusOffset * (5 / (fieldStrength + 0.1));
const currentRingRadius = ringRadius + wave + deviation;
targetPos.x = projectedTargetX + currentRingRadius * Math.cos(angle);
targetPos.y = projectedTargetY + currentRingRadius * Math.sin(angle);
targetPos.z =
mz * depthFactor + Math.sin(t) * (1 * waveAmplitude * depthFactor);
}
particle.cx += (targetPos.x - particle.cx) * lerpSpeed;
particle.cy += (targetPos.y - particle.cy) * lerpSpeed;
particle.cz += (targetPos.z - particle.cz) * lerpSpeed;
dummy.position.set(particle.cx, particle.cy, particle.cz);
dummy.lookAt(projectedTargetX, projectedTargetY, particle.cz);
dummy.rotateX(Math.PI / 2);
const currentDistToMouse = Math.sqrt(
Math.pow(particle.cx - projectedTargetX, 2) +
Math.pow(particle.cy - projectedTargetY, 2)
);
const distFromRing = Math.abs(currentDistToMouse - ringRadius);
let scaleFactor = 1 - distFromRing / 10;
scaleFactor = Math.max(0, Math.min(1, scaleFactor));
const finalScale =
scaleFactor *
(0.8 + Math.sin(t * pulseSpeed) * 0.2 * particleVariance) *
particleSize;
dummy.scale.set(finalScale, finalScale, finalScale);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
// Set individual instance color
if (hasColors) {
colorObj.set(colors[i % colors.length]);
mesh.setColorAt(i, colorObj);
} else {
colorObj.set(color);
mesh.setColorAt(i, colorObj);
}
});
mesh.instanceMatrix.needsUpdate = true;
if (mesh.instanceColor) {
mesh.instanceColor.needsUpdate = true;
}
});
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
{particleShape === 'capsule' && (
<capsuleGeometry args={[0.1, 0.4, 4, 8]} />
)}
{particleShape === 'sphere' && <sphereGeometry args={[0.2, 16, 16]} />}
{particleShape === 'box' && <boxGeometry args={[0.3, 0.3, 0.3]} />}
{particleShape === 'tetrahedron' && <tetrahedronGeometry args={[0.3]} />}
<meshBasicMaterial color={hasColors ? '#ffffff' : color} />
</instancedMesh>
);
};
export interface AntigravityWrapperProps
extends React.HTMLAttributes<HTMLDivElement>, AntigravityProps {}
const Antigravity = React.forwardRef<HTMLDivElement, AntigravityWrapperProps>(
(
{
count,
magnetRadius,
ringRadius,
waveSpeed,
waveAmplitude,
particleSize,
lerpSpeed,
color,
colors,
autoAnimate,
particleVariance,
rotationSpeed,
depthFactor,
pulseSpeed,
particleShape,
fieldStrength,
className,
style,
...props
},
ref
) => {
return (
<div
ref={ref}
className={cn('w-full h-full relative overflow-hidden', className)}
style={style}
{...props}
>
<Canvas camera={{ position: [0, 0, 50], fov: 35 }}>
<AntigravityInner
count={count}
magnetRadius={magnetRadius}
ringRadius={ringRadius}
waveSpeed={waveSpeed}
waveAmplitude={waveAmplitude}
particleSize={particleSize}
lerpSpeed={lerpSpeed}
color={color}
colors={colors}
autoAnimate={autoAnimate}
particleVariance={particleVariance}
rotationSpeed={rotationSpeed}
depthFactor={depthFactor}
pulseSpeed={pulseSpeed}
particleShape={particleShape}
fieldStrength={fieldStrength}
/>
</Canvas>
</div>
);
}
);
Antigravity.displayName = 'Antigravity';
export { Antigravity };