<jupyter_start><jupyter_text>Bouncing balls!> Figuring out collision for circles and bounce off.- toc: true - badges: true- comments: true- categories: [physic]  Quick introduction Recently I have decided to write a python mini physic/game/graphic engine, but I was struggling with implementing geometry concepts (analytical geometry) to the code, and so I decided to write a bunch of small projects on a similar theme, where this one was one of them.Once I finally grasped the translation between geometry concept to the code, and I was able to finish other projects without any problems.<jupyter_code>#hide
import matplotlib.pyplot as plt<jupyter_output><empty_output><jupyter_text>Theory of collision When creating a circle we need only two parameters: coordinations (x, y) and radius. This is also enough to determine collision and later even response (including the vector of velocity).<jupyter_code>#hide
p1, p2 = (-0.19, -0.1), (0.13, 0.16)
#hide-input
plt.scatter(*p1, s=20000, facecolors='none', edgecolors='blue')
plt.scatter(*p2, s=20000, facecolors='none', edgecolors='orange')

plt.xlim(-0.5 , 0.5) 
plt.ylim(-0.5 , 0.5) 
plt.show()<jupyter_output><empty_output><jupyter_text>We have two circles that are colliding, but to find out if this case is true, we can use the Pythagorean theory.<jupyter_code>#hide-input
# circle
plt.scatter(*p1, s=20000, facecolors='none', edgecolors='blue')
plt.scatter(*p2, s=20000, facecolors='none', edgecolors='orange')
# line
plt.plot([p1[0], p2[0]], [p1[1], p2[1]], "purple")
plt.plot([p1[0], p2[0]], [p1[1], p1[1]], "teal")
plt.plot([p2[0], p2[0]], [p1[1], p2[1]], "maroon")
# points
plt.plot(*p1, "r", marker="o")
plt.plot(*p2, "r", marker="o")


plt.xlim(-0.5 , 0.5) 
plt.ylim(-0.5 , 0.5)
plt.legend(["hypotenuse", "Adjacent", "Opposite"])
plt.show()<jupyter_output><empty_output><jupyter_text>By squaring 'adjacent' and 'opposite' and adding them together, we get a squared 'hypotenuse'. $$\text{hypotenuse}^2 = \text{adjacent}^2 + \text{opposite}^2$$$$\text{c}^2 = \text{a}^2 + \text{b}^2$$$$\text{c} = \sqrt{\text{a}^2 + \text{b}^2}$$ It's quite easy to get 'adjacent' and 'opposite' because we operate on a plane with a `Cartesian coordinate system`, we take coordinations of both circles and deduct them from each other. $$\text{dx} = \text{Circle2.x} - \text{Circle1.x}$$$$\text{dy} = \text{Circle2.y} - \text{Circle1.y}$$ Then we can calculate the 'hypotenuse'. $$\text{distance} = \sqrt{\text{dx}^2 + \text{dy}^2}$$$$\text{OR}$$$$\text{distance} = \text{math.hypot(dx, dy)}$$ With this, we get a one-dimensional distance between those two circles. We just need to sum the radius of the first and the second circle. If the hypotenuse is smaller than the sum of their radiuses that it's a collision. This was easy stuff, but more tricky is to figure out how they would bounce from each other.  Response We have to assume that both circles have some velocity (vector with x and y value that each loop move circle). Theoretically, two circles collide on an infinitely small point. The collision would be exactly in the middle of 'hypotenuse', which would be the distance between those two circles. We then flip 'hypotenuse' about 180 degrees (this would be arctangent). $$\text{tangent} = \text{math.atan2(dy, dx)}$$ > Note: Tangent is a straight line that shares only one point on some circle<jupyter_code>#hide-input
# circle
p1, p2 = (-0.19, -0.15), (0.21, 0.05)
plt.scatter(*p1, s=20000, facecolors='none', edgecolors='blue')
plt.scatter(*p2, s=20000, facecolors='none', edgecolors='orange')
# line

plt.plot([-0.1, 0.1], [0.4, -0.4], "purple")
plt.plot([p1[0], p2[0]], [p1[1], p2[1]], "maroon")
# points
plt.plot(*p1, "r", marker="o")
plt.plot(*p2, "r", marker="o")


plt.xlim(-0.5 , 0.5) 
plt.ylim(-0.5 , 0.5)
plt.show()<jupyter_output><empty_output><jupyter_text>This tangent will now act as sort of a wall. From this wall, we will be able to determine the direction of the bounce.When you throw a small ball on the floor it will bounce back, but its velocity would mirror the other side of an imagined wall that is rectangular to the floor and the ball would fly further away from you with quadratic jumps. Simple physic. We'll do same thing here (even with gravity in code).Right now, If we run the code, then the circles will stick to each other. This is because computers can't compute infinitely small point, and so we will approximate the point of collision, and also we need to write the change of velocity for bouncing from each other. First, we need to get the right angle. $$\text{angle} = \text{1/2} * \text{math.pi} + \text{tangent}$$ Pi is 180 degree, so `tangent + 90 degree` will give us the angle we want and then we use the function of sinus and cosines to 'mirror' velocity to simulate bouncing from each other. $$\text{Circle1.velocity.x  -=  math.sin(angle)}$$$$\text{Circle1.velocity.y  +=  math.cos(angle)}$$$$\text{Circle2.velocity.x  +=  math.sin(angle)}$$$$\text{Circle2.velocity.y  -=  math.cos(angle)}$$ In our case, we also don't deal with weight, elasticity, friction and so on..., so we can skip it.That's it. If you're not familiar with sines and cosines, then you can check the source [here](https://www.khanacademy.org/math/trigonometry), but let's say you have a circle with position (0, 0) and radius 1. You want figure out 60 degree angle.<jupyter_code>#hide-input
# circle & plot
fig, ax = plt.subplots()
ax.set_xlim((-1.6, 1.6))
ax.set_ylim((-1.1, 1.1))

# lines
plt.plot([0, 0.5], [0, 0.8660], color="purple")
plt.plot([0, 0.5], [0.8660, 0.8660], color="maroon", linestyle=':')
plt.plot([0.5, 0.5], [0, 0.8660], color="teal", linestyle=':')
plt.plot([0, 0], [-1, 1], color="black")
plt.plot([-1, 1], [0, 0], color="black")
c1 = plt.Circle((0, 0), 1, color="navy", fill=False)

ax.add_patch(c1)

plt.legend(["hypotenuse of 60°", "sine", "cosine"])

plt.show()<jupyter_output><empty_output><jupyter_text>Here, `sin(60 degree) return 0.8660` (on y-axis) and `cos(60 degree) return 0.5` (on x-axis). If 'hypotenuse' would be a velocity, then we have its coordination and we can use a desired angle to rotate it.  Code Code is design for python 3, but there shouldn't be any troubles following in different language.  GUI For simplicity, we will go with pygame as GUI. First, we import libraries:<jupyter_code>#hide-output
import pygame
from sys import exit<jupyter_output><empty_output><jupyter_text>First, we need to define the size of the window and the frame rate per seconds of that window.<jupyter_code>#hide-output
# pygame & simulation limitation settings
FPS = 60
SIZE = 700
WSIZE = (SIZE, SIZE)<jupyter_output><empty_output><jupyter_text>We then initiate pygame.<jupyter_code>#hide-output
# init pygame
pygame.init()
pygame.display.set_caption("Bouncing balls") # set a name
screen = pygame.display.set_mode(WSIZE) # create window
clock = pygame.time.Clock() # create in-game time (for FPS)<jupyter_output><empty_output><jupyter_text>Now we can create the main loop, where the whole program will compute and display results. With the pygame, we also need to create an event (this is for exiting window or pressing keys).<jupyter_code>#hide-output
while 1:
    clock.tick(FPS) # setting in-game time

    # pygame event (closing window)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            exit()
    #... (physic)<jupyter_output><empty_output><jupyter_text>The only thing we need to write is small iterations, where we will draw all circles and update the display (also what you draw in pygame stays on display, so we need to redraw it to the background colour).<jupyter_code>#hide-output
    #... (physic)
    screen.fill((0, 0, 0)) # RGB color of BLACK
    """
    for b in balls:
        pygame.draw.circle(screen, b.color, b.pos, b.radius) # choose window to draw on; color of circle; position of circle; radius of circle
    """
    pygame.display.flip() # update display<jupyter_output><empty_output><jupyter_text>The last thing to do is to create circles so we could draw them.  PHYSIC Before the main loop, we need to set up some variables for the determining style of simulation. Here we want circles of different sizes but with the same weight and with gravity to play its role.<jupyter_code>#hide-output
GRAVITY = 0.05
generate_circles = 40
MAX_RADIUS = 40<jupyter_output><empty_output><jupyter_text>We also import basic libraries:<jupyter_code>#hide-output
from random import randint, uniform
import math<jupyter_output><empty_output><jupyter_text>We generate a list of Circles (as a class), where later in the main loop we will update its positions.<jupyter_code>#hide-output
# generate circles
balls = [Circle() for _ in range(generate_circles)]<jupyter_output><empty_output><jupyter_text>Before we can utilize it, we need to write that class for `Circle()`. We also use `randint` and `uniform` function for generating random parameters. We use it to generate radius, position (as a tuple), velocity (also x, y), colour (Red, Green, Blue).<jupyter_code>#hide-output
class Circle:
    def __init__(self):
        self.radius = randint(10, MAX_RADIUS)
        self.pos = (randint(MAX_RADIUS, SIZE-MAX_RADIUS), randint(MAX_RADIUS, SIZE-MAX_RADIUS)) # SIZE is max size of window (GUI)
        self.vel = (uniform(0, 0.1), uniform(0, 0.1))
        self.color = (randint(32, 255), randint(32, 255), randint(32, 255)) # RGB<jupyter_output><empty_output><jupyter_text>Now it would be helpful to create two functions for updating position and velocity:<jupyter_code>#hide-output
    def update_vel(self, vector):
        x,y = self.vel
        self.vel = (x+vector[0], y+vector[1])

    def update_pos(self):
        px, py = self.pos
        x, y = self.vel[0]+px, self.vel[1]+py
        self.pos = (x, y)<jupyter_output><empty_output><jupyter_text>And now we can construct the main function for collision and bounce:<jupyter_code>#hide-output
    def collision(self, other):
        dx = other.pos[0] - self.pos[0]
        dy = other.pos[1] - self.pos[1]
        distance = math.hypot(dx, dy) # --> math.sqrt(dx**2 + dy**2)

        if distance < self.radius+other.radius:
            #...<jupyter_output><empty_output><jupyter_text>We now know if a collision occurs; if yes then we need to change velocity (and update position so they would go further from each other):<jupyter_code>#hide-output
            # get angle for bouncing
            tangent = math.atan2(dy, dx)
            angle = (0.5 * math.pi + tangent)
            
            # get velocity of both circles
            self_x, self_y = self.vel
            other_x, other_y = other.vel

            # move vectors based on new angle (for velocity)
            self_x -= math.sin(angle)
            self_y += math.cos(angle)
            other_x += math.sin(angle)
            other_y -= math.cos(angle)
            
            # update
            self.vel = (self_x, self_y)
            self.update_pos()
            other.vel = (other_x, other_y)
            other.update_pos()<jupyter_output><empty_output><jupyter_text>The class is constructed. The only missing piece is creating logic for updating simulation in the main loop: When checking for a collision we want to check the selected `Circle()` with every `Circle()` (except itself):<jupyter_code>#hide-output
while 1:
    #... (GUI)
    
    # update circles (physic)
    for b1 in balls:
        for b2 in balls:
            if b1 == b2: continue # skip it self
            # bounce out of collision
            b1.collision(b2)<jupyter_output><empty_output><jupyter_text>After checking it with every class we also apply gravity for that selected `Circle()` (which is vector going down (technically up, because of GUI)).<jupyter_code>#hide-output
        # move down by gravity
        b1.update_vel((0, GRAVITY))

        # update pos
        b1.update_pos()<jupyter_output><empty_output><jupyter_text>A small detail is to restrict it to the window so it wouldn't fly off and update position.<jupyter_code>#hide-output
        # pos
        x, y = b1.pos
    
        # bounce of walls
        vx, vy = b1.vel
        if x+b1.radius > SIZE and vx != abs(vx) * -1: vx *= -1
        elif x-b1.radius < 0  and vx == abs(vx) * -1: vx *= -1
        if y+b1.radius > SIZE and vy != abs(vy) * -1: vy *= -1
        elif y-b1.radius < 0  and vy == abs(vy) * -1: vy *= -1
        b1.vel = (vx, vy)

        # update pos
        b1.update_pos()
    #... (GUI)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.126043673731535, 'fraction_numerical': 0.025048169556840076, 'mean_word_length': 3.8889324960753533, 'pattern_counts': {'":': 0, '<': 84, '<?xml version=': 0, '>': 86, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '20', 'dir': 'jupyter-structured-clean-dedup', 'id': '516919', 'license': 'Apache-2.0', 'n_tokens_mistral': 4332, 'n_tokens_neox': 4071, 'n_words': 1434, 'path': '_notebooks/2021-06-18-Bouncing-balls!.ipynb', 'repo_name': 'ludius0/my-blog', 'size': '12456'}