Skip to content

Instantly share code, notes, and snippets.

@sampottinger
Created January 22, 2025 21:23
Show Gist options
  • Save sampottinger/9813a8604fa20a2f764c41d989ac599c to your computer and use it in GitHub Desktop.
Save sampottinger/9813a8604fa20a2f764c41d989ac599c to your computer and use it in GitHub Desktop.
"""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