# Bereitet die freigestellten Standbilder der Figuren-Bibliothek für Remotion auf. # # Eingabe: freigestellte PNGs in voller Leinwand (1376 × 768, wie aus der # Bibliothek), z. B. aus frei.py. Alle Bilder einer Figur werden mit demselben # Rahmen zugeschnitten, damit Posen beim Überblenden deckungsgleich bleiben. # # Ausgabe: public/folge02/figuren/.webp und src/folge02/posen.ts # python3 werkzeuge/posen.py [] import json, os, sys, glob from PIL import Image QUELLE = sys.argv[1] CLIPS = sys.argv[2] if len(sys.argv) > 2 else None HIER = os.path.dirname(os.path.abspath(__file__)) ZIEL = os.path.join(HIER, "..", "public", "folge02", "figuren") TS = os.path.join(HIER, "..", "src", "folge02", "posen.ts") os.makedirs(ZIEL, exist_ok=True) # Referenzpose je Figur: deren Höhe gilt als „Figurhöhe“, deren Fußpunkt als Anker. REF = {"pino": "pino_ref_sitzend", "noa": "noa_ref_stehend", "lulu": "lulu_A6_blick-rechts"} # Clip-Leinwand (1284 × 716) ist die verkleinerte Bild-Leinwand (1376 × 768). K_CLIP = 1376 / 1284 posen = {} for figur in REF: dateien = sorted(glob.glob(f"{QUELLE}/{figur}_*.png")) bilder = {os.path.basename(d)[:-4]: Image.open(d) for d in dateien} boxen = {n: b.getchannel("A").getbbox() for n, b in bilder.items()} rahmen = [min(b[0] for b in boxen.values()) - 4, min(b[1] for b in boxen.values()) - 4, max(b[2] for b in boxen.values()) + 4, max(b[3] for b in boxen.values()) + 4] rahmen = [max(0, rahmen[0]), max(0, rahmen[1]), min(1376, rahmen[2]), min(768, rahmen[3])] for n, b in bilder.items(): b.crop(rahmen).save(f"{ZIEL}/{n}.webp", quality=90, method=5) posen[n] = {"figur": figur, "datei": f"folge02/figuren/{n}.webp", "x": rahmen[0], "y": rahmen[1], "b": rahmen[2] - rahmen[0], "h": rahmen[3] - rahmen[1], "box": boxen[n]} # A5 (Blick links) ist eine Spiegelung von A6 an der Bildmitte: zurückschieben, # damit die Figur beim Wechsel A6 ↔ A5 nicht seitlich springt. for n in posen: if n.startswith(f"{figur}_A5_"): a6 = boxen[n.replace("A5_blick-links", "A6_blick-rechts")] posen[n]["x"] += (a6[0] + a6[2]) / 2 - (boxen[n][0] + boxen[n][2]) / 2 print(figur, len(bilder), rahmen) figuren = {} for figur, ref in REF.items(): b = posen[ref]["box"] figuren[figur] = {"refHoehe": b[3] - b[1], "fussX": (b[0] + b[2]) / 2, "fussY": b[3]} clips = {} if CLIPS: for j in sorted(glob.glob(f"{CLIPS}/*.json")): n = os.path.basename(j)[:-5] d = json.load(open(j)) bx = d["box"] clips[n] = {"figur": n.split("_")[0], "datei": f"folge02/clips/{n}.webm", "x": bx[0] * K_CLIP, "y": bx[1] * K_CLIP, "b": (bx[2] - bx[0]) * K_CLIP, "h": (bx[3] - bx[1]) * K_CLIP, "frames": d["frames"]} with open(TS, "w") as f: f.write("// Automatisch erzeugt von werkzeuge/posen.py – nicht von Hand ändern.\n") f.write("// Koordinaten in der Bild-Leinwand der Figur (1376 × 768).\n\n") f.write("export type Pose = {figur: string; datei: string; x: number; y: number; b: number; h: number; box: number[]};\n") f.write("export type ClipDaten = {figur: string; datei: string; x: number; y: number; b: number; h: number; frames: number};\n\n") j = lambda o: json.dumps(o, indent="\t") f.write("export const FIGUREN = " + j(figuren) + " as const;\n\n") f.write("export const POSEN: Record = " + j(posen) + ";\n\n") f.write("export const CLIPS: Record = " + j(clips) + ";\n") print("posen.ts", len(posen), "Posen,", len(clips), "Clips")