smurfs / src /rendering /sprite_generator_pixel_art.py
BolyosCsaba
initial commit
82fa936
"""
Pixel Art Sprite Generator - Charmander-style blocky pixel art
Tiny, blocky, simple rectangles - matches charmander.png reference
Based on draw_charmander_test method
"""
import numpy as np
import math
import random
from PIL import Image, ImageDraw
from typing import Tuple
from .sprite_generator_base import SpriteGeneratorBase
from .sprite_generator import Sprite
from util import PokemonColorPalette
class PixelArtSpriteGenerator(SpriteGeneratorBase):
"""Pixel art sprite generator - Charmander-style blocky pixel art"""
# Pure black outline color
OUTLINE_COLOR = (0, 0, 0)
def generate_smurf_sprites(self, color_tint: Tuple[Tuple[int, int, int], Tuple[int, int, int]] = None,
size_scale: float = 1.0) -> dict:
"""Generate pixel art style dwarf humanoid monster sprites"""
from entities.enums import AnimationType
# Generate random color pair if not provided - use Pokemon color palette
if color_tint is None:
color_tint = PokemonColorPalette.get_random_color_pair()
main_color, accent_color = color_tint
return {
AnimationType.IDLE.value: self._gen_smurf_idle(main_color, accent_color, size_scale),
AnimationType.WALK.value: self._gen_smurf_walk(main_color, accent_color, size_scale),
AnimationType.GATHER.value: self._gen_smurf_gather(main_color, accent_color, size_scale),
AnimationType.HAPPY.value: self._gen_smurf_happy(main_color, accent_color, size_scale),
AnimationType.ANGRY.value: self._gen_smurf_angry(main_color, accent_color, size_scale),
AnimationType.CHAT.value: self._gen_smurf_chat(main_color, accent_color, size_scale),
AnimationType.PLAY.value: self._gen_smurf_play(main_color, accent_color, size_scale)
}
@staticmethod
def _quantize_color(color: Tuple[int, int, int], levels: int = 8) -> Tuple[int, int, int]:
"""Quantize color to pixel art style"""
return tuple((c // levels) * levels for c in color)
@staticmethod
def _draw_character(draw: ImageDraw, x: int, y: int, scale: float,
body_color: Tuple[int, int, int],
belly_color: Tuple[int, int, int],
expression: str = 'neutral',
x_offset: int = 0,
y_offset: int = 0,
leg_offset_left: int = 0,
leg_offset_right: int = 0,
arm_offset_left: int = 0,
arm_offset_right: int = 0):
"""
Draw a charmander-style character - TINY pixel art style
Based on draw_charmander_test - simple blocky pixels, not smooth shapes!
Args:
draw: ImageDraw object
x, y: Base position
scale: Scale factor
body_color: RGB color for body (main color)
belly_color: RGB color for belly (accent color)
expression: 'neutral', 'happy', 'angry', 'chatting'
x_offset, y_offset: Position offsets for animation
leg_offset_left, leg_offset_right: Leg position offsets for walking
arm_offset_left, arm_offset_right: Arm position offsets for animation
"""
s = scale
black = PixelArtSpriteGenerator.OUTLINE_COLOR
# Quantize colors for pixel art style
body_color = PixelArtSpriteGenerator._quantize_color(body_color)
belly_color = PixelArtSpriteGenerator._quantize_color(belly_color)
# Head (large, blocky) - top 2/3 of sprite
# Original: 10x8 at scale 4, so base is 10x8
head_x = x + int(x_offset * s)
head_y = y + int(y_offset * s)
head_w = int(10 * s)
head_h = int(8 * s)
draw.rectangle([head_x, head_y, head_x + head_w, head_y + head_h],
fill=body_color, outline=black, width=1)
# Eyes - just TWO BLACK PIXELS, not white circles!
eye_size = int(1 * s)
eye_y = head_y + int(3 * s)
if expression == 'angry':
# Angry eyes - slightly closer together, lower
eye_y = head_y + int(4 * s)
draw.rectangle([head_x + int(3*s), eye_y,
head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black)
draw.rectangle([head_x + int(6*s), eye_y,
head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black)
# Angry eyebrows (blocky rectangles above eyes)
draw.rectangle([head_x + int(2*s), head_y + int(2*s),
head_x + int(4*s), head_y + int(3*s)], fill=black)
draw.rectangle([head_x + int(6*s), head_y + int(2*s),
head_x + int(8*s), head_y + int(3*s)], fill=black)
elif expression == 'happy':
# Happy eyes - normal position
draw.rectangle([head_x + int(3*s), eye_y,
head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black)
draw.rectangle([head_x + int(6*s), eye_y,
head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black)
# Happy mouth (blocky U-shape)
mouth_y = head_y + int(6 * s)
# Base of smile
draw.rectangle([head_x + int(2*s), mouth_y,
head_x + int(8*s), mouth_y + int(1*s)], fill=black)
# Left curve
draw.rectangle([head_x + int(1*s), mouth_y + int(1*s),
head_x + int(2*s), mouth_y + int(2*s)], fill=black)
# Right curve
draw.rectangle([head_x + int(8*s), mouth_y + int(1*s),
head_x + int(9*s), mouth_y + int(2*s)], fill=black)
elif expression == 'chatting':
# Normal eyes
draw.rectangle([head_x + int(3*s), eye_y,
head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black)
draw.rectangle([head_x + int(6*s), eye_y,
head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black)
# Chatting mouth (open - blocky rectangle)
mouth_y = head_y + int(5 * s)
draw.rectangle([head_x + int(4*s), mouth_y,
head_x + int(6*s), mouth_y + int(2*s)], fill=black)
else: # neutral
# Normal eyes
draw.rectangle([head_x + int(3*s), eye_y,
head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black)
draw.rectangle([head_x + int(6*s), eye_y,
head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black)
# Simple mouth (horizontal line)
mouth_y = head_y + int(6 * s)
draw.rectangle([head_x + int(3*s), mouth_y,
head_x + int(7*s), mouth_y + int(1*s)], fill=black)
# Body (smaller, with lighter belly)
body_x = head_x + int(1 * s) + int(x_offset * s)
body_y = head_y + int(8 * s) + int(y_offset * s)
body_w = int(8 * s)
body_h = int(6 * s)
draw.rectangle([body_x, body_y, body_x + body_w, body_y + body_h],
fill=body_color, outline=black, width=1)
# Belly patch (lighter color)
belly_x = body_x + int(2 * s)
belly_y = body_y + int(3 * s)
belly_w = int(4 * s)
belly_h = int(3 * s)
draw.rectangle([belly_x, belly_y, belly_x + belly_w, belly_y + belly_h], fill=belly_color)
# Arms (tiny, with offsets for animation)
# Left arm
arm_left_x1 = body_x - int(2*s) + int(arm_offset_left * s)
arm_left_x2 = body_x + int(arm_offset_left * s)
draw.rectangle([arm_left_x1, body_y + int(1*s),
arm_left_x2, body_y + int(3*s)],
fill=body_color, outline=black, width=1)
# Right arm
arm_right_x1 = body_x + body_w + int(arm_offset_right * s)
arm_right_x2 = body_x + body_w + int(2*s) + int(arm_offset_right * s)
draw.rectangle([arm_right_x1, body_y + int(1*s),
arm_right_x2, body_y + int(3*s)],
fill=body_color, outline=black, width=1)
# Legs (tiny, with offsets for walking)
leg_y = body_y + body_h
# Left leg
leg_left_x1 = body_x + int(1*s) + int(leg_offset_left * s)
leg_left_x2 = body_x + int(3*s) + int(leg_offset_left * s)
leg_left_y2 = leg_y + int(2*s) + int(leg_offset_left * s)
draw.rectangle([leg_left_x1, leg_y,
leg_left_x2, leg_left_y2],
fill=body_color, outline=black, width=1)
# Right leg
leg_right_x1 = body_x + int(5*s) + int(leg_offset_right * s)
leg_right_x2 = body_x + int(7*s) + int(leg_offset_right * s)
leg_right_y2 = leg_y + int(2*s) + int(leg_offset_right * s)
draw.rectangle([leg_right_x1, leg_y,
leg_right_x2, leg_right_y2],
fill=body_color, outline=black, width=1)
def _gen_smurf_idle(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite:
"""Idle animation - slight breathing"""
frames = []
base_size = int(32 * scale)
for i in range(2):
img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Slight vertical breathing motion
y_offset = 0.3 if i == 0 else 0
# Center character on 32x32 canvas
# Character base size is ~10x14, so center it
char_x = int((base_size - 10 * scale) / 2)
char_y = int((base_size - 14 * scale) / 2) + int(y_offset * scale)
self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'neutral',
y_offset=int(y_offset))
frames.append(np.array(img))
return Sprite(frames=frames, frame_speed=15)
def _gen_smurf_walk(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite:
"""Walking animation"""
frames = []
base_size = int(32 * scale)
for i in range(4):
img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Walking cycle: legs alternate, slight body bob
leg_cycle = i % 2
leg_offset = 1.5 if leg_cycle == 0 else -1.5
y_bob = 0.5 if i % 2 == 0 else 0
# Center character
char_x = int((base_size - 10 * scale) / 2)
char_y = int((base_size - 14 * scale) / 2) + int(y_bob * scale)
# Alternate leg positions
leg_left = leg_offset if i < 2 else -leg_offset
leg_right = -leg_offset if i < 2 else leg_offset
self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'neutral',
y_offset=int(y_bob),
leg_offset_left=int(leg_left),
leg_offset_right=int(leg_right))
frames.append(np.array(img))
return Sprite(frames=frames, frame_speed=8)
def _gen_smurf_gather(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite:
"""Gathering animation - bending down"""
frames = []
base_size = int(32 * scale)
for i in range(4):
img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Bend down progressively
bend = i * 1.2
# Center character
char_x = int((base_size - 10 * scale) / 2)
char_y = int((base_size - 14 * scale) / 2) + int(bend * scale)
# Arms reach down
arm_offset = i * 0.5
self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'neutral',
y_offset=int(bend),
arm_offset_left=int(arm_offset),
arm_offset_right=int(arm_offset))
frames.append(np.array(img))
return Sprite(frames=frames, frame_speed=10)
def _gen_smurf_happy(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite:
"""Happy animation - jumping"""
frames = []
base_size = int(32 * scale)
for i in range(6):
img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Jump up and down
jump = abs(math.sin(i * math.pi / 3)) * 3
# Center character
char_x = int((base_size - 10 * scale) / 2)
char_y = int((base_size - 14 * scale) / 2) - int(jump * scale)
self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'happy',
y_offset=-int(jump))
frames.append(np.array(img))
return Sprite(frames=frames, frame_speed=5)
def _gen_smurf_angry(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite:
"""Angry animation - shaking"""
frames = []
base_size = int(32 * scale)
# Red-tint the colors when angry
angry_main = (min(255, main_color[0] + 40), max(0, main_color[1] - 20), max(0, main_color[2] - 20))
angry_accent = (min(255, accent_color[0] + 40), max(0, accent_color[1] - 20), max(0, accent_color[2] - 20))
for i in range(4):
img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Shake left and right
shake = (i % 2) * 2 - 1 # -1 or 1
# Center character
char_x = int((base_size - 10 * scale) / 2)
char_y = int((base_size - 14 * scale) / 2)
self._draw_character(draw, char_x, char_y, scale, angry_main, angry_accent, 'angry',
x_offset=shake)
frames.append(np.array(img))
return Sprite(frames=frames, frame_speed=8)
def _gen_smurf_chat(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite:
"""Chatting animation"""
frames = []
base_size = int(32 * scale)
for i in range(4):
img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Slight nod
nod = (i % 2) * 0.5
# Center character
char_x = int((base_size - 10 * scale) / 2)
char_y = int((base_size - 14 * scale) / 2) + int(nod * scale)
# Expression alternates between neutral and chatting (mouth open/closed)
expression = 'chatting' if i % 2 == 0 else 'neutral'
self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, expression,
y_offset=int(nod))
frames.append(np.array(img))
return Sprite(frames=frames, frame_speed=12)
def _gen_smurf_play(self, main_color: Tuple[int, int, int], accent_color: Tuple[int, int, int], scale: float) -> Sprite:
"""Playing animation - bouncing"""
frames = []
base_size = int(32 * scale)
for i in range(6):
img = Image.new('RGBA', (base_size, base_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Bounce up and down
bounce = abs(math.sin(i * math.pi / 3)) * 2.5
# Center character
char_x = int((base_size - 10 * scale) / 2)
char_y = int((base_size - 14 * scale) / 2) - int(bounce * scale)
self._draw_character(draw, char_x, char_y, scale, main_color, accent_color, 'happy',
y_offset=-int(bounce))
frames.append(np.array(img))
return Sprite(frames=frames, frame_speed=10)
@staticmethod
def draw_charmander_test(img: Image, draw: ImageDraw, x: int, y: int, scale: int = 4):
"""
Draw a static charmander-like monster - TINY pixel art style (10x14 pixels)
Mimics charmander.png exactly - simple blocky pixels, not smooth shapes!
"""
s = scale
orange = (255, 140, 0)
yellow = (255, 220, 0)
black = (0, 0, 0)
brown = (139, 69, 19)
# Head (large, blocky, orange) - top 2/3 of sprite
# Draw as rectangles/pixels, not ellipses!
# This should be maybe 8-10 pixels wide, 8-10 pixels tall
head_x = x
head_y = y
head_w = int(10 * s)
head_h = int(8 * s)
draw.rectangle([head_x, head_y, head_x + head_w, head_y + head_h],
fill=orange, outline=black, width=1)
# Eyes - just TWO BLACK PIXELS, not white circles!
eye_size = int(1 * s)
eye_y = head_y + int(3 * s)
draw.rectangle([head_x + int(3*s), eye_y,
head_x + int(3*s) + eye_size, eye_y + eye_size], fill=black)
draw.rectangle([head_x + int(6*s), eye_y,
head_x + int(6*s) + eye_size, eye_y + eye_size], fill=black)
# Body (smaller, orange with yellow belly)
body_x = head_x + int(1 * s)
body_y = head_y + int(8 * s)
body_w = int(8 * s)
body_h = int(6 * s)
draw.rectangle([body_x, body_y, body_x + body_w, body_y + body_h],
fill=orange, outline=black, width=1)
# Yellow belly patch
belly_x = body_x + int(2 * s)
belly_y = body_y + int(3 * s)
belly_w = int(4 * s)
belly_h = int(3 * s)
draw.rectangle([belly_x, belly_y, belly_x + belly_w, belly_y + belly_h], fill=yellow)
# Arms (tiny, orange)
# Left arm
draw.rectangle([body_x - int(2*s), body_y + int(1*s),
body_x, body_y + int(3*s)], fill=orange, outline=black, width=1)
# Right arm
draw.rectangle([body_x + body_w, body_y + int(1*s),
body_x + body_w + int(2*s), body_y + int(3*s)], fill=orange, outline=black, width=1)
# Legs (tiny, orange)
leg_y = body_y + body_h
draw.rectangle([body_x + int(1*s), leg_y,
body_x + int(3*s), leg_y + int(2*s)], fill=orange, outline=black, width=1)
draw.rectangle([body_x + int(5*s), leg_y,
body_x + int(7*s), leg_y + int(2*s)], fill=orange, outline=black, width=1)
# Brown pixel on right side (tail detail)
draw.rectangle([body_x + body_w + int(1*s), body_y + int(4*s),
body_x + body_w + int(2*s), body_y + int(5*s)], fill=brown)