- gemeinsame Bausteine nach src/baukasten/, Bibliothek nach public/bibliothek/, Orte nach public/orte/ - je Szene eine Komposition (F02-01-…), werkzeuge/render.mjs rendert nur geänderte Szenen und klebt sie zusammen; --entwurf mit halber Auflösung und eingeblendeter Zeit/Zeile - werkzeuge/stimmen.mts erzeugt ElevenLabs-Aufnahmen, Zeitplan übernimmt ihre Länge - Folge 2: Geh- und Hinsetz-Clips statt Standbild-Wechsel, neue Vögel, fallendes Blatt, Noa malt den Kalender (Blick von oben) - ANLEITUNG_PC.md für Studio und Rendern unter Windows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
163 lines
6.1 KiB
TypeScript
163 lines
6.1 KiB
TypeScript
import React from 'react';
|
|
import {spring, useCurrentFrame, useVideoConfig} from 'remotion';
|
|
import {kurve} from '../bewegung';
|
|
import {Figur, Kamera} from './figur';
|
|
import {Blende, Vignette, kamFahrt} from './helfer';
|
|
import {Blatt} from './natur';
|
|
import {SCHRIFT, Untertitel} from './untertitel';
|
|
import {plane, type Plan, type Zeile} from './zeit';
|
|
|
|
// Das Bilderbuch: öffnet sich am Anfang (Intro) und klappt am Ende zu.
|
|
// Buchrücken bei x = 960; geschlossen liegt der Deckel rechts davon.
|
|
|
|
const OBEN = 190;
|
|
const HOEHE = 700;
|
|
const BREITE = 700;
|
|
|
|
const Seite: React.FC<{links: number; children?: React.ReactNode}> = ({links, children}) => (
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
left: links,
|
|
top: OBEN,
|
|
width: BREITE,
|
|
height: HOEHE,
|
|
background: 'linear-gradient(90deg, #efe3c9 0%, #fbf4e4 12%, #fbf4e4 88%, #f1e6cd 100%)',
|
|
boxShadow: '0 10px 40px rgba(60,40,20,0.25)',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
const Titel: React.FC<{text: string; groesse: number; farbe?: string; y: number}> = ({text, groesse, farbe = '#c8642c', y}) => (
|
|
<div style={{position: 'absolute', left: 0, right: 0, top: y, textAlign: 'center', fontFamily: SCHRIFT, fontWeight: 600, fontSize: groesse, color: farbe}}>{text}</div>
|
|
);
|
|
|
|
// Deckel: dreht sich um den Buchrücken (0° = zu, -180° = offen)
|
|
const Deckel: React.FC<{winkel: number}> = ({winkel}) => (
|
|
<div style={{position: 'absolute', left: 960, top: OBEN - 14, width: BREITE + 16, height: HOEHE + 28, transformOrigin: '0 50%', transform: `perspective(2600px) rotateY(${winkel}deg)`, transformStyle: 'preserve-3d'}}>
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
backfaceVisibility: 'hidden',
|
|
borderRadius: '6px 18px 18px 6px',
|
|
background: 'radial-gradient(ellipse at 40% 30%, #74a05c, #4f7a3e 70%, #3f6532)',
|
|
boxShadow: 'inset 14px 0 18px rgba(0,0,0,0.25), 0 14px 40px rgba(40,30,20,0.35)',
|
|
}}
|
|
>
|
|
<div style={{position: 'absolute', left: 40, right: 40, top: 40, bottom: 40, border: '6px solid #e9c97a', borderRadius: 14}} />
|
|
<Titel text="Pino, Noa & Lulu" groesse={84} farbe="#f6dc95" y={120} />
|
|
<div style={{position: 'absolute', left: 0, right: 0, top: 260, height: 360}}>
|
|
<Figur pose="pino_A3_lachen" x={250} y={330} hoehe={120} schatten={false} />
|
|
<Figur pose="noa_A3_lachen" x={370} y={330} hoehe={250} schatten={false} />
|
|
<Figur pose="lulu_A3_lachen" x={520} y={330} hoehe={330} schatten={false} />
|
|
</div>
|
|
</div>
|
|
<div style={{position: 'absolute', inset: 0, backfaceVisibility: 'hidden', transform: 'rotateY(180deg)', background: '#f1e6cd', borderRadius: '18px 6px 6px 18px'}}>
|
|
<div style={{position: 'absolute', inset: 14, background: 'linear-gradient(270deg, #efe3c9 0%, #fbf4e4 12%)', overflow: 'hidden'}}>
|
|
{[0, 1, 2, 3, 4, 5].map((i) => (
|
|
<Blatt key={i} x={120 + i * 95} y={600 + Math.sin(i * 2.1) * 40} r={i * 47} s={0.9} farbe={['#c8642c', '#d99a2b', '#a8452a'][i % 3]} />
|
|
))}
|
|
<Titel text="Pino, Noa & Lulu" groesse={70} y={230} />
|
|
<Titel text="Eine Geschichte aus Noas Garten" groesse={34} farbe="#8a6a4a" y={340} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const Innenseiten: React.FC<{frame: number; auf: number; titel: string; posen: [string, string, string]}> = ({frame, auf, titel, posen}) => {
|
|
const {fps} = useVideoConfig();
|
|
const pop = (i: number) => spring({frame: frame - auf - 8 - i * 6, fps, config: {damping: 11, stiffness: 120}});
|
|
return (
|
|
<>
|
|
<Seite links={960}>
|
|
<Titel text={titel} groesse={68} y={60} farbe="#3f5a8a" />
|
|
{posen.map((p, i) => {
|
|
const s = pop(i);
|
|
const x = [180, 340, 520][i];
|
|
const h = [96, 230, 440][i];
|
|
return <Figur key={p} pose={p} x={x} y={650} hoehe={h} stauchY={s} stauchX={0.6 + 0.4 * s} deckkraft={Math.min(1, s * 2)} />;
|
|
})}
|
|
</Seite>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const Tisch: React.FC = () => (
|
|
<div style={{position: 'absolute', inset: -400, background: 'radial-gradient(ellipse at 50% 45%, #e9cfa4 0%, #c99a63 60%, #a87a4a 100%)'}} />
|
|
);
|
|
|
|
// Intro: Buch geht auf, Figuren springen auf die Seite, Kamera fährt in die Seite.
|
|
// Die Folge plant die Zeilen selbst (mit `introZeilen`), damit Aufnahmen greifen.
|
|
export const introZeilen = (folgenTitel: string): Zeile[] => [
|
|
{id: 'buch', dauer: 2.2},
|
|
{id: 't1', wer: 'ERZ', text: 'Pino, Noa und Lulu.'},
|
|
{id: 't2', wer: 'ERZ', text: `Heute: ${folgenTitel}.`},
|
|
];
|
|
export const INTRO_OPTIONEN = {vorlauf: 0.3, nachlauf: 1.6};
|
|
|
|
type IntroProps = {plan: Plan; titel: string; posen?: [string, string, string]};
|
|
|
|
export const Intro: React.FC<IntroProps> = ({plan, titel, posen = ['pino_A3_lachen', 'noa_A3_lachen', 'lulu_A3_lachen']}) => {
|
|
const frame = useCurrentFrame();
|
|
const {z, dauer} = plan;
|
|
const auf = 30;
|
|
const winkel = kurve(frame, [
|
|
[auf, 0],
|
|
[auf + 40, -180],
|
|
]);
|
|
const kam = kamFahrt(frame, [
|
|
[0, 1310, 540, 1.25],
|
|
[auf, 1310, 540, 1.25],
|
|
[auf + 45, 960, 540, 1.05],
|
|
[z.t2.ende, 960, 540, 1.08],
|
|
[dauer, 1310, 520, 2.6],
|
|
]);
|
|
return (
|
|
<>
|
|
<Kamera {...kam} frei>
|
|
<Tisch />
|
|
{winkel < -90 && <Innenseiten frame={frame} auf={auf + 30} titel={titel} posen={posen} />}
|
|
{winkel >= -90 && <Seite links={960} />}
|
|
<Deckel winkel={winkel} />
|
|
</Kamera>
|
|
<Vignette />
|
|
<Untertitel zeilen={plan.liste} />
|
|
<Blende dauer={dauer} rein={12} raus={14} />
|
|
</>
|
|
);
|
|
};
|
|
|
|
// Outro: Seite mit winkenden Figuren, Buch klappt zu.
|
|
export const OUTRO = plane([{id: 'buch', dauer: 4.5}], {vorlauf: 0.2, nachlauf: 0.6});
|
|
|
|
export const Outro: React.FC<{posen?: [string, string, string]}> = ({posen = ['pino_B3_schlafen', 'noa_A_arm-hoch-winken', 'lulu_A3_lachen']}) => {
|
|
const frame = useCurrentFrame();
|
|
const {dauer} = OUTRO;
|
|
const zu = 60;
|
|
const winkel = kurve(frame, [
|
|
[zu, -180],
|
|
[zu + 40, 0],
|
|
]);
|
|
const kam = kamFahrt(frame, [
|
|
[0, 1310, 520, 2.4],
|
|
[25, 960, 540, 1.05],
|
|
[zu + 20, 1150, 540, 1.1],
|
|
[dauer, 1310, 540, 1.2],
|
|
]);
|
|
return (
|
|
<>
|
|
<Kamera {...kam} frei>
|
|
<Tisch />
|
|
{winkel < -90 && <Innenseiten frame={frame} auf={-60} titel="Bis bald!" posen={posen} />}
|
|
{winkel >= -90 && <Seite links={960} />}
|
|
<Deckel winkel={winkel} />
|
|
</Kamera>
|
|
<Vignette />
|
|
<Blende dauer={dauer} rein={10} raus={20} farbe="#1c1a24" />
|
|
</>
|
|
);
|
|
};
|