Remotion: Baukasten für alle Folgen, Szenen einzeln renderbar, Stimmen-Skript
- gemeinsame Bausteine nach src/baukasten/, Bibliothek nach public/bibliothek/, Orte nach public/orte/ - je Szene eine Komposition (F02-01-…), werkzeuge/render.mjs rendert nur geänderte Szenen und klebt sie zusammen; --entwurf mit halber Auflösung und eingeblendeter Zeit/Zeile - werkzeuge/stimmen.mts erzeugt ElevenLabs-Aufnahmen, Zeitplan übernimmt ihre Länge - Folge 2: Geh- und Hinsetz-Clips statt Standbild-Wechsel, neue Vögel, fallendes Blatt, Noa malt den Kalender (Blick von oben) - ANLEITUNG_PC.md für Studio und Rendern unter Windows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3
remotion/.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
node_modules/
|
||||
out/
|
||||
public/folge02/clips
|
||||
public/bibliothek/clips
|
||||
public/stimmen
|
||||
|
|
|
|||
53
remotion/ANLEITUNG_PC.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Remotion auf dem eigenen PC (Windows)
|
||||
|
||||
Damit kannst du Folgen sofort im Browser anschauen und durchspulen, ohne zu
|
||||
rendern, und schneller rendern als auf dem VPS (mehr Kerne, Grafikkarte).
|
||||
|
||||
## Einmalig einrichten
|
||||
1. **Node.js** (LTS, z. B. 22) installieren: https://nodejs.org – Standard-
|
||||
einstellungen. **Git** installieren: https://git-scm.com
|
||||
2. Repo holen (in der Eingabeaufforderung `cmd`):
|
||||
```cmd
|
||||
cd %USERPROFILE%
|
||||
git clone ssh://git@git.larpaso.de:2222/pascal/youtube.git
|
||||
cd youtube\remotion
|
||||
npm install
|
||||
```
|
||||
3. **Clips vom Server holen** (stehen nicht im Git, ca. 3 MB):
|
||||
```cmd
|
||||
ssh -t deploy@larpaso.de "sudo cp -r /var/lib/docker/volumes/claude_shared/_data/youtube/figuren_bibliothek/clips_frei ~/clips_frei && sudo chown -R deploy ~/clips_frei"
|
||||
scp -r deploy@larpaso.de:~/clips_frei %USERPROFILE%\youtube\remotion\public\bibliothek\clips
|
||||
```
|
||||
Ohne Clips läuft alles trotzdem, an ihrer Stelle steht dann ein Standbild.
|
||||
Sobald es Sprachaufnahmen gibt, genauso `/shared/youtube/stimmen` nach
|
||||
`public\stimmen` kopieren.
|
||||
|
||||
## Anschauen (Studio)
|
||||
```cmd
|
||||
cd %USERPROFILE%\youtube\remotion
|
||||
npx remotion studio
|
||||
```
|
||||
Es öffnet sich der Browser (sonst http://localhost:3000). Links die
|
||||
Kompositionen: `F02` ist die ganze Folge, `F02-03-…` einzelne Szenen.
|
||||
Mit Leertaste abspielen, mit der Zeitleiste springen. Ändert jemand den
|
||||
Code (`git pull`), aktualisiert sich die Vorschau von selbst.
|
||||
|
||||
Tipp: Unten rechts die Vorschau-Qualität auf „Half“ stellen, wenn es ruckelt.
|
||||
|
||||
## Rendern
|
||||
```cmd
|
||||
node werkzeuge\render.mjs F02 --entwurf
|
||||
node werkzeuge\render.mjs F02
|
||||
node werkzeuge\render.mjs F02 --szene 3
|
||||
```
|
||||
Ergebnis: `out\F02\F02-entwurf.mp4` bzw. `out\F02\F02.mp4`. Schon
|
||||
gerenderte, unveränderte Szenen werden übersprungen. Das Skript nutzt alle
|
||||
Kerne bis auf einen und die Grafikkarte.
|
||||
|
||||
## Aktualisieren
|
||||
```cmd
|
||||
cd %USERPROFILE%\youtube
|
||||
git pull
|
||||
cd remotion
|
||||
npm install
|
||||
```
|
||||
|
|
@ -61,43 +61,78 @@ python3 werkzeuge/rig.py # braucht numpy, scipy, pillow
|
|||
und `uebersicht.png` prüfen. In einer Szene: `gelenk('lulu', 'hals', grad)`
|
||||
(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 „?“.
|
||||
## Folgen bauen (Baukasten)
|
||||
Eine Folge besteht aus Szenen, jede Szene aus Sprechzeilen und Bewegungen,
|
||||
die an diesen Zeilen hängen. Alles Wiederverwendbare liegt in
|
||||
`src/baukasten/`, jede Folge hat einen eigenen Ordner (`src/folge02/`).
|
||||
Untertitel stehen für die Stimmen, solange es keine Aufnahmen gibt;
|
||||
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) |
|
||||
| `src/baukasten/figur.tsx` | `Figur` (Standbilder mit Posenwechsel), `ClipFigur` (freigestellte Clips), `Kamera` |
|
||||
| `src/baukasten/helfer.tsx` | Kamerafahrt, Atmen, Gehen, Hüpfen, Wackeln, Blenden, Ankerpunkte |
|
||||
| `src/baukasten/natur.tsx` | Blätter, Nest, Vögel, Schnee, Zzz, Funkeln, große Rufe |
|
||||
| `src/baukasten/orte.tsx` | Orte als SVG (Garten Tag/Nacht, Blick von oben, Noas Zimmer, Kalender); vorberechnet nach `public/orte/` |
|
||||
| `src/baukasten/buch.tsx` | Bilderbuch-Intro und -Outro |
|
||||
| `src/baukasten/zeit.ts` | Zeitplan aus den Zeilen (geschätzt oder aus den Aufnahmen) |
|
||||
| `src/baukasten/untertitel.tsx` | Untertitel, Mitmach-Pause, Stimmen abspielen |
|
||||
| `src/baukasten/folge.tsx` | macht aus der Szenenliste die Kompositionen |
|
||||
| `src/baukasten/posen.ts` | automatisch erzeugt von `werkzeuge/posen.py` |
|
||||
| `src/folge02/` | Folge 2: `Folge02.tsx` (Szenenliste), `szene2.tsx` … `szene7.tsx`, `lied.tsx`, `stimmen.json` |
|
||||
| `public/bibliothek/figuren/` | freigestellte Standbilder (WebP, im Git) |
|
||||
| `public/bibliothek/clips/` | freigestellte Clips (WebM mit Alpha, nicht im Git; VPS: Link auf `/shared/youtube/figuren_bibliothek/clips_frei/`) |
|
||||
| `public/stimmen/` | Sprachaufnahmen (nicht im Git; VPS: Link auf `/shared/youtube/stimmen/`) |
|
||||
|
||||
**Kompositionen:** `F02` ist die ganze Folge, `F02-01-Intro`,
|
||||
`F02-02-Gaehnen-ueberall` … jede Szene einzeln.
|
||||
|
||||
**Neue Folge:** Ordner `src/folgeNN/` anlegen, `plan.ts` und `stimmen.json`
|
||||
(`{}`) aus Folge 2 kopieren und den Ordnernamen anpassen, Szenen nach dem
|
||||
Muster von `szene2.tsx` schreiben (oben die Zeilen mit `plane('sX', [...])`,
|
||||
darunter Posenfolgen und Kamerafahrt, die sich auf `z.<zeile>.start/ende`
|
||||
beziehen), in `FolgeNN.tsx` auflisten und in `src/Root.tsx` eintragen.
|
||||
|
||||
### Rendern
|
||||
```bash
|
||||
node werkzeuge/render.mjs F02 --entwurf # schnelle Fassung zum Begutachten
|
||||
node werkzeuge/render.mjs F02 # volle Qualität
|
||||
node werkzeuge/render.mjs F02 --szene 3 # nur Szene 3 neu
|
||||
```
|
||||
Das Skript rendert jede Szene einzeln nach `out/F02/` und klebt sie
|
||||
zusammen. Szenen, deren Code, Bilder oder Aufnahmen sich nicht geändert
|
||||
haben, werden übersprungen. Im Entwurf stehen oben links Gesamtzeit, Szene,
|
||||
Szenenzeit und Zeilen-ID – für Rückmeldungen einfach diese Angaben nennen
|
||||
(z. B. „2:47, Szene 4, Zeile l1: Lulus Kopf zu weit links“).
|
||||
Auf dem VPS: vorher `export LD_LIBRARY_PATH=$HOME/.local/chrome-libs/root/usr/lib/x86_64-linux-gnu`.
|
||||
|
||||
### Vorschau ohne Rendern
|
||||
`npx remotion studio` öffnet alle Kompositionen im Browser; Änderungen am
|
||||
Code sind sofort zu sehen. Anleitung für einen Windows-PC: `ANLEITUNG_PC.md`.
|
||||
|
||||
### Stimmen (ElevenLabs)
|
||||
1. Stimmen-IDs je Sprecher in `werkzeuge/stimmen.config.json` eintragen
|
||||
(`ALLE` und `NOA_LULU` verweisen auf einen anderen Sprecher).
|
||||
2. `npx tsx werkzeuge/stimmen.mts F02 --probe` zeigt, welche Zeilen erzeugt
|
||||
würden, und zählt die Zeichen (kostet nichts).
|
||||
3. `ELEVENLABS_API_KEY=… npx tsx werkzeuge/stimmen.mts F02` erzeugt die
|
||||
Aufnahmen nach `public/stimmen/folge02/` und trägt ihre Länge in
|
||||
`src/folge02/stimmen.json` ein. Der Zeitplan richtet sich danach; ändert
|
||||
sich ein Text, wird nur diese Zeile neu erzeugt.
|
||||
|
||||
Untertitel ausblenden: `--props='{"untertitel": false}'` (bzw. in
|
||||
`render.mjs` die `props.json`).
|
||||
|
||||
### Neue Standbilder oder Clips
|
||||
```bash
|
||||
python3 werkzeuge/freistellen.py <zielordner> bild1.png … # ohne Zuschneiden
|
||||
python3 werkzeuge/clip_freistellen.py <arbeitsordner> <clips_frei> clip1.mp4 …
|
||||
python3 werkzeuge/posen.py <zielordner> <clips_frei> # Posen und Clips eintragen
|
||||
```
|
||||
Mit `pip install "rembg[gpu]"` nutzt `rembg` eine Nvidia-Grafikkarte
|
||||
automatisch (auf dem VPS gibt es keine; dort ca. 3 s pro Bild).
|
||||
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`.
|
||||
`npx remotion still src/index.ts F2Ort-garten public/orte/garten.webp --image-format=webp`
|
||||
|
||||
## Bekannte Grenzen
|
||||
- Die Figuren haben nur die Pose ihres Bildes. Neue Posen brauchen ein neues
|
||||
|
|
|
|||
509
remotion/package-lock.json
generated
|
|
@ -16,7 +16,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/three": "^0.169.0"
|
||||
"@types/three": "^0.169.0",
|
||||
"tsx": "^4.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
|
|
@ -1643,6 +1644,21 @@
|
|||
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
|
|
@ -1655,6 +1671,19 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz",
|
||||
"integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
|
|
@ -2387,6 +2416,16 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
|
|
@ -2712,6 +2751,474 @@
|
|||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
|
||||
"integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.23.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
|
||||
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
|
||||
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
|
||||
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
|
||||
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
|
||||
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
|
||||
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
|
||||
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
|
||||
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
|
||||
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
|
||||
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
|
||||
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.23.1",
|
||||
"@esbuild/android-arm": "0.23.1",
|
||||
"@esbuild/android-arm64": "0.23.1",
|
||||
"@esbuild/android-x64": "0.23.1",
|
||||
"@esbuild/darwin-arm64": "0.23.1",
|
||||
"@esbuild/darwin-x64": "0.23.1",
|
||||
"@esbuild/freebsd-arm64": "0.23.1",
|
||||
"@esbuild/freebsd-x64": "0.23.1",
|
||||
"@esbuild/linux-arm": "0.23.1",
|
||||
"@esbuild/linux-arm64": "0.23.1",
|
||||
"@esbuild/linux-ia32": "0.23.1",
|
||||
"@esbuild/linux-loong64": "0.23.1",
|
||||
"@esbuild/linux-mips64el": "0.23.1",
|
||||
"@esbuild/linux-ppc64": "0.23.1",
|
||||
"@esbuild/linux-riscv64": "0.23.1",
|
||||
"@esbuild/linux-s390x": "0.23.1",
|
||||
"@esbuild/linux-x64": "0.23.1",
|
||||
"@esbuild/netbsd-x64": "0.23.1",
|
||||
"@esbuild/openbsd-arm64": "0.23.1",
|
||||
"@esbuild/openbsd-x64": "0.23.1",
|
||||
"@esbuild/sunos-x64": "0.23.1",
|
||||
"@esbuild/win32-arm64": "0.23.1",
|
||||
"@esbuild/win32-ia32": "0.23.1",
|
||||
"@esbuild/win32-x64": "0.23.1"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/three": "^0.169.0"
|
||||
"@types/three": "^0.169.0",
|
||||
"tsx": "^4.19.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
BIN
remotion/public/orte/kalender_leer.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
|
|
@ -3,8 +3,9 @@ import {Composition} from 'remotion';
|
|||
import {HintergrundAbend, PapierStruktur, VordergrundAbend} from './szenen/Hintergruende';
|
||||
import {LaterneTest} from './szenen/LaterneTest';
|
||||
import {VerformTest} from './szenen/VerformTest';
|
||||
import {ORTE, OrtStill} from './folge02/orte';
|
||||
import {FOLGE02_DAUER, Folge02} from './folge02/Folge02';
|
||||
import {FolgenKompositionen} from './baukasten/folge';
|
||||
import {ORTE, OrtStill} from './baukasten/orte';
|
||||
import {F02} from './folge02/Folge02';
|
||||
|
||||
const BILD = {fps: 30, width: 1920, height: 1080} as const;
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ export const Root: React.FC = () => (
|
|||
<Composition id="HintergrundAbend" component={HintergrundAbend} durationInFrames={1} {...BILD} />
|
||||
<Composition id="VordergrundAbend" component={VordergrundAbend} durationInFrames={1} {...BILD} />
|
||||
<Composition id="PapierStruktur" component={PapierStruktur} durationInFrames={1} {...BILD} />
|
||||
<Composition id="Folge02" component={Folge02} durationInFrames={FOLGE02_DAUER} {...BILD} />
|
||||
<FolgenKompositionen folge={F02} />
|
||||
{Object.keys(ORTE).map((name) => (
|
||||
<Composition key={name} id={`F2Ort-${name.replace('_', '-')}`} component={OrtStill} defaultProps={{name}} durationInFrames={1} {...BILD} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {Figur, Kamera} from './figur';
|
|||
import {Blende, Vignette, kamFahrt} from './helfer';
|
||||
import {Blatt} from './natur';
|
||||
import {SCHRIFT, Untertitel} from './untertitel';
|
||||
import {plane} from './zeit';
|
||||
import {plane, type Plan, type Zeile} from './zeit';
|
||||
|
||||
// Das Bilderbuch: öffnet sich am Anfang (Intro) und klappt am Ende zu.
|
||||
// Buchrücken bei x = 960; geschlossen liegt der Deckel rechts davon.
|
||||
|
|
@ -91,19 +91,19 @@ const Tisch: React.FC = () => (
|
|||
);
|
||||
|
||||
// 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,
|
||||
);
|
||||
// Die Folge plant die Zeilen selbst (mit `introZeilen`), damit Aufnahmen greifen.
|
||||
export const introZeilen = (folgenTitel: string): Zeile[] => [
|
||||
{id: 'buch', dauer: 2.2},
|
||||
{id: 't1', wer: 'ERZ', text: 'Pino, Noa und Lulu.'},
|
||||
{id: 't2', wer: 'ERZ', text: `Heute: ${folgenTitel}.`},
|
||||
];
|
||||
export const INTRO_OPTIONEN = {vorlauf: 0.3, nachlauf: 1.6};
|
||||
|
||||
export const Intro: React.FC = () => {
|
||||
type IntroProps = {plan: Plan; titel: string; posen?: [string, string, string]};
|
||||
|
||||
export const Intro: React.FC<IntroProps> = ({plan, titel, posen = ['pino_A3_lachen', 'noa_A3_lachen', 'lulu_A3_lachen']}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const {z, dauer} = INTRO;
|
||||
const {z, dauer} = plan;
|
||||
const auf = 30;
|
||||
const winkel = kurve(frame, [
|
||||
[auf, 0],
|
||||
|
|
@ -120,21 +120,21 @@ export const Intro: React.FC = () => {
|
|||
<>
|
||||
<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 && <Innenseiten frame={frame} auf={auf + 30} titel={titel} posen={posen} />}
|
||||
{winkel >= -90 && <Seite links={960} />}
|
||||
<Deckel winkel={winkel} />
|
||||
</Kamera>
|
||||
<Vignette />
|
||||
<Untertitel zeilen={INTRO.liste} />
|
||||
<Untertitel zeilen={plan.liste} />
|
||||
<Blende dauer={dauer} rein={12} raus={14} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Outro: Seite mit winkenden Figuren, Buch klappt zu.
|
||||
export const OUTRO = plane([{id: 'buch', dauer: 4.5}], 0.2, 0.6);
|
||||
export const OUTRO = plane([{id: 'buch', dauer: 4.5}], {vorlauf: 0.2, nachlauf: 0.6});
|
||||
|
||||
export const Outro: React.FC = () => {
|
||||
export const Outro: React.FC<{posen?: [string, string, string]}> = ({posen = ['pino_B3_schlafen', 'noa_A_arm-hoch-winken', 'lulu_A3_lachen']}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const {dauer} = OUTRO;
|
||||
const zu = 60;
|
||||
|
|
@ -152,7 +152,7 @@ export const Outro: React.FC = () => {
|
|||
<>
|
||||
<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 && <Innenseiten frame={frame} auf={-60} titel="Bis bald!" posen={posen} />}
|
||||
{winkel >= -90 && <Seite links={960} />}
|
||||
<Deckel winkel={winkel} />
|
||||
</Kamera>
|
||||
|
|
@ -131,6 +131,19 @@ export const ClipFigur: React.FC<ClipProps> = ({name, start, hoehe, tempo = 1, s
|
|||
);
|
||||
};
|
||||
|
||||
// Gehende Figur: Geh-Clip der Bibliothek in Schleife (Blick nach links,
|
||||
// `spiegeln` = nach rechts). Die Bewegung über den Boden macht die Szene (x).
|
||||
export const Geher: React.FC<Omit<ClipProps, 'name' | 'start' | 'schleife'> & {figur: FigurName}> = ({figur, ...rest}) => (
|
||||
<ClipFigur name={`${figur}_C1_gehen-schleife`} start={0} schleife ersatz={`${figur}_A7-1_gehen`} {...rest} />
|
||||
);
|
||||
|
||||
// Noa geht in die Hocke: Clip „hinsetzen“ bis zur Hocke (ca. 1,65 s), danach
|
||||
// bleibt das letzte Bild stehen. `auf` spielt ihn rückwärts (aufstehen).
|
||||
export const NOA_HOCKE_FRAMES = 50; // Dauer in Frames der Komposition bei Tempo 1
|
||||
export const NoaHocke: React.FC<Omit<ClipProps, 'name' | 'bis' | 'rueckwaerts'> & {auf?: boolean}> = ({auf = false, ...rest}) => (
|
||||
<ClipFigur name="noa_C5_hinsetzen" bis={1.65} rueckwaerts={auf} ersatz={auf ? 'noa_ref_stehend' : 'noa_B_hocken'} {...rest} />
|
||||
);
|
||||
|
||||
// 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}) => {
|
||||
107
remotion/src/baukasten/folge.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import React from 'react';
|
||||
import {AbsoluteFill, Composition, Series, getInputProps, useCurrentFrame} from 'remotion';
|
||||
import {SCHRIFT} from './untertitel';
|
||||
import type {Geplant} from './zeit';
|
||||
|
||||
// Eine Folge = Liste von Szenen. Daraus entstehen die Komposition der ganzen
|
||||
// Folge und je Szene eine eigene Komposition (zum einzelnen Rendern):
|
||||
// F02 ganze Folge
|
||||
// F02-03-... nur Szene 3
|
||||
// Mit --props='{"entwurf": true}' blendet jede Szene oben links Szene, Zeit
|
||||
// und Zeilen-ID ein, damit Rückmeldungen genau sagen können, wo etwas ist.
|
||||
|
||||
export type SzenenEintrag = {
|
||||
name: string;
|
||||
datei: string; // Quelldatei der Szene (für werkzeuge/render.mjs)
|
||||
plan: {dauer: number; liste: Geplant[]};
|
||||
K: React.FC;
|
||||
};
|
||||
|
||||
const BILD = {fps: 30, width: 1920, height: 1080} as const;
|
||||
|
||||
const zeit = (f: number) => {
|
||||
const s = Math.floor(f / 30);
|
||||
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
export const kennung = (kuerzel: string, i: number, name: string) =>
|
||||
`${kuerzel}-${String(i + 1).padStart(2, '0')}-${name
|
||||
.replace(/ä/g, 'ae')
|
||||
.replace(/ö/g, 'oe')
|
||||
.replace(/ü/g, 'ue')
|
||||
.replace(/ß/g, 'ss')
|
||||
.replace(/[^A-Za-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')}`;
|
||||
|
||||
const Entwurf: React.FC<{szenen: SzenenEintrag[]; index: number; versatz: number}> = ({szenen, index, versatz}) => {
|
||||
const f = useCurrentFrame();
|
||||
const sz = szenen[index];
|
||||
const zeile = sz.plan.liste.find((l) => f >= l.start && f < l.frei + 10);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 16,
|
||||
top: 14,
|
||||
padding: '6px 14px',
|
||||
borderRadius: 10,
|
||||
background: 'rgba(20, 16, 24, 0.72)',
|
||||
color: '#fff',
|
||||
fontFamily: SCHRIFT,
|
||||
fontSize: 26,
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{zeit(versatz + f)} · Szene {index + 1} „{sz.name}“ · {zeit(f)}
|
||||
{zeile ? ` · ${zeile.id}` : ''}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const istEntwurf = () => getInputProps().entwurf === true;
|
||||
|
||||
const versatzVon = (szenen: SzenenEintrag[], index: number) => szenen.slice(0, index).reduce((s, x) => s + x.plan.dauer, 0);
|
||||
|
||||
export const folgenDauer = (szenen: SzenenEintrag[]) => versatzVon(szenen, szenen.length);
|
||||
|
||||
const EinzelSzene: React.FC<{szenen: SzenenEintrag[]; index: number}> = ({szenen, index}) => {
|
||||
const {K} = szenen[index];
|
||||
return (
|
||||
<AbsoluteFill style={{background: '#f4ead8'}}>
|
||||
<K />
|
||||
{istEntwurf() && <Entwurf szenen={szenen} index={index} versatz={versatzVon(szenen, index)} />}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
// Komponenten einmal je Folge bauen (Kompositions-Props müssen ohne
|
||||
// Funktionen auskommen, darum stecken die Szenen in den Komponenten selbst).
|
||||
export const baueFolge = (kuerzel: string, szenen: SzenenEintrag[]) => {
|
||||
const Ganz: React.FC = () => (
|
||||
<AbsoluteFill style={{background: '#f4ead8'}}>
|
||||
<Series>
|
||||
{szenen.map((s, i) => (
|
||||
<Series.Sequence key={s.name} durationInFrames={s.plan.dauer} name={s.name}>
|
||||
<EinzelSzene szenen={szenen} index={i} />
|
||||
</Series.Sequence>
|
||||
))}
|
||||
</Series>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
const einzeln = szenen.map((s, i) => {
|
||||
const E: React.FC = () => <EinzelSzene szenen={szenen} index={i} />;
|
||||
return {id: kennung(kuerzel, i, s.name), K: E, dauer: s.plan.dauer, datei: s.datei, name: s.name};
|
||||
});
|
||||
return {kuerzel, dauer: folgenDauer(szenen), Ganz, einzeln};
|
||||
};
|
||||
export type Folge = ReturnType<typeof baueFolge>;
|
||||
|
||||
// Alle Kompositionen einer Folge (für Root.tsx)
|
||||
export const FolgenKompositionen: React.FC<{folge: Folge}> = ({folge}) => (
|
||||
<>
|
||||
<Composition id={folge.kuerzel} component={folge.Ganz} durationInFrames={folge.dauer} {...BILD} />
|
||||
{folge.einzeln.map((e) => (
|
||||
<Composition key={e.id} id={e.id} component={e.K} durationInFrames={e.dauer} {...BILD} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
|
@ -69,20 +69,41 @@ export const Blaetterregen: React.FC<{start: number; x0: number; y0: number; bod
|
|||
);
|
||||
};
|
||||
|
||||
// Ein Vogelschwarm zieht nach rechts oben davon.
|
||||
export const Voegel: React.FC<{start: number; anzahl?: number; skala?: number}> = ({start, anzahl = 11, skala = 1}) => {
|
||||
// Ein Vogel von der Seite (fliegt nach rechts). `schlag` -1 … 1 = Flügel unten … oben.
|
||||
const Vogel: React.FC<{x: number; y: number; g: number; schlag: number; neigung: number}> = ({x, y, g, schlag, neigung}) => {
|
||||
const h = -8 - schlag * 24; // Flügelspitze relativ zur Schulter
|
||||
const fluegel = (dx: number, dy: number) => `M ${3 + dx} ${-3 + dy} Q ${-3 + dx} ${h * 0.55 + dy} ${-15 + dx} ${h + dy} Q ${-6 + dx} ${h * 0.35 + dy} ${-7 + dx} ${1 + dy} Z`;
|
||||
return (
|
||||
<g transform={`translate(${x} ${y}) rotate(${neigung}) scale(${g / 30})`}>
|
||||
<path d={fluegel(4, -1)} fill="#3a3540" opacity={0.85} />
|
||||
<path d="M -14 0 L -27 -6 L -25 0 L -27 6 Z" fill="#4a4550" />
|
||||
<ellipse cx={0} cy={0} rx={16} ry={7} fill="#4f4a55" />
|
||||
<ellipse cx={1} cy={3} rx={11} ry={3.5} fill="#8a7f78" opacity={0.6} />
|
||||
<circle cx={15} cy={-4} r={6.5} fill="#4f4a55" />
|
||||
<path d="M 20 -6 L 28 -3.5 L 20 -1.5 Z" fill="#c98a3c" />
|
||||
<circle cx={17} cy={-5.5} r={1.4} fill="#f4ead8" />
|
||||
<path d={fluegel(0, 0)} fill="#5c5662" />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
// Ein Vogelschwarm zieht in V-Form nach rechts oben davon.
|
||||
export const Voegel: React.FC<{start: number; anzahl?: number; skala?: number}> = ({start, anzahl = 9, 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" />;
|
||||
const reihe = Math.ceil(i / 2) * (i % 2 ? 1 : -1); // Spitze vorn, Äste nach hinten
|
||||
const x = 160 + t * 5.5 - Math.abs(reihe) * 75 * skala + r('x') * 20;
|
||||
const y = 600 - t * 1.5 + reihe * 42 * skala + Math.sin(t * 0.05 + r('w') * 6) * 8;
|
||||
// Flügelschlag mit kurzen Gleitphasen
|
||||
const takt = t * 0.38 + r('p') * 6;
|
||||
const gleiten = Math.sin(t * 0.02 + r('q') * 6) > 0.6;
|
||||
const schlag = gleiten ? 0.15 : Math.sin(takt);
|
||||
const g = (34 + r('g') * 8) * skala * (1 - Math.abs(reihe) * 0.03);
|
||||
return <Vogel key={i} x={x} y={y} g={g} schlag={schlag} neigung={-12 + Math.cos(takt) * 3} />;
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -8,6 +8,10 @@ import './untertitel'; // lädt die Schrift für den Kalender
|
|||
const B = 1920;
|
||||
const H = 1080;
|
||||
|
||||
// Maße im Garten (passen zu den Bildern garten und garten_nacht)
|
||||
export const BODEN = 935; // Fußlinie der Figuren auf der Wiese
|
||||
export const NEST_X = 1480; // Platz vor dem Holzstapel
|
||||
|
||||
const Filter: React.FC = () => (
|
||||
<defs>
|
||||
<filter id="aq" x="-10%" y="-10%" width="120%" height="120%">
|
||||
|
|
@ -307,9 +311,13 @@ export const TischSVG: React.FC = () => (
|
|||
);
|
||||
|
||||
// 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];
|
||||
const K_FARBEN = ['#d9534f', '#e0a020', '#5f8a4a', '#3f5a8a', '#a0526b', '#c8642c'];
|
||||
const K_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];
|
||||
const kTuer = (i: number) => ({x: 330 + (i % 6) * 215, y: 250 + Math.floor(i / 6) * 180, farbe: K_FARBEN[i % K_FARBEN.length], nummer: K_NUMMERN[i]});
|
||||
|
||||
export const KalenderSVG: React.FC<{leer?: boolean}> = ({leer = false}) => {
|
||||
const farben = K_FARBEN;
|
||||
const nummern = K_NUMMERN;
|
||||
return (
|
||||
<svg viewBox={`0 0 ${B} ${H}`} style={{position: 'absolute', inset: 0, width: '100%', height: '100%'}}>
|
||||
<Filter />
|
||||
|
|
@ -319,7 +327,7 @@ export const KalenderSVG: React.FC = () => {
|
|||
))}
|
||||
<g transform="rotate(-2 960 540)">
|
||||
<rect x={250} y={90} width={1420} height={900} fill="#fdf8ec" filter="url(#aq)" />
|
||||
<g filter="url(#stift)">
|
||||
{!leer && <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>
|
||||
|
|
@ -340,7 +348,7 @@ export const KalenderSVG: React.FC = () => {
|
|||
{/* 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>}
|
||||
</g>
|
||||
<Korn staerke={0.15} />
|
||||
</svg>
|
||||
|
|
@ -348,8 +356,93 @@ export const KalenderSVG: React.FC = () => {
|
|||
};
|
||||
export const KALENDER_TUER1 = {x: 330 + 4 * 215 + 90, y: 250 + 1 * 180 + 75}; // Türchen „1“ (Index 10)
|
||||
|
||||
// Noa malt den Kalender (Blick von oben auf das Blatt, über `kalender_leer`).
|
||||
// `p` 0…1: Titel, dann die Türchen Zeile für Zeile. Die Stiftspitze folgt dem Strich.
|
||||
const drehe = (x: number, y: number) => {
|
||||
const w = (-2 * Math.PI) / 180;
|
||||
const dx = x - 960;
|
||||
const dy = y - 540;
|
||||
return {x: 960 + dx * Math.cos(w) - dy * Math.sin(w), y: 540 + dx * Math.sin(w) + dy * Math.cos(w)};
|
||||
};
|
||||
const umfangPunkt = (x: number, y: number, b: number, h: number, t: number) => {
|
||||
const u = 2 * (b + h);
|
||||
let d = t * u;
|
||||
if (d < b) return {x: x + d, y};
|
||||
d -= b;
|
||||
if (d < h) return {x: x + b, y: y + d};
|
||||
d -= h;
|
||||
if (d < b) return {x: x + b - d, y: y + h};
|
||||
d -= b;
|
||||
return {x, y: y + h - d};
|
||||
};
|
||||
|
||||
export const KalenderMalen: React.FC<{p: number; wackeln: number}> = ({p, wackeln}) => {
|
||||
const titelAnteil = 0.08;
|
||||
const tp = Math.min(1, p / titelAnteil);
|
||||
const tuerP = Math.max(0, (p - titelAnteil) / (1 - titelAnteil)) * 24;
|
||||
const aktiv = Math.min(23, Math.floor(tuerP));
|
||||
const innen = tuerP - aktiv;
|
||||
let spitze: {x: number; y: number};
|
||||
let farbe = '#c8642c';
|
||||
if (p < titelAnteil) {
|
||||
spitze = {x: 560 + tp * 800, y: 175 + Math.sin(tp * 60) * 14};
|
||||
} else {
|
||||
const t = kTuer(aktiv);
|
||||
farbe = t.farbe;
|
||||
spitze = innen < 0.75 ? umfangPunkt(t.x, t.y, 180, 150, innen / 0.75) : {x: t.x + 62 + Math.sin(innen * 40) * 18, y: t.y + 75 + Math.cos(innen * 30) * 22};
|
||||
}
|
||||
const s = drehe(spitze.x, spitze.y);
|
||||
s.x += Math.sin(wackeln * 0.9) * 2;
|
||||
s.y += Math.cos(wackeln * 1.3) * 2;
|
||||
return (
|
||||
<svg viewBox={`0 0 ${B} ${H}`} style={{position: 'absolute', inset: 0, width: '100%', height: '100%'}}>
|
||||
<Filter />
|
||||
<g transform="rotate(-2 960 540)">
|
||||
<g filter="url(#stift)">
|
||||
<clipPath id="titelclip">
|
||||
<rect x={540} y={100} width={tp * 840} height={120} />
|
||||
</clipPath>
|
||||
<text x={960} y={190} textAnchor="middle" fontSize={78} fontFamily="Fredoka, sans-serif" fontWeight={600} fill="#c8642c" clipPath="url(#titelclip)">
|
||||
Pinos Winterfest-Kalender
|
||||
</text>
|
||||
{new Array(24).fill(0).map((_, i) => {
|
||||
if (i > aktiv || p < titelAnteil) return null;
|
||||
const t = kTuer(i);
|
||||
const q = i < aktiv ? 1 : innen;
|
||||
return (
|
||||
<g key={i}>
|
||||
<rect x={t.x} y={t.y} width={180} height={150} rx={14} fill="none" stroke={t.farbe} strokeWidth={7} pathLength={1} strokeDasharray={1} strokeDashoffset={1 - Math.min(1, q / 0.75)} />
|
||||
{q > 0.75 && (
|
||||
<>
|
||||
<circle cx={t.x + 158} cy={t.y + 75} r={7} fill={t.farbe} />
|
||||
<text x={t.x + 82} y={t.y + 102} textAnchor="middle" fontSize={74} fontFamily="Fredoka, sans-serif" fontWeight={600} fill={t.farbe} opacity={Math.min(1, (q - 0.75) / 0.15)}>
|
||||
{t.nummer}
|
||||
</text>
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</g>
|
||||
{/* Noas Hand mit Buntstift, von unten rechts */}
|
||||
<g transform={`translate(${s.x} ${s.y}) rotate(-28)`}>
|
||||
<path d="M -9 -4 L 0 -24 L 9 -4 Z" fill="#f1d2a6" />
|
||||
<path d="M -3 -17 L 0 -24 L 3 -17 Z" fill={farbe} />
|
||||
<rect x={-11} y={-6} width={22} height={260} rx={4} fill={farbe} />
|
||||
<rect x={-11} y={-6} width={7} height={260} fill="rgba(255,255,255,0.18)" />
|
||||
<ellipse cx={-6} cy={150} rx={62} ry={56} fill="#f2c4a2" />
|
||||
<ellipse cx={-40} cy={118} rx={20} ry={26} fill="#f2c4a2" />
|
||||
<ellipse cx={30} cy={124} rx={22} ry={18} fill="#f2c4a2" />
|
||||
<path d="M -70 175 L 60 175 L 110 900 L -150 900 Z" fill="#d39b3a" />
|
||||
<path d="M -72 172 Q -6 196 62 172 L 66 200 Q -6 222 -76 200 Z" fill="#c1862c" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// 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}} />;
|
||||
export const Ort: React.FC<{name: string}> = ({name}) => <Img src={staticFile(`orte/${name}.webp`)} style={{position: 'absolute', inset: 0, width: B, height: H}} />;
|
||||
|
||||
// Kompositionen zum Vorberechnen
|
||||
export const ORTE: Record<string, React.FC> = {
|
||||
|
|
@ -360,6 +453,7 @@ export const ORTE: Record<string, React.FC> = {
|
|||
zimmer_wand: () => <ZimmerWandSVG />,
|
||||
zimmer_tisch: () => <TischSVG />,
|
||||
kalender: () => <KalenderSVG />,
|
||||
kalender_leer: () => <KalenderSVG leer />,
|
||||
};
|
||||
export const OrtStill: React.FC<{name: string}> = ({name}) => {
|
||||
const K = ORTE[name];
|
||||
|
|
@ -25,7 +25,7 @@ export const FIGUREN = {
|
|||
export const POSEN: Record<string, Pose> = {
|
||||
"pino_A1_augen-zu": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A1_augen-zu.webp",
|
||||
"datei": "bibliothek/figuren/pino_A1_augen-zu.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -39,7 +39,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A2_augen-halb": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A2_augen-halb.webp",
|
||||
"datei": "bibliothek/figuren/pino_A2_augen-halb.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -53,7 +53,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A3_lachen": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A3_lachen.webp",
|
||||
"datei": "bibliothek/figuren/pino_A3_lachen.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -67,7 +67,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A4_staunen": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A4_staunen.webp",
|
||||
"datei": "bibliothek/figuren/pino_A4_staunen.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -81,7 +81,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A5_blick-links": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A5_blick-links.webp",
|
||||
"datei": "bibliothek/figuren/pino_A5_blick-links.webp",
|
||||
"x": 297.5,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -95,7 +95,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A6_blick-rechts": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A6_blick-rechts.webp",
|
||||
"datei": "bibliothek/figuren/pino_A6_blick-rechts.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -109,7 +109,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A7-1_gehen": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A7-1_gehen.webp",
|
||||
"datei": "bibliothek/figuren/pino_A7-1_gehen.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -123,7 +123,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A7-2_gehen": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A7-2_gehen.webp",
|
||||
"datei": "bibliothek/figuren/pino_A7-2_gehen.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -137,7 +137,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A7-3_gehen": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A7-3_gehen.webp",
|
||||
"datei": "bibliothek/figuren/pino_A7-3_gehen.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -151,7 +151,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A7-4_gehen": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A7-4_gehen.webp",
|
||||
"datei": "bibliothek/figuren/pino_A7-4_gehen.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -165,7 +165,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A8_rueckansicht": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A8_rueckansicht.webp",
|
||||
"datei": "bibliothek/figuren/pino_A8_rueckansicht.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -179,7 +179,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A_hinterbeine": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A_hinterbeine.webp",
|
||||
"datei": "bibliothek/figuren/pino_A_hinterbeine.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -193,7 +193,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A_kugel": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A_kugel.webp",
|
||||
"datei": "bibliothek/figuren/pino_A_kugel.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -207,7 +207,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A_laufen-1": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A_laufen-1.webp",
|
||||
"datei": "bibliothek/figuren/pino_A_laufen-1.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -221,7 +221,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A_laufen-2": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A_laufen-2.webp",
|
||||
"datei": "bibliothek/figuren/pino_A_laufen-2.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -235,7 +235,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A_laufen-3": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A_laufen-3.webp",
|
||||
"datei": "bibliothek/figuren/pino_A_laufen-3.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -249,7 +249,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_A_laufen-4": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_A_laufen-4.webp",
|
||||
"datei": "bibliothek/figuren/pino_A_laufen-4.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -263,7 +263,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_B1_traurig": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_B1_traurig.webp",
|
||||
"datei": "bibliothek/figuren/pino_B1_traurig.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -277,7 +277,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_B2_erschrocken": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_B2_erschrocken.webp",
|
||||
"datei": "bibliothek/figuren/pino_B2_erschrocken.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -291,7 +291,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_B3_schlafen": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_B3_schlafen.webp",
|
||||
"datei": "bibliothek/figuren/pino_B3_schlafen.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -305,7 +305,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_B_blick-nach-oben": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_B_blick-nach-oben.webp",
|
||||
"datei": "bibliothek/figuren/pino_B_blick-nach-oben.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -319,7 +319,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_B_schlaeft-eingerollt": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_B_schlaeft-eingerollt.webp",
|
||||
"datei": "bibliothek/figuren/pino_B_schlaeft-eingerollt.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -333,7 +333,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"pino_ref_sitzend": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/figuren/pino_ref_sitzend.webp",
|
||||
"datei": "bibliothek/figuren/pino_ref_sitzend.webp",
|
||||
"x": 391,
|
||||
"y": 69,
|
||||
"b": 623,
|
||||
|
|
@ -347,7 +347,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A1_augen-zu": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A1_augen-zu.webp",
|
||||
"datei": "bibliothek/figuren/noa_A1_augen-zu.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -361,7 +361,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A2_augen-halb": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A2_augen-halb.webp",
|
||||
"datei": "bibliothek/figuren/noa_A2_augen-halb.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -375,7 +375,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A3_lachen": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A3_lachen.webp",
|
||||
"datei": "bibliothek/figuren/noa_A3_lachen.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -389,7 +389,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A4_staunen": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A4_staunen.webp",
|
||||
"datei": "bibliothek/figuren/noa_A4_staunen.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -403,7 +403,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A5_blick-links": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A5_blick-links.webp",
|
||||
"datei": "bibliothek/figuren/noa_A5_blick-links.webp",
|
||||
"x": 314.0,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -417,7 +417,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A6_blick-rechts": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A6_blick-rechts.webp",
|
||||
"datei": "bibliothek/figuren/noa_A6_blick-rechts.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -431,7 +431,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A7-1_gehen": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A7-1_gehen.webp",
|
||||
"datei": "bibliothek/figuren/noa_A7-1_gehen.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -445,7 +445,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A7-2_gehen": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A7-2_gehen.webp",
|
||||
"datei": "bibliothek/figuren/noa_A7-2_gehen.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -459,7 +459,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A7-3_gehen": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A7-3_gehen.webp",
|
||||
"datei": "bibliothek/figuren/noa_A7-3_gehen.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -473,7 +473,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A7-4_gehen": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A7-4_gehen.webp",
|
||||
"datei": "bibliothek/figuren/noa_A7-4_gehen.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -487,7 +487,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A8_rueckansicht": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A8_rueckansicht.webp",
|
||||
"datei": "bibliothek/figuren/noa_A8_rueckansicht.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -501,7 +501,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A_arm-hoch-winken": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A_arm-hoch-winken.webp",
|
||||
"datei": "bibliothek/figuren/noa_A_arm-hoch-winken.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -515,7 +515,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A_arme-hoch": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A_arme-hoch.webp",
|
||||
"datei": "bibliothek/figuren/noa_A_arme-hoch.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -529,7 +529,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_A_laterne": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_A_laterne.webp",
|
||||
"datei": "bibliothek/figuren/noa_A_laterne.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -543,7 +543,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_B1_traurig": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_B1_traurig.webp",
|
||||
"datei": "bibliothek/figuren/noa_B1_traurig.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -557,7 +557,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_B2_erschrocken": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_B2_erschrocken.webp",
|
||||
"datei": "bibliothek/figuren/noa_B2_erschrocken.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -571,7 +571,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_B3_schlafen": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_B3_schlafen.webp",
|
||||
"datei": "bibliothek/figuren/noa_B3_schlafen.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -585,7 +585,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_B_hocken": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_B_hocken.webp",
|
||||
"datei": "bibliothek/figuren/noa_B_hocken.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -599,7 +599,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_B_sitzt": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_B_sitzt.webp",
|
||||
"datei": "bibliothek/figuren/noa_B_sitzt.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -613,7 +613,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_B_zeigen": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_B_zeigen.webp",
|
||||
"datei": "bibliothek/figuren/noa_B_zeigen.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -627,7 +627,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"noa_ref_stehend": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/figuren/noa_ref_stehend.webp",
|
||||
"datei": "bibliothek/figuren/noa_ref_stehend.webp",
|
||||
"x": 339,
|
||||
"y": 156,
|
||||
"b": 715,
|
||||
|
|
@ -641,7 +641,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A1_augen-zu": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A1_augen-zu.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A1_augen-zu.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -655,7 +655,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A2_augen-halb": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A2_augen-halb.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A2_augen-halb.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -669,7 +669,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A3_lachen": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A3_lachen.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A3_lachen.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -683,7 +683,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A4_staunen": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A4_staunen.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A4_staunen.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -697,7 +697,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A5_blick-links": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A5_blick-links.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A5_blick-links.webp",
|
||||
"x": 373.5,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -711,7 +711,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A6_blick-rechts": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A6_blick-rechts.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A6_blick-rechts.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -725,7 +725,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A7-1_gehen": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A7-1_gehen.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A7-1_gehen.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -739,7 +739,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A7-2_gehen": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A7-2_gehen.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A7-2_gehen.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -753,7 +753,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A7-3_gehen": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A7-3_gehen.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A7-3_gehen.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -767,7 +767,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A7-4_gehen": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A7-4_gehen.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A7-4_gehen.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -781,7 +781,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A8_rueckansicht": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A8_rueckansicht.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A8_rueckansicht.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -795,7 +795,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A_apfel": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A_apfel.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A_apfel.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -809,7 +809,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_A_hals-unten": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_A_hals-unten.webp",
|
||||
"datei": "bibliothek/figuren/lulu_A_hals-unten.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -823,7 +823,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_B1_traurig": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_B1_traurig.webp",
|
||||
"datei": "bibliothek/figuren/lulu_B1_traurig.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -837,7 +837,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_B2_erschrocken": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_B2_erschrocken.webp",
|
||||
"datei": "bibliothek/figuren/lulu_B2_erschrocken.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -851,7 +851,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_B3_schlafen": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_B3_schlafen.webp",
|
||||
"datei": "bibliothek/figuren/lulu_B3_schlafen.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -865,7 +865,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_B_kopf-profil": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_B_kopf-profil.webp",
|
||||
"datei": "bibliothek/figuren/lulu_B_kopf-profil.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -879,7 +879,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
},
|
||||
"lulu_B_liegt": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/figuren/lulu_B_liegt.webp",
|
||||
"datei": "bibliothek/figuren/lulu_B_liegt.webp",
|
||||
"x": 322,
|
||||
"y": 23,
|
||||
"b": 698,
|
||||
|
|
@ -896,7 +896,7 @@ export const POSEN: Record<string, Pose> = {
|
|||
export const CLIPS: Record<string, ClipDaten> = {
|
||||
"lulu_C9_hals-runter": {
|
||||
"figur": "lulu",
|
||||
"datei": "folge02/clips/lulu_C9_hals-runter.webm",
|
||||
"datei": "bibliothek/clips/lulu_C9_hals-runter.webm",
|
||||
"x": 294.70404984423675,
|
||||
"y": 24.64797507788162,
|
||||
"b": 709.4330218068535,
|
||||
|
|
@ -905,7 +905,7 @@ export const CLIPS: Record<string, ClipDaten> = {
|
|||
},
|
||||
"noa_C4_winken": {
|
||||
"figur": "noa",
|
||||
"datei": "folge02/clips/noa_C4_winken.webm",
|
||||
"datei": "bibliothek/clips/noa_C4_winken.webm",
|
||||
"x": 529.3956386292834,
|
||||
"y": 158.6043613707165,
|
||||
"b": 413.6573208722741,
|
||||
|
|
@ -914,7 +914,7 @@ export const CLIPS: Record<string, ClipDaten> = {
|
|||
},
|
||||
"pino_C3_freudensprung": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/clips/pino_C3_freudensprung.webm",
|
||||
"datei": "bibliothek/clips/pino_C3_freudensprung.webm",
|
||||
"x": 382.5794392523364,
|
||||
"y": 0.0,
|
||||
"b": 512.2492211838006,
|
||||
|
|
@ -923,7 +923,7 @@ export const CLIPS: Record<string, ClipDaten> = {
|
|||
},
|
||||
"pino_C7_einrollen": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/clips/pino_C7_einrollen.webm",
|
||||
"datei": "bibliothek/clips/pino_C7_einrollen.webm",
|
||||
"x": 390.08099688473516,
|
||||
"y": 101.80685358255451,
|
||||
"b": 552.9719626168223,
|
||||
|
|
@ -932,7 +932,7 @@ export const CLIPS: Record<string, ClipDaten> = {
|
|||
},
|
||||
"pino_C8_aufrichten": {
|
||||
"figur": "pino",
|
||||
"datei": "folge02/clips/pino_C8_aufrichten.webm",
|
||||
"datei": "bibliothek/clips/pino_C8_aufrichten.webm",
|
||||
"x": 387.9376947040498,
|
||||
"y": 62.155763239875384,
|
||||
"b": 563.6884735202492,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import {interpolate, useCurrentFrame} from 'remotion';
|
||||
import {Audio, Sequence, getInputProps, getStaticFiles, interpolate, staticFile, useCurrentFrame} from 'remotion';
|
||||
import {loadFont} from '@remotion/google-fonts/Fredoka';
|
||||
import type {Geplant, Sprecher} from './zeit';
|
||||
|
||||
|
|
@ -22,13 +22,33 @@ const FARBE: Record<Sprecher, string> = {
|
|||
NOA_LULU: '#a0526b',
|
||||
};
|
||||
|
||||
// Untertitel für die Vorschau ohne Ton. Mit Sprachaufnahmen können sie aus.
|
||||
// Vorhandene Dateien in public/ (zum Bündeln bekannt). Fehlt eine Aufnahme
|
||||
// auf diesem Rechner, läuft die Szene ohne sie statt mit einem Fehler.
|
||||
const VORHANDEN = new Set(getStaticFiles().map((d) => d.name));
|
||||
|
||||
// Sprachaufnahmen der Zeilen abspielen
|
||||
export const Stimmen: React.FC<{zeilen: Geplant[]}> = ({zeilen}) => (
|
||||
<>
|
||||
{zeilen.map((l) =>
|
||||
l.audio && VORHANDEN.has(l.audio) ? (
|
||||
<Sequence key={l.id} from={l.start} durationInFrames={l.ende - l.start + 30} layout="none">
|
||||
<Audio src={staticFile(l.audio)} />
|
||||
</Sequence>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// Untertitel (für die Vorschau ohne Ton) und Stimmen. Untertitel lassen sich
|
||||
// beim Rendern abschalten: --props='{"untertitel": false}'
|
||||
export const Untertitel: React.FC<{zeilen: Geplant[]}> = ({zeilen}) => {
|
||||
const frame = useCurrentFrame();
|
||||
if (getInputProps().untertitel === false) return <Stimmen zeilen={zeilen} />;
|
||||
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 (
|
||||
<>
|
||||
<Stimmen zeilen={zeilen} />
|
||||
{z && z.wer && (
|
||||
<div
|
||||
style={{
|
||||
65
remotion/src/baukasten/zeit.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Zeitplan einer Szene aus ihren Sprechzeilen.
|
||||
//
|
||||
// Ohne Sprachaufnahme wird die Dauer jeder Zeile aus der Textlänge geschätzt.
|
||||
// Gibt es eine Aufnahme (werkzeuge/stimmen.ts schreibt sie in die
|
||||
// stimmen.json der Folge), gilt ihre echte Länge, 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
|
||||
};
|
||||
|
||||
// Aufnahmen einer Folge: Schlüssel „szene/zeile“
|
||||
export type Aufnahme = {datei: string; dauer: number; text: string};
|
||||
export type Aufnahmen = Record<string, Aufnahme>;
|
||||
|
||||
export type Geplant = Zeile & {start: number; ende: number; frei: number; audio?: string};
|
||||
|
||||
export type PlanOptionen = {
|
||||
vorlauf?: number; // Sekunden vor der ersten Zeile
|
||||
nachlauf?: number; // Sekunden nach der letzten Zeile
|
||||
szene?: string; // Schlüssel der Szene für die Aufnahmen
|
||||
aufnahmen?: Aufnahmen;
|
||||
ordner?: string; // Ordner der Aufnahmen in public/
|
||||
};
|
||||
|
||||
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, szene, aufnahmen, ordner}: PlanOptionen = {}) => {
|
||||
let t = vorlauf * FPS;
|
||||
const liste: Geplant[] = [];
|
||||
for (const z of zeilen) {
|
||||
// Aufnahme nur verwenden, wenn sie zum aktuellen Text passt
|
||||
const a = szene && z.text ? aufnahmen?.[`${szene}/${z.id}`] : undefined;
|
||||
const passt = a && a.text === z.text;
|
||||
const d = Math.round((passt ? a.dauer + 0.15 : 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, audio: passt && ordner ? `${ordner}/${a.datei}` : undefined});
|
||||
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, szene};
|
||||
};
|
||||
|
||||
export type Plan = ReturnType<typeof plane>;
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import {AbsoluteFill, Series} from 'remotion';
|
||||
import {INTRO, Intro, OUTRO, Outro} from './buch';
|
||||
import {INTRO_OPTIONEN, Intro, OUTRO, Outro, introZeilen} from '../baukasten/buch';
|
||||
import {baueFolge, type SzenenEintrag} from '../baukasten/folge';
|
||||
import {LIED, Lied} from './lied';
|
||||
import {plane} from './plan';
|
||||
import {S2, Szene2} from './szene2';
|
||||
import {S3, Szene3} from './szene3';
|
||||
import {S4, Szene4} from './szene4';
|
||||
|
|
@ -9,31 +10,21 @@ 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).
|
||||
// Folge 2 „Pino ist müde“ – komplett per Code aus der Figuren-Bibliothek.
|
||||
|
||||
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},
|
||||
const TITEL = 'Pino ist müde';
|
||||
const INTRO = plane('intro', introZeilen(TITEL), INTRO_OPTIONEN);
|
||||
|
||||
export const SZENEN: SzenenEintrag[] = [
|
||||
{name: 'Intro', datei: 'Folge02.tsx', plan: INTRO, K: () => <Intro plan={INTRO} titel={TITEL} posen={['pino_A2_augen-halb', 'noa_A3_lachen', 'lulu_A3_lachen']} />},
|
||||
{name: 'Gähnen überall', datei: 'szene2.tsx', plan: S2, K: Szene2},
|
||||
{name: 'Lulu sieht es von oben', datei: 'szene3.tsx', plan: S3, K: Szene3},
|
||||
{name: 'Ein Bett aus Blättern', datei: 'szene4.tsx', plan: S4, K: Szene4},
|
||||
{name: 'Lied', datei: 'lied.tsx', plan: LIED, K: Lied},
|
||||
{name: 'Pino will nicht schlafen', datei: 'szene5.tsx', plan: S5, K: Szene5},
|
||||
{name: 'Noas Idee', datei: 'szene6.tsx', plan: S6, K: Szene6},
|
||||
{name: 'Gute Nacht, Pino', datei: 'szene7.tsx', plan: S7, K: Szene7},
|
||||
{name: 'Outro', datei: 'Folge02.tsx', plan: OUTRO, 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>
|
||||
);
|
||||
export const F02 = baueFolge('F02', SZENEN);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,23 @@
|
|||
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';
|
||||
import {Figur, Kamera} from '../baukasten/figur';
|
||||
import {Blende, Vignette} from '../baukasten/helfer';
|
||||
import {BODEN, Ort} from '../baukasten/orte';
|
||||
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
|
||||
import {plane} from './plan';
|
||||
|
||||
// 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(
|
||||
export const LIED = plane('lied',
|
||||
[
|
||||
{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,
|
||||
{vorlauf: 1.0, nachlauf: 1.0},
|
||||
);
|
||||
|
||||
const Note: React.FC<{x: number; y: number; s: number; farbe: string}> = ({x, y, s, farbe}) => (
|
||||
|
|
|
|||
7
remotion/src/folge02/plan.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import {plane as basis, type PlanOptionen, type Zeile} from '../baukasten/zeit';
|
||||
import AUFNAHMEN from './stimmen.json';
|
||||
|
||||
// Zeitplan einer Szene dieser Folge; nimmt vorhandene Sprachaufnahmen
|
||||
// (public/stimmen/folge02/, erzeugt von werkzeuge/stimmen.ts) automatisch mit.
|
||||
export const plane = (szene: string, zeilen: Zeile[], opt: PlanOptionen = {}) =>
|
||||
basis(zeilen, {...opt, szene, aufnahmen: AUFNAHMEN, ordner: 'stimmen/folge02'});
|
||||
1
remotion/src/folge02/stimmen.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
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';
|
||||
import {Figur, Geher, Kamera} from '../baukasten/figur';
|
||||
import {Blende, Vignette, atmen, hopfStauch, huepfer, kamFahrt, ruck} from '../baukasten/helfer';
|
||||
import {Blaetterwind, Zzz} from '../baukasten/natur';
|
||||
import {BODEN, Ort} from '../baukasten/orte';
|
||||
import {Untertitel} from '../baukasten/untertitel';
|
||||
import {plane} from './plan';
|
||||
|
||||
// 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([
|
||||
export const S2 = plane('s2', [
|
||||
{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.'},
|
||||
|
|
@ -71,7 +70,7 @@ export const Szene2: React.FC = () => {
|
|||
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);
|
||||
const pinoY = BODEN + huepfer(f, schreck, 36, 12);
|
||||
|
||||
// Noa kommt von links
|
||||
const noaAn = z.n1.start - 4;
|
||||
|
|
@ -95,7 +94,7 @@ export const Szene2: React.FC = () => {
|
|||
];
|
||||
// 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 noaY = BODEN + kichern + huepfer(f, z.n1.start, 18, 10);
|
||||
|
||||
const kam = kamFahrt(f, [
|
||||
[0, 960, 540, 1.02],
|
||||
|
|
@ -115,8 +114,8 @@ export const Szene2: React.FC = () => {
|
|||
<>
|
||||
<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} />
|
||||
{noaGeht ? <Geher figur="noa" spiegeln x={noaX} y={BODEN} /> : <Figur pose={noaPose} x={noaX} y={noaY} stauchY={atmen(f, 2)} />}
|
||||
{pinoGeht ? <Geher figur="pino" x={pinoX} y={BODEN} tempo={1.3} /> : <Figur pose={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} />
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
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';
|
||||
import {ClipFigur, Figur, Geher, Kamera, NoaHocke} from '../baukasten/figur';
|
||||
import {Blende, Vignette, atmen, hopfStauch, huepfer, kamFahrt} from '../baukasten/helfer';
|
||||
import {Blaetterwind, Blatt, Voegel} from '../baukasten/natur';
|
||||
import {BODEN, Ort} from '../baukasten/orte';
|
||||
import {NOA_X, PINO_X} from './szene2';
|
||||
import {Untertitel} from '../baukasten/untertitel';
|
||||
import {plane} from './plan';
|
||||
|
||||
// Szene 3: Lulu sieht es von oben. Winterschlaf wird erklärt, Pino ist traurig.
|
||||
|
||||
export const LULU_X = 1520;
|
||||
|
||||
export const S3 = plane([
|
||||
export const S3 = plane('s3', [
|
||||
{id: 'bild0', dauer: 3.2},
|
||||
{id: 'bild1', dauer: 2.4},
|
||||
{id: 'l1', wer: 'LULU', text: 'Guten Morgen, ihr zwei!'},
|
||||
|
|
@ -55,7 +55,7 @@ export const Szene3: React.FC = () => {
|
|||
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)} />;
|
||||
lulu = <Geher figur="lulu" x={luluX} y={luluY} />;
|
||||
} 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) {
|
||||
|
|
@ -82,6 +82,13 @@ export const Szene3: React.FC = () => {
|
|||
[aufsBlatt, PINO_X],
|
||||
[aufsBlatt + 24, GROSSES_BLATT_X],
|
||||
]);
|
||||
// Das große Blatt segelt vom Apfelbaum herunter und landet neben Pino
|
||||
const blattFaellt = aufsBlatt - 80;
|
||||
const u = Math.min(1, Math.max(0, (f - blattFaellt) / 70));
|
||||
const schaukeln = Math.sin(u * 8) * (1 - u);
|
||||
const blattX = GROSSES_BLATT_X - 180 * (1 - u) + schaukeln * 70;
|
||||
const blattY = BODEN - 8 - 560 * (1 - u);
|
||||
const blattR = 88 + schaukeln * 45;
|
||||
const traurig = kurve(f, [
|
||||
[aufsBlatt + 20, 0],
|
||||
[aufsBlatt + 50, 1],
|
||||
|
|
@ -109,8 +116,8 @@ export const Szene3: React.FC = () => {
|
|||
// Noa kniet sich zu Pino
|
||||
const knien = z.bild3.start + 10;
|
||||
const noaX = kurve(f, [
|
||||
[knien, NOA_X + 20],
|
||||
[knien + 30, NOA_X + 140],
|
||||
[knien - 36, NOA_X + 20],
|
||||
[knien, NOA_X + 140],
|
||||
]);
|
||||
const noaPose: [number, string][] = [
|
||||
[0, 'noa_A6_blick-rechts'],
|
||||
|
|
@ -118,13 +125,8 @@ export const Szene3: React.FC = () => {
|
|||
[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 noaGeht = f >= knien - 36 && f < knien;
|
||||
|
||||
const kam = kamFahrt(f, [
|
||||
[0, 1150, 820, 1.7],
|
||||
|
|
@ -147,13 +149,13 @@ export const Szene3: React.FC = () => {
|
|||
{imOben ? (
|
||||
<Kamera x={kurve(f, [[obenStart, 900], [obenEnde, 1060]])} y={540} zoom={1.05}>
|
||||
<Ort name="oben" />
|
||||
<Voegel start={obenStart - 40} skala={1.9} />
|
||||
<Voegel start={obenStart - 40} skala={1.6} />
|
||||
</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)} />}
|
||||
{noaGeht ? <Geher figur="noa" spiegeln x={noaX} y={BODEN} /> : f >= knien ? <NoaHocke start={knien} x={noaX} y={BODEN} /> : <Figur pose={noaPose} x={noaX} y={BODEN} stauchY={atmen(f, 2)} />}
|
||||
{f > blattFaellt && <Blatt x={blattX} y={blattY} r={blattR} s={2.6} farbe="#c8642c" />}
|
||||
<Figur pose={pinoPose} x={pinoX} y={pinoY} stauchY={pinoStauchY} drehung={pinoDreh} blende={6} />
|
||||
{lulu}
|
||||
</Kamera>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
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';
|
||||
import {Figur, Geher, Kamera, NOA_HOCKE_FRAMES, NoaHocke} from '../baukasten/figur';
|
||||
import {Blende, LULU_MAUL_LINKS, Vignette, atmen, hopfStauch, huepfer, kamFahrt, punkt, ruck} from '../baukasten/helfer';
|
||||
import {Blaetterregen, Blatt, LAUB, Nest, Ruf} from '../baukasten/natur';
|
||||
import {BODEN, NEST_X, Ort} from '../baukasten/orte';
|
||||
import {NOA_X, PINO_X} from './szene2';
|
||||
import {Untertitel} from '../baukasten/untertitel';
|
||||
import {plane} from './plan';
|
||||
|
||||
// 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([
|
||||
export const S4 = plane('s4', [
|
||||
{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.'},
|
||||
|
|
@ -60,16 +59,24 @@ export const Szene4: React.FC = () => {
|
|||
]);
|
||||
const noaGeht = f > noaGehtVon && f < noaGehtBis;
|
||||
const noaPose: [number, string][] = [
|
||||
[0, 'noa_B_hocken'],
|
||||
[z.n1.start + 30, 'noa_A3_lachen'],
|
||||
[0, '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'],
|
||||
];
|
||||
// Hocke: Clip vorwärts (runter) oder rückwärts (aufstehen); sonst Standbilder
|
||||
const hocke = (() => {
|
||||
const w = (start: number, auf: boolean) => ({start, auf});
|
||||
if (f < z.n1.start + 30) return w(-1000, false);
|
||||
if (f < z.n1.start + 30 + NOA_HOCKE_FRAMES) return w(z.n1.start + 30, true);
|
||||
if (f >= z.n2.start - 15 && f < z.c5.ende) return w(z.n2.start - 15, false);
|
||||
if (f >= z.c5.ende && f < z.c5.ende + NOA_HOCKE_FRAMES) return w(z.c5.ende, true);
|
||||
if (f >= zurueck && f < z.e4.start) return w(-1000, false);
|
||||
if (f >= z.e4.start && f < z.e4.start + NOA_HOCKE_FRAMES) return w(z.e4.start, true);
|
||||
return null;
|
||||
})();
|
||||
// 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)
|
||||
|
|
@ -181,17 +188,24 @@ export const Szene4: React.FC = () => {
|
|||
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} />
|
||||
{noaGeht ? (
|
||||
<Geher figur="noa" spiegeln x={noaX} y={BODEN} />
|
||||
) : hocke ? (
|
||||
<NoaHocke start={hocke.start} auf={hocke.auf} x={noaX + schieben * 10} y={BODEN} stauchY={1 - 0.03 * beugen} drehung={beugen * 3 + schieben * 2} />
|
||||
) : (
|
||||
<Figur pose={noaPose} x={noaX} y={BODEN} stauchY={atmen(f, 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>
|
||||
) : pinoGeht ? (
|
||||
<Geher figur="pino" spiegeln={pinoSpiegel} x={pinoX} y={BODEN} tempo={1.3} />
|
||||
) : (
|
||||
<Figur
|
||||
pose={pinoGeht ? gehPose('pino', f, 8) : pinoPose}
|
||||
spiegeln={pinoSpiegel}
|
||||
pose={pinoPose}
|
||||
x={pinoX}
|
||||
y={BODEN + zaehlHuepfer + huepfer(f, niesen + 2, 30, 12) + (pinoGeht ? gehWippen(f, 8, 4) : 0)}
|
||||
y={BODEN + zaehlHuepfer + huepfer(f, niesen + 2, 30, 12)}
|
||||
stauchY={atmen(f, 1) * hopfStauch(f, niesen + 2)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
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';
|
||||
import {ClipFigur, Figur, Kamera} from '../baukasten/figur';
|
||||
import {Blende, Vignette, atmen, hopfStauch, huepfer, kamFahrt, ruck} from '../baukasten/helfer';
|
||||
import {Funkeln, Nest} from '../baukasten/natur';
|
||||
import {BODEN, NEST_X, Ort} from '../baukasten/orte';
|
||||
import {SCHRIFT, Untertitel} from '../baukasten/untertitel';
|
||||
import {plane} from './plan';
|
||||
|
||||
// Szene 5: Pino will nicht schlafen. Noa hat eine Idee.
|
||||
|
||||
|
|
@ -16,7 +14,7 @@ const NOA_X5 = 1150;
|
|||
const PINO_RAUS_X = 1370;
|
||||
const LULU_X5 = 1790;
|
||||
|
||||
export const S5 = plane([
|
||||
export const S5 = plane('s5', [
|
||||
{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!'},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
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';
|
||||
import {ClipFigur, Figur, Kamera} from '../baukasten/figur';
|
||||
import {Blende, Vignette, atmen, huepfer, kamFahrt} from '../baukasten/helfer';
|
||||
import {Funkeln} from '../baukasten/natur';
|
||||
import {KALENDER_TUER1, KalenderMalen, Ort} from '../baukasten/orte';
|
||||
import {Untertitel} from '../baukasten/untertitel';
|
||||
import {plane} from './plan';
|
||||
|
||||
// Szene 6: Noas Idee. Abends in Noas Zimmer, Noa malt den Kalender.
|
||||
|
||||
|
|
@ -16,8 +16,9 @@ const PINO_X6 = 1090;
|
|||
const TISCH_Y = 802;
|
||||
const LULU_X6 = 1440;
|
||||
|
||||
export const S6 = plane([
|
||||
export const S6 = plane('s6', [
|
||||
{id: 'bild0', dauer: 3.0},
|
||||
{id: 'malen', dauer: 4.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.'},
|
||||
|
|
@ -29,27 +30,16 @@ export const S6 = plane([
|
|||
{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;
|
||||
// Noa malt: Blick von oben aufs Blatt, während „malen“ und während sie antwortet (n1)
|
||||
const malP = kurve(f, [
|
||||
[z.malen.start, 0],
|
||||
[z.n1.ende, 1],
|
||||
]);
|
||||
const malSicht = (f >= z.malen.start && f < z.l1.start) || (f >= z.n1.start && f < z.n1.ende + 6);
|
||||
const malt = f < z.n1.ende;
|
||||
|
||||
const luluX = kurve(f, [
|
||||
|
|
@ -81,6 +71,19 @@ export const Szene6: React.FC = () => {
|
|||
[dauer, KALENDER_TUER1.x - 40, KALENDER_TUER1.y, 1.6],
|
||||
]);
|
||||
|
||||
if (malSicht) {
|
||||
return (
|
||||
<>
|
||||
<Kamera x={kurve(f, [[z.malen.start, 900], [z.n1.ende, 1020]])} y={560} zoom={1.12}>
|
||||
<Ort name="kalender_leer" />
|
||||
<KalenderMalen p={malP} wackeln={f} />
|
||||
</Kamera>
|
||||
<Vignette />
|
||||
<Untertitel zeilen={S6.liste} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (kalender) {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -141,8 +144,6 @@ export const Szene6: React.FC = () => {
|
|||
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} />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
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';
|
||||
import {ClipFigur, Figur, Kamera} from '../baukasten/figur';
|
||||
import {Blende, Vignette, atmen, kamFahrt} from '../baukasten/helfer';
|
||||
import {Nest, Schnee, Zzz} from '../baukasten/natur';
|
||||
import {BODEN, NEST_X, Ort} from '../baukasten/orte';
|
||||
import {Untertitel} from '../baukasten/untertitel';
|
||||
import {plane} from './plan';
|
||||
|
||||
// Szene 7: Gute Nacht, Pino. Nacht im Garten, die erste Schneeflocke fällt.
|
||||
|
||||
|
|
@ -15,7 +13,7 @@ const NOA_X7 = 1140;
|
|||
const LULU_X7 = 1800;
|
||||
const LATERNE = {x: 1290, y: BODEN + 10};
|
||||
|
||||
export const S7 = plane(
|
||||
export const S7 = plane('s7',
|
||||
[
|
||||
{id: 'bild0', dauer: 3.4},
|
||||
{id: 'n1', wer: 'NOA', text: 'Schlaf gut, Pino. Nur bis morgen.', leise: true},
|
||||
|
|
@ -24,8 +22,7 @@ export const S7 = plane(
|
|||
{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,
|
||||
{vorlauf: 0.5, nachlauf: 1.6},
|
||||
);
|
||||
|
||||
const Laterne: React.FC<{frame: number}> = ({frame}) => {
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
// 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};
|
||||
};
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
# 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
|
||||
# Ausgabe: public/bibliothek/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
|
||||
|
|
@ -12,8 +12,8 @@ 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")
|
||||
ZIEL = os.path.join(HIER, "..", "public", "bibliothek", "figuren")
|
||||
TS = os.path.join(HIER, "..", "src", "baukasten", "posen.ts")
|
||||
os.makedirs(ZIEL, exist_ok=True)
|
||||
|
||||
# Referenzpose je Figur: deren Höhe gilt als „Figurhöhe“, deren Fußpunkt als Anker.
|
||||
|
|
@ -31,7 +31,7 @@ for figur in REF:
|
|||
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],
|
||||
posen[n] = {"figur": figur, "datei": f"bibliothek/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.
|
||||
|
|
@ -52,7 +52,7 @@ if CLIPS:
|
|||
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",
|
||||
clips[n] = {"figur": n.split("_")[0], "datei": f"bibliothek/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"]}
|
||||
|
||||
|
|
|
|||
116
remotion/werkzeuge/render.mjs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Rendert eine Folge Szene für Szene und klebt sie zusammen.
|
||||
// Szenen, an denen sich nichts geändert hat, werden nicht neu gerendert.
|
||||
//
|
||||
// node werkzeuge/render.mjs F02 ganze Folge, volle Qualität
|
||||
// node werkzeuge/render.mjs F02 --entwurf halbe Auflösung + Zeit/Szene eingeblendet
|
||||
// node werkzeuge/render.mjs F02 --szene 3,5 nur diese Szenen (neu) rendern
|
||||
// node werkzeuge/render.mjs F02 --neu alles neu, Zwischenstand ignorieren
|
||||
// node werkzeuge/render.mjs F02 --kerne 6 Anzahl paralleler Browser-Tabs
|
||||
// node werkzeuge/render.mjs F02 --ziel /shared/youtube/folge02/folge02.mp4
|
||||
//
|
||||
// Läuft unter Linux und Windows. Ergebnis und Zwischenstand: out/<kürzel>/
|
||||
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import {createHash} from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const WURZEL = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
process.chdir(WURZEL);
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const kuerzel = args.find((a) => !a.startsWith('--'));
|
||||
if (!kuerzel) {
|
||||
console.error('Bitte Folge angeben, z. B.: node werkzeuge/render.mjs F02');
|
||||
process.exit(1);
|
||||
}
|
||||
const wert = (name) => {
|
||||
const i = args.indexOf(name);
|
||||
return i >= 0 ? args[i + 1] : undefined;
|
||||
};
|
||||
const entwurf = args.includes('--entwurf');
|
||||
const neu = args.includes('--neu');
|
||||
const nurSzenen = wert('--szene')?.split(',').map(Number);
|
||||
const kerne = Number(wert('--kerne') ?? Math.max(1, os.cpus().length - 1));
|
||||
const ziel = wert('--ziel');
|
||||
|
||||
const nummer = kuerzel.replace(/\D/g, '');
|
||||
const ordner = `src/folge${nummer}`;
|
||||
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
||||
const lauf = (cmd, a, opt = {}) => {
|
||||
const r = spawnSync(cmd, a, {stdio: opt.still ? 'pipe' : 'inherit', encoding: 'utf8', shell: process.platform === 'win32'});
|
||||
if (r.status !== 0) {
|
||||
if (opt.still) console.error(r.stdout, r.stderr);
|
||||
throw new Error(`Fehlgeschlagen: ${cmd} ${a.join(' ')}`);
|
||||
}
|
||||
return r.stdout;
|
||||
};
|
||||
|
||||
// Szenenliste aus dem Code der Folge holen
|
||||
const liste = JSON.parse(
|
||||
lauf(npx, ['tsx', 'werkzeuge/szenenliste.mts', `./${ordner}/Folge${nummer}.tsx`, kuerzel], {still: true})
|
||||
.split('\n')
|
||||
.filter((z) => z.startsWith('['))
|
||||
.pop(),
|
||||
);
|
||||
|
||||
// Fingerabdruck: gemeinsamer Code + Bilder/Clips/Stimmen + eigene Szenendatei
|
||||
const hashDatei = (h, p) => {
|
||||
const st = fs.statSync(p);
|
||||
if (st.isDirectory()) {
|
||||
for (const n of fs.readdirSync(p).sort()) hashDatei(h, path.join(p, n));
|
||||
} else {
|
||||
h.update(p.replaceAll('\\', '/'));
|
||||
h.update(fs.readFileSync(p));
|
||||
}
|
||||
};
|
||||
const szenenDateien = new Set(liste.map((s) => s.datei));
|
||||
const gemeinsam = createHash('sha256');
|
||||
for (const p of ['src/baukasten', 'src/bewegung.ts', 'public', 'package-lock.json']) if (fs.existsSync(p)) hashDatei(gemeinsam, p);
|
||||
for (const n of fs.readdirSync(ordner).sort()) {
|
||||
const p = path.join(ordner, n);
|
||||
if (!szenenDateien.has(n) || n === `Folge${nummer}.tsx`) hashDatei(gemeinsam, p);
|
||||
}
|
||||
const basis = gemeinsam.digest('hex');
|
||||
|
||||
const qualitaet = entwurf ? 'entwurf' : 'voll';
|
||||
const aus = path.join('out', kuerzel, qualitaet);
|
||||
fs.mkdirSync(aus, {recursive: true});
|
||||
const propsDatei = path.join(aus, 'props.json');
|
||||
fs.writeFileSync(propsDatei, JSON.stringify({entwurf}));
|
||||
const gl = process.platform === 'linux' ? 'swangle' : 'angle';
|
||||
|
||||
const teile = [];
|
||||
const start = Date.now();
|
||||
for (const [i, s] of liste.entries()) {
|
||||
const h = createHash('sha256');
|
||||
h.update(basis + qualitaet + s.dauer);
|
||||
hashDatei(h, path.join(ordner, s.datei));
|
||||
const fp = h.digest('hex');
|
||||
const datei = path.join(aus, `${s.id}.mp4`);
|
||||
const fpDatei = datei + '.fingerabdruck';
|
||||
const aktuell = fs.existsSync(datei) && fs.existsSync(fpDatei) && fs.readFileSync(fpDatei, 'utf8') === fp;
|
||||
const gewaehlt = nurSzenen ? nurSzenen.includes(i + 1) : !aktuell || neu;
|
||||
teile.push(datei);
|
||||
if (!gewaehlt) {
|
||||
console.log(`= Szene ${i + 1} „${s.name}“ unverändert`);
|
||||
continue;
|
||||
}
|
||||
console.log(`▶ Szene ${i + 1} „${s.name}“ (${(s.dauer / 30).toFixed(1)} s) …`);
|
||||
const t0 = Date.now();
|
||||
const a = ['remotion', 'render', 'src/index.ts', s.id, datei, `--concurrency=${kerne}`, `--gl=${gl}`, `--props=${propsDatei}`, '--log=error'];
|
||||
if (entwurf) a.push('--scale=0.5');
|
||||
lauf(npx, a);
|
||||
fs.writeFileSync(fpDatei, fp);
|
||||
const sek = (Date.now() - t0) / 1000;
|
||||
console.log(` fertig in ${sek.toFixed(0)} s (${(s.dauer / sek).toFixed(1)} Bilder/s)`);
|
||||
}
|
||||
|
||||
// Zusammenkleben (ohne neu zu kodieren)
|
||||
const listeDatei = path.join(aus, 'teile.txt');
|
||||
fs.writeFileSync(listeDatei, teile.map((t) => `file '${path.resolve(t).replaceAll('\\', '/')}'`).join('\n'));
|
||||
const ergebnis = ziel ?? path.join('out', kuerzel, `${kuerzel}${entwurf ? '-entwurf' : ''}.mp4`);
|
||||
lauf(npx, ['remotion', 'ffmpeg', '-v', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', listeDatei, '-c', 'copy', '-movflags', '+faststart', ergebnis]);
|
||||
console.log(`\nFertig: ${ergebnis} (${((Date.now() - start) / 60000).toFixed(1)} min)`);
|
||||
13
remotion/werkzeuge/stimmen.config.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"modell": "eleven_multilingual_v2",
|
||||
"format": "mp3_44100_128",
|
||||
"einstellungen": {"stability": 0.5, "similarity_boost": 0.75, "style": 0.2},
|
||||
"stimmen": {
|
||||
"ERZ": "",
|
||||
"PINO": "",
|
||||
"NOA": "",
|
||||
"LULU": "",
|
||||
"ALLE": "ERZ",
|
||||
"NOA_LULU": "NOA"
|
||||
}
|
||||
}
|
||||
102
remotion/werkzeuge/stimmen.mts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Erzeugt die Sprachaufnahmen einer Folge mit ElevenLabs und trägt ihre
|
||||
// Länge in src/folgeNN/stimmen.json ein. Danach passt sich der Zeitplan der
|
||||
// Szenen automatisch an die echten Längen an.
|
||||
//
|
||||
// ELEVENLABS_API_KEY=… npx tsx werkzeuge/stimmen.mts F02
|
||||
// … F02 --szene s3 nur eine Szene
|
||||
// … F02 --neu alle Zeilen neu erzeugen
|
||||
// … F02 --probe nur anzeigen, was erzeugt würde (kostet nichts)
|
||||
//
|
||||
// Der Schlüssel kommt nur aus der Umgebungsvariable, nie aus einer Datei im Repo.
|
||||
// Stimmen-IDs und Modell stehen in werkzeuge/stimmen.config.json.
|
||||
// Aufnahmen landen in public/stimmen/folgeNN/ (nicht im Git; auf dem VPS ein
|
||||
// Link nach /shared/youtube/stimmen/). Nur Zeilen, deren Text sich geändert
|
||||
// hat oder deren Datei fehlt, werden neu erzeugt.
|
||||
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const WURZEL = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
process.chdir(WURZEL);
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const kuerzel = args.find((a) => !a.startsWith('--'));
|
||||
const wert = (n: string) => (args.includes(n) ? args[args.indexOf(n) + 1] : undefined);
|
||||
const nurSzene = wert('--szene');
|
||||
const neu = args.includes('--neu');
|
||||
const probe = args.includes('--probe');
|
||||
if (!kuerzel) throw new Error('Folge angeben, z. B. F02');
|
||||
|
||||
const nummer = kuerzel.replace(/\D/g, '');
|
||||
const cfg = JSON.parse(fs.readFileSync('werkzeuge/stimmen.config.json', 'utf8')) as {
|
||||
modell: string;
|
||||
format: string;
|
||||
stimmen: Record<string, string>;
|
||||
einstellungen?: Record<string, unknown>;
|
||||
};
|
||||
const schluessel = process.env.ELEVENLABS_API_KEY;
|
||||
if (!schluessel && !probe) throw new Error('ELEVENLABS_API_KEY ist nicht gesetzt.');
|
||||
|
||||
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
||||
const ausgabe = spawnSync(npx, ['tsx', 'werkzeuge/szenenliste.mts', `./src/folge${nummer}/Folge${nummer}.tsx`, kuerzel], {encoding: 'utf8', shell: process.platform === 'win32'});
|
||||
const szenen = JSON.parse(ausgabe.stdout.split('\n').filter((z) => z.startsWith('[')).pop()!) as {
|
||||
szene?: string;
|
||||
zeilen: {id: string; wer?: string; text: string; leise?: boolean}[];
|
||||
}[];
|
||||
|
||||
const jsonDatei = `src/folge${nummer}/stimmen.json`;
|
||||
const aufnahmen: Record<string, {datei: string; dauer: number; text: string}> = JSON.parse(fs.readFileSync(jsonDatei, 'utf8'));
|
||||
const ordner = `public/stimmen/folge${nummer}`;
|
||||
fs.mkdirSync(ordner, {recursive: true});
|
||||
|
||||
// Stimme je Sprecher; ein Eintrag kann auf einen anderen Sprecher verweisen (z. B. ALLE → ERZ)
|
||||
const stimmeVon = (wer: string): string | undefined => {
|
||||
const v = cfg.stimmen[wer];
|
||||
if (!v) return undefined;
|
||||
return v in cfg.stimmen ? stimmeVon(v) : v;
|
||||
};
|
||||
|
||||
const laenge = (datei: string) => {
|
||||
const r = spawnSync(npx, ['remotion', 'ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', datei], {
|
||||
encoding: 'utf8',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
return Math.round(parseFloat(r.stdout.trim()) * 100) / 100;
|
||||
};
|
||||
|
||||
let zeichen = 0;
|
||||
let erzeugt = 0;
|
||||
for (const s of szenen) {
|
||||
if (!s.szene || (nurSzene && s.szene !== nurSzene)) continue;
|
||||
for (const z of s.zeilen) {
|
||||
const schl = `${s.szene}/${z.id}`;
|
||||
const datei = `${s.szene}-${z.id}.mp3`;
|
||||
const pfad = path.join(ordner, datei);
|
||||
const alt = aufnahmen[schl];
|
||||
if (!neu && alt && alt.text === z.text && fs.existsSync(pfad)) continue;
|
||||
const stimme = z.wer && stimmeVon(z.wer);
|
||||
if (!stimme) {
|
||||
console.warn(`! ${schl}: keine Stimme für „${z.wer}“ in stimmen.config.json`);
|
||||
continue;
|
||||
}
|
||||
zeichen += z.text.length;
|
||||
if (probe) {
|
||||
console.log(`~ ${schl} (${z.wer}): ${z.text}`);
|
||||
continue;
|
||||
}
|
||||
const antwort = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${stimme}?output_format=${cfg.format}`, {
|
||||
method: 'POST',
|
||||
headers: {'xi-api-key': schluessel!, 'Content-Type': 'application/json', Accept: 'audio/mpeg'},
|
||||
body: JSON.stringify({text: z.text, model_id: cfg.modell, voice_settings: cfg.einstellungen}),
|
||||
});
|
||||
if (!antwort.ok) throw new Error(`${schl}: ElevenLabs antwortet ${antwort.status} ${await antwort.text()}`);
|
||||
fs.writeFileSync(pfad, Buffer.from(await antwort.arrayBuffer()));
|
||||
aufnahmen[schl] = {datei, dauer: laenge(pfad), text: z.text};
|
||||
fs.writeFileSync(jsonDatei, JSON.stringify(aufnahmen, null, '\t') + '\n');
|
||||
erzeugt++;
|
||||
console.log(`✓ ${schl} ${aufnahmen[schl].dauer} s`);
|
||||
}
|
||||
}
|
||||
console.log(`\n${probe ? 'Würde erzeugen' : 'Erzeugt'}: ${probe ? '' : erzeugt + ' Zeilen, '}${zeichen} Zeichen`);
|
||||