Created
April 21, 2025 08:22
-
-
Save shricodev/d4a3e1d9e02cfd83a80ae11ecba3d38e to your computer and use it in GitHub Desktop.
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> | |
<meta charset="utf-8" /> | |
<title>Galton Board</title> | |
<style> | |
html, | |
body { | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
background: #fff; | |
} | |
#controls { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
color: #000; | |
font-family: sans-serif; | |
z-index: 10; | |
} | |
#controls input { | |
vertical-align: middle; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="controls"> | |
Gravity: | |
<input | |
type="range" | |
id="gravitySlider" | |
min="0" | |
max="2" | |
step="0.01" | |
value="1" | |
/> | |
</div> | |
<canvas id="world"></canvas> | |
<!-- Matter.js --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script> | |
<script> | |
(function () { | |
const { | |
Engine, | |
Render, | |
Runner, | |
World, | |
Bodies, | |
Body, | |
Events, | |
Constraint, | |
} = Matter; | |
const WIDTH = 500, | |
HEIGHT = 700; | |
const engine = Engine.create(); | |
engine.gravity.y = 1; | |
const world = engine.world; | |
// renderer | |
const canvas = document.getElementById("world"); | |
canvas.width = WIDTH; | |
canvas.height = HEIGHT; | |
const render = Render.create({ | |
canvas: canvas, | |
engine: engine, | |
options: { | |
width: WIDTH, | |
height: HEIGHT, | |
wireframes: false, | |
background: "#fff", | |
}, | |
}); | |
Render.run(render); | |
Runner.run(Runner.create(), engine); | |
// colors & thickness | |
const wallColor = "yellow", | |
pegColor = "red", | |
paddleColor = "orange", | |
ballColor = "#000"; | |
const T = 20; | |
// walls & ground | |
World.add(world, [ | |
Bodies.rectangle(WIDTH / 2, HEIGHT + T / 2, WIDTH, T, { | |
isStatic: true, | |
render: { fillStyle: wallColor }, | |
}), | |
Bodies.rectangle(-T / 2, HEIGHT / 2, T, HEIGHT, { | |
isStatic: true, | |
render: { fillStyle: wallColor }, | |
}), | |
Bodies.rectangle(WIDTH + T / 2, HEIGHT / 2, T, HEIGHT, { | |
isStatic: true, | |
render: { fillStyle: wallColor }, | |
}), | |
]); | |
// bins (thick dividers) | |
const BIN_COUNT = 10, | |
binW = WIDTH / BIN_COUNT; | |
for (let i = 0; i <= BIN_COUNT; i++) { | |
World.add( | |
world, | |
Bodies.rectangle(i * binW, HEIGHT - 100, 4, 200, { | |
isStatic: true, | |
render: { fillStyle: wallColor }, | |
}), | |
); | |
} | |
// pegs – fewer & farther apart | |
const pegR = 6, | |
pegRows = 4, | |
pegSpacingX = 80, | |
pegSpacingY = 100, | |
pegStartY = 180, | |
pegCols = Math.floor(WIDTH / pegSpacingX); | |
for (let row = 0; row < pegRows; row++) { | |
for (let col = 0; col < pegCols; col++) { | |
const x = | |
col * pegSpacingX + | |
(row % 2 ? pegSpacingX / 2 : 0) + | |
pegSpacingX / 2; | |
const y = pegStartY + row * pegSpacingY; | |
World.add( | |
world, | |
Bodies.circle(x, y, pegR, { | |
isStatic: true, | |
render: { fillStyle: pegColor }, | |
}), | |
); | |
} | |
} | |
// spinning paddles | |
const paddles = []; | |
const paddleYs = [250, 390, 530]; | |
paddleYs.forEach((py) => { | |
const paddle = Bodies.rectangle(WIDTH / 2, py, 200, 10, { | |
render: { fillStyle: paddleColor }, | |
}); | |
const pivot = Constraint.create({ | |
pointA: { x: WIDTH / 2, y: py }, | |
bodyB: paddle, | |
pointB: { x: 0, y: 0 }, | |
stiffness: 1, | |
length: 0, | |
}); | |
paddle.spinSpeed = | |
(0.02 + Math.random() * 0.03) * (Math.random() < 0.5 ? -1 : 1); | |
World.add(world, [paddle, pivot]); | |
paddles.push(paddle); | |
}); | |
Events.on(engine, "beforeUpdate", () => { | |
paddles.forEach((p) => Body.rotate(p, p.spinSpeed)); | |
}); | |
// visible funnel walls | |
const funnelTopY = 40, | |
funnelBotY = 100, | |
funnelHalfW = 50, | |
ft = 10; | |
// left wall | |
World.add( | |
world, | |
Bodies.rectangle( | |
WIDTH / 2 - funnelHalfW - ft / 2, | |
(funnelTopY + funnelBotY) / 2, | |
ft, | |
funnelBotY - funnelTopY, | |
{ isStatic: true, render: { fillStyle: wallColor } }, | |
), | |
); | |
// right wall | |
World.add( | |
world, | |
Bodies.rectangle( | |
WIDTH / 2 + funnelHalfW + ft / 2, | |
(funnelTopY + funnelBotY) / 2, | |
ft, | |
funnelBotY - funnelTopY, | |
{ isStatic: true, render: { fillStyle: wallColor } }, | |
), | |
); | |
// spawn balls in wide tube | |
function spawnBall() { | |
const tubeW = funnelHalfW * 2 - 10; | |
const x0 = WIDTH / 2 + (Math.random() * tubeW - tubeW / 2); | |
const y0 = funnelTopY + 5; | |
const radius = 8 + Math.random() * 4; | |
const ball = Bodies.circle(x0, y0, radius, { | |
restitution: 0.3 + Math.random() * 0.7, | |
friction: 0.001 + Math.random() * 0.1, | |
density: 0.0005 + Math.random() * 0.002, | |
render: { fillStyle: ballColor }, | |
}); | |
World.add(world, ball); | |
} | |
setInterval(spawnBall, 300); | |
// gravity slider | |
document | |
.getElementById("gravitySlider") | |
.addEventListener("input", (e) => { | |
engine.gravity.y = parseFloat(e.target.value); | |
}); | |
})(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment