diff --git a/examples/bouncy-bubbles-p5js/bouncy_bubbles.py b/examples/bouncy-bubbles-p5js/bouncy_bubbles.py new file mode 100644 index 0000000..58bb296 --- /dev/null +++ b/examples/bouncy-bubbles-p5js/bouncy_bubbles.py @@ -0,0 +1,96 @@ +# Bouncy Bubbles +# based on code from Keith Peters. Multiple-object collision. +# Adapted from https://p5js.org/examples/motion-bouncy-bubbles.html +# Port to pyscript by Ben Alkov, 2022-07 + +# We could also use p5.js' math functions, but might as well use Python's +import math + +# Not strictly necessary, but seeing naked e.g. `document`, `window`, etc. really bothers me +import js + +from pyodide import create_proxy + +NUM_BALLS = 13 +SPRING = 0.05 +GRAVITY = 0.03 +FRICTION = -0.9 +BALLS = [] +HEIGHT = 400 +WIDTH = 720 + +# Convenience +p5js = js.window + + +class Ball(): + def __init__(self, x, y, dia): + self.x = x + self.y = y + self.diameter = dia + self.vx = 0 + self.vy = 0 + + def collide(self): + for other_ball in BALLS: + dx = other_ball.x - self.x + dy = other_ball.y - self.y + distance = math.sqrt(dx * dx + dy * dy) + min_dist = other_ball.diameter / 2 + self.diameter / 2 + if (distance < min_dist): + angle = math.atan2(dy, dx) + targetX = self.x + math.cos(angle) * min_dist + targetY = self.y + math.sin(angle) * min_dist + ax = (targetX - other_ball.x) * SPRING + ay = (targetY - other_ball.y) * SPRING + self.vx -= ax + self.vy -= ay + other_ball.vx += ax + other_ball.vy += ay + + def move(self): + self.vy += GRAVITY + self.x += self.vx + self.y += self.vy + if self.x + self.diameter / 2 > WIDTH: + self.x = WIDTH - self.diameter / 2 + self.vx *= FRICTION + elif self.x - self.diameter / 2 < 0: + self.x = self.diameter / 2 + self.vx *= FRICTION + + if self.y + self.diameter / 2 > HEIGHT: + self.y = HEIGHT - self.diameter / 2 + self.vy *= FRICTION + elif (self.y - self.diameter / 2 < 0): + self.y = self.diameter / 2 + self.vy *= FRICTION + + def display(self): + p5js.ellipse(self.x, self.y, self.diameter, self.diameter) + + +# These functions are named per convention: p5.js doesn't know anything about them + +def setup(): + global BALLS + + p5js.createCanvas(WIDTH, HEIGHT) + BALLS = [Ball(p5js.random(WIDTH), p5js.random(HEIGHT), p5js.random(30, 70)) + for _ in range(NUM_BALLS)] + p5js.noStroke() + p5js.fill(255, 204) + p5js.background(0) + + +def draw(*args): + p5js.background(0) + for ball in BALLS: + ball.collide() + ball.move() + ball.display() + p5js.requestAnimationFrame(create_proxy(draw)) + + +setup() +js.window.requestAnimationFrame(create_proxy(draw)) diff --git a/examples/bouncy-bubbles-p5js/index.html b/examples/bouncy-bubbles-p5js/index.html new file mode 100644 index 0000000..3879c9b --- /dev/null +++ b/examples/bouncy-bubbles-p5js/index.html @@ -0,0 +1,24 @@ + + + + + + + Bouncy Bubbles + + + + - autoclose_loader: true + + + + + + + + diff --git a/tests/test_examples.py b/tests/test_examples.py index df8ba50..94e4179 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -33,7 +33,8 @@ ] EXAMPLES = [ - "hello_world" + "hello_world", + "bouncy-bubbles-p5js", ] TEST_PARAMS = { @@ -42,6 +43,11 @@ "pattern": "\\d+:\\d+:\\d+", "title": "PyScript Hello World", }, + "bouncy-bubbles-p5js": { + "file": "bouncy-bubbles-p5js/index.html", + "pattern": "false", + "title": "Bouncy Bubbles", + }, } @@ -87,16 +93,17 @@ def test_examples(example, http_server): # DOM from within the timing loop for a string that is not present in the # initial markup but should appear by way of rendering - re_sub_content = re.compile(TEST_PARAMS[example]["pattern"]) - py_rendered = False # Flag to be set to True when condition met + if TEST_PARAMS[example]["pattern"] != "false": + re_sub_content = re.compile(TEST_PARAMS[example]["pattern"]) + py_rendered = False # Flag to be set to True when condition met - for _ in range(TEST_ITERATIONS): - time.sleep(TEST_TIME_INCREMENT) - content = page.inner_html("*") - if re_sub_content.search(content): - py_rendered = True - break + for _ in range(TEST_ITERATIONS): + time.sleep(TEST_TIME_INCREMENT) + content = page.inner_html("*") + if re_sub_content.search(content): + py_rendered = True + break - assert py_rendered # nosec + assert py_rendered # nosec browser.close()