#!/usr/bin/env python3"""Draws every picture of the game (assets/): python3 tools/art.py (numpy + Pillow)Everything is made by this script: no picture comes from anywhere else."""import json, math, os, randomimport numpy as npfrom PIL import Image, ImageDraw, ImageFilter, ImageFontROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'assets')W, H = 480, 232rng = np.random.default_rng(7)random.seed(7)def out(name): path = os.path.join(ROOT, name) os.makedirs(os.path.dirname(path), exist_ok=True) return pathdef hexc(s, a=255): s = s.lstrip('#') return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16), a)def lerp(a, b, t): return tuple(int(round(a[i] + (b[i] - a[i]) * t)) for i in range(3))def gradient(w, h, stops, dither=1.2): """vertical gradient, stops = [(pos 0..1, '#hex'), ...], lightly dithered (no banding)""" img = np.zeros((h, w, 3), dtype=np.float32) for y in range(h): t = y / (h - 1) for i in range(len(stops) - 1): if stops[i][0] <= t <= stops[i + 1][0]: k = (t - stops[i][0]) / max(1e-6, stops[i + 1][0] - stops[i][0]) img[y, :] = lerp(hexc(stops[i][1]), hexc(stops[i + 1][1]), k) break img += rng.uniform(-dither, dither, (h, w, 1)) if dither else 0 return Image.fromarray(np.clip(img, 0, 255).astype(np.uint8), 'RGB').convert('RGBA')def glow(size, color, power=2.0, strength=1.0): c = hexc(color) y, x = np.mgrid[0:size, 0:size] d = np.sqrt((x - size / 2 + 0.5) ** 2 + (y - size / 2 + 0.5) ** 2) / (size / 2) a = np.clip(1 - d, 0, 1) ** power * 255 * strength img = np.zeros((size, size, 4), dtype=np.uint8) img[..., 0], img[..., 1], img[..., 2] = c[0], c[1], c[2] img[..., 3] = np.clip(a, 0, 255) return Image.fromarray(img, 'RGBA')def tile_noise(size, cells, seed): """tileable value noise 0..1""" r = np.random.default_rng(seed) g = r.random((cells, cells)) ys, xs = np.mgrid[0:size, 0:size] * cells / size x0, y0 = xs.astype(int) % cells, ys.astype(int) % cells x1, y1 = (x0 + 1) % cells, (y0 + 1) % cells fx, fy = xs - np.floor(xs), ys - np.floor(ys) fx, fy = fx * fx * (3 - 2 * fx), fy * fy * (3 - 2 * fy) return (g[y0, x0] * (1 - fx) + g[y0, x1] * fx) * (1 - fy) + (g[y1, x0] * (1 - fx) + g[y1, x1] * fx) * fydef earth_tile(base, dark, light, speck, seed): n = tile_noise(64, 4, seed) * 0.55 + tile_noise(64, 8, seed + 1) * 0.3 + tile_noise(64, 16, seed + 2) * 0.15 b, d, l = np.array(hexc(base)[:3]), np.array(hexc(dark)[:3]), np.array(hexc(light)[:3]) img = np.zeros((64, 64, 3)) t = n[..., None] img = np.where(t < 0.5, d + (b - d) * (t / 0.5), b + (l - b) * ((t - 0.5) / 0.5)) # bands: a few quantised levels keep the pixel-art look img = np.round(img / 10) * 10 im = Image.fromarray(np.clip(img, 0, 255).astype(np.uint8), 'RGB').convert('RGBA') px = im.load() r = random.Random(seed) for _ in range(26): # pebbles x, y = r.randrange(64), r.randrange(64) c = hexc(speck) px[x, y] = c px[(x + 1) % 64, y] = c px[x, (y + 1) % 64] = hexc(dark) return imdef steel_tile(): im = Image.new('RGBA', (32, 32), hexc('#56627a')) d = ImageDraw.Draw(im) for by in (0, 16): for bx in (0, 16): d.rectangle([bx, by, bx + 15, by + 15], fill=hexc('#5d6a84')) d.line([bx, by, bx + 15, by], fill=hexc('#8fa0bd')) d.line([bx, by, bx, by + 15], fill=hexc('#7d8dab')) d.line([bx, by + 15, bx + 15, by + 15], fill=hexc('#333c50')) d.line([bx + 15, by, bx + 15, by + 15], fill=hexc('#3c465c')) for rx, ry in ((2, 2), (12, 2), (2, 12), (12, 12)): d.point((bx + rx, by + ry), fill=hexc('#c5d2ea')) d.point((bx + rx + 1, by + ry + 1), fill=hexc('#2c3446')) d.line([bx + 5, by + 10, bx + 10, by + 5], fill=hexc('#6c7a96')) return imdef ridge(width, base, amp, seeds, sharp=1.0): """a tileable skyline: y of the ridge for each x""" xs = np.arange(width) y = np.zeros(width) for k, (freq, a, ph) in enumerate(seeds): y += a * np.sin(2 * math.pi * freq * xs / width + ph) y = np.abs(y) ** sharp * np.sign(y) return base - amp * ydef silhouette(width, height, ys, top, bottom, alpha=255): img = np.zeros((height, width, 4), dtype=np.uint8) t, b = hexc(top), hexc(bottom) for x in range(width): y0 = int(max(0, min(height - 1, ys[x]))) for y in range(y0, height): k = (y - y0) / max(1, height - y0) img[y, x, :3] = lerp(t, b, min(1, k * 1.4)) img[y, x, 3] = alpha return Image.fromarray(img, 'RGBA')def pines(im, color, count, ymin, ymax, hmin, hmax, seed): d = ImageDraw.Draw(im) r = random.Random(seed) w = im.size[0] for i in range(count): x = int((i + r.random() * 0.8) * w / count) base = r.randint(ymin, ymax) h = r.randint(hmin, hmax) for ox in (-w, 0, w): for k in range(4): ww = (4 - k) * h // 9 + 2 yy = base - k * h // 4 d.polygon([(x + ox - ww, yy), (x + ox, yy - h // 3 - 3), (x + ox + ww, yy)], fill=color) d.rectangle([x + ox - 1, base, x + ox + 1, base + 6], fill=color)def crystals(im, colors, count, ymin, ymax, seed, down=False): d = ImageDraw.Draw(im) r = random.Random(seed) w = im.size[0] for i in range(count): x = int((i + r.random()) * w / count) base = r.randint(ymin, ymax) h = r.randint(14, 44) ww = r.randint(3, 8) lean = r.randint(-6, 6) c1, c2 = hexc(colors[0]), hexc(colors[1]) s = 1 if down else -1 for ox in (-w, 0, w): d.polygon([(x + ox - ww, base), (x + ox + lean, base + s * h), (x + ox, base)], fill=c1) d.polygon([(x + ox, base), (x + ox + lean, base + s * h), (x + ox + ww, base)], fill=c2)def blob_trees(im, colors, count, ymin, ymax, rmin, rmax, seed): """round autumn trees: a trunk and a crown of overlapping blobs (wraps around)""" d = ImageDraw.Draw(im) r = random.Random(seed) w = im.size[0] for i in range(count): x = int((i + r.random() * 0.7) * w / count) base = r.randint(ymin, ymax) rad = r.randint(rmin, rmax) for ox in (-w, 0, w): d.rectangle([x + ox - 1, base - rad, x + ox + 1, base + 8], fill=hexc(colors[-1])) for k in range(5): bx = x + ox + r.randint(-rad // 2, rad // 2) by = base - rad - r.randint(0, rad) br = r.randint(rad // 2, rad) d.ellipse([bx - br, by - br, bx + br, by + br], fill=hexc(colors[k % (len(colors) - 1)]))def gear(d, cx, cy, rad, teeth, color, hole=(0, 0, 0, 0)): pts = [] for i in range(teeth * 4): a = i * 2 * math.pi / (teeth * 4) rr = rad if i % 4 in (0, 1) else rad * 0.8 pts.append((cx + rr * math.cos(a), cy + rr * math.sin(a))) d.polygon(pts, fill=color) d.ellipse([cx - rad * 0.32, cy - rad * 0.32, cx + rad * 0.32, cy + rad * 0.32], fill=hole)def gears(im, color, count, ymin, ymax, rmin, rmax, seed): d = ImageDraw.Draw(im) r = random.Random(seed) w = im.size[0] for i in range(count): x = int((i + r.random()) * w / count) y = r.randint(ymin, ymax) rad = r.randint(rmin, rmax) for ox in (-w, 0, w): gear(d, x + ox, y, rad, max(6, rad // 3), hexc(color))def seaweed(im, colors, count, ymin, ymax, seed): d = ImageDraw.Draw(im) r = random.Random(seed) w = im.size[0] for i in range(count): x = int((i + r.random()) * w / count) base = r.randint(ymin, ymax) h = r.randint(30, 70) c = hexc(colors[i % len(colors)]) for ox in (-w, 0, w): pts = [(x + ox + 3 * math.sin(k / 4 + i), base - k) for k in range(h)] d.line(pts, fill=c, width=3) for k in range(6, h, 9): px, py = pts[k] d.ellipse([px - 3, py - 2, px + 3, py + 2], fill=c)def sky_extra(name, sky, k=1): """what is in the sky of the newer worlds, at scale k (1 retro, 3 HD): light rays and bubbles, a low sun, a forge glow""" d = ImageDraw.Draw(sky) if name == 'reef': rays = Image.new('RGBA', sky.size, (0, 0, 0, 0)) rd = ImageDraw.Draw(rays) for i, x in enumerate((60, 150, 230, 330, 410)): rd.polygon([((x - 6) * k, 0), ((x + 10) * k, 0), ((x + 60 + i * 8) * k, 232 * k), ((x + 20 + i * 8) * k, 232 * k)], fill=(220, 255, 255, 34)) sky.alpha_composite(rays.filter(ImageFilter.GaussianBlur(2 * k))) r = random.Random(5) for _ in range(40): x, y, rad = r.uniform(0, W), r.uniform(0, H), r.choice([1, 1, 1.5, 2, 3]) d.ellipse([(x - rad) * k, (y - rad) * k, (x + rad) * k, (y + rad) * k], outline=(220, 255, 255, 150), width=max(1, k // 2 + 1)) if name == 'autumn': sky.alpha_composite(glow(240 * k, '#ffc890', 1.5, 0.75), (230 * k, 100 * k)) d.ellipse([322 * k, 192 * k, 378 * k, 248 * k], fill=hexc('#ffe8c0')) if name == 'clockwork': sky.alpha_composite(glow(380 * k, '#ffb04c', 1.4, 0.5), (60 * k, 120 * k))THEMES = { 'forest': { 'sky': [(0, '#161a44'), (0.45, '#3b2a6b'), (0.75, '#b0527a'), (1, '#ffb070')], 'earth': ('#7a4b2c', '#4a2a1c', '#a36a3c', '#c9955a'), 'grass': ['#3fe07a', '#22b05c', '#127a44'], 'edge': '#2c170f', 'glow': 'amber', 'liquid': ['#35c8ff', '#1668d8', '#0c2f86'], 'mote': '#ffe9a0', 'fog': [255, 150, 110], }, 'cave': { 'sky': [(0, '#050818'), (0.5, '#0c1c3c'), (1, '#12404c')], 'earth': ('#3d4a74', '#232a4c', '#56689c', '#8ab4e8'), 'grass': ['#5cf2e0', '#27b8c0', '#167a92'], 'edge': '#141830', 'glow': 'cyan', 'liquid': ['#5cf2e0', '#1e8fb0', '#0b3a5c'], 'mote': '#9af7ff', 'fog': [40, 150, 170], }, 'forge': { 'sky': [(0, '#12060c'), (0.5, '#3a0c14'), (0.85, '#8c2a12'), (1, '#e0641c')], 'earth': ('#4c3a44', '#2a1e28', '#6a5460', '#b08c84'), 'grass': ['#ffb347', '#e0641c', '#8c2a12'], 'edge': '#160c12', 'glow': 'amber', 'liquid': ['#ffe066', '#ff7a1c', '#b01c0c'], 'mote': '#ffb347', 'fog': [255, 90, 30], }, 'night': { 'sky': [(0, '#030414'), (0.5, '#0c1040'), (0.85, '#2c2a78'), (1, '#5c4a9c')], 'earth': ('#4a5a8c', '#2a3158', '#6c80b8', '#e8f0ff'), 'grass': ['#ffffff', '#cfe0ff', '#8ea4e0'], 'edge': '#161a36', 'glow': 'cyan', 'liquid': ['#b0e8ff', '#4c8ce8', '#1c2c8c'], 'mote': '#ffffff', 'fog': [120, 120, 255], }, 'desert': { 'sky': [(0, '#2c1848'), (0.4, '#a03c5c'), (0.75, '#f08c3c'), (1, '#ffd86c')], 'earth': ('#c88c4c', '#8c5428', '#e8b46c', '#fff0b0'), 'grass': ['#ffe08c', '#e8a84c', '#b0702c'], 'edge': '#4a2410', 'glow': 'amber', 'liquid': ['#7cf0d8', '#2ca0a0', '#0c4c5c'], 'mote': '#ffe8b0', 'fog': [255, 170, 90], }, 'ice': { 'sky': [(0, '#16307c'), (0.5, '#4c8cd0'), (1, '#c8ecff')], 'earth': ('#8cb8e0', '#5c84c0', '#c0e0f8', '#ffffff'), 'grass': ['#ffffff', '#e0f4ff', '#a8d0f0'], 'edge': '#2c4c84', 'glow': 'cyan', 'liquid': ['#e0f8ff', '#5cb0e8', '#1c4c9c'], 'mote': '#ffffff', 'fog': [200, 230, 255], }, 'jungle': { 'sky': [(0, '#04141c'), (0.5, '#0c3c3c'), (1, '#2c7c5c')], 'earth': ('#3c5c34', '#1c3420', '#5c8444', '#b0f08c'), 'grass': ['#b8ff6c', '#5cd04c', '#2c8c3c'], 'edge': '#0c1c10', 'glow': 'green', 'liquid': ['#b8ff6c', '#3cb06c', '#0c4c3c'], 'mote': '#c8ff8c', 'fog': [90, 220, 140], }, 'sky': { 'sky': [(0, '#2c2484'), (0.45, '#8c5cc8'), (0.8, '#f09cc0'), (1, '#ffe0c0')], 'earth': ('#d8c8e8', '#9c84c0', '#f4ecff', '#ffd23c'), 'grass': ['#ffe08c', '#ffc04c', '#d8902c'], 'edge': '#4c3478', 'glow': 'amber', 'liquid': ['#ffffff', '#c8b0f0', '#7c5cc0'], 'mote': '#fff0c0', 'fog': [255, 200, 230], }, 'autumn': { 'sky': [(0, '#2a1838'), (0.4, '#8c3c4c'), (0.75, '#f0905c'), (1, '#ffd08c')], 'earth': ('#8c5434', '#5a3020', '#b07444', '#e0a060'), 'grass': ['#ffc04c', '#e06c2c', '#a0401c'], 'edge': '#2c140c', 'glow': 'amber', 'liquid': ['#8ce0ff', '#3c8cc8', '#1c3c7c'], 'mote': '#ffb060', 'fog': [255, 150, 90], }, 'reef': { 'sky': [(0, '#5ce0e8'), (0.35, '#1c8cb0'), (1, '#06284c')], 'earth': ('#c89c78', '#8c6450', '#e8c09c', '#ff9c9c'), 'grass': ['#ffa0c0', '#e0507c', '#9c2c5c'], 'edge': '#3c1c2c', 'glow': 'cyan', 'liquid': ['#c8fff0', '#4cd0c0', '#0c6c7c'], 'mote': '#c8ffff', 'fog': [80, 200, 230], }, 'clockwork': { 'sky': [(0, '#100c08'), (0.45, '#3c2814'), (0.8, '#8c5c24'), (1, '#d8a04c')], 'earth': ('#6c5c4c', '#3c3028', '#8c7c68', '#e0b860'), 'grass': ['#ffd23c', '#c89028', '#8c5c18'], 'edge': '#1c140c', 'glow': 'amber', 'liquid': ['#ffe8a0', '#e09c3c', '#8c4c1c'], 'mote': '#ffd890', 'fog': [255, 190, 110], },}def sky_overlay(name, t): """what is in the sky over its gradient: stars, sun, moon, aurora (a transparent layer)""" sky = Image.new('RGBA', (W, H), (0, 0, 0, 0)) d = ImageDraw.Draw(sky) r = random.Random(sum(map(ord, name))) if name in ('forest', 'night', 'cave', 'jungle', 'sky'): for _ in range(140 if name not in ('cave', 'jungle') else 60): x, y = r.randrange(W), int(r.random() ** 1.6 * H * 0.8) a = r.randint(60, 230) d.point((x, y), fill=(255, 255, 255, a) if name != 'cave' else (150, 240, 255, a)) if r.random() < 0.08: d.point((x + 1, y), fill=(255, 255, 255, a // 2)); d.point((x - 1, y), fill=(255, 255, 255, a // 2)) d.point((x, y + 1), fill=(255, 255, 255, a // 2)); d.point((x, y - 1), fill=(255, 255, 255, a // 2)) if name == 'desert': sky.alpha_composite(glow(260, '#fff0b0', 1.5, 0.8), (60, 70)) d.ellipse([162, 172, 218, 228], fill=hexc('#fff8d8')) if name == 'ice': sky.alpha_composite(glow(200, '#ffffff', 1.7, 0.7), (300, -40)) d.ellipse([388, 48, 412, 72], fill=hexc('#ffffff')) if name == 'sky': sky.alpha_composite(glow(300, '#ffd8a0', 1.5, 0.7), (90, 60)) if name == 'forest': # a big low sun sky.alpha_composite(glow(220, '#ffd9a0', 1.6, 0.75), (300, 96)) d.ellipse([382, 178, 438, 234], fill=hexc('#fff0c8')) if name == 'night': # moon + aurora sky.alpha_composite(glow(150, '#b8c8ff', 1.8, 0.6), (330, -10)) moon = Image.new('RGBA', (W, H), (0, 0, 0, 0)) md = ImageDraw.Draw(moon) md.ellipse([388, 44, 424, 80], fill=hexc('#f4f6ff')) md.ellipse([399, 39, 431, 71], fill=(0, 0, 0, 0)) sky.alpha_composite(moon) aur = Image.new('RGBA', (W, H), (0, 0, 0, 0)) ad = ImageDraw.Draw(aur) for x in range(W): top = 40 + 18 * math.sin(x / 37) + 9 * math.sin(x / 11 + 2) for k in range(46): a = int(90 * (1 - k / 46) ** 1.5 * (0.6 + 0.4 * math.sin(x / 5.0 + k / 9))) ad.point((x, int(top + k)), fill=(80, 255, 190, max(0, a))) sky.alpha_composite(aur.filter(ImageFilter.GaussianBlur(1.2))) if name == 'forge': sky.alpha_composite(glow(420, '#ff8a2c', 1.3, 0.55), (30, 110)) sky_extra(name, sky) return skydef backgrounds(): for name, t in THEMES.items(): sky = gradient(W, H, t['sky']) sky.alpha_composite(sky_overlay(name, t)) sky.convert('RGB').save(out(f'bg/{name}-sky.png')) fw = 512 if name == 'forest': far = silhouette(fw, H, ridge(fw, 150, 30, [(2, 0.7, 0.5), (5, 0.4, 2.0), (11, 0.15, 1.0)]), '#5a3a78', '#2c1c50') near = silhouette(fw, H, ridge(fw, 196, 12, [(3, 0.8, 1.2), (7, 0.3, 0.3)]), '#241846', '#140c2c') pines(far, hexc('#3c2862'), 18, 150, 176, 20, 34, 3) pines(near, hexc('#1c1238'), 11, 190, 206, 40, 78, 5) elif name == 'cave': far = silhouette(fw, H, ridge(fw, 170, 26, [(2, 0.6, 1.5), (6, 0.5, 0.2), (13, 0.2, 2.0)], 0.7), '#16305c', '#0a1630') crystals(far, ('#2c6c9c', '#1c4c7c'), 16, 150, 190, 9) crystals(far, ('#1c3c6c', '#142c54'), 14, 0, 0, 12, down=True) near = silhouette(fw, H, ridge(fw, 204, 12, [(3, 0.8, 0.4), (9, 0.3, 1.1)]), '#0c1a38', '#060c1c') crystals(near, ('#5cf2e0', '#27a0c0'), 9, 196, 216, 21) crystals(near, ('#0e2040', '#0a1830'), 12, 0, 0, 33, down=True) elif name == 'forge': far = silhouette(fw, H, ridge(fw, 150, 44, [(1, 0.8, 0.3), (3, 0.5, 1.4), (9, 0.18, 0.5)], 1.4), '#5c1c1c', '#2a0c10') near = silhouette(fw, H, ridge(fw, 198, 16, [(2, 0.7, 2.2), (8, 0.35, 0.6)], 0.6), '#220a10', '#100408') d2 = ImageDraw.Draw(near) r2 = random.Random(4) for i in range(7): # chimneys with a glowing mouth x = int((i + r2.random() * 0.6) * fw / 7) h = r2.randint(40, 90) d2.rectangle([x, 206 - h, x + 9, 232], fill=hexc('#1a080c')) d2.rectangle([x - 2, 206 - h, x + 11, 210 - h], fill=hexc('#2c0e12')) d2.rectangle([x + 2, 204 - h, x + 7, 206 - h], fill=hexc('#ffb347')) elif name == 'desert': far = silhouette(fw, H, ridge(fw, 168, 26, [(1, 0.9, 0.4), (3, 0.5, 2.1)], 0.8), '#b8503c', '#6c2430') near = silhouette(fw, H, ridge(fw, 204, 16, [(2, 0.9, 1.7), (5, 0.3, 0.2)], 0.7), '#4a1c28', '#240c14') d2 = ImageDraw.Draw(near) r2 = random.Random(11) for i in range(6): # cacti x = int((i + r2.random() * 0.7) * fw / 6); h = r2.randint(22, 40) d2.rounded_rectangle([x, 212 - h, x + 5, 232], 2, fill=hexc('#1c0810')) d2.rounded_rectangle([x - 6, 212 - h * 0.6, x - 3, 212 - h * 0.25], 1, fill=hexc('#1c0810')); d2.rectangle([x - 6, 212 - h * 0.3, x, 212 - h * 0.25], fill=hexc('#1c0810')) d2.rounded_rectangle([x + 8, 212 - h * 0.8, x + 11, 212 - h * 0.45], 1, fill=hexc('#1c0810')); d2.rectangle([x + 5, 212 - h * 0.5, x + 11, 212 - h * 0.45], fill=hexc('#1c0810')) elif name == 'ice': ys = ridge(fw, 138, 48, [(2, 0.7, 0.9), (5, 0.45, 2.4), (12, 0.14, 0.3)], 1.5) far = silhouette(fw, H, ys, '#7ca8e0', '#3c64b0') d2 = ImageDraw.Draw(far) for x in range(fw): if ys[x] < 128: d2.line([x, int(ys[x]), x, int(ys[x] + (128 - ys[x]) * 0.5 + 2 * math.sin(x / 3))], fill=hexc('#ffffff')) near = silhouette(fw, H, ridge(fw, 202, 12, [(3, 0.8, 0.2), (8, 0.3, 1.4)]), '#2c4c94', '#142454') crystals(near, ('#e8f8ff', '#9cc8f0'), 12, 196, 214, 17) elif name == 'jungle': far = silhouette(fw, H, ridge(fw, 150, 26, [(2, 0.7, 1.1), (6, 0.4, 0.6), (13, 0.15, 2.2)], 0.8), '#1c5c4c', '#0c2c2c') pines(far, hexc('#164a40'), 22, 146, 176, 26, 48, 13) near = silhouette(fw, H, ridge(fw, 200, 14, [(3, 0.8, 2.0), (7, 0.3, 0.9)]), '#082420', '#041010') pines(near, hexc('#061a18'), 9, 190, 208, 60, 110, 15) crystals(near, ('#8cff9c', '#3cb86c'), 10, 204, 220, 19) elif name == 'sky': def clouds(color, dark, count, ymin, ymax, seed): im = Image.new('RGBA', (fw, H), (0, 0, 0, 0)) dd = ImageDraw.Draw(im); rr = random.Random(seed) for i in range(count): x = int((i + rr.random()) * fw / count); y = rr.randint(ymin, ymax) for k in range(6): w, h = rr.randint(22, 46), rr.randint(10, 20); ox2 = rr.randint(-40, 40) for ox in (-fw, 0, fw): dd.ellipse([x + ox + ox2 - w, y - h + k * 3, x + ox + ox2 + w, y + h + k * 3 + 40], fill=hexc(dark if k > 3 else color)) return im far = clouds('#f4c8e0', '#c898d0', 7, 150, 190, 23) near = clouds('#8c68c0', '#5c3c94', 5, 196, 216, 29) d2 = ImageDraw.Draw(near) r2 = random.Random(31) for i in range(5): # temple columns x = int((i + r2.random() * 0.6) * fw / 5); h = r2.randint(50, 100) d2.rectangle([x, 214 - h, x + 9, 232], fill=hexc('#3c2868')); d2.rectangle([x - 3, 210 - h, x + 12, 215 - h], fill=hexc('#4c3480')) d2.rectangle([x + 2, 216 - h, x + 3, 232], fill=hexc('#4c3480')) elif name == 'autumn': far = silhouette(fw, H, ridge(fw, 160, 24, [(2, 0.8, 0.7), (5, 0.4, 2.2), (11, 0.15, 0.4)]), '#7c3c4c', '#3c1c30') blob_trees(far, ['#a04c3c', '#8c3c3c', '#b8603c', '#5c2830'], 14, 160, 182, 8, 14, 41) near = silhouette(fw, H, ridge(fw, 204, 12, [(3, 0.8, 1.4), (8, 0.3, 0.5)]), '#3c1828', '#1c0c18') blob_trees(near, ['#5c1c24', '#4c1820', '#6c2428', '#240a10'], 8, 198, 210, 16, 26, 43) elif name == 'reef': far = silhouette(fw, H, ridge(fw, 168, 24, [(2, 0.7, 1.9), (5, 0.4, 0.3), (12, 0.2, 1.2)], 0.8), '#145c7c', '#0c2c4c') seaweed(far, ['#1c7c7c', '#2c6c8c'], 18, 168, 190, 51) near = silhouette(fw, H, ridge(fw, 204, 12, [(3, 0.8, 0.6), (7, 0.3, 2.0)]), '#0c3c5c', '#041c34') seaweed(near, ['#0c2c3c', '#14343c', '#c0406c'], 12, 204, 216, 53) crystals(near, ('#ff7ca0', '#c0406c'), 7, 206, 220, 55) elif name == 'clockwork': far = silhouette(fw, H, ridge(fw, 176, 18, [(2, 0.7, 0.2), (6, 0.4, 1.1)], 0.7), '#4c3418', '#24180c') gears(far, '#5c3c1c', 9, 90, 170, 14, 30, 61) near = silhouette(fw, H, ridge(fw, 206, 10, [(3, 0.8, 2.6), (9, 0.3, 0.8)]), '#24180c', '#100a04') gears(near, '#1c1208', 6, 180, 205, 18, 34, 63) d2 = ImageDraw.Draw(near) for y in (150, 172): # pipes d2.rectangle([0, y, fw, y + 3], fill=hexc('#2c1c0c')) else: far = silhouette(fw, H, ridge(fw, 140, 46, [(2, 0.7, 2.5), (5, 0.45, 0.4), (12, 0.14, 1.9)], 1.5), '#3c3c8c', '#161644') d2 = ImageDraw.Draw(far) ys = ridge(fw, 140, 46, [(2, 0.7, 2.5), (5, 0.45, 0.4), (12, 0.14, 1.9)], 1.5) for x in range(fw): # snow caps if ys[x] < 128: d2.line([x, int(ys[x]), x, int(ys[x] + (128 - ys[x]) * 0.45 + 2 * math.sin(x / 3))], fill=hexc('#dfe8ff')) near = silhouette(fw, H, ridge(fw, 200, 12, [(3, 0.8, 0.9), (7, 0.3, 2.3)]), '#12143c', '#080a20') pines(near, hexc('#0c0e2c'), 12, 194, 208, 34, 64, 8) far.save(out(f'bg/{name}-far.png')) near.save(out(f'bg/{name}-near.png')) e = t['earth'] earth_tile(e[0], e[1], e[2], e[3], sum(map(ord, name)) % 97 + 3).save(out(f'tiles/{name}-earth.png')) steel_tile().save(out('tiles/steel.png')) json.dump({k: {'grass': v['grass'], 'edge': v['edge'], 'glow': v['glow'], 'liquid': v['liquid'], 'mote': v['mote'], 'fog': v['fog']} for k, v in THEMES.items()}, open(out('themes.json'), 'w'), indent=1)# --- the Lumikin ---------------------------------------------------------------------------------------------PAL = { 'o': '#1c1230', # outline 'h': '#ffe9c4', 'H': '#f0c090', # face 'e': '#2a1c3c', # eye 'c': '#2ee0c4', 'C': '#159c9c', 'k': '#0c6878', # cloak 'f': '#ffd23c', 'F': '#ff8a1c', 'w': '#fff8d8', # flame 'r': '#ff4c4c', # warning flame 'l': '#3c2440', # legs 'g': '#5ce06c', 'G': '#2c9c4c', # leaf 't': '#c8d4e8', 'T': '#8c5a34', # tool metal / wood 'b': '#e8b46c', 'B': '#b07a3c', # brick 's': '#ffffff',}CELL = 16class Cell: def __init__(self): self.p = {} def put(self, x, y, c): if 0 <= x < CELL and 0 <= y < CELL: self.p[(x, y)] = c def rect(self, x, y, w, h, c): for yy in range(y, y + h): for xx in range(x, x + w): self.put(xx, yy, c) def outline(self): for (x, y) in list(self.p.keys()): for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): q = (x + dx, y + dy) if q not in self.p and 0 <= q[0] < CELL and 0 <= q[1] < CELL: self.p[q] = 'o_' for q, c in self.p.items(): if c == 'o_': self.p[q] = 'o' def image(self): im = Image.new('RGBA', (CELL, CELL), (0, 0, 0, 0)) for (x, y), c in self.p.items(): im.putpixel((x, y), hexc(PAL[c])) return imdef lumi(dy=0, legs=0, arms='none', flame='f', tool=None, swing=0, lean=0, small=0): """one frame, facing right; feet on row 15""" c = Cell() y = dy if small: # leaving: shrinking into a spark s = small c.rect(7, 9 - s * 2 + y, 2, 2, 'w') if s < 3: c.rect(6, 10 - s + y, 4, 5 - s, 'c') c.rect(6, 7 - s + y, 4, 3, 'h') for k in range(s + 1): c.put(4 + k * 3, 5 - s + (k % 2) * 2 + y, 's') if s < 3: c.outline() return c.image() hx = 5 + lean # legs if legs == 'dangle': c.put(6, 14 + y, 'l'); c.put(9, 14 + y, 'l'); c.put(6, 15 + y, 'l') if swing % 2 else c.put(9, 15 + y, 'l') elif legs == 'wide': c.rect(5, 14 + y, 1, 2, 'l'); c.rect(10, 14 + y, 1, 2, 'l') else: a = [(6, 9), (7, 9), (7, 8), (6, 8), (6, 9), (5, 9), (6, 10), (6, 9)][legs % 8] lift = legs % 4 c.rect(a[0], 14 + y, 1, 1 if lift == 1 else 2, 'l') c.rect(a[1], 14 + y, 1, 1 if lift == 3 else 2, 'l') # cloak c.rect(5 + lean, 10 + y, 6, 4, 'c') c.rect(9 + lean, 10 + y, 2, 4, 'C') c.rect(5 + lean, 13 + y, 6, 1, 'C') c.put(5 + lean, 13 + y, 'k'); c.put(10 + lean, 13 + y, 'k') c.put(7 + lean, 11 + y, 'f') # a little clasp # head c.rect(hx, 5 + y, 6, 5, 'h') c.rect(hx, 9 + y, 6, 1, 'H') c.p.pop((hx, 5 + y), None); c.p.pop((hx + 5, 5 + y), None) c.put(hx + 2, 7 + y, 'e'); c.put(hx + 4, 7 + y, 'e') c.put(hx + 5, 8 + y, 'H') # hood rim c.rect(hx, 5 + y, 2, 1, 'c') if False else None # flame on the head fy = 2 + y + (1 if legs in (1, 5) else 0) c.rect(hx + 2, fy + 1, 2, 2, flame) c.put(hx + 3, fy, flame); c.put(hx + 2, fy + 1, 'w') c.put(hx + 2 + (swing % 2), fy - 1, 'F' if flame == 'f' else 'f') if swing % 3 != 2 else None # arms and tools if arms == 'up': c.rect(4, 7 + y, 1, 3, 'c'); c.rect(11, 7 + y, 1, 3, 'c'); c.put(4, 6 + y, 'h'); c.put(11, 6 + y, 'h') elif arms == 'out': c.rect(2, 10 + y, 3, 1, 'c'); c.rect(11, 10 + y, 3, 1, 'c'); c.put(1, 10 + y, 'h'); c.put(14, 10 + y, 'h') c.put(1, 9 + y, 'h') if swing % 2 else None c.put(14, 9 + y, 'h') if swing % 2 else None elif arms == 'climb': a = swing % 4 c.rect(11, 8 + y - (a in (0, 1)), 2, 1, 'c'); c.put(13, 8 + y - (a in (0, 1)), 'h') c.rect(11, 11 + y - (a in (2, 3)), 2, 1, 'c'); c.put(13, 11 + y - (a in (2, 3)), 'h') elif arms == 'punch': r = [0, 2, 3, 1][swing % 4] c.rect(11, 10 + y, r, 2, 'c'); c.rect(11 + r, 9 + y, 2, 3, 'h') if swing % 4 == 2: c.put(15, 8 + y, 's'); c.put(15, 12 + y, 's') elif arms == 'pick': a = swing % 4 pts = [[(11, 9), (12, 8), (13, 7), (14, 6)], [(11, 9), (12, 9), (13, 8), (14, 8)], [(11, 10), (12, 11), (13, 12), (14, 13)], [(11, 10), (12, 10), (13, 10), (14, 10)]][a] for q in pts: c.put(q[0], q[1] + y, 'T') ex, ey = pts[-1] for q in ((0, -1), (0, 0), (0, 1), (1, -2), (1, 2)) if a != 2 else ((-1, 0), (0, 0), (1, 0), (-2, 1), (2, 1)): c.put(ex + q[0], ey + q[1] + y, 't') c.put(11, 10 + y, 'h') elif arms == 'shovel': a = swing % 4 d = [0, 1, 2, 1][a] c.rect(11, 9 + y + d, 1, 4, 'T'); c.rect(10, 13 + y + d, 3, 2, 't'); c.put(11, 8 + y + d, 'h') c.rect(4, 10 + y, 1, 2, 'c') elif arms == 'brick': a = swing % 4 d = [0, 0, 2, 3][a] c.rect(11, 10 + y + d // 2, 1, 1, 'c') c.rect(11, 11 + y + d, 4, 2, 'b'); c.rect(11, 12 + y + d, 4, 1, 'B') elif arms == 'walk': s = [0, 1, 0, -1][legs % 4] c.put(4, 11 + y + s, 'c'); c.put(11, 11 + y - s, 'C') if tool == 'leaf': k = swing % 2 c.rect(3, 1 + k, 10, 1, 'g'); c.rect(2, 2 + k, 12, 1, 'g'); c.rect(1, 3 + k, 3, 1, 'G'); c.rect(12, 3 + k, 3, 1, 'G') c.rect(5, 0 + k, 6, 1, 'G'); c.put(8, 2 + k, 'G'); c.put(8, 3 + k, 'G'); c.put(8, 4 + k, 'G') c.put(4, 4 + k, 'G'); c.put(11, 4 + k, 'G'); c.put(4, 5 + k, 'G'); c.put(11, 5 + k, 'G') c.outline() return c.image()def sprites(): rows = [ [lumi(legs=i, arms='walk', swing=i) for i in range(8)], # 0 walk [lumi(legs='dangle', arms='up', swing=i) for i in range(4)], # 1 fall [lumi(dy=1, legs='dangle', arms='up', tool='leaf', swing=i) for i in range(4)], # 2 glide [lumi(legs=i * 2, arms='climb', swing=i, lean=1) for i in range(4)], # 3 climb [lumi(legs='wide', arms='out', flame='r' if i % 2 else 'f', swing=i) for i in range(4)], # 4 stop [lumi(legs='wide', arms='brick', swing=i, dy=1 if i >= 2 else 0) for i in range(4)], # 5 bridge [lumi(legs=i * 2, arms='punch', swing=i) for i in range(4)], # 6 bash [lumi(legs='wide', arms='pick', swing=i) for i in range(4)], # 7 mine [lumi(legs='wide', arms='shovel', swing=i, dy=[0, 0, 1, 0][i]) for i in range(4)], # 8 dig [lumi(small=i + 1, dy=-i) for i in range(3)] + [lumi(small=3, dy=-4)], # 9 leave ] sheet = Image.new('RGBA', (8 * CELL, len(rows) * CELL), (0, 0, 0, 0)) for r, frames in enumerate(rows): for i, f in enumerate(frames): sheet.paste(f, (i * CELL, r * CELL)) sheet.save(out('sprites/lumi.png')) # big versions for the site and the icon big = rows[0][0].resize((CELL * 16, CELL * 16), Image.NEAREST) big.save(out('sprites/lumi-big.png')) for name, col in (('amber', '#ffc24c'), ('cyan', '#5cf2e0'), ('white', '#ffffff'), ('red', '#ff5c3c'), ('violet', '#b07cff'), ('green', '#6cf08c')): glow(64, col, 2.2, 0.55).save(out(f'sprites/glow-{name}.png')) glow(160, '#ffffff', 1.4, 0.9).save(out('sprites/flash.png')) # the hatch: a hanging lantern-gate hatch = Image.new('RGBA', (48, 26), (0, 0, 0, 0)) d = ImageDraw.Draw(hatch) d.rectangle([0, 0, 47, 7], fill=hexc('#3c2c5c')); d.rectangle([0, 0, 47, 1], fill=hexc('#7c64b0')); d.rectangle([0, 7, 47, 8], fill=hexc('#1c1230')) for x in (3, 43): d.rectangle([x, 8, x + 1, 18], fill=hexc('#7c64b0')); d.rectangle([x - 1, 18, x + 2, 23], fill=hexc('#ffd23c')); d.point((x, 19), fill=hexc('#ffffff')) d.polygon([(8, 9), (22, 9), (10, 20)], fill=hexc('#5c4690')); d.polygon([(39, 9), (25, 9), (37, 20)], fill=hexc('#4c3a7c')) d.line([(8, 9), (10, 20)], fill=hexc('#1c1230')); d.line([(39, 9), (37, 20)], fill=hexc('#1c1230')) for x in range(6, 42, 6): d.point((x, 4), fill=hexc('#ffd23c')) hatch.save(out('sprites/hatch.png')) # the exit: a stone arch with a swirling light door, 6 frames of 40x44 ex = Image.new('RGBA', (40 * 6, 44), (0, 0, 0, 0)) for f in range(6): im = Image.new('RGBA', (40, 44), (0, 0, 0, 0)) d = ImageDraw.Draw(im) d.pieslice([4, 2, 35, 40], 180, 360, fill=hexc('#6c5c9c')); d.rectangle([4, 21, 35, 43], fill=hexc('#6c5c9c')) d.pieslice([9, 8, 30, 36], 180, 360, fill=hexc('#fff8d8')); d.rectangle([9, 22, 30, 43], fill=hexc('#fff8d8')) for y in range(9, 44): for x in range(9, 31): if im.getpixel((x, y))[:3] == (255, 248, 216): a = math.atan2(y - 28, x - 19.5) + f * math.pi / 3 r = math.hypot(x - 19.5, y - 28) v = 0.5 + 0.5 * math.sin(a * 2 + r * 0.45) im.putpixel((x, y), lerp(hexc('#ffd23c'), hexc('#fffbe8'), v) + (255,) if r > 4 else (255, 255, 255, 255)) d.arc([4, 2, 35, 40], 180, 360, fill=hexc('#b8a8e8')); d.line([4, 21, 4, 43], fill=hexc('#b8a8e8')); d.line([35, 21, 35, 43], fill=hexc('#3c2c5c')) for (x, y) in ((6, 30), (33, 26), (7, 16), (31, 12), (19, 4)): d.rectangle([x, y, x + 1, y + 1], fill=hexc('#4c3c7c')) d.rectangle([17, 0, 22, 5], fill=hexc('#ffd23c')); d.rectangle([18, 1, 21, 4], fill=hexc('#fff8d8')) ex.paste(im, (f * 40, 0)) ex.save(out('sprites/exit.png'))# --- icons, logo, hud ----------------------------------------------------------------------------------------ICONS = { 'climb': ['..........', '......##..', '.....#..#.', '...##...#.', '..#.....#.', '.#...##.#.', '.#..#..##.', '.#.#....#.', '.##.....#.', '..........'], 'glide': ['..######..', '.########.', '##########', '#..#..#..#', '....#.....', '....#.....', '...###....', '...###....', '...#.#....', '..........'], 'boom': ['.#..#..#..', '..#.#.#...', '...###....', '#########.', '...###....', '..#.#.#...', '.#..#..#..', '....#.....', '..........', '..........'], 'stop': ['...####...', '..######..', '.########.', '.##....##.', '.##....##.', '.########.', '..######..', '...####...', '....##....', '....##....'], 'bridge': ['.......###', '.......###', '.....###..', '.....###..', '...###....', '...###....', '.###......', '.###......', '#.........', '..........'], 'bash': ['..........', '.....#....', '......#...', '#######...', '########..', '#######...', '......#...', '.....#....', '..........', '..........'], 'mine': ['.#####....', '#....###..', '......###.', '.....#.##.', '....#...#.', '...#....#.', '..#.......', '.#........', '#.........', '..........'], 'dig': ['....##....', '....##....', '....##....', '....##....', '.########.', '..######..', '...####...', '....##....', '..........', '..........'], 'minus': ['..........', '..........', '..........', '.########.', '.########.', '..........', '..........', '..........', '..........', '..........'], 'plus': ['....##....', '....##....', '....##....', '.########.', '.########.', '....##....', '....##....', '....##....', '..........', '..........'], 'pause': ['..........', '.###..###.', '.###..###.', '.###..###.', '.###..###.', '.###..###.', '.###..###.', '.###..###.', '..........', '..........'], 'fast': ['..........', '.#....#...', '.##...##..', '.###..###.', '.####.####', '.###..###.', '.##...##..', '.#....#...', '..........', '..........'], 'nuke': ['...####...', '..#.##.#..', '.#..##..#.', '.#.####.#.', '.###..###.', '.##.##.##.', '..#.##.#..', '...####...', '..........', '..........'], 'play': ['..........', '..#.......', '..###.....', '..#####...', '..#######.', '..#####...', '..###.....', '..#.......', '..........', '..........'], 'lock': ['...####...', '..#....#..', '..#....#..', '.########.', '.########.', '.###..###.', '.###..###.', '.########.', '..........', '..........'], 'star': ['....##....', '....##....', '.########.', '..######..', '..######..', '..##..##..', '.##....##.', '..........', '..........', '..........'], 'turn_left': ['...#......', '..##......', '.#######..', '..##....#.', '...#....#.', '.........#', '.........#', '........#.', '..........', '..........'], 'turn_right': ['......#...', '......##..', '..#######.', '.#....##..', '.#....#...', '#.........', '#.........', '.#........', '..........', '..........'],}def icons(): names = list(ICONS.keys()) for variant, (fg, sh) in {'': ('#ffffff', '#1c1230'), '-dim': ('#6c6488', '#1c1230'), '-gold': ('#ffd23c', '#5c2c0c')}.items(): sheet = Image.new('RGBA', (len(names) * 12, 12), (0, 0, 0, 0)) for i, n in enumerate(names): for y, row in enumerate(ICONS[n]): for x, ch in enumerate(row): if ch == '#': sheet.putpixel((i * 12 + x + 2, y + 2), hexc(sh)) for y, row in enumerate(ICONS[n]): for x, ch in enumerate(row): if ch == '#': sheet.putpixel((i * 12 + x + 1, y + 1), hexc(fg)) pass # the icon sheets are drawn by tools/art_hd.py (shapes), in both sizes json.dump(names, open(out('icons.json'), 'w'))def logo(): font = ImageFont.truetype(os.path.join(ROOT, 'fonts', 'ui.ttf'), 30) text = 'LUMIKINS' w, h = 190, 44 mask = Image.new('L', (w, h), 0) d = ImageDraw.Draw(mask) x = 6 for ch in text: d.text((x, 2), ch, font=font, fill=255) x += d.textlength(ch, font=font) + 2 mask = mask.point(lambda v: 255 if v > 110 else 0) w = int(x + 6) mask = mask.crop((0, 0, w, h)) grad = gradient(w, h, [(0, '#fffbe0'), (0.35, '#ffd23c'), (0.7, '#ff8a1c'), (1, '#e0428c')]) im = Image.new('RGBA', (w, h), (0, 0, 0, 0)) edge = mask.filter(ImageFilter.MaxFilter(5)) im.paste(Image.new('RGBA', (w, h), hexc('#1c1230')), (0, 2), edge) im.paste(Image.new('RGBA', (w, h), hexc('#3c1c5c')), (0, 0), edge) im.paste(grad, (0, 0), mask) px = im.load() m = mask.load() for yy in range(1, h): for xx in range(w): if m[xx, yy] and not m[xx, yy - 1]: px[xx, yy] = (255, 255, 255, 255) big = im.resize((w * 2, h * 2), Image.NEAREST) canvas = Image.new('RGBA', (w * 2 + 80, h * 2 + 60), (0, 0, 0, 0)) halo = Image.new('RGBA', canvas.size, (0, 0, 0, 0)) halo.paste(Image.new('RGBA', big.size, (255, 170, 60, 150)), (40, 30), big.split()[3]) canvas.alpha_composite(halo.filter(ImageFilter.GaussianBlur(12))) canvas.alpha_composite(big, (40, 30)) canvas.save(out('sprites/logo.png'))def hud(): bar = gradient(480, 38, [(0, '#2c2050'), (0.12, '#1c1438'), (1, '#0c0818')]) d = ImageDraw.Draw(bar) d.line([0, 0, 479, 0], fill=hexc('#7c64b0')); d.line([0, 1, 479, 1], fill=hexc('#3c2c6c')) bar.convert('RGB').save(out('sprites/hud.png')) def button(w, h, top, bottom, rim, name): b = gradient(w, h, [(0, top), (1, bottom)]) d = ImageDraw.Draw(b) d.rectangle([0, 0, w - 1, h - 1], outline=hexc(rim)) d.line([1, 1, w - 2, 1], fill=lerp(hexc(top), (255, 255, 255), 0.35) + (255,)) for q in ((0, 0), (w - 1, 0), (0, h - 1), (w - 1, h - 1)): b.putpixel(q, (0, 0, 0, 0)) b.save(out(f'sprites/{name}.png')) button(32, 32, '#3c2c6c', '#1c1438', '#5c4a94', 'btn') button(32, 32, '#ffb347', '#c8501c', '#fff0b0', 'btn-on') button(32, 32, '#231a40', '#120c24', '#2c2250', 'btn-off') button(24, 15, '#3c2c6c', '#1c1438', '#5c4a94', 'btn-small') button(24, 15, '#ffb347', '#c8501c', '#fff0b0', 'btn-small-on') button(200, 22, '#3c2c6c', '#1c1438', '#5c4a94', 'btn-wide') button(200, 22, '#ffb347', '#c8501c', '#fff0b0', 'btn-wide-on') y, x = np.mgrid[0:270, 0:480] d2 = np.sqrt(((x - 240) / 300) ** 2 + ((y - 125) / 190) ** 2) a = np.clip((d2 - 0.55) * 1.5, 0, 1) ** 1.6 * 200 v = np.zeros((270, 480, 4), dtype=np.uint8) v[..., 2] = 16 v[..., 3] = (np.round(a / 6) * 6).astype(np.uint8) Image.fromarray(v, 'RGBA').save(out('sprites/vignette.png')) cur = Image.new('RGBA', (15, 15), (0, 0, 0, 0)) d = ImageDraw.Draw(cur) for c, o in ((hexc('#1c1230'), 1), (hexc('#ffffff'), 0)): for (a1, b1, a2, b2) in ((0, 0, 4, 0), (0, 0, 0, 4), (10, 0, 14, 0), (14, 0, 14, 4), (0, 10, 0, 14), (0, 14, 4, 14), (14, 10, 14, 14), (10, 14, 14, 14)): d.line([a1 + o * (1 if a1 < 7 else -1) * 0, b1 + o, a2, b2 + o], fill=c) if o else d.line([a1, b1, a2, b2], fill=c) cur.save(out('sprites/cursor.png')) gold = Image.new('RGBA', (15, 15), (0, 0, 0, 0)) for yy in range(15): for xx in range(15): p = cur.getpixel((xx, yy)) gold.putpixel((xx, yy), hexc('#ffd23c') if p[:3] == (255, 255, 255) and p[3] else p) gold.save(out('sprites/cursor-on.png'))def icon(): im = gradient(128, 128, [(0, '#161a44'), (0.6, '#3b2a6b'), (1, '#b0527a')]) im.alpha_composite(glow(120, '#ffc24c', 1.6, 0.9), (4, 0)) l = Image.open(out('sprites/lumi.png')).crop((0, 0, 16, 16)).resize((96, 96), Image.NEAREST) im.alpha_composite(l, (16, 22)) d = ImageDraw.Draw(im) d.rectangle([0, 112, 127, 127], fill=hexc('#4a2a1c')); d.rectangle([0, 110, 127, 113], fill=hexc('#3fe07a')) im.resize((512, 512), Image.NEAREST).convert('RGB').save(out('icon.png'))if __name__ == '__main__': backgrounds() sprites() icons() logo() hud() icon() print('assets drawn')