-
-
Save Aakash67/c50c2388f4d73e9b6a5c35125147805e to your computer and use it in GitHub Desktop.
Interactive Bouncing Balls in a 3D Cube using Gemini 2.5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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