127 lines
5.8 KiB
Python
Executable File
127 lines
5.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Normalize existing original artwork into gameplay-scale v2 atlases."""
|
|
|
|
from pathlib import Path
|
|
import random
|
|
from PIL import Image, ImageDraw, ImageEnhance
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ASSETS = ROOT / "assets/images/play/princess-lima"
|
|
FRAME_W, FRAME_H = 48, 64
|
|
SOURCE_CELL = 314
|
|
|
|
|
|
def crop_subject(source: Image.Image, index: int) -> Image.Image:
|
|
col, row = index % 4, index // 4
|
|
cell = source.crop((col * SOURCE_CELL, row * SOURCE_CELL,
|
|
(col + 1) * SOURCE_CELL, (row + 1) * SOURCE_CELL))
|
|
box = cell.getbbox()
|
|
return cell.crop(box) if box else cell
|
|
|
|
|
|
def fitted(subject: Image.Image, max_w: int = 43, max_h: int = 57) -> Image.Image:
|
|
copy = subject.copy()
|
|
resampling = getattr(Image, "Resampling", Image)
|
|
copy.thumbnail((max_w, max_h), resampling.LANCZOS)
|
|
copy = ImageEnhance.Contrast(copy).enhance(1.08)
|
|
return copy
|
|
|
|
|
|
def make_actor_atlas() -> None:
|
|
source = Image.open(ASSETS / "cast-atlas.png").convert("RGBA")
|
|
# Stable actor row contract used by v2-data.js.
|
|
source_frames = [0, 4, 5, 6, 7, 11, 8, 9, 10, 11, 12, 13, 14, 13, 7, 4]
|
|
atlas = Image.new("RGBA", (FRAME_W * 16, FRAME_H * len(source_frames)))
|
|
for actor_row, source_index in enumerate(source_frames):
|
|
subject = fitted(crop_subject(source, source_index))
|
|
for direction in range(4):
|
|
for phase in range(4):
|
|
frame = Image.new("RGBA", (FRAME_W, FRAME_H))
|
|
bob = (0, -1, 0, 1)[phase]
|
|
stride = (-1, 0, 1, 0)[phase]
|
|
x = (FRAME_W - subject.width) // 2 + (stride if direction in (1, 3) else 0)
|
|
y = FRAME_H - subject.height - 3 + bob
|
|
transpose = getattr(Image, "Transpose", Image)
|
|
image = subject.transpose(transpose.FLIP_LEFT_RIGHT) if direction == 3 else subject
|
|
frame.alpha_composite(image, (x, y))
|
|
atlas.alpha_composite(frame, ((direction * 4 + phase) * FRAME_W, actor_row * FRAME_H))
|
|
atlas.save(ASSETS / "actor-atlas-v2.png", optimize=True)
|
|
|
|
|
|
def make_effect_atlas() -> None:
|
|
size = 32
|
|
atlas = Image.new("RGBA", (size * 8, size * 2))
|
|
palette = [
|
|
(255, 226, 132), (243, 132, 74), (118, 205, 255), (179, 118, 255),
|
|
(144, 222, 136), (235, 238, 246), (255, 107, 119), (89, 65, 120)
|
|
]
|
|
for index, color in enumerate(palette):
|
|
draw = ImageDraw.Draw(atlas)
|
|
x = index * size
|
|
draw.ellipse((x + 12, 12, x + 20, 20), fill=color + (245,))
|
|
draw.polygon([(x + 16, 2), (x + 19, 12), (x + 16, 10), (x + 13, 12)], fill=color + (210,))
|
|
draw.polygon([(x + 16, 30), (x + 19, 20), (x + 16, 22), (x + 13, 20)], fill=color + (150,))
|
|
draw.polygon([(x + 2, 16), (x + 12, 13), (x + 10, 16), (x + 12, 19)], fill=color + (180,))
|
|
draw.polygon([(x + 30, 16), (x + 20, 13), (x + 22, 16), (x + 20, 19)], fill=color + (180,))
|
|
y = size
|
|
draw.arc((x + 2, y + 4, x + 30, y + 30), 205, 335, fill=color + (235,), width=5)
|
|
draw.arc((x + 6, y + 8, x + 26, y + 26), 205, 335, fill=(255, 247, 210, 220), width=2)
|
|
atlas.save(ASSETS / "effects-atlas-v2.png", optimize=True)
|
|
|
|
|
|
def make_world_tiles() -> None:
|
|
themes = [
|
|
("#526c3d", "#354932", "#ad8b58", "#516776"),
|
|
("#24553b", "#15392c", "#8b704a", "#2b6771"),
|
|
("#526f70", "#2c474f", "#85806d", "#217885"),
|
|
("#a6bac9", "#60778b", "#e0e8ec", "#58788f"),
|
|
("#62523a", "#3d3329", "#9a7950", "#6c4430"),
|
|
("#43374d", "#241e2d", "#706179", "#4e264e"),
|
|
("#403845", "#211d27", "#817068", "#782f35"),
|
|
("#382b42", "#18131e", "#74527e", "#873d72"),
|
|
("#c9bd95", "#746d67", "#dccb91", "#537399"),
|
|
]
|
|
atlas = Image.new("RGBA", (128, 288))
|
|
for row, colors in enumerate(themes):
|
|
for kind, base in enumerate(colors):
|
|
tile = Image.new("RGBA", (32, 32), base)
|
|
draw = ImageDraw.Draw(tile)
|
|
rng = random.Random(row * 97 + kind * 29)
|
|
rgb = tuple(int(base[index:index + 2], 16) for index in (1, 3, 5))
|
|
dark = tuple(max(0, int(value * .68)) for value in rgb) + (255,)
|
|
light = tuple(min(255, int(value * 1.22)) for value in rgb) + (255,)
|
|
if kind == 0:
|
|
for _ in range(30):
|
|
x, y = rng.randrange(2, 30), rng.randrange(2, 30)
|
|
draw.point((x, y), fill=light if rng.random() > .72 else dark)
|
|
for _ in range(3):
|
|
x, y = rng.randrange(4, 28), rng.randrange(4, 28)
|
|
draw.line((x - 2, y, x + 2, y - 1), fill=dark)
|
|
elif kind == 1:
|
|
for y in range(0, 32, 8):
|
|
draw.line((0, y, 32, y), fill=dark, width=2)
|
|
offset = 5 if y % 16 else 0
|
|
for x in range(-offset, 32, 11):
|
|
draw.line((x, y, x + 4, y + 8), fill=dark)
|
|
draw.line((x + 1, y + 2, x + 4, y + 2), fill=light)
|
|
elif kind == 2:
|
|
tile.paste(base, (0, 0, 32, 32))
|
|
for y in range(2, 32, 9):
|
|
offset = 5 if y % 18 else 0
|
|
for x in range(-offset, 32, 10):
|
|
draw.rounded_rectangle((x, y, x + 8, y + 6), radius=2, fill=light, outline=dark)
|
|
draw.line((0, 31, 32, 31), fill=dark)
|
|
else:
|
|
for y in range(4, 32, 7):
|
|
draw.arc((-7, y - 4, 13, y + 5), 195, 345, fill=light, width=2)
|
|
draw.arc((11, y - 4, 31, y + 5), 195, 345, fill=dark, width=2)
|
|
atlas.alpha_composite(tile, (kind * 32, row * 32))
|
|
atlas.save(ASSETS / "world-tiles-v2.png", optimize=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
make_actor_atlas()
|
|
make_effect_atlas()
|
|
make_world_tiles()
|
|
print("Generated normalized actor and effects atlases.")
|