157 lines
6 KiB
Python
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()
|