Player Architecture
How the PanelWave Player library is layered internally — types, utilities, services, and components — and how data flows from manifest load to a rendered panel.
This page describes the internal architecture of @panelwave/player for contributors and developers doing deep integrations. If you just want to embed the player, start with Installation and Embedding Quickstart.
Workspace layout
The player lives in an Angular CLI workspace with three projects:
| Project | Path | Purpose |
|---|---|---|
player | projects/player/ | The publishable library (ng-packagr, entry point src/public-api.ts) |
demo | projects/demo/ | The demo application — local development (npm start), the E2E suite's host, and the live demo at panelwave.github.io/player |
reader | projects/reader/ | The public reader app behind read.panelwave.org — mounts pw-player-shell full-window for a manifest the server names |
Inside the library, projects/player/src/lib/ is organized into four layers:
src/lib/
├── types/ Pure TypeScript interfaces (manifest, panel, graph, variables, …)
├── utils/ Framework-free pure functions (ComicBalloon, JSON Logic, assets, locales)
├── services/ Injectable singletons (manifest, state, flow, media, tracking, …)
└── components/ Standalone Angular components (shell, viewport, layers, overlays, modals)
Key design rules:
- Types have no dependencies. Everything in
src/lib/types/is interface-only and stays aligned with@panelwave/typesand the PanelWave schema. - Utils are framework-free.
comic-balloon.ts,balloon-config.ts,json-logic-utils.ts,asset-utils.ts,locale-utils.ts,video-config-utils.ts, andanimation-utils.tsare plain functions/classes with no Angular imports, which is what allows the balloon renderer to be shared with the CMS. - Services are
providedIn: 'root'singletons. They own all cross-component state and side effects. - Components are standalone with
ChangeDetectionStrategy.OnPush. There are no NgModules; each component declares its ownimports.
State management
The player's runtime state is managed with RxJS, not with a state-management library and not with Angular signals:
- Services expose
BehaviorSubject-backed observables (PlayerStateService.currentPanel$,VariableStoreService.store,VideoControllerService.events$, …). - Components subscribe with
takeUntil(destroy$)and hold plain fields for template binding under OnPush change detection. - Preferences, tracking consent, and
persistent-scope variables are persisted tolocalStorage(keyspw-preferences,pw-tracking-consent,pw-variables-persistent).
There are two PlayerStateService implementations in the source tree. The one exported from the public API and injected by PlayerShellComponent is src/lib/services/player-state.service.ts — a small store exposing currentPanel$, locale$ and the persisted reader preferences$. A richer store implementing the full PlayerState interface (overlays, viewport, event bus) exists at src/lib/state/player-state.service.ts but is currently not exported and not wired into the shell. When contributing, check which one your change targets.
Data flow: from manifest to rendered panel
Step by step:
- Load. The host passes a
manifestUrl(fetched withHttpClientviaManifestService.loadManifestFromUrl()) or aPanelWaveManifestobject toPlayerShellComponent. - Validate and index.
ManifestServiceperforms structural validation (requiredpanelwave.version,meta, non-emptychapters, per-chapterpanelsandgraph.entry) and buildsMapindexes for panels, chapters, and assets so all later lookups are O(1). - Initialize context. Manifest variable definitions are registered,
initialVariablesare seeded, the paywall is armed (PaywallServicegets the manifest's rules and the host's entitlement snapshot, topped up with an age verification stored on the device), and an entitlement adapter's context — if one is provided — is seeded on top. The initial locale is applied to both content (PlayerStateService.setLocale) and GUI strings (TranslationService.setLanguage). The manifest'strackingsection configuresTrackingService(consent requirement, event whitelist, endpoint). - Navigate to the start. The host's initial position if given, otherwise the reader's stored bookmark for this work, otherwise the first chapter's
graph.entry(resolved viaFlowEngineService) — from the player release after 1.2.0 shown behind the work's cover when it has one (showCover), and gated like any other move: a locked entry panel opens with the age gate or paywall over its placeholder. Setting the current panel emitspanelChangeonce. - Render.
ViewportComponentreceives the current panel (or page, in page view) and renders each layer throughLayerRendererComponent, which delegates to the type-specific layer components.SpeechBubblesComponentrenders balloons on top using the ComicBalloon engine. - Navigate onward. User input (keys, swipes, toolbar, hotspots) triggers graph traversal through
FlowEngineServicewith the current variable context; a chapter without edges follows its reading order.
View modes
The player renders in one of three view modes ('panel' | 'page' | 'canvas'):
- Panel view (default): one panel at a time; navigation follows the chapter graph edge by edge.
- Page view: a whole comic page with its
layout.placements; navigation moves page by page, andPage.readingOrderdrives things like video sequencing. Page view is only offered when at least one chapter definespages. From the player release after 1.2.0 it shows the page sequence of one output format, picked for the screen (pageFormat,utils/page-format-utils.ts), in that format's aspect ratio, and pages on into the adjacent chapters. - Canvas view: chapters with an infinite-canvas layout (format 1.4) render on
pw-canvas-stagewith a moving camera — see Canvas View.
The shell owns the mode switch (onToggleView()) and re-arms autoplay and the video sequencer when it changes.