import time
import array
import struct
try:
import zlib
except ImportError:
pass
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')
[docs]
def color565(r, g, b):
"""Convert 24-bit RGB color to 16-bit."""
return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3
[docs]
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)
[docs]
class BMP16:
"""Read 16-color BMP files."""
def __init__(self, filename):
self.filename = filename
self.colors = 0
def read_header(self):
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, palette=None):
if palette is None:
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):
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(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
[docs]
class PNG16:
"""Read 16-color PNG files."""
def __init__(self, filename):
self.filename = filename
def read_header(self):
with open(self.filename, 'rb') as f:
magic = f.read(8)
assert magic == b'\x89PNG\r\n\x1a\n'
(
size, chunk, self.width, self.height, self.depth, self.mode,
self.compression, self.filters, self.interlaced, crc
) = struct.unpack(">I4sIIBBBBB4s", f.read(25))
assert size == 13 # header length
assert chunk == b'IHDR'
if self.depth not in {4, 8} or self.mode != 3 or self.interlaced != 0:
raise ValueError("16-color non-interaced PNG expected")
def read_palette(self, palette=None):
if palette is None:
palette = array.array('H', (0 for i in range(16)))
with open(self.filename, 'rb') as f:
f.seek(8 + 25)
while True:
size, chunk = struct.unpack(">I4s", f.read(8))
if chunk == b'PLTE':
break
f.seek(size + 4, 1)
colors = size // 3
if colors > 16:
raise ValueError("16-color PNG expected")
for color in range(colors):
c = color565(*struct.unpack("BBB", f.read(3)))
palette[color] = ((c << 8) | (c >> 8)) & 0xffff
return palette
def read_data(self, buffer=None):
data = bytearray()
with open(self.filename, 'rb') as f:
f.seek(8 + 25)
while True:
size, chunk = struct.unpack(">I4s", f.read(8))
if chunk == b'IEND':
break
elif chunk != b'IDAT':
f.seek(size + 4, 1)
continue
data.extend(f.read(size))
f.seek(4, 1) # skip CRC
data = zlib.decompress(data)
line_size = (self.width + 1) >> 1
if buffer is None:
buffer = bytearray(line_size * self.height)
if self.depth == 4:
for line in range(self.height):
a = line * line_size
b = line * (line_size + 1)
assert data[b] == 0 # no filter
buffer[a:a + line_size] = data[b + 1:b + 1 + line_size]
elif self.depth == 8:
for line in range(self.height):
a = line * line_size
b = line * (self.width + 1)
assert data[b] == 0 # no filter
b += 1
for col in range(line_size):
buffer[a] = (data[b] & 0x0f) << 4
b += 1
try:
buffer[a] |= data[b] & 0x0f
except IndexError:
pass
b += 1
a += 1
return buffer
[docs]
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
[docs]
@classmethod
def from_bmp16(cls, filename):
"""Read the bank from a BMP file."""
return cls.from_image(filename)
[docs]
@classmethod
def from_image(cls, filename):
"""Read the bank from an image file."""
if filename.lower().endswith(".bmp"):
image = BMP16(filename)
elif filename.lower().endswith(".png"):
image = PNG16(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)
[docs]
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)
[docs]
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
[docs]
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))
[docs]
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)
[docs]
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
[docs]
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))
[docs]
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
[docs]
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
[docs]
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
[docs]
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))
[docs]
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)
[docs]
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
[docs]
def clear(self):
"""Clear all text from the layer."""
for i in range(self.width * self.height):
self.buffer[i] = 0
[docs]
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
display.root_group = None
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
[docs]
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()
[docs]
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)
[docs]
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)