Přeskočit na obsah

Kreslení napřímo

C modul picogame je i obyčejná kreslicí knihovna. Canvas kreslí tvary, bitmapy a text do libovolného RGB565 bufferu a Bitmap obalí libovolný buffer pixelů, aniž by ho kopírovala. Sprity a Scene jsou postavené nad tím a používat je nemusíš.

Jak kreslit, záleží na tom, kde leží pixely obrazovky.

  • Desky s framebufferem (Fruit Jam a další DVI desky): celá obrazovka je buffer v RAM. Položíš na něj Canvas a kreslíš, stejně jako v SDL nebo Pygame. Všechno pod dalším nadpisem platí pro tyhle desky.
  • SPI displeje (PicoPad a většina desek s malým displejem): pixely jsou v displeji a na kopii není místo - snímek 320×240 v RGB565 má 150 KB a hra na RP2040 má volných 25-40 KB RAM. picogame tam celý snímek nikdy nedrží; vykresluje po několika řádcích a posílá je do displeje. Kreslit můžeš pořád napřímo, ale uvnitř callbacku StripDraw, který se volá pro každý pruh.
import board
import displayio
import picodvi
import picogame as pg
displayio.release_displays()
framebuffer = picodvi.Framebuffer(320, 240, color_depth=16,
clk_dp=board.CKP, clk_dn=board.CKN,
red_dp=board.D0P, red_dn=board.D0N,
green_dp=board.D1P, green_dn=board.D1N,
blue_dp=board.D2P, blue_dn=board.D2N)
screen = pg.Canvas(320, 240, buffer=framebuffer)
def color(r, g, b):
"""RGB565 v pořadí bajtů, které čte picodvi."""
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)

screen teď kreslí do paměti, kterou DVI výstup zobrazuje. Žádné volání refresh není potřeba: co zapíšeš, je na obrazovce.

Pro všechny barvy na téhle obrazovce, včetně palet, používej color(). pg.rgb565() vrací pořadí bajtů pro SPI displeje, takže na DVI obrazovce by se z červené stala modrá.

screen.clear(color(0, 0, 40))
screen.fill_rect(200, 10, 50, 30, color(255, 0, 0))
screen.fill_circle(290, 25, 15, color(0, 255, 0))
screen.line(200, 50, 310, 50, color(255, 255, 255))

Canvas má dále pixel, rect, circle, ring, triangle, fill_triangle, ellipse, fill_ellipse, fill_round_rect a frame3d, plus dávková volání pro mnoho trojúhelníků nebo úseků najednou. Úplný seznam je v referenci.

Jednotlivé pixely můžeš zapisovat i přímo do bufferu: memoryview(framebuffer) má jednu položku na pixel, takže pixels[y * 320 + x] = color(255, 255, 255).

Soubor přečte adafruit_imageload. pg.Bitmap pak používá tytéž pixely, převádí se jen paleta:

import array
import adafruit_imageload
def load_image(path):
"""Načte 8bitový (indexovaný) BMP jako picogame Bitmap."""
bitmap, palette = adafruit_imageload.load(
path,
bitmap=lambda width, height, colors: displayio.Bitmap(width, height, 256),
palette=displayio.Palette)
colors = array.array("H", [0] * 256)
for index in range(len(palette)):
rgb = palette[index]
colors[index] = color(rgb >> 16, (rgb >> 8) & 0xFF, rgb & 0xFF)
pixels = memoryview(bitmap)
return pg.Bitmap(pixels, bitmap.width, bitmap.height, format=pg.PAL8,
palette=colors, stride=len(pixels) // bitmap.height)
picture = load_image("/picture.bmp")
screen.blit(picture, 10, 10)

Obrázek ulož jako 8bitový (256barevný) BMP. Umí to většina grafických editorů; v Pillow je to Image.open("picture.png").convert("RGB").quantize(256).save("picture.bmp").

blit(bitmap, x, y, frame=0, flip_x=False, flip_y=False) vykreslí bitmapu na libovolné místo, oříznutou podle canvasu. Chceš-li jednu barvu palety průhlednou, předej její index jako transparent= při vytváření pg.Bitmap.

import terminalio
screen.text(200, 60, "Hello, DVI", color(255, 255, 255), terminalio.FONT)
screen.text(200, 76, "on a box", color(0, 0, 0), terminalio.FONT, color(255, 200, 0))

Argumenty jsou x, y, text, barva, font a volitelně barva pozadí jako šestý argument. x, y je levý horní roh. terminalio.FONT má 6×12 pixelů na znak. Tahle C cesta bere jen vestavěný font.

Vlastní font (BDF nebo PCF)

Sekce “Vlastní font (BDF nebo PCF)”

Pro jakýkoli jiný font načte glyfy adafruit_bitmap_font a z každého se stane malá Bitmap, kterou vykreslíš přes blit:

from adafruit_bitmap_font import bitmap_font
font = bitmap_font.load_font("/fonts/myfont.pcf")
glyph_cache = {}
def glyph_bitmap(glyph, text_color):
"""Převede jeden glyf fontu na picogame Bitmap s průhledným pozadím."""
width, height = glyph.width, glyph.height
pixels = bytearray(width * height)
for y in range(height):
for x in range(width):
pixels[y * width + x] = glyph.bitmap[x, y]
palette = array.array("H", (0, text_color))
return pg.Bitmap(pixels, width, height, format=pg.PAL8,
palette=palette, transparent=0)
def draw_text(canvas, x, y, text, text_color):
"""Vykreslí text s účařím na y."""
font.load_glyphs(text)
for char in text:
glyph = font.get_glyph(ord(char))
if glyph is None:
continue
if glyph.width and glyph.height: # mezera má prázdnou bitmapu
key = (char, text_color)
if key not in glyph_cache:
glyph_cache[key] = glyph_bitmap(glyph, text_color)
canvas.blit(glyph_cache[key], x + glyph.dx, y - glyph.height - glyph.dy)
x += glyph.shift_x
draw_text(screen, 10, 170, "Proportional text", color(255, 220, 0))

Tohle zvládá i proporcionální fonty: každý glyf se umístí podle vlastního posunu a šířky. Každý znak se převede jednou pro každou barvu a pak se bere z glyph_cache.

Stejný kód čte BDF i PCF. U velkých fontů dej přednost PCF: je to binární formát, obvykle zhruba poloviční oproti stejnému BDF, a adafruit_bitmap_font z něj čte jen glyfy, o které požádáš. BDF na PCF převede bdftopcf (součást nástrojů X.Org).

Canvas umí kreslit do libovolného bytearray a pg.Bitmap z něj udělá něco, co jde vykreslit přes blit:

panel_pixels = bytearray(120 * 40 * 2) # šířka * výška * 2 bajty (RGB565)
panel = pg.Canvas(120, 40, buffer=panel_pixels)
panel.clear(color(70, 70, 90))
panel.rect(0, 0, 120, 40, color(255, 255, 255))
panel.text(8, 14, "off-screen", color(255, 255, 255), terminalio.FONT)
panel_bitmap = pg.Bitmap(panel_pixels, 120, 40)
screen.blit(panel_bitmap, 190, 190)

Takhle můžeš panel, sprite nebo celý snímek poskládat jednou a vykreslovat ho mnohokrát. Je to obyčejný bytearray, takže s týmiž pixely může pracovat i tvůj vlastní kód - viper, nativní modul, ulab.

Žádná smyčka snímků, do které by ses musel zapojit, není: smaž, co se pohnulo, nakresli to na novém místě, opakuj.

import time
x = 0
while True:
screen.fill_rect(x, 200, 20, 20, color(0, 0, 40)) # smaž starý čtverec
x = (x + 2) % 160
screen.fill_rect(x, 200, 20, 20, color(0, 200, 255)) # nakresli nový
time.sleep(1 / 60)

Kreslíš do obrazu, který je právě na obrazovce, takže velké překreslení může být vidět rozpracované. Velké změny poskládej v bufferu mimo obrazovku a výsledek vykresli jedním voláním.

Na SPI displeji: StripDraw

Sekce “Na SPI displeji: StripDraw”

Na SPI displeji vykresluje obrazovku Scene po pruzích řádků a vrstva StripDraw volá tvou funkci pro každý pruh. Uvnitř dostaneš Canvas pro daný pruh a používáš stejná volání jako výše:

import board
import terminalio
import picogame as pg
display = board.DISPLAY
WIDTH, HEIGHT = display.width, display.height
STRIP_HEIGHT = 8
scene = pg.Scene(display,
bytearray(WIDTH * STRIP_HEIGHT * 2), bytearray(WIDTH * STRIP_HEIGHT * 2))
ball_x = 0
def draw(view, strip_x, strip_y, strip_width, strip_height):
# view pokrývá řádky obrazovky strip_y .. strip_y + strip_height - 1,
# takže od každého y na obrazovce odečti strip_y
view.clear(pg.rgb565(0, 0, 40))
view.fill_circle(ball_x, HEIGHT // 2 - strip_y, 20, pg.rgb565(0, 255, 0))
view.text(10, 10 - strip_y, "Hello", pg.rgb565(255, 255, 255), terminalio.FONT)
scene.add(pg.StripDraw(draw, 0, 0, WIDTH, HEIGHT))
while True:
ball_x = (ball_x + 2) % WIDTH
scene.refresh()

Barvy tady dává pg.rgb565(), protože tohle je pořadí bajtů SPI displejů. Co se do jednoho pruhu nevejde - obrázek, delší řádek textu - se prostě vykreslí znovu, oříznuté, v dalším pruhu.

Tvoje funkce běží pro každý pruh, takže v ní jen kresli. Pozice a stav hry spočítej jednou za snímek, před scene.refresh().

StripDraw se překresluje při každém refresh, což je správně pro obsah, který se hýbe. Obsah, který se mění jen občas, vytvoř s always_dirty=False a po změně zavolej jeho invalidate(); jinak ho scéna přeskočí.

Všechno výše je celý C modul viděný zespodu. Scene, Sprite, Tilemap a pomocné knihovny picogame_* přidávají překreslování jen toho, co se změnilo, kolize, animace, vstup a textové widgety. Jak do sebe zapadají, ukazují stránky Jak picogame funguje a Kreslicí cesty.