Zum Inhalt

Physarum Universal App — macOS Playback (Increment 3a) Design Spec

Status: Draft · 2026-06-02 · author: Johannes (brainstormed with Claude) Builds on: 2026-06-02-native-ambiance-app-design.md (the native-pivot North Star). This spec scopes the first concrete app target: a SwiftUI Multiplatform app whose macOS build delivers Playback + Preset-Browser + curated Live-Sliders, on top of the already-complete native PhysarumEngine.

1. Kontext & Ziel

Die native PhysarumEngine (Swift/Metal) ist feature-complete + getestet: alle 25 Presets, voller Post-FX-Stack inkl. Temporal-Feedback, 19 XCTests grün. Was fehlt, ist eine App darüber. Diese Spec definiert das Universal-App-Gerüst (ein SwiftUI- Multiplatform-Codebase für macOS + iOS + iPadOS + tvOS) und baut als ersten Schritt die macOS-Playback-UI aus.

Warum macOS zuerst (statt tvOS-Hero): läuft sofort ohne Simulator/Gerät (sofort iterierbar, remote-tauglich), validiert das komplette Multiplatform-Gerüst end-to-end (Xcode-Projekt, App-Target, Engine-Einbindung, SwiftUI-Lifecycle), bevor die tvOS- Spezifika (Focus-Engine, Top-Shelf) dazukommen. Der bestehende PhysarumViewer (AppKit) ist faktisch schon ein macOS-Playback-Prototyp. tvOS folgt auf bewährtem Gerüst.

2. Festgezurrte Entscheidungen (aus dem Brainstorming 2026-06-02)

Achse Entscheidung
Erster Target macOS-Playback. Das Universal-Gerüst (alle 4 Plattformen) wird angelegt, nur die macOS-UI v1 ausgebaut.
Funktionsumfang v1 Playback (Fenster + Fullscreen) + Preset-Browser (25 mit Thumbnails) + Auto-Cycle + kuratierte Live-Slider.
Target-Organisation Ein Multiplatform-App-Target (Physarum), das das PhysarumEngine-SwiftPM-Package einbindet. SwiftUI Multiplatform, ein Codebase.
Projekt-Generierung XcodeGen — project.yml (versioniert) generiert die .xcodeproj (gitignored). Setup ist Code, kein GUI-Klicken.
macOS-DMG-Strategie Native löst die WKWebView-DMG ab — bei Reife (Cutover erst mit dem Authoring-Editor). Bis dahin reift die native App parallel; die WKWebView-DMG bleibt offiziell.
Browser-Web-App Codeberg Pages + embeddable Player bleiben unangetastet (freier Universal-Zugang, unabhängig von der DMG).
Live-Slider Kuratierter Satz respawn-freier Parameter (Post-FX + sim-seitig). agentCount/spawnPattern bleiben Respawn-Only → kein Slider in v1.

3. Architektur — Repo-Struktur & Gerüst

apple/
  PhysarumEngine/              # bestehendes SwiftPM-Package (Engine, plattform-agnostisch)
    Package.swift              # platforms erweitern: .macOS(.v14) + .iOS(.v17) + .tvOS(.v17)
  PhysarumApp/                 # NEU — die Universal-App (XcodeGen)
    project.yml                # XcodeGen-Manifest → generiert Physarum.xcodeproj (gitignored)
    Sources/
      App/                     # @main SwiftUI App + Scene
      Player/                  # MetalView (NS/UIViewRepresentable um MTKView) + RenderCoordinator
      Library/                 # Preset-Browser (LazyVGrid der 25 + Thumbnails)
      Controls/                # Live-Slider-Panel
      Model/                   # AppState (@Observable), PresetCatalog, LiveParams
    Resources/                 # 25 Preset-JSONs, Thumbnails, Assets.xcassets, Info.plist, Entitlements

Kernpunkte: - Ein Multiplatform-App-Target Physarum; Destinations macOS (aktiv), iOS/iPadOS/tvOS im Manifest vorbereitet, aber UI noch nicht ausgebaut. - Engine bleibt unangetastet als lokale SwiftPM-Dependency. swift test läuft unabhängig weiter. Nur Package.swift bekommt die zusätzlichen Plattformen. - XcodeGen: project.yml ist Single-Source; .xcodeproj generiert + gitignored. - Bestehende Dev-Tools bleiben: PhysarumViewer/EngineCheck/PresetGallery als macOS-only swift run-Tools. Die WKWebView-DMG (macos/) bleibt separat. - Preset-Assets: Das Export-Skript (scripts/export-native-presets.ts) bekommt das App-Resources/-Verzeichnis als drittes Ziel — Single-Source bleibt apple/presets/, kein Drift. Thumbnails werden ebenfalls ins Bundle kopiert.

4. Komponenten & Datenfluss

Komponente Rolle
PhysarumApp (@main) App-Entry, hält AppState
AppState (@Observable) selectedPreset, liveParams (Overrides), isFullscreen, autoCycle, PresetCatalog
PresetCatalog lädt die 25 JSONs aus dem Bundle → [Preset] (sortiert)
LiveParams die respawn-freien Overrides + Mapping auf PresetParams
PlayerView MetalView (NS/UIViewRepresentable um MTKView) + RenderCoordinator (MTKViewDelegate)
RenderCoordinator hält SimEngine + Renderer; treibt den 60fps-Loop; reagiert auf Preset-/Live-Param-Wechsel
LibraryView LazyVGrid der Presets mit Thumbnails → setzt selectedPreset
ControlsView Slider-Panel, gebunden an AppState.liveParams

macOS-Layout v1: NavigationSplitView — Library (Sidebar) · Player (Detail) · Controls (Inspector). Fullscreen → reines Playback.

Datenfluss:

Browser-Klick   → AppState.selectedPreset → RenderCoordinator: neue SimEngine(preset) + resetFeedback()
Slider          → AppState.liveParams     → RenderCoordinator pusht Live-Params (kein Respawn)
Auto-Cycle-Timer → nächster Preset (wie Browser-Klick)
RenderLoop (draw): sim.step() → renderer.render(sim, livePreset, into: drawable)

5. Engine-Erweiterungen (im PhysarumEngine-Package)

  1. Package.swift platforms: .macOS(.v14) → + .iOS(.v17) + .tvOS(.v17). Die transpilierten MSL-Shader + der Metal-Code sind cross-platform; die macOS-only Executables werden nur via swift run auf macOS gebaut (von der App nicht referenziert).
  2. SimEngine.updateLiveParams(_:): übernimmt die respawn-freien Felder (decayRate, fadeStrength, trailSaturation, diffusionFreq, chaosStrength/-Interval, rhythmAmplitude, exposure, headIntensity, simSpeed, per-species turn/move/sensor/deposit, matrix) ins gespeicherte Preset, sodass writeParams() sie ab dem nächsten Frame nutzt. Nicht übernommen: agentCount, spawnPattern, populationShare (brauchen Re-Seed).
  3. Post-FX-Live-Params (exposure/bloom/feedback/vignette/saturation/contrast/kaleidoscope/ blendMode/backgroundColor) brauchen keine Engine-Änderung: die App reicht den live-editierten Preset jeden Frame an renderer.render(...), das ihn bereits liest.

6. Kuratierte Live-Slider (v1)

Gruppiert, hoher visueller Impact, alle respawn-frei: - Motion: simSpeed (1–8), decayRate (Trail-Persistenz) - Glow: bloomIntensity, feedbackAmount (Echo/Trails) - Color/Tone: exposure, saturation, contrast, vignette

Ein „Reset auf Preset-Defaults"-Button stellt die Originalwerte wieder her. (Exakte Range/ Step-Werte aus dem Web-paramConfig/Schema im Plan übernehmen.)

7. Plattform-Strategie

  • Geteilt (≈90%): Engine-Package, AppState, PresetCatalog, LiveParams, RenderCoordinator-Logik, der Großteil der Views.
  • Plattform-spezifisch via #if os(): der dünne MetalView-Wrapper (NSViewRepresentable ↔ UIViewRepresentable) und Navigation/Input (macOS: SplitView + Maus/Tastatur · tvOS: Focus-Engine + Siri-Remote · iOS: Touch). MTKView läuft überall.
  • project.yml deklariert alle Destinations; nur macOS-UI wird v1 ausgebaut. tvOS/iOS-UI sind je ein eigenes Increment.

8. Testing & Verifikation

  • Engine-XCTests laufen unverändert weiter (19 grün), inkl. neuer Tests für updateLiveParams (respawn-freie Felder werden übernommen, agentCount/spawnPattern nicht).
  • App-Logik (unit-testbar, eigenes Test-Target oder via die Engine-Suite, soweit ohne UI): PresetCatalog lädt alle 25 JSONs; LiveParams→PresetParams-Mapping ist korrekt.
  • SwiftUI-Views: keine Unit-Tests; Verifikation über xcodebuild (macOS-Target baut)
  • visuell (laufende App screenshotten / Johannes sieht sie auf dem Mac).
  • Verifikations-Gate: xcodegen generate → xcodebuild -scheme Physarum -destination 'platform=macOS' build grün; App startet, rendert ein Preset, Browser + Slider reagieren.

9. Distribution-Roadmap

  • Jetzt: native App ist ein lokaler Dev-Build (xcodebuild/Xcode-Run). Keine neue DMG.
  • Bei Editor-Reife (späteres Increment): native App wird die macOS-DMG (signiert + notarisiert über die bestehende build-macos-app.sh-Kette, angepasst auf das App-Target). Die WKWebView-DMG wird dann zurückgezogen.
  • Offener Roadmap-Punkt: Audio-Reaktivität + Capture/Export sind Web-only — vor dem Cutover entscheiden: nativ portieren oder bewusst fallenlassen (Spec hatte Audio v1-out).

10. Scope v1 (in) / Out-of-scope

In: macOS-App · Playback (Fenster + Fullscreen) · Preset-Browser (25 + Thumbnails) · Auto-Cycle · kuratierte Live-Slider · „Reset auf Defaults" · die zwei Engine-Erweiterungen.

Out-of-scope (spätere Increments): tvOS/iOS/iPadOS-UI · voller Parameter-Editor · Custom-Preset-Speichern/-Verwalten · Audio · Capture/Export · Hue Entertainment · CloudKit-Sync · neue notarisierte DMG.

11. Offene Risiken / noch zu verifizieren

  • XcodeGen-Verfügbarkeit: brew install xcodegen (autonom installierbar). Falls unerwünscht, Fallback Tuist/Xcode-GUI — aber XcodeGen ist gewählt.
  • project.yml Multiplatform-Single-Target-Syntax: ein Target mit mehreren supportedDestinations (XcodeGen unterstützt das); im Plan verifizieren.
  • Live-Param-Update Thread-Safety: updateLiveParams wird vom Main/Draw-Thread gerufen, writeParams läuft im Draw-Loop — beide auf derselben SimEngine; im Plan sicherstellen, dass der Push vor dem nächsten step() sichtbar ist (einfachster Weg: beide auf dem Draw-Thread).
  • Asset-Pfad im Bundle: Bundle.main (App) vs Bundle.module (Package) — die App lädt Presets/Thumbnails aus Bundle.main; im Plan korrekt verdrahten.