137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Generate multi-cell "box" sprites for CHROMA SOLSTICE.
|
||
|
|
|
||
|
|
Each cell is 16x16. A single box has a 1px transparent margin on every edge
|
||
|
|
(so a 14x14 body inside 16x16). For multi-cell blocks we keep that 1px margin
|
||
|
|
only on the *outer* silhouette of the block -- edges that could touch other
|
||
|
|
things in the level -- while cells that are part of the same block stay truly
|
||
|
|
connected (no seam, no internal margin).
|
||
|
|
|
||
|
|
The per-channel fill is the same concentric-ring format as the shipped
|
||
|
|
box_r / box_g / box_b: a pixel is decided purely by k, its inset depth
|
||
|
|
(chebyshev distance in from the nearest outside pixel, minus 1; k==0 is the
|
||
|
|
margin ring and is always transparent). The three channels each leave their
|
||
|
|
own gaps so the colors underneath stay visible when drawn additively.
|
||
|
|
|
||
|
|
red ON: k in {1,2} or k>=5 (2px ring, 2px gap, solid core)
|
||
|
|
green ON: k odd or k>=5 (alternating 1px rings, solid core)
|
||
|
|
blue ON: k==1 or k>=4 (1px ring, 2px gap, solid core)
|
||
|
|
|
||
|
|
Run from inside art/game/ : python3 gen_composite_boxes.py
|
||
|
|
"""
|
||
|
|
from itertools import product
|
||
|
|
from collections import deque
|
||
|
|
from PIL import Image
|
||
|
|
|
||
|
|
CELL = 16
|
||
|
|
|
||
|
|
RULES = {
|
||
|
|
'r': lambda k: (1 <= k <= 2) or (k >= 5),
|
||
|
|
'g': lambda k: (k >= 5) or (k >= 1 and k % 2 == 1),
|
||
|
|
'b': lambda k: (k == 1) or (k >= 4),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def inset_grid(cells, W, H):
|
||
|
|
"""Return k[y][x] for the W*CELL x H*CELL pixel grid (-1 = outside)."""
|
||
|
|
Wp, Hp = W * CELL, H * CELL
|
||
|
|
occ = [[False] * Wp for _ in range(Hp)]
|
||
|
|
for (cx, cy) in cells:
|
||
|
|
for y in range(cy * CELL, cy * CELL + CELL):
|
||
|
|
for x in range(cx * CELL, cx * CELL + CELL):
|
||
|
|
occ[y][x] = True
|
||
|
|
|
||
|
|
INF = 10 ** 9
|
||
|
|
dist = [[INF] * Wp for _ in range(Hp)]
|
||
|
|
dq = deque()
|
||
|
|
for y in range(Hp):
|
||
|
|
for x in range(Wp):
|
||
|
|
if not occ[y][x]:
|
||
|
|
dist[y][x] = 0
|
||
|
|
dq.append((x, y))
|
||
|
|
while dq: # multi-source BFS with chebyshev (8-neighbour) steps
|
||
|
|
x, y = dq.popleft()
|
||
|
|
for dy in (-1, 0, 1):
|
||
|
|
for dx in (-1, 0, 1):
|
||
|
|
if dx == 0 and dy == 0:
|
||
|
|
continue
|
||
|
|
nx, ny = x + dx, y + dy
|
||
|
|
if 0 <= nx < Wp and 0 <= ny < Hp and dist[ny][nx] > dist[y][x] + 1:
|
||
|
|
dist[ny][nx] = dist[y][x] + 1
|
||
|
|
dq.append((nx, ny))
|
||
|
|
|
||
|
|
k = [[-1] * Wp for _ in range(Hp)]
|
||
|
|
for y in range(Hp):
|
||
|
|
for x in range(Wp):
|
||
|
|
if occ[y][x]:
|
||
|
|
# the region beyond the image edge counts as outside too
|
||
|
|
edged = min(x + 1, Wp - x, y + 1, Hp - y)
|
||
|
|
k[y][x] = min(dist[y][x], edged) - 1
|
||
|
|
return k
|
||
|
|
|
||
|
|
|
||
|
|
def render(cells, W, H, channel):
|
||
|
|
k = inset_grid(cells, W, H)
|
||
|
|
rule = RULES[channel]
|
||
|
|
img = Image.new('RGBA', (W * CELL, H * CELL), (0, 0, 0, 0))
|
||
|
|
px = img.load()
|
||
|
|
for y in range(H * CELL):
|
||
|
|
for x in range(W * CELL):
|
||
|
|
if k[y][x] >= 0 and rule(k[y][x]):
|
||
|
|
px[x, y] = (255, 255, 255, 255)
|
||
|
|
return img
|
||
|
|
|
||
|
|
|
||
|
|
def connected(cells):
|
||
|
|
cells = set(cells)
|
||
|
|
start = next(iter(cells))
|
||
|
|
seen = {start}
|
||
|
|
stack = [start]
|
||
|
|
while stack:
|
||
|
|
x, y = stack.pop()
|
||
|
|
for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
|
||
|
|
n = (x + dx, y + dy)
|
||
|
|
if n in cells and n not in seen:
|
||
|
|
seen.add(n)
|
||
|
|
stack.append(n)
|
||
|
|
return len(seen) == len(cells)
|
||
|
|
|
||
|
|
|
||
|
|
def enum(W, H):
|
||
|
|
"""All connected polyominoes whose bounding box is exactly W x H."""
|
||
|
|
coords = [(x, y) for y in range(H) for x in range(W)]
|
||
|
|
out = []
|
||
|
|
for bits in range(1, 1 << (W * H)):
|
||
|
|
cells = [coords[i] for i in range(W * H) if bits >> i & 1]
|
||
|
|
if len(cells) < 2:
|
||
|
|
continue
|
||
|
|
xs = [c[0] for c in cells]
|
||
|
|
ys = [c[1] for c in cells]
|
||
|
|
if min(xs) != 0 or max(xs) != W - 1 or min(ys) != 0 or max(ys) != H - 1:
|
||
|
|
continue
|
||
|
|
if not connected(cells):
|
||
|
|
continue
|
||
|
|
out.append(cells)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def mask_name(cells, W, H):
|
||
|
|
rows = [['0'] * W for _ in range(H)]
|
||
|
|
for x, y in cells:
|
||
|
|
rows[y][x] = '1'
|
||
|
|
return '-'.join(''.join(r) for r in rows)
|
||
|
|
|
||
|
|
|
||
|
|
BBOXES = [(1, 2), (2, 1), (2, 2), (3, 2), (2, 3)]
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
total = 0
|
||
|
|
for (W, H) in BBOXES:
|
||
|
|
for cells in enum(W, H):
|
||
|
|
mask = mask_name(cells, W, H)
|
||
|
|
for ch in 'rgb':
|
||
|
|
fn = 'box_%dx%d_%s_%s.png' % (W, H, mask, ch)
|
||
|
|
render(cells, W, H, ch).save(fn)
|
||
|
|
total += 1
|
||
|
|
print('wrote %d files' % total)
|