import React from 'react'; import {AbsoluteFill, Audio, Sequence, interpolate, spring, staticFile, useCurrentFrame} from 'remotion'; import {INTRO_OPTIONEN, Intro, OUTRO, Outro} from '../../baukasten/buch'; import {ClipFigur, Figur, Kamera, type Posenfolge} from '../../baukasten/figur'; import {Blende, LULU_KOPF, Vignette, huepfer, kamFahrt, luluMaulC9, punkt} from '../../baukasten/helfer'; import {Apfel, Funkeln, Ruf} from '../../baukasten/natur'; import {BODEN, Ort} from '../../baukasten/orte'; import {SCHRIFT} from '../../baukasten/untertitel'; import {plane} from '../../baukasten/zeit'; import {Karaoke} from '../karaoke'; import {Noten} from '../lied03/Lied03'; import {DAUER, ECHO, LIED, T, ZAHLEN, ZEILEN} from './zeiten'; // Liedvideo „Eins, zwei – Pino ist dabei!“ (Pinos Liederbuch, Lied 1, Zählen bis 10) – ENTWURF. // Ton je Sprache: Suno-Fassungen auf −14 LUFS in public/musik/lieder/lied01-.mp3 (siehe zeiten.ts). // Sprache wie bei den Folgen: npx remotion render … Lied01 --props='{"sprache":"es"}'. // Bei jeder Zahl fliegt ein Apfel vom alten Apfelbaum in Pinos Korb; oben rechts stehen die Zahl // und die Finger (Daumen = 1, wie in Deutschland üblich; ab 6 ist die erste Hand voll). // Im Instrumental holt Lulu sich den obersten Apfel: „Jetzt sind es nur noch …?“ – „Neun!“ // Gesprochenes (ERZ-Ansage, Lulus Frage, Noas Frage, „Mampf!“) fehlt noch; im Entwurf nur als Schrift. const FPS = 30; const s = (sek: number) => Math.round(sek * FPS); const SCHLAG = (FPS * 60) / 102; // gemessen am Refrain: ein Takt ≈ 2,35 s const ANFANG = s(T.c1); const PINO_X = 600; const KORB_X = 790; const RAND_Y = BODEN - 118; // Oberkante des Korbs const NOA_X = 1430; // Lulu senkt den Hals (Clip lulu_C9_hals-runter) bis Clipbild 75: dann ist das Maul // 302 px links von ihren Füßen genau auf Höhe des obersten Apfels im Korb. const C9_TEMPO = 1.25; const C9_START = 17; const C9_TIEF = 75; const c9 = (bilder: number) => (bilder * FPS) / 24 / C9_TEMPO; const LULU_X = KORB_X + 302; const LULU_Y = BODEN - 6; // Plätze der zehn Äpfel im Korb (relativ zur Korbmitte am Rand): unten 5, darüber 4, oben 1 const PLAETZE: [number, number][] = [ [-76, -10], [-38, -13], [0, -14], [38, -13], [76, -10], [-57, -42], [-19, -45], [19, -45], [57, -42], [0, -76], ]; // Wo die Äpfel vorher im alten Apfelbaum hängen const AM_BAUM: [number, number][] = [ [470, 300], [300, 340], [610, 390], [180, 420], [420, 430], [540, 480], [250, 500], [680, 520], [360, 545], [130, 560], ]; const FLUG = 20; // Frames vom Baum in den Korb // Zeitplan des Gags const G = s(T.gag); const RUNTER = G + 24; const UNTEN = RUNTER + c9(C9_TIEF - C9_START); const HOCH = UNTEN + 20; const OBEN = HOCH + c9(C9_TIEF); const MAMPF = OBEN + 8; const imAbschnitt = (f: number, von: number, bis: number) => f >= s(von) && f < s(bis); const kamera = (f: number) => { const P: [number, number, number, number][] = [ [T.c1, 960, 650, 1.08], [T.v1 - 0.6, 830, 640, 1.2], // Baum, Korb, Pino [T.c2, 960, 650, 1.08], [T.v2 - 0.6, 830, 640, 1.2], [T.echo, 900, 690, 1.28], // Äpfel im Korb leuchten [T.gag, 960, 620, 1.1], // Lulu ganz im Bild [T.frage, 900, 670, 1.22], [T.c3, 960, 650, 1.08], [T.ende, 960, 630, 1.04], ]; const p: [number, number, number, number][] = []; P.forEach(([t, x, y, z], i) => { if (i > 0) p.push([s(t) - 21, ...P[i - 1].slice(1)] as [number, number, number, number]); p.push([s(t), x, y, z]); }); return kamFahrt(f, p); }; // Noa klatscht im Refrain auf jeden zweiten Schlag const klatschen = (von: number, bis: number): Posenfolge => { const folge: Posenfolge = []; for (let t = ANFANG; t < s(bis); t += 2 * SCHLAG) { if (t < s(von)) continue; const k = Math.round(t); folge.push([k - 3, 'noa_A_klatschen'], [k + 4, 'noa_A3_lachen']); } return folge; }; const zeigen = (zeiten: number[]): Posenfolge => zeiten.flatMap((t) => [[s(t) - 2, 'noa_B_zeigen'], [s(t) + 12, 'noa_A3_lachen']] as Posenfolge); const NOA: Posenfolge = [ [0, 'noa_A3_lachen'], ...klatschen(T.c1, T.c1 + 9.2), ...zeigen(ZAHLEN.slice(0, 5)), ...klatschen(T.c2, T.c2 + 9.2), ...zeigen(ZAHLEN.slice(5)), [s(T.echo) - 4, 'noa_B_zeigen'], [s(T.echoEnde + 0.4), 'noa_A3_lachen'], [UNTEN, 'noa_A5_blick-links'], [MAMPF, 'noa_A4_staunen'], [s(T.frage), 'noa_B_zeigen'], [s(T.antwort), 'noa_A_arme-hoch'], [s(T.antwort + 1.4), 'noa_A3_lachen'], ...klatschen(T.c3, T.c3 + 9.2), [s(T.ende), 'noa_A_arm-hoch-winken'], ]; const PINO: Posenfolge = [ [0, 'pino_A3_lachen'], [UNTEN - 10, 'pino_A4_staunen'], [MAMPF + 20, 'pino_A3_lachen'], [s(T.antwort), 'pino_A_hinterbeine'], [s(T.antwort + 1.6), 'pino_A3_lachen'], ]; const LULU: Posenfolge = [ [0, 'lulu_A3_lachen'], [G, 'lulu_A5_blick-links'], // „Darf ich einen Apfel?“ [OBEN, 'lulu_A_apfel'], [MAMPF + 14, 'lulu_A1_augen-zu'], [MAMPF + 24, 'lulu_A3_lachen'], ]; // Korb: Rückwand (vor den Äpfeln) und Vorderseite (danach) const KorbHinten: React.FC = () => ( ); const KorbVorne: React.FC = () => ( {[20, 46, 72, 98].map((y) => ( ))} {[-80, -48, -16, 16, 48, 80].map((x) => ( ))} ); // Ein Apfel des Liedes: hängt im Baum, fliegt bei seiner Zahl in den Korb, leuchtet beim Zählen const Liedapfel: React.FC<{i: number; f: number}> = ({i, f}) => { // Apfel 10 holt sich Lulu: vom Korb ins Maul, mit dem Hals wieder hoch, dann ist er im Bild lulu_A_apfel if (i === 9 && f >= UNTEN && f < OBEN) { const bild = f < HOCH ? C9_TIEF : C9_TIEF - ((f - HOCH) * C9_TEMPO * 24) / FPS; const m = luluMaulC9(bild); const maul = punkt('lulu', LULU_X, LULU_Y, m.cx, m.cy); const p = interpolate(f, [UNTEN, UNTEN + 6], [0, 1], {extrapolateRight: 'clamp'}); const x = interpolate(p, [0, 1], [KORB_X + PLAETZE[9][0], maul.x]); const y = interpolate(p, [0, 1], [RAND_Y + PLAETZE[9][1], maul.y + 6]); return ; } if (i === 9 && f >= OBEN) return null; const start = s(ZAHLEN[i]) - 6; const [bx, by] = AM_BAUM[i]; const zx = KORB_X + PLAETZE[i][0]; const zy = RAND_Y + PLAETZE[i][1]; if (f < start) return ; const p = Math.min(1, (f - start) / FLUG); const x = interpolate(p, [0, 1], [bx, zx]); const y = interpolate(p, [0, 1], [by, zy]) - Math.sin(p * Math.PI) * 160; // Beim Zählen nach Strophe 2 leuchtet der Apfel kurz auf const leuchten = interpolate(f, [s(ECHO[i]), s(ECHO[i]) + 4, s(ECHO[i]) + 18], [0, 1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); const landen = p >= 1 ? 1 + 0.12 * Math.sin(Math.min(1, (f - start - FLUG) / 8) * Math.PI) : 1; return ( <> {leuchten > 0 && (
)} ); }; // Hand mit hochgestreckten Fingern (Daumen zählt als 1) const HAUT = '#f4cfa6'; const KONTUR = '#b9825a'; const FINGER: {x: number; b: number; l: number; dreh: number}[] = [ {x: 22, b: 24, l: 62, dreh: -48}, // Daumen {x: 48, b: 22, l: 84, dreh: -6}, {x: 74, b: 22, l: 92, dreh: 0}, {x: 100, b: 22, l: 84, dreh: 5}, {x: 124, b: 20, l: 66, dreh: 12}, ]; const Hand: React.FC<{finger: number; neu: number; spiegeln?: boolean}> = ({finger, neu, spiegeln = false}) => ( {FINGER.map((fi, k) => { const hoch = k < finger; // der zuletzt dazugekommene Finger schnellt hoch const l = hoch ? (k === finger - 1 ? 18 + (fi.l - 18) * neu : fi.l) : 18; const fussY = k === 0 ? 150 : 112; return ( ); })} {/* eingeklappte Finger als kleine Knöchel */} {FINGER.slice(1).map((fi, k) => (k + 1 >= finger ? : null))} ); // Zahl und Hände oben rechts type Anzeige = {n: number | '?'; seit: number; deck: number}; const anzeige = (f: number): Anzeige | null => { const zaehle = (zeiten: number[], von: number, bis: number, basis = 0) => { if (!imAbschnitt(f, von, bis)) return null; const k = zeiten.filter((t) => f >= s(t)).length; const seit = k > 0 ? f - s(zeiten[k - 1]) : 0; const deck = Math.min(interpolate(f, [s(von), s(von) + 8], [0, 1], {extrapolateRight: 'clamp'}), interpolate(f, [s(bis) - 10, s(bis)], [1, 0], {extrapolateLeft: 'clamp'})); return {n: basis + k, seit, deck}; }; return ( zaehle(ZAHLEN.slice(0, 5), ZAHLEN[0] - 0.2, T.c2 - 0.3) ?? zaehle(ZAHLEN.slice(5), ZAHLEN[5] - 0.2, T.echo - 0.1, 5) ?? zaehle(ECHO, ECHO[0] - 0.1, T.echoEnde + 1) ?? (imAbschnitt(f, T.frage, T.c3 - 0.5) ? { n: f >= s(T.antwort) ? 9 : '?', seit: f >= s(T.antwort) ? f - s(T.antwort) : f - s(T.frage), deck: Math.min(interpolate(f, [s(T.frage), s(T.frage) + 8], [0, 1], {extrapolateRight: 'clamp'}), interpolate(f, [s(T.c3 - 0.5) - 10, s(T.c3 - 0.5)], [1, 0], {extrapolateLeft: 'clamp'})), } : null) ); }; const Zahltafel: React.FC = () => { const f = useCurrentFrame(); const a = anzeige(f); if (!a || a.n === 0) return null; const pop = spring({frame: a.seit, fps: FPS, config: {damping: 9, stiffness: 160}}); const neu = interpolate(a.seit, [0, 8], [0, 1], {extrapolateRight: 'clamp'}); let inhalt: React.ReactNode; if (a.n === '?') { // drei Punkte zählen herunter bis zur Antwort const frei = s(T.antwort) - s(T.frage + 1.2); const punkte = f < s(T.frage + 1.2) ? 3 : Math.max(1, 3 - Math.floor(((f - s(T.frage + 1.2)) / frei) * 3)); inhalt =
{f < s(T.frage + 1.2) ? '?' : '•'.repeat(punkte)}
; } else { const n = a.n; inhalt = ( <>
{n}
{n > 5 && }
); } return (
{inhalt}
); }; const Garten: React.FC = () => { const f = useCurrentFrame(); const kam = kamera(f); const ph = (f - ANFANG) / SCHLAG; const schlag = Math.abs(Math.sin(ph * Math.PI)); const wipp = (phase: number, h: number) => -Math.abs(Math.sin(ph * Math.PI + phase)) * h; const schwank = (phase: number, g: number) => Math.sin((ph * Math.PI) / 2 + phase) * g; const ruhig = f >= s(T.c3) ? 0.6 : 1; const singt = f >= ANFANG && f < s(T.ende + 1); const pinoHuepf = [...ZAHLEN, ...ECHO].reduce((a, t) => a + huepfer(f, s(t) + 14, 24, 11), 0) + huepfer(f, s(T.antwort), 40, 14); const luluImClip = f >= RUNTER && f < OBEN; const abend = interpolate(f, [s(T.c3 - 1), s(T.c3 + 2)], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); const chor = [T.c1, T.c2, T.c3].some((c) => imAbschnitt(f, c, c + 9.6)); const kopf = punkt('lulu', LULU_X, LULU_Y, LULU_KOPF.cx, LULU_KOPF.cy); return ( <>
{luluImClip ? ( f < HOCH ? ( ) : ( ) ) : ( )} {new Array(10).fill(0).map((_, i) => ( ))}
{chor && } {/* Liedtitel im ersten Refrain */}
♫ {LIED.titel} ♫
{/* Lulus Frage im Instrumental, solange die Aufnahme fehlt */}
{LIED.bitte}
); }; // Intro (Buch geht auf) bis kurz vor das erste gesungene Wort const INTRO_DAUER = s(T.c1 - 0.2); const INTRO = plane([{id: 'buch', dauer: 1.5}, {id: 't1', dauer: 1.2}, {id: 't2', dauer: 0.5}], {...INTRO_OPTIONEN, vorlauf: 0.2, nachlauf: 0.4}); const OUTRO_START = DAUER - OUTRO.dauer; export const Lied01: React.FC = () => ( ); export const LIED01_DAUER = DAUER;