diff options
author | Raghuram Subramani <raghus2247@gmail.com> | 2022-06-24 18:23:36 +0530 |
---|---|---|
committer | Raghuram Subramani <raghus2247@gmail.com> | 2022-06-24 18:23:36 +0530 |
commit | 08422ad7e5ae71a1a625e8a2bda6d233fd68ceee (patch) | |
tree | be3431f4d218e9bf8edf7826b1a86a976ae8215b /circuitpython/frozen | |
parent | 2423eadd0b6fd0eb4d92c8ddd0f605a6b1075b09 (diff) |
finish parser, modify files
Diffstat (limited to 'circuitpython/frozen')
-rw-r--r--[l---------] | circuitpython/frozen/circuitpython-stage/docs/README.rst | 45 | ||||
-rw-r--r--[l---------] | circuitpython/frozen/circuitpython-stage/feather_m4_minitft_featherwing/stage.py | 631 | ||||
-rw-r--r--[l---------] | circuitpython/frozen/circuitpython-stage/itsybitsy_m4_express/stage.py | 631 | ||||
-rw-r--r--[l---------] | circuitpython/frozen/circuitpython-stage/meowbit/stage.py | 631 | ||||
-rw-r--r--[l---------] | circuitpython/frozen/circuitpython-stage/pewpew_m4/stage.py | 631 | ||||
-rw-r--r--[l---------] | circuitpython/frozen/circuitpython-stage/picosystem/stage.py | 631 | ||||
-rw-r--r--[l---------] | circuitpython/frozen/circuitpython-stage/pybadge/stage.py | 631 | ||||
-rw-r--r--[l---------] | circuitpython/frozen/circuitpython-stage/pygamer/stage.py | 631 | ||||
-rw-r--r--[l---------] | circuitpython/frozen/circuitpython-stage/ugame10/stage.py | 631 |
9 files changed, 5084 insertions, 9 deletions
diff --git a/circuitpython/frozen/circuitpython-stage/docs/README.rst b/circuitpython/frozen/circuitpython-stage/docs/README.rst index 89a0106..dfadce4 120000..100644 --- a/circuitpython/frozen/circuitpython-stage/docs/README.rst +++ b/circuitpython/frozen/circuitpython-stage/docs/README.rst @@ -1 +1,44 @@ -../README.rst
\ No newline at end of file +Stage – a Tile and Sprite Engine +******************************** + +Stage is a library that lets you display tile grids and sprites on SPI-based +RGB displays in CircuitPython. It is mostly made with video games in mind, but +it can be useful in making any kind of graphical user interface too. + +For performance reasons, a part of this library has been written in C and has +to be compiled as part of the CircuitPython firmware as the ``_stage`` module. +For memory saving reasons, it's best if this library is also included in the +firmware, as a frozen module. + + +API Reference +************* + +The API reference is available at `<http://circuitpython-stage.readthedocs.io>`_. + +stage +===== +.. automodule:: stage + :members: + + +ugame +======= +.. module:: ugame + +.. data:: display + + An initialized display, that can be used for creating Stage objects. + +.. data:: buttons + + An instance of ``GamePad`` or other similar class, that has a + ``get_pressed`` method for retrieving a bit mask of pressed buttons. That + value can be then checked with & operator against the constants: ``K_UP``, + ``K_DOWN``, ``K_LEFT``, ``K_RIGHT``, ``K_X``, ``K_O`` and on some platforms + also: ``K_START`` and ``K_SELECT``. + +.. data:: audio + + And instance of the ``Audio`` or other similar class, that has ``play``, + ``stop`` and ``mute`` methods. diff --git a/circuitpython/frozen/circuitpython-stage/feather_m4_minitft_featherwing/stage.py b/circuitpython/frozen/circuitpython-stage/feather_m4_minitft_featherwing/stage.py index 2dedc93..44ab69a 120000..100644 --- a/circuitpython/frozen/circuitpython-stage/feather_m4_minitft_featherwing/stage.py +++ b/circuitpython/frozen/circuitpython-stage/feather_m4_minitft_featherwing/stage.py @@ -1 +1,630 @@ -../stage.py
\ No newline at end of file +import time +import array +import digitalio +import struct + +import _stage + + +FONT = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' + b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' + b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' + b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' + b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' + b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' + b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' + b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' + b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' + b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@U\xd0\xff' + b'\xf4\xaa\xbdV\xad\x01m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00' + b'm\x00m\x00m\x00m\x00m\x00\xbd\x01\xf9V\xe4\xff\x90\xaa@UUU\xff\xff' + b'\xaa\xaaUU\x00\x00\x00\x00\x00\x00\x00\x00U\x01\xff\x06' + b'\xea\x1b\x95o@n\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m' + b'\x00m\x00m\x00m\x00m\x00m@o\xd5k\xff\x1a\xaa\x06U\x01' + b'\x00\x00\x00\x00\x00\x00\x00\x00UU\xff\xff\xaa\xaaUU' + b'\x00\x00\x00\x00\x00UE\xfe\xd9\xef\xdd\x9f\xad\x9f\xad\x9a' + b'\x00\x00\x00\x00\x00\x00U\x15\xf7o\xa7jW\x15v\x00\xadu\xed\xda' + b'\xddv\x99\xe6E\x9a\x00U\x00\x00\x00\x00m\x00W\x00n\x00\x15\x00' + b'\x1b\x00\x05\x00\x00\x00\x00\x00\xaa\x00\xaa\x00\xaa\x00\xaa\x00' + b'\x00\xaa\x00\xaa\x00\xaa\x00\xaaP\x05\x94\x16\xa4\x1b\xe4\x1b' + b'\xe4\x1a\xa4\x1aT\x15\x00\x00P\x00\xd0\x01\xd0\x07\xd4\x19' + b'\xf9\x1d\xbd\x05T\x00\x00\x00T\x05\xf5\x17\xdd\x1d\xdd\x1d' + b'\xf5\x17\xe4\x06T\x05\x00\x00\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16' + b'\x14\x05\x00\x00T\x15\xf5\x1f\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1d' + b'P\x15\x00\x00\x00\x00P\x01\xe4\x06\xf4\x07\xe4\x06P\x01' + b'\x00\x00\x00\x00U\x15\xdd\x1d\xdd\x1d\x99\x19U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01\xd0\x01\x90\x01P\x01\xd0\x01' + b'P\x01\x00\x00T\x05t\x07d\x06T\x05\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x14\x05u\x17\xed\x1et\x07\xed\x1eu\x17\x14\x05\x00\x00' + b'T\x15\xf5\x1b\x99\x05\xf5\x17\x94\x19\xf9\x17U\x05\x00\x00' + b'\x15\x14\x1d\x1dU\x07\xd0\x01t\x15\x1d\x1d\x05\x15\x00\x00' + b'T\x01\xe4\x05u\x07\xdd\x01]\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01' + b'\x90\x01P\x01\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06' + b'\x90\x01\xd0\x01\x90\x01P\x06@\x05\x00\x00T\x00d\x01' + b'\x90\x01\xd0\x01\x90\x01d\x01T\x00\x00\x00\x00\x00\x14\x05' + b't\x07\xd0\x01t\x07\x14\x05\x00\x00\x00\x00P\x01\x90\x01' + b'\xd5\x15\xf9\x1b\xd5\x15\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'\x00\x00P\x01\xd0\x01\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01P\x01\x00\x00\x00\x04\x00\x1d' + b'@\x07\xd0\x01t\x00\x1d\x00\x04\x00\x00\x00T\x05\xe5\x16' + b'Y\x1a\xdd\x1di\x19\xe5\x16T\x05\x00\x00@\x01\xd0\x01' + b'\xe4\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00T\x05\xf9\x17' + b'U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00T\x05\xf5\x17]\x1d\x94\x07' + b']\x1d\xf5\x17T\x05\x00\x00P\x00t\x00]\x05]\x17\xfd\x1fU\x17' + b'@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1bU\x1d\xf9\x1bU\x05\x00\x00' + b'T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1bT\x05\x00\x00U\x15\xfd\x1f' + b'U\x19\xd0\x06d\x01t\x00T\x00\x00\x00T\x05\xf5\x17]\x1d\xf5\x17' + b']\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b]\x1d\xf9\x1fT\x1d\xf9\x17' + b'U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01P\x01' + b'\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01\x90\x01' + b'P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00\xd0\x01@\x07' + b'\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05\xf9\x1bU\x15' + b'\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07\xd0\x01t\x00' + b'\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16P\x05\xd0\x01' + b'P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1bY\x15\xf5\x06' + b'T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d\x15\x15\x00\x00' + b'U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17U\x05\x00\x00T\x05\xf5\x06' + b']\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00U\x01\xbd\x05]\x17\x1d\x1d' + b']\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06]\x01\xfd\x01]\x15\xfd\x1b' + b'U\x15\x00\x00U\x15\xfd\x1b]\x15]\x00\xbd\x01]\x01\x15\x00\x00\x00' + b'T\x15\xf5\x1b]\x05\xdd\x1fY\x1d\xf5\x1bT\x15\x00\x00' + b'\x15\x15\x1d\x1d]\x1d\xfd\x1f]\x1d\x1d\x1d\x15\x15\x00\x00' + b'T\x05\xe4\x06\xd0\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00' + b'\x00\x15\x00\x1d\x00\x1d\x05\x1d]\x19\xf5\x17T\x05\x00\x00' + b'\x15\x14\x1d\x1d]\x07\xfd\x01]\x07\x1d\x1d\x15\x14\x00\x00' + b'\x15\x00\x1d\x00\x1d\x00\x1d\x00]\x15\xfd\x1fU\x15\x00\x00' + b'\x05\x14\x1d\x1dm\x1e\xdd\x1d]\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x05\x15\x1d\x1dm\x1d\xdd\x1d]\x1e\x1d\x1d\x15\x14\x00\x00' + b'T\x01\xb5\x05]\x17\x1d\x1d]\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16' + b']\x19]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' + b']\x1e\xe5\x07T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d' + b'\x15\x15\x00\x00T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17' + b'U\x05\x00\x00U\x15\xf9\x1b\xd5\x15\xd0\x01\xd0\x01\xd0\x01' + b'P\x01\x00\x00\x15\x15\x1d\x1d\x1d\x1d\x19\x1du\x19\xd4\x17' + b'P\x05\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17d\x06\xd0\x01' + b'@\x00\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xd9\x19u\x17' + b'\x14\x05\x00\x00\x05\x14\x1d\x1dt\x07\xd0\x01t\x07\x1d\x1d' + b'\x05\x14\x00\x00\x15\x15\x1d\x1d\x19\x19u\x17\x94\x05\xd0\x01' + b'P\x01\x00\x00U\x15\xf9\x1bU\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00' + b'T\x05\xf4\x06t\x01t\x00t\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00' + b't\x00\xd0\x01@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07' + b'P\x07\xe4\x07T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19' + b'\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' + b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' + b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' + b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' + b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' + b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' + b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' + b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' + b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' + b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' + b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' + b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' + b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' + b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' + b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' + b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' + b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' + b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' + b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' + b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' + b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' + b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' + b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' + b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' + b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' + b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' + b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') + +PALETTE = (b'\xf8\x1f\x00\x00\xcey\xff\xff\xf8\x1f\x00\x19\xfc\xe0\xfd\xe0' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + +def color565(r, g, b): + """Convert 24-bit RGB color to 16-bit.""" + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): + """Return True if the two rectangles intersect.""" + if bx1 is None: + bx1 = bx0 + if by1 is None: + by1 = by0 + return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1) + + +class BMP16: + """Read 16-color BMP files.""" + + def __init__(self, filename): + self.filename = filename + self.colors = 0 + + def read_header(self): + """Read the file's header information.""" + + if self.colors: + return + with open(self.filename, 'rb') as f: + f.seek(10) + self.data = int.from_bytes(f.read(4), 'little') + f.seek(18) + self.width = int.from_bytes(f.read(4), 'little') + self.height = int.from_bytes(f.read(4), 'little') + f.seek(46) + self.colors = int.from_bytes(f.read(4), 'little') + + def read_palette(self): + """Read the color palette information.""" + + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(self.data - self.colors * 4) + for color in range(self.colors): + buffer = f.read(4) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + """Read the image data.""" + line_size = self.width >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + + with open(self.filename, 'rb') as f: + f.seek(self.data) + index = (self.height - 1) * line_size + for line in range(self.height): + chunk = f.read(line_size) + buffer[index:index + line_size] = chunk + index -= line_size + return buffer + + +def read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + dictionary = LZWDict(code_size) + bit = 0 + try: + byte = next(data) + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) + except StopIteration: + return + + +class GIF16: + """Read 16-color GIF files.""" + + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + header = f.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not GIF file") + self.width, self.height, flags, self.background, self.aspect = ( + struct.unpack('<HHBBB', f.read(7))) + self.palette_size = 1 << ((flags & 0x07) + 1) + if not flags & 0x80: + raise NotImplementedError() + if self.palette_size > 16: + raise ValueError("Too many colors (%d/16)." % self.palette_size) + + def read_palette(self): + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(13) + for color in range(self.palette_size): + buffer = f.read(3) + c = color565(buffer[0], buffer[1], buffer[2]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + line_size = (self.width + 1) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + with open(self.filename, 'rb') as f: + f.seek(13 + self.palette_size * 3) + while True: # skip to first frame + block_type = f.read(1)[0] + if block_type == 0x2c: + break + elif block_type == 0x21: # skip extension + extension_type = f.read(1)[0] + while True: + size = f.read(1)[0] + if size == 0: + break + f.seek(1, size) + elif block_type == 0x3b: + raise NotImplementedError() + x, y, w, h, flags = struct.unpack('<HHHHB', f.read(9)) + if (flags & 0x80 or flags & 0x40 or + w != self.width or h != self.height or x != 0 or y != 0): + raise NotImplementedError() + min_code_size = f.read(1)[0] + x = 0 + y = 0 + for decoded in lzw_decode(read_blockstream(f), min_code_size): + for pixel in decoded: + if x & 0x01: + buffer[(x >> 1) + y * line_size] |= pixel + else: + buffer[(x >> 1) + y * line_size] = pixel << 4 + x += 1 + if (x >= self.width): + x = 0 + y += 1 + return buffer + + +class Bank: + """ + Store graphics for the tiles and sprites. + + A single bank stores exactly 16 tiles, each 16x16 pixels in 16 possible + colors, and a 16-color palette. We just like the number 16. + + """ + + def __init__(self, buffer=None, palette=None): + self.buffer = buffer + self.palette = palette + + @classmethod + def from_bmp16(cls, filename): + """Read the bank from a BMP file.""" + return cls.from_image(filename) + + + @classmethod + def from_image(cls, filename): + """Read the bank from an image file.""" + if filename.lower().endswith(".gif"): + image = GIF16(filename) + elif filename.lower().endswith(".bmp"): + image = BMP16(filename) + else: + raise ValueError("Unsupported format") + image.read_header() + if image.width != 16 or image.height != 256: + raise ValueError("Image size not 16x256") + palette = image.read_palette() + buffer = image.read_data() + return cls(buffer, palette) + + +class Grid: + """ + A grid is a layer of tiles that can be displayed on the screen. Each square + can contain any of the 16 tiles from the associated bank. + """ + + def __init__(self, bank, width=8, height=8, palette=None, buffer=None): + self.x = 0 + self.y = 0 + self.z = 0 + self.stride = (width + 1) & 0xfe + self.width = width + self.height = height + self.bank = bank + self.palette = palette or bank.palette + self.buffer = buffer or bytearray((self.stride * height)>>1) + self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, + self.palette, self.buffer) + + def tile(self, x, y, tile=None): + """Get or set what tile is displayed in the given place.""" + + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + index = (y * self.stride + x) >> 1 + b = self.buffer[index] + if tile is None: + return b & 0x0f if x & 0x01 else b >> 4 + if x & 0x01: + b = b & 0xf0 | tile + else: + b = b & 0x0f | (tile << 4) + self.buffer[index] = b + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + +class WallGrid(Grid): + """ + A special grid, shifted from its parents by half a tile, useful for making + nice-looking corners of walls and similar structures. + """ + + def __init__(self, grid, walls, bank, palette=None): + super().__init__(bank, grid.width + 1, grid.height + 1, palette) + self.grid = grid + self.walls = walls + self.update() + self.move(self.x - 8, self.y - 8) + + def update(self): + for y in range(self.height): + for x in range(self.width): + t = 0 + bit = 1 + for dy in (-1, 0): + for dx in (-1, 0): + if self.grid.tile(x + dx, y + dy) in self.walls: + t |= bit + bit <<= 1 + self.tile(x, y, t) + + +class Sprite: + """ + A sprite is a layer containing just a single tile from the associated bank, + that can be positioned anywhere on the screen. + """ + + def __init__(self, bank, frame, x, y, z=0, rotation=0, palette=None): + self.bank = bank + self.palette = palette or bank.palette + self.frame = frame + self.rotation = rotation + self.x = x + self.y = y + self.z = z + self.layer = _stage.Layer(1, 1, self.bank.buffer, self.palette) + self.layer.move(x, y) + self.layer.frame(frame, rotation) + self.px = x + self.py = y + + def move(self, x, y, z=None): + """Move the sprite to the given place.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def set_frame(self, frame=None, rotation=None): + """ + Set the current graphic and rotation of the sprite. + + The possible values for rotation are: 0 - none, 1 - 90 degrees + clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - + mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and + mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ + + if frame is not None: + self.frame = frame + if rotation is not None: + self.rotation = rotation + self.layer.frame(self.frame, self.rotation) + + def update(self): + pass + + +class Text: + """Text layer. For displaying text.""" + + def __init__(self, width, height, font=None, palette=None, buffer=None): + self.width = width + self.height = height + self.font = font or FONT + self.palette = palette or PALETTE + self.buffer = buffer or bytearray(width * height) + self.layer = _stage.Text(width, height, self.font, + self.palette, self.buffer) + self.column = 0 + self.row = 0 + self.x = 0 + self.y = 0 + self.z = 0 + + def char(self, x, y, c=None, hightlight=False): + """Get or set the character at the given location.""" + if not 0 <= x < self.width or not 0 <= y < self.height: + return + if c is None: + return chr(self.buffer[y * self.width + x]) + c = ord(c) + if hightlight: + c |= 0x80 + self.buffer[y * self.width + x] = c + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def cursor(self, x=None, y=None): + """Move the text cursor to the specified row and column.""" + if y is not None: + self.row = min(max(0, y), self.width - 1) + if x is not None: + self.column = min(max(0, x), self.height - 1) + + def text(self, text, hightlight=False): + """ + Display text starting at the current cursor location. + Return the dimensions of the rendered text. + """ + longest = 0 + tallest = 0 + for c in text: + if c != '\n': + self.char(self.column, self.row, c, hightlight) + self.column += 1 + if self.column >= self.width or c == '\n': + longest = max(longest, self.column) + self.column = 0 + self.row += 1 + if self.row >= self.height: + tallest = max(tallest, self.row) + self.row = 0 + longest = max(longest, self.column) + tallest = max(tallest, self.row) + (1 if self.column > 0 else 0) + return longest * 8, tallest * 8 + + def clear(self): + """Clear all text from the layer.""" + for i in range(self.width * self.height): + self.buffer[i] = 0 + + +class Stage: + """ + Represents what is being displayed on the screen. + + The ``display`` parameter is displayio.Display representing an initialized + display connected to the device. + + The ``fps`` specifies the maximum frame rate to be enforced. + + The ``scale`` specifies an optional scaling up of the display, to use + 2x2 or 3x3, etc. pixels. If not specified, it is inferred from the display + size (displays wider than 256 pixels will have scale=2, for example). + """ + buffer = bytearray(512) + + def __init__(self, display, fps=6, scale=None): + if scale is None: + self.scale = max(1, display.width // 128) + else: + self.scale = scale + self.layers = [] + self.display = display + self.width = display.width // self.scale + self.height = display.height // self.scale + self.last_tick = time.monotonic() + self.tick_delay = 1 / fps + self.vx = 0 + self.vy = 0 + + def tick(self): + """Wait for the start of the next frame.""" + self.last_tick += self.tick_delay + wait = max(0, self.last_tick - time.monotonic()) + if wait: + time.sleep(wait) + else: + self.last_tick = time.monotonic() + + def render_block(self, x0=None, y0=None, x1=None, y1=None): + """Update a rectangle of the screen.""" + if x0 is None: + x0 = self.vx + if y0 is None: + y0 = self.vy + if x1 is None: + x1 = self.width + self.vx + if y1 is None: + y1 = self.height + self.vy + x0 = min(max(0, x0 - self.vx), self.width - 1) + y0 = min(max(0, y0 - self.vy), self.height - 1) + x1 = min(max(1, x1 - self.vx), self.width) + y1 = min(max(1, y1 - self.vy), self.height) + if x0 >= x1 or y0 >= y1: + return + layers = [l.layer for l in self.layers] + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) + + def render_sprites(self, sprites): + """Update the spots taken by all the sprites in the list.""" + layers = [l.layer for l in self.layers] + for sprite in sprites: + x = int(sprite.x) - self.vx + y = int(sprite.y) - self.vy + x0 = max(0, min(self.width - 1, min(sprite.px, x))) + y0 = max(0, min(self.height - 1, min(sprite.py, y))) + x1 = max(1, min(self.width, max(sprite.px, x) + 16)) + y1 = max(1, min(self.height, max(sprite.py, y) + 16)) + sprite.px = x + sprite.py = y + if x0 >= x1 or y0 >= y1: + continue + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) diff --git a/circuitpython/frozen/circuitpython-stage/itsybitsy_m4_express/stage.py b/circuitpython/frozen/circuitpython-stage/itsybitsy_m4_express/stage.py index 2dedc93..44ab69a 120000..100644 --- a/circuitpython/frozen/circuitpython-stage/itsybitsy_m4_express/stage.py +++ b/circuitpython/frozen/circuitpython-stage/itsybitsy_m4_express/stage.py @@ -1 +1,630 @@ -../stage.py
\ No newline at end of file +import time +import array +import digitalio +import struct + +import _stage + + +FONT = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' + b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' + b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' + b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' + b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' + b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' + b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' + b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' + b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' + b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@U\xd0\xff' + b'\xf4\xaa\xbdV\xad\x01m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00' + b'm\x00m\x00m\x00m\x00m\x00\xbd\x01\xf9V\xe4\xff\x90\xaa@UUU\xff\xff' + b'\xaa\xaaUU\x00\x00\x00\x00\x00\x00\x00\x00U\x01\xff\x06' + b'\xea\x1b\x95o@n\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m' + b'\x00m\x00m\x00m\x00m\x00m@o\xd5k\xff\x1a\xaa\x06U\x01' + b'\x00\x00\x00\x00\x00\x00\x00\x00UU\xff\xff\xaa\xaaUU' + b'\x00\x00\x00\x00\x00UE\xfe\xd9\xef\xdd\x9f\xad\x9f\xad\x9a' + b'\x00\x00\x00\x00\x00\x00U\x15\xf7o\xa7jW\x15v\x00\xadu\xed\xda' + b'\xddv\x99\xe6E\x9a\x00U\x00\x00\x00\x00m\x00W\x00n\x00\x15\x00' + b'\x1b\x00\x05\x00\x00\x00\x00\x00\xaa\x00\xaa\x00\xaa\x00\xaa\x00' + b'\x00\xaa\x00\xaa\x00\xaa\x00\xaaP\x05\x94\x16\xa4\x1b\xe4\x1b' + b'\xe4\x1a\xa4\x1aT\x15\x00\x00P\x00\xd0\x01\xd0\x07\xd4\x19' + b'\xf9\x1d\xbd\x05T\x00\x00\x00T\x05\xf5\x17\xdd\x1d\xdd\x1d' + b'\xf5\x17\xe4\x06T\x05\x00\x00\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16' + b'\x14\x05\x00\x00T\x15\xf5\x1f\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1d' + b'P\x15\x00\x00\x00\x00P\x01\xe4\x06\xf4\x07\xe4\x06P\x01' + b'\x00\x00\x00\x00U\x15\xdd\x1d\xdd\x1d\x99\x19U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01\xd0\x01\x90\x01P\x01\xd0\x01' + b'P\x01\x00\x00T\x05t\x07d\x06T\x05\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x14\x05u\x17\xed\x1et\x07\xed\x1eu\x17\x14\x05\x00\x00' + b'T\x15\xf5\x1b\x99\x05\xf5\x17\x94\x19\xf9\x17U\x05\x00\x00' + b'\x15\x14\x1d\x1dU\x07\xd0\x01t\x15\x1d\x1d\x05\x15\x00\x00' + b'T\x01\xe4\x05u\x07\xdd\x01]\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01' + b'\x90\x01P\x01\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06' + b'\x90\x01\xd0\x01\x90\x01P\x06@\x05\x00\x00T\x00d\x01' + b'\x90\x01\xd0\x01\x90\x01d\x01T\x00\x00\x00\x00\x00\x14\x05' + b't\x07\xd0\x01t\x07\x14\x05\x00\x00\x00\x00P\x01\x90\x01' + b'\xd5\x15\xf9\x1b\xd5\x15\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'\x00\x00P\x01\xd0\x01\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01P\x01\x00\x00\x00\x04\x00\x1d' + b'@\x07\xd0\x01t\x00\x1d\x00\x04\x00\x00\x00T\x05\xe5\x16' + b'Y\x1a\xdd\x1di\x19\xe5\x16T\x05\x00\x00@\x01\xd0\x01' + b'\xe4\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00T\x05\xf9\x17' + b'U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00T\x05\xf5\x17]\x1d\x94\x07' + b']\x1d\xf5\x17T\x05\x00\x00P\x00t\x00]\x05]\x17\xfd\x1fU\x17' + b'@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1bU\x1d\xf9\x1bU\x05\x00\x00' + b'T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1bT\x05\x00\x00U\x15\xfd\x1f' + b'U\x19\xd0\x06d\x01t\x00T\x00\x00\x00T\x05\xf5\x17]\x1d\xf5\x17' + b']\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b]\x1d\xf9\x1fT\x1d\xf9\x17' + b'U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01P\x01' + b'\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01\x90\x01' + b'P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00\xd0\x01@\x07' + b'\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05\xf9\x1bU\x15' + b'\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07\xd0\x01t\x00' + b'\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16P\x05\xd0\x01' + b'P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1bY\x15\xf5\x06' + b'T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d\x15\x15\x00\x00' + b'U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17U\x05\x00\x00T\x05\xf5\x06' + b']\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00U\x01\xbd\x05]\x17\x1d\x1d' + b']\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06]\x01\xfd\x01]\x15\xfd\x1b' + b'U\x15\x00\x00U\x15\xfd\x1b]\x15]\x00\xbd\x01]\x01\x15\x00\x00\x00' + b'T\x15\xf5\x1b]\x05\xdd\x1fY\x1d\xf5\x1bT\x15\x00\x00' + b'\x15\x15\x1d\x1d]\x1d\xfd\x1f]\x1d\x1d\x1d\x15\x15\x00\x00' + b'T\x05\xe4\x06\xd0\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00' + b'\x00\x15\x00\x1d\x00\x1d\x05\x1d]\x19\xf5\x17T\x05\x00\x00' + b'\x15\x14\x1d\x1d]\x07\xfd\x01]\x07\x1d\x1d\x15\x14\x00\x00' + b'\x15\x00\x1d\x00\x1d\x00\x1d\x00]\x15\xfd\x1fU\x15\x00\x00' + b'\x05\x14\x1d\x1dm\x1e\xdd\x1d]\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x05\x15\x1d\x1dm\x1d\xdd\x1d]\x1e\x1d\x1d\x15\x14\x00\x00' + b'T\x01\xb5\x05]\x17\x1d\x1d]\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16' + b']\x19]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' + b']\x1e\xe5\x07T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d' + b'\x15\x15\x00\x00T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17' + b'U\x05\x00\x00U\x15\xf9\x1b\xd5\x15\xd0\x01\xd0\x01\xd0\x01' + b'P\x01\x00\x00\x15\x15\x1d\x1d\x1d\x1d\x19\x1du\x19\xd4\x17' + b'P\x05\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17d\x06\xd0\x01' + b'@\x00\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xd9\x19u\x17' + b'\x14\x05\x00\x00\x05\x14\x1d\x1dt\x07\xd0\x01t\x07\x1d\x1d' + b'\x05\x14\x00\x00\x15\x15\x1d\x1d\x19\x19u\x17\x94\x05\xd0\x01' + b'P\x01\x00\x00U\x15\xf9\x1bU\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00' + b'T\x05\xf4\x06t\x01t\x00t\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00' + b't\x00\xd0\x01@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07' + b'P\x07\xe4\x07T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19' + b'\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' + b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' + b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' + b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' + b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' + b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' + b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' + b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' + b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' + b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' + b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' + b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' + b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' + b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' + b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' + b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' + b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' + b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' + b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' + b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' + b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' + b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' + b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' + b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' + b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' + b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' + b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') + +PALETTE = (b'\xf8\x1f\x00\x00\xcey\xff\xff\xf8\x1f\x00\x19\xfc\xe0\xfd\xe0' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + +def color565(r, g, b): + """Convert 24-bit RGB color to 16-bit.""" + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): + """Return True if the two rectangles intersect.""" + if bx1 is None: + bx1 = bx0 + if by1 is None: + by1 = by0 + return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1) + + +class BMP16: + """Read 16-color BMP files.""" + + def __init__(self, filename): + self.filename = filename + self.colors = 0 + + def read_header(self): + """Read the file's header information.""" + + if self.colors: + return + with open(self.filename, 'rb') as f: + f.seek(10) + self.data = int.from_bytes(f.read(4), 'little') + f.seek(18) + self.width = int.from_bytes(f.read(4), 'little') + self.height = int.from_bytes(f.read(4), 'little') + f.seek(46) + self.colors = int.from_bytes(f.read(4), 'little') + + def read_palette(self): + """Read the color palette information.""" + + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(self.data - self.colors * 4) + for color in range(self.colors): + buffer = f.read(4) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + """Read the image data.""" + line_size = self.width >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + + with open(self.filename, 'rb') as f: + f.seek(self.data) + index = (self.height - 1) * line_size + for line in range(self.height): + chunk = f.read(line_size) + buffer[index:index + line_size] = chunk + index -= line_size + return buffer + + +def read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + dictionary = LZWDict(code_size) + bit = 0 + try: + byte = next(data) + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) + except StopIteration: + return + + +class GIF16: + """Read 16-color GIF files.""" + + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + header = f.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not GIF file") + self.width, self.height, flags, self.background, self.aspect = ( + struct.unpack('<HHBBB', f.read(7))) + self.palette_size = 1 << ((flags & 0x07) + 1) + if not flags & 0x80: + raise NotImplementedError() + if self.palette_size > 16: + raise ValueError("Too many colors (%d/16)." % self.palette_size) + + def read_palette(self): + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(13) + for color in range(self.palette_size): + buffer = f.read(3) + c = color565(buffer[0], buffer[1], buffer[2]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + line_size = (self.width + 1) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + with open(self.filename, 'rb') as f: + f.seek(13 + self.palette_size * 3) + while True: # skip to first frame + block_type = f.read(1)[0] + if block_type == 0x2c: + break + elif block_type == 0x21: # skip extension + extension_type = f.read(1)[0] + while True: + size = f.read(1)[0] + if size == 0: + break + f.seek(1, size) + elif block_type == 0x3b: + raise NotImplementedError() + x, y, w, h, flags = struct.unpack('<HHHHB', f.read(9)) + if (flags & 0x80 or flags & 0x40 or + w != self.width or h != self.height or x != 0 or y != 0): + raise NotImplementedError() + min_code_size = f.read(1)[0] + x = 0 + y = 0 + for decoded in lzw_decode(read_blockstream(f), min_code_size): + for pixel in decoded: + if x & 0x01: + buffer[(x >> 1) + y * line_size] |= pixel + else: + buffer[(x >> 1) + y * line_size] = pixel << 4 + x += 1 + if (x >= self.width): + x = 0 + y += 1 + return buffer + + +class Bank: + """ + Store graphics for the tiles and sprites. + + A single bank stores exactly 16 tiles, each 16x16 pixels in 16 possible + colors, and a 16-color palette. We just like the number 16. + + """ + + def __init__(self, buffer=None, palette=None): + self.buffer = buffer + self.palette = palette + + @classmethod + def from_bmp16(cls, filename): + """Read the bank from a BMP file.""" + return cls.from_image(filename) + + + @classmethod + def from_image(cls, filename): + """Read the bank from an image file.""" + if filename.lower().endswith(".gif"): + image = GIF16(filename) + elif filename.lower().endswith(".bmp"): + image = BMP16(filename) + else: + raise ValueError("Unsupported format") + image.read_header() + if image.width != 16 or image.height != 256: + raise ValueError("Image size not 16x256") + palette = image.read_palette() + buffer = image.read_data() + return cls(buffer, palette) + + +class Grid: + """ + A grid is a layer of tiles that can be displayed on the screen. Each square + can contain any of the 16 tiles from the associated bank. + """ + + def __init__(self, bank, width=8, height=8, palette=None, buffer=None): + self.x = 0 + self.y = 0 + self.z = 0 + self.stride = (width + 1) & 0xfe + self.width = width + self.height = height + self.bank = bank + self.palette = palette or bank.palette + self.buffer = buffer or bytearray((self.stride * height)>>1) + self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, + self.palette, self.buffer) + + def tile(self, x, y, tile=None): + """Get or set what tile is displayed in the given place.""" + + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + index = (y * self.stride + x) >> 1 + b = self.buffer[index] + if tile is None: + return b & 0x0f if x & 0x01 else b >> 4 + if x & 0x01: + b = b & 0xf0 | tile + else: + b = b & 0x0f | (tile << 4) + self.buffer[index] = b + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + +class WallGrid(Grid): + """ + A special grid, shifted from its parents by half a tile, useful for making + nice-looking corners of walls and similar structures. + """ + + def __init__(self, grid, walls, bank, palette=None): + super().__init__(bank, grid.width + 1, grid.height + 1, palette) + self.grid = grid + self.walls = walls + self.update() + self.move(self.x - 8, self.y - 8) + + def update(self): + for y in range(self.height): + for x in range(self.width): + t = 0 + bit = 1 + for dy in (-1, 0): + for dx in (-1, 0): + if self.grid.tile(x + dx, y + dy) in self.walls: + t |= bit + bit <<= 1 + self.tile(x, y, t) + + +class Sprite: + """ + A sprite is a layer containing just a single tile from the associated bank, + that can be positioned anywhere on the screen. + """ + + def __init__(self, bank, frame, x, y, z=0, rotation=0, palette=None): + self.bank = bank + self.palette = palette or bank.palette + self.frame = frame + self.rotation = rotation + self.x = x + self.y = y + self.z = z + self.layer = _stage.Layer(1, 1, self.bank.buffer, self.palette) + self.layer.move(x, y) + self.layer.frame(frame, rotation) + self.px = x + self.py = y + + def move(self, x, y, z=None): + """Move the sprite to the given place.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def set_frame(self, frame=None, rotation=None): + """ + Set the current graphic and rotation of the sprite. + + The possible values for rotation are: 0 - none, 1 - 90 degrees + clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - + mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and + mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ + + if frame is not None: + self.frame = frame + if rotation is not None: + self.rotation = rotation + self.layer.frame(self.frame, self.rotation) + + def update(self): + pass + + +class Text: + """Text layer. For displaying text.""" + + def __init__(self, width, height, font=None, palette=None, buffer=None): + self.width = width + self.height = height + self.font = font or FONT + self.palette = palette or PALETTE + self.buffer = buffer or bytearray(width * height) + self.layer = _stage.Text(width, height, self.font, + self.palette, self.buffer) + self.column = 0 + self.row = 0 + self.x = 0 + self.y = 0 + self.z = 0 + + def char(self, x, y, c=None, hightlight=False): + """Get or set the character at the given location.""" + if not 0 <= x < self.width or not 0 <= y < self.height: + return + if c is None: + return chr(self.buffer[y * self.width + x]) + c = ord(c) + if hightlight: + c |= 0x80 + self.buffer[y * self.width + x] = c + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def cursor(self, x=None, y=None): + """Move the text cursor to the specified row and column.""" + if y is not None: + self.row = min(max(0, y), self.width - 1) + if x is not None: + self.column = min(max(0, x), self.height - 1) + + def text(self, text, hightlight=False): + """ + Display text starting at the current cursor location. + Return the dimensions of the rendered text. + """ + longest = 0 + tallest = 0 + for c in text: + if c != '\n': + self.char(self.column, self.row, c, hightlight) + self.column += 1 + if self.column >= self.width or c == '\n': + longest = max(longest, self.column) + self.column = 0 + self.row += 1 + if self.row >= self.height: + tallest = max(tallest, self.row) + self.row = 0 + longest = max(longest, self.column) + tallest = max(tallest, self.row) + (1 if self.column > 0 else 0) + return longest * 8, tallest * 8 + + def clear(self): + """Clear all text from the layer.""" + for i in range(self.width * self.height): + self.buffer[i] = 0 + + +class Stage: + """ + Represents what is being displayed on the screen. + + The ``display`` parameter is displayio.Display representing an initialized + display connected to the device. + + The ``fps`` specifies the maximum frame rate to be enforced. + + The ``scale`` specifies an optional scaling up of the display, to use + 2x2 or 3x3, etc. pixels. If not specified, it is inferred from the display + size (displays wider than 256 pixels will have scale=2, for example). + """ + buffer = bytearray(512) + + def __init__(self, display, fps=6, scale=None): + if scale is None: + self.scale = max(1, display.width // 128) + else: + self.scale = scale + self.layers = [] + self.display = display + self.width = display.width // self.scale + self.height = display.height // self.scale + self.last_tick = time.monotonic() + self.tick_delay = 1 / fps + self.vx = 0 + self.vy = 0 + + def tick(self): + """Wait for the start of the next frame.""" + self.last_tick += self.tick_delay + wait = max(0, self.last_tick - time.monotonic()) + if wait: + time.sleep(wait) + else: + self.last_tick = time.monotonic() + + def render_block(self, x0=None, y0=None, x1=None, y1=None): + """Update a rectangle of the screen.""" + if x0 is None: + x0 = self.vx + if y0 is None: + y0 = self.vy + if x1 is None: + x1 = self.width + self.vx + if y1 is None: + y1 = self.height + self.vy + x0 = min(max(0, x0 - self.vx), self.width - 1) + y0 = min(max(0, y0 - self.vy), self.height - 1) + x1 = min(max(1, x1 - self.vx), self.width) + y1 = min(max(1, y1 - self.vy), self.height) + if x0 >= x1 or y0 >= y1: + return + layers = [l.layer for l in self.layers] + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) + + def render_sprites(self, sprites): + """Update the spots taken by all the sprites in the list.""" + layers = [l.layer for l in self.layers] + for sprite in sprites: + x = int(sprite.x) - self.vx + y = int(sprite.y) - self.vy + x0 = max(0, min(self.width - 1, min(sprite.px, x))) + y0 = max(0, min(self.height - 1, min(sprite.py, y))) + x1 = max(1, min(self.width, max(sprite.px, x) + 16)) + y1 = max(1, min(self.height, max(sprite.py, y) + 16)) + sprite.px = x + sprite.py = y + if x0 >= x1 or y0 >= y1: + continue + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) diff --git a/circuitpython/frozen/circuitpython-stage/meowbit/stage.py b/circuitpython/frozen/circuitpython-stage/meowbit/stage.py index 2dedc93..44ab69a 120000..100644 --- a/circuitpython/frozen/circuitpython-stage/meowbit/stage.py +++ b/circuitpython/frozen/circuitpython-stage/meowbit/stage.py @@ -1 +1,630 @@ -../stage.py
\ No newline at end of file +import time +import array +import digitalio +import struct + +import _stage + + +FONT = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' + b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' + b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' + b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' + b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' + b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' + b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' + b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' + b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' + b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@U\xd0\xff' + b'\xf4\xaa\xbdV\xad\x01m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00' + b'm\x00m\x00m\x00m\x00m\x00\xbd\x01\xf9V\xe4\xff\x90\xaa@UUU\xff\xff' + b'\xaa\xaaUU\x00\x00\x00\x00\x00\x00\x00\x00U\x01\xff\x06' + b'\xea\x1b\x95o@n\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m' + b'\x00m\x00m\x00m\x00m\x00m@o\xd5k\xff\x1a\xaa\x06U\x01' + b'\x00\x00\x00\x00\x00\x00\x00\x00UU\xff\xff\xaa\xaaUU' + b'\x00\x00\x00\x00\x00UE\xfe\xd9\xef\xdd\x9f\xad\x9f\xad\x9a' + b'\x00\x00\x00\x00\x00\x00U\x15\xf7o\xa7jW\x15v\x00\xadu\xed\xda' + b'\xddv\x99\xe6E\x9a\x00U\x00\x00\x00\x00m\x00W\x00n\x00\x15\x00' + b'\x1b\x00\x05\x00\x00\x00\x00\x00\xaa\x00\xaa\x00\xaa\x00\xaa\x00' + b'\x00\xaa\x00\xaa\x00\xaa\x00\xaaP\x05\x94\x16\xa4\x1b\xe4\x1b' + b'\xe4\x1a\xa4\x1aT\x15\x00\x00P\x00\xd0\x01\xd0\x07\xd4\x19' + b'\xf9\x1d\xbd\x05T\x00\x00\x00T\x05\xf5\x17\xdd\x1d\xdd\x1d' + b'\xf5\x17\xe4\x06T\x05\x00\x00\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16' + b'\x14\x05\x00\x00T\x15\xf5\x1f\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1d' + b'P\x15\x00\x00\x00\x00P\x01\xe4\x06\xf4\x07\xe4\x06P\x01' + b'\x00\x00\x00\x00U\x15\xdd\x1d\xdd\x1d\x99\x19U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01\xd0\x01\x90\x01P\x01\xd0\x01' + b'P\x01\x00\x00T\x05t\x07d\x06T\x05\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x14\x05u\x17\xed\x1et\x07\xed\x1eu\x17\x14\x05\x00\x00' + b'T\x15\xf5\x1b\x99\x05\xf5\x17\x94\x19\xf9\x17U\x05\x00\x00' + b'\x15\x14\x1d\x1dU\x07\xd0\x01t\x15\x1d\x1d\x05\x15\x00\x00' + b'T\x01\xe4\x05u\x07\xdd\x01]\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01' + b'\x90\x01P\x01\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06' + b'\x90\x01\xd0\x01\x90\x01P\x06@\x05\x00\x00T\x00d\x01' + b'\x90\x01\xd0\x01\x90\x01d\x01T\x00\x00\x00\x00\x00\x14\x05' + b't\x07\xd0\x01t\x07\x14\x05\x00\x00\x00\x00P\x01\x90\x01' + b'\xd5\x15\xf9\x1b\xd5\x15\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'\x00\x00P\x01\xd0\x01\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01P\x01\x00\x00\x00\x04\x00\x1d' + b'@\x07\xd0\x01t\x00\x1d\x00\x04\x00\x00\x00T\x05\xe5\x16' + b'Y\x1a\xdd\x1di\x19\xe5\x16T\x05\x00\x00@\x01\xd0\x01' + b'\xe4\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00T\x05\xf9\x17' + b'U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00T\x05\xf5\x17]\x1d\x94\x07' + b']\x1d\xf5\x17T\x05\x00\x00P\x00t\x00]\x05]\x17\xfd\x1fU\x17' + b'@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1bU\x1d\xf9\x1bU\x05\x00\x00' + b'T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1bT\x05\x00\x00U\x15\xfd\x1f' + b'U\x19\xd0\x06d\x01t\x00T\x00\x00\x00T\x05\xf5\x17]\x1d\xf5\x17' + b']\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b]\x1d\xf9\x1fT\x1d\xf9\x17' + b'U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01P\x01' + b'\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01\x90\x01' + b'P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00\xd0\x01@\x07' + b'\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05\xf9\x1bU\x15' + b'\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07\xd0\x01t\x00' + b'\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16P\x05\xd0\x01' + b'P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1bY\x15\xf5\x06' + b'T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d\x15\x15\x00\x00' + b'U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17U\x05\x00\x00T\x05\xf5\x06' + b']\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00U\x01\xbd\x05]\x17\x1d\x1d' + b']\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06]\x01\xfd\x01]\x15\xfd\x1b' + b'U\x15\x00\x00U\x15\xfd\x1b]\x15]\x00\xbd\x01]\x01\x15\x00\x00\x00' + b'T\x15\xf5\x1b]\x05\xdd\x1fY\x1d\xf5\x1bT\x15\x00\x00' + b'\x15\x15\x1d\x1d]\x1d\xfd\x1f]\x1d\x1d\x1d\x15\x15\x00\x00' + b'T\x05\xe4\x06\xd0\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00' + b'\x00\x15\x00\x1d\x00\x1d\x05\x1d]\x19\xf5\x17T\x05\x00\x00' + b'\x15\x14\x1d\x1d]\x07\xfd\x01]\x07\x1d\x1d\x15\x14\x00\x00' + b'\x15\x00\x1d\x00\x1d\x00\x1d\x00]\x15\xfd\x1fU\x15\x00\x00' + b'\x05\x14\x1d\x1dm\x1e\xdd\x1d]\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x05\x15\x1d\x1dm\x1d\xdd\x1d]\x1e\x1d\x1d\x15\x14\x00\x00' + b'T\x01\xb5\x05]\x17\x1d\x1d]\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16' + b']\x19]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' + b']\x1e\xe5\x07T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d' + b'\x15\x15\x00\x00T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17' + b'U\x05\x00\x00U\x15\xf9\x1b\xd5\x15\xd0\x01\xd0\x01\xd0\x01' + b'P\x01\x00\x00\x15\x15\x1d\x1d\x1d\x1d\x19\x1du\x19\xd4\x17' + b'P\x05\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17d\x06\xd0\x01' + b'@\x00\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xd9\x19u\x17' + b'\x14\x05\x00\x00\x05\x14\x1d\x1dt\x07\xd0\x01t\x07\x1d\x1d' + b'\x05\x14\x00\x00\x15\x15\x1d\x1d\x19\x19u\x17\x94\x05\xd0\x01' + b'P\x01\x00\x00U\x15\xf9\x1bU\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00' + b'T\x05\xf4\x06t\x01t\x00t\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00' + b't\x00\xd0\x01@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07' + b'P\x07\xe4\x07T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19' + b'\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' + b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' + b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' + b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' + b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' + b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' + b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' + b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' + b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' + b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' + b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' + b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' + b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' + b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' + b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' + b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' + b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' + b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' + b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' + b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' + b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' + b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' + b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' + b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' + b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' + b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' + b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') + +PALETTE = (b'\xf8\x1f\x00\x00\xcey\xff\xff\xf8\x1f\x00\x19\xfc\xe0\xfd\xe0' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + +def color565(r, g, b): + """Convert 24-bit RGB color to 16-bit.""" + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): + """Return True if the two rectangles intersect.""" + if bx1 is None: + bx1 = bx0 + if by1 is None: + by1 = by0 + return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1) + + +class BMP16: + """Read 16-color BMP files.""" + + def __init__(self, filename): + self.filename = filename + self.colors = 0 + + def read_header(self): + """Read the file's header information.""" + + if self.colors: + return + with open(self.filename, 'rb') as f: + f.seek(10) + self.data = int.from_bytes(f.read(4), 'little') + f.seek(18) + self.width = int.from_bytes(f.read(4), 'little') + self.height = int.from_bytes(f.read(4), 'little') + f.seek(46) + self.colors = int.from_bytes(f.read(4), 'little') + + def read_palette(self): + """Read the color palette information.""" + + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(self.data - self.colors * 4) + for color in range(self.colors): + buffer = f.read(4) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + """Read the image data.""" + line_size = self.width >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + + with open(self.filename, 'rb') as f: + f.seek(self.data) + index = (self.height - 1) * line_size + for line in range(self.height): + chunk = f.read(line_size) + buffer[index:index + line_size] = chunk + index -= line_size + return buffer + + +def read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + dictionary = LZWDict(code_size) + bit = 0 + try: + byte = next(data) + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) + except StopIteration: + return + + +class GIF16: + """Read 16-color GIF files.""" + + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + header = f.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not GIF file") + self.width, self.height, flags, self.background, self.aspect = ( + struct.unpack('<HHBBB', f.read(7))) + self.palette_size = 1 << ((flags & 0x07) + 1) + if not flags & 0x80: + raise NotImplementedError() + if self.palette_size > 16: + raise ValueError("Too many colors (%d/16)." % self.palette_size) + + def read_palette(self): + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(13) + for color in range(self.palette_size): + buffer = f.read(3) + c = color565(buffer[0], buffer[1], buffer[2]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + line_size = (self.width + 1) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + with open(self.filename, 'rb') as f: + f.seek(13 + self.palette_size * 3) + while True: # skip to first frame + block_type = f.read(1)[0] + if block_type == 0x2c: + break + elif block_type == 0x21: # skip extension + extension_type = f.read(1)[0] + while True: + size = f.read(1)[0] + if size == 0: + break + f.seek(1, size) + elif block_type == 0x3b: + raise NotImplementedError() + x, y, w, h, flags = struct.unpack('<HHHHB', f.read(9)) + if (flags & 0x80 or flags & 0x40 or + w != self.width or h != self.height or x != 0 or y != 0): + raise NotImplementedError() + min_code_size = f.read(1)[0] + x = 0 + y = 0 + for decoded in lzw_decode(read_blockstream(f), min_code_size): + for pixel in decoded: + if x & 0x01: + buffer[(x >> 1) + y * line_size] |= pixel + else: + buffer[(x >> 1) + y * line_size] = pixel << 4 + x += 1 + if (x >= self.width): + x = 0 + y += 1 + return buffer + + +class Bank: + """ + Store graphics for the tiles and sprites. + + A single bank stores exactly 16 tiles, each 16x16 pixels in 16 possible + colors, and a 16-color palette. We just like the number 16. + + """ + + def __init__(self, buffer=None, palette=None): + self.buffer = buffer + self.palette = palette + + @classmethod + def from_bmp16(cls, filename): + """Read the bank from a BMP file.""" + return cls.from_image(filename) + + + @classmethod + def from_image(cls, filename): + """Read the bank from an image file.""" + if filename.lower().endswith(".gif"): + image = GIF16(filename) + elif filename.lower().endswith(".bmp"): + image = BMP16(filename) + else: + raise ValueError("Unsupported format") + image.read_header() + if image.width != 16 or image.height != 256: + raise ValueError("Image size not 16x256") + palette = image.read_palette() + buffer = image.read_data() + return cls(buffer, palette) + + +class Grid: + """ + A grid is a layer of tiles that can be displayed on the screen. Each square + can contain any of the 16 tiles from the associated bank. + """ + + def __init__(self, bank, width=8, height=8, palette=None, buffer=None): + self.x = 0 + self.y = 0 + self.z = 0 + self.stride = (width + 1) & 0xfe + self.width = width + self.height = height + self.bank = bank + self.palette = palette or bank.palette + self.buffer = buffer or bytearray((self.stride * height)>>1) + self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, + self.palette, self.buffer) + + def tile(self, x, y, tile=None): + """Get or set what tile is displayed in the given place.""" + + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + index = (y * self.stride + x) >> 1 + b = self.buffer[index] + if tile is None: + return b & 0x0f if x & 0x01 else b >> 4 + if x & 0x01: + b = b & 0xf0 | tile + else: + b = b & 0x0f | (tile << 4) + self.buffer[index] = b + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + +class WallGrid(Grid): + """ + A special grid, shifted from its parents by half a tile, useful for making + nice-looking corners of walls and similar structures. + """ + + def __init__(self, grid, walls, bank, palette=None): + super().__init__(bank, grid.width + 1, grid.height + 1, palette) + self.grid = grid + self.walls = walls + self.update() + self.move(self.x - 8, self.y - 8) + + def update(self): + for y in range(self.height): + for x in range(self.width): + t = 0 + bit = 1 + for dy in (-1, 0): + for dx in (-1, 0): + if self.grid.tile(x + dx, y + dy) in self.walls: + t |= bit + bit <<= 1 + self.tile(x, y, t) + + +class Sprite: + """ + A sprite is a layer containing just a single tile from the associated bank, + that can be positioned anywhere on the screen. + """ + + def __init__(self, bank, frame, x, y, z=0, rotation=0, palette=None): + self.bank = bank + self.palette = palette or bank.palette + self.frame = frame + self.rotation = rotation + self.x = x + self.y = y + self.z = z + self.layer = _stage.Layer(1, 1, self.bank.buffer, self.palette) + self.layer.move(x, y) + self.layer.frame(frame, rotation) + self.px = x + self.py = y + + def move(self, x, y, z=None): + """Move the sprite to the given place.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def set_frame(self, frame=None, rotation=None): + """ + Set the current graphic and rotation of the sprite. + + The possible values for rotation are: 0 - none, 1 - 90 degrees + clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - + mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and + mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ + + if frame is not None: + self.frame = frame + if rotation is not None: + self.rotation = rotation + self.layer.frame(self.frame, self.rotation) + + def update(self): + pass + + +class Text: + """Text layer. For displaying text.""" + + def __init__(self, width, height, font=None, palette=None, buffer=None): + self.width = width + self.height = height + self.font = font or FONT + self.palette = palette or PALETTE + self.buffer = buffer or bytearray(width * height) + self.layer = _stage.Text(width, height, self.font, + self.palette, self.buffer) + self.column = 0 + self.row = 0 + self.x = 0 + self.y = 0 + self.z = 0 + + def char(self, x, y, c=None, hightlight=False): + """Get or set the character at the given location.""" + if not 0 <= x < self.width or not 0 <= y < self.height: + return + if c is None: + return chr(self.buffer[y * self.width + x]) + c = ord(c) + if hightlight: + c |= 0x80 + self.buffer[y * self.width + x] = c + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def cursor(self, x=None, y=None): + """Move the text cursor to the specified row and column.""" + if y is not None: + self.row = min(max(0, y), self.width - 1) + if x is not None: + self.column = min(max(0, x), self.height - 1) + + def text(self, text, hightlight=False): + """ + Display text starting at the current cursor location. + Return the dimensions of the rendered text. + """ + longest = 0 + tallest = 0 + for c in text: + if c != '\n': + self.char(self.column, self.row, c, hightlight) + self.column += 1 + if self.column >= self.width or c == '\n': + longest = max(longest, self.column) + self.column = 0 + self.row += 1 + if self.row >= self.height: + tallest = max(tallest, self.row) + self.row = 0 + longest = max(longest, self.column) + tallest = max(tallest, self.row) + (1 if self.column > 0 else 0) + return longest * 8, tallest * 8 + + def clear(self): + """Clear all text from the layer.""" + for i in range(self.width * self.height): + self.buffer[i] = 0 + + +class Stage: + """ + Represents what is being displayed on the screen. + + The ``display`` parameter is displayio.Display representing an initialized + display connected to the device. + + The ``fps`` specifies the maximum frame rate to be enforced. + + The ``scale`` specifies an optional scaling up of the display, to use + 2x2 or 3x3, etc. pixels. If not specified, it is inferred from the display + size (displays wider than 256 pixels will have scale=2, for example). + """ + buffer = bytearray(512) + + def __init__(self, display, fps=6, scale=None): + if scale is None: + self.scale = max(1, display.width // 128) + else: + self.scale = scale + self.layers = [] + self.display = display + self.width = display.width // self.scale + self.height = display.height // self.scale + self.last_tick = time.monotonic() + self.tick_delay = 1 / fps + self.vx = 0 + self.vy = 0 + + def tick(self): + """Wait for the start of the next frame.""" + self.last_tick += self.tick_delay + wait = max(0, self.last_tick - time.monotonic()) + if wait: + time.sleep(wait) + else: + self.last_tick = time.monotonic() + + def render_block(self, x0=None, y0=None, x1=None, y1=None): + """Update a rectangle of the screen.""" + if x0 is None: + x0 = self.vx + if y0 is None: + y0 = self.vy + if x1 is None: + x1 = self.width + self.vx + if y1 is None: + y1 = self.height + self.vy + x0 = min(max(0, x0 - self.vx), self.width - 1) + y0 = min(max(0, y0 - self.vy), self.height - 1) + x1 = min(max(1, x1 - self.vx), self.width) + y1 = min(max(1, y1 - self.vy), self.height) + if x0 >= x1 or y0 >= y1: + return + layers = [l.layer for l in self.layers] + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) + + def render_sprites(self, sprites): + """Update the spots taken by all the sprites in the list.""" + layers = [l.layer for l in self.layers] + for sprite in sprites: + x = int(sprite.x) - self.vx + y = int(sprite.y) - self.vy + x0 = max(0, min(self.width - 1, min(sprite.px, x))) + y0 = max(0, min(self.height - 1, min(sprite.py, y))) + x1 = max(1, min(self.width, max(sprite.px, x) + 16)) + y1 = max(1, min(self.height, max(sprite.py, y) + 16)) + sprite.px = x + sprite.py = y + if x0 >= x1 or y0 >= y1: + continue + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) diff --git a/circuitpython/frozen/circuitpython-stage/pewpew_m4/stage.py b/circuitpython/frozen/circuitpython-stage/pewpew_m4/stage.py index 2dedc93..44ab69a 120000..100644 --- a/circuitpython/frozen/circuitpython-stage/pewpew_m4/stage.py +++ b/circuitpython/frozen/circuitpython-stage/pewpew_m4/stage.py @@ -1 +1,630 @@ -../stage.py
\ No newline at end of file +import time +import array +import digitalio +import struct + +import _stage + + +FONT = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' + b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' + b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' + b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' + b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' + b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' + b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' + b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' + b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' + b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@U\xd0\xff' + b'\xf4\xaa\xbdV\xad\x01m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00' + b'm\x00m\x00m\x00m\x00m\x00\xbd\x01\xf9V\xe4\xff\x90\xaa@UUU\xff\xff' + b'\xaa\xaaUU\x00\x00\x00\x00\x00\x00\x00\x00U\x01\xff\x06' + b'\xea\x1b\x95o@n\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m' + b'\x00m\x00m\x00m\x00m\x00m@o\xd5k\xff\x1a\xaa\x06U\x01' + b'\x00\x00\x00\x00\x00\x00\x00\x00UU\xff\xff\xaa\xaaUU' + b'\x00\x00\x00\x00\x00UE\xfe\xd9\xef\xdd\x9f\xad\x9f\xad\x9a' + b'\x00\x00\x00\x00\x00\x00U\x15\xf7o\xa7jW\x15v\x00\xadu\xed\xda' + b'\xddv\x99\xe6E\x9a\x00U\x00\x00\x00\x00m\x00W\x00n\x00\x15\x00' + b'\x1b\x00\x05\x00\x00\x00\x00\x00\xaa\x00\xaa\x00\xaa\x00\xaa\x00' + b'\x00\xaa\x00\xaa\x00\xaa\x00\xaaP\x05\x94\x16\xa4\x1b\xe4\x1b' + b'\xe4\x1a\xa4\x1aT\x15\x00\x00P\x00\xd0\x01\xd0\x07\xd4\x19' + b'\xf9\x1d\xbd\x05T\x00\x00\x00T\x05\xf5\x17\xdd\x1d\xdd\x1d' + b'\xf5\x17\xe4\x06T\x05\x00\x00\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16' + b'\x14\x05\x00\x00T\x15\xf5\x1f\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1d' + b'P\x15\x00\x00\x00\x00P\x01\xe4\x06\xf4\x07\xe4\x06P\x01' + b'\x00\x00\x00\x00U\x15\xdd\x1d\xdd\x1d\x99\x19U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01\xd0\x01\x90\x01P\x01\xd0\x01' + b'P\x01\x00\x00T\x05t\x07d\x06T\x05\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x14\x05u\x17\xed\x1et\x07\xed\x1eu\x17\x14\x05\x00\x00' + b'T\x15\xf5\x1b\x99\x05\xf5\x17\x94\x19\xf9\x17U\x05\x00\x00' + b'\x15\x14\x1d\x1dU\x07\xd0\x01t\x15\x1d\x1d\x05\x15\x00\x00' + b'T\x01\xe4\x05u\x07\xdd\x01]\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01' + b'\x90\x01P\x01\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06' + b'\x90\x01\xd0\x01\x90\x01P\x06@\x05\x00\x00T\x00d\x01' + b'\x90\x01\xd0\x01\x90\x01d\x01T\x00\x00\x00\x00\x00\x14\x05' + b't\x07\xd0\x01t\x07\x14\x05\x00\x00\x00\x00P\x01\x90\x01' + b'\xd5\x15\xf9\x1b\xd5\x15\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'\x00\x00P\x01\xd0\x01\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01P\x01\x00\x00\x00\x04\x00\x1d' + b'@\x07\xd0\x01t\x00\x1d\x00\x04\x00\x00\x00T\x05\xe5\x16' + b'Y\x1a\xdd\x1di\x19\xe5\x16T\x05\x00\x00@\x01\xd0\x01' + b'\xe4\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00T\x05\xf9\x17' + b'U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00T\x05\xf5\x17]\x1d\x94\x07' + b']\x1d\xf5\x17T\x05\x00\x00P\x00t\x00]\x05]\x17\xfd\x1fU\x17' + b'@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1bU\x1d\xf9\x1bU\x05\x00\x00' + b'T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1bT\x05\x00\x00U\x15\xfd\x1f' + b'U\x19\xd0\x06d\x01t\x00T\x00\x00\x00T\x05\xf5\x17]\x1d\xf5\x17' + b']\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b]\x1d\xf9\x1fT\x1d\xf9\x17' + b'U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01P\x01' + b'\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01\x90\x01' + b'P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00\xd0\x01@\x07' + b'\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05\xf9\x1bU\x15' + b'\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07\xd0\x01t\x00' + b'\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16P\x05\xd0\x01' + b'P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1bY\x15\xf5\x06' + b'T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d\x15\x15\x00\x00' + b'U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17U\x05\x00\x00T\x05\xf5\x06' + b']\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00U\x01\xbd\x05]\x17\x1d\x1d' + b']\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06]\x01\xfd\x01]\x15\xfd\x1b' + b'U\x15\x00\x00U\x15\xfd\x1b]\x15]\x00\xbd\x01]\x01\x15\x00\x00\x00' + b'T\x15\xf5\x1b]\x05\xdd\x1fY\x1d\xf5\x1bT\x15\x00\x00' + b'\x15\x15\x1d\x1d]\x1d\xfd\x1f]\x1d\x1d\x1d\x15\x15\x00\x00' + b'T\x05\xe4\x06\xd0\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00' + b'\x00\x15\x00\x1d\x00\x1d\x05\x1d]\x19\xf5\x17T\x05\x00\x00' + b'\x15\x14\x1d\x1d]\x07\xfd\x01]\x07\x1d\x1d\x15\x14\x00\x00' + b'\x15\x00\x1d\x00\x1d\x00\x1d\x00]\x15\xfd\x1fU\x15\x00\x00' + b'\x05\x14\x1d\x1dm\x1e\xdd\x1d]\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x05\x15\x1d\x1dm\x1d\xdd\x1d]\x1e\x1d\x1d\x15\x14\x00\x00' + b'T\x01\xb5\x05]\x17\x1d\x1d]\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16' + b']\x19]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' + b']\x1e\xe5\x07T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d' + b'\x15\x15\x00\x00T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17' + b'U\x05\x00\x00U\x15\xf9\x1b\xd5\x15\xd0\x01\xd0\x01\xd0\x01' + b'P\x01\x00\x00\x15\x15\x1d\x1d\x1d\x1d\x19\x1du\x19\xd4\x17' + b'P\x05\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17d\x06\xd0\x01' + b'@\x00\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xd9\x19u\x17' + b'\x14\x05\x00\x00\x05\x14\x1d\x1dt\x07\xd0\x01t\x07\x1d\x1d' + b'\x05\x14\x00\x00\x15\x15\x1d\x1d\x19\x19u\x17\x94\x05\xd0\x01' + b'P\x01\x00\x00U\x15\xf9\x1bU\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00' + b'T\x05\xf4\x06t\x01t\x00t\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00' + b't\x00\xd0\x01@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07' + b'P\x07\xe4\x07T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19' + b'\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' + b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' + b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' + b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' + b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' + b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' + b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' + b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' + b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' + b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' + b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' + b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' + b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' + b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' + b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' + b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' + b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' + b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' + b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' + b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' + b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' + b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' + b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' + b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' + b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' + b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' + b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') + +PALETTE = (b'\xf8\x1f\x00\x00\xcey\xff\xff\xf8\x1f\x00\x19\xfc\xe0\xfd\xe0' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + +def color565(r, g, b): + """Convert 24-bit RGB color to 16-bit.""" + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): + """Return True if the two rectangles intersect.""" + if bx1 is None: + bx1 = bx0 + if by1 is None: + by1 = by0 + return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1) + + +class BMP16: + """Read 16-color BMP files.""" + + def __init__(self, filename): + self.filename = filename + self.colors = 0 + + def read_header(self): + """Read the file's header information.""" + + if self.colors: + return + with open(self.filename, 'rb') as f: + f.seek(10) + self.data = int.from_bytes(f.read(4), 'little') + f.seek(18) + self.width = int.from_bytes(f.read(4), 'little') + self.height = int.from_bytes(f.read(4), 'little') + f.seek(46) + self.colors = int.from_bytes(f.read(4), 'little') + + def read_palette(self): + """Read the color palette information.""" + + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(self.data - self.colors * 4) + for color in range(self.colors): + buffer = f.read(4) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + """Read the image data.""" + line_size = self.width >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + + with open(self.filename, 'rb') as f: + f.seek(self.data) + index = (self.height - 1) * line_size + for line in range(self.height): + chunk = f.read(line_size) + buffer[index:index + line_size] = chunk + index -= line_size + return buffer + + +def read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + dictionary = LZWDict(code_size) + bit = 0 + try: + byte = next(data) + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) + except StopIteration: + return + + +class GIF16: + """Read 16-color GIF files.""" + + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + header = f.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not GIF file") + self.width, self.height, flags, self.background, self.aspect = ( + struct.unpack('<HHBBB', f.read(7))) + self.palette_size = 1 << ((flags & 0x07) + 1) + if not flags & 0x80: + raise NotImplementedError() + if self.palette_size > 16: + raise ValueError("Too many colors (%d/16)." % self.palette_size) + + def read_palette(self): + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(13) + for color in range(self.palette_size): + buffer = f.read(3) + c = color565(buffer[0], buffer[1], buffer[2]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + line_size = (self.width + 1) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + with open(self.filename, 'rb') as f: + f.seek(13 + self.palette_size * 3) + while True: # skip to first frame + block_type = f.read(1)[0] + if block_type == 0x2c: + break + elif block_type == 0x21: # skip extension + extension_type = f.read(1)[0] + while True: + size = f.read(1)[0] + if size == 0: + break + f.seek(1, size) + elif block_type == 0x3b: + raise NotImplementedError() + x, y, w, h, flags = struct.unpack('<HHHHB', f.read(9)) + if (flags & 0x80 or flags & 0x40 or + w != self.width or h != self.height or x != 0 or y != 0): + raise NotImplementedError() + min_code_size = f.read(1)[0] + x = 0 + y = 0 + for decoded in lzw_decode(read_blockstream(f), min_code_size): + for pixel in decoded: + if x & 0x01: + buffer[(x >> 1) + y * line_size] |= pixel + else: + buffer[(x >> 1) + y * line_size] = pixel << 4 + x += 1 + if (x >= self.width): + x = 0 + y += 1 + return buffer + + +class Bank: + """ + Store graphics for the tiles and sprites. + + A single bank stores exactly 16 tiles, each 16x16 pixels in 16 possible + colors, and a 16-color palette. We just like the number 16. + + """ + + def __init__(self, buffer=None, palette=None): + self.buffer = buffer + self.palette = palette + + @classmethod + def from_bmp16(cls, filename): + """Read the bank from a BMP file.""" + return cls.from_image(filename) + + + @classmethod + def from_image(cls, filename): + """Read the bank from an image file.""" + if filename.lower().endswith(".gif"): + image = GIF16(filename) + elif filename.lower().endswith(".bmp"): + image = BMP16(filename) + else: + raise ValueError("Unsupported format") + image.read_header() + if image.width != 16 or image.height != 256: + raise ValueError("Image size not 16x256") + palette = image.read_palette() + buffer = image.read_data() + return cls(buffer, palette) + + +class Grid: + """ + A grid is a layer of tiles that can be displayed on the screen. Each square + can contain any of the 16 tiles from the associated bank. + """ + + def __init__(self, bank, width=8, height=8, palette=None, buffer=None): + self.x = 0 + self.y = 0 + self.z = 0 + self.stride = (width + 1) & 0xfe + self.width = width + self.height = height + self.bank = bank + self.palette = palette or bank.palette + self.buffer = buffer or bytearray((self.stride * height)>>1) + self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, + self.palette, self.buffer) + + def tile(self, x, y, tile=None): + """Get or set what tile is displayed in the given place.""" + + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + index = (y * self.stride + x) >> 1 + b = self.buffer[index] + if tile is None: + return b & 0x0f if x & 0x01 else b >> 4 + if x & 0x01: + b = b & 0xf0 | tile + else: + b = b & 0x0f | (tile << 4) + self.buffer[index] = b + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + +class WallGrid(Grid): + """ + A special grid, shifted from its parents by half a tile, useful for making + nice-looking corners of walls and similar structures. + """ + + def __init__(self, grid, walls, bank, palette=None): + super().__init__(bank, grid.width + 1, grid.height + 1, palette) + self.grid = grid + self.walls = walls + self.update() + self.move(self.x - 8, self.y - 8) + + def update(self): + for y in range(self.height): + for x in range(self.width): + t = 0 + bit = 1 + for dy in (-1, 0): + for dx in (-1, 0): + if self.grid.tile(x + dx, y + dy) in self.walls: + t |= bit + bit <<= 1 + self.tile(x, y, t) + + +class Sprite: + """ + A sprite is a layer containing just a single tile from the associated bank, + that can be positioned anywhere on the screen. + """ + + def __init__(self, bank, frame, x, y, z=0, rotation=0, palette=None): + self.bank = bank + self.palette = palette or bank.palette + self.frame = frame + self.rotation = rotation + self.x = x + self.y = y + self.z = z + self.layer = _stage.Layer(1, 1, self.bank.buffer, self.palette) + self.layer.move(x, y) + self.layer.frame(frame, rotation) + self.px = x + self.py = y + + def move(self, x, y, z=None): + """Move the sprite to the given place.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def set_frame(self, frame=None, rotation=None): + """ + Set the current graphic and rotation of the sprite. + + The possible values for rotation are: 0 - none, 1 - 90 degrees + clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - + mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and + mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ + + if frame is not None: + self.frame = frame + if rotation is not None: + self.rotation = rotation + self.layer.frame(self.frame, self.rotation) + + def update(self): + pass + + +class Text: + """Text layer. For displaying text.""" + + def __init__(self, width, height, font=None, palette=None, buffer=None): + self.width = width + self.height = height + self.font = font or FONT + self.palette = palette or PALETTE + self.buffer = buffer or bytearray(width * height) + self.layer = _stage.Text(width, height, self.font, + self.palette, self.buffer) + self.column = 0 + self.row = 0 + self.x = 0 + self.y = 0 + self.z = 0 + + def char(self, x, y, c=None, hightlight=False): + """Get or set the character at the given location.""" + if not 0 <= x < self.width or not 0 <= y < self.height: + return + if c is None: + return chr(self.buffer[y * self.width + x]) + c = ord(c) + if hightlight: + c |= 0x80 + self.buffer[y * self.width + x] = c + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def cursor(self, x=None, y=None): + """Move the text cursor to the specified row and column.""" + if y is not None: + self.row = min(max(0, y), self.width - 1) + if x is not None: + self.column = min(max(0, x), self.height - 1) + + def text(self, text, hightlight=False): + """ + Display text starting at the current cursor location. + Return the dimensions of the rendered text. + """ + longest = 0 + tallest = 0 + for c in text: + if c != '\n': + self.char(self.column, self.row, c, hightlight) + self.column += 1 + if self.column >= self.width or c == '\n': + longest = max(longest, self.column) + self.column = 0 + self.row += 1 + if self.row >= self.height: + tallest = max(tallest, self.row) + self.row = 0 + longest = max(longest, self.column) + tallest = max(tallest, self.row) + (1 if self.column > 0 else 0) + return longest * 8, tallest * 8 + + def clear(self): + """Clear all text from the layer.""" + for i in range(self.width * self.height): + self.buffer[i] = 0 + + +class Stage: + """ + Represents what is being displayed on the screen. + + The ``display`` parameter is displayio.Display representing an initialized + display connected to the device. + + The ``fps`` specifies the maximum frame rate to be enforced. + + The ``scale`` specifies an optional scaling up of the display, to use + 2x2 or 3x3, etc. pixels. If not specified, it is inferred from the display + size (displays wider than 256 pixels will have scale=2, for example). + """ + buffer = bytearray(512) + + def __init__(self, display, fps=6, scale=None): + if scale is None: + self.scale = max(1, display.width // 128) + else: + self.scale = scale + self.layers = [] + self.display = display + self.width = display.width // self.scale + self.height = display.height // self.scale + self.last_tick = time.monotonic() + self.tick_delay = 1 / fps + self.vx = 0 + self.vy = 0 + + def tick(self): + """Wait for the start of the next frame.""" + self.last_tick += self.tick_delay + wait = max(0, self.last_tick - time.monotonic()) + if wait: + time.sleep(wait) + else: + self.last_tick = time.monotonic() + + def render_block(self, x0=None, y0=None, x1=None, y1=None): + """Update a rectangle of the screen.""" + if x0 is None: + x0 = self.vx + if y0 is None: + y0 = self.vy + if x1 is None: + x1 = self.width + self.vx + if y1 is None: + y1 = self.height + self.vy + x0 = min(max(0, x0 - self.vx), self.width - 1) + y0 = min(max(0, y0 - self.vy), self.height - 1) + x1 = min(max(1, x1 - self.vx), self.width) + y1 = min(max(1, y1 - self.vy), self.height) + if x0 >= x1 or y0 >= y1: + return + layers = [l.layer for l in self.layers] + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) + + def render_sprites(self, sprites): + """Update the spots taken by all the sprites in the list.""" + layers = [l.layer for l in self.layers] + for sprite in sprites: + x = int(sprite.x) - self.vx + y = int(sprite.y) - self.vy + x0 = max(0, min(self.width - 1, min(sprite.px, x))) + y0 = max(0, min(self.height - 1, min(sprite.py, y))) + x1 = max(1, min(self.width, max(sprite.px, x) + 16)) + y1 = max(1, min(self.height, max(sprite.py, y) + 16)) + sprite.px = x + sprite.py = y + if x0 >= x1 or y0 >= y1: + continue + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) diff --git a/circuitpython/frozen/circuitpython-stage/picosystem/stage.py b/circuitpython/frozen/circuitpython-stage/picosystem/stage.py index 2dedc93..44ab69a 120000..100644 --- a/circuitpython/frozen/circuitpython-stage/picosystem/stage.py +++ b/circuitpython/frozen/circuitpython-stage/picosystem/stage.py @@ -1 +1,630 @@ -../stage.py
\ No newline at end of file +import time +import array +import digitalio +import struct + +import _stage + + +FONT = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' + b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' + b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' + b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' + b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' + b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' + b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' + b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' + b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' + b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@U\xd0\xff' + b'\xf4\xaa\xbdV\xad\x01m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00' + b'm\x00m\x00m\x00m\x00m\x00\xbd\x01\xf9V\xe4\xff\x90\xaa@UUU\xff\xff' + b'\xaa\xaaUU\x00\x00\x00\x00\x00\x00\x00\x00U\x01\xff\x06' + b'\xea\x1b\x95o@n\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m' + b'\x00m\x00m\x00m\x00m\x00m@o\xd5k\xff\x1a\xaa\x06U\x01' + b'\x00\x00\x00\x00\x00\x00\x00\x00UU\xff\xff\xaa\xaaUU' + b'\x00\x00\x00\x00\x00UE\xfe\xd9\xef\xdd\x9f\xad\x9f\xad\x9a' + b'\x00\x00\x00\x00\x00\x00U\x15\xf7o\xa7jW\x15v\x00\xadu\xed\xda' + b'\xddv\x99\xe6E\x9a\x00U\x00\x00\x00\x00m\x00W\x00n\x00\x15\x00' + b'\x1b\x00\x05\x00\x00\x00\x00\x00\xaa\x00\xaa\x00\xaa\x00\xaa\x00' + b'\x00\xaa\x00\xaa\x00\xaa\x00\xaaP\x05\x94\x16\xa4\x1b\xe4\x1b' + b'\xe4\x1a\xa4\x1aT\x15\x00\x00P\x00\xd0\x01\xd0\x07\xd4\x19' + b'\xf9\x1d\xbd\x05T\x00\x00\x00T\x05\xf5\x17\xdd\x1d\xdd\x1d' + b'\xf5\x17\xe4\x06T\x05\x00\x00\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16' + b'\x14\x05\x00\x00T\x15\xf5\x1f\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1d' + b'P\x15\x00\x00\x00\x00P\x01\xe4\x06\xf4\x07\xe4\x06P\x01' + b'\x00\x00\x00\x00U\x15\xdd\x1d\xdd\x1d\x99\x19U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01\xd0\x01\x90\x01P\x01\xd0\x01' + b'P\x01\x00\x00T\x05t\x07d\x06T\x05\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x14\x05u\x17\xed\x1et\x07\xed\x1eu\x17\x14\x05\x00\x00' + b'T\x15\xf5\x1b\x99\x05\xf5\x17\x94\x19\xf9\x17U\x05\x00\x00' + b'\x15\x14\x1d\x1dU\x07\xd0\x01t\x15\x1d\x1d\x05\x15\x00\x00' + b'T\x01\xe4\x05u\x07\xdd\x01]\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01' + b'\x90\x01P\x01\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06' + b'\x90\x01\xd0\x01\x90\x01P\x06@\x05\x00\x00T\x00d\x01' + b'\x90\x01\xd0\x01\x90\x01d\x01T\x00\x00\x00\x00\x00\x14\x05' + b't\x07\xd0\x01t\x07\x14\x05\x00\x00\x00\x00P\x01\x90\x01' + b'\xd5\x15\xf9\x1b\xd5\x15\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'\x00\x00P\x01\xd0\x01\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01P\x01\x00\x00\x00\x04\x00\x1d' + b'@\x07\xd0\x01t\x00\x1d\x00\x04\x00\x00\x00T\x05\xe5\x16' + b'Y\x1a\xdd\x1di\x19\xe5\x16T\x05\x00\x00@\x01\xd0\x01' + b'\xe4\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00T\x05\xf9\x17' + b'U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00T\x05\xf5\x17]\x1d\x94\x07' + b']\x1d\xf5\x17T\x05\x00\x00P\x00t\x00]\x05]\x17\xfd\x1fU\x17' + b'@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1bU\x1d\xf9\x1bU\x05\x00\x00' + b'T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1bT\x05\x00\x00U\x15\xfd\x1f' + b'U\x19\xd0\x06d\x01t\x00T\x00\x00\x00T\x05\xf5\x17]\x1d\xf5\x17' + b']\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b]\x1d\xf9\x1fT\x1d\xf9\x17' + b'U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01P\x01' + b'\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01\x90\x01' + b'P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00\xd0\x01@\x07' + b'\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05\xf9\x1bU\x15' + b'\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07\xd0\x01t\x00' + b'\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16P\x05\xd0\x01' + b'P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1bY\x15\xf5\x06' + b'T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d\x15\x15\x00\x00' + b'U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17U\x05\x00\x00T\x05\xf5\x06' + b']\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00U\x01\xbd\x05]\x17\x1d\x1d' + b']\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06]\x01\xfd\x01]\x15\xfd\x1b' + b'U\x15\x00\x00U\x15\xfd\x1b]\x15]\x00\xbd\x01]\x01\x15\x00\x00\x00' + b'T\x15\xf5\x1b]\x05\xdd\x1fY\x1d\xf5\x1bT\x15\x00\x00' + b'\x15\x15\x1d\x1d]\x1d\xfd\x1f]\x1d\x1d\x1d\x15\x15\x00\x00' + b'T\x05\xe4\x06\xd0\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00' + b'\x00\x15\x00\x1d\x00\x1d\x05\x1d]\x19\xf5\x17T\x05\x00\x00' + b'\x15\x14\x1d\x1d]\x07\xfd\x01]\x07\x1d\x1d\x15\x14\x00\x00' + b'\x15\x00\x1d\x00\x1d\x00\x1d\x00]\x15\xfd\x1fU\x15\x00\x00' + b'\x05\x14\x1d\x1dm\x1e\xdd\x1d]\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x05\x15\x1d\x1dm\x1d\xdd\x1d]\x1e\x1d\x1d\x15\x14\x00\x00' + b'T\x01\xb5\x05]\x17\x1d\x1d]\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16' + b']\x19]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' + b']\x1e\xe5\x07T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d' + b'\x15\x15\x00\x00T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17' + b'U\x05\x00\x00U\x15\xf9\x1b\xd5\x15\xd0\x01\xd0\x01\xd0\x01' + b'P\x01\x00\x00\x15\x15\x1d\x1d\x1d\x1d\x19\x1du\x19\xd4\x17' + b'P\x05\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17d\x06\xd0\x01' + b'@\x00\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xd9\x19u\x17' + b'\x14\x05\x00\x00\x05\x14\x1d\x1dt\x07\xd0\x01t\x07\x1d\x1d' + b'\x05\x14\x00\x00\x15\x15\x1d\x1d\x19\x19u\x17\x94\x05\xd0\x01' + b'P\x01\x00\x00U\x15\xf9\x1bU\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00' + b'T\x05\xf4\x06t\x01t\x00t\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00' + b't\x00\xd0\x01@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07' + b'P\x07\xe4\x07T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19' + b'\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' + b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' + b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' + b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' + b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' + b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' + b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' + b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' + b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' + b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' + b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' + b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' + b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' + b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' + b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' + b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' + b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' + b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' + b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' + b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' + b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' + b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' + b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' + b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' + b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' + b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' + b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') + +PALETTE = (b'\xf8\x1f\x00\x00\xcey\xff\xff\xf8\x1f\x00\x19\xfc\xe0\xfd\xe0' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + +def color565(r, g, b): + """Convert 24-bit RGB color to 16-bit.""" + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): + """Return True if the two rectangles intersect.""" + if bx1 is None: + bx1 = bx0 + if by1 is None: + by1 = by0 + return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1) + + +class BMP16: + """Read 16-color BMP files.""" + + def __init__(self, filename): + self.filename = filename + self.colors = 0 + + def read_header(self): + """Read the file's header information.""" + + if self.colors: + return + with open(self.filename, 'rb') as f: + f.seek(10) + self.data = int.from_bytes(f.read(4), 'little') + f.seek(18) + self.width = int.from_bytes(f.read(4), 'little') + self.height = int.from_bytes(f.read(4), 'little') + f.seek(46) + self.colors = int.from_bytes(f.read(4), 'little') + + def read_palette(self): + """Read the color palette information.""" + + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(self.data - self.colors * 4) + for color in range(self.colors): + buffer = f.read(4) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + """Read the image data.""" + line_size = self.width >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + + with open(self.filename, 'rb') as f: + f.seek(self.data) + index = (self.height - 1) * line_size + for line in range(self.height): + chunk = f.read(line_size) + buffer[index:index + line_size] = chunk + index -= line_size + return buffer + + +def read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + dictionary = LZWDict(code_size) + bit = 0 + try: + byte = next(data) + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) + except StopIteration: + return + + +class GIF16: + """Read 16-color GIF files.""" + + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + header = f.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not GIF file") + self.width, self.height, flags, self.background, self.aspect = ( + struct.unpack('<HHBBB', f.read(7))) + self.palette_size = 1 << ((flags & 0x07) + 1) + if not flags & 0x80: + raise NotImplementedError() + if self.palette_size > 16: + raise ValueError("Too many colors (%d/16)." % self.palette_size) + + def read_palette(self): + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(13) + for color in range(self.palette_size): + buffer = f.read(3) + c = color565(buffer[0], buffer[1], buffer[2]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + line_size = (self.width + 1) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + with open(self.filename, 'rb') as f: + f.seek(13 + self.palette_size * 3) + while True: # skip to first frame + block_type = f.read(1)[0] + if block_type == 0x2c: + break + elif block_type == 0x21: # skip extension + extension_type = f.read(1)[0] + while True: + size = f.read(1)[0] + if size == 0: + break + f.seek(1, size) + elif block_type == 0x3b: + raise NotImplementedError() + x, y, w, h, flags = struct.unpack('<HHHHB', f.read(9)) + if (flags & 0x80 or flags & 0x40 or + w != self.width or h != self.height or x != 0 or y != 0): + raise NotImplementedError() + min_code_size = f.read(1)[0] + x = 0 + y = 0 + for decoded in lzw_decode(read_blockstream(f), min_code_size): + for pixel in decoded: + if x & 0x01: + buffer[(x >> 1) + y * line_size] |= pixel + else: + buffer[(x >> 1) + y * line_size] = pixel << 4 + x += 1 + if (x >= self.width): + x = 0 + y += 1 + return buffer + + +class Bank: + """ + Store graphics for the tiles and sprites. + + A single bank stores exactly 16 tiles, each 16x16 pixels in 16 possible + colors, and a 16-color palette. We just like the number 16. + + """ + + def __init__(self, buffer=None, palette=None): + self.buffer = buffer + self.palette = palette + + @classmethod + def from_bmp16(cls, filename): + """Read the bank from a BMP file.""" + return cls.from_image(filename) + + + @classmethod + def from_image(cls, filename): + """Read the bank from an image file.""" + if filename.lower().endswith(".gif"): + image = GIF16(filename) + elif filename.lower().endswith(".bmp"): + image = BMP16(filename) + else: + raise ValueError("Unsupported format") + image.read_header() + if image.width != 16 or image.height != 256: + raise ValueError("Image size not 16x256") + palette = image.read_palette() + buffer = image.read_data() + return cls(buffer, palette) + + +class Grid: + """ + A grid is a layer of tiles that can be displayed on the screen. Each square + can contain any of the 16 tiles from the associated bank. + """ + + def __init__(self, bank, width=8, height=8, palette=None, buffer=None): + self.x = 0 + self.y = 0 + self.z = 0 + self.stride = (width + 1) & 0xfe + self.width = width + self.height = height + self.bank = bank + self.palette = palette or bank.palette + self.buffer = buffer or bytearray((self.stride * height)>>1) + self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, + self.palette, self.buffer) + + def tile(self, x, y, tile=None): + """Get or set what tile is displayed in the given place.""" + + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + index = (y * self.stride + x) >> 1 + b = self.buffer[index] + if tile is None: + return b & 0x0f if x & 0x01 else b >> 4 + if x & 0x01: + b = b & 0xf0 | tile + else: + b = b & 0x0f | (tile << 4) + self.buffer[index] = b + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + +class WallGrid(Grid): + """ + A special grid, shifted from its parents by half a tile, useful for making + nice-looking corners of walls and similar structures. + """ + + def __init__(self, grid, walls, bank, palette=None): + super().__init__(bank, grid.width + 1, grid.height + 1, palette) + self.grid = grid + self.walls = walls + self.update() + self.move(self.x - 8, self.y - 8) + + def update(self): + for y in range(self.height): + for x in range(self.width): + t = 0 + bit = 1 + for dy in (-1, 0): + for dx in (-1, 0): + if self.grid.tile(x + dx, y + dy) in self.walls: + t |= bit + bit <<= 1 + self.tile(x, y, t) + + +class Sprite: + """ + A sprite is a layer containing just a single tile from the associated bank, + that can be positioned anywhere on the screen. + """ + + def __init__(self, bank, frame, x, y, z=0, rotation=0, palette=None): + self.bank = bank + self.palette = palette or bank.palette + self.frame = frame + self.rotation = rotation + self.x = x + self.y = y + self.z = z + self.layer = _stage.Layer(1, 1, self.bank.buffer, self.palette) + self.layer.move(x, y) + self.layer.frame(frame, rotation) + self.px = x + self.py = y + + def move(self, x, y, z=None): + """Move the sprite to the given place.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def set_frame(self, frame=None, rotation=None): + """ + Set the current graphic and rotation of the sprite. + + The possible values for rotation are: 0 - none, 1 - 90 degrees + clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - + mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and + mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ + + if frame is not None: + self.frame = frame + if rotation is not None: + self.rotation = rotation + self.layer.frame(self.frame, self.rotation) + + def update(self): + pass + + +class Text: + """Text layer. For displaying text.""" + + def __init__(self, width, height, font=None, palette=None, buffer=None): + self.width = width + self.height = height + self.font = font or FONT + self.palette = palette or PALETTE + self.buffer = buffer or bytearray(width * height) + self.layer = _stage.Text(width, height, self.font, + self.palette, self.buffer) + self.column = 0 + self.row = 0 + self.x = 0 + self.y = 0 + self.z = 0 + + def char(self, x, y, c=None, hightlight=False): + """Get or set the character at the given location.""" + if not 0 <= x < self.width or not 0 <= y < self.height: + return + if c is None: + return chr(self.buffer[y * self.width + x]) + c = ord(c) + if hightlight: + c |= 0x80 + self.buffer[y * self.width + x] = c + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def cursor(self, x=None, y=None): + """Move the text cursor to the specified row and column.""" + if y is not None: + self.row = min(max(0, y), self.width - 1) + if x is not None: + self.column = min(max(0, x), self.height - 1) + + def text(self, text, hightlight=False): + """ + Display text starting at the current cursor location. + Return the dimensions of the rendered text. + """ + longest = 0 + tallest = 0 + for c in text: + if c != '\n': + self.char(self.column, self.row, c, hightlight) + self.column += 1 + if self.column >= self.width or c == '\n': + longest = max(longest, self.column) + self.column = 0 + self.row += 1 + if self.row >= self.height: + tallest = max(tallest, self.row) + self.row = 0 + longest = max(longest, self.column) + tallest = max(tallest, self.row) + (1 if self.column > 0 else 0) + return longest * 8, tallest * 8 + + def clear(self): + """Clear all text from the layer.""" + for i in range(self.width * self.height): + self.buffer[i] = 0 + + +class Stage: + """ + Represents what is being displayed on the screen. + + The ``display`` parameter is displayio.Display representing an initialized + display connected to the device. + + The ``fps`` specifies the maximum frame rate to be enforced. + + The ``scale`` specifies an optional scaling up of the display, to use + 2x2 or 3x3, etc. pixels. If not specified, it is inferred from the display + size (displays wider than 256 pixels will have scale=2, for example). + """ + buffer = bytearray(512) + + def __init__(self, display, fps=6, scale=None): + if scale is None: + self.scale = max(1, display.width // 128) + else: + self.scale = scale + self.layers = [] + self.display = display + self.width = display.width // self.scale + self.height = display.height // self.scale + self.last_tick = time.monotonic() + self.tick_delay = 1 / fps + self.vx = 0 + self.vy = 0 + + def tick(self): + """Wait for the start of the next frame.""" + self.last_tick += self.tick_delay + wait = max(0, self.last_tick - time.monotonic()) + if wait: + time.sleep(wait) + else: + self.last_tick = time.monotonic() + + def render_block(self, x0=None, y0=None, x1=None, y1=None): + """Update a rectangle of the screen.""" + if x0 is None: + x0 = self.vx + if y0 is None: + y0 = self.vy + if x1 is None: + x1 = self.width + self.vx + if y1 is None: + y1 = self.height + self.vy + x0 = min(max(0, x0 - self.vx), self.width - 1) + y0 = min(max(0, y0 - self.vy), self.height - 1) + x1 = min(max(1, x1 - self.vx), self.width) + y1 = min(max(1, y1 - self.vy), self.height) + if x0 >= x1 or y0 >= y1: + return + layers = [l.layer for l in self.layers] + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) + + def render_sprites(self, sprites): + """Update the spots taken by all the sprites in the list.""" + layers = [l.layer for l in self.layers] + for sprite in sprites: + x = int(sprite.x) - self.vx + y = int(sprite.y) - self.vy + x0 = max(0, min(self.width - 1, min(sprite.px, x))) + y0 = max(0, min(self.height - 1, min(sprite.py, y))) + x1 = max(1, min(self.width, max(sprite.px, x) + 16)) + y1 = max(1, min(self.height, max(sprite.py, y) + 16)) + sprite.px = x + sprite.py = y + if x0 >= x1 or y0 >= y1: + continue + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) diff --git a/circuitpython/frozen/circuitpython-stage/pybadge/stage.py b/circuitpython/frozen/circuitpython-stage/pybadge/stage.py index 2dedc93..44ab69a 120000..100644 --- a/circuitpython/frozen/circuitpython-stage/pybadge/stage.py +++ b/circuitpython/frozen/circuitpython-stage/pybadge/stage.py @@ -1 +1,630 @@ -../stage.py
\ No newline at end of file +import time +import array +import digitalio +import struct + +import _stage + + +FONT = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' + b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' + b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' + b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' + b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' + b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' + b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' + b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' + b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' + b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@U\xd0\xff' + b'\xf4\xaa\xbdV\xad\x01m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00' + b'm\x00m\x00m\x00m\x00m\x00\xbd\x01\xf9V\xe4\xff\x90\xaa@UUU\xff\xff' + b'\xaa\xaaUU\x00\x00\x00\x00\x00\x00\x00\x00U\x01\xff\x06' + b'\xea\x1b\x95o@n\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m' + b'\x00m\x00m\x00m\x00m\x00m@o\xd5k\xff\x1a\xaa\x06U\x01' + b'\x00\x00\x00\x00\x00\x00\x00\x00UU\xff\xff\xaa\xaaUU' + b'\x00\x00\x00\x00\x00UE\xfe\xd9\xef\xdd\x9f\xad\x9f\xad\x9a' + b'\x00\x00\x00\x00\x00\x00U\x15\xf7o\xa7jW\x15v\x00\xadu\xed\xda' + b'\xddv\x99\xe6E\x9a\x00U\x00\x00\x00\x00m\x00W\x00n\x00\x15\x00' + b'\x1b\x00\x05\x00\x00\x00\x00\x00\xaa\x00\xaa\x00\xaa\x00\xaa\x00' + b'\x00\xaa\x00\xaa\x00\xaa\x00\xaaP\x05\x94\x16\xa4\x1b\xe4\x1b' + b'\xe4\x1a\xa4\x1aT\x15\x00\x00P\x00\xd0\x01\xd0\x07\xd4\x19' + b'\xf9\x1d\xbd\x05T\x00\x00\x00T\x05\xf5\x17\xdd\x1d\xdd\x1d' + b'\xf5\x17\xe4\x06T\x05\x00\x00\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16' + b'\x14\x05\x00\x00T\x15\xf5\x1f\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1d' + b'P\x15\x00\x00\x00\x00P\x01\xe4\x06\xf4\x07\xe4\x06P\x01' + b'\x00\x00\x00\x00U\x15\xdd\x1d\xdd\x1d\x99\x19U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01\xd0\x01\x90\x01P\x01\xd0\x01' + b'P\x01\x00\x00T\x05t\x07d\x06T\x05\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x14\x05u\x17\xed\x1et\x07\xed\x1eu\x17\x14\x05\x00\x00' + b'T\x15\xf5\x1b\x99\x05\xf5\x17\x94\x19\xf9\x17U\x05\x00\x00' + b'\x15\x14\x1d\x1dU\x07\xd0\x01t\x15\x1d\x1d\x05\x15\x00\x00' + b'T\x01\xe4\x05u\x07\xdd\x01]\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01' + b'\x90\x01P\x01\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06' + b'\x90\x01\xd0\x01\x90\x01P\x06@\x05\x00\x00T\x00d\x01' + b'\x90\x01\xd0\x01\x90\x01d\x01T\x00\x00\x00\x00\x00\x14\x05' + b't\x07\xd0\x01t\x07\x14\x05\x00\x00\x00\x00P\x01\x90\x01' + b'\xd5\x15\xf9\x1b\xd5\x15\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'\x00\x00P\x01\xd0\x01\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01P\x01\x00\x00\x00\x04\x00\x1d' + b'@\x07\xd0\x01t\x00\x1d\x00\x04\x00\x00\x00T\x05\xe5\x16' + b'Y\x1a\xdd\x1di\x19\xe5\x16T\x05\x00\x00@\x01\xd0\x01' + b'\xe4\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00T\x05\xf9\x17' + b'U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00T\x05\xf5\x17]\x1d\x94\x07' + b']\x1d\xf5\x17T\x05\x00\x00P\x00t\x00]\x05]\x17\xfd\x1fU\x17' + b'@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1bU\x1d\xf9\x1bU\x05\x00\x00' + b'T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1bT\x05\x00\x00U\x15\xfd\x1f' + b'U\x19\xd0\x06d\x01t\x00T\x00\x00\x00T\x05\xf5\x17]\x1d\xf5\x17' + b']\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b]\x1d\xf9\x1fT\x1d\xf9\x17' + b'U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01P\x01' + b'\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01\x90\x01' + b'P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00\xd0\x01@\x07' + b'\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05\xf9\x1bU\x15' + b'\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07\xd0\x01t\x00' + b'\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16P\x05\xd0\x01' + b'P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1bY\x15\xf5\x06' + b'T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d\x15\x15\x00\x00' + b'U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17U\x05\x00\x00T\x05\xf5\x06' + b']\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00U\x01\xbd\x05]\x17\x1d\x1d' + b']\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06]\x01\xfd\x01]\x15\xfd\x1b' + b'U\x15\x00\x00U\x15\xfd\x1b]\x15]\x00\xbd\x01]\x01\x15\x00\x00\x00' + b'T\x15\xf5\x1b]\x05\xdd\x1fY\x1d\xf5\x1bT\x15\x00\x00' + b'\x15\x15\x1d\x1d]\x1d\xfd\x1f]\x1d\x1d\x1d\x15\x15\x00\x00' + b'T\x05\xe4\x06\xd0\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00' + b'\x00\x15\x00\x1d\x00\x1d\x05\x1d]\x19\xf5\x17T\x05\x00\x00' + b'\x15\x14\x1d\x1d]\x07\xfd\x01]\x07\x1d\x1d\x15\x14\x00\x00' + b'\x15\x00\x1d\x00\x1d\x00\x1d\x00]\x15\xfd\x1fU\x15\x00\x00' + b'\x05\x14\x1d\x1dm\x1e\xdd\x1d]\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x05\x15\x1d\x1dm\x1d\xdd\x1d]\x1e\x1d\x1d\x15\x14\x00\x00' + b'T\x01\xb5\x05]\x17\x1d\x1d]\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16' + b']\x19]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' + b']\x1e\xe5\x07T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d' + b'\x15\x15\x00\x00T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17' + b'U\x05\x00\x00U\x15\xf9\x1b\xd5\x15\xd0\x01\xd0\x01\xd0\x01' + b'P\x01\x00\x00\x15\x15\x1d\x1d\x1d\x1d\x19\x1du\x19\xd4\x17' + b'P\x05\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17d\x06\xd0\x01' + b'@\x00\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xd9\x19u\x17' + b'\x14\x05\x00\x00\x05\x14\x1d\x1dt\x07\xd0\x01t\x07\x1d\x1d' + b'\x05\x14\x00\x00\x15\x15\x1d\x1d\x19\x19u\x17\x94\x05\xd0\x01' + b'P\x01\x00\x00U\x15\xf9\x1bU\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00' + b'T\x05\xf4\x06t\x01t\x00t\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00' + b't\x00\xd0\x01@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07' + b'P\x07\xe4\x07T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19' + b'\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' + b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' + b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' + b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' + b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' + b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' + b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' + b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' + b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' + b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' + b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' + b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' + b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' + b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' + b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' + b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' + b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' + b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' + b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' + b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' + b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' + b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' + b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' + b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' + b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' + b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' + b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') + +PALETTE = (b'\xf8\x1f\x00\x00\xcey\xff\xff\xf8\x1f\x00\x19\xfc\xe0\xfd\xe0' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + +def color565(r, g, b): + """Convert 24-bit RGB color to 16-bit.""" + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): + """Return True if the two rectangles intersect.""" + if bx1 is None: + bx1 = bx0 + if by1 is None: + by1 = by0 + return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1) + + +class BMP16: + """Read 16-color BMP files.""" + + def __init__(self, filename): + self.filename = filename + self.colors = 0 + + def read_header(self): + """Read the file's header information.""" + + if self.colors: + return + with open(self.filename, 'rb') as f: + f.seek(10) + self.data = int.from_bytes(f.read(4), 'little') + f.seek(18) + self.width = int.from_bytes(f.read(4), 'little') + self.height = int.from_bytes(f.read(4), 'little') + f.seek(46) + self.colors = int.from_bytes(f.read(4), 'little') + + def read_palette(self): + """Read the color palette information.""" + + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(self.data - self.colors * 4) + for color in range(self.colors): + buffer = f.read(4) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + """Read the image data.""" + line_size = self.width >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + + with open(self.filename, 'rb') as f: + f.seek(self.data) + index = (self.height - 1) * line_size + for line in range(self.height): + chunk = f.read(line_size) + buffer[index:index + line_size] = chunk + index -= line_size + return buffer + + +def read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + dictionary = LZWDict(code_size) + bit = 0 + try: + byte = next(data) + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) + except StopIteration: + return + + +class GIF16: + """Read 16-color GIF files.""" + + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + header = f.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not GIF file") + self.width, self.height, flags, self.background, self.aspect = ( + struct.unpack('<HHBBB', f.read(7))) + self.palette_size = 1 << ((flags & 0x07) + 1) + if not flags & 0x80: + raise NotImplementedError() + if self.palette_size > 16: + raise ValueError("Too many colors (%d/16)." % self.palette_size) + + def read_palette(self): + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(13) + for color in range(self.palette_size): + buffer = f.read(3) + c = color565(buffer[0], buffer[1], buffer[2]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + line_size = (self.width + 1) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + with open(self.filename, 'rb') as f: + f.seek(13 + self.palette_size * 3) + while True: # skip to first frame + block_type = f.read(1)[0] + if block_type == 0x2c: + break + elif block_type == 0x21: # skip extension + extension_type = f.read(1)[0] + while True: + size = f.read(1)[0] + if size == 0: + break + f.seek(1, size) + elif block_type == 0x3b: + raise NotImplementedError() + x, y, w, h, flags = struct.unpack('<HHHHB', f.read(9)) + if (flags & 0x80 or flags & 0x40 or + w != self.width or h != self.height or x != 0 or y != 0): + raise NotImplementedError() + min_code_size = f.read(1)[0] + x = 0 + y = 0 + for decoded in lzw_decode(read_blockstream(f), min_code_size): + for pixel in decoded: + if x & 0x01: + buffer[(x >> 1) + y * line_size] |= pixel + else: + buffer[(x >> 1) + y * line_size] = pixel << 4 + x += 1 + if (x >= self.width): + x = 0 + y += 1 + return buffer + + +class Bank: + """ + Store graphics for the tiles and sprites. + + A single bank stores exactly 16 tiles, each 16x16 pixels in 16 possible + colors, and a 16-color palette. We just like the number 16. + + """ + + def __init__(self, buffer=None, palette=None): + self.buffer = buffer + self.palette = palette + + @classmethod + def from_bmp16(cls, filename): + """Read the bank from a BMP file.""" + return cls.from_image(filename) + + + @classmethod + def from_image(cls, filename): + """Read the bank from an image file.""" + if filename.lower().endswith(".gif"): + image = GIF16(filename) + elif filename.lower().endswith(".bmp"): + image = BMP16(filename) + else: + raise ValueError("Unsupported format") + image.read_header() + if image.width != 16 or image.height != 256: + raise ValueError("Image size not 16x256") + palette = image.read_palette() + buffer = image.read_data() + return cls(buffer, palette) + + +class Grid: + """ + A grid is a layer of tiles that can be displayed on the screen. Each square + can contain any of the 16 tiles from the associated bank. + """ + + def __init__(self, bank, width=8, height=8, palette=None, buffer=None): + self.x = 0 + self.y = 0 + self.z = 0 + self.stride = (width + 1) & 0xfe + self.width = width + self.height = height + self.bank = bank + self.palette = palette or bank.palette + self.buffer = buffer or bytearray((self.stride * height)>>1) + self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, + self.palette, self.buffer) + + def tile(self, x, y, tile=None): + """Get or set what tile is displayed in the given place.""" + + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + index = (y * self.stride + x) >> 1 + b = self.buffer[index] + if tile is None: + return b & 0x0f if x & 0x01 else b >> 4 + if x & 0x01: + b = b & 0xf0 | tile + else: + b = b & 0x0f | (tile << 4) + self.buffer[index] = b + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + +class WallGrid(Grid): + """ + A special grid, shifted from its parents by half a tile, useful for making + nice-looking corners of walls and similar structures. + """ + + def __init__(self, grid, walls, bank, palette=None): + super().__init__(bank, grid.width + 1, grid.height + 1, palette) + self.grid = grid + self.walls = walls + self.update() + self.move(self.x - 8, self.y - 8) + + def update(self): + for y in range(self.height): + for x in range(self.width): + t = 0 + bit = 1 + for dy in (-1, 0): + for dx in (-1, 0): + if self.grid.tile(x + dx, y + dy) in self.walls: + t |= bit + bit <<= 1 + self.tile(x, y, t) + + +class Sprite: + """ + A sprite is a layer containing just a single tile from the associated bank, + that can be positioned anywhere on the screen. + """ + + def __init__(self, bank, frame, x, y, z=0, rotation=0, palette=None): + self.bank = bank + self.palette = palette or bank.palette + self.frame = frame + self.rotation = rotation + self.x = x + self.y = y + self.z = z + self.layer = _stage.Layer(1, 1, self.bank.buffer, self.palette) + self.layer.move(x, y) + self.layer.frame(frame, rotation) + self.px = x + self.py = y + + def move(self, x, y, z=None): + """Move the sprite to the given place.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def set_frame(self, frame=None, rotation=None): + """ + Set the current graphic and rotation of the sprite. + + The possible values for rotation are: 0 - none, 1 - 90 degrees + clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - + mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and + mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ + + if frame is not None: + self.frame = frame + if rotation is not None: + self.rotation = rotation + self.layer.frame(self.frame, self.rotation) + + def update(self): + pass + + +class Text: + """Text layer. For displaying text.""" + + def __init__(self, width, height, font=None, palette=None, buffer=None): + self.width = width + self.height = height + self.font = font or FONT + self.palette = palette or PALETTE + self.buffer = buffer or bytearray(width * height) + self.layer = _stage.Text(width, height, self.font, + self.palette, self.buffer) + self.column = 0 + self.row = 0 + self.x = 0 + self.y = 0 + self.z = 0 + + def char(self, x, y, c=None, hightlight=False): + """Get or set the character at the given location.""" + if not 0 <= x < self.width or not 0 <= y < self.height: + return + if c is None: + return chr(self.buffer[y * self.width + x]) + c = ord(c) + if hightlight: + c |= 0x80 + self.buffer[y * self.width + x] = c + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def cursor(self, x=None, y=None): + """Move the text cursor to the specified row and column.""" + if y is not None: + self.row = min(max(0, y), self.width - 1) + if x is not None: + self.column = min(max(0, x), self.height - 1) + + def text(self, text, hightlight=False): + """ + Display text starting at the current cursor location. + Return the dimensions of the rendered text. + """ + longest = 0 + tallest = 0 + for c in text: + if c != '\n': + self.char(self.column, self.row, c, hightlight) + self.column += 1 + if self.column >= self.width or c == '\n': + longest = max(longest, self.column) + self.column = 0 + self.row += 1 + if self.row >= self.height: + tallest = max(tallest, self.row) + self.row = 0 + longest = max(longest, self.column) + tallest = max(tallest, self.row) + (1 if self.column > 0 else 0) + return longest * 8, tallest * 8 + + def clear(self): + """Clear all text from the layer.""" + for i in range(self.width * self.height): + self.buffer[i] = 0 + + +class Stage: + """ + Represents what is being displayed on the screen. + + The ``display`` parameter is displayio.Display representing an initialized + display connected to the device. + + The ``fps`` specifies the maximum frame rate to be enforced. + + The ``scale`` specifies an optional scaling up of the display, to use + 2x2 or 3x3, etc. pixels. If not specified, it is inferred from the display + size (displays wider than 256 pixels will have scale=2, for example). + """ + buffer = bytearray(512) + + def __init__(self, display, fps=6, scale=None): + if scale is None: + self.scale = max(1, display.width // 128) + else: + self.scale = scale + self.layers = [] + self.display = display + self.width = display.width // self.scale + self.height = display.height // self.scale + self.last_tick = time.monotonic() + self.tick_delay = 1 / fps + self.vx = 0 + self.vy = 0 + + def tick(self): + """Wait for the start of the next frame.""" + self.last_tick += self.tick_delay + wait = max(0, self.last_tick - time.monotonic()) + if wait: + time.sleep(wait) + else: + self.last_tick = time.monotonic() + + def render_block(self, x0=None, y0=None, x1=None, y1=None): + """Update a rectangle of the screen.""" + if x0 is None: + x0 = self.vx + if y0 is None: + y0 = self.vy + if x1 is None: + x1 = self.width + self.vx + if y1 is None: + y1 = self.height + self.vy + x0 = min(max(0, x0 - self.vx), self.width - 1) + y0 = min(max(0, y0 - self.vy), self.height - 1) + x1 = min(max(1, x1 - self.vx), self.width) + y1 = min(max(1, y1 - self.vy), self.height) + if x0 >= x1 or y0 >= y1: + return + layers = [l.layer for l in self.layers] + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) + + def render_sprites(self, sprites): + """Update the spots taken by all the sprites in the list.""" + layers = [l.layer for l in self.layers] + for sprite in sprites: + x = int(sprite.x) - self.vx + y = int(sprite.y) - self.vy + x0 = max(0, min(self.width - 1, min(sprite.px, x))) + y0 = max(0, min(self.height - 1, min(sprite.py, y))) + x1 = max(1, min(self.width, max(sprite.px, x) + 16)) + y1 = max(1, min(self.height, max(sprite.py, y) + 16)) + sprite.px = x + sprite.py = y + if x0 >= x1 or y0 >= y1: + continue + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) diff --git a/circuitpython/frozen/circuitpython-stage/pygamer/stage.py b/circuitpython/frozen/circuitpython-stage/pygamer/stage.py index 2dedc93..44ab69a 120000..100644 --- a/circuitpython/frozen/circuitpython-stage/pygamer/stage.py +++ b/circuitpython/frozen/circuitpython-stage/pygamer/stage.py @@ -1 +1,630 @@ -../stage.py
\ No newline at end of file +import time +import array +import digitalio +import struct + +import _stage + + +FONT = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' + b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' + b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' + b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' + b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' + b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' + b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' + b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' + b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' + b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@U\xd0\xff' + b'\xf4\xaa\xbdV\xad\x01m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00' + b'm\x00m\x00m\x00m\x00m\x00\xbd\x01\xf9V\xe4\xff\x90\xaa@UUU\xff\xff' + b'\xaa\xaaUU\x00\x00\x00\x00\x00\x00\x00\x00U\x01\xff\x06' + b'\xea\x1b\x95o@n\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m' + b'\x00m\x00m\x00m\x00m\x00m@o\xd5k\xff\x1a\xaa\x06U\x01' + b'\x00\x00\x00\x00\x00\x00\x00\x00UU\xff\xff\xaa\xaaUU' + b'\x00\x00\x00\x00\x00UE\xfe\xd9\xef\xdd\x9f\xad\x9f\xad\x9a' + b'\x00\x00\x00\x00\x00\x00U\x15\xf7o\xa7jW\x15v\x00\xadu\xed\xda' + b'\xddv\x99\xe6E\x9a\x00U\x00\x00\x00\x00m\x00W\x00n\x00\x15\x00' + b'\x1b\x00\x05\x00\x00\x00\x00\x00\xaa\x00\xaa\x00\xaa\x00\xaa\x00' + b'\x00\xaa\x00\xaa\x00\xaa\x00\xaaP\x05\x94\x16\xa4\x1b\xe4\x1b' + b'\xe4\x1a\xa4\x1aT\x15\x00\x00P\x00\xd0\x01\xd0\x07\xd4\x19' + b'\xf9\x1d\xbd\x05T\x00\x00\x00T\x05\xf5\x17\xdd\x1d\xdd\x1d' + b'\xf5\x17\xe4\x06T\x05\x00\x00\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16' + b'\x14\x05\x00\x00T\x15\xf5\x1f\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1d' + b'P\x15\x00\x00\x00\x00P\x01\xe4\x06\xf4\x07\xe4\x06P\x01' + b'\x00\x00\x00\x00U\x15\xdd\x1d\xdd\x1d\x99\x19U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01\xd0\x01\x90\x01P\x01\xd0\x01' + b'P\x01\x00\x00T\x05t\x07d\x06T\x05\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x14\x05u\x17\xed\x1et\x07\xed\x1eu\x17\x14\x05\x00\x00' + b'T\x15\xf5\x1b\x99\x05\xf5\x17\x94\x19\xf9\x17U\x05\x00\x00' + b'\x15\x14\x1d\x1dU\x07\xd0\x01t\x15\x1d\x1d\x05\x15\x00\x00' + b'T\x01\xe4\x05u\x07\xdd\x01]\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01' + b'\x90\x01P\x01\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06' + b'\x90\x01\xd0\x01\x90\x01P\x06@\x05\x00\x00T\x00d\x01' + b'\x90\x01\xd0\x01\x90\x01d\x01T\x00\x00\x00\x00\x00\x14\x05' + b't\x07\xd0\x01t\x07\x14\x05\x00\x00\x00\x00P\x01\x90\x01' + b'\xd5\x15\xf9\x1b\xd5\x15\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'\x00\x00P\x01\xd0\x01\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01P\x01\x00\x00\x00\x04\x00\x1d' + b'@\x07\xd0\x01t\x00\x1d\x00\x04\x00\x00\x00T\x05\xe5\x16' + b'Y\x1a\xdd\x1di\x19\xe5\x16T\x05\x00\x00@\x01\xd0\x01' + b'\xe4\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00T\x05\xf9\x17' + b'U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00T\x05\xf5\x17]\x1d\x94\x07' + b']\x1d\xf5\x17T\x05\x00\x00P\x00t\x00]\x05]\x17\xfd\x1fU\x17' + b'@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1bU\x1d\xf9\x1bU\x05\x00\x00' + b'T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1bT\x05\x00\x00U\x15\xfd\x1f' + b'U\x19\xd0\x06d\x01t\x00T\x00\x00\x00T\x05\xf5\x17]\x1d\xf5\x17' + b']\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b]\x1d\xf9\x1fT\x1d\xf9\x17' + b'U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01P\x01' + b'\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01\x90\x01' + b'P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00\xd0\x01@\x07' + b'\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05\xf9\x1bU\x15' + b'\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07\xd0\x01t\x00' + b'\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16P\x05\xd0\x01' + b'P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1bY\x15\xf5\x06' + b'T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d\x15\x15\x00\x00' + b'U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17U\x05\x00\x00T\x05\xf5\x06' + b']\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00U\x01\xbd\x05]\x17\x1d\x1d' + b']\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06]\x01\xfd\x01]\x15\xfd\x1b' + b'U\x15\x00\x00U\x15\xfd\x1b]\x15]\x00\xbd\x01]\x01\x15\x00\x00\x00' + b'T\x15\xf5\x1b]\x05\xdd\x1fY\x1d\xf5\x1bT\x15\x00\x00' + b'\x15\x15\x1d\x1d]\x1d\xfd\x1f]\x1d\x1d\x1d\x15\x15\x00\x00' + b'T\x05\xe4\x06\xd0\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00' + b'\x00\x15\x00\x1d\x00\x1d\x05\x1d]\x19\xf5\x17T\x05\x00\x00' + b'\x15\x14\x1d\x1d]\x07\xfd\x01]\x07\x1d\x1d\x15\x14\x00\x00' + b'\x15\x00\x1d\x00\x1d\x00\x1d\x00]\x15\xfd\x1fU\x15\x00\x00' + b'\x05\x14\x1d\x1dm\x1e\xdd\x1d]\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x05\x15\x1d\x1dm\x1d\xdd\x1d]\x1e\x1d\x1d\x15\x14\x00\x00' + b'T\x01\xb5\x05]\x17\x1d\x1d]\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16' + b']\x19]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' + b']\x1e\xe5\x07T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d' + b'\x15\x15\x00\x00T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17' + b'U\x05\x00\x00U\x15\xf9\x1b\xd5\x15\xd0\x01\xd0\x01\xd0\x01' + b'P\x01\x00\x00\x15\x15\x1d\x1d\x1d\x1d\x19\x1du\x19\xd4\x17' + b'P\x05\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17d\x06\xd0\x01' + b'@\x00\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xd9\x19u\x17' + b'\x14\x05\x00\x00\x05\x14\x1d\x1dt\x07\xd0\x01t\x07\x1d\x1d' + b'\x05\x14\x00\x00\x15\x15\x1d\x1d\x19\x19u\x17\x94\x05\xd0\x01' + b'P\x01\x00\x00U\x15\xf9\x1bU\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00' + b'T\x05\xf4\x06t\x01t\x00t\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00' + b't\x00\xd0\x01@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07' + b'P\x07\xe4\x07T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19' + b'\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' + b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' + b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' + b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' + b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' + b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' + b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' + b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' + b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' + b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' + b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' + b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' + b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' + b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' + b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' + b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' + b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' + b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' + b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' + b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' + b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' + b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' + b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' + b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' + b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' + b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' + b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') + +PALETTE = (b'\xf8\x1f\x00\x00\xcey\xff\xff\xf8\x1f\x00\x19\xfc\xe0\xfd\xe0' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + +def color565(r, g, b): + """Convert 24-bit RGB color to 16-bit.""" + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): + """Return True if the two rectangles intersect.""" + if bx1 is None: + bx1 = bx0 + if by1 is None: + by1 = by0 + return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1) + + +class BMP16: + """Read 16-color BMP files.""" + + def __init__(self, filename): + self.filename = filename + self.colors = 0 + + def read_header(self): + """Read the file's header information.""" + + if self.colors: + return + with open(self.filename, 'rb') as f: + f.seek(10) + self.data = int.from_bytes(f.read(4), 'little') + f.seek(18) + self.width = int.from_bytes(f.read(4), 'little') + self.height = int.from_bytes(f.read(4), 'little') + f.seek(46) + self.colors = int.from_bytes(f.read(4), 'little') + + def read_palette(self): + """Read the color palette information.""" + + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(self.data - self.colors * 4) + for color in range(self.colors): + buffer = f.read(4) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + """Read the image data.""" + line_size = self.width >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + + with open(self.filename, 'rb') as f: + f.seek(self.data) + index = (self.height - 1) * line_size + for line in range(self.height): + chunk = f.read(line_size) + buffer[index:index + line_size] = chunk + index -= line_size + return buffer + + +def read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + dictionary = LZWDict(code_size) + bit = 0 + try: + byte = next(data) + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) + except StopIteration: + return + + +class GIF16: + """Read 16-color GIF files.""" + + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + header = f.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not GIF file") + self.width, self.height, flags, self.background, self.aspect = ( + struct.unpack('<HHBBB', f.read(7))) + self.palette_size = 1 << ((flags & 0x07) + 1) + if not flags & 0x80: + raise NotImplementedError() + if self.palette_size > 16: + raise ValueError("Too many colors (%d/16)." % self.palette_size) + + def read_palette(self): + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(13) + for color in range(self.palette_size): + buffer = f.read(3) + c = color565(buffer[0], buffer[1], buffer[2]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + line_size = (self.width + 1) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + with open(self.filename, 'rb') as f: + f.seek(13 + self.palette_size * 3) + while True: # skip to first frame + block_type = f.read(1)[0] + if block_type == 0x2c: + break + elif block_type == 0x21: # skip extension + extension_type = f.read(1)[0] + while True: + size = f.read(1)[0] + if size == 0: + break + f.seek(1, size) + elif block_type == 0x3b: + raise NotImplementedError() + x, y, w, h, flags = struct.unpack('<HHHHB', f.read(9)) + if (flags & 0x80 or flags & 0x40 or + w != self.width or h != self.height or x != 0 or y != 0): + raise NotImplementedError() + min_code_size = f.read(1)[0] + x = 0 + y = 0 + for decoded in lzw_decode(read_blockstream(f), min_code_size): + for pixel in decoded: + if x & 0x01: + buffer[(x >> 1) + y * line_size] |= pixel + else: + buffer[(x >> 1) + y * line_size] = pixel << 4 + x += 1 + if (x >= self.width): + x = 0 + y += 1 + return buffer + + +class Bank: + """ + Store graphics for the tiles and sprites. + + A single bank stores exactly 16 tiles, each 16x16 pixels in 16 possible + colors, and a 16-color palette. We just like the number 16. + + """ + + def __init__(self, buffer=None, palette=None): + self.buffer = buffer + self.palette = palette + + @classmethod + def from_bmp16(cls, filename): + """Read the bank from a BMP file.""" + return cls.from_image(filename) + + + @classmethod + def from_image(cls, filename): + """Read the bank from an image file.""" + if filename.lower().endswith(".gif"): + image = GIF16(filename) + elif filename.lower().endswith(".bmp"): + image = BMP16(filename) + else: + raise ValueError("Unsupported format") + image.read_header() + if image.width != 16 or image.height != 256: + raise ValueError("Image size not 16x256") + palette = image.read_palette() + buffer = image.read_data() + return cls(buffer, palette) + + +class Grid: + """ + A grid is a layer of tiles that can be displayed on the screen. Each square + can contain any of the 16 tiles from the associated bank. + """ + + def __init__(self, bank, width=8, height=8, palette=None, buffer=None): + self.x = 0 + self.y = 0 + self.z = 0 + self.stride = (width + 1) & 0xfe + self.width = width + self.height = height + self.bank = bank + self.palette = palette or bank.palette + self.buffer = buffer or bytearray((self.stride * height)>>1) + self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, + self.palette, self.buffer) + + def tile(self, x, y, tile=None): + """Get or set what tile is displayed in the given place.""" + + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + index = (y * self.stride + x) >> 1 + b = self.buffer[index] + if tile is None: + return b & 0x0f if x & 0x01 else b >> 4 + if x & 0x01: + b = b & 0xf0 | tile + else: + b = b & 0x0f | (tile << 4) + self.buffer[index] = b + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + +class WallGrid(Grid): + """ + A special grid, shifted from its parents by half a tile, useful for making + nice-looking corners of walls and similar structures. + """ + + def __init__(self, grid, walls, bank, palette=None): + super().__init__(bank, grid.width + 1, grid.height + 1, palette) + self.grid = grid + self.walls = walls + self.update() + self.move(self.x - 8, self.y - 8) + + def update(self): + for y in range(self.height): + for x in range(self.width): + t = 0 + bit = 1 + for dy in (-1, 0): + for dx in (-1, 0): + if self.grid.tile(x + dx, y + dy) in self.walls: + t |= bit + bit <<= 1 + self.tile(x, y, t) + + +class Sprite: + """ + A sprite is a layer containing just a single tile from the associated bank, + that can be positioned anywhere on the screen. + """ + + def __init__(self, bank, frame, x, y, z=0, rotation=0, palette=None): + self.bank = bank + self.palette = palette or bank.palette + self.frame = frame + self.rotation = rotation + self.x = x + self.y = y + self.z = z + self.layer = _stage.Layer(1, 1, self.bank.buffer, self.palette) + self.layer.move(x, y) + self.layer.frame(frame, rotation) + self.px = x + self.py = y + + def move(self, x, y, z=None): + """Move the sprite to the given place.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def set_frame(self, frame=None, rotation=None): + """ + Set the current graphic and rotation of the sprite. + + The possible values for rotation are: 0 - none, 1 - 90 degrees + clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - + mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and + mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ + + if frame is not None: + self.frame = frame + if rotation is not None: + self.rotation = rotation + self.layer.frame(self.frame, self.rotation) + + def update(self): + pass + + +class Text: + """Text layer. For displaying text.""" + + def __init__(self, width, height, font=None, palette=None, buffer=None): + self.width = width + self.height = height + self.font = font or FONT + self.palette = palette or PALETTE + self.buffer = buffer or bytearray(width * height) + self.layer = _stage.Text(width, height, self.font, + self.palette, self.buffer) + self.column = 0 + self.row = 0 + self.x = 0 + self.y = 0 + self.z = 0 + + def char(self, x, y, c=None, hightlight=False): + """Get or set the character at the given location.""" + if not 0 <= x < self.width or not 0 <= y < self.height: + return + if c is None: + return chr(self.buffer[y * self.width + x]) + c = ord(c) + if hightlight: + c |= 0x80 + self.buffer[y * self.width + x] = c + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def cursor(self, x=None, y=None): + """Move the text cursor to the specified row and column.""" + if y is not None: + self.row = min(max(0, y), self.width - 1) + if x is not None: + self.column = min(max(0, x), self.height - 1) + + def text(self, text, hightlight=False): + """ + Display text starting at the current cursor location. + Return the dimensions of the rendered text. + """ + longest = 0 + tallest = 0 + for c in text: + if c != '\n': + self.char(self.column, self.row, c, hightlight) + self.column += 1 + if self.column >= self.width or c == '\n': + longest = max(longest, self.column) + self.column = 0 + self.row += 1 + if self.row >= self.height: + tallest = max(tallest, self.row) + self.row = 0 + longest = max(longest, self.column) + tallest = max(tallest, self.row) + (1 if self.column > 0 else 0) + return longest * 8, tallest * 8 + + def clear(self): + """Clear all text from the layer.""" + for i in range(self.width * self.height): + self.buffer[i] = 0 + + +class Stage: + """ + Represents what is being displayed on the screen. + + The ``display`` parameter is displayio.Display representing an initialized + display connected to the device. + + The ``fps`` specifies the maximum frame rate to be enforced. + + The ``scale`` specifies an optional scaling up of the display, to use + 2x2 or 3x3, etc. pixels. If not specified, it is inferred from the display + size (displays wider than 256 pixels will have scale=2, for example). + """ + buffer = bytearray(512) + + def __init__(self, display, fps=6, scale=None): + if scale is None: + self.scale = max(1, display.width // 128) + else: + self.scale = scale + self.layers = [] + self.display = display + self.width = display.width // self.scale + self.height = display.height // self.scale + self.last_tick = time.monotonic() + self.tick_delay = 1 / fps + self.vx = 0 + self.vy = 0 + + def tick(self): + """Wait for the start of the next frame.""" + self.last_tick += self.tick_delay + wait = max(0, self.last_tick - time.monotonic()) + if wait: + time.sleep(wait) + else: + self.last_tick = time.monotonic() + + def render_block(self, x0=None, y0=None, x1=None, y1=None): + """Update a rectangle of the screen.""" + if x0 is None: + x0 = self.vx + if y0 is None: + y0 = self.vy + if x1 is None: + x1 = self.width + self.vx + if y1 is None: + y1 = self.height + self.vy + x0 = min(max(0, x0 - self.vx), self.width - 1) + y0 = min(max(0, y0 - self.vy), self.height - 1) + x1 = min(max(1, x1 - self.vx), self.width) + y1 = min(max(1, y1 - self.vy), self.height) + if x0 >= x1 or y0 >= y1: + return + layers = [l.layer for l in self.layers] + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) + + def render_sprites(self, sprites): + """Update the spots taken by all the sprites in the list.""" + layers = [l.layer for l in self.layers] + for sprite in sprites: + x = int(sprite.x) - self.vx + y = int(sprite.y) - self.vy + x0 = max(0, min(self.width - 1, min(sprite.px, x))) + y0 = max(0, min(self.height - 1, min(sprite.py, y))) + x1 = max(1, min(self.width, max(sprite.px, x) + 16)) + y1 = max(1, min(self.height, max(sprite.py, y) + 16)) + sprite.px = x + sprite.py = y + if x0 >= x1 or y0 >= y1: + continue + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) diff --git a/circuitpython/frozen/circuitpython-stage/ugame10/stage.py b/circuitpython/frozen/circuitpython-stage/ugame10/stage.py index 2dedc93..44ab69a 120000..100644 --- a/circuitpython/frozen/circuitpython-stage/ugame10/stage.py +++ b/circuitpython/frozen/circuitpython-stage/ugame10/stage.py @@ -1 +1,630 @@ -../stage.py
\ No newline at end of file +import time +import array +import digitalio +import struct + +import _stage + + +FONT = (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'P\x01\xd4\x05\xf5\x17\xed\x1e\xd5\x15\xd0\x01P\x01\x00\x00' + b'P\x01\xd0\x01\xd5\x15\xed\x1e\xf5\x17\xd4\x05P\x01\x00\x00' + b'P\x01\xd0\x05\x95\x17\xfd\x1f\x95\x17\xd0\x05P\x01\x00\x00' + b'P\x01\xd4\x01\xb5\x15\xfd\x1f\xb5\x15\xd4\x01P\x01\x00\x00' + b'T\x05\xf9\x1b\xdd\x1d}\x1f\xd9\x19\xa9\x1aT\x05\x00\x00' + b'T\x05\xf9\x1b]\x1d\xdd\x1dY\x19\xa9\x1aT\x05\x00\x00P\x01\xd0\x01' + b'\xe5\x16\xfd\x1f\xe4\x06t\x07\x14\x05\x00\x00P\x01\xd5\x15' + b']\x1d\x95\x15\xf4\x07\xe4\x06T\x05\x00\x00\x14\x05y\x1b' + b'\xfd\x1f\xf9\x1b\xe4\x06\xd0\x01@\x00\x00\x00P\x01\xf4\x06' + b'\xad\x1b\xed\x1b\xf9\x1a\xa4\x06P\x01\x00\x00@U\xd0\xff' + b'\xf4\xaa\xbdV\xad\x01m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00' + b'm\x00m\x00m\x00m\x00m\x00\xbd\x01\xf9V\xe4\xff\x90\xaa@UUU\xff\xff' + b'\xaa\xaaUU\x00\x00\x00\x00\x00\x00\x00\x00U\x01\xff\x06' + b'\xea\x1b\x95o@n\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m\x00m' + b'\x00m\x00m\x00m\x00m\x00m@o\xd5k\xff\x1a\xaa\x06U\x01' + b'\x00\x00\x00\x00\x00\x00\x00\x00UU\xff\xff\xaa\xaaUU' + b'\x00\x00\x00\x00\x00UE\xfe\xd9\xef\xdd\x9f\xad\x9f\xad\x9a' + b'\x00\x00\x00\x00\x00\x00U\x15\xf7o\xa7jW\x15v\x00\xadu\xed\xda' + b'\xddv\x99\xe6E\x9a\x00U\x00\x00\x00\x00m\x00W\x00n\x00\x15\x00' + b'\x1b\x00\x05\x00\x00\x00\x00\x00\xaa\x00\xaa\x00\xaa\x00\xaa\x00' + b'\x00\xaa\x00\xaa\x00\xaa\x00\xaaP\x05\x94\x16\xa4\x1b\xe4\x1b' + b'\xe4\x1a\xa4\x1aT\x15\x00\x00P\x00\xd0\x01\xd0\x07\xd4\x19' + b'\xf9\x1d\xbd\x05T\x00\x00\x00T\x05\xf5\x17\xdd\x1d\xdd\x1d' + b'\xf5\x17\xe4\x06T\x05\x00\x00\x14\x05e\x16y\x1b\xd4\x05y\x1be\x16' + b'\x14\x05\x00\x00T\x15\xf5\x1f\x9d\x19\xf5\x1d\xd4\x1d\xd0\x1d' + b'P\x15\x00\x00\x00\x00P\x01\xe4\x06\xf4\x07\xe4\x06P\x01' + b'\x00\x00\x00\x00U\x15\xdd\x1d\xdd\x1d\x99\x19U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\x15\xdd\x1d' + b'U\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01\xd0\x01\x90\x01P\x01\xd0\x01' + b'P\x01\x00\x00T\x05t\x07d\x06T\x05\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x14\x05u\x17\xed\x1et\x07\xed\x1eu\x17\x14\x05\x00\x00' + b'T\x15\xf5\x1b\x99\x05\xf5\x17\x94\x19\xf9\x17U\x05\x00\x00' + b'\x15\x14\x1d\x1dU\x07\xd0\x01t\x15\x1d\x1d\x05\x15\x00\x00' + b'T\x01\xe4\x05u\x07\xdd\x01]\x17\xe5\x1dT\x14\x00\x00P\x01\xd0\x01' + b'\x90\x01P\x01\x00\x00\x00\x00\x00\x00\x00\x00@\x05P\x06' + b'\x90\x01\xd0\x01\x90\x01P\x06@\x05\x00\x00T\x00d\x01' + b'\x90\x01\xd0\x01\x90\x01d\x01T\x00\x00\x00\x00\x00\x14\x05' + b't\x07\xd0\x01t\x07\x14\x05\x00\x00\x00\x00P\x01\x90\x01' + b'\xd5\x15\xf9\x1b\xd5\x15\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'\x00\x00P\x01\xd0\x01\x90\x01P\x01\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00P\x01\xd0\x01P\x01\x00\x00\x00\x04\x00\x1d' + b'@\x07\xd0\x01t\x00\x1d\x00\x04\x00\x00\x00T\x05\xe5\x16' + b'Y\x1a\xdd\x1di\x19\xe5\x16T\x05\x00\x00@\x01\xd0\x01' + b'\xe4\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00T\x05\xf9\x17' + b'U\x1d\xf4\x17Y\x05\xfd\x1fU\x15\x00\x00T\x05\xf5\x17]\x1d\x94\x07' + b']\x1d\xf5\x17T\x05\x00\x00P\x00t\x00]\x05]\x17\xfd\x1fU\x17' + b'@\x05\x00\x00U\x15\xfd\x1b]\x05\xfd\x1bU\x1d\xf9\x1bU\x05\x00\x00' + b'T\x15\xf5\x1b]\x05\xfd\x1b]\x1d\xf9\x1bT\x05\x00\x00U\x15\xfd\x1f' + b'U\x19\xd0\x06d\x01t\x00T\x00\x00\x00T\x05\xf5\x17]\x1d\xf5\x17' + b']\x1d\xf5\x17T\x05\x00\x00T\x05\xf9\x1b]\x1d\xf9\x1fT\x1d\xf9\x17' + b'U\x05\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01P\x01' + b'\x00\x00\x00\x00\x00\x00P\x01\xd0\x01P\x01\xd0\x01\x90\x01' + b'P\x01\x00\x00\x00\x05@\x07\xd0\x01t\x00\xd0\x01@\x07' + b'\x00\x05\x00\x00\x00\x00U\x15\xf9\x1bT\x05\xf9\x1bU\x15' + b'\x00\x00\x00\x00\x14\x00t\x00\xd0\x01@\x07\xd0\x01t\x00' + b'\x14\x00\x00\x00T\x05\xe5\x17]\x1d\xd5\x16P\x05\xd0\x01' + b'P\x01\x00\x00T\x05\xb5\x17\xdd\x1d\x9d\x1bY\x15\xf5\x06' + b'T\x05\x00\x00P\x00\xe4\x01Y\x07]\x1d\xed\x1e]\x1d\x15\x15\x00\x00' + b'U\x01\xfd\x05]\x07\xed\x16]\x1d\xfd\x17U\x05\x00\x00T\x05\xf5\x06' + b']\x01\x1d\x14]\x1d\xf5\x17T\x05\x00\x00U\x01\xbd\x05]\x17\x1d\x1d' + b']\x1d\xfd\x16U\x05\x00\x00U\x05\xfd\x06]\x01\xfd\x01]\x15\xfd\x1b' + b'U\x15\x00\x00U\x15\xfd\x1b]\x15]\x00\xbd\x01]\x01\x15\x00\x00\x00' + b'T\x15\xf5\x1b]\x05\xdd\x1fY\x1d\xf5\x1bT\x15\x00\x00' + b'\x15\x15\x1d\x1d]\x1d\xfd\x1f]\x1d\x1d\x1d\x15\x15\x00\x00' + b'T\x05\xe4\x06\xd0\x01\xd0\x01\xd0\x01\xe4\x06T\x05\x00\x00' + b'\x00\x15\x00\x1d\x00\x1d\x05\x1d]\x19\xf5\x17T\x05\x00\x00' + b'\x15\x14\x1d\x1d]\x07\xfd\x01]\x07\x1d\x1d\x15\x14\x00\x00' + b'\x15\x00\x1d\x00\x1d\x00\x1d\x00]\x15\xfd\x1fU\x15\x00\x00' + b'\x05\x14\x1d\x1dm\x1e\xdd\x1d]\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x05\x15\x1d\x1dm\x1d\xdd\x1d]\x1e\x1d\x1d\x15\x14\x00\x00' + b'T\x01\xb5\x05]\x17\x1d\x1d]\x1d\xe5\x17T\x05\x00\x00U\x05\xfd\x16' + b']\x19]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x01\xb5\x05]\x17\x1d\x1d' + b']\x1e\xe5\x07T\x1d\x00\x15U\x05\xfd\x16]\x19]\x1d\xfd\x07]\x1d' + b'\x15\x15\x00\x00T\x05\xf5\x07]\x01\xe5\x06T\x1d\xf9\x17' + b'U\x05\x00\x00U\x15\xf9\x1b\xd5\x15\xd0\x01\xd0\x01\xd0\x01' + b'P\x01\x00\x00\x15\x15\x1d\x1d\x1d\x1d\x19\x1du\x19\xd4\x17' + b'P\x05\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17d\x06\xd0\x01' + b'@\x00\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xd9\x19u\x17' + b'\x14\x05\x00\x00\x05\x14\x1d\x1dt\x07\xd0\x01t\x07\x1d\x1d' + b'\x05\x14\x00\x00\x15\x15\x1d\x1d\x19\x19u\x17\x94\x05\xd0\x01' + b'P\x01\x00\x00U\x15\xf9\x1bU\x07\xd0\x01t\x15\xf9\x1bU\x15\x00\x00' + b'T\x05\xf4\x06t\x01t\x00t\x01\xf4\x06T\x05\x00\x00\x05\x00\x1d\x00' + b't\x00\xd0\x01@\x07\x00\x1d\x00\x14\x00\x00T\x05\xe4\x07P\x07@\x07' + b'P\x07\xe4\x07T\x05\x00\x00@\x00\xd0\x01t\x07\x19\x19' + b'\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'U\x15\xf9\x1bU\x15\x00\x00P\x00\xb4\x01\xd4\x06P\x07@\x01\x00\x00' + b'\x00\x00\x00\x00\x00\x00T\x15\xe5\x1f]\x1d]\x1d\xf5\x1f' + b'T\x15\x00\x00\x15\x00]\x05\xfd\x16]\x1d]\x1d\xfd\x17U\x05\x00\x00' + b'\x00\x00T\x05\xe5\x07]\x05]\x1d\xf5\x16T\x05\x00\x00\x00\x15T\x1d' + b'\xe5\x1f]\x1d]\x1d\xf5\x1fT\x15\x00\x00\x00\x00T\x05' + b'\xf5\x17\xad\x1e]\x15\xf5\x07T\x05\x00\x00@\x15P\x1e' + b'\xd4\x15\xf4\x07\xd4\x05\xd0\x01\xd0\x01P\x01\x00\x00T\x15' + b'\xe5\x1f]\x1d\xf5\x1fT\x1d\xf9\x16U\x05\x15\x00]\x05\xfd\x16]\x1d' + b'\x1d\x1d\x1d\x1d\x15\x15\x00\x00P\x01\xd0\x01P\x01\xd0\x01' + b'\xd0\x01\xd0\x01P\x01\x00\x00@\x05@\x07@\x05@\x07E\x07]\x07' + b'\xe5\x05T\x01\x15\x00\x1d\x14]\x1d\xfd\x06]\x19\x1d\x1d' + b'\x15\x14\x00\x00T\x00t\x00t\x00t\x00d\x05\xd4\x07P\x05\x00\x00' + b'\x00\x00U\x05\xfd\x17\xdd\x19\xdd\x1d]\x1d\x15\x15\x00\x00' + b'\x00\x00U\x05\xfd\x17]\x19\x1d\x1d\x1d\x1d\x15\x15\x00\x00' + b'\x00\x00T\x05\xe5\x17]\x1d]\x1d\xf5\x17T\x05\x00\x00\x00\x00U\x05' + b'\xfd\x17]\x1d]\x1d\xfd\x17]\x05\x15\x00\x00\x00T\x15\xf5\x1f]\x1d' + b']\x1d\xf5\x1fT\x1d\x00\x15\x00\x00U\x05\xdd\x16}\x1d]\x04\x1d\x00' + b'\x15\x00\x00\x00\x00\x00T\x15\xe5\x1f\xad\x05\x94\x1e\xfd\x16' + b'U\x05\x00\x00T\x00u\x05\xfd\x07t\x01t\x01\xd4\x07P\x05\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d\x1d\x1d]\x1d\xe5\x1fT\x15\x00\x00' + b'\x00\x00\x05\x14\x1d\x1d\x19\x19u\x17\xd4\x05P\x01\x00\x00' + b'\x00\x00\x15\x15]\x1d\xdd\x1d\xd9\x19u\x17T\x05\x00\x00' + b'\x00\x00\x15\x15m\x1e\xd4\x05\xd4\x05m\x1e\x15\x15\x00\x00' + b'\x00\x00\x15\x15\x1d\x1d]\x1d\xe5\x1fT\x1d\xfd\x17U\x05' + b'\x00\x00U\x15\xfd\x1f\xa4\x15\x95\x06\xfd\x1fU\x15\x00\x00' + b'@\x05\x90\x07\xd0\x01t\x01\xd0\x01\x90\x07@\x05\x00\x00' + b'P\x01\x90\x01\xd0\x01\xd0\x01\xd0\x01\x90\x01P\x01\x00\x00' + b'T\x00\xb4\x01\xd0\x01P\x07\xd0\x01\xb4\x01T\x00\x00\x00' + b'\x00\x00T\x00u\x15\xd9\x19U\x17@\x05\x00\x00\x00\x00U\x15\xfd\x1f' + b'\xed\x1e\xbd\x1f\xed\x1e\xfd\x1fU\x15\x00\x00') + +PALETTE = (b'\xf8\x1f\x00\x00\xcey\xff\xff\xf8\x1f\x00\x19\xfc\xe0\xfd\xe0' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + +def color565(r, g, b): + """Convert 24-bit RGB color to 16-bit.""" + return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 + + +def collide(ax0, ay0, ax1, ay1, bx0, by0, bx1=None, by1=None): + """Return True if the two rectangles intersect.""" + if bx1 is None: + bx1 = bx0 + if by1 is None: + by1 = by0 + return not (ax1 < bx0 or ay1 < by0 or ax0 > bx1 or ay0 > by1) + + +class BMP16: + """Read 16-color BMP files.""" + + def __init__(self, filename): + self.filename = filename + self.colors = 0 + + def read_header(self): + """Read the file's header information.""" + + if self.colors: + return + with open(self.filename, 'rb') as f: + f.seek(10) + self.data = int.from_bytes(f.read(4), 'little') + f.seek(18) + self.width = int.from_bytes(f.read(4), 'little') + self.height = int.from_bytes(f.read(4), 'little') + f.seek(46) + self.colors = int.from_bytes(f.read(4), 'little') + + def read_palette(self): + """Read the color palette information.""" + + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(self.data - self.colors * 4) + for color in range(self.colors): + buffer = f.read(4) + c = color565(buffer[2], buffer[1], buffer[0]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + """Read the image data.""" + line_size = self.width >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + + with open(self.filename, 'rb') as f: + f.seek(self.data) + index = (self.height - 1) * line_size + for line in range(self.height): + chunk = f.read(line_size) + buffer[index:index + line_size] = chunk + index -= line_size + return buffer + + +def read_blockstream(f): + while True: + size = f.read(1)[0] + if size == 0: + break + for i in range(size): + yield f.read(1)[0] + + +class EndOfData(Exception): + pass + + +class LZWDict: + def __init__(self, code_size): + self.code_size = code_size + self.clear_code = 1 << code_size + self.end_code = self.clear_code + 1 + self.codes = [] + self.clear() + + def clear(self): + self.last = b'' + self.code_len = self.code_size + 1 + self.codes[:] = [] + + def decode(self, code): + if code == self.clear_code: + self.clear() + return b'' + elif code == self.end_code: + raise EndOfData() + elif code < self.clear_code: + value = bytes([code]) + elif code <= len(self.codes) + self.end_code: + value = self.codes[code - self.end_code - 1] + else: + value = self.last + self.last[0:1] + if self.last: + self.codes.append(self.last + value[0:1]) + if (len(self.codes) + self.end_code + 1 >= 1 << self.code_len and + self.code_len < 12): + self.code_len += 1 + self.last = value + return value + + +def lzw_decode(data, code_size): + dictionary = LZWDict(code_size) + bit = 0 + try: + byte = next(data) + try: + while True: + code = 0 + for i in range(dictionary.code_len): + code |= ((byte >> bit) & 0x01) << i + bit += 1 + if bit >= 8: + bit = 0 + byte = next(data) + yield dictionary.decode(code) + except EndOfData: + while True: + next(data) + except StopIteration: + return + + +class GIF16: + """Read 16-color GIF files.""" + + def __init__(self, filename): + self.filename = filename + + def read_header(self): + with open(self.filename, 'rb') as f: + header = f.read(6) + if header not in {b'GIF87a', b'GIF89a'}: + raise ValueError("Not GIF file") + self.width, self.height, flags, self.background, self.aspect = ( + struct.unpack('<HHBBB', f.read(7))) + self.palette_size = 1 << ((flags & 0x07) + 1) + if not flags & 0x80: + raise NotImplementedError() + if self.palette_size > 16: + raise ValueError("Too many colors (%d/16)." % self.palette_size) + + def read_palette(self): + palette = array.array('H', (0 for i in range(16))) + with open(self.filename, 'rb') as f: + f.seek(13) + for color in range(self.palette_size): + buffer = f.read(3) + c = color565(buffer[0], buffer[1], buffer[2]) + palette[color] = ((c << 8) | (c >> 8)) & 0xffff + return palette + + def read_data(self, buffer=None): + line_size = (self.width + 1) >> 1 + if buffer is None: + buffer = bytearray(line_size * self.height) + with open(self.filename, 'rb') as f: + f.seek(13 + self.palette_size * 3) + while True: # skip to first frame + block_type = f.read(1)[0] + if block_type == 0x2c: + break + elif block_type == 0x21: # skip extension + extension_type = f.read(1)[0] + while True: + size = f.read(1)[0] + if size == 0: + break + f.seek(1, size) + elif block_type == 0x3b: + raise NotImplementedError() + x, y, w, h, flags = struct.unpack('<HHHHB', f.read(9)) + if (flags & 0x80 or flags & 0x40 or + w != self.width or h != self.height or x != 0 or y != 0): + raise NotImplementedError() + min_code_size = f.read(1)[0] + x = 0 + y = 0 + for decoded in lzw_decode(read_blockstream(f), min_code_size): + for pixel in decoded: + if x & 0x01: + buffer[(x >> 1) + y * line_size] |= pixel + else: + buffer[(x >> 1) + y * line_size] = pixel << 4 + x += 1 + if (x >= self.width): + x = 0 + y += 1 + return buffer + + +class Bank: + """ + Store graphics for the tiles and sprites. + + A single bank stores exactly 16 tiles, each 16x16 pixels in 16 possible + colors, and a 16-color palette. We just like the number 16. + + """ + + def __init__(self, buffer=None, palette=None): + self.buffer = buffer + self.palette = palette + + @classmethod + def from_bmp16(cls, filename): + """Read the bank from a BMP file.""" + return cls.from_image(filename) + + + @classmethod + def from_image(cls, filename): + """Read the bank from an image file.""" + if filename.lower().endswith(".gif"): + image = GIF16(filename) + elif filename.lower().endswith(".bmp"): + image = BMP16(filename) + else: + raise ValueError("Unsupported format") + image.read_header() + if image.width != 16 or image.height != 256: + raise ValueError("Image size not 16x256") + palette = image.read_palette() + buffer = image.read_data() + return cls(buffer, palette) + + +class Grid: + """ + A grid is a layer of tiles that can be displayed on the screen. Each square + can contain any of the 16 tiles from the associated bank. + """ + + def __init__(self, bank, width=8, height=8, palette=None, buffer=None): + self.x = 0 + self.y = 0 + self.z = 0 + self.stride = (width + 1) & 0xfe + self.width = width + self.height = height + self.bank = bank + self.palette = palette or bank.palette + self.buffer = buffer or bytearray((self.stride * height)>>1) + self.layer = _stage.Layer(self.stride, self.height, self.bank.buffer, + self.palette, self.buffer) + + def tile(self, x, y, tile=None): + """Get or set what tile is displayed in the given place.""" + + if not 0 <= x < self.width or not 0 <= y < self.height: + return 0 + index = (y * self.stride + x) >> 1 + b = self.buffer[index] + if tile is None: + return b & 0x0f if x & 0x01 else b >> 4 + if x & 0x01: + b = b & 0xf0 | tile + else: + b = b & 0x0f | (tile << 4) + self.buffer[index] = b + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + +class WallGrid(Grid): + """ + A special grid, shifted from its parents by half a tile, useful for making + nice-looking corners of walls and similar structures. + """ + + def __init__(self, grid, walls, bank, palette=None): + super().__init__(bank, grid.width + 1, grid.height + 1, palette) + self.grid = grid + self.walls = walls + self.update() + self.move(self.x - 8, self.y - 8) + + def update(self): + for y in range(self.height): + for x in range(self.width): + t = 0 + bit = 1 + for dy in (-1, 0): + for dx in (-1, 0): + if self.grid.tile(x + dx, y + dy) in self.walls: + t |= bit + bit <<= 1 + self.tile(x, y, t) + + +class Sprite: + """ + A sprite is a layer containing just a single tile from the associated bank, + that can be positioned anywhere on the screen. + """ + + def __init__(self, bank, frame, x, y, z=0, rotation=0, palette=None): + self.bank = bank + self.palette = palette or bank.palette + self.frame = frame + self.rotation = rotation + self.x = x + self.y = y + self.z = z + self.layer = _stage.Layer(1, 1, self.bank.buffer, self.palette) + self.layer.move(x, y) + self.layer.frame(frame, rotation) + self.px = x + self.py = y + + def move(self, x, y, z=None): + """Move the sprite to the given place.""" + + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def set_frame(self, frame=None, rotation=None): + """ + Set the current graphic and rotation of the sprite. + + The possible values for rotation are: 0 - none, 1 - 90 degrees + clockwise, 2 - 180 degrees, 3 - 90 degrees counter-clockwise, 4 - + mirrored, 5 - 90 degrees clockwise and mirrored, 6 - 180 degrees and + mirrored, 7 - 90 degrees counter-clockwise and mirrored. """ + + if frame is not None: + self.frame = frame + if rotation is not None: + self.rotation = rotation + self.layer.frame(self.frame, self.rotation) + + def update(self): + pass + + +class Text: + """Text layer. For displaying text.""" + + def __init__(self, width, height, font=None, palette=None, buffer=None): + self.width = width + self.height = height + self.font = font or FONT + self.palette = palette or PALETTE + self.buffer = buffer or bytearray(width * height) + self.layer = _stage.Text(width, height, self.font, + self.palette, self.buffer) + self.column = 0 + self.row = 0 + self.x = 0 + self.y = 0 + self.z = 0 + + def char(self, x, y, c=None, hightlight=False): + """Get or set the character at the given location.""" + if not 0 <= x < self.width or not 0 <= y < self.height: + return + if c is None: + return chr(self.buffer[y * self.width + x]) + c = ord(c) + if hightlight: + c |= 0x80 + self.buffer[y * self.width + x] = c + + def move(self, x, y, z=None): + """Shift the whole layer respective to the screen.""" + self.x = x + self.y = y + if z is not None: + self.z = z + self.layer.move(int(x), int(y)) + + def cursor(self, x=None, y=None): + """Move the text cursor to the specified row and column.""" + if y is not None: + self.row = min(max(0, y), self.width - 1) + if x is not None: + self.column = min(max(0, x), self.height - 1) + + def text(self, text, hightlight=False): + """ + Display text starting at the current cursor location. + Return the dimensions of the rendered text. + """ + longest = 0 + tallest = 0 + for c in text: + if c != '\n': + self.char(self.column, self.row, c, hightlight) + self.column += 1 + if self.column >= self.width or c == '\n': + longest = max(longest, self.column) + self.column = 0 + self.row += 1 + if self.row >= self.height: + tallest = max(tallest, self.row) + self.row = 0 + longest = max(longest, self.column) + tallest = max(tallest, self.row) + (1 if self.column > 0 else 0) + return longest * 8, tallest * 8 + + def clear(self): + """Clear all text from the layer.""" + for i in range(self.width * self.height): + self.buffer[i] = 0 + + +class Stage: + """ + Represents what is being displayed on the screen. + + The ``display`` parameter is displayio.Display representing an initialized + display connected to the device. + + The ``fps`` specifies the maximum frame rate to be enforced. + + The ``scale`` specifies an optional scaling up of the display, to use + 2x2 or 3x3, etc. pixels. If not specified, it is inferred from the display + size (displays wider than 256 pixels will have scale=2, for example). + """ + buffer = bytearray(512) + + def __init__(self, display, fps=6, scale=None): + if scale is None: + self.scale = max(1, display.width // 128) + else: + self.scale = scale + self.layers = [] + self.display = display + self.width = display.width // self.scale + self.height = display.height // self.scale + self.last_tick = time.monotonic() + self.tick_delay = 1 / fps + self.vx = 0 + self.vy = 0 + + def tick(self): + """Wait for the start of the next frame.""" + self.last_tick += self.tick_delay + wait = max(0, self.last_tick - time.monotonic()) + if wait: + time.sleep(wait) + else: + self.last_tick = time.monotonic() + + def render_block(self, x0=None, y0=None, x1=None, y1=None): + """Update a rectangle of the screen.""" + if x0 is None: + x0 = self.vx + if y0 is None: + y0 = self.vy + if x1 is None: + x1 = self.width + self.vx + if y1 is None: + y1 = self.height + self.vy + x0 = min(max(0, x0 - self.vx), self.width - 1) + y0 = min(max(0, y0 - self.vy), self.height - 1) + x1 = min(max(1, x1 - self.vx), self.width) + y1 = min(max(1, y1 - self.vy), self.height) + if x0 >= x1 or y0 >= y1: + return + layers = [l.layer for l in self.layers] + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) + + def render_sprites(self, sprites): + """Update the spots taken by all the sprites in the list.""" + layers = [l.layer for l in self.layers] + for sprite in sprites: + x = int(sprite.x) - self.vx + y = int(sprite.y) - self.vy + x0 = max(0, min(self.width - 1, min(sprite.px, x))) + y0 = max(0, min(self.height - 1, min(sprite.py, y))) + x1 = max(1, min(self.width, max(sprite.px, x) + 16)) + y1 = max(1, min(self.height, max(sprite.py, y) + 16)) + sprite.px = x + sprite.py = y + if x0 >= x1 or y0 >= y1: + continue + _stage.render(x0, y0, x1, y1, layers, self.buffer, + self.display, self.scale, self.vx, self.vy) |