Web Animations Hard
Interactive 3D Mouse Scene
Grid of 400 spheres with mouse-reactive repulsion physics, spring return, and distance-based color.
Open in Lab
MCP
three.js instanced-mesh raycaster
Targets: JS HTML
Code
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #030510;
color: #f0f4fb;
font-family: "Inter", "SF Pro Display", system-ui, sans-serif;
overflow: hidden;
height: 100vh;
}
#canvas-container {
position: fixed;
inset: 0;
z-index: 0;
}
#canvas-container canvas {
display: block;
width: 100%;
height: 100%;
}
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 2;
text-align: center;
padding: 3rem 2rem;
pointer-events: none;
}
.eyebrow {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.18em;
color: #86e8ff;
margin-bottom: 0.75rem;
display: block;
}
h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 700;
letter-spacing: -0.03em;
text-shadow: 0 0 40px rgba(134, 232, 255, 0.2);
}
.subtitle {
font-size: clamp(0.85rem, 1.5vw, 1rem);
color: rgba(255, 255, 255, 0.5);
max-width: 420px;
margin: 0.5rem auto 0;
line-height: 1.5;
}
.btn-back {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
z-index: 10;
padding: 0.6rem 1.5rem;
border-radius: 999px;
border: 1px solid rgba(134, 232, 255, 0.2);
color: #86e8ff;
text-decoration: none;
font: 600 0.8rem / 1 "Inter", system-ui, sans-serif;
background: rgba(3, 5, 16, 0.7);
backdrop-filter: blur(8px);
transition: all 0.25s;
}
.btn-back:hover {
background: rgba(134, 232, 255, 0.08);
border-color: #86e8ff;
}if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
import * as THREE from "three";
initDemoShell({
title: "Interactive 3D Scene",
category: "3d",
tech: ["three.js", "instanced-mesh", "raycaster"],
});
let reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add("reduced-motion");
window.addEventListener("motion-preference", (e) => {
reduced = e.detail.reduced;
});
// Config
const GRID = 20; // 20x20 = 400 spheres
const SPACING = 0.6;
const REPULSION_RADIUS = 3;
const REPULSION_FORCE = 0.08;
const SPRING = 0.04;
const DAMPING = 0.88;
// Scene
const container = document.getElementById("canvas-container");
const scene = new THREE.Scene();
scene.background = new THREE.Color("#030510");
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 8, 10);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// Lights
scene.add(new THREE.AmbientLight(0x222244, 0.8));
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(5, 8, 5);
scene.add(dirLight);
const pointLight = new THREE.PointLight(0x86e8ff, 2, 20);
pointLight.position.set(0, 3, 0);
scene.add(pointLight);
// Instanced mesh
const geo = new THREE.SphereGeometry(0.15, 16, 16);
const mat = new THREE.MeshStandardMaterial({ metalness: 0.6, roughness: 0.3 });
const count = GRID * GRID;
const mesh = new THREE.InstancedMesh(geo, mat, count);
scene.add(mesh);
// Particle state
const dummy = new THREE.Object3D();
const color = new THREE.Color();
const particles = [];
const halfGrid = ((GRID - 1) * SPACING) / 2;
for (let i = 0; i < GRID; i++) {
for (let j = 0; j < GRID; j++) {
const x = i * SPACING - halfGrid;
const z = j * SPACING - halfGrid;
particles.push({
homeX: x,
homeZ: z,
x,
y: 0,
z,
vx: 0,
vy: 0,
vz: 0,
});
}
}
// Mouse → 3D plane intersection
const mouse = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
const intersectPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const mouseWorld = new THREE.Vector3();
document.addEventListener("mousemove", (e) => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
});
// Color palette
const coldColor = new THREE.Color("#86e8ff");
const warmColor = new THREE.Color("#ff40d6");
// Animate
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
// Raycast mouse to ground plane
raycaster.setFromCamera(mouse, camera);
raycaster.ray.intersectPlane(intersectPlane, mouseWorld);
for (let i = 0; i < count; i++) {
const p = particles[i];
if (!reduced) {
// Distance from mouse
const dx = p.x - mouseWorld.x;
const dz = p.z - mouseWorld.z;
const dist = Math.sqrt(dx * dx + dz * dz);
// Repulsion
if (dist < REPULSION_RADIUS && dist > 0.01) {
const force = (1 - dist / REPULSION_RADIUS) * REPULSION_FORCE;
p.vx += (dx / dist) * force;
p.vz += (dz / dist) * force;
p.vy += force * 0.5; // push up too
}
// Spring back to home
p.vx += (p.homeX - p.x) * SPRING;
p.vz += (p.homeZ - p.z) * SPRING;
p.vy += (0 - p.y) * SPRING;
// Damping
p.vx *= DAMPING;
p.vy *= DAMPING;
p.vz *= DAMPING;
// Integrate
p.x += p.vx;
p.y += p.vy;
p.z += p.vz;
}
// Update instance
dummy.position.set(p.x, p.y, p.z);
const scale = 1 + p.y * 0.8; // scale with height
dummy.scale.setScalar(Math.max(scale, 0.3));
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
// Color by distance from mouse
const dist = Math.sqrt((p.x - mouseWorld.x) ** 2 + (p.z - mouseWorld.z) ** 2);
const t2 = Math.min(dist / REPULSION_RADIUS, 1);
color.copy(warmColor).lerp(coldColor, t2);
mesh.setColorAt(i, color);
}
mesh.instanceMatrix.needsUpdate = true;
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
// Subtle camera sway
if (!reduced) {
camera.position.x = Math.sin(t * 0.15) * 0.5;
camera.lookAt(0, 0, 0);
}
renderer.render(scene, camera);
}
animate();
// Resize
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
window.addEventListener("beforeunload", () => {
geo.dispose();
mat.dispose();
renderer.dispose();
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive 3D Scene — stealthisdesign</title>
<link rel="stylesheet" href="style.css">
<script type="importmap">{"imports":{"gsap":"https://esm.sh/gsap@3.13.0","gsap/ScrollTrigger":"https://esm.sh/gsap@3.13.0/ScrollTrigger","gsap/SplitText":"https://esm.sh/gsap@3.13.0/SplitText","gsap/Flip":"https://esm.sh/gsap@3.13.0/Flip","gsap/ScrambleTextPlugin":"https://esm.sh/gsap@3.13.0/ScrambleTextPlugin","gsap/TextPlugin":"https://esm.sh/gsap@3.13.0/TextPlugin","gsap/all":"https://esm.sh/gsap@3.13.0/all","gsap/":"https://esm.sh/gsap@3.13.0/","lenis":"https://esm.sh/lenis@1.1.13/dist/lenis.mjs","three":"https://esm.sh/three@0.171.0","three/addons/":"https://esm.sh/three@0.171.0/examples/jsm/"}}</script>
</head>
<body>
<div id="canvas-container" aria-hidden="true"></div>
<div class="overlay">
<span class="eyebrow">Demo 10</span>
<h1>Interactive 3D</h1>
<p class="subtitle">A grid of 400 instanced spheres react to your mouse with repulsion physics and spring-based return. Colors shift by distance.</p>
</div>
<a href="/" class="btn-back">Back to Showcase</a>
<script type="module" src="script.js"></script>
</body>
</html>Interactive 3D Mouse Scene
Grid of 400 spheres with mouse-reactive repulsion physics, spring return, and distance-based color.
Source
- Repository:
libs-genclaude - Original demo id:
10-interactive-3d
Notes
Grid of 400 spheres with mouse-reactive repulsion physics, spring return, and distance-based color.