Kinematografisches Scrollytelling
Basisarchitektur (nicht verhandelbar)
- Ein einziger Pin, eine einzige Timeline. Der Hero (oder das Hauptkapitel) pinnt einen Container, der N Viewport-Höhen überdauert, und ein Fortschritt von 0→1 steuert alles. Wenn mehrere konkurrierende Scroll-Animationen existieren, handelt es sich um eine normale Website mit Effekten – das ist nicht das, was wir meinen.
- Nie Scroll-Jacking. Der Benutzer steuert die Geschwindigkeit mit seinem nativen Scrollen; die Animation wird „gescrubbt“ (
scrub: true). Es ist verboten, das Mausrad zu kapern oder das Scrollen zu übernehmen. - Ein einziger Animationsloop. Lenis in den GSAP-Ticker integriert (
autoRaf: false) und ScrollTrigger synchronisiert; oder nativer Scroll mit Scrub. Zwei konkurrierende Loops sind die häufigste Ursache für Ruckeln und lassen sich nicht durch Optimieren von Assets beheben. - Text lebt im DOM, über dem Canvas positioniert, mit Ebenen pro Fortschritt. Niemals Text innerhalb des Canvas rendern.
Framesequenzen (Apple-ähnliche Technik)
- Bevorzugte Quelle: echtes Video → Frames mit
ffmpeg(fps=12..24, auf Bildschirmauflösung skaliert). Export als WebP q≈68-75 und durch Betrachten des Ergebnisses justieren, nicht nach Bauchgefühl. - Maximal ~90-120 Frames; auf Mobilgeräten die Hälfte oder eine 720p-Version ausliefern.
- Laden mit gleitendem Fenster: ±6-10 Frames um den aktuellen herum behalten; niemals die gesamte Sequenz dekodieren (eine vollständige 1080p-Sequenz zerstört iOS Safari).
- Vorladen mit
IntersectionObserver(rootMargin ~800px),requestIdleCallbackfür Decode undfetchPriority="low"außer beim Hero. - Canvas mit
devicePixelRatioauf 1,5 begrenzt undcover/containberechnet; nur dekodierte Frames zeichnen; fehlt der exakte Frame, den nächstgelegenen bereits geladenen verwenden (niemals schwarz zeichnen). - Unveränderlicher Cache (
Cache-Control: public, max-age=31536000, immutable) mit versioniertem Dateinamen. - Fallback bei
prefers-reduced-motionund langsamer Verbindung: statisches Poster + normale Kapitel ohne Pin. - Einfachere Alternative, wenn keine Nachbearbeitung nötig ist:
srceines<img>-Elements pro Frame austauschen (zuverlässig in allen Browsern, kein Canvas nötig).
Video-Scrubbing (falls <video> verwendet wird)
currentTimegesteuert durch Fortschritt in einem einzigenrequestAnimationFrame, mit Glättung; Seeks zusammenfassen (Safari staut die Warteschlange).- Als Blob laden, damit es zu 100% seekbar ist; mit kurzem GOP kodieren (Keyframe alle paar Frames), sonst ruckelt das Scrubbing.
- Niemals Autoplay: Das Scrollen steuert es.
Kapitel und Overlays
- Beats definiert durch Fortschrittsbereiche (10%, 35%, 60%, 85%...), mit Ein-/Ausblendungen und abwechselnden Seiten für Rhythmus.
- Jedes Kapitel behält seinen Text im DOM in korrekter semantischer Reihenfolge für SEO.
- Horizontale Galerien: Abschnitt pinnen +
xan Fortschritt gekoppelt; die Karten werden zur Mitte hin skaliert.
Leistung und Überprüfung
- Nur
transform,opacityundfilteranimieren;will-changegezielt setzen und nach Abschluss entfernen. - Frames mit
requestAnimationFramelokal und auf echtem Mobilgerät messen, wenn möglich; Ziel 60fps auf einem durchschnittlichen Laptop. - Headless-Aufnahmen können das Canvas schwarz lesen (Buffer nicht erhalten): Über DOM überprüfen (berechnete Styles, Elemente, über
window.debugexponierter Zustand) und mit aktiviertem Reduced-Mode-Modus. - Bei 375/768/1280/1440 testen; das Resizing eines Desktop-Browsers reproduziert den iOS-Speichercrash nicht: Dies im QA-Bericht vermerken.
Empfohlene Datenstruktur
Ein geordnetes Array von „Einstellungen“: jede Einstellung ist { typ: 'frames' | 'overlay' | 'galerie', ... }. Die Reihenfolge des Films ändern = die Liste neu ordnen. Die Frames werden durch ein Muster referenziert (/seq/einstellung-01/0001.webp).