packages feed

brillo-examples-2.0.0: picture/Styrene/Collide.hs

-- | Physics for bead bouncing.
module Collide where

import Actor (Actor (..))
import Brillo.Data.Point (Point)
import Brillo.Data.Point.Arithmetic qualified as Pt
import Brillo.Data.Vector (
  angleVV,
  detV,
  dotV,
  mulSV,
  normalizeV,
  rotateV,
 )
import Brillo.Geometry.Line (closestPointOnLine)


-- Config -----------------------------------------------------------------------------------------
-- How bouncy the beads are
--      at 0.2 and they look like melting plastic.
--      at 0.8 and they look like bouncy rubber balls.
--      at > 1 and they gain energy with each bounce and escape the box.
--
beadBeadLoss :: Float
beadBeadLoss = 0.95
beadWallLoss :: Float
beadWallLoss = 0.8


-- | Move a bead which is in contact with a wall.
collideBeadWall ::
  -- | the bead
  Actor ->
  -- | the wall that bead is in contact with
  Actor ->
  -- | the new bead
  Actor
collideBeadWall
  bead@(Bead _ix _ _radius pBead _vIn)
  (Wall _ pWall1 pWall2) =
    let
      -- Take the collision point as being the point on the wall which is
      -- closest to the bead's center.
      pCollision = closestPointOnLine pWall1 pWall2 pBead
    in
      -- then do a static, non energy transfering collision.
      collideBeadPointStatic
        bead
        pCollision
        beadWallLoss
collideBeadWall _ _ = error "collideBeadWall: not a bead and a wall"


-- | Move two beads which have bounced into each other.
collideBeadBeadElastic ::
  Actor ->
  Actor ->
  (Actor, Actor)
collideBeadBeadElastic
  (Bead ix1 mode1 r1 p1 v1)
  (Bead ix2 mode2 r2 p2 v2) =
    let mass1 = 1
        mass2 = 1

        -- the axis of collision (towards p2)
        vCollision@(cX, cY) = normalizeV (p2 Pt.- p1)
        vCollisionR = (cY, -cX)

        -- the velocity component of each bead along the axis of collision
        s1 = dotV v1 vCollision
        s2 = dotV v2 vCollision

        -- work out new velocities along the collision
        s1' = (s1 * (mass1 - mass2) + 2 * mass2 * s2) / (mass1 + mass2)
        s2' = (s2 * (mass2 - mass1) + 2 * mass1 * s1) / (mass1 + mass2)

        -- the velocity components at right angles to the collision
        --      there is no friction in the collision so these don't change
        k1 = dotV v1 vCollisionR
        k2 = dotV v2 vCollisionR

        -- new bead velocities
        v1' = mulSV s1' vCollision Pt.+ mulSV k1 vCollisionR
        v2' = mulSV s2' vCollision Pt.+ mulSV k2 vCollisionR

        v1_slow = mulSV beadBeadLoss v1'
        v2_slow = mulSV beadBeadLoss v2'

        -- work out the point of collision
        u1 = r1 / (r1 + r2)

        pCollision =
          p1 Pt.+ mulSV u1 (p2 Pt.- p1)

        -- place the beads just next to each other so they are no longer overlapping.
        p1' = pCollision Pt.- (r1 + 0.001) `mulSV` vCollision
        p2' = pCollision Pt.+ (r2 + 0.001) `mulSV` vCollision

        bead1' = Bead ix1 mode1 r1 p1' v1_slow
        bead2' = Bead ix2 mode2 r2 p2' v2_slow
    in  (bead1', bead2')
collideBeadBeadElastic _ _ = error "collideBeadBeadElastic: not two beads"


collideBeadBeadStatic ::
  Actor ->
  Actor ->
  Actor
collideBeadBeadStatic
  bead1@(Bead _ix1 _ radius1 pBead1 _)
  (Bead _ix2 _ radius2 pBead2 _) =
    let
      -- Take the collision point as being between the center's of the two beads.
      -- For beads which have the same radius the collision point is half way between
      -- their centers and u == 0.5
      u = radius1 / (radius1 + radius2)
      pCollision = pBead1 Pt.+ mulSV u (pBead2 Pt.- pBead1)

      bead1' =
        collideBeadPointStatic
          bead1
          pCollision
          beadBeadLoss
    in
      bead1'
collideBeadBeadStatic _ _ = error "collideBeadBeadStatic: not two beads"


-- | Move a bead which has collided with something.
collideBeadPointStatic ::
  -- | the bead which collided with something
  Actor ->
  -- | the point of collision (should be near the bead's surface)
  Point ->
  -- | velocity scaling factor (how much to slow the bead down after the collision)
  Float ->
  Actor
collideBeadPointStatic
  (Bead ix mode radius pBead vIn)
  pCollision
  velLoss =
    let
      -- take a normal vector from the wall to the bead.
      --      this vector is at a right angle to the wall.
      vNormal = normalizeV (pBead Pt.- pCollision)

      -- the bead at pBead is overlapping with what it collided with, but we don't want that.
      --      place the bead so it's surface is just next to the point of collision.
      pBead_new = pCollision Pt.+ (radius + 0.01) `mulSV` vNormal

      -- work out the angle of incidence for the bounce.
      --      this is the angle between the surface normal and
      --      the direction of travel for the bead.
      aInc = angleVV vNormal (Pt.negate vIn)

      -- aInc2 is the angle between the wall /surface/ and
      --      the direction of travel.
      aInc2 = (pi / 2) - aInc

      -- take the determinant between the surface normal and the direction of travel.
      --      This will tell us what direction the bead hit the wall.
      --      The diagram shows the sign of the determinant for the four possiblities.
      --
      --           \ +ve                                -ve /
      --            \                                      /
      --             \/                                  \/
      --   pWall1 ---------- pWall2           pWall1 ---------- pWall2
      --             /\                                  /\
      --            /                                      \
      --           / -ve                                +ve \
      --
      determinant = detV vIn vNormal

      -- Use the determinant to rotate the bead's velocity vector for the bounce.
      vOut
        | determinant > 0 = rotateV (2 * aInc2) vIn
        | otherwise = rotateV (negate (2 * aInc2)) vIn

      -- Slow down the bead when it hits the wall
      vSlow = velLoss `mulSV` vOut

      bead1_new = Bead ix mode radius pBead_new vSlow
    in
      bead1_new
collideBeadPointStatic _ _ _ =
  error "collideBeadPointStatic: not a bead and a point"