singe/util/openRoadArt.py

219 lines
8.5 KiB
Python

# Draws the art for testScripts/openRoad, a road-racing game drawn a screen row at a time: every
# picture is made from shapes here, so it is ours, small, and can be drawn again.
#
# The road is not a picture of a road. It is a strip of one-pixel rows, each a whole cross-section
# of road or grass, which the game cuts out and stretches to the road's width at every row of the
# screen. The rows come in four kinds (light road, dark road, light grass, dark grass, alternating
# down the track to show it moving) at HAZE levels each, fading toward the sky's colour, so the far
# road fades with no blending at all.
#
# The cars and the scenery are one picture each. The game leans the player's car into turns and spins
# it in a crash by turning that one picture.
#
# Usage: python3 util/openRoadArt.py
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", "openRoad", "art")
W = 320 # The game's overlay
H = 240
HORIZON_Y = 110 # Where the horizon sits on a level road; the game assumes it
STRIP_W = 256 # A road row's width in pixels, edge of rumble to edge of rumble
RUMBLE_W = 16 # Each rumble strip's share of it
LINE_W = 4 # A lane marking
LANES = 3
HAZE = 8 # Fade levels for each kind of row, nearest first
HAZE_RGB = (196, 214, 226) # What the far road fades into: the sky at the horizon
KINDS = ( # Each kind of row: the colours across it
{"road": (112, 112, 118), "rumble": (240, 240, 240), "lines": True},
{"road": (104, 104, 110), "rumble": (200, 40, 40), "lines": False},
{"grass": (72, 150, 64)},
{"grass": (62, 134, 56)},
)
def blank(width, height):
return Image.new("RGBA", (width, height), (0, 0, 0, 0))
def faded(colour, level):
t = level / (HAZE - 1) * 0.85
return tuple(int(colour[c] * (1 - t) + HAZE_RGB[c] * t) for c in range(3)) + (255,)
def save(image, name):
image.save(os.path.join(OUT, name))
print("%-14s %d x %d" % (name, image.width, image.height))
# One row of road: rumble, asphalt with its lane markings, and rumble again.
def roadRow(kind, level):
row = [faded(kind["road"], level)] * STRIP_W
edge = faded(kind["rumble"], level)
for x in range(RUMBLE_W):
row[x] = edge
row[STRIP_W - 1 - x] = edge
if kind["lines"]:
asphalt = STRIP_W - RUMBLE_W * 2
for lane in range(1, LANES):
centre = RUMBLE_W + asphalt * lane // LANES
for x in range(centre - LINE_W // 2, centre + LINE_W // 2):
row[x] = faded((236, 236, 236), level)
return row
# Every row kind at every haze level: row kind * HAZE + level.
def strip():
image = Image.new("RGBA", (STRIP_W, len(KINDS) * HAZE))
pixels = image.load()
for k, kind in enumerate(KINDS):
for level in range(HAZE):
row = roadRow(kind, level) if "road" in kind else [faded(kind["grass"], level)] * STRIP_W
for x in range(STRIP_W):
pixels[x, k * HAZE + level] = row[x]
return image
# The sky, a sun, and below the horizon the hazy colour of far land, for when the road drops away
# over a crest and leaves the ground beyond it showing.
def sky():
image = Image.new("RGBA", (W, H))
draw = ImageDraw.Draw(image)
for y in range(H):
if y <= HORIZON_Y:
t = y / HORIZON_Y
draw.line([(0, y), (W, y)], fill=(int(60 + 136 * t), int(120 + 94 * t), int(210 + 16 * t), 255))
else:
draw.line([(0, y), (W, y)], fill=faded((72, 150, 64), HAZE - 1))
draw.ellipse([226, 22, 262, 58], fill=(255, 244, 200, 255))
clouds = blank(W, H)
cloudy = ImageDraw.Draw(clouds)
for box in ((20, 30, 80, 46), (50, 24, 110, 44), (140, 56, 200, 68), (270, 70, 320, 82)):
cloudy.ellipse(box, fill=(255, 255, 255, 200))
return Image.alpha_composite(image, clouds.filter(ImageFilter.GaussianBlur(2)))
# Hills along the horizon, twice the screen's width so they can scroll round as the road curves, with
# their foot on the picture's bottom row.
def hills():
width = W * 2
height = 48
image = blank(width, height)
draw = ImageDraw.Draw(image)
ranges = (
(((0, 30), (70, 8), (150, 26), (230, 4), (330, 22), (420, 10), (520, 28), (600, 12), (640, 30)), (120, 150, 170)),
(((0, 40), (40, 30), (110, 38), (190, 24), (260, 36), (360, 28), (440, 40), (500, 26), (580, 34), (640, 40)), (90, 128, 120)),
)
for peaks, shade in ranges:
draw.polygon([(0, height)] + list(peaks) + [(width, height)], fill=shade + (255,))
return image
# The player's car from behind: body, rear window, lights, and tyres.
def car():
image = blank(80, 44)
draw = ImageDraw.Draw(image)
draw.rectangle([4, 32, 18, 43], fill=(30, 30, 30, 255))
draw.rectangle([61, 32, 75, 43], fill=(30, 30, 30, 255))
draw.polygon([(2, 20), (10, 14), (70, 14), (78, 20), (78, 38), (2, 38)], fill=(210, 30, 30, 255))
draw.polygon([(16, 14), (22, 2), (58, 2), (64, 14)], fill=(180, 24, 24, 255))
draw.polygon([(20, 13), (25, 4), (55, 4), (60, 13)], fill=(60, 80, 110, 255))
draw.rectangle([6, 22, 20, 27], fill=(255, 90, 60, 255))
draw.rectangle([60, 22, 74, 27], fill=(255, 90, 60, 255))
draw.rectangle([30, 30, 50, 35], fill=(240, 220, 120, 255))
draw.line([(2, 38), (78, 38)], fill=(120, 16, 16, 255), width=2)
return image
# Traffic from behind: a plainer car, in blue, so it never reads as the player's.
def traffic():
image = blank(64, 40)
draw = ImageDraw.Draw(image)
draw.rectangle([4, 30, 14, 39], fill=(30, 30, 30, 255))
draw.rectangle([49, 30, 59, 39], fill=(30, 30, 30, 255))
draw.polygon([(2, 16), (8, 12), (56, 12), (62, 16), (62, 34), (2, 34)], fill=(50, 90, 190, 255))
draw.polygon([(12, 12), (17, 1), (47, 1), (52, 12)], fill=(40, 74, 160, 255))
draw.polygon([(16, 11), (20, 3), (44, 3), (48, 11)], fill=(150, 180, 210, 255))
draw.rectangle([5, 19, 14, 23], fill=(255, 80, 60, 255))
draw.rectangle([49, 19, 58, 23], fill=(255, 80, 60, 255))
return image
def truck():
image = blank(64, 72)
draw = ImageDraw.Draw(image)
draw.rectangle([4, 62, 16, 71], fill=(30, 30, 30, 255))
draw.rectangle([47, 62, 59, 71], fill=(30, 30, 30, 255))
draw.rectangle([1, 2, 62, 62], fill=(230, 220, 190, 255), outline=(120, 110, 90, 255))
draw.line([(32, 4), (32, 60)], fill=(150, 140, 120, 255), width=2)
draw.rectangle([3, 54, 12, 58], fill=(255, 80, 60, 255))
draw.rectangle([51, 54, 60, 58], fill=(255, 80, 60, 255))
return image
def palm():
image = blank(56, 112)
draw = ImageDraw.Draw(image)
for i in range(14):
x = 26 + int((i / 13) ** 2 * 6)
draw.rectangle([x, 26 + i * 6, x + 5, 32 + i * 6], fill=(130 - i * 2, 90, 50, 255))
for points in (((28, 26), (2, 30), (8, 22)), ((28, 26), (54, 30), (48, 22)), ((28, 26), (10, 8), (18, 6)),
((28, 26), (46, 8), (38, 6)), ((28, 26), (26, 0), (32, 2)), ((28, 26), (0, 44), (6, 36)), ((28, 26), (56, 44), (50, 36))):
draw.polygon(points, fill=(40, 130, 50, 255))
draw.ellipse([22, 20, 34, 30], fill=(90, 70, 40, 255))
return image
def bush():
image = blank(48, 24)
draw = ImageDraw.Draw(image)
for box in ((0, 8, 22, 24), (12, 2, 36, 24), (26, 6, 48, 24)):
draw.ellipse(box, fill=(46, 110, 44, 255))
return image
# A curve warning: a board on two posts with a mark that reads the same either way, since the game
# cuts hill-hidden scenery with spriteDrawGrid, which draws the picture unmirrored.
def sign():
image = blank(56, 56)
draw = ImageDraw.Draw(image)
draw.rectangle([10, 30, 14, 55], fill=(90, 90, 90, 255))
draw.rectangle([42, 30, 46, 55], fill=(90, 90, 90, 255))
draw.rectangle([0, 2, 55, 34], fill=(250, 210, 40, 255), outline=(40, 40, 40, 255))
draw.rectangle([25, 6, 30, 22], fill=(30, 30, 30, 255))
draw.rectangle([25, 26, 30, 30], fill=(30, 30, 30, 255))
return image
# The start and checkpoint arch over the road: two pillars and a banner, open in between.
def arch():
image = blank(256, 96)
draw = ImageDraw.Draw(image)
draw.rectangle([0, 0, 15, 95], fill=(220, 220, 220, 255), outline=(80, 80, 80, 255))
draw.rectangle([240, 0, 255, 95], fill=(220, 220, 220, 255), outline=(80, 80, 80, 255))
draw.rectangle([0, 0, 255, 22], fill=(30, 30, 30, 255))
for x in range(0, 256, 16):
draw.rectangle([x, 2 + (x // 16 % 2) * 9, x + 7, 10 + (x // 16 % 2) * 9], fill=(250, 250, 250, 255))
return image
def main():
os.makedirs(OUT, exist_ok=True)
save(strip(), "strip.png")
save(sky(), "sky.png")
save(hills(), "hills.png")
save(car(), "car.png")
save(traffic(), "traffic.png")
save(truck(), "truck.png")
save(palm(), "palm.png")
save(bush(), "bush.png")
save(sign(), "sign.png")
save(arch(), "arch.png")
if __name__ == "__main__":
main()