# 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()