secret-student / scripts /generate_character_sprites.py
asanwari's picture
fix: preserve character colors in canvas renderer
cdb1892
Raw
History Blame Contribute Delete
9.42 kB
from pathlib import Path
from PIL import Image, ImageChops, ImageColor, ImageDraw
FRAME_WIDTH = 32
FRAME_HEIGHT = 48
DIRECTIONS = ("down", "left", "right", "up")
STEPS = (-1, 0, 1)
OUTPUT_DIR = Path(__file__).parents[1] / "frontend" / "static" / "assets" / "player"
OUTLINE = "#17202a"
SKIN = "#e8aa78"
SKIN_LIGHT = "#f4c18f"
SHOE = "#252d36"
WHITE = "#fff2c6"
TINT_DARK = "#737373"
TINT_MID = "#b8b8b8"
TINT_LIGHT = "#f2f2f2"
PACK_DARK = "#75551e"
PACK_MID = "#bd8e2f"
PACK_LIGHT = "#e8bd55"
def rect(draw, box, fill, outline=None):
draw.rectangle(box, fill=fill, outline=outline)
def polygon(draw, points, fill, outline=None):
draw.polygon(points, fill=fill)
if outline:
draw.line([*points, points[0]], fill=outline, width=1)
def frame_origin(direction_index, step_index):
return step_index * FRAME_WIDTH, direction_index * FRAME_HEIGHT
def draw_body(draw, ox, oy, direction, step):
bob = 1 if step else 0
y = oy + bob
if direction == "down":
rect(draw, (ox + 9, y + 5, ox + 22, y + 18), SKIN, OUTLINE)
rect(draw, (ox + 11, y + 6, ox + 20, y + 10), SKIN_LIGHT)
rect(draw, (ox + 12, y + 19, ox + 19, y + 22), SKIN, OUTLINE)
rect(draw, (ox + 11, y + 11, ox + 12, y + 12), OUTLINE)
rect(draw, (ox + 19, y + 11, ox + 20, y + 12), OUTLINE)
rect(draw, (ox + 14, y + 15, ox + 17, y + 15), "#9b523f")
rect(draw, (ox + 4, y + 25, ox + 7, y + 31), SKIN, OUTLINE)
rect(draw, (ox + 24, y + 25, ox + 27, y + 31), SKIN, OUTLINE)
elif direction == "up":
rect(draw, (ox + 9, y + 5, ox + 22, y + 18), SKIN, OUTLINE)
rect(draw, (ox + 12, y + 19, ox + 19, y + 22), SKIN, OUTLINE)
rect(draw, (ox + 4, y + 25, ox + 7, y + 31), SKIN, OUTLINE)
rect(draw, (ox + 24, y + 25, ox + 27, y + 31), SKIN, OUTLINE)
else:
facing_right = direction == "right"
rect(draw, (ox + 10, y + 5, ox + 21, y + 18), SKIN, OUTLINE)
nose_x = ox + (22 if facing_right else 9)
rect(draw, (nose_x, y + 11, nose_x, y + 13), SKIN_LIGHT, OUTLINE)
eye_x = ox + (19 if facing_right else 11)
rect(draw, (eye_x, y + 10, eye_x + 1, y + 11), OUTLINE)
rect(draw, (ox + 12, y + 19, ox + 18, y + 22), SKIN, OUTLINE)
hand_x = ox + (23 if facing_right else 6)
rect(draw, (hand_x, y + 26, hand_x + 3, y + 31), SKIN, OUTLINE)
left_lift = 1 if step < 0 else 0
right_lift = 1 if step > 0 else 0
rect(draw, (ox + 8, y + 42 - left_lift, ox + 14, y + 46 - left_lift), SHOE, OUTLINE)
rect(draw, (ox + 17, y + 42 - right_lift, ox + 23, y + 46 - right_lift), SHOE, OUTLINE)
def draw_gear_back(draw, ox, oy, direction, step):
bob = 1 if step else 0
y = oy + bob
if direction == "up":
rect(draw, (ox + 7, y + 20, ox + 24, y + 35), PACK_DARK, OUTLINE)
rect(draw, (ox + 9, y + 21, ox + 22, y + 31), PACK_MID)
rect(draw, (ox + 11, y + 22, ox + 20, y + 24), PACK_LIGHT)
elif direction == "left":
rect(draw, (ox + 18, y + 21, ox + 26, y + 35), PACK_DARK, OUTLINE)
rect(draw, (ox + 20, y + 23, ox + 25, y + 31), PACK_MID)
elif direction == "right":
rect(draw, (ox + 5, y + 21, ox + 13, y + 35), PACK_DARK, OUTLINE)
rect(draw, (ox + 6, y + 23, ox + 11, y + 31), PACK_MID)
else:
rect(draw, (ox + 21, y + 22, ox + 27, y + 34), PACK_DARK, OUTLINE)
rect(draw, (ox + 22, y + 24, ox + 26, y + 31), PACK_MID)
def draw_pants(draw, ox, oy, direction, step):
bob = 1 if step else 0
y = oy + bob
left_lift = 1 if step < 0 else 0
right_lift = 1 if step > 0 else 0
rect(draw, (ox + 9, y + 34, ox + 22, y + 39), TINT_DARK, OUTLINE)
rect(draw, (ox + 9, y + 35, ox + 14, y + 43 - left_lift), TINT_MID, OUTLINE)
rect(draw, (ox + 17, y + 35, ox + 22, y + 43 - right_lift), TINT_MID, OUTLINE)
if direction in ("left", "right"):
rect(draw, (ox + 16, y + 36, ox + 22, y + 42 - right_lift), TINT_DARK)
else:
rect(draw, (ox + 10, y + 36, ox + 11, y + 41 - left_lift), TINT_LIGHT)
rect(draw, (ox + 18, y + 36, ox + 19, y + 41 - right_lift), TINT_LIGHT)
def draw_shirt(draw, ox, oy, direction, step):
bob = 1 if step else 0
y = oy + bob
arm_shift = step
rect(draw, (ox + 9, y + 21, ox + 22, y + 35), TINT_MID, OUTLINE)
rect(draw, (ox + 10, y + 22, ox + 12, y + 33), TINT_LIGHT)
if direction == "up":
rect(draw, (ox + 8, y + 22, ox + 10, y + 33), TINT_DARK, OUTLINE)
rect(draw, (ox + 21, y + 22, ox + 23, y + 33), TINT_DARK, OUTLINE)
elif direction == "left":
polygon(draw, [(ox + 10, y + 22), (ox + 7, y + 24 + arm_shift), (ox + 6, y + 31 + arm_shift), (ox + 9, y + 32), (ox + 12, y + 25)], TINT_MID, OUTLINE)
polygon(draw, [(ox + 21, y + 22), (ox + 24, y + 25 - arm_shift), (ox + 23, y + 31 - arm_shift), (ox + 20, y + 30)], TINT_DARK, OUTLINE)
elif direction == "right":
polygon(draw, [(ox + 21, y + 22), (ox + 24, y + 24 - arm_shift), (ox + 25, y + 31 - arm_shift), (ox + 22, y + 32), (ox + 19, y + 25)], TINT_MID, OUTLINE)
polygon(draw, [(ox + 10, y + 22), (ox + 7, y + 25 + arm_shift), (ox + 8, y + 31 + arm_shift), (ox + 11, y + 30)], TINT_DARK, OUTLINE)
else:
polygon(draw, [(ox + 9, y + 22), (ox + 5, y + 24 + arm_shift), (ox + 5, y + 30 + arm_shift), (ox + 8, y + 31), (ox + 11, y + 25)], TINT_MID, OUTLINE)
polygon(draw, [(ox + 22, y + 22), (ox + 26, y + 24 - arm_shift), (ox + 26, y + 30 - arm_shift), (ox + 23, y + 31), (ox + 20, y + 25)], TINT_DARK, OUTLINE)
polygon(draw, [(ox + 13, y + 21), (ox + 16, y + 25), (ox + 19, y + 21)], WHITE, OUTLINE)
def draw_hair(draw, ox, oy, direction, step):
bob = 1 if step else 0
y = oy + bob
if direction == "up":
rect(draw, (ox + 8, y + 4, ox + 23, y + 16), TINT_DARK, OUTLINE)
rect(draw, (ox + 10, y + 5, ox + 20, y + 8), TINT_LIGHT)
rect(draw, (ox + 8, y + 13, ox + 10, y + 19), TINT_DARK, OUTLINE)
rect(draw, (ox + 21, y + 13, ox + 23, y + 19), TINT_DARK, OUTLINE)
elif direction == "left":
polygon(draw, [(ox + 9, y + 5), (ox + 21, y + 4), (ox + 23, y + 8), (ox + 21, y + 12), (ox + 18, y + 9), (ox + 9, y + 10)], TINT_MID, OUTLINE)
rect(draw, (ox + 18, y + 6, ox + 21, y + 8), TINT_LIGHT)
rect(draw, (ox + 20, y + 13, ox + 22, y + 19), TINT_DARK, OUTLINE)
elif direction == "right":
polygon(draw, [(ox + 22, y + 5), (ox + 10, y + 4), (ox + 8, y + 8), (ox + 10, y + 12), (ox + 13, y + 9), (ox + 22, y + 10)], TINT_MID, OUTLINE)
rect(draw, (ox + 10, y + 6, ox + 13, y + 8), TINT_LIGHT)
rect(draw, (ox + 9, y + 13, ox + 11, y + 19), TINT_DARK, OUTLINE)
else:
polygon(draw, [(ox + 8, y + 10), (ox + 10, y + 5), (ox + 14, y + 3), (ox + 21, y + 5), (ox + 23, y + 9), (ox + 21, y + 11), (ox + 18, y + 8), (ox + 15, y + 11), (ox + 12, y + 8), (ox + 10, y + 13)], TINT_MID, OUTLINE)
rect(draw, (ox + 12, y + 5, ox + 18, y + 6), TINT_LIGHT)
rect(draw, (ox + 9, y + 12, ox + 11, y + 19), TINT_DARK, OUTLINE)
rect(draw, (ox + 20, y + 11, ox + 22, y + 19), TINT_DARK, OUTLINE)
def draw_gear_front(draw, ox, oy, direction, step):
bob = 1 if step else 0
y = oy + bob
if direction == "down":
rect(draw, (ox + 21, y + 23, ox + 23, y + 32), PACK_DARK, OUTLINE)
rect(draw, (ox + 22, y + 25, ox + 23, y + 27), PACK_LIGHT)
elif direction == "up":
rect(draw, (ox + 10, y + 22, ox + 11, y + 30), PACK_LIGHT)
rect(draw, (ox + 20, y + 22, ox + 21, y + 30), PACK_LIGHT)
elif direction == "left":
rect(draw, (ox + 18, y + 22, ox + 19, y + 31), PACK_LIGHT)
else:
rect(draw, (ox + 12, y + 22, ox + 13, y + 31), PACK_LIGHT)
DRAWERS = {
"body": draw_body,
"gear-back": draw_gear_back,
"pants": draw_pants,
"shirt": draw_shirt,
"hair": draw_hair,
"gear-front": draw_gear_front,
}
PALETTES = {
"shirt": {
"red": "#d94b48", "blue": "#3f71b5", "green": "#4d8d59",
"yellow": "#e1b840", "purple": "#765495", "teal": "#398b87",
},
"pants": {
"navy": "#263b60", "charcoal": "#3d4148", "brown": "#6f4d3b",
"olive": "#5d673f", "blue": "#365f86", "plum": "#61405d",
},
"hair": {
"black": "#1d2025", "dark_brown": "#3b271f", "brown": "#70462c",
"blond": "#c99a45", "auburn": "#8a3f2c",
},
}
def tint_atlas(atlas, color):
solid = Image.new("RGB", atlas.size, ImageColor.getrgb(color))
shaded = ImageChops.multiply(atlas.convert("RGB"), solid)
return Image.merge("RGBA", (*shaded.split(), atlas.getchannel("A")))
def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
for name, drawer in DRAWERS.items():
atlas = Image.new("RGBA", (FRAME_WIDTH * 3, FRAME_HEIGHT * 4), (0, 0, 0, 0))
draw = ImageDraw.Draw(atlas)
for direction_index, direction in enumerate(DIRECTIONS):
for step_index, step in enumerate(STEPS):
drawer(draw, *frame_origin(direction_index, step_index), direction, step)
if name in PALETTES:
for color_name, color in PALETTES[name].items():
tint_atlas(atlas, color).save(OUTPUT_DIR / f"student-{name}-{color_name}.png", optimize=True)
else:
atlas.save(OUTPUT_DIR / f"student-{name}.png", optimize=True)
if __name__ == "__main__":
main()