219 lines
8.5 KiB
Python
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()
|