Lulus Hals-Clip ohne Zoom, Noa klatscht im Lied, Nestbau ruhiger

- clipkorrektur.ts: Higgsfield-Kamera zoomt/schwenkt im Clip lulu_C9 (Beine
  +20 % breiter, 79 px versetzt) – je Clipbild zurückgerechnet, auch die
  Apfel-Bahn im Maul
- neue Pose noa_A_klatschen (Higgsfield, 1,5 Credits, Auftrag cbb2880c),
  Lied in Folge 1 und 2: Noa klatscht auf jeden zweiten Schlag (klatschFolge)
- schwebende Klatsch-Hände wieder entfernt
- Folge 2, Nestbau: kein seitliches Hin- und Herrutschen von Noa und Lulu
- .gitignore: .env/.env.local (ElevenLabs-Schlüssel)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MyLusGH2U5AudjoARey3N
This commit is contained in:
Lara 2026-11-02 21:55:08 +01:00
parent 7de2b609ba
commit 1db1001dfc
12 changed files with 100 additions and 17 deletions

4
.gitignore vendored
View file

@ -15,3 +15,7 @@ node_modules/
__pycache__/
schnitt/ausgabe/
schnitt/tmp/
# Zugangsschlüssel (ElevenLabs u. a.), nie ins Git
.env
.env.local

View file

@ -44,6 +44,7 @@ unter `figuren/bibliothek/`. Die **Clips** (MP4) sind nicht im Git (siehe
| noa_A8_rueckansicht | Rückansicht | 1 | 1,5 | |
| noa_A_arm-hoch-winken | rechter Arm hoch, winkend | 1 | 1,5 | |
| noa_A_arme-hoch | beide Arme hoch | 1 | 1,5 | |
| noa_A_klatschen | klatscht, Fäustlinge vor der Brust (02.11.2026, Nano Banana 2 aus noa_ref_stehend, für das Lied) | 1 | 1,5 | ohne rembg freigestellt |
| noa_A_laterne | Laterne in rechter Hand | 2 | 3 | 1. Versuch ohne Laterne; 2. gespiegelt; Laterne ragt seitlich weit heraus |
| noa_B1_traurig | traurig | 2 | 1,5 | 1. Versuch abgebrochen, erstattet |
| noa_B2_erschrocken | kurz erschrocken | 1 | 1,5 | |

View file

@ -61,3 +61,4 @@ lulu_B3_schlafen.png 23_e3e6898f-ba75-410d-9488-936dd5af4
lulu_B_kopf-profil.png 23_806c2339-90e8-42af-92b0-36b390e7d
noa_B1_traurig.png 030f14b3-a839-44d0-90bf-5a89e1fc92a9
pino_B3_schlafen.png dc751ba2-c05b-493b-95e1-c92668f1c627
noa_A_klatschen.png cbb2880c-95c7-4276-9cdf-a7eef639c5a7

1 noa_A1_augen-zu.png 46_c019cf04-d4ca-4e0c-8567-85858231b
61 lulu_B_kopf-profil.png 23_806c2339-90e8-42af-92b0-36b390e7d
62 noa_B1_traurig.png 030f14b3-a839-44d0-90bf-5a89e1fc92a9
63 pino_B3_schlafen.png dc751ba2-c05b-493b-95e1-c92668f1c627
64 noa_A_klatschen.png cbb2880c-95c7-4276-9cdf-a7eef639c5a7

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View file

@ -0,0 +1,42 @@
// Korrektur für Clips, in denen die Kamera von Higgsfield leicht zoomt oder schwenkt.
// Gemessen an den Beinen (Mitte und Breite) je Clip-Bild, in Pixeln des Clipbildes.
// Angewendet wird die Rückrechnung: die Beine bleiben am Platz und gleich breit.
type Messung = [number, number, number]; // [Clip-Bild (24 fps), Beinmitte x, Beinbreite]
const MESSUNGEN: Record<string, {bildBreite: number; fussY: number; werte: Messung[]}> = {
// Lulu senkt den Hals: Beine werden um 20 % breiter und wandern 79 px zur Seite
'lulu_C9_hals-runter': {
bildBreite: 662, // Breite des Clipbildes in Pixeln
fussY: 659,
werte: [
[0, 417.5, 211], [16, 417, 210], [20, 417.5, 211], [24, 418.5, 211], [28, 420.5, 213], [32, 423, 214],
[36, 426.5, 215], [40, 430, 216], [44, 434, 218], [48, 438, 218], [52, 442.5, 221], [56, 447, 222],
[60, 452, 224], [64, 457, 226], [68, 461, 228], [72, 466.5, 231], [76, 470.5, 233], [80, 475, 236],
[84, 479.5, 239], [88, 483.5, 241], [92, 487, 244], [96, 490.5, 247], [100, 493.5, 249], [104, 495.5, 251],
[108, 496.5, 253], [120, 496.5, 253],
],
},
};
export type Korrektur = {k: number; mitte: number; ziel: number; fussY: number; bildBreite: number};
// Für Clip-Bild `b`: Maßstab k und Beinmitte (Ist und Soll), alles in Clip-Pixeln.
export const clipKorrektur = (name: string, b: number): Korrektur | null => {
const m = MESSUNGEN[name];
if (!m) return null;
const w = m.werte;
const bb = Math.min(w[w.length - 1][0], Math.max(0, b));
let i = 0;
while (i < w.length - 2 && w[i + 1][0] < bb) i++;
const [b0, m0, w0] = w[i];
const [b1, m1, w1] = w[i + 1];
const t = b1 === b0 ? 0 : (bb - b0) / (b1 - b0);
const mitte = m0 + (m1 - m0) * t;
const breite = w0 + (w1 - w0) * t;
return {k: w[0][2] / breite, mitte, ziel: w[0][1], fussY: m.fussY, bildBreite: m.bildBreite};
};
// Punkt (in Clip-Pixeln) so verschieben, wie die Korrektur das Bild verschiebt
export const korrigiere = (kor: Korrektur | null, x: number, y: number) =>
kor ? {x: kor.ziel + kor.k * (x - kor.mitte), y: kor.fussY + kor.k * (y - kor.fussY)} : {x, y};

View file

@ -1,5 +1,6 @@
import React from 'react';
import {Freeze, Img, OffthreadVideo, getStaticFiles, staticFile, useCurrentFrame, useVideoConfig} from 'remotion';
import {clipKorrektur} from './clipkorrektur';
import {CLIPS, FIGUREN, POSEN} from './posen';
// Figuren aus der Bibliothek (freigestellte Standbilder und Clips).
@ -149,9 +150,15 @@ export const ClipFigur: React.FC<ClipProps> = ({name, start, hoehe, tempo = 1, s
let t = Math.max(0, (frame - start) * tempo);
t = schleife ? t % (laenge - 1) : Math.min(t, laenge - 1);
if (rueckwaerts) t = laenge - 1 - t;
// Zoom/Schwenk der Higgsfield-Kamera im Clip zurückrechnen (Beine bleiben am Platz)
const kor = clipKorrektur(name, (t * 24) / fps);
const px = kor ? c.b / kor.bildBreite : 1;
const korStil: React.CSSProperties = kor
? {transformOrigin: `${kor.mitte * px}px ${kor.fussY * px}px`, transform: `translateX(${(kor.ziel - kor.mitte) * px}px) scale(${kor.k})`}
: {};
return (
<Rahmen {...lage} figur={figur} hoehe={hoehe ?? NOA_HOEHE * GROESSE[figur]}>
<div style={{position: 'absolute', left: c.x, top: c.y, width: c.b, height: c.h}}>
<div style={{position: 'absolute', left: c.x, top: c.y, width: c.b, height: c.h, ...korStil}}>
<Freeze frame={Math.floor(t)}>
<OffthreadVideo src={staticFile(c.datei)} transparent muted style={{width: '100%', height: '100%'}} />
</Freeze>

View file

@ -1,3 +1,4 @@
import {clipKorrektur, korrigiere} from './clipkorrektur';
import React from 'react';
import {interpolate, useCurrentFrame} from 'remotion';
import {kurve} from '../bewegung';
@ -88,6 +89,18 @@ export const luluMaulC9 = (clipBild: number) => {
const [b0, x0, y0] = C9_NASE[i];
const [b1, x1, y1] = C9_NASE[i + 1];
const t = b1 === b0 ? 0 : (b - b0) / (b1 - b0);
// Maul knapp über der Unterkante der Nase
return {cx: C9.x + (x0 + (x1 - x0) * t) * C9.s, cy: C9.y + (y0 + (y1 - y0) * t - 6) * C9.s};
// Maul knapp über der Unterkante der Nase; dieselbe Zoom-Korrektur wie das Clipbild
const p = korrigiere(clipKorrektur('lulu_C9_hals-runter', b), x0 + (x1 - x0) * t, y0 + (y1 - y0) * t - 6);
return {cx: C9.x + p.x * C9.s, cy: C9.y + p.y * C9.s};
};
// Klatschen im Lied: Noa klatscht auf jeden zweiten Schlag (Schlag 1, 3, 5 … ab Liedbeginn).
// Liefert Posenwechsel für die Zeit von `von` bis `bis` (Hände zusammen kurz um den Schlag).
export const klatschFolge = (von: number, bis: number, takt: number, basis = 'noa_A3_lachen'): [number, string][] => {
const folge: [number, string][] = [];
for (let t = takt; t < bis; t += 2 * takt) {
if (t - 3 < von) continue;
folge.push([t - 3, 'noa_A_klatschen'], [t + 4, basis]);
}
return folge;
};

View file

@ -513,6 +513,20 @@ export const POSEN: Record<string, Pose> = {
701
]
},
"noa_A_klatschen": {
"figur": "noa",
"datei": "bibliothek/figuren/noa_A_klatschen.webp",
"x": 339,
"y": 156,
"b": 715,
"h": 551,
"box": [
543,
165,
807,
700
]
},
"noa_A_arme-hoch": {
"figur": "noa",
"datei": "bibliothek/figuren/noa_A_arme-hoch.webp",

View file

@ -1,8 +1,8 @@
import React from 'react';
import {random, useCurrentFrame} from 'remotion';
import {Figur, Kamera} from '../baukasten/figur';
import {Blende, Vignette} from '../baukasten/helfer';
import {Blume, Klatschen, Laubhaufen} from '../baukasten/natur';
import {Blende, Vignette, klatschFolge} from '../baukasten/helfer';
import {Blume, Laubhaufen} from '../baukasten/natur';
import {BODEN, Ort} from '../baukasten/orte';
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
import {TEXTE, plane} from './plan';
@ -57,10 +57,13 @@ export const Lied: React.FC = () => {
[z.s4.start, 'pino_A_hinterbeine'],
[z.w1.start, 'pino_A3_lachen'],
];
// Noa klatscht im Takt (jeder zweite Schlag), bei „zusammen kriegen wir alles hin“ Arme hoch
const noaPose: [number, string][] = [
[0, 'noa_A3_lachen'],
...klatschFolge(z.r1.start, z.r4.start, TAKT),
[z.r4.start, 'noa_A_arme-hoch'],
[z.s1.start, 'noa_A3_lachen'],
...klatschFolge(z.s1.start, z.w4.start, TAKT),
[z.w4.start, 'noa_A_arme-hoch'],
];
return (
@ -69,7 +72,7 @@ export const Lied: React.FC = () => {
<Ort name="garten_herbst" />
<Laubhaufen x={HAUFEN_X + 380} y={BODEN + 6} />
<Figur pose={luluPose} x={1300} y={BODEN} drehung={schwank(0.5, 2.2)} stauchY={1 - 0.012 * schlag} blende={8} />
<Figur pose={noaPose} x={980} y={BODEN + wipp(0.3, 10)} drehung={schwank(0, 3)} blende={8} />
<Figur pose={noaPose} x={980} y={BODEN + wipp(0.3, 10)} drehung={schwank(0, 3)} blende={3} />
<Figur pose={pinoPose} x={760} y={BODEN + wipp(0, 16)} drehung={schwank(1, 4)} stauchY={1 - 0.03 * schlag} blende={8} />
{f >= z.s4.start && f < z.w1.start && <Blume x={764} y={BODEN - 40} s={0.5} />}
</Kamera>
@ -85,8 +88,6 @@ export const Lied: React.FC = () => {
<div style={{position: 'absolute', top: 60, left: 0, right: 0, textAlign: 'center', fontFamily: SCHRIFT, fontWeight: 600, fontSize: 40, color: '#8a6a4a', opacity: Math.min(1, f / 20)}}>
♫ {TEXTE.lied} ♫
</div>
{/* Klatsch-Hände: auf jeden zweiten Schlag zum Mitklatschen */}
<Klatschen x={960} y={215} takt={TAKT} s={1.3} deckkraft={Math.min(1, f / 20)} />
<Vignette />
<Untertitel zeilen={LIED.liste} />
<Blende dauer={dauer} rein={12} raus={12} />

View file

@ -1,8 +1,7 @@
import React from 'react';
import {random, useCurrentFrame} from 'remotion';
import {Figur, Kamera} from '../baukasten/figur';
import {Blende, Vignette} from '../baukasten/helfer';
import {Klatschen} from '../baukasten/natur';
import {Blende, Vignette, klatschFolge} from '../baukasten/helfer';
import {BODEN, Ort} from '../baukasten/orte';
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
import {TEXTE, plane} from './plan';
@ -43,11 +42,14 @@ export const Lied: React.FC = () => {
<Figur
pose={[
[0, 'noa_A3_lachen'],
// Noa klatscht im Takt (jeder zweite Schlag)
...klatschFolge(z.r1.start, z.r4.start, TAKT),
[z.r4.start, 'noa_A_arme-hoch'],
]}
x={960}
y={BODEN + wipp(0.3, 10)}
drehung={schwank(0, 3)}
blende={3}
/>
<Figur pose="pino_A3_lachen" x={720} y={BODEN + wipp(0, 16)} drehung={schwank(1, 4)} stauchY={1 - 0.03 * schlag} />
</Kamera>
@ -59,8 +61,6 @@ export const Lied: React.FC = () => {
<div style={{position: 'absolute', top: 60, left: 0, right: 0, textAlign: 'center', fontFamily: SCHRIFT, fontWeight: 600, fontSize: 40, color: '#8a6a4a', opacity: Math.min(1, f / 20)}}>
♫ {TEXTE.lied} ♫
</div>
{/* Klatsch-Hände: auf jeden zweiten Schlag zum Mitklatschen */}
<Klatschen x={960} y={215} takt={TAKT} s={1.3} deckkraft={Math.min(1, f / 20)} />
<Vignette />
<Untertitel zeilen={LIED.liste} />
<Blende dauer={dauer} rein={12} raus={12} />

View file

@ -83,8 +83,8 @@ export const Szene4: React.FC = () => {
const hockeStauch = hockZeiten.reduce((s, [a, b]) => s * (1 - 0.05 * bump(a)) * (1 + 0.03 * bump(b)), 1);
// beim Hinlegen jedes Blatts kurz nach vorn beugen
const beugen = zaehlen.reduce((s, c) => s + Math.max(0, Math.sin(Math.min(1, Math.max(0, (f - c + 8) / 16)) * Math.PI)), 0);
// beim Nestbau schieben (vor und zurück)
const schieben = f > zurueck && f < z.bild4.ende ? Math.sin((f - zurueck) * 0.22) : 0;
// beim Nestbau: früher seitliches Hin- und Herrutschen (wirkte unruhig) – jetzt ruhig, nur die Blätter bewegen sich
// Pino
const pinoGehtVon = zumStapel + 40;
@ -202,7 +202,7 @@ export const Szene4: React.FC = () => {
kam.y += ruck(f, niesen, 9, 22);
const lulu = <Figur pose={luluPose} x={LULU_BAUM_X} y={luluY} stauchY={luluStauch} drehung={luluDreh} />;
const luluBeimNest = f >= zurueck && <Figur pose="lulu_A_hals-unten" x={2030} y={BODEN - 4} drehung={schieben * 1.2} />;
const luluBeimNest = f >= zurueck && <Figur pose="lulu_A_hals-unten" x={2030} y={BODEN - 4} />;
return (
<>
@ -225,10 +225,10 @@ export const Szene4: React.FC = () => {
) : (
<Figur
pose={noaPose}
x={noaX + (inHocke ? schieben * 10 : 0)}
x={noaX}
y={BODEN}
stauchY={atmen(f, 2) * hockeStauch * (inHocke ? 1 - 0.03 * beugen : 1)}
drehung={inHocke ? beugen * 3 + schieben * 2 : 0}
drehung={inHocke ? beugen * 3 : 0}
blende={HOCKE_BLENDE}
/>
)}