solution_348.py

#!/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()