orbit/dev/make-faction-sprites.py

442 lines
19 KiB
Python

#!/usr/bin/env python3
"""Paint the sixteen faction fighters into assets/images/ships-enemy.png.
Frames 30-37 (row 3) are the APEX (the predators — angular, red-hot);
frames 40-47 (row 4) are the MINDWEAVE (the weavers — floating, violet-cold).
Each cell is 128x128 inside the 10x10 sheet. Every ship is drawn nose-UP
(the game rotates the frame at runtime).
Drawn at 4x supersample (512) and downscaled for clean edges, in the sheet's
existing idiom: dark warm hulls, bright accent lines, engine glow at the
bottom. The other cells (the pirates, the neutrals) are left untouched.
"""
import math
from PIL import Image, ImageDraw, ImageFilter
SHEET = 'assets/images/ships-enemy.png'
CELL = 128
SS = 4 # supersample
W = CELL * SS # 512 draw space
C = W / 2 # center
def lerp(a, b, t):
return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(3))
def vgrad(shape_pts, top, bottom):
"""A vertical two-tone fill: paint the polygon, then shade the lower
half toward `bottom` by compositing a semi-transparent band clipped to
the polygon. `shape_pts` in 512-space."""
return top, bottom
class Ship:
"""A 512-space canvas with helpers. All coords in 512-space."""
def __init__(self):
self.im = Image.new('RGBA', (W, W), (0, 0, 0, 0))
self.d = ImageDraw.Draw(self.im)
def poly(self, pts, fill):
self.d.polygon(pts, fill=fill)
def oval(self, box, fill=None, outline=None, width=0):
self.d.ellipse(box, fill=fill, outline=outline, width=width)
def line(self, a, b, fill, width=4):
self.d.line([a, b], fill=fill, width=width)
def arc(self, box, a0, a1, fill, width=6):
self.d.arc(box, a0, a1, fill=fill, width=width)
def sline(self, a, b, fill, w1, w2):
"""A tapered stripe (two-segment fake taper)."""
mx = (a[0] + b[0]) / 2
my = (a[1] + b[1]) / 2
self.line(a, (mx, my), fill, int((w1 + w2) / 2))
self.line((mx, my), b, fill, w2)
def glow_dot(self, x, y, r, color):
"""A radial glow (engine / orb), drawn as concentred fading rings."""
rgb = color[:3] if len(color) >= 3 else (255, 255, 255)
for i in range(r, 0, -1):
a = int(255 * (i / r) ** 2 * 0.9)
self.oval((x - i, y - i, x + i, y + i), fill=(rgb[0], rgb[1], rgb[2], a))
def finish(self):
return self.im.resize((CELL, CELL), Image.LANCZOS)
# ---------------------------------------------------------------- the APEX
# Angular, predatory, nose-up. Dark warm hulls; red-hot accents.
APEX_HULL = (74, 66, 70, 255)
APEX_DARK = (40, 34, 40, 255)
APEX_LITE = (110, 98, 104, 255)
APEX_EDGE = (150, 132, 140, 255)
APEX_RED = (216, 62, 44, 255)
APEX_ORANGE = (236, 128, 52, 255)
APEX_CANY = (26, 30, 38, 255)
APEX_ENGINE = (255, 150, 60)
def raptor():
"""30 — a narrow dart with a forward spine cannon and thin intakes."""
s = Ship()
s.poly([(C, 40), (C + 46, 300), (C + 70, 430), (C + 34, 470), (C - 34, 470), (C - 70, 430), (C - 46, 300)], APEX_HULL)
s.poly([(C, 40), (C + 30, 300), (C + 22, 430), (C - 22, 430), (C - 30, 300)], APEX_DARK)
s.poly([(C, 40), (C + 12, 220), (C - 12, 220)], APEX_ORANGE) # the spine
s.oval((C - 18, 150, C + 18, 190), fill=APEX_CANY) # the canopy
s.sline((C - 60, 330), (C - 84, 420), APEX_EDGE, 10, 16) # the intakes
s.sline((C + 60, 330), (C + 84, 420), APEX_EDGE, 10, 16)
s.poly([(C - 16, 452), (C - 40, 486), (C - 8, 486)], APEX_DARK) # the split tail
s.poly([(C + 16, 452), (C + 40, 486), (C + 8, 486)], APEX_DARK)
s.glow_dot(C - 22, 480, 14, APEX_ENGINE)
s.glow_dot(C + 22, 480, 14, APEX_ENGINE)
s.poly([(C, 40), (C + 10, 90), (C - 10, 90)], APEX_RED) # the nose tip
return s
def mauler():
"""31 — a broad hammer hull with the big forward cannon."""
s = Ship()
s.poly([(C, 70), (C + 78, 150), (C + 96, 330), (C + 60, 450), (C - 60, 450), (C - 96, 330), (C - 78, 150)], APEX_HULL)
s.poly([(C, 120), (C + 40, 180), (C + 46, 340), (C - 46, 340), (C - 40, 180)], APEX_DARK)
s.poly([(C - 26, 30), (C + 26, 30), (C + 20, 170), (C - 20, 170)], APEX_EDGE) # the cannon tube
s.oval((C - 34, 18, C + 34, 54), fill=APEX_DARK, outline=APEX_RED, width=8) # the muzzle
s.oval((C - 16, 190, C + 16, 230), fill=APEX_CANY)
s.poly([(C - 96, 330), (C - 130, 400), (C - 84, 420)], APEX_LITE) # the shoulder blades
s.poly([(C + 96, 330), (C + 130, 400), (C + 84, 420)], APEX_LITE)
s.line((C - 60, 250), (C - 60, 380), APEX_RED, 10)
s.line((C + 60, 250), (C + 60, 380), APEX_RED, 10)
s.poly([(C - 20, 430), (C - 34, 470), (C, 460)], APEX_DARK)
s.poly([(C + 20, 430), (C + 34, 470), (C, 460)], APEX_DARK)
s.glow_dot(C, 466, 16, APEX_ENGINE)
return s
def stalker():
"""32 — the cloaker: a long thin blade with four stealth fins."""
s = Ship()
s.poly([(C, 24), (C + 26, 220), (C + 40, 420), (C - 40, 420), (C - 26, 220)], APEX_HULL)
s.poly([(C, 24), (C + 12, 240), (C + 16, 400), (C - 16, 400), (C - 12, 240)], APEX_DARK)
for sx in (-1, 1):
s.poly([(C + sx * 30, 180), (C + sx * 120, 240), (C + sx * 96, 300), (C + sx * 34, 300)], APEX_LITE)
s.poly([(C + sx * 34, 320), (C + sx * 110, 380), (C + sx * 88, 420), (C + sx * 38, 420)], APEX_DARK)
s.line((C + sx * 36, 196), (C + sx * 108, 252), APEX_RED, 6)
s.oval((C - 10, 140, C + 10, 176), fill=APEX_CANY)
s.line((C, 60), (C, 200), APEX_ORANGE, 8)
s.poly([(C - 14, 410), (C, 470), (C + 14, 410)], APEX_DARK)
s.glow_dot(C, 462, 12, APEX_ENGINE)
return s
def screamer():
"""33 — the wide V wings with the spray nozzles along each edge."""
s = Ship()
s.poly([(C, 60), (C + 30, 160), (C + 44, 380), (C - 44, 380), (C - 30, 160)], APEX_HULL)
s.poly([(C - 26, 150), (C - 150, 330), (C - 120, 380), (C - 30, 300)], APEX_LITE)
s.poly([(C + 26, 150), (C + 150, 330), (C + 120, 380), (C + 30, 300)], APEX_LITE)
for sx in (-1, 1):
for i, t in enumerate((0.15, 0.38, 0.6, 0.82)):
x = C + sx * (30 + t * 115)
y = 150 + t * 185
s.oval((x - 9, y - 9, x + 9, y + 9), fill=APEX_ORANGE)
s.poly([(C - 18, 60), (C + 18, 60), (C + 12, 140), (C - 12, 140)], APEX_EDGE)
s.oval((C - 12, 180, C + 12, 220), fill=APEX_CANY)
s.line((C, 240), (C, 360), APEX_RED, 8)
s.poly([(C - 20, 370), (C - 44, 440), (C, 430)], APEX_DARK)
s.poly([(C + 20, 370), (C + 44, 440), (C, 430)], APEX_DARK)
s.glow_dot(C - 24, 430, 12, APEX_ENGINE)
s.glow_dot(C + 24, 430, 12, APEX_ENGINE)
return s
def reaver():
"""34 — twin rapid-fire pods flanking a lean hull."""
s = Ship()
s.poly([(C, 50), (C + 44, 160), (C + 56, 400), (C - 56, 400), (C - 44, 160)], APEX_HULL)
s.poly([(C, 100), (C + 24, 180), (C + 30, 380), (C - 30, 380), (C - 24, 180)], APEX_DARK)
for sx in (-1, 1):
s.oval((C + sx * 96 - 34, 190, C + sx * 96 + 34, 258), fill=APEX_LITE)
s.oval((C + sx * 96 - 24, 120, C + sx * 96 + 24, 172), fill=APEX_EDGE)
s.oval((C + sx * 96 - 14, 96, C + sx * 96 + 14, 130), fill=APEX_RED)
s.poly([(C + sx * 40, 220), (C + sx * 72, 214), (C + sx * 72, 252), (C + sx * 44, 262)], APEX_EDGE)
s.oval((C - 14, 150, C + 14, 196), fill=APEX_CANY)
s.poly([(C, 50), (C + 12, 110), (C - 12, 110)], APEX_RED)
s.line((C - 34, 280), (C + 34, 280), APEX_RED, 8)
s.line((C - 38, 320), (C + 38, 320), APEX_RED, 6)
s.poly([(C - 18, 390), (C - 30, 452), (C, 444)], APEX_DARK)
s.poly([(C + 18, 390), (C + 30, 452), (C, 444)], APEX_DARK)
s.glow_dot(C - 18, 444, 12, APEX_ENGINE)
s.glow_dot(C + 18, 444, 12, APEX_ENGINE)
return s
def fang():
"""35 — the burst striker: two curved fang blades sweeping up."""
s = Ship()
for sx in (-1, 1):
pts = []
for i in range(13):
t = i / 12
x = C + sx * (28 + math.sin(t * 1.9) * 92 * (1 - t * 0.25))
y = 400 - t * 360
pts.append((x, y))
for i in range(12, -1, -1):
t = i / 12
x = C + sx * (24 + math.sin(t * 1.9) * 66 * (1 - t * 0.3))
y = 430 - t * 370
pts.append((x, y))
s.poly(pts, APEX_LITE)
s.line((C + sx * 40, 380), (C + sx * (40 + math.sin(0.85 * 1.9) * 80), 380 - 0.7 * 360), APEX_RED, 6)
s.poly([(C, 120), (C + 34, 220), (C + 40, 430), (C - 40, 430), (C - 34, 220)], APEX_HULL)
s.poly([(C, 160), (C + 18, 240), (C + 22, 400), (C - 22, 400), (C - 18, 240)], APEX_DARK)
s.oval((C - 12, 240, C + 12, 280), fill=APEX_CANY)
s.line((C, 130), (C, 220), APEX_ORANGE, 8)
s.glow_dot(C - 16, 428, 12, APEX_ENGINE)
s.glow_dot(C + 16, 428, 12, APEX_ENGINE)
return s
def tyrant():
"""36 — the broad hex hull with the two heavy missile bays."""
s = Ship()
s.poly([(C, 60), (C + 84, 120), (C + 108, 280), (C + 64, 440), (C - 64, 440), (C - 108, 280), (C - 84, 120)], APEX_HULL)
s.poly([(C, 110), (C + 44, 160), (C + 52, 340), (C - 52, 340), (C - 44, 160)], APEX_DARK)
for sx in (-1, 1):
s.poly([(C + sx * 58, 150), (C + sx * 96, 160), (C + sx * 96, 260), (C + sx * 60, 250)], APEX_LITE)
s.oval((C + sx * 88 - 16, 128, C + sx * 88 + 16, 164), fill=APEX_DARK, outline=APEX_RED, width=6)
s.oval((C - 16, 170, C + 16, 214), fill=APEX_CANY)
s.line((C - 70, 300), (C + 70, 300), APEX_RED, 12)
s.poly([(C, 60), (C + 14, 120), (C - 14, 120)], APEX_ORANGE)
s.poly([(C - 24, 420), (C - 40, 470), (C, 458)], APEX_DARK)
s.poly([(C + 24, 420), (C + 40, 470), (C, 458)], APEX_DARK)
s.glow_dot(C, 462, 16, APEX_ENGINE)
return s
def alpha():
"""37 — the flagship: layered wings, the crown fins, gold-hot trim."""
s = Ship()
s.poly([(C, 30), (C + 60, 140), (C + 76, 330), (C + 44, 460), (C - 44, 460), (C - 76, 330), (C - 60, 140)], APEX_HULL)
s.poly([(C - 30, 150), (C - 140, 340), (C - 104, 396), (C - 34, 310)], APEX_LITE)
s.poly([(C + 30, 150), (C + 140, 340), (C + 104, 396), (C + 34, 310)], APEX_LITE)
s.poly([(C - 40, 320), (C - 96, 420), (C - 60, 448), (C - 40, 390)], APEX_EDGE)
s.poly([(C + 40, 320), (C + 96, 420), (C + 60, 448), (C + 40, 390)], APEX_EDGE)
s.poly([(C, 30), (C + 26, 160), (C + 30, 360), (C - 30, 360), (C - 26, 160)], APEX_DARK)
s.poly([(C, 30), (C + 10, 120), (C - 10, 120)], (232, 196, 84, 255)) # the gold tip
for sx in (-1, 1):
s.poly([(C + sx * 18, 120), (C + sx * 54, 60), (C + sx * 40, 150)], (232, 196, 84, 255)) # the crown
s.oval((C - 14, 180, C + 14, 224), fill=APEX_CANY)
s.line((C - 50, 260), (C + 50, 260), APEX_RED, 10)
s.oval((C - 40, 350, C + 40, 400), outline=(232, 196, 84, 255), width=8)
s.poly([(C - 16, 440), (C - 30, 484), (C, 474)], APEX_DARK)
s.poly([(C + 16, 440), (C + 30, 484), (C, 474)], APEX_DARK)
s.glow_dot(C - 18, 476, 13, APEX_ENGINE)
s.glow_dot(C + 18, 476, 13, APEX_ENGINE)
return s
# --------------------------------------------------------------- the WEAVE
# Floating, organic, calm. Cool violet hulls; violet-cyan lights.
WE_HULL = (70, 62, 88, 255)
WE_DARK = (42, 36, 58, 255)
WE_LITE = (108, 98, 136, 255)
WE_EDGE = (148, 138, 176, 255)
WE_VIOLET = (156, 106, 224, 255)
WE_CYAN = (92, 208, 216, 255)
WE_PEARL = (214, 190, 255, 255)
WE_ENGINE = (170, 120, 255)
WE_CANY = (24, 26, 44, 255)
def mindblade():
"""40 — the ringed blade: the annulus around a dark core."""
s = Ship()
s.oval((C - 104, 128, C + 104, 384), outline=WE_CYAN, width=14)
s.oval((C - 104, 128, C + 104, 384), outline=WE_VIOLET, width=4)
for a in range(0, 360, 45):
x = C + 98 * math.cos(math.radians(a))
y = 256 + 98 * math.sin(math.radians(a))
s.oval((x - 12, y - 12, x + 12, y + 12), fill=WE_PEARL)
s.poly([(C, 90), (C + 44, 190), (C + 44, 330), (C, 422), (C - 44, 330), (C - 44, 190)], WE_HULL)
s.poly([(C, 130), (C + 26, 200), (C + 26, 320), (C, 380), (C - 26, 320), (C - 26, 200)], WE_DARK)
s.oval((C - 16, 220, C + 16, 268), fill=WE_CANY)
s.glow_dot(C, 256, 22, WE_VIOLET)
s.glow_dot(C, 60, 14, WE_CYAN)
s.glow_dot(C, 452, 14, WE_ENGINE)
return s
def telekine():
"""41 — the floating orb above the hull; two small orb-hands."""
s = Ship()
s.glow_dot(C, 78, 26, WE_CYAN)
s.oval((C - 18, 60, C + 18, 96), fill=WE_PEARL)
s.line((C, 104), (C, 140), WE_VIOLET, 6)
s.poly([(C, 140), (C + 52, 220), (C + 60, 380), (C + 30, 452), (C - 30, 452), (C - 60, 380), (C - 52, 220)], WE_HULL)
s.poly([(C, 180), (C + 32, 240), (C + 36, 380), (C - 36, 380), (C - 32, 240)], WE_DARK)
for sx in (-1, 1):
s.oval((C + sx * 118 - 22, 250, C + sx * 118 + 22, 294), fill=WE_LITE)
s.glow_dot(C + sx * 118, 272, 12, WE_CYAN)
s.line((C + sx * 56, 280), (C + sx * 98, 272), WE_EDGE, 6)
s.oval((C - 14, 240, C + 14, 286), fill=WE_CANY)
s.arc((C - 60, 340, C + 60, 420), 150, 30, WE_VIOLET, 8)
s.glow_dot(C - 20, 446, 12, WE_ENGINE)
s.glow_dot(C + 20, 446, 12, WE_ENGINE)
return s
def phantasm():
"""42 — the ghost: layered translucent shrouds and a wavy tail."""
s = Ship()
s.poly([(C, 80), (C + 70, 200), (C + 84, 340), (C + 52, 400), (C - 52, 400), (C - 84, 340), (C - 70, 200)], WE_HULL)
for i, (w, a) in enumerate(((110, 150), (96, 190), (82, 230))):
pts = []
for k in range(24):
t = k / 23
x = C - w + t * 2 * w
y = 300 + i * 26 + math.sin(t * math.pi * 2 + i) * 12
pts.append((x, y))
s.poly(pts, (WE_LITE[0], WE_LITE[1], WE_LITE[2], a))
for sx in (-1, 1):
pts = [(C + sx * 40, 160)]
for k in range(9):
t = k / 8
pts.append((C + sx * (44 + math.sin(t * 2.2) * 46), 380 - t * 160))
for k in range(8, -1, -1):
t = k / 8
pts.append((C + sx * (34 + math.sin(t * 2.2) * 30), 400 - t * 170))
s.poly(pts, (120, 110, 156, 200))
s.oval((C - 20, 180, C + 20, 236), fill=WE_DARK)
s.oval((C - 14, 196, C + 14, 222), fill=WE_CANY)
s.arc((C - 26, 150, C + 26, 200), 200, 340, WE_PEARL, 6)
s.glow_dot(C, 60, 14, WE_VIOLET)
s.glow_dot(C - 18, 420, 10, WE_ENGINE)
s.glow_dot(C + 18, 420, 10, WE_ENGINE)
return s
def weaver():
"""43 — the diamond hull in the web between two halo rings."""
s = Ship()
for r in (118, 82):
s.oval((C - r, 256 - r, C + r, 256 + r), outline=WE_EDGE, width=6)
for a in range(0, 360, 30):
x1 = C + 82 * math.cos(math.radians(a))
y1 = 256 + 82 * math.sin(math.radians(a))
x2 = C + 118 * math.cos(math.radians(a))
y2 = 256 + 118 * math.sin(math.radians(a))
s.line((x1, y1), (x2, y2), (148, 138, 176, 160), 3)
s.poly([(C, 120), (C + 54, 256), (C, 392), (C - 54, 256)], WE_HULL)
s.poly([(C, 158), (C + 34, 256), (C, 354), (C - 34, 256)], WE_DARK)
s.oval((C - 12, 236, C + 12, 276), fill=WE_CANY)
s.glow_dot(C, 256, 18, WE_CYAN)
s.glow_dot(C, 96, 12, WE_VIOLET)
s.glow_dot(C, 416, 12, WE_ENGINE)
return s
def lure():
"""44 — the hook: the crescent ring around the nose, the bait dots."""
s = Ship()
s.arc((C - 96, 96, C + 96, 304), 300, 240, WE_CYAN, 12)
for a in (300, 330, 270, 240):
x = C + 96 * math.cos(math.radians(a))
y = 200 + 96 * math.sin(math.radians(a))
s.glow_dot(x, y, 8, WE_PEARL)
s.poly([(C, 150), (C + 56, 230), (C + 64, 380), (C + 34, 450), (C - 34, 450), (C - 64, 380), (C - 56, 230)], WE_HULL)
s.poly([(C, 190), (C + 34, 250), (C + 38, 380), (C - 38, 380), (C - 34, 250)], WE_DARK)
for i, y in enumerate((230, 270, 310)):
s.line((C - 30 + i * 4, y), (C + 30 - i * 4, y), WE_VIOLET, 6)
s.oval((C - 14, 230, C + 14, 276), fill=WE_CANY)
s.glow_dot(C, 176, 12, WE_PEARL)
s.glow_dot(C - 20, 444, 11, WE_ENGINE)
s.glow_dot(C + 20, 444, 11, WE_ENGINE)
return s
def dreamer():
"""45 — the soft dome shell, the inner glow, the closed eyes."""
s = Ship()
s.oval((C - 84, 120, C + 84, 404), fill=WE_HULL)
s.oval((C - 84, 120, C + 84, 404), outline=WE_EDGE, width=8)
s.oval((C - 58, 156, C + 58, 380), fill=WE_DARK)
s.oval((C - 40, 190, C + 40, 330), fill=(120, 104, 160, 120))
s.glow_dot(C, 260, 34, WE_VIOLET)
s.arc((C - 30, 210, C + 30, 260), 200, 340, WE_PEARL, 6) # the closed eyes
for sx in (-1, 1):
s.poly([(C + sx * 60, 350), (C + sx * 110, 400), (C + sx * 70, 430)], WE_LITE)
s.oval((C - 10, 120, C + 10, 150), fill=WE_CYAN)
s.glow_dot(C - 22, 440, 11, WE_ENGINE)
s.glow_dot(C + 22, 440, 11, WE_ENGINE)
return s
def serpent():
"""46 — the winding tail, the three eye-orbs."""
s = Ship()
pts_top, pts_bot = [], []
for k in range(17):
t = k / 16
x = C + math.sin(t * math.pi * 2.4) * 58
y = 60 + t * 380
wdt = 26 * (1 - t * 0.55)
pts_top.append((x - wdt, y))
pts_bot.append((x + wdt, y))
s.poly(pts_top + pts_bot[::-1], WE_HULL)
for k in range(17):
t = k / 16
x = C + math.sin(t * math.pi * 2.4) * 58
y = 60 + t * 380
wdt = 14 * (1 - t * 0.55)
s.oval((x - wdt, y - wdt, x + wdt, y + wdt), fill=(120, 104, 160, 90) if k % 2 else (WE_DARK[0], WE_DARK[1], WE_DARK[2], 255))
s.oval((C - 44, 40, C + 44, 124), fill=WE_LITE)
s.oval((C - 44, 40, C + 44, 124), outline=WE_EDGE, width=6)
for dx in (-18, 0, 18):
s.glow_dot(C + dx, 82, 9, WE_CYAN)
s.line((C - 30, 130), (C + 30, 130), WE_VIOLET, 6)
s.glow_dot(C, 470, 10, WE_ENGINE)
return s
def overmind():
"""47 — the crown of six orbs, the triple halo, the great core."""
s = Ship()
for r in (124, 96, 68):
s.oval((C - r, 256 - r, C + r, 256 + r), outline=WE_EDGE, width=5)
for a in range(0, 360, 60):
x = C + 124 * math.cos(math.radians(a))
y = 256 + 124 * math.sin(math.radians(a))
s.glow_dot(x, y, 12, WE_PEARL)
s.oval((C - 46, 210, C + 46, 302), fill=WE_DARK)
s.oval((C - 46, 210, C + 46, 302), outline=WE_VIOLET, width=8)
s.glow_dot(C, 256, 30, WE_CYAN)
s.oval((C - 12, 240, C + 12, 272), fill=WE_CANY)
s.oval((C - 26, 108, C + 26, 150), fill=WE_HULL)
s.oval((C - 26, 362, C + 26, 404), fill=WE_HULL)
s.glow_dot(C, 96, 12, WE_VIOLET)
s.glow_dot(C, 416, 12, WE_VIOLET)
s.glow_dot(C - 60, 452, 9, WE_ENGINE)
s.glow_dot(C + 60, 452, 9, WE_ENGINE)
return s
APEX_SHIPS = [raptor, mauler, stalker, screamer, reaver, fang, tyrant, alpha]
WEAVE_SHIPS = [mindblade, telekine, phantasm, weaver, lure, dreamer, serpent, overmind]
def main():
im = Image.open(SHEET).convert('RGBA')
for i, maker in enumerate(APEX_SHIPS):
im.paste(maker().finish(), (i * CELL, 3 * CELL)) # frames 30-37 (row 3)
for i, maker in enumerate(WEAVE_SHIPS):
im.paste(maker().finish(), (i * CELL, 4 * CELL)) # frames 40-47 (row 4)
im.save(SHEET)
print('painted 16 frames into', SHEET)
if __name__ == '__main__':
main()