Created
January 22, 2025 21:23
-
-
Save sampottinger/9813a8604fa20a2f764c41d989ac599c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Simulation of bouncing balls with user interaction. | |
Simulation of bouncing balls with user interaction as an example for | |
interactivedatascience.courses as part of Tutorial 3. | |
License: BSD-3-Clause | |
Author: A Samuel Pottinger | |
""" | |
import sketchingpy | |
import time | |
WIDTH = 500 # pixels | |
HEIGHT = 400 # pixels | |
class Ball: | |
"""A ball which bounces against the mouse and sketch edges.""" | |
def __init__(self, position_x, position_y, velocity_x, velocity_y): | |
"""Create a new ball. | |
Args: | |
position_x: The starting x coordinate of the ball in pixels. | |
position_y: The starting y coordinate of the ball in pixels. | |
velocity_x: The horizontal component of the ball's velocity in | |
pixels per second. | |
velocity_y: The vertical component of the ball's velocity in | |
pixels per second. | |
""" | |
self._position_x = position_x | |
self._position_y = position_y | |
self._velocity_x = velocity_x | |
self._velocity_y = velocity_y | |
def update(self, duration, mouse_x, mouse_y): | |
"""Update the position and velocity of the ball. | |
Args: | |
duration: The time in seconds ellapsed since update was last called. | |
mouse_x: The horizontal coordinate of the mouse pointer in pixels. | |
mouse_y: The vertical coordinate of the mouse pointer in pixels. | |
""" | |
self._position_x = self._position_x + self._velocity_x * duration | |
self._position_y = self._position_y + self._velocity_y * duration | |
if self._position_x > WIDTH: | |
self._position_x = WIDTH | |
self._reverse_x() | |
elif self._position_x < 0: | |
self._position_x = 0 | |
self._reverse_x() | |
if self._position_y > HEIGHT: | |
self._position_y = HEIGHT | |
self._reverse_y() | |
elif self._position_y < 0: | |
self._position_y = 0 | |
self._reverse_y() | |
if self._get_is_near_mouse(mouse_x, mouse_y): | |
x_collision = self._get_hit_mouse_in_direction( | |
self._position_x, | |
mouse_x, | |
self._velocity_x | |
) | |
y_collision = self._get_hit_mouse_in_direction( | |
self._position_y, | |
mouse_y, | |
self._velocity_y | |
) | |
if x_collision: | |
self._reverse_x() | |
elif y_collision: | |
self._reverse_y() | |
def get_x(self): | |
"""Get the current horizontal position of this ball. | |
Returns: | |
The current x position of this ball in pixels. | |
""" | |
return self._position_x | |
def get_y(self): | |
"""Get the current vertical position of this ball. | |
Returns: | |
The current y position of this ball in pixels. | |
""" | |
return self._position_y | |
def _reverse_x(self): | |
"""Switch the horizontal component of the ball's velocity.""" | |
self._velocity_x = self._velocity_x * -1 | |
def _reverse_y(self): | |
"""Switch the vertical component of the ball's velocity.""" | |
self._velocity_y = self._velocity_y * -1 | |
def _get_is_near_mouse(self, mouse_x, mouse_y): | |
"""Determine if the ball has collieded with the mouse. | |
Args: | |
mouse_x: The horizontal coordinate of the mouse pointer in pixels. | |
mouse_y: The vertical coordinate of the mouse pointer in pixels. | |
Returns: | |
True if the mouse has collided with this ball and false otherwise. | |
""" | |
x_near = abs(mouse_x - self._position_x) < 10 | |
y_near = abs(mouse_y - self._position_y) < 10 | |
return x_near and y_near | |
def _get_hit_mouse_in_direction(self, coordinate, mouse, velocity): | |
"""Determine if the ball was hit along a specific axis. | |
Args: | |
coordinate: The position of the ball in this axis in pixels. | |
mouse: The coordinate of the mouse poiner in this axes in pixels. | |
velocity: The velocity of the ball in this axis in pixels / second. | |
Returns: | |
True if the ball was hit in this direction and false otherwise. | |
""" | |
if mouse > coordinate and velocity > 0: | |
distance = mouse - coordinate | |
return distance < 10 | |
elif mouse < coordinate and velocity < 0: | |
distance = mouse - coordinate | |
return distance > -10 | |
else: | |
return False | |
class Simulation: | |
"""Simulation made up of multiple bouncing balls.""" | |
def __init__(self): | |
"""Create a new simulation with 3 balls.""" | |
self._balls = [ | |
Ball(WIDTH / 2, HEIGHT / 2, -10, -10), | |
Ball(WIDTH / 2, HEIGHT / 2, -10, 10), | |
Ball(WIDTH / 2, HEIGHT / 2, 10, 0) | |
] | |
self._last_time = time.time() | |
def update(self, mouse_x, mouse_y): | |
"""Update all balls within the simluation. | |
Args: | |
mouse_x: The horizontal coordinate of the mouse pointer in pixels. | |
mouse_y: The vertical coordinate of the mouse pointer in pixels. | |
""" | |
new_time = time.time() | |
duration = new_time - self._last_time | |
self._last_time = new_time | |
for ball in self._balls: | |
ball.update(duration, mouse_x, mouse_y) | |
def get_balls(self): | |
"""Get all balls active in the simulation. | |
Returns: | |
List of balls. | |
""" | |
return self._balls | |
sketch = sketchingpy.Sketch2D(WIDTH, HEIGHT) | |
simulation = Simulation() | |
def update_and_draw_balls(self): | |
"""The on_step callback for the sketch. | |
The on_step callback for the sketch which updates and then draws each ball. | |
""" | |
mouse = sketch.get_mouse() | |
mouse_x = mouse.get_pointer_x() | |
mouse_y = mouse.get_pointer_y() | |
simulation.update(mouse_x, mouse_y) | |
for ball in simulation.get_balls(): | |
sketch.draw_ellipse(ball.get_x(), ball.get_y(), 2, 2) | |
sketch.on_step(update_and_draw_balls) | |
sketch.show() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment