---
name: assemble_animated_sprite
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "def assemble_animated_sprite(frame_paths: list, out_dir: str, *, name: str = \"anim\", fps: int = 8, fmt: str = \"webp\", loop: bool = True, spritesheet: bool = True, pad: int = 0) -> dict"
description: "Ensambla N frames PNG RGBA (p.ej. los frames de un walk cycle ya pixelizados a 32x32 con alpha) en DOS entregables: un sprite sheet horizontal (1 fila x N columnas) PNG RGBA con la transparencia intacta, y una animacion en loop WEBP lossless o GIF animado. Es la pieza de ensamblado final de cualquier animacion de sprite. Salta frames que falten o no abran (aviso en error, no aborta); normaliza tamano al primer frame valido reescalando con NEAREST. Solo PIL. No-throw. Devuelve {ok, spritesheet_path, animation_path, n_frames, frame_size, fmt, error}."
tags: [gamedev-2d, comfyui, sprite, animation]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
  - name: frame_paths
    desc: "lista de rutas a PNG RGBA en orden de reproduccion; los que falten o no abran se saltan (aviso en error)."
  - name: out_dir
    desc: "directorio de salida; se crea si no existe. Se escriben '<name>_sheet.png' y '<name>.<ext>' dentro."
  - name: name
    desc: "nombre base de los ficheros generados (keyword-only, default 'anim')."
  - name: fps
    desc: "frames por segundo de la animacion; duration_ms = round(1000/max(1,fps)) por frame (keyword-only, default 8)."
  - name: fmt
    desc: "formato de la animacion: 'webp' (recomendado, lossless, alpha completo) o 'gif' (alpha binario) (keyword-only)."
  - name: loop
    desc: "si True la animacion se repite indefinidamente (loop=0); si False una sola vez (keyword-only, default True)."
  - name: spritesheet
    desc: "si True genera tambien el sprite sheet horizontal PNG RGBA (keyword-only, default True)."
  - name: pad
    desc: "pixeles de separacion transparente entre columnas del sheet (keyword-only, default 0)."
output: "dict con ok (bool, True si se produjo la animacion con >=1 frame valido), spritesheet_path (str, '' si spritesheet=False o fallo), animation_path (str, '' si fallo), n_frames (int, frames validos usados), frame_size ([w,h] del frame normalizado), fmt (str, 'webp'|'gif'), error (str, avisos y/o error; '' si limpio)."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/ml/assemble_animated_sprite.py"
---

## Ejemplo

```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.assemble_animated_sprite import assemble_animated_sprite

# Frames de un walk cycle ya pixelizados a 32x32 RGBA (p.ej. salida del pipeline ComfyUI):
frames = [
    "/tmp/walk/frame_00.png",
    "/tmp/walk/frame_01.png",
    "/tmp/walk/frame_02.png",
    "/tmp/walk/frame_03.png",
]
res = assemble_animated_sprite(frames, "/tmp/walk_out", name="hero_walk", fps=8, fmt="webp")
# {'ok': True,
#  'spritesheet_path': '/tmp/walk_out/hero_walk_sheet.png',
#  'animation_path': '/tmp/walk_out/hero_walk.webp',
#  'n_frames': 4, 'frame_size': [32, 32], 'fmt': 'webp', 'error': ''}
```

## Cuando usarla

Al final de cualquier pipeline de animacion de sprite, cuando ya tienes los frames
sueltos (pixelizados, con alpha) y necesitas (a) verlos animados en bucle para validar
el ciclo a ojo y (b) un sprite sheet horizontal listo para que un motor de juego lo
trocee por columnas. Tipico despues de generar un walk cycle frame a frame con ComfyUI
y pasarlo por el pixelizado: este es el paso de "juntarlo todo". Usa `fmt="webp"` por
defecto; `fmt="gif"` solo si necesitas compatibilidad con visores que no abren WEBP.

## Gotchas

- **GIF solo tiene alpha binario** (1 bit): cada pixel es opaco o totalmente
  transparente, los pixeles con `alpha < 128` se vuelven transparentes y se pierde el
  anti-aliasing del borde. **WEBP (lossless) es el formato recomendado** para sprites con
  alpha — conserva el canal alpha completo y no ensucia el pixel-art. Usa GIF solo por
  compatibilidad.
- Al guardar GIF, PIL **reoptimiza la paleta** y el indice de transparencia puede
  cambiar (p.ej. de 255 a 1 al releer): es normal, los pixeles transparentes se
  preservan (verificable convirtiendo el frame a RGBA y mirando el canal alpha).
- **Frames que faltan o no abren se SALTAN** (se anota en `error`), no se aborta: la
  animacion se monta con los frames validos. Si quedan **0 frames validos** → `ok=False`.
- El campo `error` puede venir **no vacio aunque `ok=True`**: ahi van los avisos de
  frames saltados. `ok` refleja si se genero la animacion, no la ausencia de avisos.
- El tamano se normaliza al **primer frame valido**; los frames de tamano distinto se
  reescalan con **NEAREST** (sin interpolacion, preserva el pixel-art duro), lo que puede
  deformarlos si su aspect ratio difiere. Asegurate de que todos los frames ya vienen al
  mismo tamano.
- Escribe en disco: crea `out_dir` si no existe; si no hay permiso de escritura, el
  fallo del sheet va a `error` como aviso y el de la animacion pone `ok=False`.
- `disposal=2` limpia el lienzo entre frames (transparencia correcta en cada paso); sin
  el, los frames se acumularian unos sobre otros.
