#!/usr/bin/python3
# ==============================================================
# bouncing ball
# ==============================================================
# from: Coding a Physics Engine from scratch!
# https://www.youtube.com/watch?v=nXrEX6j-Mws&t=122s
# ==============================================================
#
# design nodes:
#
# speed is the number of pixels the ball moves per frame.
# it should not be larger than the ball's radius. it
# should be much smaller to increase the fidelity of the
# animation/simulation.
#
# ==============================================================
from dataclasses import dataclass
import user_interface as ui
import graphics as gr
import numpy
import time
VERBOSE = True
# --------------------------------------------------------------
# ---- class definitions
# --------------------------------------------------------------
# ---- box object ----------------------------------------------
##@dataclass
class Box:
def __init__(self,upper_left_x,upper_left_y,
lower_right_x,lower_right_y):
self.top = upper_left_y
self.right = lower_right_x
self.bottom = lower_right_y
self.left = upper_left_x
# ---- display string
def __str__(self):
return 'Box : ' +\
f'top={self.top}, ' +\
f'right={self.right}, ' +\
f'bottom={self.bottom}, ' +\
f'left={self.left}'
# ---- ball object ---------------------------------------------
class Ball:
def __init__(self,coord_x:float,coord_y:float,
radius:float,speed_x:float,speed_y:float):
self.x = coord_x
self.y = coord_y
self.radius = radius
self.speed_x = speed_x
self.speed_y = speed_y
# ---- display string
def __str__(self):
return 'Ball: ' +\
f'x={self.x}, ' +\
f'y={self.y}, ' +\
f'radius={self.radius}, ' +\
f'speed_x={self.speed_x}, ' +\
f'speed_y={self.speed_y}'
# ---- simulate the ball movement in one frame
def simulate(self, win, box:Box):
'''
simulate the ball movement, animate one frame
(move the ball to a new location)
code designed for graphics window coordinate
'''
# ---- don't go past the bottom of the box
if self.y >= box.bottom - self.radius:
# ---- ball moving towards the bottom?
if self.speed_y > 0:
# ---- reverse direction
self.speed_y = -self.speed_y
# ---- make sure the ball is not outside the box
self.y = box.bottom - self.radius
# ---- don't go past the top of the box
if self.y <= box.top + self.radius:
# ---- ball moving towards the top?
if self.speed_y < 0:
# ---- reverse direction
self.speed_y = -self.speed_y
# ---- make sure the ball is not outside the box
self.y = box.top + self.radius
# ---- don't go past the right of the box
if self.x >= box.right - self.radius:
# ---- ball moving towards the right?
if self.speed_x > 0:
# ---- reverse direction
self.speed_x = -self.speed_x
# ---- make sure the ball is not outside the box
self.x = box.right - self.radius
# ---- don't go past the left of the box
if self.x <= box.left + self.radius:
# ---- ball moving towards the left?
if self.speed_x < 0:
# ---- reverse direction
self.speed_x = -self.speed_x
# ---- make sure the ball is not outside the box
self.x = box.top + self.radius
# ---- move the ball
self.x += self.speed_x
self.y += self.speed_y
return (self.x,self.y,self.speed_x,self.speed_y)
# --------------------------------------------------------------
# ---- create a graphics window
# --------------------------------------------------------------
def create_a_graphics_window(width,height,axes=True,
title:str='graphics Window'):
# ---- create window
win = gr.GraphWin(title,width,height)
win.setBackground("white")
# ---- X,Y center of graphics window
xaxis = round(width/2.0)
yaxis = round(height/2.0)
if VERBOSE:
print()
print(f'center of window: {xaxis},{yaxis}')
if axes:
# ---- draw X axis (line)
xl = gr.Line(gr.Point(0,yaxis),gr.Point(width-1,yaxis))
xl.setWidth(1)
xl.setFill("black")
xl.draw(win)
# ---- draw Y axis (line)
yl = gr.Line(gr.Point(xaxis,0),gr.Point(xaxis,height-1))
yl.setWidth(1)
yl.setFill("black")
yl.draw(win)
return win
# --------------------------------------------------------------
# ---- create/draw a rectangle graphics-object
# ---- note: x0,y0,x1,y1 are graphics window coordinates
# --------------------------------------------------------------
def draw_rectangle(win,x0,y0,x1,y1,fill_color=None,
outline_color='black'):
robj = gr.Rectangle(gr.Point(x0,y0),gr.Point(x1,y1))
if fill_color is not None:
robj.setFill(fill_color)
robj.setOutline(outline_color)
robj.setWidth(2)
robj.draw(win)
return robj
#---------------------------------------------------------------
# ---- create/draw a line graphics-object
# ---- Note: x0,y0,x1,y1 are graphics window coordinates
# --------------------------------------------------------------
def draw_line(win,x0,y0,x1,y1,width=1,color='black'):
lobj = gr.Line(gr.Point(x0,y0),gr.Point(x1,y1))
lobj.setWidth(width)
lobj.setFill(color)
lobj.draw(win)
return lobj
#---------------------------------------------------------------
# ---- create/draw a circle graphics-object
# ---- Note: x,y are graphics window coordinates
# --------------------------------------------------------------
def draw_circle(win,x,y,radius,fill_color='red',outline=True):
cobj = gr.Circle(gr.Point(x,y),radius)
cobj.setFill(fill_color)
if outline:
cobj.setOutline('black')
cobj.setWidth(2)
cobj.draw(win)
return cobj
#---------------------------------------------------------------
# ---- create/draw a "raw" pixel graphics-object
# ---- Note: x,y are "raw" graphics window coordinates
# --------------------------------------------------------------
def draw_raw_pixel(win,x,y,color="red"):
win.plotPixel(x,y,color)
return
# --------------------------------------------------------------
# ---- main
# --------------------------------------------------------------
if __name__ == '__main__':
win = create_a_graphics_window(501,501,axes=False,
title='Physics Window - Window Coordinates')
# ----------------------------------------------------------
# ---- other coordinate systems
# ----------------------------------------------------------
# ---- Caresian coordinates
# ---- box: x = -200 to 200, y = -200 to 200
# ---- origin: 0,0 is center of window
# ---- for example:
# ball = Ball(0.0, 0.0, 30, 50.0, -50.0)
# box = Box(-200.0, 200.0, 200.0, -200.0)
# ----------------------------------------------------------
# ---- lower-left-corner coordinates
# ---- box: x = 100 to 400, y = 100 to 400 (upward)
# ---- origin: 0,0 in lower left corner of window
# ---- for example:
# ball = Ball(250.0, 250.0, 30, 50.0, -50.0)
# box = Box(100.0, 400.0, 400.0, 100.0)
# ----------------------------------------------------------
# ---- coordinate system used by this code
# ----
# ---- graphics window (upper-left-corner) coordinates
# ---- for example:
# ---- box: x = 100 to 400, y = 100 to 400 (downward)
# ---- origin: 0,0 in upper left corner of window
# ---- draw box
x0 = 100.0
y0 = 100.0
x1 = 400.0
y1 = 400.0
box = Box(x0,y0,x1,y1)
robj = draw_rectangle(win,x0,y0,x1,y1)
# ---- draw ball (offset from the center)
ball_x = (x0+x1)/2.0 - 10.0
ball_y = (y0+y1)/2.0 - 30.0
ball_radius = 30
speed_x = 10.0
speed_y = 15.0
ball = Ball(ball_x,ball_y,ball_radius,speed_x,speed_y)
ball_obj = draw_circle(win,ball_x,ball_y,ball_radius)
# ---- display initial state of animation/simulation
if VERBOSE:
print()
print("initial state")
print(ball)
print(box)
print()
# ---- draw inner rectangle (box minus radius)
ir_x0 = x0 + ball_radius
ir_y0 = y0 + ball_radius
ir_x1 = x1 - ball_radius
ir_y1 = y1 - ball_radius
robj = draw_rectangle(win,ir_x0,ir_y0,ir_x1,ir_y1,
outline_color="red")
# ---- animate/simulate (draw each frame)
frame = 1
while(True):
if frame > 100: break # for testing
# ---- slow things down so we can see what is going on
time.sleep(0.1)
# ---- animate/simulate one frame
b = ball.simulate(win,box) # calculate move
ball_obj.move(b[2],b[3]) # move in graphics window
frame += 1
# ---- end program, click in graphics window
click = win.getMouse()
win.close()
print()