Folge 2 „Pino ist müde“ komplett in Remotion (ohne Ton, ohne Higgsfield)
Szenen aus der Figuren-Bibliothek: freigestellte Standbilder mit Posenwechsel, freigestellte Clips (WebM mit Alpha in /shared), Orte als vorberechnete Aquarell-SVGs, Untertitel statt Stimmen. Zeitplan aus den Sprechzeilen, damit ElevenLabs-Aufnahmen später nur die Dauer setzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1
remotion/.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
out/
|
out/
|
||||||
|
public/folge02/clips
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,44 @@ python3 werkzeuge/rig.py # braucht numpy, scipy, pillow
|
||||||
und `uebersicht.png` prüfen. In einer Szene: `gelenk('lulu', 'hals', grad)`
|
und `uebersicht.png` prüfen. In einer Szene: `gelenk('lulu', 'hals', grad)`
|
||||||
(siehe `src/szenen/VerformTest.tsx`).
|
(siehe `src/szenen/VerformTest.tsx`).
|
||||||
|
|
||||||
|
## Folge 2 „Pino ist müde“ (`Folge02`)
|
||||||
|
Komplette Folge (ca. 5:20) nur mit Code, Figuren-Bibliothek und ohne Ton:
|
||||||
|
Untertitel ersetzen vorerst die Stimmen, Mitmach-Pausen zeigen ein „?“.
|
||||||
|
|
||||||
|
| Pfad | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/folge02/Folge02.tsx` | Reihenfolge der Szenen |
|
||||||
|
| `src/folge02/szene2.tsx` … `szene7.tsx`, `lied.tsx`, `buch.tsx` | die Szenen; jede hat oben ihre Sprechzeilen (`plane([...])`) |
|
||||||
|
| `src/folge02/zeit.ts` | Zeitplan aus den Zeilen; Dauer wird aus der Textlänge geschätzt |
|
||||||
|
| `src/folge02/figur.tsx` | `Figur` (Standbilder mit Posenwechsel), `ClipFigur` (freigestellte Clips), `Kamera` |
|
||||||
|
| `src/folge02/orte.tsx` | Orte als SVG; vorberechnet nach `public/folge02/orte/` |
|
||||||
|
| `src/folge02/natur.tsx` | Blätter, Nest, Vögel, Schnee, Zzz, Funkeln, große Rufe |
|
||||||
|
| `src/folge02/posen.ts` | automatisch erzeugt von `werkzeuge/posen.py` |
|
||||||
|
| `public/folge02/figuren/` | freigestellte Standbilder (WebP, im Git) |
|
||||||
|
| `public/folge02/clips` | Link auf `/shared/youtube/folge02/clips_frei/` (WebM mit Alpha, nicht im Git) |
|
||||||
|
|
||||||
|
Orte neu vorberechnen (nach Änderungen in `orte.tsx`):
|
||||||
|
```bash
|
||||||
|
npx remotion still src/index.ts F2Ort-garten public/folge02/orte/garten.webp --image-format=webp
|
||||||
|
```
|
||||||
|
|
||||||
|
Rendern:
|
||||||
|
```bash
|
||||||
|
npx remotion render src/index.ts Folge02 /shared/youtube/folge02/folge02-vorschau.mp4 --gl=swangle
|
||||||
|
```
|
||||||
|
Auf einem anderen Rechner fehlen die Clips: den Ordner
|
||||||
|
`/shared/youtube/folge02/clips_frei/` nach `public/folge02/clips/` kopieren.
|
||||||
|
Fehlt ein Clip, zeigt `ClipFigur` ein Standbild der Figur.
|
||||||
|
|
||||||
|
**Ton nachrüsten (ElevenLabs):** Je Zeile die Audiodatei erzeugen, ihre
|
||||||
|
Länge als `dauer` in die Zeile eintragen und die Datei als `<Audio>` am
|
||||||
|
`start` der Zeile einbinden. Alle Bewegungen hängen an den Zeilen und
|
||||||
|
verschieben sich mit.
|
||||||
|
|
||||||
|
**Neue Standbilder/Clips:** Standbilder mit `rembg` freistellen, ohne
|
||||||
|
Zuschneiden (gleiche Leinwand), dann
|
||||||
|
`python3 werkzeuge/posen.py <ordner_freigestellt> /shared/youtube/folge02/clips_frei`.
|
||||||
|
|
||||||
## Bekannte Grenzen
|
## Bekannte Grenzen
|
||||||
- Die Figuren haben nur die Pose ihres Bildes. Neue Posen brauchen ein neues
|
- Die Figuren haben nur die Pose ihres Bildes. Neue Posen brauchen ein neues
|
||||||
Bild (z. B. aus Gemini).
|
Bild (z. B. aus Gemini).
|
||||||
|
|
|
||||||
10
remotion/package-lock.json
generated
|
|
@ -7,6 +7,7 @@
|
||||||
"name": "pino-remotion",
|
"name": "pino-remotion",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remotion/cli": "^4.0.300",
|
"@remotion/cli": "^4.0.300",
|
||||||
|
"@remotion/google-fonts": "^4.0.300",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"remotion": "^4.0.300",
|
"remotion": "^4.0.300",
|
||||||
|
|
@ -672,6 +673,15 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@remotion/google-fonts": {
|
||||||
|
"version": "4.0.300",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remotion/google-fonts/-/google-fonts-4.0.300.tgz",
|
||||||
|
"integrity": "sha512-lH2TpMyPrgfxnwtLZLbGZ3QyCNPrIySC0clhz3k9THs9gMnol5I5uVUk3+eXU0+ApQZJNhhWSdsn7wcQti9nMQ==",
|
||||||
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
|
"dependencies": {
|
||||||
|
"remotion": "4.0.300"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@remotion/media-parser": {
|
"node_modules/@remotion/media-parser": {
|
||||||
"version": "4.0.300",
|
"version": "4.0.300",
|
||||||
"resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.300.tgz",
|
"resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.300.tgz",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remotion/cli": "^4.0.300",
|
"@remotion/cli": "^4.0.300",
|
||||||
|
"@remotion/google-fonts": "^4.0.300",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"remotion": "^4.0.300",
|
"remotion": "^4.0.300",
|
||||||
|
|
|
||||||
BIN
remotion/public/folge02/figuren/lulu_A1_augen-zu.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
remotion/public/folge02/figuren/lulu_A2_augen-halb.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
remotion/public/folge02/figuren/lulu_A3_lachen.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
remotion/public/folge02/figuren/lulu_A4_staunen.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
remotion/public/folge02/figuren/lulu_A5_blick-links.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
remotion/public/folge02/figuren/lulu_A6_blick-rechts.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
remotion/public/folge02/figuren/lulu_A7-1_gehen.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
remotion/public/folge02/figuren/lulu_A7-2_gehen.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
remotion/public/folge02/figuren/lulu_A7-3_gehen.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
remotion/public/folge02/figuren/lulu_A7-4_gehen.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
remotion/public/folge02/figuren/lulu_A8_rueckansicht.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
remotion/public/folge02/figuren/lulu_A_apfel.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
remotion/public/folge02/figuren/lulu_A_hals-unten.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
remotion/public/folge02/figuren/lulu_B1_traurig.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
remotion/public/folge02/figuren/lulu_B2_erschrocken.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
remotion/public/folge02/figuren/lulu_B3_schlafen.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
remotion/public/folge02/figuren/lulu_B_kopf-profil.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
remotion/public/folge02/figuren/lulu_B_liegt.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
remotion/public/folge02/figuren/noa_A1_augen-zu.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
remotion/public/folge02/figuren/noa_A2_augen-halb.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
remotion/public/folge02/figuren/noa_A3_lachen.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
remotion/public/folge02/figuren/noa_A4_staunen.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
remotion/public/folge02/figuren/noa_A5_blick-links.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
remotion/public/folge02/figuren/noa_A6_blick-rechts.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
remotion/public/folge02/figuren/noa_A7-1_gehen.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
remotion/public/folge02/figuren/noa_A7-2_gehen.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
remotion/public/folge02/figuren/noa_A7-3_gehen.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
remotion/public/folge02/figuren/noa_A7-4_gehen.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
remotion/public/folge02/figuren/noa_A8_rueckansicht.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
remotion/public/folge02/figuren/noa_A_arm-hoch-winken.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
remotion/public/folge02/figuren/noa_A_arme-hoch.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
remotion/public/folge02/figuren/noa_A_laterne.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
remotion/public/folge02/figuren/noa_B1_traurig.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
remotion/public/folge02/figuren/noa_B2_erschrocken.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
remotion/public/folge02/figuren/noa_B3_schlafen.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
remotion/public/folge02/figuren/noa_B_hocken.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
remotion/public/folge02/figuren/noa_B_sitzt.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
remotion/public/folge02/figuren/noa_B_zeigen.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
remotion/public/folge02/figuren/noa_ref_stehend.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
remotion/public/folge02/figuren/pino_A1_augen-zu.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
remotion/public/folge02/figuren/pino_A2_augen-halb.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
remotion/public/folge02/figuren/pino_A3_lachen.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
remotion/public/folge02/figuren/pino_A4_staunen.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
remotion/public/folge02/figuren/pino_A5_blick-links.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
remotion/public/folge02/figuren/pino_A6_blick-rechts.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
remotion/public/folge02/figuren/pino_A7-1_gehen.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
remotion/public/folge02/figuren/pino_A7-2_gehen.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
remotion/public/folge02/figuren/pino_A7-3_gehen.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
remotion/public/folge02/figuren/pino_A7-4_gehen.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
remotion/public/folge02/figuren/pino_A8_rueckansicht.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
remotion/public/folge02/figuren/pino_A_hinterbeine.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
remotion/public/folge02/figuren/pino_A_kugel.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
remotion/public/folge02/figuren/pino_A_laufen-1.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
remotion/public/folge02/figuren/pino_A_laufen-2.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
remotion/public/folge02/figuren/pino_A_laufen-3.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
remotion/public/folge02/figuren/pino_A_laufen-4.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
remotion/public/folge02/figuren/pino_B1_traurig.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
remotion/public/folge02/figuren/pino_B2_erschrocken.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
remotion/public/folge02/figuren/pino_B3_schlafen.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
remotion/public/folge02/figuren/pino_B_blick-nach-oben.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
remotion/public/folge02/figuren/pino_B_schlaeft-eingerollt.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
remotion/public/folge02/figuren/pino_ref_sitzend.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
remotion/public/folge02/orte/garten.webp
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
remotion/public/folge02/orte/garten_nacht.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
remotion/public/folge02/orte/kalender.webp
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
remotion/public/folge02/orte/oben.webp
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
remotion/public/folge02/orte/zimmer_himmel.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
remotion/public/folge02/orte/zimmer_tisch.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
remotion/public/folge02/orte/zimmer_wand.webp
Normal file
|
After Width: | Height: | Size: 221 KiB |
|
|
@ -3,6 +3,8 @@ import {Composition} from 'remotion';
|
||||||
import {HintergrundAbend, PapierStruktur, VordergrundAbend} from './szenen/Hintergruende';
|
import {HintergrundAbend, PapierStruktur, VordergrundAbend} from './szenen/Hintergruende';
|
||||||
import {LaterneTest} from './szenen/LaterneTest';
|
import {LaterneTest} from './szenen/LaterneTest';
|
||||||
import {VerformTest} from './szenen/VerformTest';
|
import {VerformTest} from './szenen/VerformTest';
|
||||||
|
import {ORTE, OrtStill} from './folge02/orte';
|
||||||
|
import {FOLGE02_DAUER, Folge02} from './folge02/Folge02';
|
||||||
|
|
||||||
const BILD = {fps: 30, width: 1920, height: 1080} as const;
|
const BILD = {fps: 30, width: 1920, height: 1080} as const;
|
||||||
|
|
||||||
|
|
@ -13,5 +15,9 @@ export const Root: React.FC = () => (
|
||||||
<Composition id="HintergrundAbend" component={HintergrundAbend} durationInFrames={1} {...BILD} />
|
<Composition id="HintergrundAbend" component={HintergrundAbend} durationInFrames={1} {...BILD} />
|
||||||
<Composition id="VordergrundAbend" component={VordergrundAbend} durationInFrames={1} {...BILD} />
|
<Composition id="VordergrundAbend" component={VordergrundAbend} durationInFrames={1} {...BILD} />
|
||||||
<Composition id="PapierStruktur" component={PapierStruktur} durationInFrames={1} {...BILD} />
|
<Composition id="PapierStruktur" component={PapierStruktur} durationInFrames={1} {...BILD} />
|
||||||
|
<Composition id="Folge02" component={Folge02} durationInFrames={FOLGE02_DAUER} {...BILD} />
|
||||||
|
{Object.keys(ORTE).map((name) => (
|
||||||
|
<Composition key={name} id={`F2Ort-${name.replace('_', '-')}`} component={OrtStill} defaultProps={{name}} durationInFrames={1} {...BILD} />
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
39
remotion/src/folge02/Folge02.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {AbsoluteFill, Series} from 'remotion';
|
||||||
|
import {INTRO, Intro, OUTRO, Outro} from './buch';
|
||||||
|
import {LIED, Lied} from './lied';
|
||||||
|
import {S2, Szene2} from './szene2';
|
||||||
|
import {S3, Szene3} from './szene3';
|
||||||
|
import {S4, Szene4} from './szene4';
|
||||||
|
import {S5, Szene5} from './szene5';
|
||||||
|
import {S6, Szene6} from './szene6';
|
||||||
|
import {S7, Szene7} from './szene7';
|
||||||
|
|
||||||
|
// Folge 2 „Pino ist müde“ – komplett per Code aus der Figuren-Bibliothek,
|
||||||
|
// noch ohne Ton (Untertitel statt Stimmen).
|
||||||
|
|
||||||
|
export const SZENEN: {name: string; dauer: number; K: React.FC}[] = [
|
||||||
|
{name: 'Intro', dauer: INTRO.dauer, K: Intro},
|
||||||
|
{name: 'Gähnen überall', dauer: S2.dauer, K: Szene2},
|
||||||
|
{name: 'Lulu sieht es von oben', dauer: S3.dauer, K: Szene3},
|
||||||
|
{name: 'Ein Bett aus Blättern', dauer: S4.dauer, K: Szene4},
|
||||||
|
{name: 'Lied', dauer: LIED.dauer, K: Lied},
|
||||||
|
{name: 'Pino will nicht schlafen', dauer: S5.dauer, K: Szene5},
|
||||||
|
{name: 'Noas Idee', dauer: S6.dauer, K: Szene6},
|
||||||
|
{name: 'Gute Nacht, Pino', dauer: S7.dauer, K: Szene7},
|
||||||
|
{name: 'Outro', dauer: OUTRO.dauer, K: Outro},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FOLGE02_DAUER = SZENEN.reduce((s, x) => s + x.dauer, 0);
|
||||||
|
|
||||||
|
export const Folge02: React.FC = () => (
|
||||||
|
<AbsoluteFill style={{background: '#f4ead8'}}>
|
||||||
|
<Series>
|
||||||
|
{SZENEN.map(({name, dauer, K}) => (
|
||||||
|
<Series.Sequence key={name} durationInFrames={dauer} name={name}>
|
||||||
|
<K />
|
||||||
|
</Series.Sequence>
|
||||||
|
))}
|
||||||
|
</Series>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
163
remotion/src/folge02/buch.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
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} 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.
|
||||||
|
export const INTRO = plane(
|
||||||
|
[
|
||||||
|
{id: 'buch', dauer: 2.2},
|
||||||
|
{id: 't1', wer: 'ERZ', text: 'Pino, Noa und Lulu.'},
|
||||||
|
{id: 't2', wer: 'ERZ', text: 'Heute: Pino ist müde.'},
|
||||||
|
],
|
||||||
|
0.3,
|
||||||
|
1.6,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Intro: React.FC = () => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const {z, dauer} = INTRO;
|
||||||
|
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="Pino ist müde" posen={['pino_A2_augen-halb', 'noa_A3_lachen', 'lulu_A3_lachen']} />}
|
||||||
|
{winkel >= -90 && <Seite links={960} />}
|
||||||
|
<Deckel winkel={winkel} />
|
||||||
|
</Kamera>
|
||||||
|
<Vignette />
|
||||||
|
<Untertitel zeilen={INTRO.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}], 0.2, 0.6);
|
||||||
|
|
||||||
|
export const Outro: React.FC = () => {
|
||||||
|
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={['pino_B3_schlafen', 'noa_A_arm-hoch-winken', 'lulu_A3_lachen']} />}
|
||||||
|
{winkel >= -90 && <Seite links={960} />}
|
||||||
|
<Deckel winkel={winkel} />
|
||||||
|
</Kamera>
|
||||||
|
<Vignette />
|
||||||
|
<Blende dauer={dauer} rein={10} raus={20} farbe="#1c1a24" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
152
remotion/src/folge02/figur.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {Freeze, Img, OffthreadVideo, staticFile, useCurrentFrame, useVideoConfig} from 'remotion';
|
||||||
|
import {CLIPS, FIGUREN, POSEN} from './posen';
|
||||||
|
|
||||||
|
// Figuren aus der Bibliothek (freigestellte Standbilder und Clips).
|
||||||
|
// Alle Bilder einer Figur liegen in derselben Leinwand (1376 × 768); deshalb
|
||||||
|
// passen Posen beim Überblenden aufeinander. Verankert wird an den Füßen der
|
||||||
|
// Referenzpose: (x, y) ist der Fußpunkt auf dem Bildschirm, `hoehe` die Höhe
|
||||||
|
// der Referenzpose in Bildschirm-Pixeln.
|
||||||
|
|
||||||
|
// Größenverhältnis wie in figuren/groessen.jpg (Noa = 1)
|
||||||
|
export const GROESSE = {pino: 0.4, noa: 1, lulu: 2.05} as const;
|
||||||
|
export type FigurName = keyof typeof GROESSE;
|
||||||
|
|
||||||
|
// Posenfolge: [[frame, pose], ...]. Liefert die aktuelle Pose und, während
|
||||||
|
// eines Wechsels, die vorige Pose samt Mischanteil.
|
||||||
|
export type Posenfolge = [number, string][];
|
||||||
|
export const posenStand = (frame: number, folge: Posenfolge, blende = 5) => {
|
||||||
|
let i = 0;
|
||||||
|
while (i + 1 < folge.length && frame >= folge[i + 1][0]) i++;
|
||||||
|
const [start, pose] = folge[i];
|
||||||
|
const mix = i > 0 ? Math.min(1, (frame - start) / blende) : 1;
|
||||||
|
return {pose, von: mix < 1 ? folge[i - 1][1] : undefined, mix};
|
||||||
|
};
|
||||||
|
|
||||||
|
type Lage = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
hoehe: number;
|
||||||
|
spiegeln?: boolean;
|
||||||
|
drehung?: number; // Grad, um den Fußpunkt
|
||||||
|
stauchX?: number;
|
||||||
|
stauchY?: number;
|
||||||
|
deckkraft?: number;
|
||||||
|
schatten?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
figur: FigurName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gemeinsamer Rahmen: Leinwand der Figur so legen, dass der Fußpunkt auf (x, y) liegt.
|
||||||
|
const Rahmen: React.FC<Lage> = ({x, y, hoehe, spiegeln, drehung = 0, stauchX = 1, stauchY = 1, deckkraft = 1, schatten = true, figur, children}) => {
|
||||||
|
const f = FIGUREN[figur];
|
||||||
|
const s = hoehe / f.refHoehe;
|
||||||
|
return (
|
||||||
|
<div style={{position: 'absolute', left: x, top: y, width: 0, height: 0, opacity: deckkraft}}>
|
||||||
|
{schatten && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: -hoehe * 0.32 * (figur === 'lulu' ? 0.7 : 1),
|
||||||
|
top: -hoehe * 0.025,
|
||||||
|
width: hoehe * 0.64 * (figur === 'lulu' ? 0.7 : 1),
|
||||||
|
height: hoehe * 0.05,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'radial-gradient(closest-side, rgba(60,45,30,0.32), rgba(60,45,30,0))',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div style={{position: 'absolute', transform: `rotate(${drehung}deg) scale(${(spiegeln ? -1 : 1) * stauchX}, ${stauchY})`, transformOrigin: '0 0'}}>
|
||||||
|
<div style={{position: 'absolute', left: -f.fussX * s, top: -f.fussY * s, transform: `scale(${s})`, transformOrigin: '0 0'}}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PosenBild: React.FC<{name: string; deckkraft?: number}> = ({name, deckkraft = 1}) => {
|
||||||
|
const p = POSEN[name];
|
||||||
|
if (!p) throw new Error(`Pose fehlt: ${name}`);
|
||||||
|
return <Img src={staticFile(p.datei)} style={{position: 'absolute', left: p.x, top: p.y, width: p.b, height: p.h, opacity: deckkraft}} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FigurProps = Omit<Lage, 'children' | 'figur' | 'hoehe'> & {
|
||||||
|
pose: string | Posenfolge;
|
||||||
|
hoehe?: number; // Bildschirmhöhe der Referenzpose; Standard: Größenverhältnis × NOA
|
||||||
|
frame?: number; // eigener Zeitbezug für Posenfolgen (Standard: aktueller Frame)
|
||||||
|
blende?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NOA_HOEHE = 340;
|
||||||
|
|
||||||
|
export const Figur: React.FC<FigurProps> = ({pose, hoehe, frame, blende, ...lage}) => {
|
||||||
|
const jetzt = useCurrentFrame();
|
||||||
|
const folge: Posenfolge = typeof pose === 'string' ? [[0, pose]] : pose;
|
||||||
|
const st = posenStand(frame ?? jetzt, folge, blende);
|
||||||
|
const figur = POSEN[st.pose].figur as FigurName;
|
||||||
|
return (
|
||||||
|
<Rahmen {...lage} figur={figur} hoehe={hoehe ?? NOA_HOEHE * GROESSE[figur]}>
|
||||||
|
{st.von && <PosenBild name={st.von} />}
|
||||||
|
<PosenBild name={st.pose} deckkraft={st.mix} />
|
||||||
|
</Rahmen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bewegungsclip (freigestellt, WebM mit Alpha). `start` = Frame, an dem der Clip
|
||||||
|
// beginnt; davor steht das erste Bild, danach das letzte. `schleife` wiederholt ihn.
|
||||||
|
type ClipProps = Omit<Lage, 'children' | 'figur' | 'hoehe'> & {
|
||||||
|
name: string;
|
||||||
|
start: number;
|
||||||
|
hoehe?: number;
|
||||||
|
tempo?: number;
|
||||||
|
schleife?: boolean;
|
||||||
|
bis?: number; // nur bis zu dieser Clip-Sekunde abspielen, dann anhalten
|
||||||
|
rueckwaerts?: boolean;
|
||||||
|
ersatz?: string; // Standbild, falls der Clip fehlt
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClipFigur: React.FC<ClipProps> = ({name, start, hoehe, tempo = 1, schleife = false, bis, rueckwaerts = false, ersatz, ...lage}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const {fps} = useVideoConfig();
|
||||||
|
const c = CLIPS[name];
|
||||||
|
if (!c) {
|
||||||
|
// Clip noch nicht freigestellt: Standbild der Figur als Ersatz
|
||||||
|
const figurName = name.split('_')[0] as FigurName;
|
||||||
|
return <Figur pose={ersatz ?? ({pino: 'pino_ref_sitzend', noa: 'noa_ref_stehend', lulu: 'lulu_A6_blick-rechts'} as const)[figurName]} hoehe={hoehe} {...lage} />;
|
||||||
|
}
|
||||||
|
const figur = c.figur as FigurName;
|
||||||
|
const laenge = (bis ?? c.frames / 24) * fps; // in Frames der Komposition bei Tempo 1
|
||||||
|
let t = Math.max(0, (frame - start) * tempo);
|
||||||
|
t = schleife ? t % (laenge - 1) : Math.min(t, laenge - 1);
|
||||||
|
if (rueckwaerts) t = laenge - 1 - t;
|
||||||
|
return (
|
||||||
|
<Rahmen {...lage} figur={figur} hoehe={hoehe ?? NOA_HOEHE * GROESSE[figur]}>
|
||||||
|
<div style={{position: 'absolute', left: c.x, top: c.y, width: c.b, height: c.h}}>
|
||||||
|
<Freeze frame={Math.floor(t)}>
|
||||||
|
<OffthreadVideo src={staticFile(c.datei)} transparent muted style={{width: '100%', height: '100%'}} />
|
||||||
|
</Freeze>
|
||||||
|
</div>
|
||||||
|
</Rahmen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Kamera: zeigt den Ausschnitt um (x, y) mit Zoom z (1 = ganze 1920 × 1080-Welt).
|
||||||
|
// Der Ausschnitt bleibt immer innerhalb der Welt (kein leerer Rand).
|
||||||
|
export const Kamera: React.FC<{x?: number; y?: number; zoom?: number; frei?: boolean; children: React.ReactNode}> = ({x: kx = 960, y: ky = 540, zoom = 1, frei = false, children}) => {
|
||||||
|
const x = frei ? kx : Math.min(1920 - 960 / zoom, Math.max(960 / zoom, kx));
|
||||||
|
const y = frei ? ky : Math.min(1080 - 540 / zoom, Math.max(540 / zoom, ky));
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{position: 'absolute', width: 1920, height: 1080, transform: `translate(960px, 540px) scale(${zoom}) translate(${-x}px, ${-y}px)`, transformOrigin: '0 0'}}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
72
remotion/src/folge02/helfer.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {interpolate, useCurrentFrame} from 'remotion';
|
||||||
|
import {kurve} from '../bewegung';
|
||||||
|
import {FIGUREN} from './posen';
|
||||||
|
import {GROESSE, NOA_HOEHE, type FigurName} from './figur';
|
||||||
|
|
||||||
|
// Kamerafahrt: [[frame, x, y, zoom], ...] mit weichen Übergängen.
|
||||||
|
export const kamFahrt = (frame: number, punkte: [number, number, number, number][]) => ({
|
||||||
|
x: kurve(frame, punkte.map(([f, x]) => [f, x])),
|
||||||
|
y: kurve(frame, punkte.map(([f, , y]) => [f, y])),
|
||||||
|
zoom: kurve(frame, punkte.map(([f, , , z]) => [f, z])),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Leichtes Atmen: Stauchung in y, je Figur etwas anders getaktet.
|
||||||
|
export const atmen = (frame: number, phase = 0, staerke = 0.008) => 1 + Math.sin(frame * 0.07 + phase) * staerke;
|
||||||
|
|
||||||
|
// Gehzyklus aus den vier Geh-Standbildern (Blick nach links; nach rechts spiegeln).
|
||||||
|
export const gehPose = (figur: FigurName, frame: number, bilderProSek = 6) => `${figur}_A7-${1 + (Math.floor((frame * bilderProSek) / 30) % 4)}_gehen`;
|
||||||
|
|
||||||
|
// Wippen beim Gehen (Höhe in px)
|
||||||
|
export const gehWippen = (frame: number, bilderProSek = 6, hoehe = 6) => -Math.abs(Math.sin((frame * bilderProSek * Math.PI) / 60)) * hoehe;
|
||||||
|
|
||||||
|
// Bildschirmpunkt eines Punktes (cx, cy) aus der Bild-Leinwand der Figur.
|
||||||
|
export const punkt = (figur: FigurName, x: number, y: number, cx: number, cy: number, hoehe = NOA_HOEHE * GROESSE[figur], spiegeln = false) => {
|
||||||
|
const f = FIGUREN[figur];
|
||||||
|
const s = hoehe / f.refHoehe;
|
||||||
|
return {x: x + (cx - f.fussX) * s * (spiegeln ? -1 : 1), y: y + (cy - f.fussY) * s};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bekannte Punkte in der Leinwand (gemessen an den Standbildern)
|
||||||
|
export const LULU_MAUL_LINKS = {cx: 690, cy: 140}; // lulu_A5 (Blick links)
|
||||||
|
export const LULU_KOPF = {cx: 714, cy: 95};
|
||||||
|
export const PINO_KOPF = {cx: 640, cy: 260};
|
||||||
|
|
||||||
|
// Weiche Blende am Anfang und Ende einer Szene (cremefarbenes Papier).
|
||||||
|
export const Blende: React.FC<{dauer: number; rein?: number; raus?: number; farbe?: string}> = ({dauer, rein = 10, raus = 10, farbe = '#f4ead8'}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const o = Math.max(
|
||||||
|
rein ? interpolate(frame, [0, rein], [1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}) : 0,
|
||||||
|
raus ? interpolate(frame, [dauer - raus, dauer], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}) : 0,
|
||||||
|
);
|
||||||
|
return o > 0 ? <div style={{position: 'absolute', inset: 0, background: farbe, opacity: o}} /> : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vignette und Papierstruktur über allem
|
||||||
|
export const Vignette: React.FC<{staerke?: number}> = ({staerke = 0.35}) => (
|
||||||
|
<div style={{position: 'absolute', inset: 0, background: `radial-gradient(ellipse at 50% 50%, rgba(0,0,0,0) 60%, rgba(40,30,20,${staerke}) 100%)`}} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wackeln (Kamera bei Schreck/Niesen): abklingend ab `start`
|
||||||
|
export const ruck = (frame: number, start: number, staerke = 10, dauer = 18) => {
|
||||||
|
const t = frame - start;
|
||||||
|
if (t < 0 || t > dauer) return 0;
|
||||||
|
return Math.sin(t * 2.2) * staerke * (1 - t / dauer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Kurzer Hüpfer: 0 → -hoehe → 0 zwischen start und start+dauer
|
||||||
|
export const huepfer = (frame: number, start: number, hoehe = 30, dauer = 12) => {
|
||||||
|
const t = (frame - start) / dauer;
|
||||||
|
if (t < 0 || t > 1) return 0;
|
||||||
|
return -Math.sin(t * Math.PI) * hoehe;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stauchung beim Landen/Abspringen um einen Hüpfer herum
|
||||||
|
export const hopfStauch = (frame: number, start: number, dauer = 12) => {
|
||||||
|
const vor = (frame - (start - 5)) / 5;
|
||||||
|
const nach = (frame - (start + dauer)) / 7;
|
||||||
|
if (vor >= 0 && vor < 1) return 1 - 0.07 * Math.sin(vor * Math.PI);
|
||||||
|
if (nach >= 0 && nach < 1) return 1 - 0.06 * Math.sin(nach * Math.PI);
|
||||||
|
if (frame >= start && frame < start + dauer) return 1.04;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
68
remotion/src/folge02/lied.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {random, useCurrentFrame} from 'remotion';
|
||||||
|
import {Figur, Kamera} from './figur';
|
||||||
|
import {Blende, Vignette} from './helfer';
|
||||||
|
import {Ort} from './orte';
|
||||||
|
import {BODEN} from './szene2';
|
||||||
|
import {SCHRIFT, Untertitel} from './untertitel';
|
||||||
|
import {plane} from './zeit';
|
||||||
|
|
||||||
|
// Refrain „Einer klein und einer groß“ (aus Folge 1), einmal. Die drei wippen im Takt.
|
||||||
|
|
||||||
|
const TAKT = 19; // Frames pro Schlag (ca. 95 bpm)
|
||||||
|
|
||||||
|
export const LIED = plane(
|
||||||
|
[
|
||||||
|
{id: 'r1', wer: 'ALLE', text: 'Einer klein und einer groß,', dauer: 3.2},
|
||||||
|
{id: 'r2', wer: 'ALLE', text: 'und dazwischen – na, wer bloß?', dauer: 3.2},
|
||||||
|
{id: 'r3', wer: 'ALLE', text: 'Noa steht in der Mitte drin,', dauer: 3.2},
|
||||||
|
{id: 'r4', wer: 'ALLE', text: 'zusammen kriegen wir alles hin!', dauer: 3.6},
|
||||||
|
],
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const Note: React.FC<{x: number; y: number; s: number; farbe: string}> = ({x, y, s, farbe}) => (
|
||||||
|
<svg width={60} height={80} viewBox="0 0 60 80" style={{position: 'absolute', left: x, top: y, transform: `scale(${s})`}}>
|
||||||
|
<ellipse cx={18} cy={64} rx={15} ry={11} fill={farbe} transform="rotate(-20 18 64)" />
|
||||||
|
<rect x={30} y={8} width={6} height={58} fill={farbe} />
|
||||||
|
<path d="M 36 8 C 50 18, 56 30, 46 44 C 50 30, 44 24, 36 22 Z" fill={farbe} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Lied: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const {z, dauer} = LIED;
|
||||||
|
const schlag = Math.abs(Math.sin((f * Math.PI) / TAKT));
|
||||||
|
const wipp = (phase: number, h: number) => -Math.abs(Math.sin((f * Math.PI) / TAKT + phase)) * h;
|
||||||
|
const schwank = (phase: number, g: number) => Math.sin((f * Math.PI) / (2 * TAKT) + phase) * g;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Kamera x={980} y={600} zoom={1.3}>
|
||||||
|
<Ort name="garten" />
|
||||||
|
<Figur pose="lulu_A3_lachen" x={1240} y={BODEN} drehung={schwank(0.5, 2.2)} stauchY={1 - 0.012 * schlag} />
|
||||||
|
<Figur
|
||||||
|
pose={[
|
||||||
|
[0, 'noa_A3_lachen'],
|
||||||
|
[z.r4.start, 'noa_A_arme-hoch'],
|
||||||
|
]}
|
||||||
|
x={960}
|
||||||
|
y={BODEN + wipp(0.3, 10)}
|
||||||
|
drehung={schwank(0, 3)}
|
||||||
|
/>
|
||||||
|
<Figur pose="pino_A3_lachen" x={720} y={BODEN + wipp(0, 16)} drehung={schwank(1, 4)} stauchY={1 - 0.03 * schlag} />
|
||||||
|
</Kamera>
|
||||||
|
{new Array(8).fill(0).map((_, i) => {
|
||||||
|
const t = ((f + i * 25) % 200) / 200;
|
||||||
|
const x = 200 + random(`n${i}`) * 1500 + Math.sin(t * 6 + i) * 30;
|
||||||
|
return <div key={i} style={{opacity: Math.sin(t * Math.PI) * 0.9}}><Note x={x} y={700 - t * 600} s={0.8 + random(`ns${i}`) * 0.6} farbe={['#c8642c', '#3f5a8a', '#5f8a4a', '#a0526b'][i % 4]} /></div>;
|
||||||
|
})}
|
||||||
|
<div style={{position: 'absolute', top: 60, left: 0, right: 0, textAlign: 'center', fontFamily: SCHRIFT, fontWeight: 600, fontSize: 40, color: '#8a6a4a', opacity: Math.min(1, f / 20)}}>
|
||||||
|
♫ Einer klein und einer groß ♫
|
||||||
|
</div>
|
||||||
|
<Vignette />
|
||||||
|
<Untertitel zeilen={LIED.liste} />
|
||||||
|
<Blende dauer={dauer} rein={12} raus={12} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
248
remotion/src/folge02/natur.tsx
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {random, useCurrentFrame} from 'remotion';
|
||||||
|
import {SCHRIFT} from './untertitel';
|
||||||
|
|
||||||
|
// Kleine, per Code gezeichnete Dinge: Blätter, Vögel, Schnee, Nest, Zzz, Funkeln.
|
||||||
|
|
||||||
|
export const LAUB = ['#c8642c', '#d99a2b', '#a8452a', '#8f5a2e', '#e0b04a', '#b7792f'];
|
||||||
|
|
||||||
|
// Ein Blatt, Spitze nach oben, ca. 70 px lang bei s = 1.
|
||||||
|
export const Blatt: React.FC<{x: number; y: number; r?: number; s?: number; farbe: string; deckkraft?: number}> = ({x, y, r = 0, s = 1, farbe, deckkraft = 1}) => (
|
||||||
|
<svg
|
||||||
|
width={80}
|
||||||
|
height={90}
|
||||||
|
viewBox="-40 -55 80 90"
|
||||||
|
style={{position: 'absolute', left: x - 40, top: y - 45, transform: `rotate(${r}deg) scale(${s})`, overflow: 'visible', opacity: deckkraft}}
|
||||||
|
>
|
||||||
|
<path d="M0,-50 C 24,-36 26,-4 0,22 C -26,-4 -24,-36 0,-50 Z" fill={farbe} stroke="rgba(80,40,20,0.35)" strokeWidth={2} />
|
||||||
|
<path d="M0,-42 L0,32" stroke="rgba(90,45,20,0.45)" strokeWidth={2.5} strokeLinecap="round" />
|
||||||
|
<path d="M0,-18 L12,-28 M0,-4 L14,-14 M0,-12 L-12,-22 M0,2 L-13,-8" stroke="rgba(90,45,20,0.3)" strokeWidth={1.6} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Blätter, die der Wind durchs Bild weht (von links nach rechts).
|
||||||
|
export const Blaetterwind: React.FC<{anzahl?: number; seed?: string; tempo?: number; yBereich?: [number, number]}> = ({
|
||||||
|
anzahl = 7,
|
||||||
|
seed = 'wind',
|
||||||
|
tempo = 1,
|
||||||
|
yBereich = [250, 850],
|
||||||
|
}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{new Array(anzahl).fill(0).map((_, i) => {
|
||||||
|
const r = (k: string) => random(`${seed}${k}${i}`);
|
||||||
|
const periode = 260 + r('p') * 200;
|
||||||
|
const t = ((frame * tempo + r('o') * periode) % periode) / periode;
|
||||||
|
const x = -120 + t * 2160;
|
||||||
|
const y = yBereich[0] + r('y') * (yBereich[1] - yBereich[0]) + Math.sin(t * 9 + r('w') * 6) * 60 + t * 120;
|
||||||
|
return <Blatt key={i} x={x} y={y} r={frame * (3 + r('d') * 4) + r('a') * 360} s={0.5 + r('s') * 0.4} farbe={LAUB[i % LAUB.length]} />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Blätterregen nach dem Niesen: Blätter fliegen ab `start` aus (x0, y0) und landen auf `boden`.
|
||||||
|
export const Blaetterregen: React.FC<{start: number; x0: number; y0: number; boden: number; anzahl?: number; aus?: number}> = ({start, x0, y0, boden, anzahl = 34, aus = 1e9}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const t = frame - start;
|
||||||
|
const deck = Math.max(0, 1 - (frame - aus) / 60);
|
||||||
|
if (t < 0 || deck <= 0) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{new Array(anzahl).fill(0).map((_, i) => {
|
||||||
|
const r = (k: string) => random(`regen${k}${i}`);
|
||||||
|
const winkel = -Math.PI * (0.05 + 0.9 * r('w'));
|
||||||
|
const v = 14 + r('v') * 22;
|
||||||
|
// erst schneller Ausstoß (abgebremst), dann langsames Herabschaukeln
|
||||||
|
const raus = 1 - Math.exp(-t / 9);
|
||||||
|
const fall = Math.max(0, t - 12);
|
||||||
|
let x = x0 + Math.cos(winkel) * v * 9 * raus + Math.sin(fall * 0.09 + r('s') * 6) * 50;
|
||||||
|
let y = y0 + Math.sin(winkel) * v * 9 * raus + fall * (2.2 + r('f') * 1.6);
|
||||||
|
const ziel = boden + r('b') * 70 - 20;
|
||||||
|
const gelandet = y >= ziel;
|
||||||
|
if (gelandet) y = ziel;
|
||||||
|
x = Math.min(1900, Math.max(20, x));
|
||||||
|
return <Blatt key={i} x={x} y={y} r={gelandet ? r('l') * 160 - 80 : t * (5 + r('d') * 6) + r('a') * 360} s={0.55 + r('g') * 0.45} farbe={LAUB[i % LAUB.length]} deckkraft={Math.min(1, deck)} />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ein Vogelschwarm zieht nach rechts oben davon.
|
||||||
|
export const Voegel: React.FC<{start: number; anzahl?: number; skala?: number}> = ({start, anzahl = 11, skala = 1}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const t = frame - start;
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 1920 1080" style={{position: 'absolute', inset: 0, width: '100%', height: '100%'}}>
|
||||||
|
{new Array(anzahl).fill(0).map((_, i) => {
|
||||||
|
const r = (k: string) => random(`vogel${k}${i}`);
|
||||||
|
const reihe = Math.abs(i - (anzahl - 1) / 2);
|
||||||
|
const x = 100 + t * 6 - reihe * 70 * skala + r('x') * 30;
|
||||||
|
const y = 560 - t * 1.6 + reihe * 38 * skala + r('y') * 20;
|
||||||
|
const schlag = Math.sin(t * 0.45 + r('p') * 6) * 12 * skala;
|
||||||
|
const g = (16 + r('g') * 6) * skala;
|
||||||
|
return <path key={i} d={`M ${x - g} ${y - schlag} Q ${x - g / 2} ${y - 4} ${x} ${y} Q ${x + g / 2} ${y - 4} ${x + g} ${y - schlag}`} stroke="#4a4646" strokeWidth={3.5 * skala} fill="none" strokeLinecap="round" />;
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Schneeflocken; `anzahl` = 1 für die erste Flocke, die auf `ziel` landet.
|
||||||
|
export const Schnee: React.FC<{start: number; ziel?: [number, number]; anzahl?: number}> = ({start, ziel, anzahl = 1}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{new Array(anzahl).fill(0).map((_, i) => {
|
||||||
|
const t = frame - start - i * 40;
|
||||||
|
if (t < 0) return null;
|
||||||
|
const r = (k: string) => random(`schnee${k}${i}`);
|
||||||
|
let x: number;
|
||||||
|
let y: number;
|
||||||
|
if (ziel && i === 0) {
|
||||||
|
const dauer = 240;
|
||||||
|
const u = Math.min(1, t / dauer);
|
||||||
|
x = ziel[0] + Math.sin(u * 7) * 40 * (1 - u);
|
||||||
|
y = -30 + (ziel[1] + 30) * u;
|
||||||
|
} else {
|
||||||
|
x = r('x') * 1920 + Math.sin(t * 0.03 + r('p') * 6) * 40;
|
||||||
|
y = -30 + t * (1.3 + r('v'));
|
||||||
|
if (y > 1100) return null;
|
||||||
|
}
|
||||||
|
const g = 7 + r('g') * 5;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: x - g,
|
||||||
|
top: y - g,
|
||||||
|
width: g * 2,
|
||||||
|
height: g * 2,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'radial-gradient(circle, #ffffff 0%, #f4f8ff 45%, rgba(240,246,255,0) 72%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Blätternest: ein runder Laubhaufen. `voll` 0…1 lässt ihn wachsen.
|
||||||
|
// Hinten und vorne getrennt, damit Pino „im“ Nest sitzen kann.
|
||||||
|
const nestBlaetter = (seite: 'hinten' | 'vorne') =>
|
||||||
|
new Array(seite === 'hinten' ? 46 : 30).fill(0).map((_, i) => {
|
||||||
|
const r = (k: string) => random(`nest${seite}${k}${i}`);
|
||||||
|
const w = Math.PI * r('w');
|
||||||
|
const rad = seite === 'hinten' ? 0.55 + r('r') * 0.5 : 0.75 + r('r') * 0.3;
|
||||||
|
return {
|
||||||
|
x: Math.cos(w) * 150 * rad * (seite === 'vorne' ? 1.05 : 1),
|
||||||
|
y: seite === 'hinten' ? -Math.sin(w) * 95 * rad : -Math.sin(w) * 38 * rad + 8,
|
||||||
|
r: r('d') * 360,
|
||||||
|
s: 0.75 + r('s') * 0.45,
|
||||||
|
farbe: LAUB[Math.floor(r('f') * LAUB.length)],
|
||||||
|
reihe: r('o'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const NEST = {hinten: nestBlaetter('hinten'), vorne: nestBlaetter('vorne')};
|
||||||
|
|
||||||
|
export const Nest: React.FC<{x: number; y: number; voll?: number; seite: 'hinten' | 'vorne'; s?: number}> = ({x, y, voll = 1, seite, s = 1}) => (
|
||||||
|
<div style={{position: 'absolute', left: x, top: y, transform: `scale(${s})`, transformOrigin: '0 0'}}>
|
||||||
|
{seite === 'hinten' && (
|
||||||
|
<div style={{position: 'absolute', left: -170, top: -30, width: 340, height: 60, borderRadius: '50%', background: 'radial-gradient(closest-side, rgba(70,45,25,0.45), rgba(70,45,25,0))'}} />
|
||||||
|
)}
|
||||||
|
{NEST[seite].map((b, i) =>
|
||||||
|
b.reihe < voll ? <Blatt key={i} x={b.x * (0.5 + 0.5 * voll)} y={b.y * (0.4 + 0.6 * voll)} r={b.r} s={b.s} farbe={b.farbe} /> : null,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Zzz, die aufsteigen und verblassen.
|
||||||
|
export const Zzz: React.FC<{x: number; y: number; start: number; ende?: number; s?: number}> = ({x, y, start, ende = 1e9, s = 1}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
if (frame < start || frame > ende + 60) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{[0, 1, 2].map((i) => {
|
||||||
|
const t = ((frame - start + i * 30) % 90) / 90;
|
||||||
|
const aus = frame > ende ? Math.max(0, 1 - (frame - ende) / 30) : 1;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: x + t * 70 * s + Math.sin(t * 6) * 10,
|
||||||
|
top: y - t * 120 * s,
|
||||||
|
fontFamily: SCHRIFT,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: (26 + t * 26) * s,
|
||||||
|
color: '#6d7fb0',
|
||||||
|
opacity: Math.sin(t * Math.PI) * aus,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
z
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Funkeln (Idee, Vorfreude): vier kleine Sterne um (x, y).
|
||||||
|
export const Funkeln: React.FC<{x: number; y: number; start: number; dauer?: number; s?: number}> = ({x, y, start, dauer = 40, s = 1}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const t = (frame - start) / dauer;
|
||||||
|
if (t < 0 || t > 1) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{[0, 1, 2, 3].map((i) => {
|
||||||
|
const w = (i / 4) * Math.PI * 2 + 0.4;
|
||||||
|
const d = (40 + 50 * t) * s;
|
||||||
|
const g = 30 * s * Math.sin(Math.PI * Math.min(1, t * 1.4));
|
||||||
|
return (
|
||||||
|
<svg key={i} width={g * 2} height={g * 2} viewBox="-10 -10 20 20" style={{position: 'absolute', left: x + Math.cos(w) * d - g, top: y + Math.sin(w) * d - g}}>
|
||||||
|
<path d="M0,-10 Q1.5,-1.5 10,0 Q1.5,1.5 0,10 Q-1.5,1.5 -10,0 Q-1.5,-1.5 0,-10 Z" fill="#ffd45e" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Großer Comic-Text (Hatschi!, Zahlen beim Zählen).
|
||||||
|
export const Ruf: React.FC<{text: string; x: number; y: number; start: number; dauer?: number; groesse?: number; farbe?: string}> = ({
|
||||||
|
text,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
start,
|
||||||
|
dauer = 30,
|
||||||
|
groesse = 120,
|
||||||
|
farbe = '#e07a3c',
|
||||||
|
}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const t = (frame - start) / dauer;
|
||||||
|
if (t < 0 || t > 1) return null;
|
||||||
|
const auf = Math.min(1, t * 6);
|
||||||
|
const s = 0.4 + 0.6 * auf + 0.12 * Math.sin(Math.min(1, t * 3) * Math.PI);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
transform: `translate(-50%, -50%) scale(${s})`,
|
||||||
|
fontFamily: SCHRIFT,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: groesse,
|
||||||
|
color: farbe,
|
||||||
|
opacity: t > 0.8 ? (1 - t) / 0.2 : 1,
|
||||||
|
WebkitTextStroke: '6px #fff8ea',
|
||||||
|
paintOrder: 'stroke fill',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
372
remotion/src/folge02/orte.tsx
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {AbsoluteFill, Img, random, staticFile} from 'remotion';
|
||||||
|
import './untertitel'; // lädt die Schrift für den Kalender
|
||||||
|
|
||||||
|
// Orte der Folge 2 in Aquarell-Optik. Werden einmal als PNG vorberechnet
|
||||||
|
// (Kompositionen „F2Ort…“, siehe README) und in den Szenen als Bild benutzt.
|
||||||
|
|
||||||
|
const B = 1920;
|
||||||
|
const H = 1080;
|
||||||
|
|
||||||
|
const Filter: React.FC = () => (
|
||||||
|
<defs>
|
||||||
|
<filter id="aq" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="2" seed="7" result="r" />
|
||||||
|
<feDisplacementMap in="SourceGraphic" in2="r" scale="10" xChannelSelector="R" yChannelSelector="G" result="rand" />
|
||||||
|
<feMorphology in="rand" operator="erode" radius="4" result="innen" />
|
||||||
|
<feGaussianBlur in="innen" stdDeviation="5" result="iw" />
|
||||||
|
<feComposite in="rand" in2="iw" operator="out" result="saum" />
|
||||||
|
<feColorMatrix in="saum" type="matrix" values="0.75 0 0 0 0 0 0.72 0 0 0 0 0 0.7 0 0 0 0 0 0.45 0" result="sd" />
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.004 0.007" numOctaves="2" seed="11" result="fl" />
|
||||||
|
<feColorMatrix in="fl" type="matrix" values="0 0 0 0 1 0 0 0 0 0.97 0 0 0 0 0.9 0 0 0 -0.4 0.2" result="hell" />
|
||||||
|
<feComposite in="hell" in2="rand" operator="in" result="wolken" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="rand" />
|
||||||
|
<feMergeNode in="wolken" />
|
||||||
|
<feMergeNode in="sd" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<filter id="stift">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="2" seed="2" result="r" />
|
||||||
|
<feDisplacementMap in="SourceGraphic" in2="r" scale="4" xChannelSelector="R" yChannelSelector="G" />
|
||||||
|
</filter>
|
||||||
|
<filter id="papierkorn">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" seed="3" />
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.5 0 0 0 0 0.45 0 0 0 0 0.4 0 0 0 0.5 0" />
|
||||||
|
</filter>
|
||||||
|
<filter id="weich6">
|
||||||
|
<feGaussianBlur stdDeviation="6" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Korn: React.FC<{staerke?: number}> = ({staerke = 0.18}) => (
|
||||||
|
<rect width={B} height={H} filter="url(#papierkorn)" style={{mixBlendMode: 'multiply'}} opacity={staerke} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kahler Ast als Pinselstrich (rekursiv verzweigt).
|
||||||
|
const Ast: React.FC<{x: number; y: number; w: number; l: number; d: number; tiefe: number; farbe: string; seed: string}> = ({x, y, w, l, d, tiefe, farbe, seed}) => {
|
||||||
|
const rad = (w * Math.PI) / 180;
|
||||||
|
const x2 = x + Math.cos(rad) * l;
|
||||||
|
const y2 = y + Math.sin(rad) * l;
|
||||||
|
const knick = (random(seed + 'k') - 0.5) * l * 0.25;
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d={`M ${x} ${y} Q ${(x + x2) / 2 + knick} ${(y + y2) / 2 - knick} ${x2} ${y2}`}
|
||||||
|
stroke={farbe}
|
||||||
|
strokeWidth={d}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
{tiefe > 0 &&
|
||||||
|
[-1, 1, 0].slice(0, tiefe > 2 ? 3 : 2).map((s, i) => (
|
||||||
|
<Ast
|
||||||
|
key={i}
|
||||||
|
x={x2}
|
||||||
|
y={y2}
|
||||||
|
w={w + s * (18 + random(seed + i) * 22)}
|
||||||
|
l={l * (0.62 + random(seed + 'l' + i) * 0.15)}
|
||||||
|
d={d * 0.62}
|
||||||
|
tiefe={tiefe - 1}
|
||||||
|
farbe={farbe}
|
||||||
|
seed={seed + i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Apfelbaum: React.FC<{x: number; y: number; s?: number; farbe: string; blaetter?: boolean}> = ({x, y, s = 1, farbe, blaetter = true}) => (
|
||||||
|
<g transform={`translate(${x} ${y}) scale(${s})`}>
|
||||||
|
<path d="M -34 0 C -26 -120, -30 -260, -14 -380 L 14 -380 C 26 -260, 22 -120, 38 0 Z" fill={farbe} filter="url(#aq)" />
|
||||||
|
<Ast x={0} y={-370} w={-90} l={150} d={22} tiefe={4} farbe={farbe} seed="a" />
|
||||||
|
<Ast x={-6} y={-330} w={-150} l={170} d={20} tiefe={4} farbe={farbe} seed="b" />
|
||||||
|
<Ast x={6} y={-320} w={-35} l={180} d={20} tiefe={4} farbe={farbe} seed="c" />
|
||||||
|
<Ast x={0} y={-250} w={-175} l={120} d={14} tiefe={3} farbe={farbe} seed="d" />
|
||||||
|
<Ast x={4} y={-240} w={-10} l={130} d={14} tiefe={3} farbe={farbe} seed="e" />
|
||||||
|
{blaetter &&
|
||||||
|
new Array(9).fill(0).map((_, i) => {
|
||||||
|
const bx = -260 + random('bl' + i) * 520;
|
||||||
|
const by = -560 + random('bly' + i) * 240;
|
||||||
|
return <ellipse key={i} cx={bx} cy={by} rx={11} ry={7} transform={`rotate(${random('br' + i) * 180} ${bx} ${by})`} fill={['#c8642c', '#d99a2b', '#a8452a'][i % 3]} />;
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Zaun: React.FC<{y: number; farbe: string; dunkel: string}> = ({y, farbe, dunkel}) => (
|
||||||
|
<g filter="url(#aq)">
|
||||||
|
<rect x={-20} y={y + 30} width={B + 40} height={16} fill={dunkel} />
|
||||||
|
<rect x={-20} y={y + 95} width={B + 40} height={16} fill={dunkel} />
|
||||||
|
{new Array(26).fill(0).map((_, i) => {
|
||||||
|
const x = -10 + i * 78 + (random('z' + i) - 0.5) * 6;
|
||||||
|
const h = 150 + random('zh' + i) * 14;
|
||||||
|
return <path key={i} d={`M ${x} ${y + 160} L ${x} ${y + 160 - h + 18} L ${x + 22} ${y + 160 - h} L ${x + 44} ${y + 160 - h + 18} L ${x + 44} ${y + 160} Z`} fill={farbe} />;
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Holzstapel: React.FC<{x: number; y: number; nacht?: boolean}> = ({x, y, nacht}) => {
|
||||||
|
const holz = nacht ? '#5b4a52' : '#a67a52';
|
||||||
|
const ring = nacht ? '#7a6670' : '#d9b48a';
|
||||||
|
const reihen = [7, 6, 5, 4];
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${x} ${y})`}>
|
||||||
|
<rect x={-20} y={-300} width={480} height={300} fill={nacht ? '#3e3644' : '#7d5c3e'} filter="url(#aq)" />
|
||||||
|
{reihen.map((n, r) =>
|
||||||
|
new Array(n).fill(0).map((_, i) => {
|
||||||
|
const cx = 34 + i * 64 + r * 32 + (random(`h${r}${i}`) - 0.5) * 6;
|
||||||
|
const cy = -34 - r * 60;
|
||||||
|
const rr = 30 + random(`hr${r}${i}`) * 4;
|
||||||
|
return (
|
||||||
|
<g key={`${r}${i}`}>
|
||||||
|
<circle cx={cx} cy={cy} r={rr} fill={holz} />
|
||||||
|
<circle cx={cx} cy={cy} r={rr * 0.78} fill={ring} />
|
||||||
|
<circle cx={cx} cy={cy} r={rr * 0.45} fill="none" stroke={holz} strokeWidth={2} opacity={0.6} />
|
||||||
|
<circle cx={cx} cy={cy} r={rr * 0.18} fill={holz} opacity={0.6} />
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
{/* Dach aus einem Brett */}
|
||||||
|
<path d="M -40 -262 L 470 -262 L 450 -240 L -20 -240 Z" fill={nacht ? '#4a3e48' : '#8a6544'} filter="url(#aq)" />
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wiese: React.FC<{nacht?: boolean; laub?: number}> = ({nacht, laub = 40}) => (
|
||||||
|
<g>
|
||||||
|
<path d="M -50 790 C 400 760, 900 780, 1300 770 S 1800 760, 1990 780 L 1990 1100 L -50 1100 Z" fill={nacht ? '#2f4148' : '#a3a873'} filter="url(#aq)" />
|
||||||
|
<path d="M -50 900 C 500 880, 1100 900, 1990 880 L 1990 1100 L -50 1100 Z" fill={nacht ? '#283840' : '#959b66'} filter="url(#aq)" opacity={0.8} />
|
||||||
|
{new Array(laub).fill(0).map((_, i) => {
|
||||||
|
const bx = random('wl' + i) * B;
|
||||||
|
const by = 820 + random('wly' + i) * 250;
|
||||||
|
return (
|
||||||
|
<ellipse
|
||||||
|
key={i}
|
||||||
|
cx={bx}
|
||||||
|
cy={by}
|
||||||
|
rx={13 + random('wr' + i) * 6}
|
||||||
|
ry={6}
|
||||||
|
transform={`rotate(${random('wd' + i) * 50 - 25} ${bx} ${by})`}
|
||||||
|
fill={nacht ? '#4a4048' : ['#c8642c', '#d99a2b', '#a8452a', '#8f5a2e'][i % 4]}
|
||||||
|
opacity={0.85}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GartenSVG: React.FC<{nacht?: boolean}> = ({nacht = false}) => (
|
||||||
|
<svg viewBox={`0 0 ${B} ${H}`} style={{position: 'absolute', inset: 0, width: '100%', height: '100%'}}>
|
||||||
|
<Filter />
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="himmelG" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
{nacht ? (
|
||||||
|
<>
|
||||||
|
<stop offset="0" stopColor="#1d2547" />
|
||||||
|
<stop offset="0.7" stopColor="#36406b" />
|
||||||
|
<stop offset="1" stopColor="#4f5a80" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<stop offset="0" stopColor="#c4ccd3" />
|
||||||
|
<stop offset="0.6" stopColor="#dfdcd2" />
|
||||||
|
<stop offset="1" stopColor="#efe6d4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width={B} height={H} fill="url(#himmelG)" />
|
||||||
|
{nacht &&
|
||||||
|
new Array(50).fill(0).map((_, i) => (
|
||||||
|
<circle key={i} cx={random('st' + i) * B} cy={random('sty' + i) * 450} r={1.2 + random('str' + i) * 2.2} fill="#fffbe8" opacity={0.5 + random('sto' + i) * 0.5} />
|
||||||
|
))}
|
||||||
|
{nacht && <circle cx={1580} cy={170} r={54} fill="#fbf1d2" filter="url(#aq)" />}
|
||||||
|
{/* ferne Hügel */}
|
||||||
|
<path d="M -50 640 C 300 560, 700 600, 1000 620 S 1600 560, 1990 600 L 1990 900 L -50 900 Z" fill={nacht ? '#3a4566' : '#bcc2bc'} filter="url(#aq)" />
|
||||||
|
<path d="M -50 700 C 400 650, 800 690, 1200 680 S 1700 650, 1990 690 L 1990 900 L -50 900 Z" fill={nacht ? '#34405a' : '#aab3a2'} filter="url(#aq)" />
|
||||||
|
{/* ferne kahle Bäume */}
|
||||||
|
{[160, 820, 1000, 1760].map((x, i) => (
|
||||||
|
<Ast key={i} x={x} y={690} w={-90} l={70} d={7} tiefe={3} farbe={nacht ? '#2c3550' : '#8f8f86'} seed={'f' + i} />
|
||||||
|
))}
|
||||||
|
<Zaun y={600} farbe={nacht ? '#4a4a5c' : '#c9a57a'} dunkel={nacht ? '#3a3a4c' : '#a8835a'} />
|
||||||
|
<Holzstapel x={1260} y={880} nacht={nacht} />
|
||||||
|
<Wiese nacht={nacht} />
|
||||||
|
<Apfelbaum x={330} y={900} s={1.05} farbe={nacht ? '#2e2a36' : '#6e5038'} blaetter={!nacht} />
|
||||||
|
<Korn staerke={nacht ? 0.12 : 0.2} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Blick von oben über den Zaun: kahle Baumkronen, weites Land, grauer Himmel.
|
||||||
|
export const ObenSVG: React.FC = () => (
|
||||||
|
<svg viewBox={`0 0 ${B} ${H}`} style={{position: 'absolute', inset: 0, width: '100%', height: '100%'}}>
|
||||||
|
<Filter />
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="himmelO" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stopColor="#b9c3cc" />
|
||||||
|
<stop offset="1" stopColor="#e8e2d6" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width={B} height={H} fill="url(#himmelO)" />
|
||||||
|
<path d="M -50 560 C 400 520, 900 540, 1300 530 S 1800 520, 1990 540 L 1990 1100 L -50 1100 Z" fill="#c2c6b6" filter="url(#aq)" />
|
||||||
|
<path d="M -50 640 C 500 600, 1000 630, 1990 610 L 1990 1100 L -50 1100 Z" fill="#b1b79c" filter="url(#aq)" />
|
||||||
|
{/* Felder */}
|
||||||
|
<path d="M 300 700 L 900 660 L 1100 760 L 420 820 Z" fill="#c9b98e" filter="url(#aq)" opacity={0.8} />
|
||||||
|
<path d="M 1100 660 L 1700 640 L 1820 740 L 1150 760 Z" fill="#a9a57c" filter="url(#aq)" opacity={0.8} />
|
||||||
|
{new Array(9).fill(0).map((_, i) => (
|
||||||
|
<Ast key={i} x={80 + i * 220 + random('ob' + i) * 60} y={1120} w={-90 + (random('obw' + i) - 0.5) * 20} l={170 + random('obl' + i) * 60} d={18} tiefe={4} farbe="#6e5a48" seed={'o' + i} />
|
||||||
|
))}
|
||||||
|
<rect x={-20} y={1010} width={B + 40} height={20} fill="#a8835a" filter="url(#aq)" />
|
||||||
|
<Korn />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Noas Zimmer am Abend: zwei Ebenen, damit Lulus Kopf im Fenster erscheinen kann.
|
||||||
|
const FENSTER = {x: 1180, y: 150, b: 520, h: 470};
|
||||||
|
|
||||||
|
export const ZimmerHimmelSVG: React.FC = () => (
|
||||||
|
<svg viewBox={`0 0 ${B} ${H}`} style={{position: 'absolute', inset: 0, width: '100%', height: '100%'}}>
|
||||||
|
<Filter />
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="abendZ" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stopColor="#27315a" />
|
||||||
|
<stop offset="1" stopColor="#6a6390" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width={B} height={H} fill="url(#abendZ)" />
|
||||||
|
<circle cx={1560} cy={250} r={40} fill="#fbf1d2" filter="url(#aq)" />
|
||||||
|
{new Array(18).fill(0).map((_, i) => (
|
||||||
|
<circle key={i} cx={FENSTER.x + random('zs' + i) * FENSTER.b} cy={FENSTER.y + random('zsy' + i) * 260} r={1.5 + random('zsr' + i) * 2} fill="#fffbe8" />
|
||||||
|
))}
|
||||||
|
<path d="M 1100 560 C 1300 520, 1500 540, 1800 530 L 1800 1100 L 1100 1100 Z" fill="#3a4566" filter="url(#aq)" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ZimmerWandSVG: React.FC = () => (
|
||||||
|
<svg viewBox={`0 0 ${B} ${H}`} style={{position: 'absolute', inset: 0, width: '100%', height: '100%'}}>
|
||||||
|
<Filter />
|
||||||
|
<defs>
|
||||||
|
<mask id="loch">
|
||||||
|
<rect width={B} height={H} fill="white" />
|
||||||
|
<rect x={FENSTER.x} y={FENSTER.y} width={FENSTER.b} height={FENSTER.h} rx={8} fill="black" />
|
||||||
|
</mask>
|
||||||
|
<radialGradient id="lampenlicht" cx="0.18" cy="0.35" r="0.75">
|
||||||
|
<stop offset="0" stopColor="#ffe2a8" stopOpacity="0.75" />
|
||||||
|
<stop offset="1" stopColor="#ffe2a8" stopOpacity="0" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<g mask="url(#loch)">
|
||||||
|
<rect width={B} height={H} fill="#e9cfa4" />
|
||||||
|
{/* Tapete mit kleinen Sternen */}
|
||||||
|
{new Array(70).fill(0).map((_, i) => (
|
||||||
|
<circle key={i} cx={(i % 10) * 200 + ((Math.floor(i / 10) % 2) * 100) + 40} cy={Math.floor(i / 10) * 120 + 60} r={5} fill="#d8b98a" />
|
||||||
|
))}
|
||||||
|
<rect width={B} height={H} fill="url(#lampenlicht)" />
|
||||||
|
{/* Boden */}
|
||||||
|
<path d="M -20 820 L 1940 820 L 1940 1100 L -20 1100 Z" fill="#b98a5a" filter="url(#aq)" />
|
||||||
|
{new Array(8).fill(0).map((_, i) => (
|
||||||
|
<line key={i} x1={-20} y1={850 + i * 32} x2={1940} y2={850 + i * 32} stroke="#a47549" strokeWidth={3} />
|
||||||
|
))}
|
||||||
|
{/* Fensterrahmen und Vorhänge */}
|
||||||
|
<rect x={FENSTER.x - 26} y={FENSTER.y - 26} width={FENSTER.b + 52} height={FENSTER.h + 52} rx={12} fill="#fbf3e4" filter="url(#aq)" />
|
||||||
|
<rect x={FENSTER.x - 50} y={FENSTER.y + FENSTER.h + 10} width={FENSTER.b + 100} height={30} rx={6} fill="#f1e2c8" filter="url(#aq)" />
|
||||||
|
<path d={`M ${FENSTER.x - 120} 90 C ${FENSTER.x - 80} 300, ${FENSTER.x - 110} 500, ${FENSTER.x - 60} 700 L ${FENSTER.x - 10} 700 C ${FENSTER.x - 40} 500, ${FENSTER.x - 20} 300, ${FENSTER.x - 10} 90 Z`} fill="#c66b5a" filter="url(#aq)" />
|
||||||
|
<path d={`M ${FENSTER.x + FENSTER.b + 120} 90 C ${FENSTER.x + FENSTER.b + 80} 300, ${FENSTER.x + FENSTER.b + 110} 500, ${FENSTER.x + FENSTER.b + 60} 700 L ${FENSTER.x + FENSTER.b + 10} 700 C ${FENSTER.x + FENSTER.b + 40} 500, ${FENSTER.x + FENSTER.b + 20} 300, ${FENSTER.x + FENSTER.b + 10} 90 Z`} fill="#c66b5a" filter="url(#aq)" />
|
||||||
|
<rect x={FENSTER.x - 150} y={80} width={FENSTER.b + 300} height={18} rx={9} fill="#8a6544" />
|
||||||
|
{/* Stehlampe links */}
|
||||||
|
<rect x={236} y={330} width={12} height={500} fill="#6e5038" />
|
||||||
|
<ellipse cx={242} cy={828} rx={70} ry={14} fill="#6e5038" />
|
||||||
|
<path d="M 160 330 L 324 330 L 290 200 L 194 200 Z" fill="#f6d58e" filter="url(#aq)" />
|
||||||
|
{/* Bild an der Wand */}
|
||||||
|
<rect x={560} y={210} width={240} height={180} rx={6} fill="#fbf3e4" stroke="#a47549" strokeWidth={10} />
|
||||||
|
<path d="M 590 360 C 640 300, 700 340, 770 290" stroke="#5f8a4a" strokeWidth={8} fill="none" />
|
||||||
|
<circle cx={720} cy={260} r={22} fill="#e0b04a" />
|
||||||
|
</g>
|
||||||
|
{/* Fensterkreuz */}
|
||||||
|
<rect x={FENSTER.x + FENSTER.b / 2 - 7} y={FENSTER.y} width={14} height={FENSTER.h} fill="#fbf3e4" />
|
||||||
|
<rect x={FENSTER.x} y={FENSTER.y + 150} width={FENSTER.b / 2} height={14} fill="#fbf3e4" />
|
||||||
|
<Korn staerke={0.14} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Niedriger Tisch (vor Noa) – eigene Ebene, damit Noas Beine dahinter verschwinden.
|
||||||
|
export const TischSVG: React.FC = () => (
|
||||||
|
<svg viewBox={`0 0 ${B} ${H}`} style={{position: 'absolute', inset: 0, width: '100%', height: '100%'}}>
|
||||||
|
<Filter />
|
||||||
|
<path d="M 420 780 L 1260 780 L 1300 830 L 380 830 Z" fill="#c99a63" filter="url(#aq)" />
|
||||||
|
<rect x={400} y={828} width={880} height={36} fill="#a87a4a" filter="url(#aq)" />
|
||||||
|
<rect x={440} y={860} width={34} height={150} fill="#8a6544" />
|
||||||
|
<rect x={1206} y={860} width={34} height={150} fill="#8a6544" />
|
||||||
|
{/* Papierbogen und Buntstifte */}
|
||||||
|
<path d="M 560 790 L 900 784 L 930 822 L 540 828 Z" fill="#fbf6ea" />
|
||||||
|
{['#d9534f', '#e0b04a', '#5f8a4a', '#3f5a8a'].map((f, i) => (
|
||||||
|
<rect key={i} x={950 + i * 26} y={800 - i * 3} width={80} height={10} rx={4} fill={f} transform={`rotate(${-8 + i * 5} ${990 + i * 26} ${805})`} />
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Der gemalte Kalender (Nahaufnahme von oben).
|
||||||
|
export const KalenderSVG: React.FC = () => {
|
||||||
|
const farben = ['#d9534f', '#e0a020', '#5f8a4a', '#3f5a8a', '#a0526b', '#c8642c'];
|
||||||
|
const nummern = [7, 15, 2, 21, 11, 18, 4, 23, 9, 13, 1, 16, 20, 6, 24, 12, 3, 17, 10, 22, 5, 14, 19, 8];
|
||||||
|
return (
|
||||||
|
<svg viewBox={`0 0 ${B} ${H}`} style={{position: 'absolute', inset: 0, width: '100%', height: '100%'}}>
|
||||||
|
<Filter />
|
||||||
|
<rect width={B} height={H} fill="#c99a63" />
|
||||||
|
{new Array(12).fill(0).map((_, i) => (
|
||||||
|
<path key={i} d={`M -20 ${i * 95 + 30} C 600 ${i * 95 + 10}, 1300 ${i * 95 + 50}, 1940 ${i * 95 + 25}`} stroke="#b88a55" strokeWidth={4} fill="none" />
|
||||||
|
))}
|
||||||
|
<g transform="rotate(-2 960 540)">
|
||||||
|
<rect x={250} y={90} width={1420} height={900} fill="#fdf8ec" filter="url(#aq)" />
|
||||||
|
<g filter="url(#stift)">
|
||||||
|
<text x={960} y={190} textAnchor="middle" fontSize={78} fontFamily="Fredoka, sans-serif" fontWeight={600} fill="#c8642c">
|
||||||
|
Pinos Winterfest-Kalender
|
||||||
|
</text>
|
||||||
|
{nummern.map((n, i) => {
|
||||||
|
const x = 330 + (i % 6) * 215;
|
||||||
|
const y = 250 + Math.floor(i / 6) * 180;
|
||||||
|
const f = farben[i % farben.length];
|
||||||
|
return (
|
||||||
|
<g key={i}>
|
||||||
|
<rect x={x} y={y} width={180} height={150} rx={14} fill="none" stroke={f} strokeWidth={7} />
|
||||||
|
<circle cx={x + 158} cy={y + 75} r={7} fill={f} />
|
||||||
|
<text x={x + 82} y={y + 102} textAnchor="middle" fontSize={74} fontFamily="Fredoka, sans-serif" fontWeight={600} fill={f}>
|
||||||
|
{n}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* kleine Kritzeleien */}
|
||||||
|
<path d="M 300 150 l 10 25 l 26 2 l -20 16 l 7 26 l -23 -14 l -23 14 l 7 -26 l -20 -16 l 26 -2 Z" fill="#e0b04a" />
|
||||||
|
<path d="M 1610 140 l 10 25 l 26 2 l -20 16 l 7 26 l -23 -14 l -23 14 l 7 -26 l -20 -16 l 26 -2 Z" fill="#e0b04a" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<Korn staerke={0.15} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const KALENDER_TUER1 = {x: 330 + 4 * 215 + 90, y: 250 + 1 * 180 + 75}; // Türchen „1“ (Index 10)
|
||||||
|
|
||||||
|
// Vorberechnete Bilder einbinden
|
||||||
|
export const Ort: React.FC<{name: string}> = ({name}) => <Img src={staticFile(`folge02/orte/${name}.webp`)} style={{position: 'absolute', inset: 0, width: B, height: H}} />;
|
||||||
|
|
||||||
|
// Kompositionen zum Vorberechnen
|
||||||
|
export const ORTE: Record<string, React.FC> = {
|
||||||
|
garten: () => <GartenSVG />,
|
||||||
|
garten_nacht: () => <GartenSVG nacht />,
|
||||||
|
oben: () => <ObenSVG />,
|
||||||
|
zimmer_himmel: () => <ZimmerHimmelSVG />,
|
||||||
|
zimmer_wand: () => <ZimmerWandSVG />,
|
||||||
|
zimmer_tisch: () => <TischSVG />,
|
||||||
|
kalender: () => <KalenderSVG />,
|
||||||
|
};
|
||||||
|
export const OrtStill: React.FC<{name: string}> = ({name}) => {
|
||||||
|
const K = ORTE[name];
|
||||||
|
return (
|
||||||
|
<AbsoluteFill>
|
||||||
|
<K />
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export {FENSTER};
|
||||||
942
remotion/src/folge02/posen.ts
Normal file
|
|
@ -0,0 +1,942 @@
|
||||||
|
// Automatisch erzeugt von werkzeuge/posen.py – nicht von Hand ändern.
|
||||||
|
// Koordinaten in der Bild-Leinwand der Figur (1376 × 768).
|
||||||
|
|
||||||
|
export type Pose = {figur: string; datei: string; x: number; y: number; b: number; h: number; box: number[]};
|
||||||
|
export type ClipDaten = {figur: string; datei: string; x: number; y: number; b: number; h: number; frames: number};
|
||||||
|
|
||||||
|
export const FIGUREN = {
|
||||||
|
"pino": {
|
||||||
|
"refHoehe": 571,
|
||||||
|
"fussX": 633.5,
|
||||||
|
"fussY": 679
|
||||||
|
},
|
||||||
|
"noa": {
|
||||||
|
"refHoehe": 535,
|
||||||
|
"fussX": 675.5,
|
||||||
|
"fussY": 700
|
||||||
|
},
|
||||||
|
"lulu": {
|
||||||
|
"refHoehe": 700,
|
||||||
|
"fussX": 714.0,
|
||||||
|
"fussY": 734
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const POSEN: Record<string, Pose> = {
|
||||||
|
"pino_A1_augen-zu": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A1_augen-zu.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
399,
|
||||||
|
105,
|
||||||
|
883,
|
||||||
|
682
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A2_augen-halb": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A2_augen-halb.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
399,
|
||||||
|
105,
|
||||||
|
883,
|
||||||
|
684
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A3_lachen": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A3_lachen.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
398,
|
||||||
|
105,
|
||||||
|
880,
|
||||||
|
682
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A4_staunen": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A4_staunen.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
398,
|
||||||
|
106,
|
||||||
|
876,
|
||||||
|
681
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A5_blick-links": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A5_blick-links.webp",
|
||||||
|
"x": 297.5,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
493,
|
||||||
|
99,
|
||||||
|
976,
|
||||||
|
683
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A6_blick-rechts": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A6_blick-rechts.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
399,
|
||||||
|
99,
|
||||||
|
883,
|
||||||
|
683
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A7-1_gehen": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A7-1_gehen.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
482,
|
||||||
|
75,
|
||||||
|
940,
|
||||||
|
683
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A7-2_gehen": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A7-2_gehen.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
473,
|
||||||
|
73,
|
||||||
|
934,
|
||||||
|
685
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A7-3_gehen": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A7-3_gehen.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
475,
|
||||||
|
74,
|
||||||
|
934,
|
||||||
|
685
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A7-4_gehen": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A7-4_gehen.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
461,
|
||||||
|
74,
|
||||||
|
932,
|
||||||
|
686
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A8_rueckansicht": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A8_rueckansicht.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
461,
|
||||||
|
79,
|
||||||
|
921,
|
||||||
|
686
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A_hinterbeine": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A_hinterbeine.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
462,
|
||||||
|
76,
|
||||||
|
914,
|
||||||
|
696
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A_kugel": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A_kugel.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
441,
|
||||||
|
106,
|
||||||
|
916,
|
||||||
|
653
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A_laufen-1": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A_laufen-1.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
423,
|
||||||
|
103,
|
||||||
|
979,
|
||||||
|
667
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A_laufen-2": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A_laufen-2.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
413,
|
||||||
|
100,
|
||||||
|
973,
|
||||||
|
669
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A_laufen-3": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A_laufen-3.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
418,
|
||||||
|
103,
|
||||||
|
973,
|
||||||
|
664
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_A_laufen-4": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_A_laufen-4.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
415,
|
||||||
|
102,
|
||||||
|
972,
|
||||||
|
666
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_B1_traurig": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_B1_traurig.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
399,
|
||||||
|
127,
|
||||||
|
891,
|
||||||
|
681
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_B2_erschrocken": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_B2_erschrocken.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
399,
|
||||||
|
104,
|
||||||
|
899,
|
||||||
|
681
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_B3_schlafen": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_B3_schlafen.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
399,
|
||||||
|
105,
|
||||||
|
883,
|
||||||
|
683
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_B_blick-nach-oben": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_B_blick-nach-oben.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
400,
|
||||||
|
110,
|
||||||
|
883,
|
||||||
|
684
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_B_schlaeft-eingerollt": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_B_schlaeft-eingerollt.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
402,
|
||||||
|
173,
|
||||||
|
1010,
|
||||||
|
651
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pino_ref_sitzend": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/figuren/pino_ref_sitzend.webp",
|
||||||
|
"x": 391,
|
||||||
|
"y": 69,
|
||||||
|
"b": 623,
|
||||||
|
"h": 631,
|
||||||
|
"box": [
|
||||||
|
395,
|
||||||
|
108,
|
||||||
|
872,
|
||||||
|
679
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A1_augen-zu": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A1_augen-zu.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
541,
|
||||||
|
164,
|
||||||
|
810,
|
||||||
|
702
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A2_augen-halb": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A2_augen-halb.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
541,
|
||||||
|
164,
|
||||||
|
810,
|
||||||
|
701
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A3_lachen": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A3_lachen.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
541,
|
||||||
|
164,
|
||||||
|
810,
|
||||||
|
701
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A4_staunen": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A4_staunen.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
541,
|
||||||
|
164,
|
||||||
|
809,
|
||||||
|
700
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A5_blick-links": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A5_blick-links.webp",
|
||||||
|
"x": 314.0,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
566,
|
||||||
|
164,
|
||||||
|
835,
|
||||||
|
701
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A6_blick-rechts": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A6_blick-rechts.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
541,
|
||||||
|
164,
|
||||||
|
810,
|
||||||
|
700
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A7-1_gehen": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A7-1_gehen.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
545,
|
||||||
|
166,
|
||||||
|
828,
|
||||||
|
697
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A7-2_gehen": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A7-2_gehen.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
526,
|
||||||
|
166,
|
||||||
|
839,
|
||||||
|
703
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A7-3_gehen": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A7-3_gehen.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
559,
|
||||||
|
165,
|
||||||
|
818,
|
||||||
|
692
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A7-4_gehen": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A7-4_gehen.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
547,
|
||||||
|
166,
|
||||||
|
823,
|
||||||
|
697
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A8_rueckansicht": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A8_rueckansicht.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
556,
|
||||||
|
168,
|
||||||
|
822,
|
||||||
|
699
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A_arm-hoch-winken": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A_arm-hoch-winken.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
544,
|
||||||
|
163,
|
||||||
|
860,
|
||||||
|
701
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A_arme-hoch": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A_arme-hoch.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
504,
|
||||||
|
163,
|
||||||
|
852,
|
||||||
|
701
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_A_laterne": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_A_laterne.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
343,
|
||||||
|
165,
|
||||||
|
835,
|
||||||
|
700
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_B1_traurig": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_B1_traurig.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
541,
|
||||||
|
170,
|
||||||
|
812,
|
||||||
|
700
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_B2_erschrocken": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_B2_erschrocken.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
546,
|
||||||
|
164,
|
||||||
|
814,
|
||||||
|
700
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_B3_schlafen": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_B3_schlafen.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
358,
|
||||||
|
267,
|
||||||
|
1050,
|
||||||
|
602
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_B_hocken": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_B_hocken.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
544,
|
||||||
|
160,
|
||||||
|
826,
|
||||||
|
703
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_B_sitzt": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_B_sitzt.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
536,
|
||||||
|
165,
|
||||||
|
956,
|
||||||
|
695
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_B_zeigen": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_B_zeigen.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
541,
|
||||||
|
165,
|
||||||
|
910,
|
||||||
|
700
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noa_ref_stehend": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/figuren/noa_ref_stehend.webp",
|
||||||
|
"x": 339,
|
||||||
|
"y": 156,
|
||||||
|
"b": 715,
|
||||||
|
"h": 551,
|
||||||
|
"box": [
|
||||||
|
541,
|
||||||
|
165,
|
||||||
|
810,
|
||||||
|
700
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A1_augen-zu": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A1_augen-zu.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
540,
|
||||||
|
27,
|
||||||
|
873,
|
||||||
|
731
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A2_augen-halb": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A2_augen-halb.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
540,
|
||||||
|
28,
|
||||||
|
872,
|
||||||
|
731
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A3_lachen": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A3_lachen.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
540,
|
||||||
|
27,
|
||||||
|
872,
|
||||||
|
735
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A4_staunen": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A4_staunen.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
540,
|
||||||
|
28,
|
||||||
|
872,
|
||||||
|
739
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A5_blick-links": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A5_blick-links.webp",
|
||||||
|
"x": 373.5,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
504,
|
||||||
|
34,
|
||||||
|
821,
|
||||||
|
734
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A6_blick-rechts": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A6_blick-rechts.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
555,
|
||||||
|
34,
|
||||||
|
873,
|
||||||
|
734
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A7-1_gehen": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A7-1_gehen.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
494,
|
||||||
|
36,
|
||||||
|
897,
|
||||||
|
734
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A7-2_gehen": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A7-2_gehen.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
506,
|
||||||
|
34,
|
||||||
|
890,
|
||||||
|
736
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A7-3_gehen": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A7-3_gehen.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
504,
|
||||||
|
33,
|
||||||
|
891,
|
||||||
|
738
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A7-4_gehen": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A7-4_gehen.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
512,
|
||||||
|
33,
|
||||||
|
931,
|
||||||
|
736
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A8_rueckansicht": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A8_rueckansicht.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
561,
|
||||||
|
37,
|
||||||
|
813,
|
||||||
|
755
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A_apfel": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A_apfel.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
540,
|
||||||
|
37,
|
||||||
|
872,
|
||||||
|
738
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_A_hals-unten": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_A_hals-unten.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
326,
|
||||||
|
143,
|
||||||
|
992,
|
||||||
|
737
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_B1_traurig": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_B1_traurig.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
494,
|
||||||
|
48,
|
||||||
|
873,
|
||||||
|
728
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_B2_erschrocken": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_B2_erschrocken.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
549,
|
||||||
|
32,
|
||||||
|
872,
|
||||||
|
730
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_B3_schlafen": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_B3_schlafen.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
407,
|
||||||
|
112,
|
||||||
|
1016,
|
||||||
|
724
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_B_kopf-profil": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_B_kopf-profil.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
509,
|
||||||
|
41,
|
||||||
|
851,
|
||||||
|
737
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lulu_B_liegt": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/figuren/lulu_B_liegt.webp",
|
||||||
|
"x": 322,
|
||||||
|
"y": 23,
|
||||||
|
"b": 698,
|
||||||
|
"h": 736,
|
||||||
|
"box": [
|
||||||
|
441,
|
||||||
|
49,
|
||||||
|
924,
|
||||||
|
723
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CLIPS: Record<string, ClipDaten> = {
|
||||||
|
"lulu_C9_hals-runter": {
|
||||||
|
"figur": "lulu",
|
||||||
|
"datei": "folge02/clips/lulu_C9_hals-runter.webm",
|
||||||
|
"x": 294.70404984423675,
|
||||||
|
"y": 24.64797507788162,
|
||||||
|
"b": 709.4330218068535,
|
||||||
|
"h": 715.8629283489096,
|
||||||
|
"frames": 121
|
||||||
|
},
|
||||||
|
"noa_C4_winken": {
|
||||||
|
"figur": "noa",
|
||||||
|
"datei": "folge02/clips/noa_C4_winken.webm",
|
||||||
|
"x": 529.3956386292834,
|
||||||
|
"y": 158.6043613707165,
|
||||||
|
"b": 413.6573208722741,
|
||||||
|
"h": 546.5420560747663,
|
||||||
|
"frames": 121
|
||||||
|
},
|
||||||
|
"pino_C3_freudensprung": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/clips/pino_C3_freudensprung.webm",
|
||||||
|
"x": 382.5794392523364,
|
||||||
|
"y": 0.0,
|
||||||
|
"b": 512.2492211838006,
|
||||||
|
"h": 681.5700934579438,
|
||||||
|
"frames": 121
|
||||||
|
},
|
||||||
|
"pino_C7_einrollen": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/clips/pino_C7_einrollen.webm",
|
||||||
|
"x": 390.08099688473516,
|
||||||
|
"y": 101.80685358255451,
|
||||||
|
"b": 552.9719626168223,
|
||||||
|
"h": 582.9781931464174,
|
||||||
|
"frames": 121
|
||||||
|
},
|
||||||
|
"pino_C8_aufrichten": {
|
||||||
|
"figur": "pino",
|
||||||
|
"datei": "folge02/clips/pino_C8_aufrichten.webm",
|
||||||
|
"x": 387.9376947040498,
|
||||||
|
"y": 62.155763239875384,
|
||||||
|
"b": 563.6884735202492,
|
||||||
|
"h": 636.5607476635513,
|
||||||
|
"frames": 121
|
||||||
|
}
|
||||||
|
};
|
||||||
128
remotion/src/folge02/szene2.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useCurrentFrame} from 'remotion';
|
||||||
|
import {kurve} from '../bewegung';
|
||||||
|
import {Figur, Kamera} from './figur';
|
||||||
|
import {Blende, Vignette, atmen, gehPose, gehWippen, hopfStauch, huepfer, kamFahrt, ruck} from './helfer';
|
||||||
|
import {Blaetterwind, Zzz} from './natur';
|
||||||
|
import {Ort} from './orte';
|
||||||
|
import {Untertitel} from './untertitel';
|
||||||
|
import {plane} from './zeit';
|
||||||
|
|
||||||
|
// Szene 2: Gähnen überall. Noas Garten am grauen Morgen.
|
||||||
|
|
||||||
|
export const BODEN = 935;
|
||||||
|
export const NOA_X = 760;
|
||||||
|
export const PINO_X = 1060;
|
||||||
|
|
||||||
|
export const S2 = plane([
|
||||||
|
{id: 'bild0', dauer: 2.5},
|
||||||
|
{id: 'e1', wer: 'ERZ', text: 'Es ist kalt geworden in Noas Garten.'},
|
||||||
|
{id: 'e2', wer: 'ERZ', text: 'Die Bäume haben fast alle Blätter verloren.'},
|
||||||
|
{id: 'bild1', dauer: 2.6},
|
||||||
|
{id: 'p1', wer: 'PINO', text: 'Hoaaah.', dauer: 2.2},
|
||||||
|
{id: 'n1', wer: 'NOA', text: 'Guten Morgen, Pino! Wollen wir spielen?'},
|
||||||
|
{id: 'p2', wer: 'PINO', text: 'Ja! Ich bin ganz wach. Gaaanz … wach …', dauer: 4.6},
|
||||||
|
{id: 'bild2', dauer: 3.2},
|
||||||
|
{id: 'n2', wer: 'NOA', text: 'Pino? Pino!', leise: true, dauer: 2.2},
|
||||||
|
{id: 'bild3', dauer: 1.0},
|
||||||
|
{id: 'p3', wer: 'PINO', text: 'Was? Ich hab nicht geschlafen!'},
|
||||||
|
{id: 'e3', wer: 'ERZ', text: 'Hat er doch.', dauer: 1.6},
|
||||||
|
{id: 'e4', wer: 'ERZ', text: 'Was meinst du: Warum ist Pino so müde?', pause: 3},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const Szene2: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const {z, dauer} = S2;
|
||||||
|
|
||||||
|
// Pino kommt von rechts getippelt und setzt sich
|
||||||
|
const pinoAn = z.bild1.ende + 6;
|
||||||
|
const pinoX = kurve(f, [
|
||||||
|
[z.bild1.start - 10, 1560],
|
||||||
|
[pinoAn, PINO_X],
|
||||||
|
]);
|
||||||
|
const pinoGeht = f < pinoAn;
|
||||||
|
const schreck = z.bild3.start;
|
||||||
|
const pinoPose: [number, string][] = [
|
||||||
|
[0, 'pino_A2_augen-halb'],
|
||||||
|
[z.p1.start + 4, 'pino_A4_staunen'],
|
||||||
|
[z.p1.ende - 10, 'pino_A2_augen-halb'],
|
||||||
|
[z.n1.start + 8, 'pino_A5_blick-links'],
|
||||||
|
[z.p2.start, 'pino_A3_lachen'],
|
||||||
|
[z.p2.start + 55, 'pino_A2_augen-halb'],
|
||||||
|
[z.p2.ende - 30, 'pino_A1_augen-zu'],
|
||||||
|
[schreck, 'pino_B2_erschrocken'],
|
||||||
|
[z.p3.start + 20, 'pino_ref_sitzend'],
|
||||||
|
[z.e3.start + 10, 'pino_A2_augen-halb'],
|
||||||
|
[z.e4.ende - 5, 'pino_A4_staunen'],
|
||||||
|
[z.e4.ende + 45, 'pino_A2_augen-halb'],
|
||||||
|
];
|
||||||
|
// Gähnen: strecken; Einschlafen: zusammensinken
|
||||||
|
const gaehn = (s: number) => kurve(f, [[s, 1], [s + 12, 1.07], [s + 45, 1.07], [s + 60, 1]]);
|
||||||
|
const sinken = kurve(f, [
|
||||||
|
[z.p2.ende - 40, 0],
|
||||||
|
[z.p2.ende + 10, 1],
|
||||||
|
[schreck, 1],
|
||||||
|
[schreck + 4, 0],
|
||||||
|
]);
|
||||||
|
const pinoStauchY =
|
||||||
|
(f > z.p1.start && f < z.p1.ende ? gaehn(z.p1.start) : 1) *
|
||||||
|
(f > z.e4.ende - 10 ? gaehn(z.e4.ende - 5) : 1) *
|
||||||
|
(1 - 0.05 * sinken) *
|
||||||
|
atmen(f, 1, sinken > 0.5 ? 0.02 : 0.008) *
|
||||||
|
hopfStauch(f, schreck);
|
||||||
|
const pinoDreh = sinken * 5 * (1 + 0.25 * Math.sin(f * 0.06)) + ruck(f, schreck, 4);
|
||||||
|
const pinoY = BODEN + huepfer(f, schreck, 36, 12) + (pinoGeht ? gehWippen(f) : 0);
|
||||||
|
|
||||||
|
// Noa kommt von links
|
||||||
|
const noaAn = z.n1.start - 4;
|
||||||
|
const noaX = kurve(f, [
|
||||||
|
[z.bild1.start + 10, -180],
|
||||||
|
[noaAn, NOA_X],
|
||||||
|
[z.n2.start, NOA_X],
|
||||||
|
[z.n2.start + 20, NOA_X + 60],
|
||||||
|
[z.p3.ende, NOA_X + 60],
|
||||||
|
[z.p3.ende + 30, NOA_X + 20],
|
||||||
|
]);
|
||||||
|
const noaGeht = f > z.bild1.start + 10 && f < noaAn;
|
||||||
|
const noaPose: [number, string][] = [
|
||||||
|
[0, 'noa_A6_blick-rechts'],
|
||||||
|
[z.n1.start, 'noa_A3_lachen'],
|
||||||
|
[z.p2.start + 20, 'noa_A6_blick-rechts'],
|
||||||
|
[z.bild2.start + 15, 'noa_A3_lachen'],
|
||||||
|
[z.n2.start, 'noa_A4_staunen'],
|
||||||
|
[z.p3.start + 10, 'noa_A3_lachen'],
|
||||||
|
[z.e4.start, 'noa_A6_blick-rechts'],
|
||||||
|
];
|
||||||
|
// Kichern: schnelles kleines Hüpfen
|
||||||
|
const kichern = f > z.bild2.start + 15 && f < z.bild2.ende ? -Math.abs(Math.sin(f * 0.5)) * 6 : 0;
|
||||||
|
const noaY = BODEN + (noaGeht ? gehWippen(f) : 0) + kichern + huepfer(f, z.n1.start, 18, 10);
|
||||||
|
|
||||||
|
const kam = kamFahrt(f, [
|
||||||
|
[0, 960, 540, 1.02],
|
||||||
|
[z.e2.ende, 900, 600, 1.15],
|
||||||
|
[z.bild1.start + 20, 1100, 700, 1.5],
|
||||||
|
[z.n1.start, 920, 740, 1.65],
|
||||||
|
[z.p2.start + 10, 1000, 790, 2.1],
|
||||||
|
[z.p2.ende, 1050, 820, 2.5],
|
||||||
|
[z.bild2.start + 30, 930, 770, 1.8],
|
||||||
|
[z.p3.start, 930, 770, 1.8],
|
||||||
|
[z.e4.start, 1040, 820, 2.3],
|
||||||
|
[dauer, 1050, 830, 2.5],
|
||||||
|
]);
|
||||||
|
kam.x += ruck(f, schreck, 6);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Kamera {...kam}>
|
||||||
|
<Ort name="garten" />
|
||||||
|
<Figur pose={noaGeht ? gehPose('noa', f) : noaPose} spiegeln={noaGeht} x={noaX} y={noaY} stauchY={atmen(f, 2)} />
|
||||||
|
<Figur pose={pinoGeht ? gehPose('pino', f, 8) : pinoPose} x={pinoX} y={pinoY} stauchY={pinoStauchY} drehung={pinoDreh} blende={4} />
|
||||||
|
<Zzz x={PINO_X + 40} y={BODEN - 150} start={z.p2.ende} ende={schreck} s={0.7} />
|
||||||
|
</Kamera>
|
||||||
|
<Blaetterwind anzahl={6} />
|
||||||
|
<Vignette />
|
||||||
|
<Untertitel zeilen={S2.liste} />
|
||||||
|
<Blende dauer={dauer} rein={12} raus={0} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
167
remotion/src/folge02/szene3.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useCurrentFrame} from 'remotion';
|
||||||
|
import {kurve} from '../bewegung';
|
||||||
|
import {ClipFigur, Figur, Kamera} from './figur';
|
||||||
|
import {Blende, Vignette, atmen, gehPose, gehWippen, hopfStauch, huepfer, kamFahrt} from './helfer';
|
||||||
|
import {Blaetterwind, Blatt, Voegel} from './natur';
|
||||||
|
import {Ort} from './orte';
|
||||||
|
import {BODEN, NOA_X, PINO_X} from './szene2';
|
||||||
|
import {Untertitel} from './untertitel';
|
||||||
|
import {plane} from './zeit';
|
||||||
|
|
||||||
|
// Szene 3: Lulu sieht es von oben. Winterschlaf wird erklärt, Pino ist traurig.
|
||||||
|
|
||||||
|
export const LULU_X = 1520;
|
||||||
|
|
||||||
|
export const S3 = plane([
|
||||||
|
{id: 'bild0', dauer: 3.2},
|
||||||
|
{id: 'bild1', dauer: 2.4},
|
||||||
|
{id: 'l1', wer: 'LULU', text: 'Guten Morgen, ihr zwei!'},
|
||||||
|
{id: 'l2', wer: 'LULU', text: 'Von hier oben sehe ich: Die Vögel fliegen weg, ganz weit in den Süden.'},
|
||||||
|
{id: 'l3', wer: 'LULU', text: 'Und die Bäume sind kahl. Der Winter kommt.'},
|
||||||
|
{id: 'e1', wer: 'ERZ', text: 'Wenn es kalt wird, finden Igel kaum noch etwas zu fressen.'},
|
||||||
|
{id: 'e2', wer: 'ERZ', text: 'Darum machen sie etwas ganz Besonderes. Sie schlafen.'},
|
||||||
|
{id: 'e3', wer: 'ERZ', text: 'Den ganzen Winter lang, bis zum Frühling. Das heißt Winterschlaf.'},
|
||||||
|
{id: 'p1', wer: 'PINO', text: 'Den ganzen Winter? Aber dann verpasse ich ja das Winterfest!'},
|
||||||
|
{id: 'bild2', dauer: 2.4},
|
||||||
|
{id: 'e4', wer: 'ERZ', text: 'Schau mal, Pinos Stacheln hängen ganz schlaff herunter.'},
|
||||||
|
{id: 'e5', wer: 'ERZ', text: 'Wie fühlt sich Pino jetzt?', pause: 3},
|
||||||
|
{id: 'e6', wer: 'ERZ', text: 'Ja. Pino ist traurig.'},
|
||||||
|
{id: 'bild3', dauer: 2.6},
|
||||||
|
{id: 'n1', wer: 'NOA', text: 'Ach, Pino. Bis zum Winterfest ist es doch noch ganz lange hin.'},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Das große Blatt, auf das Pino sich setzt
|
||||||
|
const GROSSES_BLATT_X = PINO_X + 50;
|
||||||
|
|
||||||
|
export const Szene3: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const {z, dauer} = S3;
|
||||||
|
|
||||||
|
// Lulu stakst von rechts herein, beugt dann den Hals herunter (Clip),
|
||||||
|
// hebt ihn für „von hier oben“ wieder (Clip rückwärts).
|
||||||
|
const luluAn = z.bild0.ende;
|
||||||
|
const luluX = kurve(f, [
|
||||||
|
[0, 2250],
|
||||||
|
[luluAn, LULU_X],
|
||||||
|
]);
|
||||||
|
const luluGeht = f < luluAn;
|
||||||
|
const halsRunter = z.bild1.start;
|
||||||
|
const halsHoch = z.l2.start - 10;
|
||||||
|
const obenStart = z.l2.start + 50;
|
||||||
|
const obenEnde = z.e1.ende + 5;
|
||||||
|
const imOben = f >= obenStart && f < obenEnde;
|
||||||
|
|
||||||
|
let lulu: React.ReactNode;
|
||||||
|
const luluY = BODEN - 10;
|
||||||
|
if (luluGeht) {
|
||||||
|
lulu = <Figur pose={gehPose('lulu', f, 5)} x={luluX} y={luluY + gehWippen(f, 5, 8)} />;
|
||||||
|
} else if (f < halsHoch) {
|
||||||
|
lulu = <ClipFigur name="lulu_C9_hals-runter" start={halsRunter} tempo={1.25} x={LULU_X} y={luluY} />;
|
||||||
|
} else if (f < halsHoch + 110) {
|
||||||
|
lulu = <ClipFigur name="lulu_C9_hals-runter" start={halsHoch} tempo={1.5} rueckwaerts x={LULU_X} y={luluY} />;
|
||||||
|
} else {
|
||||||
|
lulu = (
|
||||||
|
<Figur
|
||||||
|
pose={[
|
||||||
|
[0, 'lulu_A5_blick-links'],
|
||||||
|
[z.bild2.start, 'lulu_B1_traurig'],
|
||||||
|
[z.bild3.start, 'lulu_A5_blick-links'],
|
||||||
|
]}
|
||||||
|
x={LULU_X}
|
||||||
|
y={luluY}
|
||||||
|
stauchY={atmen(f, 3, 0.006)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pino
|
||||||
|
const pinoSchreck = z.p1.start;
|
||||||
|
const aufsBlatt = z.bild2.start + 10;
|
||||||
|
const pinoX = kurve(f, [
|
||||||
|
[aufsBlatt, PINO_X],
|
||||||
|
[aufsBlatt + 24, GROSSES_BLATT_X],
|
||||||
|
]);
|
||||||
|
const traurig = kurve(f, [
|
||||||
|
[aufsBlatt + 20, 0],
|
||||||
|
[aufsBlatt + 50, 1],
|
||||||
|
]);
|
||||||
|
const anlehnen = kurve(f, [
|
||||||
|
[z.bild3.start + 40, 0],
|
||||||
|
[z.bild3.start + 70, 1],
|
||||||
|
]);
|
||||||
|
const pinoPose: [number, string][] = [
|
||||||
|
[0, 'pino_A2_augen-halb'],
|
||||||
|
[z.bild0.start + 40, 'pino_A6_blick-rechts'],
|
||||||
|
[z.bild1.start + 20, 'pino_B_blick-nach-oben'],
|
||||||
|
[z.l1.start + 20, 'pino_A3_lachen'],
|
||||||
|
[z.l2.start, 'pino_B_blick-nach-oben'],
|
||||||
|
[z.e2.start, 'pino_A2_augen-halb'],
|
||||||
|
[z.e3.start + 30, 'pino_A4_staunen'],
|
||||||
|
[pinoSchreck, 'pino_B2_erschrocken'],
|
||||||
|
[aufsBlatt + 20, 'pino_B1_traurig'],
|
||||||
|
[z.bild3.start + 50, 'pino_A1_augen-zu'],
|
||||||
|
];
|
||||||
|
const pinoY = BODEN + huepfer(f, pinoSchreck, 34, 12) + huepfer(f, aufsBlatt, 14, 22) - (f > aufsBlatt + 20 ? 6 : 0);
|
||||||
|
const pinoStauchY = atmen(f, 1) * hopfStauch(f, pinoSchreck) * (1 - 0.06 * traurig + 0.06 * anlehnen * 0);
|
||||||
|
const pinoDreh = -9 * anlehnen + 3 * traurig * (1 - anlehnen);
|
||||||
|
|
||||||
|
// Noa kniet sich zu Pino
|
||||||
|
const knien = z.bild3.start + 10;
|
||||||
|
const noaX = kurve(f, [
|
||||||
|
[knien, NOA_X + 20],
|
||||||
|
[knien + 30, NOA_X + 140],
|
||||||
|
]);
|
||||||
|
const noaPose: [number, string][] = [
|
||||||
|
[0, 'noa_A6_blick-rechts'],
|
||||||
|
[z.bild1.start + 20, 'noa_A4_staunen'],
|
||||||
|
[z.l1.start + 15, 'noa_A3_lachen'],
|
||||||
|
[z.e1.start, 'noa_A6_blick-rechts'],
|
||||||
|
[z.bild2.start + 30, 'noa_B1_traurig'],
|
||||||
|
[knien + 18, 'noa_B_hocken'],
|
||||||
|
];
|
||||||
|
const noaStauch = kurve(f, [
|
||||||
|
[knien + 8, 1],
|
||||||
|
[knien + 18, 0.94],
|
||||||
|
[knien + 30, 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const kam = kamFahrt(f, [
|
||||||
|
[0, 1150, 820, 1.7],
|
||||||
|
[z.bild0.ende - 10, 1150, 820, 1.7],
|
||||||
|
[z.bild1.start + 30, 1150, 640, 1.15],
|
||||||
|
[z.l1.ende, 1120, 700, 1.3],
|
||||||
|
[z.l2.start, 1250, 520, 1.1],
|
||||||
|
[obenStart, 1300, 420, 1.2],
|
||||||
|
[obenEnde, 1050, 800, 2.2],
|
||||||
|
[z.e3.ende, 1060, 820, 2.4],
|
||||||
|
[z.p1.start + 10, 1050, 810, 2.1],
|
||||||
|
[z.bild2.start + 20, 1090, 820, 2.5],
|
||||||
|
[z.e6.ende, 1100, 830, 2.7],
|
||||||
|
[z.bild3.start + 30, 960, 790, 1.9],
|
||||||
|
[dauer, 950, 790, 2.0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{imOben ? (
|
||||||
|
<Kamera x={kurve(f, [[obenStart, 900], [obenEnde, 1060]])} y={540} zoom={1.05}>
|
||||||
|
<Ort name="oben" />
|
||||||
|
<Voegel start={obenStart - 40} skala={1.9} />
|
||||||
|
</Kamera>
|
||||||
|
) : (
|
||||||
|
<Kamera {...kam}>
|
||||||
|
<Ort name="garten" />
|
||||||
|
<Figur pose={noaPose} x={noaX} y={BODEN} stauchY={atmen(f, 2) * noaStauch} />
|
||||||
|
{f > aufsBlatt - 30 && <Blatt x={GROSSES_BLATT_X} y={BODEN - 8} r={88} s={2.6} farbe="#c8642c" deckkraft={Math.min(1, (f - aufsBlatt + 30) / 12)} />}
|
||||||
|
<Figur pose={pinoPose} x={pinoX} y={pinoY} stauchY={pinoStauchY} drehung={pinoDreh} blende={6} />
|
||||||
|
{lulu}
|
||||||
|
</Kamera>
|
||||||
|
)}
|
||||||
|
{!imOben && <Blaetterwind anzahl={4} seed="w3" />}
|
||||||
|
<Vignette />
|
||||||
|
<Untertitel zeilen={S3.liste} />
|
||||||
|
<Blende dauer={dauer} rein={0} raus={0} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
212
remotion/src/folge02/szene4.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useCurrentFrame} from 'remotion';
|
||||||
|
import {kurve} from '../bewegung';
|
||||||
|
import {Figur, Kamera} from './figur';
|
||||||
|
import {Blende, LULU_MAUL_LINKS, Vignette, atmen, gehPose, gehWippen, hopfStauch, huepfer, kamFahrt, punkt, ruck} from './helfer';
|
||||||
|
import {Blaetterregen, Blatt, LAUB, Nest, Ruf} from './natur';
|
||||||
|
import {Ort} from './orte';
|
||||||
|
import {BODEN, NOA_X, PINO_X} from './szene2';
|
||||||
|
import {Untertitel} from './untertitel';
|
||||||
|
import {plane} from './zeit';
|
||||||
|
|
||||||
|
// Szene 4: Ein Bett aus Blättern. Zählen bis 5, Lulu niest, das Nest entsteht.
|
||||||
|
|
||||||
|
export const NEST_X = 1480;
|
||||||
|
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([
|
||||||
|
{id: 'n1', wer: 'NOA', text: 'Wenn du so müde bist, brauchst du ein warmes Bett.'},
|
||||||
|
{id: 'e1', wer: 'ERZ', text: 'Igel bauen sich für den Winter ein Nest.'},
|
||||||
|
{id: 'e2', wer: 'ERZ', text: 'Aus ganz vielen Blättern, unter einer Hecke oder einem Holzstapel.'},
|
||||||
|
{id: 'e3', wer: 'ERZ', text: 'Da ist es trocken und warm.'},
|
||||||
|
{id: 'n2', wer: 'NOA', text: 'Zählst du mit?', pause: 0.6},
|
||||||
|
{id: 'c1', wer: 'NOA', text: 'Eins …', dauer: 1.1, pause: 0.5},
|
||||||
|
{id: 'c2', wer: 'NOA', text: 'Zwei …', dauer: 1.1, pause: 0.5},
|
||||||
|
{id: 'c3', wer: 'NOA', text: 'Drei …', dauer: 1.1, pause: 0.5},
|
||||||
|
{id: 'c4', wer: 'NOA', text: 'Vier …', dauer: 1.1, pause: 0.5},
|
||||||
|
{id: 'c5', wer: 'NOA', text: 'Fünf!', dauer: 1.4},
|
||||||
|
{id: 'bild1', dauer: 2.6},
|
||||||
|
{id: 'l1', wer: 'LULU', text: 'Hier oben hängen noch welche! Die erreicht sonst keiner.'},
|
||||||
|
{id: 'bild2', dauer: 2.4},
|
||||||
|
{id: 'bild3', dauer: 2.6},
|
||||||
|
{id: 'l2', wer: 'LULU', text: 'Oh! Entschuldigung!'},
|
||||||
|
{id: 'n3', wer: 'NOA', text: 'Macht nichts. Jetzt haben wir noch mehr Blätter.'},
|
||||||
|
{id: 'bild4', dauer: 4.6},
|
||||||
|
{id: 'bild5', dauer: 2.2},
|
||||||
|
{id: 'e4', wer: 'ERZ', text: 'Ein Igelnest braucht ganz viele Blätter.'},
|
||||||
|
{id: 'e5', wer: 'ERZ', text: 'Wenn ihr im Garten einen Laubhaufen liegen lasst, schenkt ihr einem Igel vielleicht ein Winterbettchen.'},
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 + 90;
|
||||||
|
const noaX = kurve(f, [
|
||||||
|
[noaGehtVon, NOA_X + 140],
|
||||||
|
[noaGehtBis, NOA_ZAEHL_X],
|
||||||
|
]);
|
||||||
|
const noaGeht = f > noaGehtVon && f < noaGehtBis;
|
||||||
|
const noaPose: [number, string][] = [
|
||||||
|
[0, 'noa_B_hocken'],
|
||||||
|
[z.n1.start + 30, 'noa_A3_lachen'],
|
||||||
|
[noaGehtBis, 'noa_A6_blick-rechts'],
|
||||||
|
[z.n2.start, 'noa_B_hocken'],
|
||||||
|
[z.c5.ende, 'noa_A3_lachen'],
|
||||||
|
[z.bild3.start + 4, 'noa_B2_erschrocken'],
|
||||||
|
[z.l2.ende, 'noa_A3_lachen'],
|
||||||
|
[zurueck, 'noa_B_hocken'],
|
||||||
|
[z.e4.start, 'noa_A3_lachen'],
|
||||||
|
];
|
||||||
|
// 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 schieben (vor und zurück)
|
||||||
|
const schieben = f > zurueck && f < z.bild4.ende ? Math.sin((f - zurueck) * 0.22) : 0;
|
||||||
|
|
||||||
|
// 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.bild4.ende - 20, NEST_X + 110],
|
||||||
|
[z.bild5.start, NEST_X],
|
||||||
|
]);
|
||||||
|
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;
|
||||||
|
const pinoPose: [number, string][] = [
|
||||||
|
[0, 'pino_A1_augen-zu'],
|
||||||
|
[z.n1.start + 20, 'pino_A2_augen-halb'],
|
||||||
|
[pinoGehtBis, 'pino_A5_blick-links'],
|
||||||
|
...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'],
|
||||||
|
[z.l1.start + 10, 'lulu_A3_lachen'],
|
||||||
|
[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 maul = punkt('lulu', LULU_BAUM_X, luluY, LULU_MAUL_LINKS.cx, LULU_MAUL_LINKS.cy);
|
||||||
|
const buendel = f > amBaum + 50 && f < niesen;
|
||||||
|
// Blätter fliegen vom Ast ins Maul
|
||||||
|
const zupfen = [0, 1, 2, 3].map((i) => {
|
||||||
|
const s = amBaum + 55 + i * 14;
|
||||||
|
const t = Math.min(1, Math.max(0, (f - s) / 16));
|
||||||
|
return {x: maul.x - 160 + i * 50 + (maul.x - (maul.x - 160 + i * 50)) * t, y: maul.y - 120 + 120 * t, sichtbar: f > s - 20 && t < 1};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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={atmen(f, 3, 0.006) * (1 + 0.02 * strecken)} drehung={zurueckLehnen * 4} />;
|
||||||
|
const luluBeimNest = f >= zurueck && <Figur pose="lulu_A_hals-unten" x={2030} y={BODEN - 4} drehung={schieben * 1.2} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Kamera {...kam}>
|
||||||
|
<Ort name="garten" />
|
||||||
|
{f >= amBaum && f < zurueck && lulu}
|
||||||
|
{buendel && (
|
||||||
|
<div style={{position: 'absolute', left: 0, top: 0}}>
|
||||||
|
{LAUB.slice(0, 5).map((c, i) => (
|
||||||
|
<Blatt key={i} x={maul.x - 6 + (i - 2) * 9} y={maul.y + 14 + Math.abs(i - 2) * 4} r={(i - 2) * 30 + 180} s={0.42} farbe={c} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{zupfen.map((b, i) => (b.sichtbar ? <Blatt key={i} x={b.x} y={b.y} r={f * 8 + i * 70} s={0.42} farbe={LAUB[i]} /> : null))}
|
||||||
|
|
||||||
|
{/* 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} />
|
||||||
|
<Figur pose={noaGeht ? gehPose('noa', f) : noaPose} spiegeln={noaGeht} x={noaX + schieben * 10} y={BODEN + (noaGeht ? gehWippen(f) : 0)} stauchY={atmen(f, 2) * (1 - 0.03 * beugen)} drehung={beugen * 3 + schieben * 2} />
|
||||||
|
{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>
|
||||||
|
) : (
|
||||||
|
<Figur
|
||||||
|
pose={pinoGeht ? gehPose('pino', f, 8) : pinoPose}
|
||||||
|
spiegeln={pinoSpiegel}
|
||||||
|
x={pinoX}
|
||||||
|
y={BODEN + zaehlHuepfer + huepfer(f, niesen + 2, 30, 12) + (pinoGeht ? gehWippen(f, 8, 4) : 0)}
|
||||||
|
stauchY={atmen(f, 1) * hopfStauch(f, niesen + 2)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<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="Hatschi!" x={1060} y={230} start={niesen} dauer={45} groesse={170} />
|
||||||
|
<Vignette />
|
||||||
|
<Untertitel zeilen={S4.liste} />
|
||||||
|
<Blende dauer={dauer} rein={0} raus={0} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
165
remotion/src/folge02/szene5.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useCurrentFrame} from 'remotion';
|
||||||
|
import {kurve} from '../bewegung';
|
||||||
|
import {ClipFigur, Figur, Kamera} from './figur';
|
||||||
|
import {Blende, Vignette, atmen, hopfStauch, huepfer, kamFahrt, ruck} from './helfer';
|
||||||
|
import {Funkeln, Nest} from './natur';
|
||||||
|
import {Ort} from './orte';
|
||||||
|
import {BODEN} from './szene2';
|
||||||
|
import {NEST_X} from './szene4';
|
||||||
|
import {SCHRIFT, Untertitel} from './untertitel';
|
||||||
|
import {plane} from './zeit';
|
||||||
|
|
||||||
|
// 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([
|
||||||
|
{id: 'bild1', dauer: 3.0},
|
||||||
|
{id: 'bild2', dauer: 2.2},
|
||||||
|
{id: 'p1', wer: 'PINO', text: 'Nein! Ich will nicht schlafen, bevor das Winterfest da ist!'},
|
||||||
|
{id: 'l1', wer: 'LULU', text: 'Aber Pino, du bist doch so müde!'},
|
||||||
|
{id: 'bild3', dauer: 3.0},
|
||||||
|
{id: 'e1', wer: 'ERZ', text: 'Hmm. Was können Noa und Lulu tun, damit Pino bis zum Winterfest wach bleibt?'},
|
||||||
|
{id: 'e2', wer: 'ERZ', text: 'Was meinst du?', pause: 3},
|
||||||
|
{id: 'bild4', dauer: 1.2},
|
||||||
|
{id: 'n1', wer: 'NOA', text: 'Ich hab eine Idee!'},
|
||||||
|
]);
|
||||||
|
|
||||||
|
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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
172
remotion/src/folge02/szene6.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useCurrentFrame} from 'remotion';
|
||||||
|
import {kurve} from '../bewegung';
|
||||||
|
import {ClipFigur, Figur, Kamera} from './figur';
|
||||||
|
import {Blende, Vignette, atmen, huepfer, kamFahrt} from './helfer';
|
||||||
|
import {Funkeln} from './natur';
|
||||||
|
import {KALENDER_TUER1, Ort} from './orte';
|
||||||
|
import {Untertitel} from './untertitel';
|
||||||
|
import {plane} from './zeit';
|
||||||
|
|
||||||
|
// Szene 6: Noas Idee. Abends in Noas Zimmer, Noa malt den Kalender.
|
||||||
|
|
||||||
|
const N = 420; // Noas Größe im Zimmer (näher an der Kamera)
|
||||||
|
const NOA_X6 = 800;
|
||||||
|
const PINO_X6 = 1090;
|
||||||
|
const TISCH_Y = 802;
|
||||||
|
const LULU_X6 = 1440;
|
||||||
|
|
||||||
|
export const S6 = plane([
|
||||||
|
{id: 'bild0', dauer: 3.0},
|
||||||
|
{id: 'l1', wer: 'LULU', text: 'Was malst du da, Noa?'},
|
||||||
|
{id: 'n1', wer: 'NOA', text: 'Einen Kalender! Mit 24 Türchen.'},
|
||||||
|
{id: 'n2', wer: 'NOA', text: 'Wir öffnen jeden Tag eins. Und hinter jedem Türchen steckt etwas, das Pino wach hält.'},
|
||||||
|
{id: 'p1', wer: 'PINO', text: 'Was denn?'},
|
||||||
|
{id: 'n3', wer: 'NOA', text: 'Etwas zum Entdecken, zum Basteln, zum Backen und zum Singen.'},
|
||||||
|
{id: 'n4', wer: 'NOA', text: 'Und wenn das letzte Türchen offen ist, ist Winterfest!'},
|
||||||
|
{id: 'bild1', dauer: 2.0},
|
||||||
|
{id: 'p2', wer: 'PINO', text: 'Meine Stacheln kribbeln!'},
|
||||||
|
{id: 'bild2', dauer: 4.2},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Kritzel auf dem Papier, wächst mit `t` (0…1)
|
||||||
|
const Kritzel: React.FC<{t: number}> = ({t}) => (
|
||||||
|
<svg viewBox="0 0 1920 1080" style={{position: 'absolute', inset: 0, width: 1920, height: 1080}}>
|
||||||
|
<path
|
||||||
|
d="M 600 806 l 30 0 l 0 14 l -30 0 z M 645 804 l 30 0 l 0 14 l -30 0 z M 690 803 l 30 0 l 0 14 l -30 0 z M 735 802 l 30 0 l 0 14 l -30 0 z M 780 801 l 30 0 l 0 14 l -30 0 z M 825 800 l 30 0 l 0 14 l -30 0 z"
|
||||||
|
stroke="#d9534f"
|
||||||
|
strokeWidth={3}
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={1000}
|
||||||
|
strokeDashoffset={1000 * (1 - t)}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Szene6: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const {z, dauer} = S6;
|
||||||
|
|
||||||
|
const malen = f < z.n1.ende ? Math.min(1, f / (z.n1.ende - 10)) : 1;
|
||||||
|
const stiftX = 600 + malen * 250 + Math.sin(f * 0.6) * 10;
|
||||||
|
const stiftY = 796 + Math.cos(f * 0.6) * 5;
|
||||||
|
const malt = f < z.n1.ende;
|
||||||
|
|
||||||
|
const luluX = kurve(f, [
|
||||||
|
[20, 1900],
|
||||||
|
[z.bild0.ende, LULU_X6],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sprung = z.bild1.start;
|
||||||
|
const imSprung = f >= sprung && f < sprung + 58;
|
||||||
|
const kribbeln = f > z.p2.start - 5 && f < z.p2.ende + 10;
|
||||||
|
const zitter = kribbeln ? Math.sin(f * 2.3) * 3 : 0;
|
||||||
|
|
||||||
|
const kalender = f >= z.bild2.start;
|
||||||
|
const tippen = [z.bild2.start + 45, z.bild2.start + 75];
|
||||||
|
const tippY = tippen.reduce((s, t) => s + Math.max(0, Math.sin(Math.min(1, Math.max(0, (f - t) / 14)) * Math.PI)) * 30, 0);
|
||||||
|
const leuchten = kalender ? 0.5 + 0.5 * Math.sin((f - z.bild2.start) * 0.15) : 0;
|
||||||
|
|
||||||
|
const kam = kamFahrt(f, [
|
||||||
|
[0, 960, 540, 1.0],
|
||||||
|
[z.bild0.ende, 1060, 560, 1.15],
|
||||||
|
[z.n1.start, 900, 640, 1.45],
|
||||||
|
[z.p1.start, 980, 650, 1.6],
|
||||||
|
[z.n4.ende, 960, 640, 1.5],
|
||||||
|
[sprung + 10, 1050, 700, 2.0],
|
||||||
|
[z.p2.ende, 1060, 710, 2.1],
|
||||||
|
]);
|
||||||
|
const kalKam = kamFahrt(f, [
|
||||||
|
[z.bild2.start, 960, 540, 1.0],
|
||||||
|
[dauer, KALENDER_TUER1.x - 40, KALENDER_TUER1.y, 1.6],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (kalender) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Kamera {...kalKam}>
|
||||||
|
<Ort name="kalender" />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: KALENDER_TUER1.x - 110,
|
||||||
|
top: KALENDER_TUER1.y - 95,
|
||||||
|
width: 220,
|
||||||
|
height: 190,
|
||||||
|
borderRadius: 26,
|
||||||
|
boxShadow: `0 0 ${30 + 30 * leuchten}px ${10 + 10 * leuchten}px rgba(255, 210, 90, ${0.35 + 0.35 * leuchten})`,
|
||||||
|
transform: 'rotate(-2deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Buntstift tippt auf Türchen 1 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: KALENDER_TUER1.x + 10,
|
||||||
|
top: KALENDER_TUER1.y - 330 - tippY + 30,
|
||||||
|
width: 36,
|
||||||
|
height: 300,
|
||||||
|
transform: 'rotate(18deg)',
|
||||||
|
transformOrigin: '50% 100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{position: 'absolute', bottom: 0, left: 6, width: 0, height: 0, borderLeft: '12px solid transparent', borderRight: '12px solid transparent', borderTop: '0', borderBottom: '40px solid #f1d2a6', transform: 'rotate(180deg)'}} />
|
||||||
|
<div style={{position: 'absolute', bottom: 40, left: 0, width: 36, height: 260, borderRadius: 6, background: 'linear-gradient(90deg, #b93f3b, #d9534f 40%, #b93f3b)'}} />
|
||||||
|
</div>
|
||||||
|
<Funkeln x={KALENDER_TUER1.x} y={KALENDER_TUER1.y} start={tippen[1] + 6} dauer={40} s={1.3} />
|
||||||
|
</Kamera>
|
||||||
|
<Vignette />
|
||||||
|
<Untertitel zeilen={S6.liste} />
|
||||||
|
<Blende dauer={dauer} rein={0} raus={14} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Kamera {...kam}>
|
||||||
|
<Ort name="zimmer_himmel" />
|
||||||
|
<Figur pose={[[0, 'lulu_A5_blick-links'], [z.n2.start, 'lulu_A3_lachen'], [z.p2.start, 'lulu_A3_lachen']]} x={luluX} y={1180} hoehe={N * 2.05} schatten={false} />
|
||||||
|
<Ort name="zimmer_wand" />
|
||||||
|
<Figur
|
||||||
|
pose={[
|
||||||
|
[0, 'noa_B_sitzt'],
|
||||||
|
[z.n1.start, 'noa_B_sitzt'],
|
||||||
|
]}
|
||||||
|
x={NOA_X6}
|
||||||
|
y={1000}
|
||||||
|
hoehe={N}
|
||||||
|
stauchY={atmen(f, 2)}
|
||||||
|
drehung={malt ? Math.sin(f * 0.3) * 0.8 : 0}
|
||||||
|
schatten={false}
|
||||||
|
/>
|
||||||
|
<Ort name="zimmer_tisch" />
|
||||||
|
<Kritzel t={malen} />
|
||||||
|
{malt && <div style={{position: 'absolute', left: stiftX, top: stiftY - 70, width: 14, height: 80, borderRadius: 4, background: '#d9534f', transform: 'rotate(25deg)'}} />}
|
||||||
|
{imSprung ? (
|
||||||
|
<ClipFigur name="pino_C3_freudensprung" start={sprung} tempo={1.4} x={PINO_X6} y={TISCH_Y} hoehe={N * 0.4} />
|
||||||
|
) : (
|
||||||
|
<Figur
|
||||||
|
pose={[
|
||||||
|
[0, 'pino_A5_blick-links'],
|
||||||
|
[z.l1.start + 10, 'pino_A6_blick-rechts'],
|
||||||
|
[z.n1.start + 10, 'pino_A5_blick-links'],
|
||||||
|
[z.p1.start, 'pino_A4_staunen'],
|
||||||
|
[z.n3.start + 20, 'pino_A5_blick-links'],
|
||||||
|
[z.n4.start + 30, 'pino_A3_lachen'],
|
||||||
|
]}
|
||||||
|
x={PINO_X6 + zitter}
|
||||||
|
y={TISCH_Y + (kribbeln ? huepfer(f, z.p2.start, 12, 10) + huepfer(f, z.p2.start + 14, 12, 10) : 0)}
|
||||||
|
hoehe={N * 0.4}
|
||||||
|
drehung={zitter}
|
||||||
|
stauchY={atmen(f, 1)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{kribbeln && <Funkeln x={PINO_X6} y={TISCH_Y - 120} start={z.p2.start} dauer={40} s={0.8} />}
|
||||||
|
</Kamera>
|
||||||
|
<Vignette />
|
||||||
|
<Untertitel zeilen={S6.liste} />
|
||||||
|
<Blende dauer={dauer} rein={14} raus={0} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
127
remotion/src/folge02/szene7.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useCurrentFrame} from 'remotion';
|
||||||
|
import {ClipFigur, Figur, Kamera} from './figur';
|
||||||
|
import {Blende, Vignette, atmen, kamFahrt} from './helfer';
|
||||||
|
import {Nest, Schnee, Zzz} from './natur';
|
||||||
|
import {Ort} from './orte';
|
||||||
|
import {BODEN} from './szene2';
|
||||||
|
import {NEST_X} from './szene4';
|
||||||
|
import {Untertitel} from './untertitel';
|
||||||
|
import {plane} from './zeit';
|
||||||
|
|
||||||
|
// Szene 7: Gute Nacht, Pino. Nacht im Garten, die erste Schneeflocke fällt.
|
||||||
|
|
||||||
|
const NOA_X7 = 1140;
|
||||||
|
const LULU_X7 = 1800;
|
||||||
|
const LATERNE = {x: 1290, y: BODEN + 10};
|
||||||
|
|
||||||
|
export const S7 = plane(
|
||||||
|
[
|
||||||
|
{id: 'bild0', dauer: 3.4},
|
||||||
|
{id: 'n1', wer: 'NOA', text: 'Schlaf gut, Pino. Nur bis morgen.', leise: true},
|
||||||
|
{id: 'e1', wer: 'ERZ', text: 'Ab dem 1. Dezember öffnen Noa, Lulu und Pino jeden Tag ein Türchen.'},
|
||||||
|
{id: 'e2', wer: 'ERZ', text: 'Und du kannst mitmachen! Schaffst du es mit Pino bis zum Winterfest?', pause: 3},
|
||||||
|
{id: 't1', wer: 'NOA_LULU', text: 'Tschüss! Bis bald!', leise: true, dauer: 2.6},
|
||||||
|
{id: 'p1', wer: 'PINO', text: '… bis bald …', leise: true, dauer: 2.6},
|
||||||
|
],
|
||||||
|
0.5,
|
||||||
|
1.6,
|
||||||
|
);
|
||||||
|
|
||||||
|
const Laterne: React.FC<{frame: number}> = ({frame}) => {
|
||||||
|
const flackern = 0.92 + 0.08 * Math.sin(frame * 0.5) * Math.sin(frame * 0.17);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{position: 'absolute', left: LATERNE.x - 18, top: LATERNE.y - 92, width: 36, height: 8, borderRadius: 3, background: '#3a3036'}} />
|
||||||
|
<div style={{position: 'absolute', left: LATERNE.x - 3, top: LATERNE.y - 112, width: 6, height: 22, background: '#3a3036'}} />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: LATERNE.x - 24,
|
||||||
|
top: LATERNE.y - 84,
|
||||||
|
width: 48,
|
||||||
|
height: 70,
|
||||||
|
borderRadius: 10,
|
||||||
|
background: `radial-gradient(circle at 50% 55%, #fff4c2 0%, #ffc95e ${40 * flackern}%, #e08a3c 100%)`,
|
||||||
|
border: '5px solid #3a3036',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{position: 'absolute', left: LATERNE.x - 30, top: LATERNE.y - 16, width: 60, height: 12, borderRadius: 4, background: '#3a3036'}} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Szene7: React.FC = () => {
|
||||||
|
const f = useCurrentFrame();
|
||||||
|
const {z, dauer} = S7;
|
||||||
|
const flackern = 0.92 + 0.08 * Math.sin(f * 0.5) * Math.sin(f * 0.17);
|
||||||
|
const winken = z.t1.start - 6;
|
||||||
|
|
||||||
|
const kam = kamFahrt(f, [
|
||||||
|
[0, 1300, 600, 1.15],
|
||||||
|
[z.bild0.ende, 1350, 700, 1.4],
|
||||||
|
[z.n1.ende, NEST_X, 820, 2.0],
|
||||||
|
[z.e2.start, 1380, 700, 1.45],
|
||||||
|
[z.t1.start, 1400, 680, 1.4],
|
||||||
|
[z.p1.start, NEST_X, 850, 2.6],
|
||||||
|
[dauer, NEST_X, 860, 2.9],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Kamera {...kam}>
|
||||||
|
<Ort name="garten_nacht" />
|
||||||
|
<div style={{position: 'absolute', inset: 0, filter: 'brightness(0.62) saturate(0.8) hue-rotate(-8deg)'}}>
|
||||||
|
{f < winken ? (
|
||||||
|
<Figur
|
||||||
|
pose={[
|
||||||
|
[0, 'noa_A6_blick-rechts'],
|
||||||
|
[z.bild0.ende - 20, 'noa_A2_augen-halb'],
|
||||||
|
[z.e1.start, 'noa_A3_lachen'],
|
||||||
|
]}
|
||||||
|
x={NOA_X7}
|
||||||
|
y={BODEN}
|
||||||
|
stauchY={atmen(f, 2)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ClipFigur name="noa_C4_winken" start={winken} tempo={1.2} x={NOA_X7} y={BODEN} />
|
||||||
|
)}
|
||||||
|
<Nest x={NEST_X} y={BODEN} seite="hinten" />
|
||||||
|
<div style={{position: 'absolute', inset: 0, clipPath: `inset(0 0 ${1080 - BODEN - 4}px 0)`}}>
|
||||||
|
<Figur pose="pino_B_schlaeft-eingerollt" x={NEST_X + 10} y={BODEN - 34} stauchY={atmen(f, 1, 0.02)} schatten={false} />
|
||||||
|
</div>
|
||||||
|
<Nest x={NEST_X} y={BODEN} seite="vorne" />
|
||||||
|
<Figur
|
||||||
|
pose={[
|
||||||
|
[0, 'lulu_A5_blick-links'],
|
||||||
|
[z.bild0.ende, 'lulu_A2_augen-halb'],
|
||||||
|
[z.t1.start, 'lulu_A3_lachen'],
|
||||||
|
]}
|
||||||
|
x={LULU_X7}
|
||||||
|
y={BODEN - 10}
|
||||||
|
stauchY={atmen(f, 3, 0.006)}
|
||||||
|
drehung={f > z.t1.start && f < z.t1.ende ? Math.sin((f - z.t1.start) * 0.25) * 2 : 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* warmes Licht der Laterne */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: LATERNE.x - 520,
|
||||||
|
top: LATERNE.y - 560,
|
||||||
|
width: 1040,
|
||||||
|
height: 900,
|
||||||
|
background: `radial-gradient(closest-side, rgba(255, 200, 110, ${0.42 * flackern}), rgba(255, 180, 90, 0.12) 55%, rgba(255, 180, 90, 0) 100%)`,
|
||||||
|
mixBlendMode: 'screen',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Laterne frame={f} />
|
||||||
|
<Zzz x={NEST_X + 50} y={BODEN - 110} start={20} s={0.7} />
|
||||||
|
<Schnee start={30} ziel={[NEST_X - 20, BODEN - 70]} />
|
||||||
|
</Kamera>
|
||||||
|
<Vignette staerke={0.5} />
|
||||||
|
<Untertitel zeilen={S7.liste} />
|
||||||
|
<Blende dauer={dauer} rein={16} raus={0} farbe="#1c1a24" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
101
remotion/src/folge02/untertitel.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {interpolate, useCurrentFrame} from 'remotion';
|
||||||
|
import {loadFont} from '@remotion/google-fonts/Fredoka';
|
||||||
|
import type {Geplant, Sprecher} from './zeit';
|
||||||
|
|
||||||
|
export const {fontFamily: SCHRIFT} = loadFont('normal', {weights: ['500', '600'], subsets: ['latin']});
|
||||||
|
|
||||||
|
const NAME: Record<Sprecher, string> = {
|
||||||
|
ERZ: 'Erzählstimme',
|
||||||
|
PINO: 'Pino',
|
||||||
|
NOA: 'Noa',
|
||||||
|
LULU: 'Lulu',
|
||||||
|
ALLE: 'Alle',
|
||||||
|
NOA_LULU: 'Noa und Lulu',
|
||||||
|
};
|
||||||
|
const FARBE: Record<Sprecher, string> = {
|
||||||
|
ERZ: '#7a6f66',
|
||||||
|
PINO: '#3f5a8a',
|
||||||
|
NOA: '#c27a12',
|
||||||
|
LULU: '#5f8a4a',
|
||||||
|
ALLE: '#a0526b',
|
||||||
|
NOA_LULU: '#a0526b',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Untertitel für die Vorschau ohne Ton. Mit Sprachaufnahmen können sie aus.
|
||||||
|
export const Untertitel: React.FC<{zeilen: Geplant[]}> = ({zeilen}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const z = zeilen.find((l) => l.text && frame >= l.start && frame < l.ende + 6);
|
||||||
|
const frage = zeilen.find((l) => (l.pause ?? 0) >= 2.5 && frame >= l.ende && frame < l.frei);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{z && z.wer && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 52,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
opacity: interpolate(frame, [z.start, z.start + 5, z.ende, z.ende + 6], [0, 1, 1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: 1400,
|
||||||
|
padding: '16px 34px 18px',
|
||||||
|
borderRadius: 34,
|
||||||
|
background: 'rgba(255, 250, 240, 0.9)',
|
||||||
|
boxShadow: '0 6px 24px rgba(60, 40, 20, 0.18)',
|
||||||
|
fontFamily: SCHRIFT,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{fontSize: 26, fontWeight: 600, color: FARBE[z.wer], letterSpacing: 1}}>
|
||||||
|
{NAME[z.wer]}
|
||||||
|
{z.leise ? ' (leise)' : ''}
|
||||||
|
</div>
|
||||||
|
<div style={{fontSize: 46, fontWeight: 500, color: '#3b2f28', lineHeight: 1.2, fontStyle: z.leise ? 'italic' : 'normal'}}>{z.text}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{frage && <MitmachPause von={frage.ende} bis={frage.frei} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mitmach-Pause: großes Fragezeichen, drei Punkte füllen sich nacheinander.
|
||||||
|
const MitmachPause: React.FC<{von: number; bis: number}> = ({von, bis}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const t = (frame - von) / (bis - von);
|
||||||
|
const auf = interpolate(frame, [von, von + 8, bis - 8, bis], [0, 1, 1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'});
|
||||||
|
const wipp = Math.sin(frame * 0.25) * 4;
|
||||||
|
return (
|
||||||
|
<div style={{position: 'absolute', right: 90, top: 80, opacity: auf, transform: `scale(${0.7 + 0.3 * auf}) rotate(${wipp}deg)`}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: '#fff6df',
|
||||||
|
boxShadow: '0 6px 20px rgba(60,40,20,0.25)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontFamily: SCHRIFT,
|
||||||
|
fontSize: 110,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#e08a3c',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
<div style={{display: 'flex', gap: 14, justifyContent: 'center', marginTop: 14}}>
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<div key={i} style={{width: 22, height: 22, borderRadius: '50%', background: t > i / 3 ? '#e08a3c' : 'rgba(255,246,223,0.9)'}} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
48
remotion/src/folge02/zeit.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Zeitplan einer Szene aus ihren Sprechzeilen.
|
||||||
|
//
|
||||||
|
// Solange es keine Sprachaufnahmen gibt, wird die Dauer jeder Zeile aus der
|
||||||
|
// Textlänge geschätzt. Kommen später die ElevenLabs-Dateien, trägt man die
|
||||||
|
// echte Länge als `dauer` ein (oder liest sie aus), und alle Bewegungen, die
|
||||||
|
// sich auf die Zeilen beziehen, rutschen automatisch mit.
|
||||||
|
|
||||||
|
export const FPS = 30;
|
||||||
|
|
||||||
|
export type Sprecher = 'ERZ' | 'PINO' | 'NOA' | 'LULU' | 'ALLE' | 'NOA_LULU';
|
||||||
|
|
||||||
|
export type Zeile = {
|
||||||
|
id: string;
|
||||||
|
wer?: Sprecher; // leer = nur Bild (Handlung ohne Text)
|
||||||
|
text?: string;
|
||||||
|
dauer?: number; // Sekunden; ohne Angabe aus der Textlänge geschätzt
|
||||||
|
pause?: number; // Sekunden Stille danach (Mitmach-Pause = 3)
|
||||||
|
leise?: boolean; // geflüstert / gemurmelt
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Geplant = Zeile & {start: number; ende: number; frei: number};
|
||||||
|
|
||||||
|
const schaetze = (z: Zeile) => {
|
||||||
|
if (z.dauer !== undefined) return z.dauer;
|
||||||
|
if (!z.text) return 1;
|
||||||
|
// Kinderfernsehen: ruhig gesprochen, ca. 13–14 Zeichen pro Sekunde
|
||||||
|
const tempo = z.wer === 'ERZ' ? 13 : 14;
|
||||||
|
return 0.6 + z.text.length / tempo;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lücke zwischen zwei Zeilen
|
||||||
|
const LUECKE = 0.35;
|
||||||
|
|
||||||
|
export const plane = (zeilen: Zeile[], vorlauf = 0.5, nachlauf = 0.8) => {
|
||||||
|
let t = vorlauf * FPS;
|
||||||
|
const liste: Geplant[] = [];
|
||||||
|
for (const z of zeilen) {
|
||||||
|
const d = Math.round(schaetze(z) * FPS);
|
||||||
|
const start = Math.round(t);
|
||||||
|
const ende = start + d;
|
||||||
|
const frei = ende + Math.round((z.pause ?? 0) * FPS);
|
||||||
|
liste.push({...z, start, ende, frei});
|
||||||
|
t = frei + LUECKE * FPS;
|
||||||
|
}
|
||||||
|
const dauer = Math.round(t + nachlauf * FPS);
|
||||||
|
const z = Object.fromEntries(liste.map((l) => [l.id, l])) as Record<string, Geplant>;
|
||||||
|
return {liste, z, dauer};
|
||||||
|
};
|
||||||
47
remotion/werkzeuge/clip_freistellen.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Clips freistellen: python3 werkzeuge/clip_freistellen.py <arbeitsordner> <zielordner> clip1.mp4 ...
|
||||||
|
# Clip freistellen: Frames -> Maske -> gemeinsamer Zuschnitt -> WebM (VP9 mit Alpha)
|
||||||
|
import sys, os, subprocess, json, glob
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageFilter
|
||||||
|
from scipy import ndimage
|
||||||
|
from rembg import remove, new_session
|
||||||
|
s = new_session("isnet-general-use")
|
||||||
|
work, ziel = sys.argv[1], sys.argv[2]
|
||||||
|
os.makedirs(ziel, exist_ok=True)
|
||||||
|
for src in sys.argv[3:]:
|
||||||
|
name = os.path.basename(src)[:-4]
|
||||||
|
if os.path.exists(f"{ziel}/{name}.webm"): continue
|
||||||
|
d = f"{work}/{name}"; os.makedirs(d + "/roh", exist_ok=True); os.makedirs(d + "/frei", exist_ok=True)
|
||||||
|
subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", src, f"{d}/roh/%04d.png"], check=True)
|
||||||
|
roh = sorted(glob.glob(f"{d}/roh/*.png")); boxen = []
|
||||||
|
for f in roh:
|
||||||
|
dst = f"{d}/frei/" + os.path.basename(f)
|
||||||
|
im = Image.open(f).convert("RGB")
|
||||||
|
if not os.path.exists(dst):
|
||||||
|
m = np.asarray(remove(im, session=s, only_mask=True)).astype(float)
|
||||||
|
hard = m > 110
|
||||||
|
lab, n = ndimage.label(hard)
|
||||||
|
if n:
|
||||||
|
sizes = ndimage.sum(hard, lab, range(1, n + 1)); keep = np.zeros_like(hard)
|
||||||
|
for i, sz in enumerate(sizes):
|
||||||
|
if sz > 0.02 * sizes.max(): keep |= lab == i + 1
|
||||||
|
hard = keep
|
||||||
|
holes = ndimage.binary_fill_holes(hard) & ~hard
|
||||||
|
hl, hn = ndimage.label(holes)
|
||||||
|
for i in range(1, hn + 1):
|
||||||
|
if (hl == i).sum() < 400: hard |= hl == i
|
||||||
|
hard = ndimage.binary_erosion(hard, iterations=2)
|
||||||
|
a = Image.fromarray((hard * 255).astype("uint8")).filter(ImageFilter.GaussianBlur(0.8))
|
||||||
|
o = im.convert("RGBA"); o.putalpha(a); o.save(dst)
|
||||||
|
boxen.append(Image.open(dst).getchannel("A").getbbox())
|
||||||
|
bx = [b for b in boxen if b]
|
||||||
|
box = [min(b[0] for b in bx) - 4, min(b[1] for b in bx) - 4, max(b[2] for b in bx) + 4, max(b[3] for b in bx) + 4]
|
||||||
|
box = [max(0, box[0]), max(0, box[1]), min(1284, box[2]), min(716, box[3])]
|
||||||
|
if (box[2] - box[0]) % 2: box[2] -= 1
|
||||||
|
if (box[3] - box[1]) % 2: box[3] -= 1
|
||||||
|
w, h = box[2] - box[0], box[3] - box[1]
|
||||||
|
subprocess.run(["ffmpeg", "-v", "error", "-y", "-framerate", "24", "-i", f"{d}/frei/%04d.png",
|
||||||
|
"-vf", f"crop={w}:{h}:{box[0]}:{box[1]}", "-c:v", "libvpx-vp9", "-pix_fmt", "yuva420p",
|
||||||
|
"-b:v", "0", "-crf", "28", "-auto-alt-ref", "0", f"{ziel}/{name}.webm"], check=True)
|
||||||
|
json.dump({"box": box, "boxen": boxen, "frames": len(roh)}, open(f"{ziel}/{name}.json", "w"))
|
||||||
|
print(name, box, len(roh), flush=True)
|
||||||
37
remotion/werkzeuge/freistellen.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Standbilder freistellen (rembg), Leinwand bleibt erhalten.
|
||||||
|
# python3 werkzeuge/freistellen.py <zielordner> bild1.png bild2.png ...
|
||||||
|
# Freistellen ohne Zuschneiden (gleiche Leinwand -> Posen deckungsgleich)
|
||||||
|
import sys, os, time
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageFilter
|
||||||
|
from scipy import ndimage
|
||||||
|
from rembg import remove, new_session
|
||||||
|
s = new_session("isnet-general-use")
|
||||||
|
out_dir = sys.argv[1]
|
||||||
|
for src in sys.argv[2:]:
|
||||||
|
dst = os.path.join(out_dir, os.path.basename(src)[:-4] + ".png")
|
||||||
|
if os.path.exists(dst): continue
|
||||||
|
t = time.time()
|
||||||
|
im = Image.open(src).convert("RGB")
|
||||||
|
m = np.asarray(remove(im, session=s, only_mask=True)).astype(float)
|
||||||
|
hard = m > 110
|
||||||
|
lab, n = ndimage.label(hard)
|
||||||
|
if n == 0: print("LEER", src); continue
|
||||||
|
sizes = ndimage.sum(hard, lab, range(1, n + 1))
|
||||||
|
keep = np.zeros_like(hard)
|
||||||
|
for i, sz in enumerate(sizes): # alle großen Teile (Laterne, Bommel) behalten
|
||||||
|
if sz > 0.02 * sizes.max(): keep |= lab == i + 1
|
||||||
|
hard = keep
|
||||||
|
holes = ndimage.binary_fill_holes(hard) & ~hard
|
||||||
|
hl, hn = ndimage.label(holes)
|
||||||
|
for i in range(1, hn + 1):
|
||||||
|
if (hl == i).sum() < 400: hard |= hl == i
|
||||||
|
rgb = np.asarray(im).astype(float)
|
||||||
|
paper = (rgb.min(-1) > 228) & (rgb.max(-1) - rgb.min(-1) < 18)
|
||||||
|
plab, pn = ndimage.label(paper & hard)
|
||||||
|
for i in range(1, pn + 1):
|
||||||
|
if (plab == i).sum() > 600: hard &= ~(plab == i)
|
||||||
|
hard = ndimage.binary_erosion(hard, iterations=2)
|
||||||
|
a = Image.fromarray((hard * 255).astype("uint8")).filter(ImageFilter.GaussianBlur(0.8))
|
||||||
|
o = im.convert("RGBA"); o.putalpha(a); o.save(dst)
|
||||||
|
print(os.path.basename(dst), round(time.time() - t, 1), flush=True)
|
||||||
68
remotion/werkzeuge/posen.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Bereitet die freigestellten Standbilder der Figuren-Bibliothek für Remotion auf.
|
||||||
|
#
|
||||||
|
# Eingabe: freigestellte PNGs in voller Leinwand (1376 × 768, wie aus der
|
||||||
|
# Bibliothek), z. B. aus frei.py. Alle Bilder einer Figur werden mit demselben
|
||||||
|
# Rahmen zugeschnitten, damit Posen beim Überblenden deckungsgleich bleiben.
|
||||||
|
#
|
||||||
|
# Ausgabe: public/folge02/figuren/<pose>.webp und src/folge02/posen.ts
|
||||||
|
# python3 werkzeuge/posen.py <ordner_freigestellt> [<ordner_clips_frei>]
|
||||||
|
import json, os, sys, glob
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
QUELLE = sys.argv[1]
|
||||||
|
CLIPS = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
ZIEL = os.path.join(HIER, "..", "public", "folge02", "figuren")
|
||||||
|
TS = os.path.join(HIER, "..", "src", "folge02", "posen.ts")
|
||||||
|
os.makedirs(ZIEL, exist_ok=True)
|
||||||
|
|
||||||
|
# Referenzpose je Figur: deren Höhe gilt als „Figurhöhe“, deren Fußpunkt als Anker.
|
||||||
|
REF = {"pino": "pino_ref_sitzend", "noa": "noa_ref_stehend", "lulu": "lulu_A6_blick-rechts"}
|
||||||
|
# Clip-Leinwand (1284 × 716) ist die verkleinerte Bild-Leinwand (1376 × 768).
|
||||||
|
K_CLIP = 1376 / 1284
|
||||||
|
|
||||||
|
posen = {}
|
||||||
|
for figur in REF:
|
||||||
|
dateien = sorted(glob.glob(f"{QUELLE}/{figur}_*.png"))
|
||||||
|
bilder = {os.path.basename(d)[:-4]: Image.open(d) for d in dateien}
|
||||||
|
boxen = {n: b.getchannel("A").getbbox() for n, b in bilder.items()}
|
||||||
|
rahmen = [min(b[0] for b in boxen.values()) - 4, min(b[1] for b in boxen.values()) - 4,
|
||||||
|
max(b[2] for b in boxen.values()) + 4, max(b[3] for b in boxen.values()) + 4]
|
||||||
|
rahmen = [max(0, rahmen[0]), max(0, rahmen[1]), min(1376, rahmen[2]), min(768, rahmen[3])]
|
||||||
|
for n, b in bilder.items():
|
||||||
|
b.crop(rahmen).save(f"{ZIEL}/{n}.webp", quality=90, method=5)
|
||||||
|
posen[n] = {"figur": figur, "datei": f"folge02/figuren/{n}.webp", "x": rahmen[0], "y": rahmen[1],
|
||||||
|
"b": rahmen[2] - rahmen[0], "h": rahmen[3] - rahmen[1], "box": boxen[n]}
|
||||||
|
# A5 (Blick links) ist eine Spiegelung von A6 an der Bildmitte: zurückschieben,
|
||||||
|
# damit die Figur beim Wechsel A6 ↔ A5 nicht seitlich springt.
|
||||||
|
for n in posen:
|
||||||
|
if n.startswith(f"{figur}_A5_"):
|
||||||
|
a6 = boxen[n.replace("A5_blick-links", "A6_blick-rechts")]
|
||||||
|
posen[n]["x"] += (a6[0] + a6[2]) / 2 - (boxen[n][0] + boxen[n][2]) / 2
|
||||||
|
print(figur, len(bilder), rahmen)
|
||||||
|
|
||||||
|
figuren = {}
|
||||||
|
for figur, ref in REF.items():
|
||||||
|
b = posen[ref]["box"]
|
||||||
|
figuren[figur] = {"refHoehe": b[3] - b[1], "fussX": (b[0] + b[2]) / 2, "fussY": b[3]}
|
||||||
|
|
||||||
|
clips = {}
|
||||||
|
if CLIPS:
|
||||||
|
for j in sorted(glob.glob(f"{CLIPS}/*.json")):
|
||||||
|
n = os.path.basename(j)[:-5]
|
||||||
|
d = json.load(open(j))
|
||||||
|
bx = d["box"]
|
||||||
|
clips[n] = {"figur": n.split("_")[0], "datei": f"folge02/clips/{n}.webm",
|
||||||
|
"x": bx[0] * K_CLIP, "y": bx[1] * K_CLIP, "b": (bx[2] - bx[0]) * K_CLIP,
|
||||||
|
"h": (bx[3] - bx[1]) * K_CLIP, "frames": d["frames"]}
|
||||||
|
|
||||||
|
with open(TS, "w") as f:
|
||||||
|
f.write("// Automatisch erzeugt von werkzeuge/posen.py – nicht von Hand ändern.\n")
|
||||||
|
f.write("// Koordinaten in der Bild-Leinwand der Figur (1376 × 768).\n\n")
|
||||||
|
f.write("export type Pose = {figur: string; datei: string; x: number; y: number; b: number; h: number; box: number[]};\n")
|
||||||
|
f.write("export type ClipDaten = {figur: string; datei: string; x: number; y: number; b: number; h: number; frames: number};\n\n")
|
||||||
|
j = lambda o: json.dumps(o, indent="\t")
|
||||||
|
f.write("export const FIGUREN = " + j(figuren) + " as const;\n\n")
|
||||||
|
f.write("export const POSEN: Record<string, Pose> = " + j(posen) + ";\n\n")
|
||||||
|
f.write("export const CLIPS: Record<string, ClipDaten> = " + j(clips) + ";\n")
|
||||||
|
print("posen.ts", len(posen), "Posen,", len(clips), "Clips")
|
||||||