708 lines
24 KiB
Python
708 lines
24 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.
|
|
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
|
|
|
|
|
|
# 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")
|
|
|
|
|
|
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 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", (size, size + 28), (0, 0, 0, 0))
|
|
d = ImageDraw.Draw(img)
|
|
d.rectangle([size // 2 - 4, size - 6, size // 2 + 3, size + 27], fill=(140, 90, 50, 255), outline=(70, 40, 20, 255))
|
|
for k, colour in enumerate([(200, 20, 30), (250, 250, 250), (200, 20, 30), (250, 250, 250), (200, 20, 30)]):
|
|
r = size // 2 - 1 - k * 6
|
|
d.ellipse([size // 2 - r, size // 2 - r, size // 2 + r, size // 2 + r], fill=colour + (255,), outline=(60, 10, 10, 255))
|
|
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()
|