diff --git a/remotion/README.md b/remotion/README.md index 8d29ced..3d69da6 100644 --- a/remotion/README.md +++ b/remotion/README.md @@ -34,6 +34,33 @@ Mit `rembg` (Modell `isnet-general-use`), danach Maske härten, nur die größte Fläche behalten, Rand um 2 px einziehen (sonst heller Saum vor dunklem Hintergrund). Am besten funktionieren Einzelbilder vor hellem Papier. +## Körperteil-Masken (Rig) +`werkzeuge/rig.py` zerlegt jede Figur in Körperteile und erzeugt: + +| Pfad | Inhalt | +| --- | --- | +| `public/figuren//teile/.png` | harte Maske, genau an der Kontur | +| `public/figuren//einfluss/.png` | weiche Maske für die Verformung: Teil + alles, was daran hängt, weicher Übergang am Gelenk, reicht über die Kontur hinaus | +| `public/figuren//uebersicht.png` | alle Teile farbig, zum Prüfen | +| `src/rigs.ts` | Gelenkpunkte und Hierarchie (automatisch erzeugt) | + +Teile: Lulu – kopf, hals, ohr_links/rechts, horn_links/rechts, rumpf, +bein_vorne_links/rechts, bein_hinten_links/rechts, schwanz, schal_schlinge, +schal_ende, schal_boden. Pino – koerper, kopf, ohr_links/rechts, bauch, +arm_links/rechts, fuss_links/rechts, muetze, bommel. Noa – kopf, +zopf_links/rechts, rumpf, arm_links/rechts, hand_links/rechts, +bein_links/rechts, stiefel_links/rechts. + +Zuordnung: Accessoires über ihre Farbe, alles andere über Kernflächen pro +Teil, die bis zur Kontur wachsen – nur innerhalb der Figur, also nie über +Lücken (Schwanz ↔ Bein). Neue Figur oder neues Bild: Skelett und Flächen in +`rig.py` eintragen, dann +```bash +python3 werkzeuge/rig.py # braucht numpy, scipy, pillow +``` +und `uebersicht.png` prüfen. In einer Szene: `gelenk('lulu', 'hals', grad)` +(siehe `src/szenen/VerformTest.tsx`). + ## Bekannte Grenzen - Die Figuren haben nur die Pose ihres Bildes. Neue Posen brauchen ein neues Bild (z. B. aus Gemini). diff --git a/remotion/public/figuren/lulu/einfluss/bein_hinten_links.png b/remotion/public/figuren/lulu/einfluss/bein_hinten_links.png new file mode 100644 index 0000000..9272899 Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/bein_hinten_links.png differ diff --git a/remotion/public/figuren/lulu/einfluss/bein_hinten_rechts.png b/remotion/public/figuren/lulu/einfluss/bein_hinten_rechts.png new file mode 100644 index 0000000..a9dd373 Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/bein_hinten_rechts.png differ diff --git a/remotion/public/figuren/lulu/einfluss/bein_vorne_links.png b/remotion/public/figuren/lulu/einfluss/bein_vorne_links.png new file mode 100644 index 0000000..10fcfa8 Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/bein_vorne_links.png differ diff --git a/remotion/public/figuren/lulu/einfluss/bein_vorne_rechts.png b/remotion/public/figuren/lulu/einfluss/bein_vorne_rechts.png new file mode 100644 index 0000000..67e8284 Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/bein_vorne_rechts.png differ diff --git a/remotion/public/figuren/lulu/einfluss/hals.png b/remotion/public/figuren/lulu/einfluss/hals.png new file mode 100644 index 0000000..239de26 Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/hals.png differ diff --git a/remotion/public/figuren/lulu/einfluss/horn_links.png b/remotion/public/figuren/lulu/einfluss/horn_links.png new file mode 100644 index 0000000..ff5a7a5 Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/horn_links.png differ diff --git a/remotion/public/figuren/lulu/einfluss/horn_rechts.png b/remotion/public/figuren/lulu/einfluss/horn_rechts.png new file mode 100644 index 0000000..8ce451c Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/horn_rechts.png differ diff --git a/remotion/public/figuren/lulu/einfluss/kopf.png b/remotion/public/figuren/lulu/einfluss/kopf.png new file mode 100644 index 0000000..3f0ad05 Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/kopf.png differ diff --git a/remotion/public/figuren/lulu/einfluss/ohr_links.png b/remotion/public/figuren/lulu/einfluss/ohr_links.png new file mode 100644 index 0000000..9145cfc Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/ohr_links.png differ diff --git a/remotion/public/figuren/lulu/einfluss/ohr_rechts.png b/remotion/public/figuren/lulu/einfluss/ohr_rechts.png new file mode 100644 index 0000000..09333d3 Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/ohr_rechts.png differ diff --git a/remotion/public/figuren/lulu/einfluss/rumpf.png b/remotion/public/figuren/lulu/einfluss/rumpf.png new file mode 100644 index 0000000..1584c9d Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/rumpf.png differ diff --git a/remotion/public/figuren/lulu/einfluss/schal_boden.png b/remotion/public/figuren/lulu/einfluss/schal_boden.png new file mode 100644 index 0000000..a9d256e Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/schal_boden.png differ diff --git a/remotion/public/figuren/lulu/einfluss/schal_ende.png b/remotion/public/figuren/lulu/einfluss/schal_ende.png new file mode 100644 index 0000000..d0681ac Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/schal_ende.png differ diff --git a/remotion/public/figuren/lulu/einfluss/schal_schlinge.png b/remotion/public/figuren/lulu/einfluss/schal_schlinge.png new file mode 100644 index 0000000..d92cce6 Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/schal_schlinge.png differ diff --git a/remotion/public/figuren/lulu/einfluss/schwanz.png b/remotion/public/figuren/lulu/einfluss/schwanz.png new file mode 100644 index 0000000..cc6330d Binary files /dev/null and b/remotion/public/figuren/lulu/einfluss/schwanz.png differ diff --git a/remotion/public/figuren/lulu/teile/bein_hinten_links.png b/remotion/public/figuren/lulu/teile/bein_hinten_links.png new file mode 100644 index 0000000..99a9e83 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/bein_hinten_links.png differ diff --git a/remotion/public/figuren/lulu/teile/bein_hinten_rechts.png b/remotion/public/figuren/lulu/teile/bein_hinten_rechts.png new file mode 100644 index 0000000..c14a4c4 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/bein_hinten_rechts.png differ diff --git a/remotion/public/figuren/lulu/teile/bein_vorne_links.png b/remotion/public/figuren/lulu/teile/bein_vorne_links.png new file mode 100644 index 0000000..177f721 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/bein_vorne_links.png differ diff --git a/remotion/public/figuren/lulu/teile/bein_vorne_rechts.png b/remotion/public/figuren/lulu/teile/bein_vorne_rechts.png new file mode 100644 index 0000000..333a497 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/bein_vorne_rechts.png differ diff --git a/remotion/public/figuren/lulu/teile/hals.png b/remotion/public/figuren/lulu/teile/hals.png new file mode 100644 index 0000000..a2d02aa Binary files /dev/null and b/remotion/public/figuren/lulu/teile/hals.png differ diff --git a/remotion/public/figuren/lulu/teile/horn_links.png b/remotion/public/figuren/lulu/teile/horn_links.png new file mode 100644 index 0000000..a5a0e59 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/horn_links.png differ diff --git a/remotion/public/figuren/lulu/teile/horn_rechts.png b/remotion/public/figuren/lulu/teile/horn_rechts.png new file mode 100644 index 0000000..dc018a3 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/horn_rechts.png differ diff --git a/remotion/public/figuren/lulu/teile/kopf.png b/remotion/public/figuren/lulu/teile/kopf.png new file mode 100644 index 0000000..dcf3a1f Binary files /dev/null and b/remotion/public/figuren/lulu/teile/kopf.png differ diff --git a/remotion/public/figuren/lulu/teile/ohr_links.png b/remotion/public/figuren/lulu/teile/ohr_links.png new file mode 100644 index 0000000..7ffcda8 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/ohr_links.png differ diff --git a/remotion/public/figuren/lulu/teile/ohr_rechts.png b/remotion/public/figuren/lulu/teile/ohr_rechts.png new file mode 100644 index 0000000..05250d4 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/ohr_rechts.png differ diff --git a/remotion/public/figuren/lulu/teile/rumpf.png b/remotion/public/figuren/lulu/teile/rumpf.png new file mode 100644 index 0000000..566e564 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/rumpf.png differ diff --git a/remotion/public/figuren/lulu/teile/schal_boden.png b/remotion/public/figuren/lulu/teile/schal_boden.png new file mode 100644 index 0000000..414efb6 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/schal_boden.png differ diff --git a/remotion/public/figuren/lulu/teile/schal_ende.png b/remotion/public/figuren/lulu/teile/schal_ende.png new file mode 100644 index 0000000..6074972 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/schal_ende.png differ diff --git a/remotion/public/figuren/lulu/teile/schal_schlinge.png b/remotion/public/figuren/lulu/teile/schal_schlinge.png new file mode 100644 index 0000000..4159bc5 Binary files /dev/null and b/remotion/public/figuren/lulu/teile/schal_schlinge.png differ diff --git a/remotion/public/figuren/lulu/teile/schwanz.png b/remotion/public/figuren/lulu/teile/schwanz.png new file mode 100644 index 0000000..486141a Binary files /dev/null and b/remotion/public/figuren/lulu/teile/schwanz.png differ diff --git a/remotion/public/figuren/lulu/uebersicht.png b/remotion/public/figuren/lulu/uebersicht.png new file mode 100644 index 0000000..01bc7c4 Binary files /dev/null and b/remotion/public/figuren/lulu/uebersicht.png differ diff --git a/remotion/public/figuren/noa/einfluss/arm_links.png b/remotion/public/figuren/noa/einfluss/arm_links.png new file mode 100644 index 0000000..1bcf588 Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/arm_links.png differ diff --git a/remotion/public/figuren/noa/einfluss/arm_rechts.png b/remotion/public/figuren/noa/einfluss/arm_rechts.png new file mode 100644 index 0000000..8afd7ea Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/arm_rechts.png differ diff --git a/remotion/public/figuren/noa/einfluss/bein_links.png b/remotion/public/figuren/noa/einfluss/bein_links.png new file mode 100644 index 0000000..9abb02f Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/bein_links.png differ diff --git a/remotion/public/figuren/noa/einfluss/bein_rechts.png b/remotion/public/figuren/noa/einfluss/bein_rechts.png new file mode 100644 index 0000000..858ab35 Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/bein_rechts.png differ diff --git a/remotion/public/figuren/noa/einfluss/hand_links.png b/remotion/public/figuren/noa/einfluss/hand_links.png new file mode 100644 index 0000000..540991b Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/hand_links.png differ diff --git a/remotion/public/figuren/noa/einfluss/hand_rechts.png b/remotion/public/figuren/noa/einfluss/hand_rechts.png new file mode 100644 index 0000000..d4f73e6 Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/hand_rechts.png differ diff --git a/remotion/public/figuren/noa/einfluss/kopf.png b/remotion/public/figuren/noa/einfluss/kopf.png new file mode 100644 index 0000000..68c2ad3 Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/kopf.png differ diff --git a/remotion/public/figuren/noa/einfluss/rumpf.png b/remotion/public/figuren/noa/einfluss/rumpf.png new file mode 100644 index 0000000..bbb77b5 Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/rumpf.png differ diff --git a/remotion/public/figuren/noa/einfluss/stiefel_links.png b/remotion/public/figuren/noa/einfluss/stiefel_links.png new file mode 100644 index 0000000..1f3cb0b Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/stiefel_links.png differ diff --git a/remotion/public/figuren/noa/einfluss/stiefel_rechts.png b/remotion/public/figuren/noa/einfluss/stiefel_rechts.png new file mode 100644 index 0000000..a03a5b8 Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/stiefel_rechts.png differ diff --git a/remotion/public/figuren/noa/einfluss/zopf_links.png b/remotion/public/figuren/noa/einfluss/zopf_links.png new file mode 100644 index 0000000..0fa01e6 Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/zopf_links.png differ diff --git a/remotion/public/figuren/noa/einfluss/zopf_rechts.png b/remotion/public/figuren/noa/einfluss/zopf_rechts.png new file mode 100644 index 0000000..743aed7 Binary files /dev/null and b/remotion/public/figuren/noa/einfluss/zopf_rechts.png differ diff --git a/remotion/public/figuren/noa/teile/arm_links.png b/remotion/public/figuren/noa/teile/arm_links.png new file mode 100644 index 0000000..d4abaa3 Binary files /dev/null and b/remotion/public/figuren/noa/teile/arm_links.png differ diff --git a/remotion/public/figuren/noa/teile/arm_rechts.png b/remotion/public/figuren/noa/teile/arm_rechts.png new file mode 100644 index 0000000..183f05a Binary files /dev/null and b/remotion/public/figuren/noa/teile/arm_rechts.png differ diff --git a/remotion/public/figuren/noa/teile/bein_links.png b/remotion/public/figuren/noa/teile/bein_links.png new file mode 100644 index 0000000..414cb1b Binary files /dev/null and b/remotion/public/figuren/noa/teile/bein_links.png differ diff --git a/remotion/public/figuren/noa/teile/bein_rechts.png b/remotion/public/figuren/noa/teile/bein_rechts.png new file mode 100644 index 0000000..0d37a54 Binary files /dev/null and b/remotion/public/figuren/noa/teile/bein_rechts.png differ diff --git a/remotion/public/figuren/noa/teile/hand_links.png b/remotion/public/figuren/noa/teile/hand_links.png new file mode 100644 index 0000000..62a13f1 Binary files /dev/null and b/remotion/public/figuren/noa/teile/hand_links.png differ diff --git a/remotion/public/figuren/noa/teile/hand_rechts.png b/remotion/public/figuren/noa/teile/hand_rechts.png new file mode 100644 index 0000000..78175c4 Binary files /dev/null and b/remotion/public/figuren/noa/teile/hand_rechts.png differ diff --git a/remotion/public/figuren/noa/teile/kopf.png b/remotion/public/figuren/noa/teile/kopf.png new file mode 100644 index 0000000..ff46e03 Binary files /dev/null and b/remotion/public/figuren/noa/teile/kopf.png differ diff --git a/remotion/public/figuren/noa/teile/rumpf.png b/remotion/public/figuren/noa/teile/rumpf.png new file mode 100644 index 0000000..1bb1c82 Binary files /dev/null and b/remotion/public/figuren/noa/teile/rumpf.png differ diff --git a/remotion/public/figuren/noa/teile/stiefel_links.png b/remotion/public/figuren/noa/teile/stiefel_links.png new file mode 100644 index 0000000..1fb6c2e Binary files /dev/null and b/remotion/public/figuren/noa/teile/stiefel_links.png differ diff --git a/remotion/public/figuren/noa/teile/stiefel_rechts.png b/remotion/public/figuren/noa/teile/stiefel_rechts.png new file mode 100644 index 0000000..df24c22 Binary files /dev/null and b/remotion/public/figuren/noa/teile/stiefel_rechts.png differ diff --git a/remotion/public/figuren/noa/teile/zopf_links.png b/remotion/public/figuren/noa/teile/zopf_links.png new file mode 100644 index 0000000..084e1f7 Binary files /dev/null and b/remotion/public/figuren/noa/teile/zopf_links.png differ diff --git a/remotion/public/figuren/noa/teile/zopf_rechts.png b/remotion/public/figuren/noa/teile/zopf_rechts.png new file mode 100644 index 0000000..316503f Binary files /dev/null and b/remotion/public/figuren/noa/teile/zopf_rechts.png differ diff --git a/remotion/public/figuren/noa/uebersicht.png b/remotion/public/figuren/noa/uebersicht.png new file mode 100644 index 0000000..daf6f84 Binary files /dev/null and b/remotion/public/figuren/noa/uebersicht.png differ diff --git a/remotion/public/figuren/pino/einfluss/arm_links.png b/remotion/public/figuren/pino/einfluss/arm_links.png new file mode 100644 index 0000000..e31108d Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/arm_links.png differ diff --git a/remotion/public/figuren/pino/einfluss/arm_rechts.png b/remotion/public/figuren/pino/einfluss/arm_rechts.png new file mode 100644 index 0000000..21d5353 Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/arm_rechts.png differ diff --git a/remotion/public/figuren/pino/einfluss/bauch.png b/remotion/public/figuren/pino/einfluss/bauch.png new file mode 100644 index 0000000..dc2ac0b Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/bauch.png differ diff --git a/remotion/public/figuren/pino/einfluss/bommel.png b/remotion/public/figuren/pino/einfluss/bommel.png new file mode 100644 index 0000000..e52659d Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/bommel.png differ diff --git a/remotion/public/figuren/pino/einfluss/fuss_links.png b/remotion/public/figuren/pino/einfluss/fuss_links.png new file mode 100644 index 0000000..dfde83c Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/fuss_links.png differ diff --git a/remotion/public/figuren/pino/einfluss/fuss_rechts.png b/remotion/public/figuren/pino/einfluss/fuss_rechts.png new file mode 100644 index 0000000..c3179d4 Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/fuss_rechts.png differ diff --git a/remotion/public/figuren/pino/einfluss/koerper.png b/remotion/public/figuren/pino/einfluss/koerper.png new file mode 100644 index 0000000..ab3db72 Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/koerper.png differ diff --git a/remotion/public/figuren/pino/einfluss/kopf.png b/remotion/public/figuren/pino/einfluss/kopf.png new file mode 100644 index 0000000..f55c933 Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/kopf.png differ diff --git a/remotion/public/figuren/pino/einfluss/muetze.png b/remotion/public/figuren/pino/einfluss/muetze.png new file mode 100644 index 0000000..0473ad5 Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/muetze.png differ diff --git a/remotion/public/figuren/pino/einfluss/ohr_links.png b/remotion/public/figuren/pino/einfluss/ohr_links.png new file mode 100644 index 0000000..dd34e4d Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/ohr_links.png differ diff --git a/remotion/public/figuren/pino/einfluss/ohr_rechts.png b/remotion/public/figuren/pino/einfluss/ohr_rechts.png new file mode 100644 index 0000000..fe4955b Binary files /dev/null and b/remotion/public/figuren/pino/einfluss/ohr_rechts.png differ diff --git a/remotion/public/figuren/pino/teile/arm_links.png b/remotion/public/figuren/pino/teile/arm_links.png new file mode 100644 index 0000000..bde9cb2 Binary files /dev/null and b/remotion/public/figuren/pino/teile/arm_links.png differ diff --git a/remotion/public/figuren/pino/teile/arm_rechts.png b/remotion/public/figuren/pino/teile/arm_rechts.png new file mode 100644 index 0000000..9653092 Binary files /dev/null and b/remotion/public/figuren/pino/teile/arm_rechts.png differ diff --git a/remotion/public/figuren/pino/teile/bauch.png b/remotion/public/figuren/pino/teile/bauch.png new file mode 100644 index 0000000..ae2c1e6 Binary files /dev/null and b/remotion/public/figuren/pino/teile/bauch.png differ diff --git a/remotion/public/figuren/pino/teile/bommel.png b/remotion/public/figuren/pino/teile/bommel.png new file mode 100644 index 0000000..0fc8f52 Binary files /dev/null and b/remotion/public/figuren/pino/teile/bommel.png differ diff --git a/remotion/public/figuren/pino/teile/fuss_links.png b/remotion/public/figuren/pino/teile/fuss_links.png new file mode 100644 index 0000000..a909bab Binary files /dev/null and b/remotion/public/figuren/pino/teile/fuss_links.png differ diff --git a/remotion/public/figuren/pino/teile/fuss_rechts.png b/remotion/public/figuren/pino/teile/fuss_rechts.png new file mode 100644 index 0000000..0fc0f61 Binary files /dev/null and b/remotion/public/figuren/pino/teile/fuss_rechts.png differ diff --git a/remotion/public/figuren/pino/teile/koerper.png b/remotion/public/figuren/pino/teile/koerper.png new file mode 100644 index 0000000..1560ab9 Binary files /dev/null and b/remotion/public/figuren/pino/teile/koerper.png differ diff --git a/remotion/public/figuren/pino/teile/kopf.png b/remotion/public/figuren/pino/teile/kopf.png new file mode 100644 index 0000000..065ba1e Binary files /dev/null and b/remotion/public/figuren/pino/teile/kopf.png differ diff --git a/remotion/public/figuren/pino/teile/muetze.png b/remotion/public/figuren/pino/teile/muetze.png new file mode 100644 index 0000000..a25efc8 Binary files /dev/null and b/remotion/public/figuren/pino/teile/muetze.png differ diff --git a/remotion/public/figuren/pino/teile/ohr_links.png b/remotion/public/figuren/pino/teile/ohr_links.png new file mode 100644 index 0000000..f31f9af Binary files /dev/null and b/remotion/public/figuren/pino/teile/ohr_links.png differ diff --git a/remotion/public/figuren/pino/teile/ohr_rechts.png b/remotion/public/figuren/pino/teile/ohr_rechts.png new file mode 100644 index 0000000..1c3d4a0 Binary files /dev/null and b/remotion/public/figuren/pino/teile/ohr_rechts.png differ diff --git a/remotion/public/figuren/pino/uebersicht.png b/remotion/public/figuren/pino/uebersicht.png new file mode 100644 index 0000000..760a8e6 Binary files /dev/null and b/remotion/public/figuren/pino/uebersicht.png differ diff --git a/remotion/src/rigs.ts b/remotion/src/rigs.ts new file mode 100644 index 0000000..0ea83ee --- /dev/null +++ b/remotion/src/rigs.ts @@ -0,0 +1,280 @@ +// Automatisch erzeugt von werkzeuge/rig.py – nicht von Hand ändern. +// Gelenkpunkte (um) in Bildpixeln der Figur, eltern = woran das Teil hängt. +// Masken: figuren//teile/.png und figuren//einfluss/.png + +export type RigTeil = {eltern: string | null; um: [number, number] | null}; + +export const RIGS: Record> = { + "lulu": { + "rumpf": { + "eltern": null, + "um": [ + 130, + 292 + ] + }, + "hals": { + "eltern": "rumpf", + "um": [ + 124, + 244 + ] + }, + "kopf": { + "eltern": "hals", + "um": [ + 111, + 150 + ] + }, + "ohr_links": { + "eltern": "kopf", + "um": [ + 44, + 57 + ] + }, + "ohr_rechts": { + "eltern": "kopf", + "um": [ + 146, + 72 + ] + }, + "horn_links": { + "eltern": "kopf", + "um": [ + 65, + 44 + ] + }, + "horn_rechts": { + "eltern": "kopf", + "um": [ + 122, + 46 + ] + }, + "bein_vorne_links": { + "eltern": "rumpf", + "um": [ + 92, + 332 + ] + }, + "bein_vorne_rechts": { + "eltern": "rumpf", + "um": [ + 124, + 340 + ] + }, + "bein_hinten_links": { + "eltern": "rumpf", + "um": [ + 186, + 345 + ] + }, + "bein_hinten_rechts": { + "eltern": "rumpf", + "um": [ + 230, + 335 + ] + }, + "schwanz": { + "eltern": "rumpf", + "um": [ + 246, + 288 + ] + }, + "schal_schlinge": { + "eltern": "hals", + "um": [ + 124, + 244 + ] + }, + "schal_ende": { + "eltern": "schal_schlinge", + "um": [ + 62, + 252 + ] + }, + "schal_boden": { + "eltern": "rumpf", + "um": [ + 160, + 360 + ] + } + }, + "pino": { + "koerper": { + "eltern": null, + "um": [ + 230, + 430 + ] + }, + "kopf": { + "eltern": "koerper", + "um": [ + 400, + 465 + ] + }, + "ohr_links": { + "eltern": "kopf", + "um": [ + 290, + 258 + ] + }, + "ohr_rechts": { + "eltern": "kopf", + "um": [ + 510, + 270 + ] + }, + "bauch": { + "eltern": "koerper", + "um": [ + 330, + 600 + ] + }, + "arm_links": { + "eltern": "koerper", + "um": [ + 322, + 462 + ] + }, + "arm_rechts": { + "eltern": "koerper", + "um": [ + 458, + 468 + ] + }, + "fuss_links": { + "eltern": "koerper", + "um": [ + 310, + 568 + ] + }, + "fuss_rechts": { + "eltern": "koerper", + "um": [ + 495, + 600 + ] + }, + "bommel": { + "eltern": "muetze", + "um": [ + 445, + 88 + ] + }, + "muetze": { + "eltern": "kopf", + "um": [ + 400, + 200 + ] + } + }, + "noa": { + "rumpf": { + "eltern": null, + "um": [ + 115, + 165 + ] + }, + "kopf": { + "eltern": "rumpf", + "um": [ + 120, + 156 + ] + }, + "zopf_links": { + "eltern": "kopf", + "um": [ + 48, + 120 + ] + }, + "zopf_rechts": { + "eltern": "kopf", + "um": [ + 178, + 108 + ] + }, + "arm_links": { + "eltern": "rumpf", + "um": [ + 64, + 172 + ] + }, + "arm_rechts": { + "eltern": "rumpf", + "um": [ + 166, + 172 + ] + }, + "bein_links": { + "eltern": "rumpf", + "um": [ + 92, + 322 + ] + }, + "bein_rechts": { + "eltern": "rumpf", + "um": [ + 128, + 322 + ] + }, + "hand_links": { + "eltern": "arm_links", + "um": [ + 54, + 292 + ] + }, + "hand_rechts": { + "eltern": "arm_rechts", + "um": [ + 186, + 275 + ] + }, + "stiefel_links": { + "eltern": "bein_links", + "um": [ + 92, + 350 + ] + }, + "stiefel_rechts": { + "eltern": "bein_rechts", + "um": [ + 132, + 350 + ] + } + } +}; diff --git a/remotion/src/szenen/VerformTest.tsx b/remotion/src/szenen/VerformTest.tsx index 13f756a..96e8caf 100644 --- a/remotion/src/szenen/VerformTest.tsx +++ b/remotion/src/szenen/VerformTest.tsx @@ -1,15 +1,26 @@ import React from 'react'; import {AbsoluteFill, Img, staticFile, useCurrentFrame} from 'remotion'; -import {feder, glatt, kreis, kurve} from '../bewegung'; +import {feder, kreis, kurve} from '../bewegung'; import {LULU, PINO} from '../figuren'; +import {RIGS} from '../rigs'; import {VerformFigur, type Knochen} from '../teile/VerformFigur'; // Test für weiche Animation (6 s, ohne Ton): Lulu beugt den Hals zu Pino -// hinunter, Pino freut sich und hüpft. Alles über Gitter-Verformung und -// Federn, Hintergrund und Papier sind vorberechnete Bilder. +// hinunter, Pino freut sich und hüpft. Alles über Gitter-Verformung mit den +// Körperteil-Masken aus werkzeuge/rig.py und Federn; Hintergrund und Papier +// sind vorberechnete Bilder. const BODEN = 884; +// Drehgelenk eines Rig-Teils: Gelenkpunkt und Einfluss-Maske (Teil + alles, was daran hängt). +const gelenk = (figur: string, teil: string, grad: number): Knochen => ({ + name: teil, + art: 'drehen', + um: RIGS[figur][teil].um!, + grad, + gewicht: {maske: `${figur}/einfluss/${teil}`}, +}); + // ---------- Lulu ---------- const halsKurve = (f: number) => kurve(f, [ @@ -29,70 +40,52 @@ const luluKnochen = (f: number): Knochen[] => { const kopf = feder((g) => -halsKurve(g) * 0.35 + neugierKurve(g), f, {steifheit: 0.07, daempfung: 0.18}); const ohrL = feder((g) => -26 * ohrZucken(g, [20, 98]), f, {steifheit: 0.25, daempfung: 0.12}) + (hals - halsTraege) * 0.6; const ohrR = feder((g) => 22 * ohrZucken(g, [60, 104]), f, {steifheit: 0.25, daempfung: 0.12}) - (hals - halsTraege) * 0.5; - const schwanz = 13 * Math.sin(f * 0.085) + feder((g) => halsKurve(g) * 0.5, f, {steifheit: 0.04, daempfung: 0.1}) - halsKurve(f) * 0.5; + const schwanz = -6 + 10 * Math.sin(f * 0.085) + feder((g) => halsKurve(g) * 0.5, f, {steifheit: 0.04, daempfung: 0.1}) - halsKurve(f) * 0.5; const schal = (halsTraege - hals) * 0.9 + 2.5 * Math.sin(f * 0.07); const atmen = Math.sin(f * 0.1); return [ - {art: 'drehen', um: [45, 56], grad: ohrL, gewicht: kreis(22, 32, 18, 42)}, - {art: 'drehen', um: [150, 68], grad: ohrR, gewicht: kreis(176, 56, 16, 38)}, - {art: 'drehen', um: [112, 158], grad: kopf, gewicht: (x, y) => glatt(172, 128, y)}, - {art: 'drehen', um: [125, 238], grad: hals, gewicht: (x, y) => glatt(244, 150, y)}, - { - art: 'drehen', - um: [244, 288], - grad: schwanz, - gewicht: (x, y) => glatt(236, 252, x) * glatt(268, 284, y) * glatt(392, 376, y), - }, - { - art: 'drehen', - um: [62, 252], - grad: schal, - gewicht: (x, y) => glatt(252, 330, y) * glatt(74, 62, x) * glatt(395, 380, y), - }, - {art: 'stauchen', um: [175, 330], sx: 1 - atmen * 0.006, sy: 1 + atmen * 0.012, gewicht: kreis(180, 300, 50, 110)}, + gelenk('lulu', 'ohr_links', ohrL), + gelenk('lulu', 'ohr_rechts', ohrR), + gelenk('lulu', 'kopf', kopf), + gelenk('lulu', 'hals', hals), + gelenk('lulu', 'schwanz', schwanz), + gelenk('lulu', 'schal_ende', schal), + {name: 'atmen', art: 'stauchen', um: [175, 330], sx: 1 - atmen * 0.006, sy: 1 + atmen * 0.012, gewicht: kreis(180, 300, 50, 110)}, ]; }; // ---------- Pino ---------- -// Sprung: Anlauf (Ducken), Flug, Landung mit Nachfedern. -const SPRUNG = {duck: 84, ab: 93, auf: 111}; +// Kleiner Freudenhüpfer: kurz ducken, abheben, landen, nachfedern. +const SPRUNG = {duck: 86, ab: 93, auf: 107}; const sprungHoehe = (f: number) => { if (f < SPRUNG.ab || f > SPRUNG.auf) return 0; const t = (f - SPRUNG.ab) / (SPRUNG.auf - SPRUNG.ab); return 4 * t * (1 - t); }; const stauchZiel = (f: number) => { - if (f >= SPRUNG.duck && f < SPRUNG.ab) return kurve(f, [[SPRUNG.duck, 1], [SPRUNG.ab - 2, 0.86]]); - if (f >= SPRUNG.ab && f <= SPRUNG.auf) return 1 + 0.09 * Math.sin(Math.PI * Math.min(1, (f - SPRUNG.ab) / 8)) * (1 - sprungHoehe(f) * 0.3); - if (f > SPRUNG.auf && f < SPRUNG.auf + 4) return 0.84; + if (f >= SPRUNG.duck && f < SPRUNG.ab) return kurve(f, [[SPRUNG.duck, 1], [SPRUNG.ab - 1, 0.95]]); + if (f >= SPRUNG.ab && f <= SPRUNG.auf) return 1 + 0.035 * Math.sin(Math.PI * Math.min(1, (f - SPRUNG.ab) / 7)); + if (f > SPRUNG.auf && f < SPRUNG.auf + 3) return 0.955; return 1; }; -const pinoKopfKurve = (f: number) => kurve(f, [[0, 0], [38, 0], [56, -9], [84, -9], [92, -3], [128, -3], [146, 7], [180, 7]]); -const freude = (f: number) => (f > SPRUNG.auf + 4 && f < SPRUNG.auf + 40 ? Math.sin((f - SPRUNG.auf - 4) * 0.35) : 0); +const pinoKopfKurve = (f: number) => kurve(f, [[0, 0], [38, 0], [56, -7], [86, -7], [94, -2], [128, -2], [146, 5], [180, 5]]); +const freude = (f: number) => (f > SPRUNG.auf + 3 && f < SPRUNG.auf + 38 ? Math.sin((f - SPRUNG.auf - 3) * 0.35) : 0); const pinoWerte = (f: number) => { - const stauch = feder(stauchZiel, f, {steifheit: 0.3, daempfung: 0.22}); - const hoch = sprungHoehe(f) * 80; + const stauch = feder(stauchZiel, f, {steifheit: 0.3, daempfung: 0.3}); + const hoch = sprungHoehe(f) * 28; const tempo = (g: number) => (sprungHoehe(g + 1) - sprungHoehe(g - 1)) * 40; - const bommel = feder((g) => tempo(g) * 1.4, f, {steifheit: 0.12, daempfung: 0.1}); + const bommel = feder((g) => tempo(g) * 0.8, f, {steifheit: 0.12, daempfung: 0.1}); + const ohr = feder((g) => (g > SPRUNG.auf && g < SPRUNG.auf + 3 ? -14 : 0), f, {steifheit: 0.25, daempfung: 0.12}); const atmen = Math.sin(f * 0.11); const knochen: Knochen[] = [ - {art: 'drehen', um: [432, 92], grad: bommel + 3 * Math.sin(f * 0.06), gewicht: kreis(432, 48, 34, 56)}, - { - art: 'drehen', - um: [330, 292], - grad: feder((g) => (g > SPRUNG.auf && g < SPRUNG.auf + 4 ? -20 : 0), f, {steifheit: 0.25, daempfung: 0.12}), - gewicht: kreis(318, 266, 22, 42), - }, - {art: 'drehen', um: [430, 500], grad: freude(f) * 14, gewicht: kreis(432, 522, 22, 42)}, - {art: 'drehen', um: [520, 495], grad: -freude(f) * 12, gewicht: kreis(540, 515, 20, 38)}, - { - art: 'drehen', - um: [380, 480], - grad: feder(pinoKopfKurve, f, {steifheit: 0.08, daempfung: 0.2}), - gewicht: (x, y) => glatt(520, 380, y) * glatt(230, 330, x), - }, - {art: 'stauchen', um: [420, 600], sx: 1 + atmen * 0.01, sy: 1 + atmen * 0.014, gewicht: kreis(420, 540, 70, 170)}, + gelenk('pino', 'bommel', bommel + 3 * Math.sin(f * 0.06)), + gelenk('pino', 'ohr_links', ohr), + gelenk('pino', 'ohr_rechts', -ohr * 0.7), + gelenk('pino', 'arm_links', freude(f) * 12), + gelenk('pino', 'arm_rechts', -freude(f) * 10), + gelenk('pino', 'kopf', feder(pinoKopfKurve, f, {steifheit: 0.08, daempfung: 0.2})), + {name: 'atmen', art: 'stauchen', um: [420, 600], sx: 1 + atmen * 0.01, sy: 1 + atmen * 0.014, gewicht: kreis(420, 540, 70, 170)}, ]; return {stauch, hoch, knochen}; }; diff --git a/remotion/src/teile/VerformFigur.tsx b/remotion/src/teile/VerformFigur.tsx index 4dc449b..1f3f4b4 100644 --- a/remotion/src/teile/VerformFigur.tsx +++ b/remotion/src/teile/VerformFigur.tsx @@ -7,24 +7,48 @@ import type {FigurDaten} from '../figuren'; // verschieben, werden die Gitterpunkte über „Knochen“ gedreht und gestaucht. // So biegen sich Hals, Ohren, Schwanz usw. weich. -export type Gewicht = (x: number, y: number) => number; +// Gewicht eines Gitterpunkts für einen Knochen: entweder eine Formel in +// Bildkoordinaten oder der Name einer Maske (Graustufen-PNG in public/figuren/, +// gleich groß wie die Figur, weiß = bewegt sich voll mit). +export type Gewicht = ((x: number, y: number) => number) | {maske: string}; -export type Knochen = - | {art: 'drehen'; um: [number, number]; grad: number; gewicht: Gewicht} - | {art: 'stauchen'; um: [number, number]; sx: number; sy: number; gewicht: Gewicht}; +type Basis = {name: string; um: [number, number]; gewicht: Gewicht}; +export type Knochen = (Basis & {art: 'drehen'; grad: number}) | (Basis & {art: 'stauchen'; sx: number; sy: number}); const RAND = 0.35; // Platz um das Bild, falls Teile über den Rand hinausschwingen -const SPALTEN = 48; -const ZEILEN = 64; +const ZELLE = 3; // Gitterweite in Bildpixeln + +type Maske = {breite: number; hoehe: number; werte: Uint8ClampedArray}; + +const maskenWert = (m: Maske, x: number, y: number) => { + const ix = Math.min(m.breite - 1, Math.max(0, Math.round(x))); + const iy = Math.min(m.hoehe - 1, Math.max(0, Math.round(y))); + return m.werte[(iy * m.breite + ix) * 4] / 255; +}; + +const ladeMaske = (name: string) => + new Promise((ok, fehler) => { + const bild = new Image(); + bild.onload = () => { + const c = document.createElement('canvas'); + c.width = bild.width; + c.height = bild.height; + const ctx = c.getContext('2d')!; + ctx.drawImage(bild, 0, 0); + ok({breite: bild.width, hoehe: bild.height, werte: ctx.getImageData(0, 0, bild.width, bild.height).data}); + }; + bild.onerror = fehler; + bild.src = staticFile(`figuren/${name}.png`); + }); // Knochen werden der Reihe nach angewendet: zuerst die äußeren (Ohr), // dann die inneren (Hals), damit Kinder mit den Eltern mitwandern. -// Die Gewichte beziehen sich immer auf die Ruheposition. -const verforme = (x: number, y: number, knochen: Knochen[]): [number, number] => { +// Die Gewichte beziehen sich auf die Ruheposition und werden pro Knochen einmal berechnet. +const verforme = (n: number, x: number, y: number, knochen: Knochen[], gewichte: Map): [number, number] => { let px = x; let py = y; for (const k of knochen) { - const w = k.gewicht(x, y); + const w = gewichte.get(k.name)![n]; if (w <= 0) continue; const [ux, uy] = k.um; if (k.art === 'drehen') { @@ -59,6 +83,8 @@ type Szene = { kamera: THREE.OrthographicCamera; pos: THREE.BufferAttribute; ruhe: Float32Array; + masken: Map; + gewichte: Map; }; export const VerformFigur: React.FC = ({daten, hoehe, x, y, knochen, drehung = 0, stauchX = 1, stauchY = 1, filter}) => { @@ -85,6 +111,8 @@ export const VerformFigur: React.FC = ({daten, hoehe, x, y, knochen, dreh renderer.outputColorSpace = THREE.SRGBColorSpace; const kamera = new THREE.OrthographicCamera(-mx, W + mx, my, -(H + my), -10, 10); + const SPALTEN = Math.ceil(W / ZELLE); + const ZEILEN = Math.ceil(H / ZELLE); const ruhe = new Float32Array((SPALTEN + 1) * (ZEILEN + 1) * 2); const uv = new Float32Array((SPALTEN + 1) * (ZEILEN + 1) * 2); const index: number[] = []; @@ -111,24 +139,40 @@ export const VerformFigur: React.FC = ({daten, hoehe, x, y, knochen, dreh geo.setIndex(index); const welt = new THREE.Scene(); - new THREE.TextureLoader().load(staticFile(daten.datei), (tex) => { + const maskenNamen = [...new Set(knochen.flatMap((k) => ('maske' in k.gewicht ? [k.gewicht.maske] : [])))]; + const textur = new THREE.TextureLoader().loadAsync(staticFile(daten.datei)); + Promise.all([textur, Promise.all(maskenNamen.map(ladeMaske))]).then(([tex, geladen]) => { tex.colorSpace = THREE.SRGBColorSpace; tex.premultiplyAlpha = true; tex.anisotropy = 4; const mat = new THREE.MeshBasicMaterial({map: tex, transparent: true, premultipliedAlpha: true}); welt.add(new THREE.Mesh(geo, mat)); - szene.current = {renderer, szene: welt, kamera, pos, ruhe}; + const masken = new Map(maskenNamen.map((m, i) => [m, geladen[i]])); + szene.current = {renderer, szene: welt, kamera, pos, ruhe, masken, gewichte: new Map()}; setBereit(true); }); return () => renderer.dispose(); + // Die Knochen-Namen und Masken stehen beim ersten Frame fest. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [daten.datei, W, H, mx, my]); useLayoutEffect(() => { const sz = szene.current; if (!sz) return; const n = sz.ruhe.length / 2; + for (const k of knochen) { + if (sz.gewichte.has(k.name)) continue; + const g = new Float32Array(n); + const gw = k.gewicht; + for (let i = 0; i < n; i++) { + const x = sz.ruhe[i * 2]; + const y = sz.ruhe[i * 2 + 1]; + g[i] = 'maske' in gw ? maskenWert(sz.masken.get(gw.maske)!, x, y) : gw(x, y); + } + sz.gewichte.set(k.name, g); + } for (let i = 0; i < n; i++) { - const [px, py] = verforme(sz.ruhe[i * 2], sz.ruhe[i * 2 + 1], knochen); + const [px, py] = verforme(i, sz.ruhe[i * 2], sz.ruhe[i * 2 + 1], knochen, sz.gewichte); sz.pos.setXYZ(i, px, -py, 0); } sz.pos.needsUpdate = true; diff --git a/remotion/werkzeuge/rig.py b/remotion/werkzeuge/rig.py new file mode 100644 index 0000000..20b613a --- /dev/null +++ b/remotion/werkzeuge/rig.py @@ -0,0 +1,356 @@ +"""Erzeugt die Körperteil-Masken („Rig“) für die freigestellten Figuren. + +Für jede Figur ist unten ein Skelett aus Knochenlinien (in Bildpixeln) definiert. +Jedes Pixel der Figur wird genau einem Teil zugeordnet: + 1. Accessoires (Schal, Mütze, Fäustlinge, Stiefel …) über ihre Farbe, + 2. alles andere über den kürzesten Weg *innerhalb der Figur* zum nächsten + Knochen. Der Weg führt nicht über Lücken, deshalb greift z. B. Lulus + Schwanz nie ins Bein daneben. + +Ausgabe je Figur unter public/figuren//: + teile/.png harte Maske des Teils (weiß = gehört dazu) + einfluss/.png weiche Maske für die Verformung: das Teil plus alles, + was daran hängt, mit weichem Übergang am Gelenk + uebersicht.png alle Teile farbig, zum Prüfen +Dazu src/rigs.ts mit Gelenkpunkten und Hierarchie. + +Aufruf: python3 werkzeuge/rig.py (braucht numpy, scipy, pillow) +""" + +import colorsys +import json +import os +import sys + +import numpy as np +from PIL import Image, ImageDraw, ImageFilter +from scipy import ndimage + +HIER = os.path.dirname(os.path.abspath(__file__)) +FIGUREN = os.path.join(HIER, "..", "public", "figuren") +RIGS_TS = os.path.join(HIER, "..", "src", "rigs.ts") + +# Knochen: name, eltern, von (Gelenk), bis. weich = Länge des Übergangs am Gelenk in px. +# Accessoire-Gruppen: Farbe + Bereich; ihre Teile werden über Startlinien getrennt. + +FIGUREN_DEF = { + "lulu": { + "knochen": [ + ("rumpf", None, (130, 292), (232, 296), 0, + [(120, 245), (170, 238), (230, 250), (248, 290), (245, 345), (200, 352), (150, 347), (105, 337), (95, 300)]), + ("hals", "rumpf", (124, 244), (112, 156), 14, + [(78, 150), (132, 140), (142, 178), (128, 205), (85, 205), (72, 176)]), + ("kopf", "hals", (111, 150), (86, 72), 10, + [(10, 60), (40, 45), (60, 47), (80, 25), (110, 30), (140, 52), (146, 90), (136, 135), (126, 152), + (95, 160), (60, 156), (25, 140), (14, 110)]), + ("ohr_links", "kopf", (44, 57), (12, 25), 5, + [(0, 10), (30, 12), (52, 40), (52, 62), (40, 70), (20, 60), (4, 40)]), + ("ohr_rechts", "kopf", (146, 72), (186, 56), 5, + [(140, 56), (165, 42), (196, 45), (193, 62), (170, 78), (146, 88), (140, 78)]), + ("horn_links", "kopf", (65, 44), (62, 8), 4, + [(55, 0), (78, 0), (78, 30), (73, 44), (58, 44), (56, 30)]), + ("horn_rechts", "kopf", (122, 46), (126, 10), 4, + [(112, 8), (137, 8), (137, 28), (129, 48), (117, 48), (112, 30)]), + ("bein_vorne_links", "rumpf", (92, 332), (80, 465), 12, + [(95, 320), (106, 335), (100, 400), (95, 445), (93, 472), (62, 472), (70, 440), (72, 400), (78, 340)]), + ("bein_vorne_rechts", "rumpf", (124, 340), (127, 475), 12, + [(108, 338), (141, 335), (140, 400), (138, 470), (141, 487), (110, 487), (115, 440), (112, 380)]), + ("bein_hinten_links", "rumpf", (186, 345), (193, 448), 12, + [(168, 342), (200, 346), (205, 400), (206, 460), (178, 460), (182, 420), (170, 380)]), + ("bein_hinten_rechts", "rumpf", (230, 335), (235, 470), 12, + [(212, 332), (243, 324), (250, 360), (248, 420), (251, 481), (222, 481), (225, 430), (215, 380)]), + ("schwanz", "rumpf", (246, 288), (266, 364), 6, + [(244, 282), (256, 300), (262, 330), (277, 350), (273, 373), (256, 373), (254, 340), (244, 300)]), + ], + "accessoires": [ + { + "farbe": "gruen", + "bereich": None, + "teile": [ + ("schal_schlinge", "hals", [(70, 228), (160, 205)], (124, 244), 10), + ("schal_ende", "schal_schlinge", [(40, 292), (45, 372)], (62, 252), 10), + ("schal_boden", "rumpf", [(230, 420), (350, 440)], (160, 360), 10), + ("schal_boden", None, [(160, 362), (190, 388)], None, 0), + ], + }, + { + "farbe": "rot", + "bereich": (283, 378, 318, 404), + "teile": [("schal_boden", None, [(300, 392)], None, 0)], + }, + ], + }, + "pino": { + "knochen": [ + ("koerper", None, (230, 430), (400, 560), 0, + [(20, 350), (150, 200), (245, 200), (262, 300), (275, 445), (300, 560), (200, 640), (60, 560)]), + ("kopf", "koerper", (400, 465), (420, 320), 26, + [(245, 150), (300, 115), (420, 120), (520, 180), (554, 240), (554, 455), (470, 465), (380, 462), + (300, 450), (268, 400), (262, 300), (240, 240)]), + ("ohr_links", "kopf", (290, 258), (256, 222), 6, "kreis", (272, 240, 27)), + ("ohr_rechts", "kopf", (510, 270), (538, 248), 6, "kreis", (525, 258, 21)), + ("bauch", "koerper", (330, 600), (430, 470), 20, + [(300, 450), (470, 460), (485, 520), (480, 600), (470, 640), (400, 645), (360, 610), (300, 545)]), + ("arm_links", "koerper", (322, 462), (370, 540), 10, + [(322, 456), (345, 470), (380, 505), (392, 540), (372, 556), (345, 548), (330, 515), (318, 480)]), + ("arm_rechts", "koerper", (458, 468), (455, 545), 10, + [(445, 465), (472, 470), (478, 520), (470, 556), (442, 556), (440, 510)]), + ("fuss_links", "koerper", (310, 568), (320, 645), 10, + [(305, 565), (355, 563), (360, 600), (350, 655), (315, 658), (305, 620)]), + ("fuss_rechts", "koerper", (495, 600), (540, 575), 10, + [(495, 560), (540, 548), (554, 575), (545, 640), (505, 640), (490, 600)]), + ], + "accessoires": [ + { + "farbe": "weiss", + "bereich": (360, 0, 500, 92), + "teile": [("bommel", "muetze", [(445, 45)], (445, 88), 8)], + }, + { + "farbe": "blau", + "bereich": (250, 60, 554, 215), + "teile": [("muetze", "kopf", [(330, 170), (480, 170)], (400, 200), 10)], + }, + ], + }, + "noa": { + "knochen": [ + ("rumpf", None, (115, 165), (115, 320), 0, + [(80, 150), (160, 150), (165, 200), (170, 320), (75, 325), (82, 200)]), + ("kopf", "rumpf", (120, 156), (112, 40), 12, + [(55, 15), (115, 3), (165, 15), (190, 45), (197, 90), (182, 108), (170, 135), (140, 152), (100, 155), + (70, 140), (48, 118), (32, 95), (30, 60)]), + ("zopf_links", "kopf", (48, 120), (15, 135), 6, + [(0, 120), (20, 108), (45, 112), (52, 130), (45, 150), (20, 155), (5, 145)]), + ("zopf_rechts", "kopf", (178, 108), (200, 126), 6, + [(172, 100), (195, 95), (210, 108), (212, 130), (195, 148), (176, 140)]), + ("arm_links", "rumpf", (64, 172), (52, 288), 12, + [(35, 180), (62, 165), (78, 180), (80, 290), (60, 300), (35, 295), (28, 240)]), + ("arm_rechts", "rumpf", (166, 172), (183, 272), 12, + [(160, 170), (180, 180), (192, 230), (195, 275), (178, 280), (165, 240), (158, 190)]), + ("bein_links", "rumpf", (92, 322), (92, 350), 6, + [(78, 320), (108, 320), (108, 352), (80, 352)]), + ("bein_rechts", "rumpf", (128, 322), (132, 350), 6, + [(110, 320), (142, 320), (142, 352), (112, 352)]), + ], + "accessoires": [ + { + "farbe": "rot", + "bereich": (0, 260, 219, 340), + "teile": [ + ("hand_links", "arm_links", [(55, 310)], (54, 292), 6), + ("hand_rechts", "arm_rechts", [(188, 292)], (186, 275), 6), + ], + }, + { + "farbe": "rot", + "bereich": (0, 340, 219, 429), + "teile": [ + ("stiefel_links", "bein_links", [(92, 362), (95, 415)], (92, 350), 6), + ("stiefel_rechts", "bein_rechts", [(140, 362), (150, 410)], (132, 350), 6), + ], + }, + ], + }, +} + + +def farbmaske(rgb, art): + h, w, _ = rgb.shape + hsv = np.array([colorsys.rgb_to_hsv(*(p / 255)) for p in rgb.reshape(-1, 3)]).reshape(h, w, 3) + hue, sat, val = hsv[..., 0] * 360, hsv[..., 1], hsv[..., 2] + if art == "gruen": + m = (hue > 52) & (hue < 180) & (sat > 0.06) & (val > 0.12) + elif art == "blau": + m = (hue > 185) & (hue < 260) & (sat > 0.12) + elif art == "rot": + m = ((hue < 22) | (hue > 335)) & (sat > 0.35) & (val > 0.3) + elif art == "weiss": + m = (sat < 0.18) & (val > 0.72) + else: + raise ValueError(art) + return m + + +def linie_pixel(punkte, form): + bild = Image.new("L", (form[1], form[0]), 0) + d = ImageDraw.Draw(bild) + if len(punkte) == 1: + x, y = punkte[0] + d.ellipse((x - 2, y - 2, x + 2, y + 2), fill=1) + else: + d.line(punkte, fill=1, width=3) + return np.asarray(bild).astype(bool) + + +def flaechen_pixel(flaeche, form): + bild = Image.new("L", (form[1], form[0]), 0) + d = ImageDraw.Draw(bild) + if flaeche[0] == "kreis": + x, y, r = flaeche[1] + d.ellipse((x - r, y - r, x + r, y + r), fill=1) + else: + d.polygon(flaeche[0], fill=1) + return np.asarray(bild).astype(bool) + + +def ausbreiten(labels, erlaubt): + """Wachsen der Labels innerhalb von `erlaubt`, Pixel für Pixel (Weg innerhalb der Figur).""" + labels = labels.copy() + kreuz = ndimage.generate_binary_structure(2, 2) + while True: + frei = erlaubt & (labels == 0) + if not frei.any(): + break + gewachsen = ndimage.grey_dilation(labels, footprint=kreuz) + neu = frei & (gewachsen > 0) + if not neu.any(): + break + labels[neu] = gewachsen[neu] + return labels + + +def rampe_entlang(form, von, bis, weich): + """0 hinter dem Gelenk, 1 nach `weich` px in Knochenrichtung.""" + if weich <= 0: + return np.ones(form) + yy, xx = np.mgrid[0 : form[0], 0 : form[1]].astype(float) + dx, dy = bis[0] - von[0], bis[1] - von[1] + l = np.hypot(dx, dy) + t = ((xx - von[0]) * dx + (yy - von[1]) * dy) / l + s = np.clip((t + weich * 0.4) / weich, 0, 1) + return s * s * (3 - 2 * s) + + +def baue(figur, d): + bild = Image.open(os.path.join(FIGUREN, f"{figur}.png")).convert("RGBA") + arr = np.asarray(bild).astype(float) + rgb, alpha = arr[..., :3], arr[..., 3] + figur_maske = alpha > 40 + form = figur_maske.shape + + namen = [] + info = {} # name -> (eltern, gelenk, bis, weich) + for name, eltern, von, bis, weich, *_ in d["knochen"]: + namen.append(name) + info[name] = (eltern, von, bis, weich) + for gruppe in d["accessoires"]: + for name, eltern, linie, gelenk, weich in gruppe["teile"]: + if name not in info: + namen.append(name) + info[name] = (eltern, gelenk, linie[-1], weich) + nummer = {n: i + 1 for i, n in enumerate(namen)} + + labels = np.zeros(form, dtype=np.int32) + + # 1. Accessoires über Farbe + for gruppe in d["accessoires"]: + m = figur_maske & farbmaske(rgb, gruppe["farbe"]) + if gruppe["bereich"]: + x0, y0, x1, y1 = gruppe["bereich"] + b = np.zeros(form, bool) + b[y0:y1, x0:x1] = True + m &= b + m = ndimage.binary_opening(m, iterations=1) + m = ndimage.binary_closing(m, iterations=2) & figur_maske + start = np.zeros(form, np.int32) + for name, _, linie, _, _ in gruppe["teile"]: + start[linie_pixel(linie, form) & m] = nummer[name] + # Flächen ohne Startlinie (z. B. Schalstück zwischen zwei Beinen) bekommen ab einer + # Mindestgröße das nächstgelegene Teil der Gruppe, kleine Farbflecken fallen weg. + lab, n = ndimage.label(m) + verbunden = set(np.unique(lab[start > 0])) - {0} + _, (iy, ix) = ndimage.distance_transform_edt(start == 0, return_indices=True) + for k in range(1, n + 1): + if k in verbunden: + continue + flaeche = lab == k + if flaeche.sum() >= 40: + ys, xs = np.where(flaeche) + y0, x0 = ys[len(ys) // 2], xs[len(xs) // 2] + start[flaeche] = start[iy[y0, x0], ix[y0, x0]] + verbunden.add(k) + behalten = np.isin(lab, list(verbunden)) & (lab > 0) + behalten = ndimage.binary_fill_holes(behalten) & figur_maske # z. B. Blümchen im Schal + gruppe_labels = ausbreiten(start, behalten) + frei = labels == 0 + labels[frei & (gruppe_labels > 0)] = gruppe_labels[frei & (gruppe_labels > 0)] + + # 2. Körper: Kernflächen der Teile (spätere Teile überschreiben frühere), dazu die Knochenlinie + start = np.zeros(form, np.int32) + rest = figur_maske & (labels == 0) + for name, _, von, bis, _, *flaeche in d["knochen"]: + kern = linie_pixel([von, bis], form) + if flaeche: + kern |= flaechen_pixel(flaeche, form) + start[kern & rest] = nummer[name] + for name in [k[0] for k in d["knochen"]]: + if not (start == nummer[name]).any(): + print(f" Warnung: Knochen {name} liegt nicht auf der Figur", file=sys.stderr) + koerper = ausbreiten(start, rest) + labels[rest] = koerper[rest] + # 3. abgeschnittene Inseln über die ganze Figur auffüllen + labels = ausbreiten(labels, figur_maske) + + ziel = os.path.join(FIGUREN, figur) + os.makedirs(os.path.join(ziel, "teile"), exist_ok=True) + os.makedirs(os.path.join(ziel, "einfluss"), exist_ok=True) + + kinder = {n: [m for m in namen if info[m][0] == n] for n in namen} + + def unterbaum(n): + return [n] + [x for k in kinder[n] for x in unterbaum(k)] + + # Für die Verformung reicht jedes Teil über die Kontur hinaus: Punkte außerhalb der Figur + # bekommen das Teil des nächsten Figurpixels. Sonst bleiben Gitterpunkte am Rand stehen, + # während die Figur sich dreht, und die Randpixel verschmieren. + _, (iy, ix) = ndimage.distance_transform_edt(~figur_maske, return_indices=True) + labels_aussen = labels[iy, ix] + + for name in namen: + teil = labels == nummer[name] + Image.fromarray((teil * 255).astype("uint8")).save(os.path.join(ziel, "teile", f"{name}.png")) + eltern, gelenk, bis, weich = info[name] + baum = np.isin(labels_aussen, [nummer[x] for x in unterbaum(name)]) + weich_bild = Image.fromarray((baum * 255).astype("uint8")).filter(ImageFilter.GaussianBlur(1.5)) + e = np.asarray(weich_bild).astype(float) / 255 + if gelenk is not None: + e *= rampe_entlang(form, gelenk, bis, weich) + Image.fromarray((e * 255).astype("uint8")).save(os.path.join(ziel, "einfluss", f"{name}.png")) + + # Übersicht + rng = np.random.default_rng(3) + farben = rng.integers(60, 255, size=(len(namen) + 1, 3)) + farben[0] = (30, 30, 60) + uebersicht = farben[labels].astype(float) + uebersicht = uebersicht * 0.55 + rgb * 0.45 + uebersicht[~figur_maske] = (30, 30, 60) + ueb = Image.fromarray(uebersicht.astype("uint8")) + dr = ImageDraw.Draw(ueb) + for name in namen: + ys, xs = np.where(labels == nummer[name]) + if len(xs): + dr.text((int(xs.mean()) - 10, int(ys.mean()) - 4), name, fill=(255, 255, 255)) + ueb.save(os.path.join(ziel, "uebersicht.png")) + + groessen = {n: int((labels == nummer[n]).sum()) for n in namen} + print(figur, groessen) + return { + n: {"eltern": info[n][0], "um": list(info[n][1]) if info[n][1] else None} + for n in namen + } + + +def main(): + rigs = {figur: baue(figur, d) for figur, d in FIGUREN_DEF.items()} + with open(RIGS_TS, "w") as f: + f.write("// Automatisch erzeugt von werkzeuge/rig.py – nicht von Hand ändern.\n") + f.write("// Gelenkpunkte (um) in Bildpixeln der Figur, eltern = woran das Teil hängt.\n") + f.write("// Masken: figuren//teile/.png und figuren//einfluss/.png\n\n") + f.write("export type RigTeil = {eltern: string | null; um: [number, number] | null};\n\n") + daten = json.dumps(rigs, indent="\t", ensure_ascii=False) + f.write(f"export const RIGS: Record> = {daten};\n") + + +if __name__ == "__main__": + main()