diff --git a/examples/pygame-ce/README.md b/examples/pygame-ce/README.md new file mode 100644 index 0000000..1a112cc --- /dev/null +++ b/examples/pygame-ce/README.md @@ -0,0 +1,18 @@ +# pygame-ce Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/pygame-ce/order.json b/examples/pygame-ce/order.json new file mode 100644 index 0000000..70c49c9 --- /dev/null +++ b/examples/pygame-ce/order.json @@ -0,0 +1,4 @@ +[ + "surfaces_and_shapes", + "rects_and_blitting" +] diff --git a/examples/pygame-ce/rects_and_blitting/code.py b/examples/pygame-ce/rects_and_blitting/code.py new file mode 100644 index 0000000..63b9e7c --- /dev/null +++ b/examples/pygame-ce/rects_and_blitting/code.py @@ -0,0 +1,92 @@ +# --------------------------------------------------------------------- +# Rects, sprites, and blitting one Surface onto another. +# --------------------------------------------------------------------- + +import pygame +pygame.init() + + +def show_surface(surface, caption=""): + buffer = io.BytesIO() + pygame.image.save(surface, buffer, "PNG") + encoded = base64.b64encode(buffer.getvalue()).decode("ascii") + label = f"
{caption}
" if caption else "" + display( + HTML( + f'{label}' + ), + append=True, + ) + + +heading("Rects: the workhorse of pygame") +note( + "A pygame.Rect describes a rectangular area: position and size. " + "Almost every game uses Rects for positioning sprites, " + "detecting collisions, and clipping draw operations." +) + +# Build a small "sprite" Surface with per-pixel alpha so its +# transparent areas don't show when we blit it later. +def make_token(color, label): + """Make a 64x64 round token with a letter on it.""" + token = pygame.Surface((64, 64), pygame.SRCALPHA) + pygame.draw.circle(token, color, (32, 32), 30) + pygame.draw.circle(token, "white", (32, 32), 30, width=3) + font = pygame.font.SysFont(None, 40) + text = font.render(label, True, "white") + token.blit(text, text.get_rect(center=(32, 32))) + return token + +red_token = make_token("crimson", "R") +blue_token = make_token("steelblue", "B") + +# A board to blit them onto. +board = pygame.Surface((480, 320)) +board.fill((20, 80, 40)) # felt-table green + +# Draw a grid of guide lines using Rect for the playfield. +play_area = pygame.Rect(20, 20, 440, 280) +pygame.draw.rect(board, (10, 50, 25), play_area) +pygame.draw.rect(board, "white", play_area, width=2) + +# Place the tokens. Surface.get_rect() gives us a Rect we can move +# around with helpers like .center, .move(), and .colliderect(). +red_rect = red_token.get_rect(center=(140, 160)) +blue_rect = blue_token.get_rect(center=(180, 170)) + +# blit() copies one Surface onto another at the rect's top-left. +board.blit(red_token, red_rect) +board.blit(blue_token, blue_rect) + +# Rects can detect overlap, useful for collision checks. +overlapping = red_rect.colliderect(blue_rect) +note(f"Do the two tokens overlap? {overlapping}") + +show_surface(board, caption="Two tokens blitted onto a board") + +heading("Animating a Rect across frames") +note( + "A game loop usually updates positions then redraws. Here we " + "render a few frames of a token sliding across the board and " + "show them as a strip so you can see the motion." +) + +frames = [] +slider = make_token("gold", "G") +slider_rect = slider.get_rect(center=(60, 160)) + +for step in range(5): + frame = board.copy() + # Move the rect 90 pixels to the right each frame. + slider_rect = slider_rect.move(90, 0) + frame.blit(slider, slider_rect) + frames.append(frame) + +# Compose all frames into one tall image. +strip = pygame.Surface((480, 320 * len(frames))) +for index, frame in enumerate(frames): + strip.blit(frame, (0, index * 320)) + +show_surface(strip, caption="Five frames of a sliding token") diff --git a/examples/pygame-ce/rects_and_blitting/config.toml b/examples/pygame-ce/rects_and_blitting/config.toml new file mode 100644 index 0000000..98e0600 --- /dev/null +++ b/examples/pygame-ce/rects_and_blitting/config.toml @@ -0,0 +1 @@ +packages = ["pygame-ce"] diff --git a/examples/pygame-ce/rects_and_blitting/setup.py b/examples/pygame-ce/rects_and_blitting/setup.py new file mode 100644 index 0000000..a0819f3 --- /dev/null +++ b/examples/pygame-ce/rects_and_blitting/setup.py @@ -0,0 +1,22 @@ +"""Setup for the rects-and-blitting example. No IPython shim here.""" +import os +os.environ.setdefault("SDL_VIDEODRIVER", "dummy") + +import io +import base64 +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) diff --git a/examples/pygame-ce/surfaces_and_shapes/code.py b/examples/pygame-ce/surfaces_and_shapes/code.py new file mode 100644 index 0000000..7e5ff48 --- /dev/null +++ b/examples/pygame-ce/surfaces_and_shapes/code.py @@ -0,0 +1,74 @@ +""" +A first look at pygame-ce: drawing on a Surface. + +Pygame is a game library, and at its heart is the Surface, a 2D +pixel canvas you draw onto. In a normal pygame program you would +call pygame.display.set_mode(...) to get a window-backed Surface +and run a game loop. Here we work with offscreen Surfaces directly, +which is the same API minus the window, and show the result inline. + +See https://pyga.me/docs/ for the full reference. +""" +from IPython.core.display import display, HTML + +# pygame-ce needs a dummy video driver when running headless inside a +# web worker. We must set this BEFORE importing pygame. +import os +os.environ.setdefault("SDL_VIDEODRIVER", "dummy") + +import io +import base64 +import pygame + + +def show_surface(surface, caption=""): + """Render a pygame Surface as an inline PNG in the page.""" + # pygame.image.save can write to any file-like object. We grab the + # PNG bytes, base64-encode them, and embed them in an tag. + buffer = io.BytesIO() + pygame.image.save(surface, buffer, "PNG") + encoded = base64.b64encode(buffer.getvalue()).decode("ascii") + label = f"
{caption}
" if caption else "" + display( + HTML( + f'{label}' + ), + append=True, + ) + + +# pygame must be initialized before most subsystems will work. +pygame.init() + +heading("Drawing shapes on a Surface") +note( + "We create a 480x320 Surface, fill the background, and draw some " + "shapes with pygame.draw. Colors can be given as RGB tuples or " + "named CSS-style strings." +) + +canvas = pygame.Surface((480, 320)) +canvas.fill((30, 30, 60)) # deep navy background + +# A row of filled circles, like balloons rising. +for index, color in enumerate(["tomato", "gold", "mediumseagreen", "skyblue"]): + center = (80 + index * 110, 200) + pygame.draw.circle(canvas, color, center, 36) + pygame.draw.circle(canvas, "white", center, 36, width=3) + +# A polygon (a simple house outline). +house = [(360, 260), (360, 180), (410, 140), (460, 180), (460, 260)] +pygame.draw.polygon(canvas, "khaki", house) +pygame.draw.polygon(canvas, "saddlebrown", house, width=4) + +# Anti-aliased line across the sky. +pygame.draw.aaline(canvas, "white", (10, 40), (470, 80)) + +show_surface(canvas, caption="Hand-drawn scene on a 480x320 Surface") + +note( + "The Surface is just an in-memory image. The same drawing calls " + "would render to the screen Surface returned by " + "pygame.display.set_mode in a regular game." +) diff --git a/examples/pygame-ce/surfaces_and_shapes/config.toml b/examples/pygame-ce/surfaces_and_shapes/config.toml new file mode 100644 index 0000000..98e0600 --- /dev/null +++ b/examples/pygame-ce/surfaces_and_shapes/config.toml @@ -0,0 +1 @@ +packages = ["pygame-ce"] diff --git a/examples/pygame-ce/surfaces_and_shapes/setup.py b/examples/pygame-ce/surfaces_and_shapes/setup.py new file mode 100644 index 0000000..bb018e1 --- /dev/null +++ b/examples/pygame-ce/surfaces_and_shapes/setup.py @@ -0,0 +1,42 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + """Wrap pyscript.display so output lands in the example target.""" + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.version_info = (9, 0, 2, '') +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True)