From 7edf2d2a9a2e4e5e60316d1293638d2795602afb Mon Sep 17 00:00:00 2001 From: Agnibha Debnath Date: Sat, 16 May 2026 15:01:46 +0530 Subject: [PATCH 1/2] Add 1D collision simulation with vector visualization --- app/(core)/data/chapters.js | 10 +- .../data/configs/CollisionSimulation.js | 153 ++++++ public/icons/collision.png | Bin 0 -> 20796 bytes simulations/CollisionSimulation.jsx | 507 ++++++++++++++++++ 4 files changed, 669 insertions(+), 1 deletion(-) create mode 100644 app/(core)/data/configs/CollisionSimulation.js create mode 100644 public/icons/collision.png create mode 100644 simulations/CollisionSimulation.jsx diff --git a/app/(core)/data/chapters.js b/app/(core)/data/chapters.js index 065c43d..8a89043 100644 --- a/app/(core)/data/chapters.js +++ b/app/(core)/data/chapters.js @@ -106,7 +106,15 @@ const chapters = [ icon: "/icons/pendulam.png", }, { - id: 0, + id: 13, + name: "1D Collision simulation", + desc: "Simulate 1D elastic collisions with velocity vectors, momentum, and kinetic energy visualization.", + link: "/simulations/CollisionSimulation", + tags: [TAGS.PHYSICS, TAGS.MEDIUM, TAGS.COLLISION, TAGS.VECTORS], + icon: "/icons/collision.png", + }, + { + id: 14, name: "Browser Performance & Stress Test", desc: "Push your browser to its limits with this physics-based stress test. Benchmark your device's rendering performance with hundreds of simultaneous physics simulations.", link: "/simulations/test", diff --git a/app/(core)/data/configs/CollisionSimulation.js b/app/(core)/data/configs/CollisionSimulation.js new file mode 100644 index 0000000..134aa21 --- /dev/null +++ b/app/(core)/data/configs/CollisionSimulation.js @@ -0,0 +1,153 @@ +// Default simulation inputs +export const INITIAL_INPUTS = { + // Mass of both collision bodies + mass1: 1, // kg + mass2: 1, // kg + + // Ball diameter in simulation units + size1: 0.55, + size2: 0.55, + + // Initial velocities + velocity1: 2, + velocity2: 2, + + // Coefficient of restitution + restitution: 1, + + // Visual effects + trailEnabled: true, + showVectors: true, + + // Ball colors + ballColor1: "#ff4444", + ballColor2: "#4488ff", +}; + +// Dynamic input configuration +export const INPUT_FIELDS = [ + // Mass controls + { + name: "mass1", + label: "m₁ - Ball 1 Mass (kg):", + type: "number", + min: 0, + placeholder: "Enter mass of ball 1", + }, + + { + name: "mass2", + label: "m₂ - Ball 2 Mass (kg):", + type: "number", + placeholder: "Enter mass of ball 2", + min: 0, + }, + + // Velocity controls + { + name: "velocity1", + label: "u₁ - Ball 1 Velocity (m/s):", + type: "number", + placeholder: "Enter velocity of ball 1", + }, + + { + name: "velocity2", + label: "u₂ - Ball 2 Velocity (m/s):", + type: "number", + placeholder: "Enter velocity of ball 2", + }, + + // Elasticity control + { + name: "restitution", + label: "e - Restitution:", + type: "number", + min: 0, + max: 1, + step: 0.1, + placeholder: "0 to 1", + }, + + // Trail rendering toggle + { + name: "trailEnabled", + label: "Enable trail", + type: "checkbox", + }, + + // Velocity vector toggle + { + name: "showVectors", + label: "Show vectors", + type: "checkbox", + }, + + // Ball color controls + { + name: "ballColor1", + label: "1st ball", + type: "color", + }, + + { + name: "ballColor2", + label: "2nd ball", + type: "color", + }, +]; + +// Maps simulation data +// for SimInfoPanel +export const SimInfoMapper = ({ body1, body2, initialState }) => { + // Initial velocities + const u1 = Math.abs(initialState?.u1 ?? 2); + + const u2 = Math.abs(initialState?.u2 ?? 2); + + // Mass values + const m1 = body1.params.mass; + const m2 = body2.params.mass; + + // Coefficient of restitution + const e = body1.params.restitution; + + // Current velocities + const v1 = body1.state.velocity.x; + + const v2 = body2.state.velocity.x; + + // Linear momentum + const p1 = body1.params.mass * v1; + + const p2 = body2.params.mass * v2; + + // Kinetic energy + const ke1 = 0.5 * body1.params.mass * v1 * v1; + + const ke2 = 0.5 * body2.params.mass * v2 * v2; + + return { + "m₁ (mass)": `${m1.toFixed(2)} kg`, + + "m₂ (mass)": `${m2.toFixed(2)} kg`, + + "e (restitution)": `${e.toFixed(2)}`, + + "u₁ (initial velocity)": `${u1.toFixed(2)} m/s`, + + "u₂ (initial velocity)": `${u2.toFixed(2)} m/s`, + + "v₁ (current velocity)": `${v1.toFixed(2)} m/s`, + + "v₂ (current velocity)": `${v2.toFixed(2)} m/s`, + + "p₁ (momentum)": `${p1.toFixed(2)} kg·m/s`, + + "p₂ (momentum)": `${p2.toFixed(2)} kg·m/s`, + + "KE₁": `${ke1.toFixed(2)} J`, + + "KE₂": `${ke2.toFixed(2)} J`, + }; +}; diff --git a/public/icons/collision.png b/public/icons/collision.png new file mode 100644 index 0000000000000000000000000000000000000000..cc01cfe0ca4f3eaec56eaacc375126de622a97ad GIT binary patch literal 20796 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 { + // Prevent invalid restitution values + if (name === "restitution") { + const num = Number(value); + + if (num < 0 || num > 1) return; + } + setInputs((prev) => ({ + ...prev, + [name]: value, + })); + }, + [setInputs] + ); + + const theory = useMemo( + () => chapters.find((ch) => ch.link === location)?.theory, + [location] + ); + + const sketch = useCallback( + (p) => { + let trailLayer = null; + + const setupSimulation = () => { + // Compute Initial Positions + + const minX = toMeters(p.width * 0.2); + const maxX = toMeters(p.width * 0.8); + + const initialX1 = minX + (maxX - minX) * 0.25; + const initialX2 = minX + (maxX - minX) * 0.75; + + const initialY1 = toMeters(p.height / 2); + const initialY2 = toMeters(p.height / 2); + + // Create or Reset Bodies + + if (!bodiesRef.current.length) { + bodiesRef.current = [ + new PhysicsBody(p, { + id: "m 1", + mass: inputsRef.current.mass1, + size: inputsRef.current.size1, + color: inputsRef.current.ballColor1, + shape: "circle", + restitution: inputsRef.current.restitution, + position: p.createVector(initialX1, initialY1), + }), + new PhysicsBody(p, { + id: "m 2", + mass: inputsRef.current.mass2, + size: inputsRef.current.size2, + color: inputsRef.current.ballColor2, + shape: "circle", + restitution: inputsRef.current.restitution, + position: p.createVector(initialX2, initialY2), + }), + ]; + } else { + bodiesRef.current[0].reset({ + position: p.createVector(initialX1, initialY1), + }); + + bodiesRef.current[1].reset({ + position: p.createVector(initialX2, initialY2), + }); + } + + // Initialize Velocities + bodiesRef.current[0].state.velocity.x = Math.abs( + inputsRef.current.velocity1 + ); + + bodiesRef.current[1].state.velocity.x = -Math.abs( + inputsRef.current.velocity2 + ); + + // Store Initial Simulation State + initialStateRef.current = { + u1: Math.abs(inputsRef.current.velocity1), + u2: Math.abs(inputsRef.current.velocity2), + }; + + // Update Simulation Info Panel + updateSimInfo( + p, + { + body1: bodiesRef.current[0], + body2: bodiesRef.current[1], + initialState: initialStateRef.current, + }, + {}, + SimInfoMapper + ); + + // Configure Trail Rendering + bodiesRef.current[0].trail.enabled = inputsRef.current.trailEnabled; + + bodiesRef.current[0].trail.maxLength = 200; + + bodiesRef.current[0].trail.color = inputsRef.current.ballColor1; + bodiesRef.current[1].trail.enabled = inputsRef.current.trailEnabled; + + bodiesRef.current[1].trail.maxLength = 200; + + bodiesRef.current[1].trail.color = inputsRef.current.ballColor2; + + // Update Physics Parameters + const { mass1, mass2, ballColor1, ballColor2, restitution } = + inputsRef.current; + + // Scale objects for smaller screens + const screenScale = p.width < 600 ? 0.7 : 1; + + bodiesRef.current[0].updateParams({ + mass: mass1, + size: inputsRef.current.size1 * screenScale, + color: ballColor1, + restitution, + }); + + bodiesRef.current[1].updateParams({ + mass: mass2, + size: inputsRef.current.size2 * screenScale, + color: ballColor2, + restitution, + }); + + // Initialize Vector Renderer + if (!forceRendererRef.current) { + forceRendererRef.current = new ForceRenderer({ + scale: 20, + showMagnitude: false, + labelSize: 14, + }); + } + + // Initialize Drag Controller + if (!dragControllerRef.current) { + dragControllerRef.current = new DragController({ + snapBack: false, + smoothing: 0.3, + }); + } + }; + + p.setup = () => { + const { clientWidth: w, clientHeight: h } = p._userNode; + p.createCanvas(w, h); + + trailLayer = p.createGraphics(w, h); + trailLayer.pixelDensity(1); + trailLayer.clear(); + + setupSimulation(); + + const bg = getBackgroundColor(); + const [r, g, b] = Array.isArray(bg) ? bg : [20, 20, 30]; + trailLayer.background(r, g, b); + p.background(r, g, b); + }; + + p.draw = () => { + // Early Exit Checks + if (!bodiesRef.current.length || !trailLayer) return; + + const dt = computeDelta(p); + if (dt <= 0) return; + + // Sync Visual Properties + const { ballColor1, ballColor2, trailEnabled } = inputsRef.current; + bodiesRef.current[0].params.color = ballColor1; + + bodiesRef.current[1].params.color = ballColor2; + + bodiesRef.current[0].trail.enabled = trailEnabled; + bodiesRef.current[0].trail.color = ballColor1; + + bodiesRef.current[1].trail.enabled = trailEnabled; + bodiesRef.current[1].trail.color = ballColor2; + + // Physics Update + if (!dragControllerRef.current.isDragging()) { + // Update body positions + bodiesRef.current.forEach((body) => { + body.step(dt); + }); + + // Ball-to-Ball Collision + const body1 = bodiesRef.current[0]; + const body2 = bodiesRef.current[1]; + const dx = body2.state.position.x - body1.state.position.x; + const distance = Math.abs(dx); + const minDistance = (body1.params.size + body2.params.size) / 2; + + // Prevent repeated collision + // response while separating + const relativeVelocity = + body2.state.velocity.x - body1.state.velocity.x; + + if (distance <= minDistance && relativeVelocity < 0) { + const u1 = body1.state.velocity.x; + const u2 = body2.state.velocity.x; + + const m1 = body1.params.mass; + const m2 = body2.params.mass; + + const e = body1.params.restitution; + + const v1 = ((m1 - e * m2) * u1 + (1 + e) * m2 * u2) / (m1 + m2); + + const v2 = ((m2 - e * m1) * u2 + (1 + e) * m1 * u1) / (m1 + m2); + + body1.state.velocity.x = v1; + body2.state.velocity.x = v2; + + // Resolve overlap + const overlap = minDistance - distance; + + if (overlap > 0) { + body1.state.position.x -= overlap / 2; + body2.state.position.x += overlap / 2; + } + } + + // Wall Collision + const leftWall = p.width * 0.2; + const rightWall = p.width * 0.8; + + const minX = toMeters(leftWall); + const maxX = toMeters(rightWall); + + bodiesRef.current.forEach((body) => { + // Keep motion horizontal + body.state.position.y = toMeters(p.height / 2); + const radius = body.params.size / 2; + + // Left wall collision + if (body.state.position.x - radius < minX) { + body.state.position.x = minX + radius; + body.state.velocity.x *= -body.params.restitution; + } + + // Right wall collision + if (body.state.position.x + radius > maxX) { + body.state.position.x = maxX - radius; + body.state.velocity.x *= -body.params.restitution; + } + }); + } + + // Update Simulation Info + if (p.frameCount % 5 === 0) { + updateSimInfo( + p, + { + body1: bodiesRef.current[0], + body2: bodiesRef.current[1], + initialState: initialStateRef.current, + }, + {}, + SimInfoMapper + ); + } + renderScene(p); + }; + + const renderScene = (p) => { + // Current background color + const bg = getBackgroundColor(); + + const [r, g, b] = Array.isArray(bg) ? bg : [20, 20, 30]; + + // Create fading trail effect + if (inputsRef.current.trailEnabled) { + trailLayer.fill(r, g, b, 60); + trailLayer.noStroke(); + + trailLayer.rect(0, 0, trailLayer.width, trailLayer.height); + + p.clear(); + p.image(trailLayer, 0, 0); + } else { + trailLayer.background(r, g, b); + p.background(r, g, b); + } + + // Simulation boundaries + const leftWall = p.width * 0.2; + const rightWall = p.width * 0.8; + + p.stroke(150); + p.strokeWeight(5); + + p.line(leftWall, 0, leftWall, p.height); + p.line(rightWall, 0, rightWall, p.height); + + bodiesRef.current.forEach((body) => { + body.checkHover(p, body.toScreenPosition()); + + // Draw collision body + body.draw(p, { + hoverEffect: true, + }); + + const pos = body.toScreenPosition(); + + // Velocity vector + if (inputsRef.current.showVectors) { + forceRendererRef.current.drawVector( + p, + pos.x, + pos.y, + body.state.velocity.x, + 0, + "#aaaaaa" + ); + } + + // Body label + p.fill(255); + p.noStroke(); + + p.textAlign(p.CENTER); + + p.textSize(p.width < 600 ? 14 : 22); + + p.textFont("Poppins"); + + p.text(body.params.id, pos.x, pos.y - 36); + }); + }; + + // Update dragged body position + p.mouseDragged = () => { + dragControllerRef.current.handleDrag(p); + }; + + // Release dragged body + p.mouseReleased = () => { + dragControllerRef.current.handleRelease(); + }; + + // Keep canvas and simulation responsive + p.windowResized = () => { + const { clientWidth: w, clientHeight: h } = p._userNode; + p.resizeCanvas(w, h); + setCanvasHeight(h); + + // Recreate trail layer + if (trailLayer) trailLayer.remove(); + trailLayer = p.createGraphics(w, h); + trailLayer.pixelDensity(1); + + const bg = getBackgroundColor(); + const [r, g, b] = Array.isArray(bg) ? bg : [20, 20, 30]; + trailLayer.background(r, g, b); + + // Reinitialize simulation if + // bodies were not created yet + if (!bodiesRef.current.length) { + setupSimulation(); + return; + } + + // Keep bodies inside canvas bounds + bodiesRef.current.forEach((body) => { + const { size } = body.params; + const radius = size / 2; + + const maxX = toMeters(w) - radius; + const maxY = toMeters(h) - radius; + + if (body.state.position.x > maxX) body.state.position.x = maxX; + + if (body.state.position.y > maxY) body.state.position.y = maxY; + + if (body.state.position.x < radius) body.state.position.x = radius; + + if (body.state.position.y < radius) body.state.position.y = radius; + }); + }; + }, + [inputsRef, updateSimInfo] + ); + + // Cleanup physics bodies on unmount + useEffect(() => { + return () => { + bodiesRef.current = []; + }; + }, []); + + // Reset simulation state and timing + const handleReset = useCallback(() => { + const wasPaused = isPaused(); + + resetTime(); + + setTimeout(() => { + // Preserve paused state after reset + if (wasPaused) setPause(true); + + // Force p5 canvas remount + setResetVersion((v) => v + 1); + }, 0); + }, []); + + return ( + { + setInputs(loadedInputs); + setResetVersion((v) => v + 1); + }} + theory={theory} + dynamicInputs={ + + } + > + } + /> + + ); +} From 9d55e1a78c49260d7d878d8d73a2f9313de57cad Mon Sep 17 00:00:00 2001 From: Agnibha Debnath Date: Sat, 16 May 2026 17:05:00 +0530 Subject: [PATCH 2/2] fix: improve simulation info update timing --- .../data/configs/CollisionSimulation.js | 4 +- simulations/CollisionSimulation.jsx | 69 +++++++++---------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/app/(core)/data/configs/CollisionSimulation.js b/app/(core)/data/configs/CollisionSimulation.js index 134aa21..57ee7c8 100644 --- a/app/(core)/data/configs/CollisionSimulation.js +++ b/app/(core)/data/configs/CollisionSimulation.js @@ -101,9 +101,9 @@ export const INPUT_FIELDS = [ // for SimInfoPanel export const SimInfoMapper = ({ body1, body2, initialState }) => { // Initial velocities - const u1 = Math.abs(initialState?.u1 ?? 2); + const u1 = Math.abs(initialState?.u1 ?? 0); - const u2 = Math.abs(initialState?.u2 ?? 2); + const u2 = Math.abs(initialState?.u2 ?? 0); // Mass values const m1 = body1.params.mass; diff --git a/simulations/CollisionSimulation.jsx b/simulations/CollisionSimulation.jsx index 2cfbb62..3e77e30 100644 --- a/simulations/CollisionSimulation.jsx +++ b/simulations/CollisionSimulation.jsx @@ -146,24 +146,6 @@ export default function CollisionSimulation() { inputsRef.current.velocity2 ); - // Store Initial Simulation State - initialStateRef.current = { - u1: Math.abs(inputsRef.current.velocity1), - u2: Math.abs(inputsRef.current.velocity2), - }; - - // Update Simulation Info Panel - updateSimInfo( - p, - { - body1: bodiesRef.current[0], - body2: bodiesRef.current[1], - initialState: initialStateRef.current, - }, - {}, - SimInfoMapper - ); - // Configure Trail Rendering bodiesRef.current[0].trail.enabled = inputsRef.current.trailEnabled; @@ -213,6 +195,23 @@ export default function CollisionSimulation() { smoothing: 0.3, }); } + // Store Initial Simulation State + initialStateRef.current = { + u1: Math.abs(inputsRef.current.velocity1), + u2: Math.abs(inputsRef.current.velocity2), + }; + + // Update Simulation Info Panel + updateSimInfo( + p, + { + body1: bodiesRef.current[0], + body2: bodiesRef.current[1], + initialState: initialStateRef.current, + }, + {}, + SimInfoMapper + ); }; p.setup = () => { @@ -321,18 +320,18 @@ export default function CollisionSimulation() { } // Update Simulation Info - if (p.frameCount % 5 === 0) { - updateSimInfo( - p, - { - body1: bodiesRef.current[0], - body2: bodiesRef.current[1], - initialState: initialStateRef.current, - }, - {}, - SimInfoMapper - ); - } + + updateSimInfo( + p, + { + body1: bodiesRef.current[0], + body2: bodiesRef.current[1], + initialState: initialStateRef.current, + }, + {}, + SimInfoMapper + ); + renderScene(p); }; @@ -468,13 +467,11 @@ export default function CollisionSimulation() { resetTime(); - setTimeout(() => { - // Preserve paused state after reset - if (wasPaused) setPause(true); + // Preserve paused state after reset + if (wasPaused) setPause(true); - // Force p5 canvas remount - setResetVersion((v) => v + 1); - }, 0); + // Force p5 canvas remount + setResetVersion((v) => v + 1); }, []); return (