# Gera as cinco imagens do LMarbles que o build do fredygames.com troca. # # No pacote do autor, o fundo do menu (gfx/title.bmp, uma foto de marmore), o painel lateral do jogo # (gfx/board.bmp, uma foto de pedra rosada) e os fundos das tres familias de fases (gfx/stone, # gfx/metal e gfx/wood: fotos da Terra, de uma protuberancia solar e de Venus, que parecem ser da NASA # e do SOHO) sao fotos sem nenhum credito no pacote. Elas ficam fora do build e do fonte hospedado, e # este script desenha substitutas so com ruido procedural (sem material de terceiros), do mesmo tamanho # e com o mesmo clima: # # gfx/title.bmp 640x480 marmore preto com veios cinza e dourados # gfx/board.bmp 200x480 pedra rosada escura com as quatro caixas rebaixadas onde o jogo # escreve (posicoes de game.c: info, tempo, figura e fases) # gfx/stone/background.bmp 440x480 planeta azul com nuvens sobre um campo de estrelas # gfx/metal/background.bmp 440x480 estrela vermelha e laranja com um brilho em volta # gfx/wood/background.bmp 440x480 planicie alaranjada com um vulcao no horizonte # # python make-images.py # # Deterministico: o ruido sai de random.Random com semente fixa e das reamostragens da Pillow (o build # usa a 12.3.0); outra versao pode gerar outros bytes, e o build-lmarbles.sh confere cada arquivo pelo # SHA-256. import math import random import sys from pathlib import Path from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageOps gfx = Path(sys.argv[1]) def noise(w, h, seed, octaves=6, base=4): """Ruido fractal em tons de cinza (0..255): grades aleatorias ampliadas com interpolacao bicubica.""" rnd = random.Random(seed) acc = None total = 0.0 amp = 1.0 for o in range(octaves): gw = base * (2 ** o) + 1 gh = max(2, gw * h // w + 1) grid = Image.new("L", (gw, gh)) grid.putdata([rnd.randrange(256) for _ in range(gw * gh)]) layer = grid.resize((w, h), Image.BICUBIC) acc = layer if acc is None else Image.blend(acc, layer, amp / (total + amp)) total += amp amp *= 0.55 return ImageOps.autocontrast(acc) def tint(gray, dark, light): """Pinta um cinza entre duas cores.""" lut = [] for c in range(3): lut += [dark[c] + (light[c] - dark[c]) * v // 255 for v in range(256)] return Image.merge("RGB", (gray, gray, gray)).point(lut) def marble(w, h, seed, freq, turb): """Veios de marmore: |sin| de uma faixa diagonal deformada por ruido; 0 no meio do veio.""" n = noise(w, h, seed, octaves=6, base=3).load() out = Image.new("L", (w, h)) px = out.load() for y in range(h): for x in range(w): t = (x * 0.6 + y) * freq + n[x, y] / 255.0 * turb px[x, y] = int(abs(math.sin(t)) * 255) return out def stars(img, seed, count): rnd = random.Random(seed) px = img.load() w, h = img.size for _ in range(count): x, y = rnd.randrange(w), rnd.randrange(h) v = rnd.choice((90, 120, 160, 200, 255)) px[x, y] = (v, v, min(255, v + 20)) def sphere(img, cx, cy, r, surface, rim, light_dir=(-0.5, -0.45)): """Esfera iluminada de um lado, com a textura colorida de superficie projetada no disco.""" px = img.load() sp = surface.load() sw, sh = surface.size lx, ly = light_dir lz = math.sqrt(max(0.0, 1 - lx * lx - ly * ly)) w, h = img.size for y in range(max(0, cy - r - 2), min(h, cy + r + 2)): for x in range(max(0, cx - r - 2), min(w, cx + r + 2)): dx, dy = (x - cx) / r, (y - cy) / r d2 = dx * dx + dy * dy if d2 > 1.0: continue dz = math.sqrt(1 - d2) shade = max(0.0, dx * lx + dy * ly + dz * lz) # coordenadas de textura "esfericas", para os detalhes encolherem na borda u = int((math.asin(max(-1, min(1, dx))) / math.pi + 0.5) * (sw - 1)) v = int((math.asin(max(-1, min(1, dy))) / math.pi + 0.5) * (sh - 1)) col = sp[u, v] edge = max(0.0, 1 - dz) ** 3 col = [col[c] * (0.12 + 0.88 * shade) + rim[c] * edge for c in range(3)] a = min(1.0, (1.0 - math.sqrt(d2)) * r) # borda suavizada old = px[x, y] px[x, y] = tuple(int(min(255, old[c] * (1 - a) + col[c] * a)) for c in range(3)) def bevel_box(draw, x0, y0, x1, y1, width, dark, light): """Caixa rebaixada: borda escura em cima e a esquerda, clara embaixo e a direita.""" for i in range(width): draw.line((x0 + i, y0 + i, x1 - i, y0 + i), fill=dark) draw.line((x0 + i, y0 + i, x0 + i, y1 - i), fill=dark) draw.line((x0 + i, y1 - i, x1 - i, y1 - i), fill=light) draw.line((x1 - i, y0 + i, x1 - i, y1 - i), fill=light) # title.bmp: marmore preto, veios cinza largos e veios dourados finos. W, H = 640, 480 base = tint(noise(W, H, 101, octaves=7, base=4), (4, 4, 6), (40, 36, 34)) grey = marble(W, H, 102, 0.010, 9.0).point(lambda v: max(0, 70 - v) * 3) gold = marble(W, H, 103, 0.023, 14.0).point(lambda v: max(0, 18 - v) * 9) title = ImageChops.add(base, tint(grey, (0, 0, 0), (90, 90, 100))) title = ImageChops.add(title, tint(gold, (0, 0, 0), (130, 100, 45))) title.save(gfx / "title.bmp") # board.bmp: pedra rosada escura, borda em relevo e as caixas onde o jogo escreve. W, H = 200, 480 board = tint(noise(W, H, 201, octaves=6, base=2), (38, 22, 24), (120, 82, 84)) vein = marble(W, H, 202, 0.018, 8.0).point(lambda v: max(0, 22 - v) * 8) board = ImageChops.add(board, tint(vein, (0, 0, 0), (150, 110, 110))) draw = ImageDraw.Draw(board) bevel_box(draw, 0, 0, W - 1, H - 1, 4, (190, 140, 140), (24, 12, 14)) # moldura em relevo for box in ((10, 10, 189, 89), (30, 95, 169, 129), (30, 140, 169, 279), (10, 285, 189, 469)): bevel_box(draw, *box, 5, (24, 12, 14), (200, 150, 150)) board.save(gfx / "board.bmp") # Fundos das fases (440x480): espaco com estrelas e um corpo celeste por familia. W, H = 440, 480 stone = Image.new("RGB", (W, H), (0, 0, 2)) stars(stone, 301, 260) # oceano, continentes (verde e marrom, borda suave) e nuvens height = noise(256, 256, 303, octaves=6, base=2) ocean = tint(height, (8, 30, 95), (30, 80, 160)) land = tint(noise(256, 256, 304, octaves=5, base=4), (60, 90, 40), (140, 120, 80)) coast = height.point(lambda v: max(0, min(255, (v - 140) * 12))) surface = Image.composite(land, ocean, coast) clouds = noise(256, 256, 302, octaves=6, base=3).point(lambda v: max(0, min(255, (v - 120) * 3))) surface = Image.composite(Image.new("RGB", (256, 256), (240, 244, 250)), surface, clouds) sphere(stone, 200, 250, 200, surface, (40, 80, 160)) stone.save(gfx / "stone" / "background.bmp") metal = Image.new("RGB", (W, H), (0, 0, 0)) stars(metal, 401, 120) glow = Image.new("L", (W, H), 0) gd = ImageDraw.Draw(glow) gd.ellipse((40, 30, 470, 460), fill=90) gd.arc((70, 30, 230, 190), 170, 330, fill=200, width=22) # protuberancia glow = glow.filter(ImageFilter.GaussianBlur(18)) metal = ImageChops.add(metal, tint(glow, (0, 0, 0), (200, 70, 10))) granules = tint(noise(256, 256, 402, octaves=6, base=6), (150, 20, 0), (255, 230, 80)) sphere(metal, 255, 245, 205, granules, (255, 120, 20), light_dir=(0.0, 0.0)) metal.save(gfx / "metal" / "background.bmp") wood = Image.new("RGB", (W, H), (0, 0, 0)) sky = Image.linear_gradient("L").resize((W, H)).point(lambda v: v // 6) wood = tint(sky, (0, 0, 0), (60, 25, 5)) ground = noise(W, H, 501, octaves=7, base=5) gpx = ground.load() wpx = wood.load() horizon = 95 for y in range(horizon, H): depth = (y - horizon) / (H - horizon) # 0 no horizonte, 1 embaixo for x in range(W): # perspectiva: a textura e lida mais esticada perto do horizonte sx = int((x - W / 2) * (0.35 + 0.65 * depth) + W / 2) % W sy = int(depth ** 0.7 * (H - 1)) t = gpx[sx, sy] / 255.0 bright = 0.35 + 0.65 * depth wpx[x, y] = (int((120 + 135 * t) * bright), int((50 + 90 * t) * bright), int((5 + 25 * t) * bright)) # vulcao baixo no horizonte vd = ImageDraw.Draw(wood) vd.polygon([(140, horizon + 1), (205, horizon - 22), (235, horizon - 22), (300, horizon + 1)], fill=(150, 80, 25)) vd.polygon([(205, horizon - 22), (235, horizon - 22), (228, horizon - 17), (212, horizon - 17)], fill=(90, 40, 10)) wood = wood.filter(ImageFilter.SMOOTH) wood.save(gfx / "wood" / "background.bmp")