Skip to content

Instantly share code, notes, and snippets.

@Aakash67
Created March 26, 2025 22:05
Show Gist options
  • Select an option

  • Save Aakash67/c50c2388f4d73e9b6a5c35125147805e to your computer and use it in GitHub Desktop.

Select an option

Save Aakash67/c50c2388f4d73e9b6a5c35125147805e to your computer and use it in GitHub Desktop.
Interactive Bouncing Balls in a 3D Cube using Gemini 2.5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js Bouncing Balls in Cube</title>
<style>
body { margin: 0; overflow: hidden; background-color: #111; }
canvas { display: block; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
let scene, camera, renderer, controls;
let cube, cubeSize = 10;
let balls = [];
const ballRadius = 0.3;
const numBalls = 10;
const mouse = new THREE.Vector2();
const raycaster = new THREE.Raycaster(); // Not strictly needed for this interaction type, but good practice
// --- Initialization ---
function init() {
// Scene
scene = new THREE.Scene();
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, cubeSize * 0.7, cubeSize * 1.8); // Position camera outside the cube
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Controls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // Optional: adds inertia to camera movement
controls.dampingFactor = 0.05;
controls.maxDistance = 50;
controls.minDistance = 5;
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); // Soft white light
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7.5);
scene.add(directionalLight);
// Cube (using BoxHelper for visibility)
const cubeGeometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
// Use EdgesGeometry for a wireframe outline
const edges = new THREE.EdgesGeometry( cubeGeometry );
cube = new THREE.LineSegments( edges, new THREE.LineBasicMaterial( { color: 0xffffff, linewidth: 2 } ) );
// Optional: Add a barely visible transparent box if you prefer that look
/*
const cubeMaterial = new THREE.MeshBasicMaterial({
color: 0xaaaaaa,
transparent: true,
opacity: 0.1,
side: THREE.DoubleSide // Render both sides
});
cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
*/
scene.add(cube);
// Balls
const ballGeometry = new THREE.SphereGeometry(ballRadius, 16, 16); // Lower segments for performance if needed
for (let i = 0; i < numBalls; i++) {
const ballMaterial = new THREE.MeshStandardMaterial({
color: Math.random() * 0xffffff,
roughness: 0.5,
metalness: 0.3
});
const ballMesh = new THREE.Mesh(ballGeometry, ballMaterial);
// Initial position inside the cube, avoiding edges
const range = cubeSize / 2 - ballRadius * 1.1; // Add a small buffer
ballMesh.position.set(
THREE.MathUtils.randFloat(-range, range),
THREE.MathUtils.randFloat(-range, range),
THREE.MathUtils.randFloat(-range, range)
);
// Initial velocity
const velocity = new THREE.Vector3(
THREE.MathUtils.randFloat(-0.05, 0.05),
THREE.MathUtils.randFloat(-0.05, 0.05),
THREE.MathUtils.randFloat(-0.05, 0.05)
);
balls.push({ mesh: ballMesh, velocity: velocity });
scene.add(ballMesh);
}
// Event Listeners
window.addEventListener('resize', onWindowResize);
window.addEventListener('mousemove', onMouseMove);
}
// --- Event Handlers ---
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onMouseMove(event) {
// Normalize mouse coordinates (-1 to +1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
const halfCubeSize = cubeSize / 2;
const boundary = halfCubeSize - ballRadius;
const mouseInfluenceFactor = 0.001; // How strongly the mouse pushes balls
const mouseRepelDistance = 2; // How close the mouse needs to be to have an effect
// Calculate mouse position in world space (projected onto a plane)
// This is a simplified way to get a general direction
const mouseWorld = new THREE.Vector3(mouse.x, mouse.y, 0.5);
mouseWorld.unproject(camera); // Convert screen coords to world coords
const mouseDir = mouseWorld.sub(camera.position).normalize();
// Intersect with a plane roughly in the middle of the cube Z=0
const distance = -camera.position.z / mouseDir.z;
const mouseTarget = camera.position.clone().add(mouseDir.multiplyScalar(distance));
// Update balls
balls.forEach(ball => {
// Apply velocity
ball.mesh.position.add(ball.velocity);
// Apply mouse repulsion
const distToMouse = ball.mesh.position.distanceTo(mouseTarget);
if (distToMouse < mouseRepelDistance) {
const repelForce = ball.mesh.position.clone().sub(mouseTarget).normalize();
// Stronger repulsion closer to the mouse
repelForce.multiplyScalar(mouseInfluenceFactor * (mouseRepelDistance - distToMouse) / mouseRepelDistance);
// Limit max force to prevent instability
if (repelForce.lengthSq() > 0.005 * 0.005) {
repelForce.setLength(0.005);
}
ball.velocity.add(repelForce);
}
// Check for collisions with cube walls
// X-axis
if (ball.mesh.position.x > boundary) {
ball.mesh.position.x = boundary; // Clamp position
ball.velocity.x *= -1;
} else if (ball.mesh.position.x < -boundary) {
ball.mesh.position.x = -boundary; // Clamp position
ball.velocity.x *= -1;
}
// Y-axis
if (ball.mesh.position.y > boundary) {
ball.mesh.position.y = boundary; // Clamp position
ball.velocity.y *= -1;
} else if (ball.mesh.position.y < -boundary) {
ball.mesh.position.y = -boundary; // Clamp position
ball.velocity.y *= -1;
}
// Z-axis
if (ball.mesh.position.z > boundary) {
ball.mesh.position.z = boundary; // Clamp position
ball.velocity.z *= -1;
} else if (ball.mesh.position.z < -boundary) {
ball.mesh.position.z = -boundary; // Clamp position
ball.velocity.z *= -1;
}
// Optional: Add slight damping/friction
// ball.velocity.multiplyScalar(0.999);
});
// Update controls if damping is enabled
controls.update();
// Render the scene
renderer.render(scene, camera);
}
// --- Start ---
init();
animate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment