youtube/remotion/src/folge02/szene4.tsx
Lara 1db1001dfc 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
2026-11-02 21:55:08 +01:00

267 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import {useCurrentFrame} from 'remotion';
import {kurve} from '../bewegung';
import {Figur, Geher, Kamera} from '../baukasten/figur';
import {Blende, LULU_MAUL_A1, LULU_MAUL_LINKS, Vignette, atmen, hopfStauch, huepfer, kamFahrt, punkt, ruck} from '../baukasten/helfer';
import {Blaetterregen, Blatt, LAUB, Nest, Ruf} from '../baukasten/natur';
import {BODEN, NEST_X, Ort} from '../baukasten/orte';
import {NOA_X, PINO_X} from './szene2';
import {GROSSES_BLATT_X} from './szene3';
import {W} from '../baukasten/sprache';
import {Untertitel} from '../baukasten/untertitel';
import {plane} from './plan';
// Szene 4: Ein Bett aus Blättern. Zählen bis 5, Lulu niest, das Nest entsteht.
const NOA_ZAEHL_X = 1170;
const PINO_ZAEHL_X = 1790;
const LULU_BAUM_X = 650;
const REIHE = [0, 1, 2, 3, 4].map((i) => ({x: 1290 + i * 78, y: BODEN + 4}));
export const S4 = plane('s4', [
{id: 'n1', wer: 'NOA'},
{id: 'e1', wer: 'ERZ'},
{id: 'e2', wer: 'ERZ'},
{id: 'e3', wer: 'ERZ'},
{id: 'n2', wer: 'NOA', pause: 0.6},
{id: 'c1', wer: 'NOA', dauer: 1.1, pause: 0.5},
{id: 'c2', wer: 'NOA', dauer: 1.1, pause: 0.5},
{id: 'c3', wer: 'NOA', dauer: 1.1, pause: 0.5},
{id: 'c4', wer: 'NOA', dauer: 1.1, pause: 0.5},
{id: 'c5', wer: 'NOA', dauer: 1.4},
{id: 'bild1', dauer: 2.6},
{id: 'l1', wer: 'LULU'},
{id: 'bild2', dauer: 2.4},
{id: 'bild3', dauer: 2.6},
{id: 'l2', wer: 'LULU'},
{id: 'n3', wer: 'NOA'},
{id: 'bild4', dauer: 4.6},
{id: 'bild5', dauer: 2.2},
{id: 'e4', wer: 'ERZ'},
{id: 'e5', wer: 'ERZ'},
]);
export const Szene4: React.FC = () => {
const f = useCurrentFrame();
const {z, dauer} = S4;
// Abschnitte (Schnitte)
const zumStapel = z.e1.start; // Noa und Pino gehen zum Holzstapel
const amBaum = z.bild1.start; // Schnitt zu Lulu am Apfelbaum
const niesen = z.bild3.start; // Hatschi!
const zurueck = z.bild4.start; // Schnitt zum Nestbau
const zaehlen = [z.c1, z.c2, z.c3, z.c4, z.c5].map((c) => c.start);
// Noa
const noaGehtVon = zumStapel + 20;
const noaGehtBis = noaGehtVon + 60;
const noaX = kurve(f, [
[noaGehtVon, NOA_X + 140],
[noaGehtBis, NOA_ZAEHL_X],
]);
const noaGeht = f > noaGehtVon && f < noaGehtBis;
const noaStehPose: [number, string][] = [
[0, 'noa_A3_lachen'],
[noaGehtBis, 'noa_A6_blick-rechts'],
[z.c5.ende, 'noa_A3_lachen'],
[z.bild3.start + 4, 'noa_B2_erschrocken'],
[z.l2.ende, 'noa_A3_lachen'],
[z.e4.start, 'noa_A3_lachen'],
];
// Hocke: Abschnitte, in denen Noa hockt; Wechsel als weiche Überblendung der Standbilder
// (der Hinsetz-Clip ist anders gezeichnet), dabei kurz absinken bzw. sich strecken
const HOCKE_BLENDE = 14;
const hockZeiten: [number, number][] = [
[-1000, z.n1.start + 30],
[z.n2.start - 15, z.c5.ende],
[zurueck, z.e4.start],
];
const inHocke = hockZeiten.some(([a, b]) => f >= a && f < b);
const stehPose = (t: number) => noaStehPose.filter(([s]) => s <= t).pop()![1];
const noaPose: [number, string][] = [...noaStehPose, ...hockZeiten.flatMap(([a, b]): [number, string][] => [[a, 'noa_B_hocken'], [b, stehPose(b)]])].sort((p, q) => p[0] - q[0]);
const bump = (t: number) => (f >= t && f < t + HOCKE_BLENDE ? Math.sin(((f - t) / HOCKE_BLENDE) * Math.PI) : 0);
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: früher seitliches Hin- und Herrutschen (wirkte unruhig) – jetzt ruhig, nur die Blätter bewegen sich
// Pino
const pinoGehtVon = zumStapel + 40;
const pinoGehtBis = pinoGehtVon + 110;
const pinoX = kurve(f, [
[pinoGehtVon, PINO_X + 50],
[pinoGehtBis, PINO_ZAEHL_X],
[zurueck + 10, PINO_ZAEHL_X],
[zurueck + 60, NEST_X + 150],
[z.bild5.start - 20, NEST_X + 150],
[z.bild5.start, NEST_X],
]);
const insNest = huepfer(f, z.bild5.start - 20, 46, 20); // Sprung ins Nest
// Anfang wie Ende von Szene 3: Pino lehnt am großen Blatt und richtet sich auf
const sitzt = kurve(f, [
[z.n1.start + 10, 1],
[z.n1.start + 34, 0],
]);
const pinoGeht = (f > pinoGehtVon && f < pinoGehtBis) || (f > zurueck + 10 && f < zurueck + 60);
const pinoSpiegel = f > pinoGehtVon && f < pinoGehtBis; // nach rechts
const imNest = f >= z.bild5.start;
// Pino sitzt beim Zählen rechts von Noa: seine Bilder schauen nach rechts, darum gespiegelt (schaut zu ihr)
const pinoZuNoa = f >= pinoGehtBis && f <= zurueck + 10;
const pinoPose: [number, string][] = [
[0, 'pino_A1_augen-zu'],
[z.n1.start + 20, 'pino_A2_augen-halb'],
[pinoGehtBis, 'pino_A6_blick-rechts'],
...zaehlen.map((c) => [c, 'pino_A4_staunen'] as [number, string]),
[z.c5.ende, 'pino_A3_lachen'],
[niesen + 4, 'pino_B2_erschrocken'],
[z.n3.start + 20, 'pino_A3_lachen'],
[z.bild4.ende - 30, 'pino_A2_augen-halb'],
[z.bild5.start, 'pino_A6_blick-rechts'],
[z.e4.start, 'pino_A3_lachen'],
[z.e5.start + 60, 'pino_A2_augen-halb'],
];
const zaehlHuepfer = zaehlen.reduce((s, c) => s + huepfer(f, c + 4, 10, 10), 0);
const nestVoll = kurve(f, [
[zurueck + 20, 0.15],
[z.bild4.ende, 1],
]);
const pinoImNestY = BODEN + 2 - huepfer(f, z.bild5.start, -10, 20);
// Lulu am Baum
const strecken = kurve(f, [
[amBaum + 10, 0],
[amBaum + 50, 1],
]);
const kitzeln = z.bild2.start;
const zurueckLehnen = kurve(f, [
[kitzeln + 10, 0],
[niesen - 4, 1],
[niesen, -0.6],
[niesen + 20, 0],
]);
const luluPose: [number, string][] = [
[0, 'lulu_A5_blick-links'],
[kitzeln + 10, 'lulu_A1_augen-zu'],
[niesen + 18, 'lulu_B2_erschrocken'],
[z.n3.start + 20, 'lulu_A3_lachen'],
];
const luluY = BODEN - 10 - strecken * 26;
const luluStauch = atmen(f, 3, 0.006) * (1 + 0.02 * strecken);
const luluDreh = zurueckLehnen * 4;
// Punkt im Bild von Lulu, mit Drehung und Stauchung wie die Figur selbst
const luluPunkt = (c: {cx: number; cy: number}) => {
const p = punkt('lulu', 0, 0, c.cx, c.cy);
const dy = p.y * luluStauch;
const a = (luluDreh * Math.PI) / 180;
return {x: LULU_BAUM_X + p.x * Math.cos(a) - dy * Math.sin(a), y: luluY + p.x * Math.sin(a) + dy * Math.cos(a)};
};
// Maul wandert mit dem Posenwechsel A5 → A1 mit
const m5 = luluPunkt(LULU_MAUL_LINKS);
const m1 = luluPunkt(LULU_MAUL_A1);
const zuA1 = kurve(f, [
[kitzeln + 10, 0],
[kitzeln + 15, 1],
]);
const maul = {x: m5.x + (m1.x - m5.x) * zuA1, y: m5.y + (m1.y - m5.y) * zuA1};
// Blätter hängen am Ast, Lulu zupft eins nach dem anderen ab; das Bündel im Maul wächst mit
const astMaul = punkt('lulu', LULU_BAUM_X, BODEN - 36, LULU_MAUL_LINKS.cx, LULU_MAUL_LINKS.cy);
const zupfen = [0, 1, 2, 3, 4].map((i) => {
const ast = {x: astMaul.x - 150 + i * 55, y: astMaul.y - 115 - (i % 2) * 35};
const los = amBaum + 50 + i * 18;
const t = Math.min(1, Math.max(0, (f - los) / 20));
const e = t * t * (3 - 2 * t);
return {
x: ast.x + (maul.x - ast.x) * e,
y: ast.y + (maul.y - ast.y) * e - Math.sin(t * Math.PI) * 30,
r: t > 0 ? 160 + t * 380 : 160 + Math.sin(f * 0.12 + i * 2) * 12,
deck: Math.min(1, Math.max(0, (f - amBaum - i * 3) / 12)),
angekommen: t >= 1,
};
});
const imMaul = f < niesen ? zupfen.filter((b) => b.angekommen).length : 0;
// Kamera
const kam = kamFahrt(f, [
[0, 950, 790, 2.0],
[z.n1.ende, 1000, 760, 1.6],
[z.e1.start + 60, 1380, 720, 1.35],
[z.e3.ende, 1450, 780, 1.6],
[z.n2.start, 1480, 820, 1.85],
[z.c5.ende, 1480, 820, 1.85],
[amBaum, 470, 400, 1.85],
[z.l1.ende, 500, 380, 1.9],
[niesen - 5, 560, 370, 2.0],
[niesen + 12, 960, 540, 1.0],
[z.n3.ende, 960, 560, 1.05],
[zurueck, 1480, 800, 1.7],
[z.bild5.start, 1480, 830, 2.4],
[z.e5.start, 1400, 760, 1.5],
[dauer, 1350, 740, 1.4],
]);
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} />;
return (
<>
<Kamera {...kam}>
<Ort name="garten" />
{f >= amBaum && f < zurueck && lulu}
{f >= amBaum && f < niesen && zupfen.map((b, i) => (!b.angekommen ? <Blatt key={i} x={b.x} y={b.y} r={b.r} s={0.42} farbe={LAUB[i]} deckkraft={b.deck} /> : null))}
{LAUB.slice(0, imMaul).map((c, i) => (
<Blatt key={`m${i}`} x={maul.x - 4 + (i - 2) * 9} y={maul.y + 12 + Math.abs(i - 2) * 4} r={(i - 2) * 30 + 180} s={0.42} farbe={c} />
))}
{/* Reihe aus fünf Blättern vor dem Holzstapel */}
{f < zurueck + 40 &&
REIHE.map((p, i) =>
f > zaehlen[i] - 6 ? <Blatt key={i} x={p.x} y={p.y - Math.max(0, 20 - (f - zaehlen[i] + 6) * 2)} r={90 + i * 10} s={0.8} farbe={LAUB[i]} deckkraft={f > zurueck ? 1 - (f - zurueck) / 40 : 1} /> : null,
)}
<Nest x={NEST_X} y={BODEN} seite="hinten" voll={f < zurueck ? 0 : nestVoll} />
{noaGeht ? (
<Geher figur="noa" spiegeln x={noaX} y={BODEN} tempo={1.25} />
) : (
<Figur
pose={noaPose}
x={noaX}
y={BODEN}
stauchY={atmen(f, 2) * hockeStauch * (inHocke ? 1 - 0.03 * beugen : 1)}
drehung={inHocke ? beugen * 3 : 0}
blende={HOCKE_BLENDE}
/>
)}
{/* das große Blatt aus Szene 3 bleibt liegen */}
{f < zurueck + 40 && <Blatt x={GROSSES_BLATT_X} y={BODEN - 8} r={88} s={2.6} farbe="#c8642c" deckkraft={f > zurueck ? 1 - (f - zurueck) / 40 : 1} />}
{imNest ? (
<div style={{position: 'absolute', inset: 0, clipPath: `inset(0 0 ${1080 - BODEN - 4}px 0)`}}>
<Figur pose={pinoPose} x={NEST_X} y={pinoImNestY} schatten={false} />
</div>
) : pinoGeht ? (
<Geher figur="pino" spiegeln={pinoSpiegel} x={pinoX} y={BODEN} tempo={1.25} />
) : (
<Figur
pose={pinoPose}
spiegeln={pinoZuNoa}
x={pinoX}
y={BODEN - 6 * sitzt + zaehlHuepfer + huepfer(f, niesen + 2, 30, 12) + insNest}
stauchY={atmen(f, 1) * hopfStauch(f, niesen + 2) * hopfStauch(f, z.bild5.start - 20, 20)}
drehung={-9 * sitzt}
/>
)}
<Nest x={NEST_X} y={BODEN} seite="vorne" voll={f < zurueck ? 0 : nestVoll} />
{luluBeimNest}
<Blaetterregen start={niesen} x0={maul.x} y0={maul.y} boden={BODEN + 20} aus={zurueck + 20} />
</Kamera>
{/* Zahlen beim Zählen */}
{zaehlen.map((c, i) => (
<Ruf key={i} text={String(i + 1)} x={960} y={300} start={c} dauer={40} groesse={190} farbe={['#d9534f', '#e0a020', '#5f8a4a', '#3f5a8a', '#a0526b'][i]} />
))}
<Ruf text={W.hatschi} x={1060} y={230} start={niesen} dauer={45} groesse={170} />
<Vignette />
<Untertitel zeilen={S4.liste} />
<Blende dauer={dauer} rein={0} raus={0} />
</>
);
};