Last active
June 10, 2025 18:57
-
-
Save qpwo/6c4044aec35534631e8ccac91c5c30a5 to your computer and use it in GitHub Desktop.
colliding cubes index html. and interlocked donuts
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> | |
<head> | |
<title>Bouncing 3D Cubes</title> | |
<style> | |
body { margin: 0; overflow: hidden; background-color: #000; } | |
#content { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
z-index: 1; | |
color: #fff; | |
font-family: sans-serif; | |
max-width: 600px; | |
text-align: center; | |
padding: 20px; | |
background: rgba(0,0,0,0.5); | |
border-radius: 8px; | |
pointer-events: none; /* Make text non-interactive */ | |
} | |
#refresh-indicator { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
z-index: 100; | |
font-size: 10vw; | |
color: white; | |
font-weight: bold; | |
font-family: sans-serif; | |
opacity: 1; | |
transition: opacity 1s ease-out; | |
pointer-events: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="refresh-indicator">REFRESH</div> | |
<div id="content"> | |
<h1>Lorem Ipsum</h1> | |
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script> | |
let scene, camera, renderer, cubes = []; | |
const numCubes = 40; | |
function init() { | |
// Scene | |
scene = new THREE.Scene(); | |
// Camera | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); | |
camera.position.z = 600; | |
// Renderer | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.insertBefore(renderer.domElement, document.getElementById('content')); | |
// Lighting | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9); | |
directionalLight.position.set(1, 1, 1); | |
scene.add(directionalLight); | |
// Cubes | |
for (let i = 0; i < numCubes; i++) { | |
const size = Math.random() * 30 + 10; | |
const geometry = new THREE.BoxGeometry(size, size, size); | |
geometry.computeBoundingSphere(); // Important for collision detection | |
const material = new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff }); | |
const cube = new THREE.Mesh(geometry, material); | |
cube.position.set( | |
(Math.random() - 0.5) * 600, | |
(Math.random() - 0.5) * 600, | |
(Math.random() - 0.5) * 600 | |
); | |
cube.velocity = new THREE.Vector3( | |
(Math.random() - 0.5) * 2, | |
(Math.random() - 0.5) * 2, | |
(Math.random() - 0.5) * 2 | |
); | |
cube.rotationSpeed = new THREE.Vector3( | |
Math.random() * 0.02, | |
Math.random() * 0.02, | |
Math.random() * 0.02 | |
); | |
// Assign spin direction | |
cube.spinDirection = (Math.random() < 0.25) ? -1 : 1; | |
cube.mass = size; // Mass proportional to size | |
cubes.push(cube); | |
scene.add(cube); | |
} | |
animate(); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
const gravityCenter = new THREE.Vector3(0, 0, 0); | |
const gravityForce = 0.4; | |
const terminalVelocity = 12; | |
for (let i = 0; i < cubes.length; i++) { | |
const cubeA = cubes[i]; | |
// Physics | |
let direction = gravityCenter.clone().sub(cubeA.position); | |
let distance = direction.length(); | |
direction.normalize().multiplyScalar(gravityForce); | |
cubeA.velocity.add(direction); | |
if (cubeA.velocity.length() > terminalVelocity) { | |
cubeA.velocity.clampLength(0, terminalVelocity); | |
} | |
let tangent = new THREE.Vector3(-cubeA.position.y, cubeA.position.x, 0).normalize(); | |
cubeA.velocity.add(tangent.multiplyScalar(0.25 * cubeA.spinDirection)); | |
// Collisions | |
for (let j = i + 1; j < cubes.length; j++) { | |
const cubeB = cubes[j]; | |
const dist = cubeA.position.distanceTo(cubeB.position); | |
const rA = cubeA.geometry.boundingSphere.radius; | |
const rB = cubeB.geometry.boundingSphere.radius; | |
if (dist < rA + rB) { | |
// Simplified elastic collision response | |
const normal = cubeA.position.clone().sub(cubeB.position).normalize(); | |
const vA = cubeA.velocity.clone(); | |
const vB = cubeB.velocity.clone(); | |
cubeA.velocity = vB; | |
cubeB.velocity = vA; | |
// Move them apart to prevent sticking | |
const overlap = (rA + rB) - dist; | |
cubeA.position.add(normal.clone().multiplyScalar(overlap / 2)); | |
cubeB.position.add(normal.clone().negate().multiplyScalar(overlap / 2)); | |
} | |
} | |
cubeA.position.add(cubeA.velocity); | |
cubeA.rotation.x += cubeA.rotationSpeed.x; | |
cubeA.rotation.y += cubeA.rotationSpeed.y; | |
cubeA.rotation.z += cubeA.rotationSpeed.z; | |
// Boundary bounce | |
const boundaryRadius = 500; | |
if (cubeA.position.length() > boundaryRadius) { | |
const boundaryNormal = cubeA.position.clone().normalize(); | |
cubeA.velocity.reflect(boundaryNormal); | |
cubeA.position.clampLength(0, boundaryRadius); | |
} | |
} | |
renderer.render(scene, camera); | |
} | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}, false); | |
init(); | |
// Flash REFRESH text | |
const refreshIndicator = document.getElementById('refresh-indicator'); | |
setTimeout(() => { | |
refreshIndicator.style.opacity = '0'; | |
}, 100); | |
setTimeout(() => { | |
refreshIndicator.remove(); | |
}, 1100); | |
</script> | |
</body> | |
</html> |
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> | |
<!-- | |
GOALS & CONSTRAINTS: | |
1. Simulation: Create a physics-based simulation of multiple interlocked donut pairs. | |
2. Initial Placement: Each pair must start perfectly interlocked. | |
- Geometric Definition: The central circle (major radius) of each donut in a pair must pass through the center of the other. The planes containing these circles must be perpendicular. | |
3. Physics Model: | |
- Each donut is a completely independent object after initialization. | |
- Collision detection must use a "pearl necklace" model (a ring of discrete spheres) for accuracy. | |
- Donuts are influenced by a central gravity-like force. | |
- A tangential force must be added to create orbital motion and prevent clumping at the center. | |
- Donuts collide with each other and bounce off a spherical world boundary. | |
FEEDBACK HISTORY & CORRECTIONS: | |
- Initial attempts used flawed math for interlocking and inaccurate collision boxes. | |
- A version that grouped pairs was rejected; donuts must be simulated separately. | |
- The "pearl necklace" collision model was introduced, but interlocking math was still incorrect, and a new "clumping" gravity issue appeared. | |
- The gravity/clumping issue was fixed by reintroducing tangential velocity. | |
- The placement logic was improved by using a temporary group but was still flawed, offsetting a donut by its radius, which is incorrect. | |
- This version implements the definitive method for initial placement: | |
a. Two donuts are created in a local group, SHARING THE SAME CENTER (0,0,0). | |
b. One donut is rotated to lie on one plane (e.g., XY), and the other is rotated to lie on a perpendicular plane (e.g., XZ). This creates the perfect interlock. | |
c. This entire local group is then given a random position and orientation in the world. | |
d. The final world coordinates are extracted for each donut, and they are then simulated independently. | |
--> | |
<html> | |
<head> | |
<title>Interlocked Donuts</title> | |
<style> | |
body { margin: 0; overflow: hidden; background-color: #000; } | |
#content { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
z-index: 1; | |
color: #fff; | |
font-family: sans-serif; | |
max-width: 600px; | |
text-align: center; | |
padding: 20px; | |
background: rgba(0,0,0,0.5); | |
border-radius: 8px; | |
pointer-events: none; | |
} | |
#refresh-indicator { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
z-index: 100; | |
font-size: 10vw; | |
color: white; | |
font-weight: bold; | |
font-family: sans-serif; | |
opacity: 1; | |
transition: opacity 1s ease-out; | |
pointer-events: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="refresh-indicator">REFRESH</div> | |
<div id="content"> | |
<h1>Interlocked Donuts</h1> | |
<p>A complex physics simulation where each donut is an independent object with a "pearl necklace" collision model.</p> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script> | |
let scene, camera, renderer, donuts = []; | |
const numPairs = 20; | |
const collisionSpheresPerDonut = 12; | |
function init() { | |
scene = new THREE.Scene(); | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); | |
camera.position.z = 700; | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.insertBefore(renderer.domElement, document.getElementById('content')); | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); | |
directionalLight.position.set(1, 1, 1); | |
scene.add(directionalLight); | |
for (let i = 0; i < numPairs; i++) { | |
const centerPos = new THREE.Vector3( (Math.random() - 0.5) * 1000, (Math.random() - 0.5) * 1000, (Math.random() - 0.5) * 1000 ); | |
const initialOrientation = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.random() * Math.PI * 2, Math.random() * Math.PI * 2, Math.random() * Math.PI * 2)); | |
const torusRadius = Math.random() * 20 + 15; | |
const tubeRadius = torusRadius * 0.4; | |
const initialVel = new THREE.Vector3( (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2 ); | |
const initialAngVel = new THREE.Vector3().random().subScalar(0.5).multiplyScalar(0.02); | |
const spinDirection = (Math.random() < 0.5) ? -1 : 1; | |
const material1 = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff, metalness: 0.4, roughness: 0.5 }); | |
const material2 = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff, metalness: 0.4, roughness: 0.5 }); | |
const geometry = new THREE.TorusGeometry(torusRadius, tubeRadius, 16, 32); | |
const donut1 = new THREE.Mesh(geometry, material1); | |
const donut2 = new THREE.Mesh(geometry, material2); | |
// --- Correct Interlocking Placement via a Temporary Group --- | |
const group = new THREE.Group(); | |
// 1. Place donuts at the group's origin and rotate them into perpendicular planes. | |
// Donut 1 is left on the default XY plane. | |
// Donut 2 is rotated 90 degrees around the X-axis to lie on the XZ plane. | |
donut2.rotation.x = Math.PI / 2; | |
group.add(donut1, donut2); | |
// 2. Apply random world position and orientation to the whole pair. | |
group.position.copy(centerPos); | |
group.quaternion.copy(initialOrientation); | |
group.updateMatrixWorld(true); // Crucial: updates children's world matrices. | |
// 3. Extract the final world transforms and add donuts to the scene as independent objects. | |
scene.add(donut1, donut2); | |
donut1.position.setFromMatrixPosition(donut1.matrixWorld); | |
donut1.quaternion.setFromRotationMatrix(donut1.matrixWorld); | |
donut2.position.setFromMatrixPosition(donut2.matrixWorld); | |
donut2.quaternion.setFromRotationMatrix(donut2.matrixWorld); | |
[donut1, donut2].forEach(donut => { | |
donut.velocity = initialVel.clone(); | |
donut.angularVelocity = initialAngVel.clone(); | |
donut.torusRadius = torusRadius; | |
donut.tubeRadius = tubeRadius; | |
donut.spinDirection = spinDirection; | |
donut.collisionSpheres = []; | |
for (let j = 0; j < collisionSpheresPerDonut; j++) { | |
const angle = (j / collisionSpheresPerDonut) * Math.PI * 2; | |
donut.collisionSpheres.push(new THREE.Vector3(Math.cos(angle) * torusRadius, Math.sin(angle) * torusRadius, 0)); | |
} | |
donuts.push(donut); | |
}); | |
} | |
animate(); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
const gravityCenter = new THREE.Vector3(0, 0, 0); | |
const gravityForce = 0.2; | |
const terminalVelocity = 15; | |
donuts.forEach(donut => { | |
let direction = gravityCenter.clone().sub(donut.position); | |
donut.velocity.add(direction.normalize().multiplyScalar(gravityForce)); | |
let tangent = new THREE.Vector3(-donut.position.y, donut.position.x, 0).normalize(); | |
donut.velocity.add(tangent.multiplyScalar(0.2 * donut.spinDirection)); | |
if (donut.velocity.length() > terminalVelocity) donut.velocity.clampLength(0, terminalVelocity); | |
donut.position.add(donut.velocity); | |
const deltaRotation = new THREE.Quaternion().setFromEuler(new THREE.Euler(donut.angularVelocity.x, donut.angularVelocity.y, donut.angularVelocity.z)); | |
donut.quaternion.multiplyQuaternions(deltaRotation, donut.quaternion); | |
}); | |
for (let i = 0; i < donuts.length; i++) { | |
for (let j = i + 1; j < donuts.length; j++) { | |
const d1 = donuts[i]; | |
const d2 = donuts[j]; | |
if (d1.position.distanceTo(d2.position) > d1.torusRadius + d1.tubeRadius + d2.torusRadius + d2.tubeRadius) continue; | |
for (const s1_local of d1.collisionSpheres) { | |
const s1_world = s1_local.clone().applyQuaternion(d1.quaternion).add(d1.position); | |
for (const s2_local of d2.collisionSpheres) { | |
const s2_world = s2_local.clone().applyQuaternion(d2.quaternion).add(d2.position); | |
const dist = s1_world.distanceTo(s2_world); | |
if (dist < d1.tubeRadius + d2.tubeRadius) { | |
const v1 = d1.velocity.clone(); | |
const v2 = d2.velocity.clone(); | |
d1.velocity.lerp(v2, 0.5); | |
d2.velocity.lerp(v1, 0.5); | |
const normal = s1_world.clone().sub(s2_world).normalize(); | |
const overlap = (d1.tubeRadius + d2.tubeRadius) - dist; | |
d1.position.add(normal.clone().multiplyScalar(overlap / 2)); | |
d2.position.add(normal.clone().negate().multiplyScalar(overlap / 2)); | |
goto_next_donut_pair_check:; | |
} | |
} | |
} | |
if (false) goto_next_donut_pair_check:; | |
} | |
} | |
donuts.forEach(donut => { | |
const boundaryRadius = 600; | |
if (donut.position.length() > boundaryRadius) { | |
donut.velocity.reflect(donut.position.clone().normalize()); | |
donut.position.clampLength(0, boundaryRadius); | |
} | |
}); | |
renderer.render(scene, camera); | |
} | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}, false); | |
init(); | |
const refreshIndicator = document.getElementById('refresh-indicator'); | |
setTimeout(() => { refreshIndicator.style.opacity = '0'; }, 100); | |
setTimeout(() => { refreshIndicator.remove(); }, 1100); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment