youtube/remotion/src/folge02/szene5.tsx
Lara 60acaa9921 Sprechtexte nach Sprachen getrennt (DE/ES/FR), Folge 2 in allen drei Sprachen renderbar
- Texte stehen nur noch in remotion/src/folge02/texte/<sprache>.json,
  die Szenen enthalten nur Ablauf und Bewegung
- Sprache beim Rendern wählbar (render.mjs --sprache es, Studio über
  --props); Zeitplan richtet sich nach Textlänge bzw. Aufnahmen der Sprache
- Baukasten-Wörter je Sprache in baukasten/sprache.ts: Kanalname im Buch
  (mit festem Umbruch für ES/FR), „Heute: …“, Sprechernamen, Kalendertitel,
  „Hatschi!“; Kalenderbild für ES/FR vorberechnet
- Stimmen je Sprache: stimmen/<sprache>.json, public/stimmen/folge02/<sprache>/,
  stimmen.mts --sprache, eigene Stimmen je Sprache möglich
- neues werkzeuge/texte.mts: prüft fehlende Übersetzungen, --tabelle zum Gegenlesen
- README, CLAUDE.md und Drehbuch-Hinweise angepasst

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 10:25:52 +01:00

163 lines
5.3 KiB
TypeScript

import React from 'react';
import {useCurrentFrame} from 'remotion';
import {kurve} from '../bewegung';
import {ClipFigur, Figur, Kamera} from '../baukasten/figur';
import {Blende, Vignette, atmen, hopfStauch, huepfer, kamFahrt, ruck} from '../baukasten/helfer';
import {Funkeln, Nest} from '../baukasten/natur';
import {BODEN, NEST_X, Ort} from '../baukasten/orte';
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
import {plane} from './plan';
// Szene 5: Pino will nicht schlafen. Noa hat eine Idee.
const NOA_X5 = 1150;
const PINO_RAUS_X = 1370;
const LULU_X5 = 1790;
export const S5 = plane('s5', [
{id: 'bild1', dauer: 3.0},
{id: 'bild2', dauer: 2.2},
{id: 'p1', wer: 'PINO'},
{id: 'l1', wer: 'LULU'},
{id: 'bild3', dauer: 3.0},
{id: 'e1', wer: 'ERZ'},
{id: 'e2', wer: 'ERZ', pause: 3},
{id: 'bild4', dauer: 1.2},
{id: 'n1', wer: 'NOA'},
]);
const Denkblase: React.FC<{x: number; y: number; start: number; ende: number}> = ({x, y, start, ende}) => {
const f = useCurrentFrame();
if (f < start || f > ende) return null;
const auf = Math.min(1, (f - start) / 12) * Math.min(1, (ende - f) / 8);
const punkte = Math.floor((f - start) / 14) % 4;
return (
<div style={{position: 'absolute', left: x, top: y, opacity: auf, transform: `scale(${0.6 + 0.4 * auf})`}}>
<div style={{position: 'absolute', left: -14, top: 70, width: 16, height: 16, borderRadius: '50%', background: '#fff8ea'}} />
<div style={{position: 'absolute', left: 4, top: 40, width: 26, height: 26, borderRadius: '50%', background: '#fff8ea'}} />
<div
style={{
position: 'absolute',
left: 20,
top: -60,
width: 140,
height: 90,
borderRadius: '50%',
background: '#fff8ea',
boxShadow: '0 4px 14px rgba(60,40,20,0.2)',
fontFamily: SCHRIFT,
fontSize: 60,
fontWeight: 600,
color: '#8a6a4a',
textAlign: 'center',
lineHeight: '70px',
}}
>
{'.'.repeat(punkte)}
</div>
</div>
);
};
export const Szene5: React.FC = () => {
const f = useCurrentFrame();
const {z, dauer} = S5;
const aufwachen = z.bild2.start;
const raus = aufwachen + 22;
const imNest = f < raus;
const stampfen = [z.p1.start + 2, z.p1.start + 40, z.p1.start + 78];
const pinoNestPose: [number, string][] = [
[0, 'pino_A2_augen-halb'],
[20, 'pino_A4_staunen'],
[60, 'pino_A2_augen-halb'],
[z.bild1.ende - 25, 'pino_A1_augen-zu'],
[aufwachen, 'pino_A4_staunen'],
];
const pinoPose: [number, string][] = [
[0, 'pino_A_hinterbeine'],
[z.l1.start + 10, 'pino_A5_blick-links'],
[z.bild3.start + 10, 'pino_B_blick-nach-oben'],
[z.bild4.start + 10, 'pino_A4_staunen'],
];
const pinoNestY = kurve(f, [
[0, BODEN + 12],
[25, BODEN + 2],
[z.bild1.ende - 20, BODEN + 10],
[aufwachen, BODEN + 6],
[aufwachen + 6, BODEN - 6],
]);
const pinoX = kurve(f, [
[raus, NEST_X - 40],
[raus + 20, PINO_RAUS_X],
]);
const stampfY = stampfen.reduce((s, t) => s + huepfer(f, t, 22, 9), 0) + huepfer(f, raus, 40, 16);
const stampfS = stampfen.reduce((s, t) => s * hopfStauch(f, t, 9), 1) * hopfStauch(f, raus, 16);
const lulu =
f < z.bild3.start + 10 ? (
<Figur
pose={[
[0, 'lulu_A6_blick-rechts'],
[z.bild2.start + 10, 'lulu_A5_blick-links'],
[z.l1.start, 'lulu_A4_staunen'],
]}
x={LULU_X5}
y={BODEN - 10}
stauchY={atmen(f, 3, 0.006)}
/>
) : (
<ClipFigur name="lulu_C9_hals-runter" start={z.bild3.start + 10} tempo={1.4} bis={3.6} x={LULU_X5} y={BODEN - 10} />
);
const ideeStart = z.bild4.start;
const noaPose: [number, string][] = [
[0, 'noa_A6_blick-rechts'],
[aufwachen + 10, 'noa_A4_staunen'],
[z.l1.start, 'noa_A6_blick-rechts'],
[z.bild3.start, 'noa_A5_blick-links'],
[z.bild3.start + 40, 'noa_A6_blick-rechts'],
[z.e1.start + 30, 'noa_A5_blick-links'],
[z.e2.start, 'noa_A6_blick-rechts'],
[ideeStart, 'noa_A_arm-hoch-winken'],
[z.n1.start + 4, 'noa_A3_lachen'],
];
const noaKopfNeigen = f > z.bild3.start && f < ideeStart ? Math.sin((f - z.bild3.start) * 0.05) * 2.5 : 0;
const kam = kamFahrt(f, [
[0, NEST_X, 860, 2.6],
[aufwachen, NEST_X, 860, 2.6],
[raus + 20, 1380, 820, 2.0],
[z.p1.ende, 1380, 800, 1.9],
[z.l1.start + 10, 1300, 620, 1.3],
[z.bild3.start + 40, 1300, 660, 1.35],
[z.e2.ende, 1280, 760, 1.6],
[ideeStart + 10, NOA_X5 + 40, 760, 1.9],
[dauer, NOA_X5 + 40, 760, 2.0],
]);
kam.x += stampfen.reduce((s, t) => s + ruck(f, t, 3, 10), 0);
return (
<>
<Kamera {...kam}>
<Ort name="garten" />
<Figur pose={noaPose} x={NOA_X5} y={BODEN + huepfer(f, ideeStart, 26, 14)} stauchY={atmen(f, 2) * hopfStauch(f, ideeStart, 14)} drehung={noaKopfNeigen} />
<Nest x={NEST_X} y={BODEN} seite="hinten" />
{imNest && (
<div style={{position: 'absolute', inset: 0, clipPath: `inset(0 0 ${1080 - BODEN - 4}px 0)`}}>
<Figur pose={pinoNestPose} x={NEST_X} y={pinoNestY} schatten={false} stauchY={atmen(f, 1, 0.012)} />
</div>
)}
<Nest x={NEST_X} y={BODEN} seite="vorne" />
{!imNest && <Figur pose={pinoPose} x={pinoX} y={BODEN + stampfY} stauchY={stampfS * atmen(f, 1)} />}
{lulu}
<Denkblase x={NOA_X5 + 70} y={BODEN - 420} start={z.bild3.start + 5} ende={ideeStart} />
<Funkeln x={NOA_X5 + 20} y={BODEN - 330} start={ideeStart + 2} dauer={36} s={1.1} />
</Kamera>
<Vignette />
<Untertitel zeilen={S5.liste} />
<Blende dauer={dauer} rein={0} raus={12} />
</>
);
};