408 lines
16 KiB
Markdown
408 lines
16 KiB
Markdown
# 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
|