# Draws the art for demos/insertCoin: every picture is made here, so it is ours and can be drawn # again. The pixel sprites are written out as rows of characters, a character a pixel, the way they # were drawn on graph paper for the machines that first showed them; the demo scales them up with # nearest-pixel stretching, so they stay square. # # Usage: python3 util/insertCoinArt.py import math import os import numpy as np from PIL import Image, ImageDraw REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT = os.path.join(REPO, "demos", "insertCoin", "art") # Colours by character for the pixel sprites; "." is transparent. INK = { "W": (255, 255, 255), "K": (20, 20, 30), "R": (230, 40, 40), "r": (140, 20, 30), "O": (255, 150, 30), "Y": (255, 230, 60), "y": (180, 140, 20), "G": (60, 220, 90), "g": (20, 120, 50), "B": (60, 140, 255), "b": (30, 60, 160), "C": (80, 230, 255), "M": (230, 60, 220), "P": (150, 70, 230), "S": (200, 200, 210), "s": (120, 120, 135), "T": (240, 190, 140), "t": (170, 110, 70), } def save(image, name): image.save(os.path.join(OUT, name)) print("%-16s %d x %d" % (name, image.width, image.height)) def pixels(rows): h = len(rows) w = max(len(r) for r in rows) img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) for y, row in enumerate(rows): for x, c in enumerate(row): if c in INK: img.putpixel((x, y), INK[c] + (255,)) return img # Frames side by side in one sheet, for spriteLoadFrames-free drawing with spriteDrawGrid. def sheet(frames): w = max(f.width for f in frames) h = max(f.height for f in frames) out = Image.new("RGBA", (w * len(frames), h), (0, 0, 0, 0)) for i, f in enumerate(frames): out.paste(f, (i * w, 0)) return out # Two kinds of alien, two frames each, in white so the demo can tint them: a domed jelly whose # tentacles sway, and a diamond-backed bug whose legs step. INVADER_A = [[ "...WWWWWW...", ".WWWWWWWWWW.", "WW.WW..WW.WW", "WWWWWWWWWWWW", "..WW.WW.WW..", ".W..W..W..W.", "W..W....W..W", "............", ], [ "...WWWWWW...", ".WWWWWWWWWW.", "WW.WW..WW.WW", "WWWWWWWWWWWW", "..WW.WW.WW..", "..W..W..W...", ".W..W..W..W.", "............", ]] INVADER_B = [[ ".....WW.....", "....WWWW....", "...WW..WW...", "..WWWWWWWW..", ".WW.WWWW.WW.", "WW..W..W..WW", "W..W....W..W", "..W......W..", ], [ ".....WW.....", "....WWWW....", "...WW..WW...", "..WWWWWWWW..", ".WW.WWWW.WW.", "WW..W..W..WW", ".W.W....W.W.", "W..........W", ]] CANNON = [ "......W......", ".....WWW.....", "....WWWWW....", ".WWWWWWWWWWW.", "WWWWWWWWWWWWW", "WW.WWWWWWW.WW", "WWWWWWWWWWWWW", ".WWW.....WWW.", ] # The runner: a kid with green spiky hair and a purple jacket, four frames of a run and one of a # jump. RUNNER = [[ "....G.G.G.......", "...GGGGGGGG.....", "...GGTTKTT......", "...GTTTTTTTTT...", "...GTTTTTTTTTT..", "...GTTTTTTTT....", ".....TTTTTTT....", "....PPBPPP......", "...PPPBPPBPPP...", "..PPPPBBBBPPPP..", "..TTPBWBBWBPTT..", "..TTTBBBBBBTTT..", "..TTBBBBBBBBTT..", "....BBB..BBB....", "...WWW....WWW...", "..WWWW....WWWW..", ], [ "....G.G.G.......", "...GGGGGGGG.....", "...GGTTKTT......", "...GTTTTTTTTT...", "...GTTTTTTTTTT..", "...GTTTTTTTT....", ".....TTTTTTT....", "....PPPPBPP.....", "...PPPPBBPPPT...", "..TPPPPBWBBBTT..", "..TTBBBBBBBBT...", "...BBBBBBBBB....", "..BBBBBBBBB.....", ".WBBB...BBB.....", ".WW......WWW....", "..........WWW...", ], [ "....G.G.G.......", "...GGGGGGGG.....", "...GGTTKTT......", "...GTTTTTTTTT...", "...GTTTTTTTTTT..", "...GTTTTTTTT....", ".....TTTTTTT....", ".....PPBPP......", "....PPPBBPP.....", "....PPPBWBB.....", "....TTBBBBB.....", ".....TBBBBB.....", ".....BBBBB......", "......BBB.......", "......WWW.......", "......WWWW......", ], [ "....G.G.G.......", "...GGGGGGGG.....", "...GGTTKTT......", "...GTTTTTTTTT...", "...GTTTTTTTTTT..", "...GTTTTTTTT....", ".....TTTTTTT....", "....PPBPPP......", "...PPPBBPPPP....", "...PPPBWBBPPT...", "...TTBBBBBBTT...", "....BBBBBBBB....", "...BBBB.BBBB....", "..WWBB...BBWW...", "..WWW.....WWW...", "................", ], [ "....G.G.G....TT.", "...GGGGGGGG..TT.", "...GGTTKTT...PP.", "...GTTTTTTTTPPP.", "...GTTTTTTTTPP..", "...GTTTTTTTPP...", ".....TTTTTTPP...", "..PPPPBPPPP.....", ".PPPPPBBPPP.....", "TTPPPBWBBB......", "TT.BBBBBBBB.....", "...BBBBBBBBB....", "..BBBB..BBBB....", "..BBB....BBB....", ".WWW......WWW...", "WWW........WWW..", ]] COIN = [[ "..YYYY..", ".YyyyyY.", "YyYYYYyY", "YyYyyYyY", "YyYyyYyY", "YyYYYYyY", ".YyyyyY.", "..YYYY..", ], [ "...YY...", "..YyyY..", "..YYyY..", "..YyyY..", "..YyyY..", "..YYyY..", "..YyyY..", "...YY...", ], [ "...YY...", "...yY...", "...yY...", "...yY...", "...yY...", "...yY...", "...yY...", "...YY...", ], [ "...YY...", "..YyyY..", "..YyYY..", "..YyyY..", "..YyyY..", "..YyYY..", "..YyyY..", "...YY...", ]] SHIP = [ ".......WW.......", "......WCCW......", "......WCCW......", ".....WSCCSW.....", ".....WSSSSW.....", "....WSSBBSSW....", "R..WSSBCCBSSW..R", "R.WSSSBCCBSSSW.R", "WWSSSSSBBSSSSSWW", "WSSSSSSSSSSSSSSW", "WSSsSSSSSSSSsSSW", ".WsssSSSSSSsssW.", "..W.OO....OO.W..", ".....OO..OO.....", "......O..O......", "................", ] KART = [ "....KKKKKKKK....", "...KRRRRRRRRK...", "..KRRWWWWWWRRK..", "..KRRRRRRRRRRK..", ".KKKKKRRRRKKKKK.", "KKKKKRRRRRRKKKKK", "KsKKRRRRRRRRKKsK", "KsKKKKKKKKKKKKsK", "KKK.SSSSSSSS.KKK", "KKK.........KKKK", ] # The boss, drawn larger and smoothly: a core, an arm, and a turret, each turned by the demo. def bossCore(size=96): y, x = np.mgrid[0:size, 0:size] + 0.5 c = size / 2 d = np.sqrt((x - c) ** 2 + (y - c) ** 2) / c a = np.arctan2(y - c, x - c) img = np.zeros((size, size, 4)) shell = (d < 1) & (d > 0.55) ring = 0.5 + 0.5 * np.cos(a * 8) img[shell, :3] = (np.array([120, 30, 160]) * (0.6 + 0.4 * ring[shell])[:, None]) + np.array([40, 10, 40]) * (1 - d[shell])[:, None] eye = d <= 0.55 glow = np.clip(1 - d / 0.55, 0, 1) img[eye, :3] = np.array([255, 60, 40]) * (0.4 + 0.6 * glow[eye])[:, None] + np.array([255, 220, 120]) * (glow[eye] ** 4)[:, None] pupil = d < 0.18 img[pupil, :3] = [20, 0, 0] img[..., 3] = np.clip((1 - d) * c, 0, 1) * 255 edge = (d > 0.95) & (d < 1) img[edge, :3] = [255, 200, 255] return Image.fromarray(np.clip(img, 0, 255).astype(np.uint8), "RGBA") def bossArm(w=96, h=24): img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(img) d.polygon([(0, 4), (w - 16, 6), (w - 4, h // 2), (w - 16, h - 6), (0, h - 4)], fill=(90, 40, 130, 255), outline=(220, 160, 255, 255)) for i in range(4): d.rectangle([10 + i * 18, 9, 18 + i * 18, h - 9], fill=(255, 120, 40, 255)) return img def turret(size=28): img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) d = ImageDraw.Draw(img) d.ellipse([2, 2, size - 3, size - 3], fill=(60, 200, 220, 255), outline=(220, 255, 255, 255)) d.rectangle([size // 2 - 3, size // 2, size // 2 + 2, size - 1], fill=(30, 80, 110, 255)) d.ellipse([size // 2 - 5, size // 2 - 5, size // 2 + 4, size // 2 + 4], fill=(255, 255, 180, 255)) return img def bullet(size=12): y, x = np.mgrid[0:size, 0:size] + 0.5 d = np.sqrt((x - size / 2) ** 2 + (y - size / 2) ** 2) / (size / 2) img = np.zeros((size, size, 4)) img[..., :3] = 255 img[..., 3] = np.clip(1 - d, 0, 1) ** 0.6 * 255 core = d < 0.45 img[~core, 1] = img[~core, 1] * 0.7 return Image.fromarray(img.astype(np.uint8), "RGBA") # The 8 by 8 font: 5 by 7 capitals, digits, and punctuation drawn here a pixel at a time, laid out # sixteen to a row for ASCII 32 to 127 with each glyph one pixel in from its cell's left. Lower # case shares the capitals. GLYPHS = { " ": [" "] * 7, "!": [" # ", " # ", " # ", " # ", " # ", " ", " # "], "\"": [" # # ", " # # ", " ", " ", " ", " ", " "], "#": [" # # ", "#####", " # # ", " # # ", " # # ", "#####", " # # "], "$": [" # ", " ####", "# # ", " ### ", " # #", "#### ", " # "], "%": ["## ", "## #", " # ", " # ", " # ", "# ##", " ##"], "&": [" ## ", "# # ", "# # ", " # ", "# # #", "# # ", " ## #"], "'": [" # ", " # ", " ", " ", " ", " ", " "], "(": [" # ", " # ", " # ", " # ", " # ", " # ", " # "], ")": [" # ", " # ", " # ", " # ", " # ", " # ", " # "], "*": [" ", " # ", "# # #", " ### ", "# # #", " # ", " "], "+": [" ", " # ", " # ", "#####", " # ", " # ", " "], ",": [" ", " ", " ", " ", " ## ", " # ", " # "], "-": [" ", " ", " ", "#####", " ", " ", " "], ".": [" ", " ", " ", " ", " ", " ## ", " ## "], "/": [" ", " #", " # ", " # ", " # ", "# ", " "], "0": [" ### ", "# #", "# ##", "# # #", "## #", "# #", " ### "], "1": [" # ", " ## ", " # ", " # ", " # ", " # ", " ### "], "2": [" ### ", "# #", " #", " # ", " # ", " # ", "#####"], "3": ["#####", " # ", " # ", " # ", " #", "# #", " ### "], "4": [" # ", " ## ", " # # ", "# # ", "#####", " # ", " # "], "5": ["#####", "# ", "#### ", " #", " #", "# #", " ### "], "6": [" ## ", " # ", "# ", "#### ", "# #", "# #", " ### "], "7": ["#####", " #", " # ", " # ", " # ", " # ", " # "], "8": [" ### ", "# #", "# #", " ### ", "# #", "# #", " ### "], "9": [" ### ", "# #", "# #", " ####", " #", " # ", " ## "], ":": [" ", " ## ", " ## ", " ", " ## ", " ## ", " "], ";": [" ", " ## ", " ## ", " ", " ## ", " # ", " # "], "<": [" # ", " # ", " # ", "# ", " # ", " # ", " # "], "=": [" ", " ", "#####", " ", "#####", " ", " "], ">": [" # ", " # ", " # ", " #", " # ", " # ", " # "], "?": [" ### ", "# #", " #", " # ", " # ", " ", " # "], "@": [" ### ", "# #", "# ###", "# # #", "# ###", "# ", " ####"], "A": [" ### ", "# #", "# #", "#####", "# #", "# #", "# #"], "B": ["#### ", "# #", "# #", "#### ", "# #", "# #", "#### "], "C": [" ### ", "# #", "# ", "# ", "# ", "# #", " ### "], "D": ["#### ", "# #", "# #", "# #", "# #", "# #", "#### "], "E": ["#####", "# ", "# ", "#### ", "# ", "# ", "#####"], "F": ["#####", "# ", "# ", "#### ", "# ", "# ", "# "], "G": [" ### ", "# #", "# ", "# ###", "# #", "# #", " ####"], "H": ["# #", "# #", "# #", "#####", "# #", "# #", "# #"], "I": [" ### ", " # ", " # ", " # ", " # ", " # ", " ### "], "J": [" ###", " # ", " # ", " # ", " # ", "# # ", " ## "], "K": ["# #", "# # ", "# # ", "## ", "# # ", "# # ", "# #"], "L": ["# ", "# ", "# ", "# ", "# ", "# ", "#####"], "M": ["# #", "## ##", "# # #", "# # #", "# #", "# #", "# #"], "N": ["# #", "# #", "## #", "# # #", "# ##", "# #", "# #"], "O": [" ### ", "# #", "# #", "# #", "# #", "# #", " ### "], "P": ["#### ", "# #", "# #", "#### ", "# ", "# ", "# "], "Q": [" ### ", "# #", "# #", "# #", "# # #", "# # ", " ## #"], "R": ["#### ", "# #", "# #", "#### ", "# # ", "# # ", "# #"], "S": [" ####", "# ", "# ", " ### ", " #", " #", "#### "], "T": ["#####", " # ", " # ", " # ", " # ", " # ", " # "], "U": ["# #", "# #", "# #", "# #", "# #", "# #", " ### "], "V": ["# #", "# #", "# #", "# #", "# #", " # # ", " # "], "W": ["# #", "# #", "# #", "# # #", "# # #", "# # #", " # # "], "X": ["# #", "# #", " # # ", " # ", " # # ", "# #", "# #"], "Y": ["# #", "# #", " # # ", " # ", " # ", " # ", " # "], "Z": ["#####", " #", " # ", " # ", " # ", "# ", "#####"], "[": [" ### ", " # ", " # ", " # ", " # ", " # ", " ### "], "\\": [" ", "# ", " # ", " # ", " # ", " #", " "], "]": [" ### ", " # ", " # ", " # ", " # ", " # ", " ### "], "^": [" # ", " # # ", "# #", " ", " ", " ", " "], "_": [" ", " ", " ", " ", " ", " ", "#####"], } def font(): img = Image.new("RGBA", (16 * 8, 6 * 8), (0, 0, 0, 0)) for code in range(32, 128): ch = chr(code) rows = GLYPHS.get(ch.upper()) if rows is None: continue i = code - 32 x0 = (i % 16) * 8 + 1 y0 = (i // 16) * 8 for y, row in enumerate(rows): for x, c in enumerate(row): if c == "#": img.putpixel((x0 + x, y0 + y), (255, 255, 255, 255)) return img # Parallax layers for the side-scroller, each tiling sideways: far mountains, near hills, and a # ground strip of grass over brick. def mountains(w=320, h=110): img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(img) rng = np.random.default_rng(4) pts = [(0, h)] x = 0 while x <= w: pts.append((x, int(rng.integers(20, 80)))) x += int(rng.integers(30, 60)) pts[-1] = (w, pts[1][1]) pts.append((w, h)) d.polygon(pts, fill=(70, 80, 150, 255)) for px, py in pts[1:-1]: d.polygon([(px - 8, py + 10), (px, py), (px + 8, py + 10)], fill=(230, 240, 255, 255)) return img def hills(w=320, h=80): img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(img) for i in range(-1, 5): cx = i * 90 + 40 d.ellipse([cx - 70, 20, cx + 70, 160], fill=(40, 150, 70, 255)) d.ellipse([cx - 50, 34, cx + 50, 120], fill=(70, 190, 90, 255)) return img def ground(w=64, h=48): img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(img) d.rectangle([0, 0, w - 1, 7], fill=(60, 200, 70, 255)) d.rectangle([0, 6, w - 1, 9], fill=(30, 120, 40, 255)) for row in range(4): y = 10 + row * 10 off = (row % 2) * 8 for col in range(-1, 5): x = col * 16 + off d.rectangle([x, y, x + 14, y + 8], fill=(190, 90, 40, 255), outline=(110, 50, 20, 255)) return img # The race track, seen from above: grass, a loop of road with a striped kerb, and a # chequered start line, at a pixel a texel. def track(size=512): y, x = np.mgrid[0:size, 0:size] + 0.5 cx = cy = size / 2 u = (x - cx) / (size * 0.36) v = (y - cy) / (size * 0.26) r = np.sqrt(u * u + v * v) img = np.zeros((size, size, 3)) grass = ((np.floor(x / 16) + np.floor(y / 16)) % 2) img[:] = np.where(grass[..., None] > 0, np.array([60, 170, 60]), np.array([50, 150, 50])) road = np.abs(r - 1) < 0.16 kerb = (np.abs(r - 1) >= 0.16) & (np.abs(r - 1) < 0.21) a = np.arctan2(v, u) img[road] = [110, 110, 120] stripes = (np.floor(a / (2 * math.pi) * 64) % 2) == 0 img[kerb & stripes] = [230, 40, 40] img[kerb & ~stripes] = [245, 245, 245] start = road & (np.abs(a) < 0.035) chk = ((np.floor(x / 4) + np.floor(y / 4)) % 2) == 0 img[start & chk] = [20, 20, 20] img[start & ~chk] = [250, 250, 250] water = (r < 0.55) img[water] = [40, 100, 220] img[water & ((np.floor((x + y) / 6) % 5) == 0)] = [90, 160, 255] return Image.fromarray(img.astype(np.uint8), "RGB") # A sky for the racers, as an equirectangular picture: a gradient and a band of pixel mountains # all the way round, so turning the camera scrolls them as the old racers scrolled theirs. def raceSky(w=2048, h=1024): img = np.zeros((h, w, 3)) t = (np.arange(h) / (h / 2))[:, None] top = np.array([40, 80, 220]) mid = np.array([140, 200, 255]) img[:] = (top + (mid - top) * np.clip(t, 0, 1)[..., None]) img[h // 2:] = [60, 170, 60] rng = np.random.default_rng(9) x = 0 ridge = np.zeros(w) while x < w: peak = int(rng.integers(20, 70)) span = int(rng.integers(60, 140)) for i in range(span): if x + i < w: ridge[x + i] = max(ridge[x + i], peak * (1 - abs(i - span / 2) / (span / 2))) x += span // 2 for i in range(w): top_y = h // 2 - int(ridge[i]) img[top_y:h // 2, i] = [80, 90, 160] img[top_y:min(top_y + 6, h // 2), i] = [235, 240, 255] img = np.repeat(np.repeat(img[::4, ::4], 4, axis=0), 4, axis=1) return Image.fromarray(img.astype(np.uint8), "RGB") # A modern dusk for the chrome to reflect: a deep blue sky warming to orange at a low sun, a dark # land below, and a few bright city lights. def duskSky(w=2048, h=1024): lon = (np.arange(w) + 0.5) / w * 2 * math.pi - math.pi lat = math.pi / 2 - (np.arange(h) + 0.5) / h * math.pi LON, LAT = np.meshgrid(lon, lat) img = np.zeros((h, w, 3)) up = np.clip(LAT / (math.pi / 2), 0, 1) img[:] = (np.array([255, 150, 70]) * (1 - up[..., None]) ** 3 + np.array([20, 40, 110]) * (1 - (1 - up[..., None]) ** 3)) sun = np.exp(-((LON - 0.6) ** 2 + (LAT - 0.08) ** 2) / 0.004) img += sun[..., None] * np.array([255, 230, 180]) * 1.5 down = LAT < 0 img[down] = np.array([18, 16, 26]) + np.array([60, 40, 30]) * np.exp(LAT[down] * 6)[..., None] rng = np.random.default_rng(2) for _ in range(400): x = int(rng.integers(0, w)) y = int(h / 2 + rng.integers(2, 40)) img[y, x] = [255, 220, 150] return Image.fromarray(np.clip(img, 0, 255).astype(np.uint8), "RGB") def scanlines(): img = Image.new("RGBA", (4, 4), (0, 0, 0, 0)) for x in range(4): img.putpixel((x, 1), (0, 0, 0, 110)) img.putpixel((x, 3), (0, 0, 0, 110)) return img def main(): os.makedirs(OUT, exist_ok=True) save(sheet([pixels(f) for f in INVADER_A]), "invaderA.png") save(sheet([pixels(f) for f in INVADER_B]), "invaderB.png") save(pixels(CANNON), "cannon.png") save(sheet([pixels(f) for f in RUNNER]), "runner.png") save(sheet([pixels(f) for f in COIN]), "coin.png") save(pixels(SHIP), "ship.png") save(pixels(KART), "kart.png") save(bossCore(), "bossCore.png") save(bossArm(), "bossArm.png") save(turret(), "turret.png") save(bullet(), "bullet.png") save(font(), "font.png") save(mountains(), "mountains.png") save(hills(), "hills.png") save(ground(), "ground.png") save(track(), "track.png") save(raceSky(), "raceSky.png") save(duskSky(), "duskSky.png") save(scanlines(), "scanlines.png") if __name__ == "__main__": main()