From bc807312a1a6711fbec25a2b4da1067afed2cf2f Mon Sep 17 00:00:00 2001 From: Lara Date: Sat, 7 Nov 2026 11:08:53 +0100 Subject: [PATCH] =?UTF-8?q?Drachen=20f=C3=BCr=20=E2=80=9ELulu=20und=20der?= =?UTF-8?q?=20Drachen=E2=80=9C:=20roter=20Papierdrachen=20mit=20Schwanz=20?= =?UTF-8?q?und=20drei=20Schleifen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - baukasten/drachen.tsx: als SVG (Raute mit Holzkreuz, leichter Stift-Rand), Schwanz mit wandernder Welle, weht mit `schwanz` zur Seite, liegt im Gras flach (`liegt`) und knickt an der Graskante um (`boden`); Schleifen gelb, grün, blau leuchten einzeln (`leuchten`, zum Zählen) - drachenHalt(): Knotenpunkt für die Drachenschnur, dreht mit - Flugbahnen: pendeln, steigen (auch aus dem Gras), trudeln (zwei Runden ins Gras), plumps (Windstoß hebt ihn kurz an); LIEGE_HOEHE für den liegenden Drachen - DrachenschnurTest zeigt die ganze Folge (14 s) mit Noa und Schnur Co-Authored-By: Claude Opus 5.5 --- remotion/src/Root.tsx | 2 +- remotion/src/baukasten/drachen.tsx | 193 ++++++++++++++++++++++ remotion/src/szenen/DrachenschnurTest.tsx | 51 +++--- 3 files changed, 225 insertions(+), 21 deletions(-) create mode 100644 remotion/src/baukasten/drachen.tsx diff --git a/remotion/src/Root.tsx b/remotion/src/Root.tsx index 243ccbf..0cd1887 100644 --- a/remotion/src/Root.tsx +++ b/remotion/src/Root.tsx @@ -29,7 +29,7 @@ export const Root: React.FC = () => ( - + diff --git a/remotion/src/baukasten/drachen.tsx b/remotion/src/baukasten/drachen.tsx new file mode 100644 index 0000000..40ea101 --- /dev/null +++ b/remotion/src/baukasten/drachen.tsx @@ -0,0 +1,193 @@ +import React from 'react'; +import {Easing, interpolate, useCurrentFrame} from 'remotion'; + +// Noas roter Papierdrachen („Lulu und der Drachen“): Raute mit Holzkreuz, +// langer Schwanz mit drei Schleifen (gelb, grün, blau). +// (x, y) = Kreuzungspunkt der Leisten auf dem Bildschirm, s = Größe (1 ≈ 120 px hoch). +// Der Körper dreht sich mit `drehung`, der Schwanz hängt immer nach unten bzw. weht +// mit dem Wind (`schwanz`: Winkel aus der Senkrechten, + = nach rechts). + +// Raute in Drachen-Koordinaten (Kreuzungspunkt = 0,0) +const OBEN = {x: 0, y: -42}; +const LINKS = {x: -40, y: 0}; +const RECHTS = {x: 40, y: 0}; +const UNTEN = {x: 0, y: 78}; +const SCHWANZ_LAENGE = 230; +// Lage der Schleifen auf dem Schwanz (Anteil der Länge) und ihre Farben +export const SCHLEIFEN = [ + {t: 0.3, farbe: '#f2c84b', rand: '#c9952a'}, // gelb + {t: 0.6, farbe: '#7fb069', rand: '#4f7f3d'}, // grün + {t: 0.9, farbe: '#5b8fd1', rand: '#335f99'}, // blau +] as const; + +const dreh = (p: {x: number; y: number}, grad: number, s: number) => { + const w = (grad * Math.PI) / 180; + return {x: (p.x * Math.cos(w) - p.y * Math.sin(w)) * s, y: (p.x * Math.sin(w) + p.y * Math.cos(w)) * s}; +}; + +// Punkt, an dem die Schnur am Drachen festgeknotet ist (Waage, knapp unter dem Kreuz). +export const drachenHalt = (x: number, y: number, s = 1, drehung = 0) => { + const p = dreh({x: 0, y: 14}, drehung, s); + return {x: x + p.x, y: y + p.y}; +}; + +type DrachenProps = { + x: number; + y: number; + s?: number; + drehung?: number; + schwanz?: number; // Winkel des Schwanzes aus der Senkrechten (Grad), Wind von links ≈ 25 + flattern?: number; // Stärke der Schwanzwelle (px), 0 = hängt ruhig + liegt?: boolean; // liegt im Gras (drehung 90): Schwanz flach zur Seite + boden?: number; // y des Grases auf dem Bildschirm: der Schwanz geht nicht darunter + leuchten?: [number, number, number]; // je Schleife 0…1 (Zählen) + deckkraft?: number; + frame?: number; +}; + +export const Drachen: React.FC = ({ + x, + y, + s = 1, + drehung = 0, + schwanz = 20, + flattern = 14, + liegt = false, + boden, + leuchten = [0, 0, 0], + deckkraft = 1, + frame, +}) => { + const jetzt = useCurrentFrame(); + const f = frame ?? jetzt; + const id = `drachen-${Math.round(x)}-${Math.round(y)}`; + + // Schwanz in Bildschirm-Koordinaten: beginnt an der unteren Spitze, läuft in Richtung + // `schwanz` (liegend: flach nach rechts) und schlägt eine wandernde Welle. + const start = dreh(UNTEN, drehung, s); + const richtung = ((liegt ? -84 : schwanz) * Math.PI) / 180; // liegend: von der Spitze flach nach links + const ax = Math.sin(richtung); + const ay = Math.cos(richtung); + const welle = liegt ? 0 : flattern; + const schwanzPunkt = (t: number) => { + const l = t * SCHWANZ_LAENGE * s; + const quer = Math.sin(t * 7 - f * 0.35) * welle * s * (0.25 + t); // nach hinten stärker + const sackt = liegt ? 0 : t * t * 30 * s * Math.sin(richtung); // weht er seitlich, hängt das Ende etwas + // Erreicht der Schwanz das Gras, liegt der Rest flach darauf (mit dem Wind nach rechts). + const grund = boden === undefined ? Infinity : boden - y - 2; + const lAuf = ay > 0.05 ? (grund - start.y) / ay : Infinity; + if (l > lAuf) return {x: start.x + ax * lAuf + (l - lAuf) * 0.95, y: grund - Math.abs(Math.sin(t * 9)) * 2 * s}; + return {x: start.x + ax * l + ay * quer, y: Math.min(grund, start.y + ay * l - ax * quer + sackt)}; + }; + const punkte = Array.from({length: 25}, (_, i) => schwanzPunkt(i / 24)); + const schwanzPfad = punkte.map((p, i) => `${i ? 'L' : 'M'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' '); + + const r = (p: {x: number; y: number}) => dreh(p, drehung, s); + const [o, li, re, u] = [r(OBEN), r(LINKS), r(RECHTS), r(UNTEN)]; + + return ( + + + + + + + + + + + + + + + + + {/* Schwanz: Kordel */} + + {/* Schleifen */} + {SCHLEIFEN.map((sl, i) => { + const p = schwanzPunkt(sl.t); + const q = schwanzPunkt(Math.min(1, sl.t + 0.03)); + const winkel = (Math.atan2(q.y - p.y, q.x - p.x) * 180) / Math.PI; + const g = leuchten[i] ?? 0; + const k = s * (1 + 0.25 * g); + return ( + + {g > 0 && } + + + + + + + ); + })} + {/* Körper */} + + + {/* helleres oberes Feld wie aufgeklebtes zweites Papier */} + + {/* Holzleisten */} + + + + + ); +}; + +// ---------- Flugbahnen ---------- +// Alle liefern {x, y, drehung} für (und damit auch für drachenHalt). +export type DrachenLage = {x: number; y: number; drehung: number; liegt?: boolean}; +// Im Gras liegt der Kreuzungspunkt so hoch über dem Boden (Raute auf der Seite, s = 1). +export const LIEGE_HOEHE = 36; +type Punkt = {x: number; y: number}; +const klemm = {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'} as const; + +// Ruhiges Tanzen am Himmel (z. B. Szene 6): sanftes Auf und Ab, leichtes Kippen. +export const pendeln = (frame: number, ort: Punkt, staerke = 1): DrachenLage => ({ + x: ort.x + Math.sin(frame * 0.045) * 40 * staerke, + y: ort.y + Math.sin(frame * 0.07 + 0.8) * 18 * staerke, + drehung: Math.sin(frame * 0.045 + 0.6) * 12 * staerke, +}); + +// Steigen von `von` nach `nach` in `dauer` Frames, mit kleinen Rucken im Wind; +// danach pendelt er am Ziel weiter. Aus dem Gras: von = {x, boden - LIEGE_HOEHE}, startDrehung = 90. +export const steigen = (frame: number, start: number, dauer: number, von: Punkt, nach: Punkt, startDrehung = 0): DrachenLage => { + const t = interpolate(frame, [start, start + dauer], [0, 1], {...klemm, easing: Easing.bezier(0.3, 0, 0.25, 1)}); + const ruck = Math.sin(t * Math.PI * 4) * (1 - t) * 18; + const p = pendeln(frame, {x: von.x + (nach.x - von.x) * t, y: von.y + (nach.y - von.y) * t}, t); + const aufrichten = startDrehung * Math.max(0, 1 - t * 5); // z. B. aus dem Gras (90) aufrichten + return {x: p.x + ruck * 0.5, y: p.y + ruck, drehung: p.drehung + (1 - t) * 15 * Math.sin(frame * 0.3) + aufrichten, liegt: frame < start}; +}; + +// Trudeln: dreht sich im Kreis und fällt in einer Spirale ins Gras (`boden` = y des Grases). +export const trudeln = (frame: number, start: number, dauer: number, von: Punkt, boden: number, nachRechts = 160): DrachenLage => { + const t = interpolate(frame, [start, start + dauer], [0, 1], klemm); + const fall = Easing.in(Easing.quad)(t); + const kreis = t * Math.PI * 4; // zwei Runden + const radius = 45 * (1 - t); + return { + x: von.x + nachRechts * t + Math.sin(kreis) * radius, + y: von.y + (boden - LIEGE_HOEHE - von.y) * fall - Math.cos(kreis) * radius * 0.5, + drehung: t * 720 + 90 * t * t, + liegt: t >= 1, + }; +}; + +// Plumps: liegt im Gras (x = Mitte, `boden` = y des Grases), ein Windstoß hebt ihn kurz an, +// dann fällt er schaukelnd zurück ins Gras. +export const plumps = (frame: number, start: number, dauer: number, x: number, boden: number, hoehe = 140): DrachenLage => { + const t = interpolate(frame, [start, start + dauer], [0, 1], klemm); + const hub = Math.sin(t * Math.PI) * hoehe; + const wackeln = Math.sin(t * Math.PI * 5) * 25 * Math.sin(t * Math.PI); + const aufrecht = interpolate(t, [0, 0.2, 0.8, 1], [90, 0, 0, 90]); + return {x: x + Math.sin(t * Math.PI) * 40, y: boden - LIEGE_HOEHE - hub, drehung: aufrecht + wackeln, liegt: t <= 0 || t >= 1}; +}; diff --git a/remotion/src/szenen/DrachenschnurTest.tsx b/remotion/src/szenen/DrachenschnurTest.tsx index 52033c5..a95a2c6 100644 --- a/remotion/src/szenen/DrachenschnurTest.tsx +++ b/remotion/src/szenen/DrachenschnurTest.tsx @@ -1,32 +1,43 @@ import React from 'react'; -import {AbsoluteFill, useCurrentFrame} from 'remotion'; +import {AbsoluteFill, interpolate, useCurrentFrame} from 'remotion'; import {Figur, NOA_HOEHE} from '../baukasten/figur'; +import {Drachen, type DrachenLage, LIEGE_HOEHE, drachenHalt, pendeln, plumps, steigen, trudeln} from '../baukasten/drachen'; import {Drachenschnur, spulenPunkt} from '../baukasten/drachenschnur'; -// Prüfszene: Noa mit Drachenspule, Schnur zu einem Platzhalter-Drachen, der pendelt. -// Links normal, rechts gespiegelt und größer (prüft die Umrechnung). -const Platzhalter: React.FC<{x: number; y: number}> = ({x, y}) => ( -
- - - -
-); +// Prüfszene Drachen (14 s): Noa hält die Spule; der Drachen liegt im Gras, plumpst, +// steigt, tanzt, trudelt zurück. Oben links ein zweiter Drachen zum Zählen der Schleifen. +const BODEN = 900; +const klemm = {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'} as const; export const DrachenschnurTest: React.FC = () => { const f = useCurrentFrame(); - const drachen1 = {x: 760 + Math.sin(f * 0.05) * 60, y: 160 + Math.sin(f * 0.08) * 25}; - const drachen2 = {x: 1180 - Math.sin(f * 0.05) * 60, y: 200 + Math.cos(f * 0.07) * 25}; - const noa1 = {x: 420, y: 900, hoehe: NOA_HOEHE}; - const noa2 = {x: 1560, y: 980, hoehe: NOA_HOEHE * 1.5, spiegeln: true}; + const noa = {x: 420, y: BODEN, hoehe: NOA_HOEHE}; + const himmel = {x: 1150, y: 230}; + + let lage: DrachenLage; + if (f < 100) lage = plumps(f, 30, 60, 980, BODEN); + else if (f < 300) lage = steigen(f, 100, 120, {x: 980, y: BODEN - LIEGE_HOEHE}, himmel, 90); + else { + const ab = pendeln(300, himmel); + lage = trudeln(f, 300, 80, ab, BODEN); + } + // Wind von links: Schwanz weht nach rechts, am Boden kaum + const schwanz = interpolate(lage.y, [300, BODEN - 120], [30, 5], klemm); + + // Zählen: Schleifen leuchten nacheinander auf + const zaehl = (i: number) => interpolate(f % 120, [i * 30 + 10, i * 30 + 18, i * 30 + 34, i * 30 + 44], [0, 1, 1, 0], klemm); + return ( - - - - - - +
+ + + + BODEN - 200 ? 0.12 : 0.05} + /> ); };