16 KiB
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
- Shader aus
dev/floating-lines.jsmigriert →FloatingLines.vue - Settings aus
dev/init-fl.htmlmigriert →LifeWaveSettings.vue+settings.jsStore - Event-Punkte mit Shader synchronisiert — Shader-Kreise sitzen exakt auf den GlowDots
- Per-Event-Farben — Jeder Shader-Kreis und jedes Liniensegment nutzt die Emotion-Farbe des Events
- GlowDot vereinfacht — Nur noch weißer Kreis + Bild als Klick-Target, Glow kommt vom Shader
- Kreisgröße synchronisiert — Settings-Slider steuert Shader-Kreis UND DOM-Dot
- Zoom entkoppelt — Zoom ändert Abstände, nicht Kreisgrößen
Gelöschte Dateien
LifeWavePath.vue— Alte SVG-Pfad-VisualisierungLifeWaveSpline.vue— Alte Spline-Kurven-VarianteLifeWaveGlow.vue— Alte Glow-Effekt-VarianteWaveSettings.vue— Alte Settings (ersetzt durchLifeWaveSettings.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:
event.customColor(falls gesetzt, hat Priorität)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 + FogwaveFocal()— Berechnet Wellenlinien entlang Bezier-SegmentenbezierClosestT()— Findet nächsten Punkt auf quadratischer Bezier-KurvemainImage()— 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-updatevon 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:
- Linien — Speed, Anzahl, Wellen-Amp, Fächerbreite, Feinheit, Welligkeit, Kurve, Kreis, Glow Größe, Glow Stärke
- Hintergrundbild — 10 vordefinierte Bilder (
/images/bg-image-1.jpgbis10.jpg) - Hintergrundfarbe — BG Mitte + BG Rand (Color Picker)
- Farbverlauf — Textarea mit Hex-Werten (eine pro Zeile)
- Extras — Dark/Light-Mode Toggle
- 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 (1995–2023) 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
- Light:
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
- 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. - Bilder nur als Demo — Key-Image-Upload und Medien-Upload sind Platzhalter (TODO).
- Kein Backend-Sync — Alle Daten liegen nur lokal (Demo-Events + localStorage für Settings).
- DPR-Abhängigkeit — Die GlowDot-Größe wird einmalig beim Mount aus
window.devicePixelRatioberechnet. Bei Wechsel zwischen Displays (z.B. Retina → Nicht-Retina) stimmt die Größe nicht mehr exakt. - Hintergrundbilder — Müssen unter
/images/bg-image-{1-10}.jpgauf 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 ~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