# 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` migriert** → `FloatingLines.vue` 2. **Settings aus `dev/init-fl.html` migriert** → `LifeWaveSettings.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):** ```js // 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, } } ``` ```glsl // 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`: ```js 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 ```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 (1995–2023) mit verschiedenen Emotionen, Presets und Bildern. ### 5.2 settings.js ```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`) ```scss $primary: #d946ef; // Fuchsia — Slider, Toggles, aktive States $secondary: #a855f7; // Purple $accent: #ec4899; // Pink ``` ### 6.3 Wichtige CSS-Hinweise **Timeline-Positionierung:** ```css /* 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:** ```css .glow-dot { transform: translate(-50%, -50%); } /* Breite/Höhe kommt dynamisch aus dem Settings-Store */ ``` --- ## 7. Bekannte Einschränkungen 1. **Max 8 Events im Shader** — `pointX[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 ```bash # Im Docker-Container quasar.app: npm run dev # → http://app.thats-me.test:9000 ``` ### Produktions-Build ```bash npm run build # → Output: frontend/dist/spa/ # Statisches SPA, einfach hochladen ``` ### Dateien für die Weiterentwicklung | Was | Wo | | ------------------ | --------------------------------------------------------------------- | | Shader-Code (GLSL) | `FloatingLines.vue` (Zeile ~67–366) | | UV-Konvertierung | `LifeWaveLayout.vue` → `screenToUV()` | | Event-Farben | `events.js` → `emotionToColor()`, `getGlowColor()` | | Settings-Defaults | `settings.js` → `FLOATING_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