diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..7d4d139 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,23 @@ +# Examples + +Small programs that show pygraphics drawing on a display. Each one says in +its docstring what it needs and where it has been run. Run them from the +repository's root, so the ones that load a picture or a font find it in +[assets/](assets/). + +| Example | What it shows | +|---|---| +| [graphics_simpletest.py](graphics_simpletest.py) | Shapes and text drawn straight on the display | +| [framebuf_simpletest.py](framebuf_simpletest.py) | The same shapes drawn into a FrameBuffer, then copied on | +| [pygraphics_landscape.py](pygraphics_landscape.py) | A landscape drawn in bands, scaled to any display | +| [rotations.py](rotations.py) | The display turned through its four rotations | +| [font_list.py](font_list.py) | Every bitmap font in a folder | +| [pbm_simpletest.py](pbm_simpletest.py) | A one-bit PBM picture shown in colour | +| [pbm_create_new.py](pbm_create_new.py) | A one-bit logo drawn, then shown in colour | +| [bmp565_simpletest.py](bmp565_simpletest.py) | A whole BMP picture loaded and shown | +| [bmp565_sprite.py](bmp565_sprite.py) | A sprite walking, read from a sprite sheet | +| [bmp565_scroll.py](bmp565_scroll.py) | A tall picture scrolled a line at a time | +| [bmp565_scroll_sprite.py](bmp565_scroll_sprite.py) | A runner on a scrolling street, played by touch | + +Full apps that use pygraphics are in +[pydevices-examples](https://github.com/PyDevices/pydevices-examples). diff --git a/examples/assets/font_8x14.bin b/examples/assets/font_8x14.bin new file mode 100644 index 0000000..2c6d8bb Binary files /dev/null and b/examples/assets/font_8x14.bin differ diff --git a/examples/assets/font_8x16.bin b/examples/assets/font_8x16.bin new file mode 100644 index 0000000..0f3cbdf Binary files /dev/null and b/examples/assets/font_8x16.bin differ diff --git a/examples/assets/font_8x8.bin b/examples/assets/font_8x8.bin new file mode 100644 index 0000000..824b61d Binary files /dev/null and b/examples/assets/font_8x8.bin differ diff --git a/examples/assets/longstreet.bmp b/examples/assets/longstreet.bmp new file mode 100644 index 0000000..92dea1e Binary files /dev/null and b/examples/assets/longstreet.bmp differ diff --git a/examples/assets/micropython.pbm b/examples/assets/micropython.pbm new file mode 100644 index 0000000..2bb093c Binary files /dev/null and b/examples/assets/micropython.pbm differ diff --git a/examples/assets/runner.bmp b/examples/assets/runner.bmp new file mode 100644 index 0000000..59e9ad7 Binary files /dev/null and b/examples/assets/runner.bmp differ diff --git a/examples/assets/warrior.bmp b/examples/assets/warrior.bmp new file mode 100644 index 0000000..d3329de Binary files /dev/null and b/examples/assets/warrior.bmp differ diff --git a/examples/bmp565_scroll.py b/examples/bmp565_scroll.py new file mode 100644 index 0000000..ce67a93 --- /dev/null +++ b/examples/bmp565_scroll.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Scroll a tall BMP picture down the display. + +Draws a picture taller than the screen one line at a time, then keeps +going with the display's hardware scrolling (vscsad), so it never needs +the whole picture in memory. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics. + +Run it from the repository's root: + python examples/bmp565_scroll.py + +On a board, copy longstreet.bmp to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +import board_config +import appdev + +app = appdev.App(board_config) +from pygraphics import BMP565 + +display_drv.rotation = 0 + +image = BMP565("examples/assets/longstreet.bmp", streamed=True) +print(f"\n{image.width=}, {image.height=}, {image.bpp=}") + + +def draw_bg(dest_x, dest_y, source_x, source_y, source_image=image, width=image.width, height=1): + display_drv.blit_rect( + source_image[source_x : source_x + width, source_y : source_y + height], + dest_x, + dest_y, + width, + height, + ) + + +def main(): + st = {"phase": "fill", "j": 0, "i": display_drv.height} + + def _tick(_=None): + if app.quit_requested: + return + if st["phase"] == "fill": + j = st["j"] + if j < display_drv.height: + draw_bg(0, j, 0, j, height=1) + st["j"] = j + 1 + if st["j"] >= display_drv.height: + st["phase"] = "scroll" + return + # scroll phase + i = st["i"] + display_drv.vscsad(i % display_drv.height) + draw_bg(0, i % display_drv.height, 0, i % image.height) + st["i"] = i + 1 + + app.every(_tick, period=1) + + +main() diff --git a/examples/bmp565_scroll_sprite.py b/examples/bmp565_scroll_sprite.py new file mode 100644 index 0000000..4678713 --- /dev/null +++ b/examples/bmp565_scroll_sprite.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""A runner on a scrolling street. + +Scrolls a street picture across the display a column at a time while a +runner sprite runs on it. Touch or click the top half to jump and shoot, +the bottom left to jump, and the bottom right to shoot. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics. + +Run it from the repository's root: + python examples/bmp565_scroll_sprite.py + +On a board, copy longstreet.bmp and runner.bmp to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from collections import namedtuple + +from board_config import display_drv +import board_config +import appdev + +app = appdev.App(board_config) +from pygraphics import BMP565 + +point = namedtuple("point", "x y") + +display_drv.rotation = 90 + +display_drv.fill(0) + +image = BMP565("examples/assets/longstreet.bmp", streamed=True, mirrored=True) +print(f"\n{image.width=}, {image.height=}, {image.bpp=}") + + +def draw_bg(dest_x, source_y, count=1, source=image): + # 1D row slice is ``count * source.width`` pixels; blit as a vertical + # strip (width=count, height=source.width). Do not use display_drv.height — + # tall panels (e.g. Android after rotation) are larger than the bitmap. + display_drv.blit_rect( + source[source_y : source_y + count], dest_x, 0, count, source.width + ) + + +char_sprites = BMP565("examples/assets/runner.bmp", streamed=True) +print(f"\n{char_sprites.width=}, {char_sprites.height=}, {char_sprites.bpp=}") +char_height = char_sprites.height // 3 +char_width = char_sprites.width // 6 +bg = char_sprites[0] +print(f"{char_width=}, {char_height=} {bg=:#0x}\n") + +run_sprites = [point(x * char_width, 0) for x in range(6)] +shoot_sprites = [point(x * char_width, char_height) for x in range(6)] +jump_sprites = [point(x * char_width, char_height * 2) for x in range(2)] +jump_shoot_sprites = [point((x + 2) * char_width, char_height * 2) for x in range(2)] +shot_sprite = point(4 * char_width, char_height * 2) + + +def draw_sprite( + dest_x, dest_y, source_x, source_y, source=char_sprites, width=char_width, height=char_height +): + display_drv.blit_rect( + source[source_x : source_x + width, source_y : source_y + height], + dest_x, + dest_y, + width, + height, + ) + + +def main(): + st = { + "i": 0, + "scroll": 0, + "char_y": display_drv.height - char_height, + "char_x": 200, + "shot_location": 0, + "sprites": run_sprites, + } + + def _on_motion(event): + if not event.buttons[0]: + st["sprites"] = run_sprites + return + touched_point = event.pos + if touched_point[1] < display_drv.height // 2: + st["sprites"] = jump_shoot_sprites + if not st["shot_location"]: + st["shot_location"] = 1 + elif touched_point[0] < display_drv.width // 2: + st["sprites"] = jump_sprites + elif touched_point[0] > display_drv.width // 2: + st["sprites"] = shoot_sprites + if not st["shot_location"]: + st["shot_location"] = 1 + else: + st["sprites"] = run_sprites + + def _tick(_=None): + # Auto-service handles QUIT; never poll from on_tick. + if app.quit_requested: + return + i = st["i"] + if i > display_drv.width: + st["scroll"] = i % display_drv.width + display_drv.vscsad(st["scroll"]) + draw_bg(i % display_drv.width, i % image.height, 1) + st["i"] = i + 1 + if i < display_drv.width: + return + + draw_x = st["scroll"] + st["char_x"] + sprite = st["sprites"][st["i"] % len(st["sprites"])] + draw_sprite(draw_x, st["char_y"], sprite.x, sprite.y) + if st["shot_location"]: + draw_sprite( + draw_x + char_width + st["shot_location"], + st["char_y"], + shot_sprite.x, + shot_sprite.y, + ) + st["shot_location"] += 8 + if st["shot_location"] > (display_drv.width - char_width) // 2: + display_drv.fill_rect( + draw_x + char_width + st["shot_location"], + st["char_y"], + char_width, + char_height, + bg, + ) + st["shot_location"] = 0 + + app.on(app.events.MOUSEMOTION, _on_motion) + # ~20 fps once scrolling; first columns also tick so quit is always serviced. + app.every(_tick, period=50) + + +main() diff --git a/examples/bmp565_simpletest.py b/examples/bmp565_simpletest.py new file mode 100644 index 0000000..d89b8f4 --- /dev/null +++ b/examples/bmp565_simpletest.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Load a BMP picture and show it. + +Loads the whole of a 16-bit BMP into memory and copies it to the display +two ways: as a slice, bmp[x0:x1, y0:y1], and as bmp[:]. Loading it all +needs RAM: a small board such as an RP2040 raises MemoryError. See +bmp565_scroll_sprite.py for reading a picture a line at a time instead. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/bmp565_simpletest.py + +On a board, copy warrior.bmp to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import BMP565, hline + +try: + from time import sleep_ms +except ImportError: + from time import sleep + + def sleep_ms(milliseconds): + sleep(milliseconds / 1000) + + +try: + bmp = BMP565("examples/assets/warrior.bmp") +except MemoryError: + raise MemoryError("this board doesn't have enough RAM to load the full image") + +print(f"{bmp.width=}, {bmp.height=}, {bmp.bpp=}") + +display_drv.fill(0x0) +display_drv.show() +display_drv.blit_rect(bmp[0 : bmp.width, 0 : bmp.height], 0, 0, bmp.width, bmp.height) +display_drv.show() +sleep_ms(1000) + +display_drv.fill(0x0) +display_drv.show() +sleep_ms(250) + +hline(bmp, 0, bmp.height // 2, bmp.width, 0xFFFF) +display_drv.blit_rect(bmp[:], 0, 0, bmp.width, bmp.height) +display_drv.show() +sleep_ms(1000) diff --git a/examples/bmp565_sprite.py b/examples/bmp565_sprite.py new file mode 100644 index 0000000..4952e93 --- /dev/null +++ b/examples/bmp565_sprite.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Walk a sprite around the screen. + +Reads a sprite sheet from a BMP a piece at a time, and draws one frame of +a walking figure every tenth of a second, turning now and then and +stopping at the edges. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics. + +Run it from the repository's root: + python examples/bmp565_sprite.py + +On a board, copy warrior.bmp to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from collections import namedtuple + +try: + from random import choice +except ImportError: + + def choice(seq): + return seq[0] + + +from board_config import display_drv +import board_config +import appdev + +app = appdev.App(board_config) +from pygraphics import BMP565 + +image = BMP565("examples/assets/warrior.bmp", streamed=True) +print(f"\n{image.width=}, {image.height=}, {image.bpp=}") +sprite_height = image.height // 4 +sprite_width = image.width // 3 +bg = image[0] +print(f"{sprite_width=}, {sprite_height=} {bg=:#0x}\n") + +back, right, fwd, left = [x * sprite_height for x in range(4)] +directions = [fwd, left, right, back] +a, b, c = [x * sprite_width for x in range(3)] +positions = [a, b, c, b] +print("Sprite coordinates:") +for col in [fwd, left, right, back]: + print(f"{(a, col)} {(b, col)} {(c, col)} {(b, col)}") + + +def draw_sprite( + dest_x, + dest_y, + source_x, + source_y, + source_image=image, + width=sprite_width, + height=sprite_height, +): + display_drv.blit_rect( + source_image[source_x : source_x + width, source_y : source_y + height], + dest_x, + dest_y, + width, + height, + ) + + +display_drv.fill(bg) + +point = namedtuple("point", "x y") +step = 3 +st = { + "location": point(0, 0), + "dir": choice(directions), + "pos_i": 0, +} + +draw_sprite(*st["location"], a, fwd) + + +def _tick(_=None): + if app.quit_requested if app else False: + return + location = st["location"] + direction = st["dir"] + if st["pos_i"] == 0 and choice((True, False, False, False, False)): + direction = choice(directions) + st["dir"] = direction + if direction == fwd and location.y + sprite_height > display_drv.height - step * 4: + return + if direction == back and location.y < step * 4: + return + if direction == left and location.x < step * 4: + return + if direction == right and location.x + sprite_width > display_drv.width - step * 4: + return + + pos = positions[st["pos_i"]] + if direction == fwd: + display_drv.fill_rect(location.x, location.y, sprite_width, step, bg) + location = point(location.x, location.y + step) + elif direction == back: + display_drv.fill_rect( + location.x, location.y + sprite_height - step, sprite_width, step, bg + ) + location = point(location.x, location.y - step) + elif direction == left: + display_drv.fill_rect( + location.x + sprite_width - step, location.y, step, sprite_height, bg + ) + location = point(location.x - step, location.y) + elif direction == right: + display_drv.fill_rect(location.x, location.y, step, sprite_height, bg) + location = point(location.x + step, location.y) + st["location"] = location + draw_sprite(*location, pos, direction) + st["pos_i"] = (st["pos_i"] + 1) % len(positions) + + +app.every(_tick, period=100) diff --git a/examples/font_list.py b/examples/font_list.py new file mode 100644 index 0000000..99737d7 --- /dev/null +++ b/examples/font_list.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Show every bitmap font in a folder. + +Finds each .bin font in examples/assets/, draws the alphabet and the +font's name in it, and scrolls the display when the list runs past the +bottom. Font files are named fontname_WxH.bin, W wide and H high. Set +EXPORT to True to also save each font as a .py module. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/font_list.py + +On a board, copy the font_*.bin files to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import Font, FrameBuffer, RGB565 +import os + + +# Change this to match your setup: True also saves each font as a .py file. +EXPORT = False + +display_drv.fill(0) +y_pos = 0 + + +def main(): + global y_pos + + # Specify the directory containing the .bin files + directory = "examples/assets/" + + def gather_font_files(directory): + font_files = [] + for file in os.listdir(directory): + if file.endswith(".bin"): + file_path = "/".join([directory, file]) + font_files.append(file_path) + return font_files + + font_files = gather_font_files(directory) + + bg_color = 0 + fg_color = 0x07E0 + scale = 1 + + for font_file in font_files: + try: + font = Font(font_file) + except RuntimeError as e: + print(e) + continue + + if EXPORT: + font.export(font.font_data.replace(".bin", ".py")) + + height = font.height + font_name = font.font_name + + buffer = bytearray(display_drv.width * height * 2) + fb = FrameBuffer(buffer, display_drv.width, height, RGB565) + fb.fill(bg_color) + string = "".join([chr(i) for i in range(65, 91)]) + ": " + font_name.split("/")[-1] + font.text(fb, string, 0, 0, fg_color, scale) + display_drv.blit_rect(buffer, 0, y_pos % display_drv.height, display_drv.width, height) + y_pos += height + if y_pos > display_drv.height: + display_drv.vscsad((y_pos - display_drv.height) % display_drv.height) + display_drv.show() + + +main() diff --git a/examples/framebuf_simpletest.py b/examples/framebuf_simpletest.py new file mode 100644 index 0000000..746bdcc --- /dev/null +++ b/examples/framebuf_simpletest.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Draw into a FrameBuffer, then copy it to the display. + +Draws the same shapes as graphics_simpletest.py, but into a pygraphics +FrameBuffer the size of the screen, and copies the whole buffer on with +blit_rect. This is how you draw without flicker on a display that has no +memory of its own. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/framebuf_simpletest.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import FrameBuffer, RGB565 +from array import array # for defining a polygon + +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + +WIDTH = display_drv.width +HEIGHT = display_drv.height +FONT_WIDTH = 8 + +# Create a frame buffer +BPP = display_drv.color_depth // 8 # Bytes per pixel +ba = bytearray(WIDTH * HEIGHT * BPP) +fb = FrameBuffer(ba, WIDTH, HEIGHT, RGB565) + + +# Define color palette +class pal: + BLACK = 0x0000 + WHITE = 0xFFFF + RED = 0xF800 if not needs_swap else 0x00F8 + GREEN = 0x07E0 if not needs_swap else 0xE007 + BLUE = 0x001F if not needs_swap else 0xF800 + CYAN = 0x07FF if not needs_swap else 0xFF07 + MAGENTA = 0xF81F if not needs_swap else 0x1FF8 + YELLOW = 0xFFE0 if not needs_swap else 0xE0FF + ORANGE = 0xFD20 if not needs_swap else 0x20FD + PURPLE = 0x8010 if not needs_swap else 0x1080 + GREY = 0x8410 if not needs_swap else 0x1084 + + +# Define objects +triangle = array("h", [0, 0, WIDTH // 2, -HEIGHT // 4, WIDTH - 1, 0]) + + +# Main loop +def main(scroll=False, animate=False, text1="framebuf", text2="simpletest", poly=triangle): + y_range = range(HEIGHT - 1, -1, -1) if animate else [HEIGHT - 1] + for y in y_range: + fb.fill(pal.BLACK) + fb.poly(0, y, poly, pal.YELLOW, True) + fb.fill_rect(WIDTH // 6, HEIGHT // 3, WIDTH * 2 // 3, HEIGHT // 3, pal.GREY) + fb.line(0, 0, WIDTH - 1, HEIGHT - 1, pal.GREEN) + fb.rect(0, 0, 15, 15, pal.RED, True) + fb.rect(WIDTH - 15, HEIGHT - 15, 15, 15, pal.BLUE, True) + fb.hline(WIDTH // 8, HEIGHT // 2, WIDTH * 3 // 4, pal.MAGENTA) + fb.vline(WIDTH // 2, HEIGHT // 4, HEIGHT // 2, pal.CYAN) + fb.pixel(WIDTH // 2, HEIGHT * 1 // 8, pal.WHITE) + fb.ellipse(WIDTH // 2, HEIGHT // 2, WIDTH // 4, HEIGHT // 8, pal.BLACK, True, 0b1111) + fb.text(text1, (WIDTH - FONT_WIDTH * len(text1)) // 2, HEIGHT // 2 - 8, pal.WHITE) + fb.text(text2, (WIDTH - FONT_WIDTH * len(text2)) // 2, HEIGHT // 2, pal.WHITE) + display_drv.blit_rect(ba, 0, 0, WIDTH, HEIGHT) + display_drv.show() + + fb.hline(0, 0, WIDTH, pal.BLACK) + fb.vline(0, 0, HEIGHT, pal.BLACK) + + scroll_range = range(min(WIDTH, HEIGHT)) if scroll else [0] + for _ in scroll_range: + fb.scroll(1, 1) + display_drv.blit_rect(ba, 0, 0, WIDTH, HEIGHT) + display_drv.show() + + +launch = lambda: main(animate=False) # noqa: E731 + +wipe = lambda: main(scroll=False) # noqa: E731 + +main() +display_drv.show() diff --git a/examples/graphics_simpletest.py b/examples/graphics_simpletest.py new file mode 100644 index 0000000..240adc9 --- /dev/null +++ b/examples/graphics_simpletest.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Draw shapes and text straight onto a display with pygraphics. + +Fills the screen, then draws a triangle, rectangles, lines, a pixel, an +ellipse and two lines of text, each with one pygraphics function call on +the display itself, with colours from palettes. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics, and palettes. + +Run it from the repository's root: + python examples/graphics_simpletest.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from array import array # for defining a polygon +from palettes import get_palette +import pygraphics + + +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + +WIDTH = display_drv.width +HEIGHT = display_drv.height +FONT_WIDTH = 8 + +# Define color palette +pal = get_palette(swapped=needs_swap) + +# Define objects +triangle = array("h", [0, 0, WIDTH // 2, -HEIGHT // 4, WIDTH - 1, 0]) + + +# Main loop +def main(animate=False, text1="Shapes", text2="simpletest", poly=triangle): + y_range = range(HEIGHT - 1, -1, -1) if animate else [HEIGHT - 1] + for y in y_range: + pygraphics.fill(display_drv, pal.BLACK) + pygraphics.poly(display_drv, 0, y, poly, pal.YELLOW, True) + pygraphics.fill_rect( + display_drv, + WIDTH // 6, + HEIGHT // 3, + WIDTH * 2 // 3, + HEIGHT // 3, + pal.GREY, + ) + pygraphics.line(display_drv, 0, 0, WIDTH - 1, HEIGHT - 1, pal.GREEN) + pygraphics.rect(display_drv, 0, 0, 15, 15, pal.RED, True) + pygraphics.rect(display_drv, WIDTH - 15, HEIGHT - 15, 15, 15, pal.BLUE, True) + pygraphics.hline(display_drv, WIDTH // 8, HEIGHT // 2, WIDTH * 3 // 4, pal.MAGENTA) + pygraphics.vline(display_drv, WIDTH // 2, HEIGHT // 4, HEIGHT // 2, pal.CYAN) + pygraphics.pixel(display_drv, WIDTH // 2, HEIGHT * 1 // 8, pal.WHITE) + pygraphics.ellipse( + display_drv, + WIDTH // 2, + HEIGHT // 2, + WIDTH // 4, + HEIGHT // 8, + pal.BLACK, + True, + 0b1111, + ) + pygraphics.text( + display_drv, text1, (WIDTH - FONT_WIDTH * len(text1)) // 2, HEIGHT // 2 - 8, pal.WHITE + ) + pygraphics.text( + display_drv, text2, (WIDTH - FONT_WIDTH * len(text2)) // 2, HEIGHT // 2, pal.WHITE + ) + + pygraphics.hline(display_drv, 0, 0, WIDTH, pal.BLACK) + pygraphics.vline(display_drv, 0, 0, HEIGHT, pal.BLACK) + + +launch = lambda: main(animate=False) # noqa: E731 + +main() +display_drv.show() diff --git a/examples/pbm_create_new.py b/examples/pbm_create_new.py new file mode 100644 index 0000000..2437b57 --- /dev/null +++ b/examples/pbm_create_new.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Draw a one-bit logo, then show it in colour. + +Draws a logo into a one-bit MONO_HLSB FrameBuffer, the format a PBM file +holds, colours it through a two-entry palette, and copies it to the +display as it is and with its background left transparent. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/pbm_create_new.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import Draw, FrameBuffer, MONO_HLSB, RGB565 + + +def draw_logo(logo): + w = logo.width + h = logo.height + black = background = 0 + blue = amber = 1 + center_x = w // 2 + center_y = h // 2 + unit = min(w, h) // 2 + draw = Draw(logo) + + logo.fill(background) + + draw.circle(center_x, center_y, unit, blue, True) + draw.circle(center_x, center_y, int(unit * 0.9), black, True) + draw.circle(center_x, center_y, int(unit * 0.8), amber, True) + + left = int(center_x - (unit * 1.2) // 2) + top = int(center_y - (unit * 1.0) // 2) + draw.round_rect(left, top, int(unit * 1.2), int(unit * 1.0), unit // 7, black, True) + + left = int(center_x - (unit * 1.1) // 2) + top = int(center_y - (unit * 0.9) // 2) + draw.round_rect(left, top, int(unit * 1.1), int(unit * 0.9), unit // 9, amber, True) + + left = center_x - (unit * 3 // 8) + top = center_y - (unit * 3 // 8) + size = unit // 4 + + draw.fill_rect(left, top, size, size, black) + draw.fill_rect(w - (left + size), h - (top + size), size, size, black) + + size = size // 2 + draw.circle(w - (left + size), top + size, size, black, True) + draw.circle(left + size, h - (top + size), size, black, True) + + +display_drv.fill(0xF800) + +w = h = 64 +logo = FrameBuffer(bytearray((w + 7) // 8 * h), w, h, MONO_HLSB) +draw_logo(logo) + +buf = bytearray(w * h * 2) +fb = FrameBuffer(buf, w, h, RGB565) +palette = FrameBuffer(memoryview(bytearray(2 * 2)), 2, 1, RGB565) + +# White on black — left and middle columns (opaque top, chromakey bottom). +palette.pixel(0, 0, 0x0000) +palette.pixel(1, 0, 0xFFFF) +fb.blit(logo, 0, 0, -1, palette) +display_drv.blit_rect(buf, 0, 0, w, h) +display_drv.blit_transparent(buf, 0, display_drv.height // 2, w, h, 0x0) +display_drv.blit_rect(buf, display_drv.width // 3, 0, w, h) +display_drv.blit_transparent( + buf, display_drv.width // 3, display_drv.height // 2, w, h, 0x0 +) + +# Green / white on blue staging — right column. +palette.pixel(0, 0, 0x0FF0) +palette.pixel(1, 0, 0xFFFF) +fb.fill(0x000F) +fb.blit(logo, 0, 0, -1, palette) +display_drv.blit_rect(buf, display_drv.width * 2 // 3, 0, w, h) +display_drv.blit_transparent( + buf, display_drv.width * 2 // 3, display_drv.height // 2, w, h, 0x000F +) + +display_drv.show() diff --git a/examples/pbm_simpletest.py b/examples/pbm_simpletest.py new file mode 100644 index 0000000..d55c0bd --- /dev/null +++ b/examples/pbm_simpletest.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Load a PBM picture and show it in colour. + +Reads a one-bit PBM file into a FrameBuffer, colours it through a +two-entry palette, and copies it to the display twice: once as it is, and +once with its background left transparent. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, and pygraphics. + +Run it from the repository's root: + python examples/pbm_simpletest.py + +On a board, copy micropython.pbm to the board's examples/assets/ first. + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +from pygraphics import FrameBuffer, RGB565, pbm_to_framebuffer + + +display_drv.fill(0xF800) +logo = pbm_to_framebuffer("examples/assets/micropython.pbm") + +# Colorize mono → RGB565 via a 2-entry palette, then blit to the display. +buf = bytearray(logo.width * logo.height * 2) +fb = FrameBuffer(buf, logo.width, logo.height, RGB565) +palette = FrameBuffer(memoryview(bytearray(2 * 2)), 2, 1, RGB565) + +# Set bits → white; unset bits stay 0 (used as chromakey below). +palette.pixel(0, 0, 0x0000) +palette.pixel(1, 0, 0xFFFF) +fb.fill(0x0000) +fb.blit(logo, 0, 0, -1, palette) + +display_drv.blit_rect(buf, display_drv.width // 3, 0, logo.width, logo.height) +display_drv.blit_transparent( + buf, display_drv.width // 3, display_drv.height // 2, logo.width, logo.height, 0x0 +) + +# Green / white on blue; transparent blit keys on the staging fill color 0x000F. +palette.pixel(0, 0, 0x0FF0) +palette.pixel(1, 0, 0xFFFF) +fb.fill(0x000F) +fb.blit(logo, 0, 0, -1, palette) + +display_drv.blit_rect(buf, display_drv.width * 2 // 3, 0, logo.width, logo.height) +display_drv.blit_transparent( + buf, display_drv.width * 2 // 3, display_drv.height // 2, logo.width, logo.height, 0x000F +) + +display_drv.show() diff --git a/examples/pygraphics_landscape.py b/examples/pygraphics_landscape.py new file mode 100644 index 0000000..d69bb41 --- /dev/null +++ b/examples/pygraphics_landscape.py @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Draw a landscape with pygraphics: sky, sun, mountains and a house. + +The picture scales to whatever display the board_config provides, from a +small OLED to a desktop window. It is drawn in horizontal bands so it fits +the memory of a small board: each band is drawn into a FrameBuffer and +copied to the display before the next one. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics, and multimer. + +Run it from the repository's root: + python examples/pygraphics_landscape.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +import board_config +import appdev +import sys + +import pygraphics +from multimer import ticks_diff, ticks_ms +from pygraphics import RGB565, FrameBuffer + +app = appdev.App(board_config) + +# The picture is designed at 320x240 and scaled to the display. +W, H = display_drv.width, display_drv.height +SX, SY = W / 320, H / 240 +S = min(SX, SY) +BAND = max(1, min(H, 12800 // W)) # rows per band: about 25 KB of RGB565 + +# Colours go to the panel in its own byte order, so nothing is swapped later. +SWAP = getattr(display_drv, "requires_byteswap", False) +if SWAP: + display_drv.disable_auto_byteswap(True) + + +def rgb(r, g, b): + c = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) + return ((c & 0xFF) << 8) | (c >> 8) if SWAP else c + + +SKY = [rgb(10, 20, 90), rgb(20, 50, 140), rgb(40, 90, 190), + rgb(70, 130, 220), rgb(110, 170, 240), rgb(160, 205, 250)] +SUN, SUN_RING = rgb(255, 220, 0), rgb(255, 150, 0) +ROCK, SNOW = rgb(110, 110, 130), rgb(250, 250, 250) +GRASS, GRASS_DARK = rgb(40, 160, 60), rgb(20, 110, 40) +WALL, ROOF, DOOR = rgb(220, 60, 50), rgb(120, 40, 30), rgb(80, 50, 20) +WHITE, BLACK = rgb(255, 255, 255), rgb(0, 0, 0) + + +def x(v): + return int(v * SX) + + +def y(v, y0): + return int(v * SY) - y0 + + +def scene(fb, y0): + """The whole picture, drawn y0 rows up; the band clips what falls outside.""" + for i, c in enumerate(SKY): + fb.fill_rect(0, y(i * 27, y0), W, int(27 * SY) + 1, c) + fb.circle(x(255), y(58, y0), int(34 * S), SUN_RING, True) + fb.circle(x(255), y(58, y0), int(28 * S), SUN, True) + fb.triangle(x(-20), y(170, y0), x(90), y(60, y0), x(200), y(170, y0), ROCK, True) + fb.triangle(x(70), y(80, y0), x(90), y(60, y0), x(110), y(80, y0), SNOW, True) + fb.triangle(x(110), y(170, y0), x(210), y(85, y0), x(330), y(170, y0), ROCK, True) + fb.triangle(x(192), y(100, y0), x(210), y(85, y0), x(228), y(100, y0), SNOW, True) + fb.fill_rect(0, y(162, y0), W, H - int(162 * SY), GRASS) + fb.fill_rect(0, y(162, y0), W, max(1, int(4 * SY)), GRASS_DARK) + fb.fill_rect(x(220), y(160, y0), x(70), int(50 * SY), WALL) + fb.triangle(x(212), y(160, y0), x(255), y(126, y0), x(298), y(160, y0), ROOF, True) + fb.fill_rect(x(248), y(182, y0), max(1, x(14)), int(28 * SY), DOOR) + if W >= 160: # captions need room; a tiny display gets the picture alone + fb.round_rect(8, 8 - y0, 136, 26, 6, BLACK, True) + fb.text16("pygraphics", 16, 13 - y0, WHITE) + fb.text(sys.implementation.name, 10, H - 24 - y0, WHITE) + fb.text("pygraphics " + pygraphics.implementation(), 10, H - 12 - y0, WHITE) + fb.rect(0, -y0, W, H, WHITE) + + +def draw(): + buf = bytearray(W * BAND * 2) + fb = FrameBuffer(buf, W, BAND, RGB565) + t0 = ticks_ms() + for y0 in range(0, H, BAND): + rows = min(BAND, H - y0) + scene(fb, y0) + display_drv.blit_rect(memoryview(buf)[: W * rows * 2], 0, y0, W, rows) + display_drv.show() + print("landscape %dx%d drawn in %d ms" % (W, H, ticks_diff(ticks_ms(), t0))) + + +draw() +app.run() diff --git a/examples/rotations.py b/examples/rotations.py new file mode 100644 index 0000000..3310e17 --- /dev/null +++ b/examples/rotations.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2026 Brad Barnett +# +# SPDX-License-Identifier: MIT +"""Turn the display through all four rotations. + +Every two seconds the display turns another 90 degrees and changes colour, +and says which rotation and colour it shows. Use it to check that a +board_config's rotation settings put the picture the right way up. + +Needs: a board_config with a display_drv, which a PyDevices board +package or pydevices-desktop provides, appdev and pygraphics, and palettes. + +Run it from the repository's root: + python examples/rotations.py + +Tested on: CPython 3.12 on Linux, in an SDL window. +""" + +from board_config import display_drv +import board_config +import appdev + +app = appdev.App(board_config) +from pygraphics import Draw, text16 +from palettes import get_palette + +pal = get_palette() +draw = Draw(display_drv) + +FONT_W = 8 +FONT_H = 16 + + +def center_text(text, y, fg, bg): + x = (display_drv.width - len(text) * FONT_W) // 2 + # Opaque label: fill bg strip then draw glyphs. + draw.fill_rect(x, y, len(text) * FONT_W, FONT_H, bg) + text16(display_drv, text, x, y, fg) + + +def clear_screen(color): + """Expand rectangles from the center toward the edges.""" + width = display_drv.width + height = display_drv.height + x_center = width // 2 + y_center = height // 2 + + for i in range(min(x_center, y_center)): + x = x_center - i + y = y_center - i + draw.rect(x, y, 2 * i + 1, 2 * i + 1, color) + + +def main(): + colors = ( + ("Red", pal.RED, pal.WHITE), + ("Green", pal.GREEN, pal.BLACK), + ("Blue", pal.BLUE, pal.WHITE), + ("Black", pal.BLACK, pal.WHITE), + ("White", pal.WHITE, pal.BLACK), + ("Yellow", pal.YELLOW, pal.BLACK), + ("Cyan", pal.CYAN, pal.BLACK), + ("Magenta", pal.MAGENTA, pal.BLACK), + ) + + st = {"rotation": 0, "color_idx": 0} + + def poll(): + rotation = st["rotation"] + color_idx = st["color_idx"] + display_drv.rotation = rotation * 90 + height = display_drv.height + fg = colors[color_idx][2] + bg = colors[color_idx][1] + + draw.fill(bg) + draw.rect(0, 0, display_drv.width, height, pal.WHITE) + clear_screen(bg) + center_text("Rotation", height // 3 - FONT_H // 2, fg, bg) + center_text(str(rotation * 90), height // 2 - FONT_H // 2, fg, bg) + center_text(colors[color_idx][0], height // 3 * 2 - FONT_H // 2, fg, bg) + st["color_idx"] = (color_idx + 1) % len(colors) + st["rotation"] = (rotation + 1) % 4 + return False + + def _tick(_=None): + poll() + + app.every(_tick, period=2000) + + +main()