Technik
Wie wir die Engine für die Graph-Navigation entworfen haben

Ein Blick unter die Haube der Navigationsschicht im Player, wo Panels Knoten sind und Kanten Bedingungen tragen: die Flow-Engine, der flache Variablenkontext, JSON Logic, das im Zweifel geschlossen scheitert, Prioritäten, geerbte Übergänge und Kamerafahrten, das Zurückblättern und die Graphanalyse, die auch das CMS nutzt.
Jedes PanelWave-Kapitel ist ein gerichteter Graph. Panels sind Knoten, und Kanten legen fest, welches Panel auf welches folgen darf, unter welcher Bedingung und mit welchem Übergang. Das Format definiert diese Struktur. Der Player muss sie durchlaufen, tausendfach pro Lesesitzung, ohne dass die Lesenden je irgendwo hängen bleiben. Um genau diesen Teil des Players geht es hier: die Flow-Engine, den Variablenspeicher, den sie befragt, und die Designentscheidungen hinter beiden. Die konzeptionelle Einführung steht unter Graph Navigation – dieser Artikel setzt sie voraus.
Warum ein Graph – und warum die Engine winzig ist
Die Alternative zum Graphen ist eine Seitenreihenfolge plus Sonderfälle: hier eine Entscheidungsseite, dort eine Sprungtabelle. Wir wollten das Gegenteil: ein einziges Grundelement, mit dem lineare Geschichten nichts kosten und Verzweigungen wenig. Ein klassischer Comic ist eine Kette von Kanten ohne Bedingung. Eine Entscheidung sind zwei Kanten mit unterschiedlichen Bedingungen. Schleife, Umweg, optionale Szene, Pfade, die wieder zusammenlaufen: alles ganz gewöhnliche Graphformen, und das Zurückblättern funktioniert überall gleich.
Deshalb konnten wir die Engine klein halten. Die Flow-Engine ist ein reiner Dienst ohne eigenen Zustand: Du gibst ihr einen Graphen, die ID des aktuellen Panels und ein Kontextobjekt, und sie sagt dir, wohin es als Nächstes geht. Alles andere, von der Leseposition bis zu den Variablenwerten, liegt in anderen Diensten. Die Engine entstand mit 43 Unit-Tests, bevor überhaupt etwas gerendert wurde, und sie ist bis heute der Teil des Players, der sich am wenigsten ändert. Die API des Dienstes steht unter Core Services.
Die vier Schritte einer Navigation
Wenn die Lesenden weiterblättern – per Taste, Wischen, Autoplay oder Hotspot –, fragt die Shell die Engine nach dem nächsten Panel. Die Engine erledigt genau vier Dinge, und zwar in dieser Reihenfolge:
Das Sequenzdiagramm und die genauen Regeln stehen unter State, Conditions and Navigation. Aus diesem Design folgen zwei Dinge. Erstens gibt es den Fallback gratis: Wer neben die bedingten Kanten eine Kante ohne Bedingung mit Priorität null setzt, sorgt dafür, dass Lesende auch ohne Entscheidung weiterkommen. Zweitens muss die Engine nie wissen, was eine Bedingung bedeutet – nur, ob sie erfüllt ist.
- Sammeln: alle Kanten, deren from das aktuelle Panel ist. Gibt es keine, ist das Panel ein Endpunkt, und die Engine meldet das, statt zu raten.
- Filtern: Für jede dieser Kanten wird die JSON-Logic-Bedingung gegen den Kontext ausgewertet. Eine Kante ohne Bedingung kommt immer durch.
- Sortieren: die übrigen nach Priorität, höchste Zahl zuerst; fehlt die Priorität, zählt sie als null. Bei Gleichstand entscheidet die Reihenfolge im Manifest.
- Zurückgeben: das Ziel der ersten übrigen Kante samt Übergang und Kantenaktionen. Bleibt keine übrig, ist das Ergebnis wieder ein leeres Ziel.
const candidates = graph.edges.filter((e) => e.from === currentPanelId);
const passing = candidates.filter((e) => evaluateJsonLogic(e.condition, context));
const winner = [...passing].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))[0];
return winner
? { nextPanelId: winner.to, transition: winner.transition ?? defaultTransition, action: winner.action }
: { nextPanelId: null };Bedingungen, die geschlossen scheitern
Bedingungen sind JSON Logic, ausgewertet über eine dünne Hülle um json-logic-js. Die Hülle kennt drei Regeln, und die festzulegen hat länger gedauert, als der Code vermuten lässt. Erstens: Eine fehlende Bedingung ist wahr, denn eine Kante ohne Bedingung muss immer begehbar sein. Zweitens: Ein boolesches Literal kommt unverändert zurück, damit man eine Kante während der Arbeit am Manifest fest ein- oder ausschalten kann. Drittens: Jeder Auswertungsfehler – unbekannter Operator, fehlerhafte Logik, geworfene Exception – wird abgefangen, protokolliert und als false zurückgegeben.
Das geschlossene Scheitern war die umstrittene Entscheidung. Bei offenem Scheitern würde eine kaputte Bedingung wie eine Fallback-Kante wirken. Der Fehler bliebe beim Erstellen unsichtbar, bis Lesende einen Pfad nehmen, den niemand wollte. Bei geschlossenem Scheitern verschwindet die kaputte Kante einfach – und das zeigen die CMS-Validierung und der Simulate-Modus des Graph Editors sofort an. Wir haben uns für die Variante entschieden, die im Editor laut ist, statt für die, die im Livebetrieb still bleibt. Derselbe Evaluator prüft jede Bedingung im Format: Kanten, Sichtbarkeitsbedingungen an Ebenen, Sprechblasen und Hotspots sowie Panel-Varianten. Wer die Regel einmal gelernt hat, kann sie überall anwenden. Dazu bringt die Hülle praktische Operatoren wie in, contains, matches, between und isEmpty mit, außerdem eine Hilfsfunktion, die alle Variablenverweise aus einer Regel zieht – so können Werkzeuge auflisten, wovon ein Kapitel abhängt.
Ein flacher Kontext aus fünf Gültigkeitsbereichen
Bedingungen sehen den Variablenspeicher nie direkt. Der Speicher hält Werte in fünf Gültigkeitsbereichen, passend zum Schema (global, chapter, page, session und persistent). Unmittelbar vor jeder Navigation lässt die Shell sie zu einem einzigen Objekt verflachen. Die Reihenfolge dabei ist bewusst gewählt: erst global, dann session, persistent, der Bereich des aktuellen Kapitels und zuletzt der der aktuellen Seite. Bei gleichen Schlüsseln gewinnt der spätere Eintrag. Eine Kapitelvariable überdeckt also eine gleichnamige globale, solange die Lesenden in diesem Kapitel sind – genau das, was ein Rätselzustand pro Kapitel braucht. IDs mit Punkt-Namensraum wie story.choice werden im Kontext zu verschachtelten Pfaden, sodass der var-Operator von JSON Logic sie ohne Sonderbehandlung findet.
Jeden Schreibvorgang prüft der Speicher gegen die Definitionen im Manifest: Typ, Wertebereich, Enum-Zugehörigkeit und Schreibschutz. Was durchfällt, wird mit einer Warnung verworfen, statt den Zustand zu beschädigen. Nur der persistente Bereich nutzt localStorage, und ist der nicht verfügbar, weicht er auf den Arbeitsspeicher aus. Die Tabelle der Gültigkeitsbereiche und die Schreibregeln stehen unter State, Conditions and Navigation; die Persistenzdetails unter Saving Progress.
Was mit der Kante reist: Übergänge und Kamerafahrten
Eine Kante sagt nicht nur, wohin es geht, sondern auch, wie der Wechsel aussieht. Seit Schema 1.2 können Kanten einen Standardübergang vom aktiven Output-Preset erben. fade-300ms setzt man also einmal pro Format statt an jeder einzelnen Kante. Die Engine löst diese Vererbung selbst auf: Hat die Kante einen eigenen Übergang, gewinnt der, sonst gilt der Standard des Presets. Ist kein Format aktiv, greift der Standard nur, wenn alle definierten Presets darin übereinstimmen; als letzter Ausweg bleibt der harte Schnitt. Die Regel ist bewusst vorsichtig: Ein Format zu raten und dann falsch zu animieren, ist schlimmer, als gar nicht zu animieren.
Mit Schema 1.4 kam die unendliche Leinwand – und damit etwas Zweites, das mit der Kante reist: die Kamerafahrt. Die Engine behandelt sie genau wie einen Übergang (erst der Wert der Kante, dann der Preset-Standard) und gibt sie zusammen mit dem Ziel zurück, damit die Canvas-Ansicht gleiten kann. Direkter, bogenförmiger oder Wegpunkt-Pfad, Zoomprofil Halten, Zurückziehen oder Eintauchen: Die Flow-Engine wertet nichts davon aus, sie reicht es nur durch. Die Navigationslogik ist in jedem Ansichtsmodus dieselbe. Genau deshalb ließ sich die Canvas-Ansicht ergänzen, ohne die Auswahlregel anzufassen. Siehe Canvas View und die Canvas-Formatreferenz.
Zurück ist dieselbe Kante, nur rückwärts
Zurück heißt nicht, einfach den letzten Eintrag vom Verlaufsstapel zu nehmen. Die Engine sucht die Kanten, die auf das aktuelle Panel zeigen, und gibt deren Quellen zurück; die Shell nimmt die erste. Für die Animation sucht sie die Kante, über die es vom vorherigen zum aktuellen Panel ging, und kehrt sie um: Aus slide-left wird slide-right, aus up wird down, und Übergänge ohne Richtung wie fade oder zoom bleiben, wie sie sind. Kamerafahrten auf der Leinwand werden genauso umgedreht, die Wegpunkte in umgekehrter Reihenfolge. Die Kamera fährt den gestalteten Pfad also zurück, statt zu springen. So passt das Zurückblättern immer zur Gestaltung, ohne dass pro Sitzung ein Animationsprotokoll gespeichert werden muss.
Das Analyse-Werkzeug – und wer es nutzt
Das Weiterschalten ist eine einzige Methode. Den Großteil der Engine macht die Graphanalyse aus, und die ignoriert Bedingungen mit Absicht. Es gibt Hilfsfunktionen, die prüfen, ob ein Panel Einstieg oder Endpunkt ist, alle Endpunkte finden, die Erreichbarkeit per tiefenbegrenzter Breitensuche testen, den kürzesten Pfad zwischen zwei Panels suchen, Zyklen per Tiefensuche mit Rekursionsstapel erkennen und jedes Panel auflisten, das von einem Startknoten aus erreichbar ist. Für strukturelle Fragen ist es richtig, Bedingungen außen vor zu lassen: Wer wissen will, ob ein Panel erreichbar ist, fragt nach dem Graphen, nicht nach dem aktuellen Wert einer Variablen.
Das CMS spricht dieselbe Sprache. Der Graph Editor markiert unerreichbare Knoten, Zyklen, isolierte Knoten und Sackgassen mit eigenen Breiten- und Tiefensuchen. Er legt die Karte mit dagre aus, hebt Probleme auf der Leinwand hervor und bietet schnelle Korrekturen an, etwa ein verwaistes Panel mit dem Einstieg zu verbinden. Sein Simulate-Modus durchläuft die Struktur genau wie die Analysefunktionen der Engine und listet jede ausgehende Kante mit ihrer Bedingung auf. Die Preflight-Prüfung für das ganze Werk wiederholt den Erreichbarkeitstest, bevor irgendetwas veröffentlicht wird. Bedingungen mit echten Werten testest du in der Preview: Dort fließen Variablenüberschreibungen in denselben flachen Kontext, den auch die Engine sieht.
Berechtigungen bleiben außerhalb des Graphen
Man hätte Monetarisierung als Bedingungen an Kanten abbilden können. Wir haben sie bewusst aus dem Graphen herausgehalten. Ob jemand ein Panel betreten darf, entscheidet ein Entitlement-Adapter, den der Host implementiert. Vor jeder Navigation wartet der Player auf seine Antwort, unterstützt von einem kurzlebigen Cache im Entitlement-Dienst. Die Engine bietet Brücken, um ein Panel, ein Kapitel oder ein Werk zu prüfen. Der Graph selbst beschreibt aber nur die Geschichte – nicht, wer dafür bezahlt hat. Geprüfte Fakten, die der Host kennt, etwa ein Berechtigungs-Flag oder das Alter einer Person, lassen sich beim Start trotzdem in schreibgeschützte Variablen schreiben. Bedingungen können dann darauf verweisen, aber nichts innerhalb der Geschichte kann sie ändern. Siehe Paywall and Entitlement.
Abwägungen, die wir wieder treffen würden – und eine, die wir überdenken
- Reine Engine, zustandsbehaftete Shell. Jede Navigation ist eine Funktion aus Graph, Position und Kontext. Das macht die Engine trivial testbar, und das CMS kann dieselbe Logik in seinem Simulator ausführen.
- Bedingungen stecken in den Daten, nicht im Code. JSON Logic ist klein genug, um es von Hand zu schreiben, und sicher genug, um es auch aus nicht vertrauenswürdigen Manifesten auszuwerten. Eigene Operatoren sind möglich, aber das Kernformat braucht keine.
- Struktur und Zustand sind getrennte Fragen. Die Analysefunktionen ignorieren Bedingungen, die Weiterschaltfunktion beachtet sie. Wer beides vermischt, bekommt auf beide Fragen manchmal die falsche Antwort.
- Kantenaktionen werden zurückgegeben, nicht ausgeführt. Die Engine liefert die Variablenänderungen einer Kante im Navigationsergebnis mit und überlässt das Anwenden dem Host. So bleibt die Engine frei von Seiteneffekten – allerdings wendet die Shell die Änderungen deshalb noch nicht automatisch an. Das ist der eine Punkt auf dieser Liste, den wir überdenken. Das aktuelle Verhalten ist unter Mutations dokumentiert.
Weiterlesen
- Graph: alle Eigenschaften von Graph, Kante und Mutation, mit Hinweisen zur Validierung.
- Player Architecture: wo die Engine im Datenfluss vom Manifest bis zum fertigen Panel sitzt.
- Variables: das Zustandsmodell, aus dem der flache Kontext entsteht.
- Verzweigte Geschichten ohne Spaghetti: Muster für Creators, die das alles gut ausnutzen.
