147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
# 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("<h", int(value * 32000))
|
|
path = os.path.join(OUT, name)
|
|
with wave.open(path, "wb") as out:
|
|
out.setnchannels(1)
|
|
out.setsampwidth(2)
|
|
out.setframerate(RATE)
|
|
out.writeframes(bytes(data))
|
|
print("%s %.2f seconds" % (name, seconds))
|
|
|
|
|
|
# A shot: a tone sliding down fast, which is the oldest arcade sound there is.
|
|
def shootShape(t, done):
|
|
pitch = 900.0 - 600.0 * done
|
|
return math.sin(2.0 * math.pi * pitch * t) * (1.0 - done) ** 2
|
|
|
|
|
|
# A bang: noise shaped by a falling envelope, with a low thump under it.
|
|
def boomShape(t, done):
|
|
seed = int(t * RATE)
|
|
noise = (((seed * 1103515245 + 12345) >> 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()
|