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