- 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
267 lines
10 KiB
TypeScript
267 lines
10 KiB
TypeScript
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} />
|
||
</>
|
||
);
|
||
};
|