singe/util/insertCoinArt.py
2026-10-06 16:54:44 -05:00

718 lines
25 KiB
Python

# 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), "k": (60, 60, 70), "q": (90, 90, 100),
}
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......",
"................",
]
# The racer's kart from behind, the left half: the right is its mirror. B, b, and h are the body,
# its shade, and its highlight, which kartSheet() paints in each team's colours; the rest is fixed.
KART_HALF = [
"......kkkkkkkkkkkkkkkkkk",
"......khhhhhhhhhhhhhhhhh",
"......kBBBBBBBBBBBBBBBBB",
"......kBBBBBBBBBBBBBBBBB",
"......kbbbbbbbbbbbbbbbbb",
"......kkkkkkkkkkkkkkkkkk",
"..............kk........",
"..............kk......yy",
"..............kk.....yYY",
"..............kk....yYYY",
"..............kk....yKKK",
"..............kk....yKKW",
"...............hhhhhhhhh",
"..KKKKKKKKK..hBBBBBBBBBB",
".KkkkkkkkkkK.hBBBBBBBBBB",
".KkqqqqqqqkKhBBBBBBBBBBB",
".KkqSSSSSqkKhBBBBBBBbbBB",
".KkqqqqqqqkKhBBBBBBBWWWW",
".KkqSSSSSqkKhBBBBBBBWWWW",
".KkqqqqqqqkKbBBBBBBBWWWW",
".KkqSSSSSqkKbbBBBBBBBBBB",
".KkqqqqqqqkKkbbbbbbbbbbb",
".KkqSSSSSqkKkkkkkkkkkkkk",
".KkqqqqqqqkK....SSSSSSSS",
".KkqSSSSSqkK....SkSkSkSS",
".KkqqqqqqqkK....SSSSSRRR",
".KkkkkkkkkkK....SSSSSRRR",
"..KKKKKKKKK.....kkkkkkkk",
"........................",
]
KART_W = len(KART_HALF[0]) * 2
# The teams: the player's red first, then the rivals.
KART_TEAMS = [((230, 40, 40), (140, 20, 30)), ((40, 110, 240), (20, 50, 140)), ((250, 210, 40), (170, 120, 10)),
((40, 200, 90), (20, 110, 50)), ((180, 70, 230), (100, 30, 140)), ((250, 140, 40), (160, 70, 20))]
# The boss, drawn larger and smoothly: a core, an arm, and a turret, each turned by the demo.
# The modern eras' sprites (the boss, its bullets, the gallery's targets) are drawn this many pixels to
# a design unit of the demo (demos/kit.lua, K.sprite), for 1920 by 1080; the pixel art is not.
DENSITY = 2.25
def dense(v):
return round(v * DENSITY)
def bossCore(size=dense(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", (dense(w), dense(h)), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
d.polygon([(0, dense(4)), (dense(w - 16), dense(6)), (dense(w - 4), dense(h / 2)), (dense(w - 16), dense(h - 6)), (0, dense(h - 4))], fill=(90, 40, 130, 255), outline=(220, 160, 255, 255), width=dense(1))
for i in range(4):
d.rectangle([dense(10 + i * 18), dense(9), dense(18 + i * 18), dense(h - 9)], fill=(255, 120, 40, 255))
return img
def turret(size=28):
img = Image.new("RGBA", (dense(size), dense(size)), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
h = size / 2
d.ellipse([dense(2), dense(2), dense(size - 3), dense(size - 3)], fill=(60, 200, 220, 255), outline=(220, 255, 255, 255), width=dense(1))
d.rectangle([dense(h - 3), dense(h), dense(h + 2), dense(size - 1)], fill=(30, 80, 110, 255))
d.ellipse([dense(h - 5), dense(h - 5), dense(h + 4), dense(h + 4)], fill=(255, 255, 180, 255))
return img
def bullet(size=dense(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
# The same glyphs as a Lua table, for the 3D scroller's block letters: rows top down, "#" lit.
def fontLua():
lines = ["-- Insert Coin's pixel font, written by util/insertCoinArt.py from its GLYPHS: rows top down, \"#\" lit.", "return {"]
for ch in sorted(GLYPHS):
rows = ", ".join('"%s"' % r for r in GLYPHS[ch])
key = ch.replace("\\", "\\\\").replace('"', '\\"')
lines.append('\t["%s"] = { %s },' % (key, rows))
lines.append("}")
with open(os.path.join(OUT, "font.lua"), "w") as f:
f.write("\n".join(lines) + "\n")
# 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")
# One screen row in three darkened: the demo repeats this down a quad over the whole screen.
def scanlines():
img = Image.new("RGBA", (1, 3), (0, 0, 0, 0))
img.putpixel((0, 2), (0, 0, 0, 110))
return img
def kartSheet():
frames = []
for body, shade in KART_TEAMS:
light = tuple(min(255, int(c * 1.25 + 45)) for c in body)
rows = [half + half[::-1] for half in KART_HALF]
assert all(len(r) == KART_W for r in rows)
img = Image.new("RGBA", (KART_W, len(rows)), (0, 0, 0, 0))
for y, row in enumerate(rows):
for x, c in enumerate(row):
colour = {"B": body, "b": shade, "h": light}.get(c, INK.get(c))
if colour is not None:
img.putpixel((x, y), colour + (255,))
frames.append(img)
return sheet(frames)
# A coin's face: gold, rimmed, with a picture from media/ stamped on it in raised gold. The stamp is
# every pixel of the picture darker than its white background.
def coinFace(source, size=256, fill=0.62):
from scipy import ndimage
pic = Image.open(os.path.join(REPO, "media", source)).convert("L")
box = pic.point(lambda v: 255 if v < 200 else 0).getbbox()
pic = pic.crop(box)
scale = size * fill / max(pic.size)
pic = pic.resize((int(pic.width * scale), int(pic.height * scale)), Image.LANCZOS)
stamp = np.zeros((size, size))
ox = (size - pic.width) // 2
oy = (size - pic.height) // 2
stamp[oy:oy + pic.height, ox:ox + pic.width] = np.clip((220 - np.asarray(pic, dtype=float)) / 60, 0, 1)
y, x = np.mgrid[0:size, 0:size] + 0.5
r = np.sqrt((x - size / 2) ** 2 + (y - size / 2) ** 2) / (size / 2)
base = np.array([220, 165, 40]) * (0.85 + 0.15 * (1 - r))[..., None]
rim = (r > 0.86) & (r < 0.93)
base[rim] = [255, 225, 120]
raised = np.array([255, 230, 130])
edge = np.clip(ndimage.sobel(stamp, 0) + ndimage.sobel(stamp, 1), -2, 2)[..., None]
img = base * (1 - stamp[..., None]) + raised * stamp[..., None] - edge * 25
return Image.fromarray(np.clip(img, 0, 255).astype(np.uint8), "RGB")
# The fighters: two martial artists posed by the angles of their limbs, drawn a pixel at a time at
# FIGHTER_W by FIGHTER_H with no smoothing, so they stay crisp when the demo scales them up. Angles are
# in degrees, 0 pointing the way the fighter faces and positive turning downward.
FIGHTER_W = 40
FIGHTER_H = 48
SKIN = (240, 190, 140)
POSES = {
# lean lead arm rear arm lead leg rear leg
"idle": (0, (60, -70), (75, -80), (70, 95), (110, 85)),
"punch": (10, (0, 0), (80, -85), (65, 100), (120, 80)),
"kick": (-15, (60, -70), (110, -60), (-5, 0), (100, 95)),
"hit": (-25, (200, 230), (220, 250), (80, 120), (105, 70)),
}
def fighter(pose, gi, belt):
lean, lead, rear, legA, legB = POSES[pose]
img = Image.new("RGBA", (FIGHTER_W, FIGHTER_H), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
hip = (18, 30)
torso = 14
neck = (hip[0] + torso * math.sin(math.radians(lean)), hip[1] - torso * math.cos(math.radians(lean)))
def limb(start, a1, a2, l1, l2, colour, width, end):
p1 = (start[0] + l1 * math.cos(math.radians(a1)), start[1] + l1 * math.sin(math.radians(a1)))
p2 = (p1[0] + l2 * math.cos(math.radians(a2)), p1[1] + l2 * math.sin(math.radians(a2)))
d.line([start, p1], fill=colour, width=width)
d.line([p1, p2], fill=colour, width=width)
d.ellipse([p2[0] - 1.5, p2[1] - 1.5, p2[0] + 1.5, p2[1] + 1.5], fill=end)
shade = tuple(int(c * 0.7) for c in gi) + (255,)
shoulder = (neck[0], neck[1] + 2)
limb(shoulder, rear[0], rear[1], 7, 7, shade, 3, SKIN + (255,))
limb(hip, legB[0], legB[1], 9, 9, shade, 4, (30, 30, 30, 255))
d.line([hip, neck], fill=gi + (255,), width=7)
d.line([(hip[0] - 3, hip[1] - 2), (hip[0] + 3, hip[1] - 2)], fill=belt + (255,), width=2)
limb(hip, legA[0], legA[1], 9, 9, gi + (255,), 4, (30, 30, 30, 255))
limb(shoulder, lead[0], lead[1], 7, 7, gi + (255,), 3, SKIN + (255,))
head = (neck[0] + 1, neck[1] - 5)
d.ellipse([head[0] - 4, head[1] - 4, head[0] + 4, head[1] + 4], fill=SKIN + (255,))
d.chord([head[0] - 4, head[1] - 5, head[0] + 4, head[1] + 3], 180, 360, fill=(30, 20, 20, 255))
d.point((head[0] + 2, head[1]), fill=(20, 20, 20, 255))
# A dark outline, the way sprites of the time were drawn.
a = np.array(img)
solid = a[..., 3] > 0
from scipy import ndimage
grown = ndimage.binary_dilation(solid, structure=[[0, 1, 0], [1, 1, 1], [0, 1, 0]])
a[grown & ~solid] = [20, 10, 20, 255]
return Image.fromarray(a)
def fighterSheet(gi, belt, mirror):
frames = [fighter(p, gi, belt) for p in ("idle", "punch", "kick", "hit")]
if mirror:
frames = [f.transpose(Image.FLIP_LEFT_RIGHT) for f in frames]
return sheet(frames)
# A shooting-gallery target: a bullseye of red and white rings on a wooden post.
def target(size=64):
img = Image.new("RGBA", (dense(size), dense(size + 28)), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
h = size / 2
d.rectangle([dense(h - 4), dense(size - 6), dense(h + 3), dense(size + 27)], fill=(140, 90, 50, 255), outline=(70, 40, 20, 255), width=dense(1))
for k, colour in enumerate([(200, 20, 30), (250, 250, 250), (200, 20, 30), (250, 250, 250), (200, 20, 30)]):
r = h - 1 - k * 6
d.ellipse([dense(h - r), dense(h - r), dense(h + r), dense(h + r)], fill=colour + (255,), outline=(60, 10, 10, 255), width=dense(1))
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")
karts = kartSheet()
save(karts, "kart.png")
# The rivals, one picture each: a 3D material takes a whole picture.
for i in range(1, len(KART_TEAMS)):
save(karts.crop((i * KART_W, 0, i * KART_W + KART_W, karts.height)), "kart%d.png" % i)
save(coinFace("SingeText.jpeg"), "coinSinge.png")
save(fighterSheet((210, 40, 40), (20, 20, 20), False), "fighterA.png")
save(target(), "target.png")
save(fighterSheet((40, 100, 230), (240, 220, 60), True), "fighterB.png")
save(coinFace("Dragon.jpeg"), "coinDragon.png")
save(bossCore(), "bossCore.png")
save(bossArm(), "bossArm.png")
save(turret(), "turret.png")
save(bullet(), "bullet.png")
save(font(), "font.png")
fontLua()
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()