Ball physics simulation
This is a simple physics simulation of a number of balls that are attracted to the mouse, and experience gravity.
It was made using an WIP branch from lukewilliamboswell/roc-ray which is a graphics platform for the roc programming language.
I wanted to find the simplest thing I could make to test the platform end to end, and also a new package lukewilliamboswell/roc-pga2d which I put together to learn more about Geometric Algebra and to help with developing the roc-ray platform and more interesting examples.
(Note: this uses the mouse... sorry mobile users I hadn't thought about you guys when making this 😅)
app [Model, init!, render!] { rr: platform "../platform/main.roc", rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.3.0/hPlOciYUhWMU7BefqNzL89g84-30fTE6l2_6Y3cxIcE.tar.br", pga: "https://github.com/lukewilliamboswell/roc-pga2d/releases/download/0.3.0/pdeyRVVsip_FFlHVK_ybcSzKxZLspU_KyMBicijEL-c.tar.br", } import rr.RocRay import rr.Draw import rr.Keys import rand.Random import pga.PGA2D Ball : { position : PGA2D.Multivector, velocity: PGA2D.Multivector, mass: F32, color: RocRay.Color, } # CONSTANTS numberOfBalls = 10 minBallMass = 1 maxBallMass = 20 minSpringK = 0.001 # Too small: barely any force maxSpringK = 1.0 # Too large: unstable/explosive behavior minDamping = 0.0 # No damping (perpetual motion) maxDamping = 1.0 # Full damping (immediate stop) Model : { balls : List Ball, springK: F32, damping: F32, gravity: F32 } init! : {} => Result Model [] init! = \{} -> RocRay.initWindow! { title: "Spring Physics" } RocRay.setTargetFPS! 60 RocRay.displayFPS! { fps: Visible, pos: { x: 10, y: 10 }} balls = generateBalls Ok { balls, springK: 0.02, damping: 0.9, gravity: 0.5, } render! : Model, RocRay.PlatformState => Result Model [] render! = \model, { mouse, keys } -> Draw.draw! White \{} -> # Display spring stiffness and damping Draw.text! { text: "Stiffnes: $(formatF32 model.springK) - press A/D to change", pos: { x: 10, y: 30 }, size: 20, color: Black, } Draw.text! { text: "Damping: $(formatF32 model.damping) - press W/S to change", pos: { x: 10, y: 50 }, size: 20, color: Black, } Draw.text! { text: "Gravity: $(formatF32 model.gravity) - press UP/DOWN to change", pos: { x: 10, y: 70 }, size: 20, color: Black, } Draw.text! { text: "Press R to reset the balls", pos: { x: 10, y: 90 }, size: 20, color: Black, } forEach! model.balls \ball -> # Get ball position w = if (Num.abs ball.position.e12) < 0.000001 then 0.000001 else ball.position.e12 ballX = ball.position.e20 / w ballY = ball.position.e01 / w # Draw spring to mouse position Draw.line! { start: { x: ballX, y: ballY }, end: mouse.position, color: Gray, } # Draw ball (vary radius based on mass) radius = 1 + (ball.mass - minBallMass) Draw.circle! { center: { x: ballX, y: ballY }, radius, color: ball.color, } {} balls = updateBalls model mouse.position |> \b -> # reset balls if space is pressed if Keys.pressed keys KeyR then generateBalls else b springK = if Keys.pressed keys KeyA then model.springK - 0.01 |> limit { min: minSpringK, max: maxSpringK } else if Keys.pressed keys KeyD then model.springK + 0.01 |> limit { min: minSpringK, max: maxSpringK } else model.springK damping = if Keys.pressed keys KeyW then model.damping + 0.01 |> limit { min: minDamping, max: maxDamping } else if Keys.pressed keys KeyS then model.damping - 0.01 |> limit { min: minDamping, max: maxDamping } else model.damping gravity = if Keys.pressed keys KeyUp then model.gravity + 0.1 |> limit { min: 0, max: 10 } else if Keys.pressed keys KeyDown then model.gravity - 0.1 |> limit { min: 0, max: 10 } else model.gravity Ok {model & balls, springK, damping, gravity } updateBalls : Model, RocRay.Vector2 -> List Ball updateBalls = \model, mousePos -> List.map model.balls \ball -> # Get ball position w = if (Num.abs ball.position.e12) < 0.000001 then 0.000001 else ball.position.e12 ballX = ball.position.e20 / w ballY = ball.position.e01 / w # Calculate spring force direction as an ideal point at infinity springForce = PGA2D.idealPoint { x: (mousePos.x - ballX) * model.springK / ball.mass, y: -1 * (mousePos.y - ballY) * model.springK / ball.mass, } # Calculate gravity force gravityForce = PGA2D.idealPoint { x: 0, y: -model.gravity, } combindedForce = PGA2D.add springForce gravityForce # Update velocity velocity = PGA2D.muls ((PGA2D.add ball.velocity combindedForce)) model.damping # Create translator from velocity translator = PGA2D.translator { dx: -velocity.e01, dy: -velocity.e20, } # Update ball position using translator position = PGA2D.mul translator ball.position { position, velocity, mass: ball.mass, color: ball.color } generateBalls : List Ball generateBalls = List.range { start: At 0, end: Before numberOfBalls } |> List.walk { seed: Random.seed 1234, balls: [] } \state, _ -> gen = Random.step state.seed ballGenerator { seed: gen.state, balls: List.append state.balls gen.value } |> .balls ballGenerator : Random.Generator Ball ballGenerator = { Random.chain <- position: { Random.chain <- x: Random.boundedU32 0 800 |> Random.map Num.toF32, y: Random.boundedU32 0 600 |> Random.map Num.toF32, } |> Random.map PGA2D.point, velocity: { Random.chain <- x: Random.boundedU32 0 10 |> Random.map Num.toF32, y: Random.boundedU32 0 10 |> Random.map Num.toF32, } |> Random.map PGA2D.idealPoint, mass: Random.boundedU32 minBallMass maxBallMass |> Random.map Num.toF32, color: { Random.chain <- r: Random.boundedU8 0 255, g: Random.boundedU8 0 255, b: Random.boundedU8 0 255, } |> Random.map \{ r, g, b } -> RGBA r g b 255, } forEach! : List a, (a => {}) => {} forEach! = \l, f! -> when l is [] -> {} [x, .. as xs] -> f! x forEach! xs f! limit : F32, { min: F32, max: F32} -> F32 limit = \x, { min, max } -> if x < min then min else if x > max then max else x # format a float to a string with 2 decimal places formatF32 : F32 -> Str formatF32 = \f32 -> rounded = Num.round (f32 * 100) whole = rounded // 100 decimal = Num.abs (rounded % 100) decimalStr = if decimal < 10 then "0$(Num.toStr decimal)" else Num.toStr decimal "$(Num.toStr whole).$(decimalStr)"