# Draws and synthesises the art kit the beginner's course uses, into docs/learn/art. # # A tutorial cannot say "find a picture of a spaceship": the pictures have to be there, named the # same on every machine, or half the readers stop at lesson nine. Everything here is drawn from # shapes rather than painted, so it is ours, it is tiny, and it can be regenerated. The engine # already gives every reader a video, two models, a font, and a click (the Singe folder it unpacks # on the first run), so this kit is only the things a 2D game needs: a ship, a rock, a shot, a # star, one walk cycle, and two noises. # # Usage: python3 util/learnKit.py import math import os import struct import wave from PIL import Image, ImageDraw REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT = os.path.join(REPO, "docs", "learn", "art") RATE = 22050 def blank(width, height): return Image.new("RGBA", (width, height), (0, 0, 0, 0)) def save(image, name): path = os.path.join(OUT, name) image.save(path) print("%s %dx%d" % (name, image.width, image.height)) # A ship pointing up: a hull, a nose, and two fins, so it reads at 32 pixels. def ship(): image = blank(32, 24) draw = ImageDraw.Draw(image) draw.polygon([(16, 0), (23, 17), (9, 17)], fill=(120, 200, 255, 255)) draw.polygon([(16, 3), (20, 16), (12, 16)], fill=(230, 245, 255, 255)) draw.polygon([(9, 12), (2, 22), (9, 20)], fill=(70, 130, 200, 255)) draw.polygon([(23, 12), (30, 22), (23, 20)], fill=(70, 130, 200, 255)) draw.rectangle([13, 17, 18, 21], fill=(255, 170, 60, 255)) draw.rectangle([14, 19, 17, 23], fill=(255, 230, 120, 255)) return image # A rock: an uneven lump with a couple of craters, so a rotation would not be missed. def rock(): image = blank(24, 24) draw = ImageDraw.Draw(image) points = [(12, 1), (19, 4), (23, 11), (20, 19), (13, 23), (5, 21), (1, 13), (3, 5)] draw.polygon(points, fill=(150, 140, 130, 255)) draw.polygon([(12, 3), (17, 6), (19, 11), (16, 17), (11, 20), (6, 18), (4, 12), (6, 6)], fill=(185, 175, 165, 255)) draw.ellipse([8, 7, 12, 11], fill=(140, 130, 120, 255)) draw.ellipse([14, 13, 17, 16], fill=(140, 130, 120, 255)) return image def shot(): image = blank(4, 10) draw = ImageDraw.Draw(image) draw.rectangle([0, 2, 3, 9], fill=(255, 210, 90, 255)) draw.rectangle([1, 0, 2, 9], fill=(255, 255, 220, 255)) return image def star(): image = blank(8, 8) draw = ImageDraw.Draw(image) draw.line([4, 0, 4, 7], fill=(255, 240, 160, 255)) draw.line([0, 4, 7, 4], fill=(255, 240, 160, 255)) draw.point([(3, 3), (5, 3), (3, 5), (5, 5)], fill=(255, 210, 90, 255)) return image # Four frames of a walk, side by side in one picture: the sheet the frames lesson steps through. # The legs swing, the arms swing the other way, and frames two and four differ so the cycle reads. def walk(): image = blank(24 * 4, 32) draw = ImageDraw.Draw(image) swing = [(-5, 5), (0, 0), (5, -5), (0, 0)] for frame in range(4): x = frame * 24 + 12 lead = swing[frame][0] back = swing[frame][1] bob = 1 if frame % 2 == 0 else 0 draw.ellipse([x - 4, 3 + bob, x + 4, 11 + bob], fill=(245, 205, 160, 255)) draw.rectangle([x - 4, 12 + bob, x + 4, 22 + bob], fill=(80, 130, 200, 255)) draw.line([x - 4, 13 + bob, x - 4 - abs(back), 19 + bob], fill=(245, 205, 160, 255), width=2) draw.line([x + 4, 13 + bob, x + 4 + abs(lead), 19 + bob], fill=(245, 205, 160, 255), width=2) draw.line([x - 1, 22 + bob, x - 1 + back, 30], fill=(60, 60, 90, 255), width=3) draw.line([x + 1, 22 + bob, x + 1 + lead, 30], fill=(40, 40, 70, 255), width=3) return image # A noise, written straight out as a 16 bit mono WAV: no library, and small enough to read. def tone(name, seconds, shape): frames = int(RATE * seconds) data = bytearray() for i in range(frames): t = i / RATE value = shape(t, t / seconds) value = max(-1.0, min(1.0, value)) data += struct.pack("> 16) % 2000 - 1000) / 1000.0 return (noise * 0.8 + math.sin(2.0 * math.pi * 70.0 * t) * 0.5) * (1.0 - done) ** 3 def main(): os.makedirs(OUT, exist_ok=True) save(ship(), "ship.png") save(rock(), "rock.png") save(shot(), "shot.png") save(star(), "star.png") save(walk(), "walk.png") tone("shoot.wav", 0.18, shootShape) tone("boom.wav", 0.45, boomShape) if __name__ == "__main__": main()