PL
games/open-lemmings / tools / art.py · commit 04e2e2a
#!/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')