singe/util/skyStrikeArt.py

157 lines
6 KiB
Python

# Draws the art for testScripts/skyStrike, a sprite-scaling flight game: every picture is made from
# shapes here, so it is ours, small, and can be drawn again.
#
# One picture of each thing and no more. The game turns the jet for its bank and its roll, turns the
# whole sky and ground for the horizon, and scales everything in the distance as it is drawn, so
# nothing here exists in a second size or at a second angle. The only sheet is the ground: six
# frames of perspective stripes, each a sixth of a stripe further on, which cycle to make the ground
# rush past.
#
# Usage: python3 util/skyStrikeArt.py
import math
import os
from PIL import Image, ImageDraw, ImageFilter
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(REPO, "testScripts", "skyStrike", "art")
# The ground sheet. Turned, it must still cover a 320 x 240 screen with the horizon anywhere within
# PITCH of the middle, so it is wider than the screen's diagonal plus that.
GROUND_SIZE = 560
GROUND_FRAMES = 6
EYE_HEIGHT = 30.0 # The camera's height over the ground, in the game's world units
FOCAL = 160.0 # The game's projection: screen offset = world offset * FOCAL / distance
STRIPE = 40.0 # World units from one ground stripe to the next of the same colour
def blank(width, height):
return Image.new("RGBA", (width, height), (0, 0, 0, 0))
def save(image, name):
image.save(os.path.join(OUT, name))
print("%-14s %d x %d" % (name, image.width, image.height))
# The player's jet from behind: fuselage, swept wings, tailplanes, a fin, and two glowing exhausts.
def jet():
image = blank(64, 40)
draw = ImageDraw.Draw(image)
draw.polygon([(2, 24), (32, 16), (62, 24), (62, 27), (32, 22), (2, 27)], fill=(120, 130, 150, 255))
draw.polygon([(14, 30), (32, 26), (50, 30), (50, 32), (14, 32)], fill=(100, 110, 130, 255))
draw.polygon([(30, 2), (34, 2), (35, 24), (29, 24)], fill=(140, 150, 170, 255))
draw.ellipse([22, 14, 42, 34], fill=(160, 170, 190, 255), outline=(60, 65, 80, 255))
draw.ellipse([25, 23, 31, 29], fill=(255, 170, 60, 255))
draw.ellipse([33, 23, 39, 29], fill=(255, 170, 60, 255))
draw.ellipse([27, 25, 29, 27], fill=(255, 250, 200, 255))
draw.ellipse([35, 25, 37, 27], fill=(255, 250, 200, 255))
draw.rectangle([29, 15, 35, 18], fill=(70, 110, 160, 255))
return image
# An enemy jet seen head on: darker, with a red nose, so it reads against the sky at any size.
def enemy():
image = blank(48, 28)
draw = ImageDraw.Draw(image)
draw.polygon([(1, 14), (24, 9), (47, 14), (47, 17), (24, 14), (1, 17)], fill=(70, 70, 80, 255))
draw.polygon([(23, 0), (25, 0), (26, 14), (22, 14)], fill=(90, 90, 100, 255))
draw.ellipse([16, 8, 32, 24], fill=(100, 100, 112, 255), outline=(30, 30, 36, 255))
draw.ellipse([21, 13, 27, 19], fill=(210, 40, 40, 255))
draw.rectangle([10, 17, 14, 21], fill=(55, 55, 62, 255))
draw.rectangle([34, 17, 38, 21], fill=(55, 55, 62, 255))
return image
def tree():
image = blank(32, 56)
draw = ImageDraw.Draw(image)
draw.rectangle([14, 42, 18, 55], fill=(90, 60, 35, 255))
for top, half, shade in ((2, 8, (30, 110, 45)), (14, 12, (25, 95, 40)), (26, 15, (20, 80, 35))):
draw.polygon([(16, top), (16 + half, top + 18), (16 - half, top + 18)], fill=shade + (255,))
return image
def rock():
image = blank(40, 24)
draw = ImageDraw.Draw(image)
draw.polygon([(2, 23), (8, 9), (17, 3), (28, 6), (37, 15), (39, 23)], fill=(125, 115, 105, 255))
draw.polygon([(17, 3), (28, 6), (24, 14), (14, 12)], fill=(160, 150, 140, 255))
return image
def cloud():
image = blank(96, 40)
draw = ImageDraw.Draw(image)
for box in ((6, 16, 46, 38), (26, 6, 66, 36), (50, 12, 90, 38), (36, 18, 80, 39)):
draw.ellipse(box, fill=(250, 250, 255, 230))
return image.filter(ImageFilter.GaussianBlur(1.2))
def shot():
image = blank(12, 12)
draw = ImageDraw.Draw(image)
draw.ellipse([0, 0, 11, 11], fill=(255, 200, 80, 160))
draw.ellipse([3, 3, 8, 8], fill=(255, 255, 220, 255))
return image
# A fireball: rings from white to dark red, ragged at the edge so it shows when it spins.
def fireball():
image = blank(48, 48)
draw = ImageDraw.Draw(image)
spikes = []
for i in range(16):
angle = i * math.pi * 2 / 16
radius = 23 if i % 2 == 0 else 15
spikes.append((24 + math.cos(angle) * radius, 24 + math.sin(angle) * radius))
draw.polygon(spikes, fill=(180, 40, 20, 230))
draw.ellipse([8, 8, 40, 40], fill=(240, 120, 30, 240))
draw.ellipse([15, 15, 33, 33], fill=(255, 220, 90, 255))
draw.ellipse([20, 20, 28, 28], fill=(255, 255, 240, 255))
return image
# The sky and the ground, one square picture with the horizon through its middle, in frames that
# move the stripes a sixth of a stripe on each. A row d pixels below the horizon is ground EYE_HEIGHT
# * FOCAL / d away, which is what spaces the stripes like perspective with no 3D at all.
def ground():
half = GROUND_SIZE // 2
sheet = blank(GROUND_SIZE * GROUND_FRAMES, GROUND_SIZE)
for frame in range(GROUND_FRAMES):
image = Image.new("RGBA", (GROUND_SIZE, GROUND_SIZE))
pixels = image.load()
phase = STRIPE * frame / GROUND_FRAMES
for row in range(GROUND_SIZE):
if row < half:
# Sky: deep blue overhead to pale at the horizon.
t = row / half
colour = (int(40 + 120 * t), int(90 + 110 * t), int(190 + 55 * t), 255)
else:
d = row - half + 0.5
z = EYE_HEIGHT * FOCAL / d
band = int(math.floor((z + phase) / (STRIPE / 2))) % 2
haze = min(1.0, z / 1500.0)
base = (70, 140, 60) if band == 0 else (55, 120, 50)
colour = tuple(int(base[c] * (1 - haze) + (150, 185, 210)[c] * haze) for c in range(3)) + (255,)
for column in range(GROUND_SIZE):
pixels[column, row] = colour
sheet.paste(image, (frame * GROUND_SIZE, 0))
return sheet
def main():
os.makedirs(OUT, exist_ok=True)
save(jet(), "jet.png")
save(enemy(), "enemy.png")
save(tree(), "tree.png")
save(rock(), "rock.png")
save(cloud(), "cloud.png")
save(shot(), "shot.png")
save(fireball(), "fireball.png")
save(ground(), "ground.png")
if __name__ == "__main__":
main()