thats-me/frontend/dev/UMSETZUNG-FLOATING-LINES.md
2026-03-06 14:01:49 +01:00

16 KiB
Raw Blame History

FloatingLines Migration & Event-Integration

Stand: 20. Februar 2026 Bereich: Frontend (Quasar/Vue.js 3)


1. Zusammenfassung

Die alte LifeWave-Visualisierung (zwei Versionen: Spline + Glow) wurde komplett durch eine einzige FloatingLines-Implementierung ersetzt. Diese basiert auf einem WebGL-Fragment-Shader (Three.js), der animierte Bezier-Linien zwischen Event-Punkten zeichnet, inklusive farbiger Glow-Kreise pro Event.

Was wurde gemacht

  1. Shader aus dev/floating-lines.js migriertFloatingLines.vue
  2. Settings aus dev/init-fl.html migriertLifeWaveSettings.vue + settings.js Store
  3. Event-Punkte mit Shader synchronisiert — Shader-Kreise sitzen exakt auf den GlowDots
  4. Per-Event-Farben — Jeder Shader-Kreis und jedes Liniensegment nutzt die Emotion-Farbe des Events
  5. GlowDot vereinfacht — Nur noch weißer Kreis + Bild als Klick-Target, Glow kommt vom Shader
  6. Kreisgröße synchronisiert — Settings-Slider steuert Shader-Kreis UND DOM-Dot
  7. Zoom entkoppelt — Zoom ändert Abstände, nicht Kreisgrößen

Gelöschte Dateien

  • LifeWavePath.vue — Alte SVG-Pfad-Visualisierung
  • LifeWaveSpline.vue — Alte Spline-Kurven-Variante
  • LifeWaveGlow.vue — Alte Glow-Effekt-Variante
  • WaveSettings.vue — Alte Settings (ersetzt durch LifeWaveSettings.vue)

2. Architektur-Übersicht

┌─────────────────────────────────────────────────────────┐
│  LifeWaveLayout.vue                                     │
│                                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │  FloatingLines.vue (WebGL Fullscreen)            │    │
│  │  - Fragment Shader (GLSL)                        │    │
│  │  - Bezier-Linien zwischen Events                 │    │
│  │  - Glow-Kreise pro Event (pointColor[])          │    │
│  │  - Animierte Wellen                              │    │
│  │  - Background Gradient + optionales Bild         │    │
│  └─────────────────────────────────────────────────┘    │
│                                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │  TimelineView.vue (scrollbar, z-index: 5)        │    │
│  │  - Horizontales Scroll-Container                 │    │
│  │  - GlowDot pro Event (Klick-Target)              │    │
│  │  - Monat/Jahr-Labels                             │    │
│  │  - Pinch-to-Zoom                                 │    │
│  │  - Emittiert @view-update an Layout              │    │
│  └─────────────────────────────────────────────────┘    │
│                                                         │
│  Header | AddEventButton | EventPanel | LifeWaveSettings│
└─────────────────────────────────────────────────────────┘

3. Datenfluss

3.1 Event-Positionen → Shader

TimelineView                    LifeWaveLayout              FloatingLines
─────────────                   ──────────────              ──────────────
displayEvents ──@viewUpdate──►  onViewUpdate()
(emotion, x, color)            │
                               ├─ shaderNumPoints (computed)
                               ├─ shaderPointX[]  (computed)  ──► pointX[8] uniform
                               ├─ shaderPointY[]  (computed)  ──► pointY[8] uniform
                               └─ shaderPointColors[] (comp.) ──► pointColor[8] uniform

Koordinaten-Konvertierung (Screen → Shader UV):

// Layout: screenToUV(sx, sy)
// sx, sy = CSS-Pixel vom oberen linken Viewport-Rand
function screenToUV(sx, sy) {
  const w = layoutWidth // = 100dvh Breite
  const h = layoutHeight // = 100dvh Höhe
  return {
    x: (2 * sx - w) / h,
    y: (2 * sy - h) / h,
  }
}
// Shader: gleiche Formel
vec2 baseUv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
baseUv.y *= -1.0;  // Y-Flip (CSS: top→bottom, GL: bottom→top)

GlowDot Y-Position:

yPercent = 50 - emotion * 35
  emotion +1.0 → top  (15%)
  emotion  0.0 → mitte (50%)
  emotion -1.0 → unten (85%)

Screen Y für Shader:

TIMELINE_TOP = 60px  (CSS: .timeline { top: 60px })
screenY = TIMELINE_TOP + (yPercent / 100) * containerHeight

3.2 Emotion-Slider → Live-Update

EventPanel                  Events Store              TimelineView          Shader
──────────                  ────────────              ────────────          ──────
v-model="ghostEmotion" ──► ghostEmotion (ref)
                           │
                           ├─ watch → persistToEvent()
                           │         (updates events[])
                           │
                           └─ sortedEvents (computed) ──► displayEvents
                                                         │
                                                         └─ watch → emitViewState()
                                                                    │
                                                            Layout: shaderPointY[]
                                                            Layout: shaderPointColors[]
                                                                    │
                                                            FloatingLines: watch → uniform update

3.3 Event-Farben

Jeder Event hat eine Glow-Farbe basierend auf:

  1. event.customColor (falls gesetzt, hat Priorität)
  2. emotionToColor(emotion, gradientPreset) — interpoliert zwischen 3 Farben
events.js: getGlowColor(event)
  → customColor || emotionToColor(emotion, gradientPreset)

10 Gradient-Presets: Standard, Sunset, Earth, Ocean, Spring,
                     Neon, Pastel, Aurora, Forest, Berry

Die Farbe fließt als pointColor[8] Uniform in den Shader:

  • Kreise: vec3 circCol = pointColor[p]
  • Liniensegmente: vec3 lineCol = mix(pointColor[s], pointColor[s+1], t_seg)

4. Komponenten-Referenz

4.1 FloatingLines.vue

Zweck: Fullscreen WebGL-Hintergrund mit animierten Bezier-Linien und Glow-Kreisen.

Technologie: Three.js mit custom Fragment Shader (GLSL).

Props:

Prop Typ Default Beschreibung
numPoints Number 0 Anzahl aktiver Punkte (max 8)
pointXValues Array [] X-UV-Koordinaten der Punkte
pointYValues Array [] Y-UV-Koordinaten der Punkte
pointColors Array [] Hex-Farben pro Punkt (z.B. '#ff0000')
lineCount Array/Number [10] Anzahl Wellenlinien
animationSpeed Number 1 Geschwindigkeit der Wellenanimation
lineSpread Number 0.05 Wellenamplitude
fanSpread Number 0.05 Fächerbreite der Linien
lineSharpness Number 8.0 Feinheit/Schärfe der Linien
waveFrequency Number 7.0 Welligkeit
bezierCurvature Number 0.2 Kurvenstärke der Bezier-Verbindungen
circleRadiusPx Number 75 Kreisradius in Pixeln
circleGlowSize Number 18 Glow-Ausdehnung um den Kreis
circleGlowStrength Number 1.5 Glow-Intensität
linesGradient Array [...] Hex-Farbwerte für Linien-Gradient
bgColorCenter String '#0a0514' Hintergrundfarbe Mitte
bgColorEdge String '#000000' Hintergrundfarbe Rand
backgroundImage String '' URL für Hintergrundbild
mixBlendMode String 'screen' CSS Blend-Mode des Canvas

Shader-Architektur:

  • drawCircle() — Zeichnet weißen Kern + farbigen Glow + Fog
  • waveFocal() — Berechnet Wellenlinien entlang Bezier-Segmenten
  • bezierClosestT() — Findet nächsten Punkt auf quadratischer Bezier-Kurve
  • mainImage() — Compositing: Background + Segmente + Kreise

4.2 GlowDot.vue

Zweck: Klickbarer DOM-Overlay pro Event (weißer Kreis + optionales Bild).

Größe: Dynamisch aus settingsStore.floatingLines.circleRadius:

const dpr = Math.min(window.devicePixelRatio || 1, 2)
const dotSize = (2 * circleRadius) / dpr // Matches shader circle

Kein Zoom-Scaling — Größe ist konstant, unabhängig vom Zoom-Level.

Props: event, x, isGhost, selected

4.3 TimelineView.vue

Zweck: Horizontal scrollbarer Container mit GlowDots und Labels.

CSS-Position: top: 60px; bottom: 70px (unterhalb Header, oberhalb AddButton)

Features:

  • Pinch-to-Zoom (Touch + Ctrl+Wheel)
  • Zoom-Range: 0.4x 3.0x
  • Scroll-to-center beim Mount (letztes Event)
  • Ghost-Event-Insertion bei Panel-Open (Create-Mode)

Emits:

  • @dotSelect(eventId) — Event angeklickt
  • @viewUpdate({ scrollLeft, viewportWidth, containerHeight, events[] }) — Bei jedem Scroll/Zoom/Resize/Event-Change

4.4 LifeWaveLayout.vue

Zweck: Haupt-Layout, orchestriert alle Komponenten.

Verantwortlichkeiten:

  • Empfängt @view-update von TimelineView
  • Konvertiert Screen-Pixel → Shader-UV-Koordinaten
  • Berechnet shaderNumPoints, shaderPointX[], shaderPointY[], shaderPointColors[]
  • Reicht Settings-Store-Werte an FloatingLines weiter
  • Parsed Gradient-Stops aus dem Textarea-String

Wichtige Konstante: TIMELINE_TOP = 60 (muss mit .timeline { top: 60px } übereinstimmen)

4.5 LifeWaveSettings.vue

Zweck: Einstellungs-Panel (Slide-Up, 75dvh).

Sektionen:

  1. Linien — Speed, Anzahl, Wellen-Amp, Fächerbreite, Feinheit, Welligkeit, Kurve, Kreis, Glow Größe, Glow Stärke
  2. Hintergrundbild — 10 vordefinierte Bilder (/images/bg-image-1.jpg bis 10.jpg)
  3. Hintergrundfarbe — BG Mitte + BG Rand (Color Picker)
  4. Farbverlauf — Textarea mit Hex-Werten (eine pro Zeile)
  5. Extras — Dark/Light-Mode Toggle
  6. Reset — Setzt alle Werte auf Defaults zurück

4.6 EventPanel.vue

Zweck: Event-Erstellung und -Bearbeitung (Slide-Up, 75dvh).

Features:

  • Key Image Upload (Platzhalter)
  • Titel-Input (inline, groß)
  • Datum-Picker (QDate mit deutscher Locale)
  • Emotion-Slider (-1 bis +1) mit Gradient-Track
  • 10 Gradient-Presets + "Standard"-Option
  • Beschreibungs-Textarea
  • Weitere Medien (Platzhalter)
  • Event löschen (nur Edit-Mode)
  • Auto-Save: Änderungen werden sofort auf das Event persistiert

5. Stores

5.1 events.js

// State
events          // Array aller Events
selectedEventId // Aktuell ausgewählter Event (oder null)
panelOpen       // Ob EventPanel offen ist
editingEventId  // ID des Events im Edit-Mode (null = Create)
ghost*          // Temporäre Felder für Live-Preview (ghostEmotion, ghostTitle, ...)

// Computed
ghostEvent      // Computed Event-Objekt aus ghost-Feldern
sortedEvents    // Nach Datum sortierte Events

// Methods
selectEvent(id), openPanel(eventId?), closePanel(), deleteEvent(id)
getGlowColor(event)  // → Hex-Farbe basierend auf Emotion + Preset

Demo-Daten: 8 Events (19952023) mit verschiedenen Emotionen, Presets und Bildern.

5.2 settings.js

// State
theme           // 'light' | 'dark'
floatingLines   // Objekt mit allen Shader-Parametern

// Methods
toggleTheme(), updateFloatingLines(changes), resetFloatingLines()

// Persistence
localStorage.setItem('thatsme-settings', JSON.stringify({...}))

Defaults: Siehe FLOATING_LINES_DEFAULTS in settings.js.


6. CSS-Architektur

6.1 Globale Styles (app.scss)

  • .glass--button — Glasmorphismus für Buttons (blur + transparenter Hintergrund)
  • .glass--panel — Glasmorphismus für Slide-Up-Panels
    • Light: background: rgba(255,255,255,0.7); color: #1a1a1a
    • Dark: background: rgba(30,30,30,0.7); color: #f5f5f5

6.2 Quasar Theme (quasar.variables.scss)

$primary: #d946ef; // Fuchsia — Slider, Toggles, aktive States
$secondary: #a855f7; // Purple
$accent: #ec4899; // Pink

6.3 Wichtige CSS-Hinweise

Timeline-Positionierung:

/* TimelineView.vue — eigene Positionierung */
.timeline {
  position: absolute;
  top: 60px;
  bottom: 70px;
}

/* LifeWaveLayout.vue — NUR z-index, KEIN inset: 0! */
/* inset: 0 würde top/bottom der Timeline überschreiben (CSS Cascade) */
.lifewave-layout__timeline {
  z-index: 5;
}

GlowDot — kein Zoom-Scaling:

.glow-dot {
  transform: translate(-50%, -50%);
}
/* Breite/Höhe kommt dynamisch aus dem Settings-Store */

7. Bekannte Einschränkungen

  1. Max 8 Events im ShaderpointX[8], pointY[8], pointColor[8] sind fest auf 8 begrenzt. Bei mehr als 8 Events werden nur die ersten 8 als Shader-Punkte dargestellt.
  2. Bilder nur als Demo — Key-Image-Upload und Medien-Upload sind Platzhalter (TODO).
  3. Kein Backend-Sync — Alle Daten liegen nur lokal (Demo-Events + localStorage für Settings).
  4. DPR-Abhängigkeit — Die GlowDot-Größe wird einmalig beim Mount aus window.devicePixelRatio berechnet. Bei Wechsel zwischen Displays (z.B. Retina → Nicht-Retina) stimmt die Größe nicht mehr exakt.
  5. Hintergrundbilder — Müssen unter /images/bg-image-{1-10}.jpg auf dem Webspace liegen.

8. Entwicklung fortsetzen

Dev-Server starten

# Im Docker-Container quasar.app:
npm run dev
# → http://app.thats-me.test:9000

Produktions-Build

npm run build
# → Output: frontend/dist/spa/
# Statisches SPA, einfach hochladen

Dateien für die Weiterentwicklung

Was Wo
Shader-Code (GLSL) FloatingLines.vue (Zeile ~67366)
UV-Konvertierung LifeWaveLayout.vuescreenToUV()
Event-Farben events.jsemotionToColor(), getGlowColor()
Settings-Defaults settings.jsFLOATING_LINES_DEFAULTS
Slider-Ranges LifeWaveSettings.vue (:min, :max, :step auf jedem q-slider)
Quasar-Theme quasar.variables.scss
Glass-Styles app.scss.glass--panel, .glass--button
Dev-Referenz dev/init-fl.html, dev/floating-lines.js (Original-Prototyp)

Nächste Schritte (offen)

  • Key-Image-Upload implementieren (Camera/File-Picker → IndexedDB/S3)
  • Medien-Gallery pro Event
  • Backend-Sync über Laravel REST API
  • Mehr als 8 Events im Shader unterstützen (dynamisches Chunking oder LOD)
  • Touch-Gesten: Long-Press auf GlowDot für Kontextmenü
  • Onboarding / Leer-Zustand wenn keine Events vorhanden