diff --git a/remotion/package-lock.json b/remotion/package-lock.json
index 7c509ad..0b61520 100644
--- a/remotion/package-lock.json
+++ b/remotion/package-lock.json
@@ -10,10 +10,12 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"remotion": "^4.0.300",
+ "three": "^0.169.0",
"typescript": "^5.6.3"
},
"devDependencies": {
- "@types/react": "^18.3.12"
+ "@types/react": "^18.3.12",
+ "@types/three": "^0.169.0"
}
},
"node_modules/@babel/parser": {
@@ -807,6 +809,13 @@
"zod": "3.22.3"
}
},
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@@ -866,6 +875,35 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.169.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.169.0.tgz",
+ "integrity": "sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": "*",
+ "@webgpu/types": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~0.18.1"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -1022,6 +1060,13 @@
"@xtuc/long": "4.2.2"
}
},
+ "node_modules/@webgpu/types": {
+ "version": "0.1.74",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.74.tgz",
+ "integrity": "sha512-lgiI4hbuLcI9unnm2cL/tvCaQU45dp0xcLWJh5uB/9MBGvIA4F8XIk7nSiBeg+K4xWGYwgZKn80LmERI5CxoTA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -1575,6 +1620,13 @@
"pend": "~1.2.0"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
@@ -1802,6 +1854,13 @@
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"license": "MIT"
},
+ "node_modules/meshoptimizer": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
+ "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -2625,6 +2684,12 @@
}
}
},
+ "node_modules/three": {
+ "version": "0.169.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.169.0.tgz",
+ "integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==",
+ "license": "MIT"
+ },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
diff --git a/remotion/package.json b/remotion/package.json
index 84aea88..acdc28e 100644
--- a/remotion/package.json
+++ b/remotion/package.json
@@ -10,9 +10,11 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"remotion": "^4.0.300",
+ "three": "^0.169.0",
"typescript": "^5.6.3"
},
"devDependencies": {
- "@types/react": "^18.3.12"
+ "@types/react": "^18.3.12",
+ "@types/three": "^0.169.0"
}
}
diff --git a/remotion/public/figuren/lulu.png b/remotion/public/figuren/lulu.png
index b101fa6..b3c71ba 100644
Binary files a/remotion/public/figuren/lulu.png and b/remotion/public/figuren/lulu.png differ
diff --git a/remotion/public/hintergrund/abend.png b/remotion/public/hintergrund/abend.png
new file mode 100644
index 0000000..b24a21c
Binary files /dev/null and b/remotion/public/hintergrund/abend.png differ
diff --git a/remotion/public/hintergrund/papier.png b/remotion/public/hintergrund/papier.png
new file mode 100644
index 0000000..f5d7dac
Binary files /dev/null and b/remotion/public/hintergrund/papier.png differ
diff --git a/remotion/public/hintergrund/vordergrund.png b/remotion/public/hintergrund/vordergrund.png
new file mode 100644
index 0000000..d25a170
Binary files /dev/null and b/remotion/public/hintergrund/vordergrund.png differ
diff --git a/remotion/src/Root.tsx b/remotion/src/Root.tsx
index ea03b57..e053309 100644
--- a/remotion/src/Root.tsx
+++ b/remotion/src/Root.tsx
@@ -1,14 +1,17 @@
import React from 'react';
import {Composition} from 'remotion';
+import {HintergrundAbend, PapierStruktur, VordergrundAbend} from './szenen/Hintergruende';
import {LaterneTest} from './szenen/LaterneTest';
+import {VerformTest} from './szenen/VerformTest';
+
+const BILD = {fps: 30, width: 1920, height: 1080} as const;
export const Root: React.FC = () => (
-
+ <>
+
+
+
+
+
+ >
);
diff --git a/remotion/src/bewegung.ts b/remotion/src/bewegung.ts
new file mode 100644
index 0000000..5d5e7c9
--- /dev/null
+++ b/remotion/src/bewegung.ts
@@ -0,0 +1,43 @@
+// Hilfen für weiche Bewegung: Kurven, Federn, Gewichte für die Verformung.
+
+export const klemm01 = (v: number) => Math.min(1, Math.max(0, v));
+
+// Weicher Übergang 0 → 1 zwischen a und b (a > b ist erlaubt und kehrt die Richtung um).
+export const glatt = (a: number, b: number, v: number) => {
+ const t = klemm01((v - a) / (b - a));
+ return t * t * (3 - 2 * t);
+};
+
+// Ease-in-out zwischen Schlüsselwerten: [[frame, wert], ...]
+export const kurve = (frame: number, punkte: [number, number][]) => {
+ if (frame <= punkte[0][0]) return punkte[0][1];
+ for (let i = 1; i < punkte.length; i++) {
+ const [f0, v0] = punkte[i - 1];
+ const [f1, v1] = punkte[i];
+ if (frame <= f1) {
+ const t = (frame - f0) / (f1 - f0);
+ return v0 + (v1 - v0) * (0.5 - 0.5 * Math.cos(Math.PI * t));
+ }
+ }
+ return punkte[punkte.length - 1][1];
+};
+
+// Gedämpfte Feder, die einem Zielsignal nachläuft (für Nachschwingen von
+// Bommel, Schal, Schwanz). Wird pro Frame deterministisch von 0 an simuliert.
+export const feder = (
+ ziel: (f: number) => number,
+ frame: number,
+ {steifheit = 0.08, daempfung = 0.18} = {},
+) => {
+ let x = ziel(0);
+ let v = 0;
+ for (let f = 1; f <= frame; f++) {
+ v += (ziel(f) - x) * steifheit - v * daempfung;
+ x += v;
+ }
+ return x;
+};
+
+// Gewicht 1 innerhalb von r0 um (cx, cy), fällt bis r1 auf 0.
+export const kreis = (cx: number, cy: number, r0: number, r1: number) => (x: number, y: number) =>
+ glatt(r1, r0, Math.hypot(x - cx, y - cy));
diff --git a/remotion/src/szenen/Hintergruende.tsx b/remotion/src/szenen/Hintergruende.tsx
new file mode 100644
index 0000000..b33bac2
--- /dev/null
+++ b/remotion/src/szenen/Hintergruende.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import {AbsoluteFill} from 'remotion';
+import {Abendhuegel, Papier, Vordergrund} from '../teile/Abendhuegel';
+
+// Standbilder zum Vorberechnen (npx remotion still …). Die SVG-Filter sind
+// teuer; als PNG kosten sie beim Rendern der Szenen fast nichts.
+
+export const HintergrundAbend: React.FC = () => ;
+
+export const VordergrundAbend: React.FC = () => ;
+
+// Nur die Papierstruktur, ohne Vignette (die ist billig und bleibt in der Szene).
+export const PapierStruktur: React.FC = () => (
+
+
+
+);
diff --git a/remotion/src/szenen/VerformTest.tsx b/remotion/src/szenen/VerformTest.tsx
new file mode 100644
index 0000000..13f756a
--- /dev/null
+++ b/remotion/src/szenen/VerformTest.tsx
@@ -0,0 +1,139 @@
+import React from 'react';
+import {AbsoluteFill, Img, staticFile, useCurrentFrame} from 'remotion';
+import {feder, glatt, kreis, kurve} from '../bewegung';
+import {LULU, PINO} from '../figuren';
+import {VerformFigur, type Knochen} from '../teile/VerformFigur';
+
+// Test für weiche Animation (6 s, ohne Ton): Lulu beugt den Hals zu Pino
+// hinunter, Pino freut sich und hüpft. Alles über Gitter-Verformung und
+// Federn, Hintergrund und Papier sind vorberechnete Bilder.
+
+const BODEN = 884;
+
+// ---------- Lulu ----------
+const halsKurve = (f: number) =>
+ kurve(f, [
+ [0, 0],
+ [26, 0],
+ [38, 5], // Anlauf: erst kurz zurück
+ [74, -30],
+ [118, -30],
+ [150, -7],
+ ]);
+const neugierKurve = (f: number) => kurve(f, [[0, 0], [82, 0], [96, 11], [124, 11], [140, 0]]);
+const ohrZucken = (f: number, bei: number[]) => (bei.some((b) => f >= b && f < b + 4) ? 1 : 0);
+
+const luluKnochen = (f: number): Knochen[] => {
+ const hals = feder(halsKurve, f, {steifheit: 0.11, daempfung: 0.2});
+ const halsTraege = feder(halsKurve, f, {steifheit: 0.05, daempfung: 0.16});
+ const kopf = feder((g) => -halsKurve(g) * 0.35 + neugierKurve(g), f, {steifheit: 0.07, daempfung: 0.18});
+ const ohrL = feder((g) => -26 * ohrZucken(g, [20, 98]), f, {steifheit: 0.25, daempfung: 0.12}) + (hals - halsTraege) * 0.6;
+ const ohrR = feder((g) => 22 * ohrZucken(g, [60, 104]), f, {steifheit: 0.25, daempfung: 0.12}) - (hals - halsTraege) * 0.5;
+ const schwanz = 13 * Math.sin(f * 0.085) + feder((g) => halsKurve(g) * 0.5, f, {steifheit: 0.04, daempfung: 0.1}) - halsKurve(f) * 0.5;
+ const schal = (halsTraege - hals) * 0.9 + 2.5 * Math.sin(f * 0.07);
+ const atmen = Math.sin(f * 0.1);
+ return [
+ {art: 'drehen', um: [45, 56], grad: ohrL, gewicht: kreis(22, 32, 18, 42)},
+ {art: 'drehen', um: [150, 68], grad: ohrR, gewicht: kreis(176, 56, 16, 38)},
+ {art: 'drehen', um: [112, 158], grad: kopf, gewicht: (x, y) => glatt(172, 128, y)},
+ {art: 'drehen', um: [125, 238], grad: hals, gewicht: (x, y) => glatt(244, 150, y)},
+ {
+ art: 'drehen',
+ um: [244, 288],
+ grad: schwanz,
+ gewicht: (x, y) => glatt(236, 252, x) * glatt(268, 284, y) * glatt(392, 376, y),
+ },
+ {
+ art: 'drehen',
+ um: [62, 252],
+ grad: schal,
+ gewicht: (x, y) => glatt(252, 330, y) * glatt(74, 62, x) * glatt(395, 380, y),
+ },
+ {art: 'stauchen', um: [175, 330], sx: 1 - atmen * 0.006, sy: 1 + atmen * 0.012, gewicht: kreis(180, 300, 50, 110)},
+ ];
+};
+
+// ---------- Pino ----------
+// Sprung: Anlauf (Ducken), Flug, Landung mit Nachfedern.
+const SPRUNG = {duck: 84, ab: 93, auf: 111};
+const sprungHoehe = (f: number) => {
+ if (f < SPRUNG.ab || f > SPRUNG.auf) return 0;
+ const t = (f - SPRUNG.ab) / (SPRUNG.auf - SPRUNG.ab);
+ return 4 * t * (1 - t);
+};
+const stauchZiel = (f: number) => {
+ if (f >= SPRUNG.duck && f < SPRUNG.ab) return kurve(f, [[SPRUNG.duck, 1], [SPRUNG.ab - 2, 0.86]]);
+ if (f >= SPRUNG.ab && f <= SPRUNG.auf) return 1 + 0.09 * Math.sin(Math.PI * Math.min(1, (f - SPRUNG.ab) / 8)) * (1 - sprungHoehe(f) * 0.3);
+ if (f > SPRUNG.auf && f < SPRUNG.auf + 4) return 0.84;
+ return 1;
+};
+const pinoKopfKurve = (f: number) => kurve(f, [[0, 0], [38, 0], [56, -9], [84, -9], [92, -3], [128, -3], [146, 7], [180, 7]]);
+const freude = (f: number) => (f > SPRUNG.auf + 4 && f < SPRUNG.auf + 40 ? Math.sin((f - SPRUNG.auf - 4) * 0.35) : 0);
+
+const pinoWerte = (f: number) => {
+ const stauch = feder(stauchZiel, f, {steifheit: 0.3, daempfung: 0.22});
+ const hoch = sprungHoehe(f) * 80;
+ const tempo = (g: number) => (sprungHoehe(g + 1) - sprungHoehe(g - 1)) * 40;
+ const bommel = feder((g) => tempo(g) * 1.4, f, {steifheit: 0.12, daempfung: 0.1});
+ const atmen = Math.sin(f * 0.11);
+ const knochen: Knochen[] = [
+ {art: 'drehen', um: [432, 92], grad: bommel + 3 * Math.sin(f * 0.06), gewicht: kreis(432, 48, 34, 56)},
+ {
+ art: 'drehen',
+ um: [330, 292],
+ grad: feder((g) => (g > SPRUNG.auf && g < SPRUNG.auf + 4 ? -20 : 0), f, {steifheit: 0.25, daempfung: 0.12}),
+ gewicht: kreis(318, 266, 22, 42),
+ },
+ {art: 'drehen', um: [430, 500], grad: freude(f) * 14, gewicht: kreis(432, 522, 22, 42)},
+ {art: 'drehen', um: [520, 495], grad: -freude(f) * 12, gewicht: kreis(540, 515, 20, 38)},
+ {
+ art: 'drehen',
+ um: [380, 480],
+ grad: feder(pinoKopfKurve, f, {steifheit: 0.08, daempfung: 0.2}),
+ gewicht: (x, y) => glatt(520, 380, y) * glatt(230, 330, x),
+ },
+ {art: 'stauchen', um: [420, 600], sx: 1 + atmen * 0.01, sy: 1 + atmen * 0.014, gewicht: kreis(420, 540, 70, 170)},
+ ];
+ return {stauch, hoch, knochen};
+};
+
+export const VerformTest: React.FC = () => {
+ const f = useCurrentFrame();
+ const zoom = 1.04 - f * 0.0002;
+ const pino = pinoWerte(f);
+ const luluNeigen = kurve(f, [[30, 0], [74, -2.5], [118, -2.5], [150, 0]]) + Math.sin(f * 0.05) * 0.5;
+ const abend = 'brightness(0.86) saturate(0.95)';
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/remotion/src/teile/Abendhuegel.tsx b/remotion/src/teile/Abendhuegel.tsx
index e4d7804..83b29c7 100644
--- a/remotion/src/teile/Abendhuegel.tsx
+++ b/remotion/src/teile/Abendhuegel.tsx
@@ -32,7 +32,7 @@ export const AquarellFilter: React.FC = () => (
{/* großflächige, weiche Farbschwankungen statt Körnung */}
-
+
@@ -143,21 +143,24 @@ export const Vordergrund: React.FC<{kameraX: number}> = ({kameraX}) => {
};
// Papierstruktur und Vignette über allem.
-export const Papier: React.FC = () => (
+export const Papier: React.FC<{nurStruktur?: boolean}> = ({nurStruktur = false}) => (
<>
-