30-04-2026
This commit is contained in:
parent
761b1156c1
commit
d054732bf5
35 changed files with 2796 additions and 505 deletions
200
frontend/dev/IMPROVEMENTS-floating-lines.md
Normal file
200
frontend/dev/IMPROVEMENTS-floating-lines.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# Verbesserungsvorschläge: floating-lines.js
|
||||
|
||||
Analyse von `floating-lines.js` (Dev-Klasse) im Vergleich zur produktiven `FloatingLines.vue`.
|
||||
**Stand: April 2026 — alle wesentlichen Punkte umgesetzt.**
|
||||
|
||||
---
|
||||
|
||||
## Was der Code macht (Kurzübersicht)
|
||||
|
||||
Ein WebGL-Fullscreen-Shader via Three.js mit drei visuellen Schichten:
|
||||
|
||||
- **Top/Bottom**: Einfache Sinus-Wellen mit Rotation (`wave()`)
|
||||
- **Middle**: Bézier-Kurven zwischen Kontrollpunkten mit animierten Fächer-Linien (`waveFocal()`) + Kreise an den Punkten
|
||||
- **Hintergrund**: Radialer Verlauf von Mitte → Rand (oder Horizont-Split bei Modus „Trennung")
|
||||
|
||||
---
|
||||
|
||||
## Fehler / Bugs
|
||||
|
||||
### 1. Totes Uniform `bgColor` ✅ Umgesetzt
|
||||
**Datei:** `floating-lines.js`, Zeile 508
|
||||
Das tote `bgColor`-Uniform wurde entfernt. Der Shader nutzt ausschließlich `bgColorCenter` + `bgColorEdge`.
|
||||
|
||||
```js
|
||||
// Entfernt:
|
||||
bgColor: { value: new Vector3(0, 0, 0) },
|
||||
```
|
||||
|
||||
### 2. Konstruktor-Parameter `middleWavePosition` wird ignoriert ✅ Umgesetzt
|
||||
Der ungenutzte Parameter wurde aus der Konstruktorsignatur entfernt.
|
||||
|
||||
### 3. Kein Konstruktor-Interface für `bgColorCenter` / `bgColorEdge` ✅ Umgesetzt
|
||||
Beide Uniforms sind jetzt als Konstruktor-Parameter verfügbar und werden korrekt initialisiert:
|
||||
|
||||
```js
|
||||
constructor({ ..., bgColorCenter = '#0a0514', bgColorEdge = '#000000', ... })
|
||||
```
|
||||
|
||||
Die Uniforms werden beim Konstruktoraufruf aus den Hex-Strings in `Vector3`-Werte konvertiert.
|
||||
|
||||
---
|
||||
|
||||
## Performance-Problem
|
||||
|
||||
### 4. `bezierClosestT` wird pro Linie neu berechnet (kritisch) ✅ Umgesetzt
|
||||
`bezierClosestT` wird jetzt **einmal pro Segment** berechnet. Die Ergebnisse (`bt`, `bPos`, `bNorm`) werden als Parameter an `waveFocal()` übergeben:
|
||||
|
||||
```glsl
|
||||
// Neue waveFocal()-Signatur (precomputed values):
|
||||
float waveFocal(vec2 uv, float fi, float totalLines, float t, vec2 bPos, vec2 bNorm)
|
||||
|
||||
// Im Segment-Loop (einmal pro Segment, nicht pro Linie):
|
||||
float bt = bezierClosestT(baseUv, sp, pc, ep);
|
||||
vec2 bPos = bmt*bmt*sp + 2.0*bmt*bt*pc + bt*bt*ep;
|
||||
vec2 bTang = normalize(2.0*bmt*(pc - sp) + 2.0*bt*(ep - pc));
|
||||
vec2 bNorm = vec2(-bTang.y, bTang.x);
|
||||
// → alle middleLineCount Aufrufe nutzen dieselben Werte
|
||||
```
|
||||
|
||||
Reduktion von O(Segmente × Linien) auf O(Segmente) `bezierClosestT`-Aufrufe pro Pixel.
|
||||
|
||||
---
|
||||
|
||||
## Fehlende Features (in `.vue` vorhanden, in `.js` nicht)
|
||||
|
||||
### 5. `lineBrightness` Uniform fehlt ✅ Umgesetzt
|
||||
`lineBrightness` ist jetzt als Konstruktor-Parameter und Uniform vorhanden. Im Shader:
|
||||
|
||||
```glsl
|
||||
col *= lineBrightness; // vor Background-Composite
|
||||
```
|
||||
|
||||
### 6. Kein Pause bei verstecktem Tab ✅ Umgesetzt
|
||||
Der `requestAnimationFrame`-Loop pausiert jetzt bei `document.hidden`:
|
||||
|
||||
```js
|
||||
this._handleVisibility = () => {
|
||||
if (document.hidden) {
|
||||
cancelAnimationFrame(this.raf)
|
||||
this.raf = 0
|
||||
} else if (!this.raf) {
|
||||
renderLoop()
|
||||
}
|
||||
}
|
||||
document.addEventListener('visibilitychange', this._handleVisibility)
|
||||
// destroy() ruft removeEventListener auf
|
||||
```
|
||||
|
||||
### 7. Kein adaptives DPR ⏭️ Offen / Optional
|
||||
Die Vue-Version misst FPS live und reduziert `devicePixelRatio` bei schlechter Performance (vor allem Mobile). Die Dev-Klasse ist ein Test-Tool und nicht für Mobile ausgelegt — diese Komplexität lohnt sich hier nicht.
|
||||
|
||||
---
|
||||
|
||||
## Code-Qualität
|
||||
|
||||
### 8. Legacy-Code: `background_color()`, `BLACK`, `PINK`, `BLUE` ✅ Umgesetzt
|
||||
Die Shader-Konstanten `BLACK`, `PINK`, `BLUE` und die Funktion `background_color()` wurden entfernt. Der Hintergrund wird immer über `bgColorCenter`/`bgColorEdge` gesteuert.
|
||||
|
||||
### 9. Redundante `enabledWaves.includes()` Checks ✅ Umgesetzt
|
||||
Die doppelten Prüfungen in den Hilfsfunktionen wurden entfernt. Die äußere Prüfung im Aufrufer ist die einzige Guard.
|
||||
|
||||
### 10. Hardcoded `* 0.5` in `getLineColor()` ✅ Umgesetzt
|
||||
Der feste `* 0.5`-Faktor wurde entfernt. `getLineColor()` gibt jetzt die volle Gradient-Farbe zurück. Die Kompensation mit `* 2.5` an den Kreisen wurde auf `* 1.5` angepasst. Die Helligkeit wird über das `lineBrightness`-Uniform gesteuert (→ Punkt 5).
|
||||
|
||||
---
|
||||
|
||||
## Optionale Verbesserungen / Ideen
|
||||
|
||||
### 11. Glättere Kreise bei höherem DPR ⏭️ Offen / Optional
|
||||
Der AA-Radius passt sich durch `iResolution` (physische Pixel bei gesetztem DPR) bereits implizit an. Keine Änderung nötig.
|
||||
|
||||
### 12. `pointSpacingX` + `pointsOffsetX` vs. explizite X-Koordinaten ⏭️ Offen / Optional
|
||||
Die Dev-Klasse behält das Auto-Spacing-Modell für einfache Testzwecke. Die Vue-Komponente nutzt explizite X-Koordinaten für die Lebenszeitlinie. Beide Ansätze sind intentional verschieden.
|
||||
|
||||
### 13. GLSL `precision highp` → `mediump` ✅ Umgesetzt
|
||||
Fragment-Shader nutzt jetzt `precision mediump float` — ausreichend für diese Visualisierung, effizienter auf Mobile/Low-End.
|
||||
|
||||
---
|
||||
|
||||
## Neue Punkte (nachträglich ergänzt)
|
||||
|
||||
### 14. Resize-Bug: Kreise und Linien desynchronisieren sich ✅ Umgesetzt
|
||||
**Betrifft:** `LifeWaveLayout.vue`
|
||||
|
||||
**Ursache:** `layoutResizeObserver` aktualisierte `layoutWidth/Height` sofort, während `TimelineView`s `@view-update` (mit den neuen CSS-Event-Positionen) erst im nächsten Frame kam — führte zu 1-Frame UV-Desync.
|
||||
|
||||
**Fix:** `requestAnimationFrame`-Wrapper im Callback:
|
||||
|
||||
```js
|
||||
layoutResizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!layoutRef.value) return
|
||||
layoutWidth.value = layoutRef.value.clientWidth
|
||||
layoutHeight.value = layoutRef.value.clientHeight
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 15. Feature: Horizont ✅ Umgesetzt (erweitert)
|
||||
Statt einer einzelnen Linie wurden **drei wählbare Horizont-Modi** implementiert:
|
||||
|
||||
| Modus | Wert | Beschreibung |
|
||||
|-------|------|--------------|
|
||||
| Aus | `'off'` | Kein Horizont-Effekt |
|
||||
| Nebel | `'fog'` | Leuchtender Band-Effekt auf Y=0, Farbe aus Gradient |
|
||||
| Trennung | `'split'` | Hintergrund wird vertikal geteilt: `bgColorCenter` oben, `bgColorEdge` unten — mit einstellbarer Blend-Breite |
|
||||
| Glow | `'glow'` | Weiches + hartes Leuchten auf dem Horizont, Farbe aus Gradient |
|
||||
|
||||
**Shader (alle Modi):**
|
||||
```glsl
|
||||
uniform int horizonMode; // 0=off 1=fog 2=split 3=glow
|
||||
uniform float horizonOpacity;
|
||||
uniform float horizonBlend;
|
||||
|
||||
if (horizonMode == 1) {
|
||||
float band = exp(-baseUv.y * baseUv.y * 5.0);
|
||||
vec3 fogColor = getLineColor(0.5, bg) * 2.0;
|
||||
col += fogColor * band * horizonOpacity;
|
||||
} else if (horizonMode == 2) {
|
||||
float blendW = max(horizonBlend * 0.7, 0.001);
|
||||
float t = smoothstep(-blendW, blendW, baseUv.y);
|
||||
bg = mix(bgColorEdge, bgColorCenter, t);
|
||||
} else if (horizonMode == 3) {
|
||||
float d2 = baseUv.y * baseUv.y;
|
||||
float softGlow = exp(-d2 * 10.0);
|
||||
float coreGlow = exp(-d2 * 70.0) * 0.7;
|
||||
vec3 glowColor = getLineColor(0.5, bg) * 3.0;
|
||||
col += glowColor * (softGlow + coreGlow) * horizonOpacity;
|
||||
}
|
||||
```
|
||||
|
||||
**Umgesetzt in:**
|
||||
- `floating-lines.js` — Shader + Konstruktor-Parameter + Uniforms
|
||||
- `FloatingLines.vue` — Shader (mit `gradientMid()` Hilfsfunktion) + Props + Uniforms + Watches
|
||||
- `settings.js` — `horizonMode: 'off'`, `horizonOpacity: 0.5`, `horizonBlend: 0.2`
|
||||
- `LifeWaveSettings.vue` — Segmented Control + bedingte Slider (Deckkraft / Übergang)
|
||||
- `LifeWaveLayout.vue` — Props weitergeleitet
|
||||
- `init-fl.html` — 4 Modus-Buttons + bedingte Slider-Sichtbarkeit
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung Prioritäten
|
||||
|
||||
| # | Typ | Priorität | Aufwand | Status |
|
||||
|---|-----|-----------|---------|--------|
|
||||
| 4 | Performance: `bezierClosestT` Hoisting | **Hoch** | Mittel | ✅ |
|
||||
| 14 | Bug: Resize-Desync (LifeWaveLayout) | **Hoch** | Minimal | ✅ |
|
||||
| 1 | Bug: totes `bgColor` Uniform | Mittel | Minimal | ✅ |
|
||||
| 3 | Bug: `bgColorCenter/Edge` nicht setzbar | Mittel | Klein | ✅ |
|
||||
| 5 | Feature: `lineBrightness` | Mittel | Klein | ✅ |
|
||||
| 10 | Qualität: hardcoded `* 0.5` | Mittel | Klein | ✅ |
|
||||
| 15 | Feature: Horizont (3 Modi) | Mittel | Mittel | ✅ |
|
||||
| 8 | Qualität: Legacy-Code entfernen | Niedrig | Klein | ✅ |
|
||||
| 6 | Feature: Tab-Pause | Niedrig | Klein | ✅ |
|
||||
| 2 | Bug: ignorierter Parameter | Niedrig | Minimal | ✅ |
|
||||
| 9 | Qualität: redundante Checks | Niedrig | Minimal | ✅ |
|
||||
| 13 | Perf: `mediump` Precision | Optional | Minimal | ✅ |
|
||||
| 7 | Feature: adaptives DPR | Optional | Groß | ⏭️ |
|
||||
| 11 | Qualität: AA-Radius explizit | Optional | Minimal | ⏭️ |
|
||||
| 12 | API: explizite X-Koordinaten | Optional | Mittel | ⏭️ |
|
||||
|
|
@ -7,7 +7,6 @@ import {
|
|||
ShaderMaterial,
|
||||
Vector3,
|
||||
Vector2,
|
||||
Clock,
|
||||
} from 'three'
|
||||
|
||||
const vertexShader = `
|
||||
|
|
@ -19,7 +18,7 @@ void main() {
|
|||
`
|
||||
|
||||
const fragmentShader = `
|
||||
precision highp float;
|
||||
precision mediump float;
|
||||
|
||||
uniform float iTime;
|
||||
uniform vec3 iResolution;
|
||||
|
|
@ -58,34 +57,24 @@ uniform float bendRadius;
|
|||
uniform float bendStrength;
|
||||
uniform float bendInfluence;
|
||||
|
||||
uniform int horizonMode; // 0=off 1=fog 2=split 3=glow
|
||||
uniform float horizonOpacity; // Nebel + Glow: Helligkeit/Dichte
|
||||
uniform float horizonBlend; // Trennung: 0=scharf, 1=weicher Übergang
|
||||
|
||||
uniform bool parallax;
|
||||
uniform float parallaxStrength;
|
||||
uniform vec2 parallaxOffset;
|
||||
|
||||
uniform float lineBrightness;
|
||||
uniform vec3 lineGradient[8];
|
||||
uniform int lineGradientCount;
|
||||
uniform vec3 bgColorCenter;
|
||||
uniform vec3 bgColorEdge;
|
||||
|
||||
const vec3 BLACK = vec3(0.0);
|
||||
const vec3 PINK = vec3(233.0, 71.0, 245.0) / 255.0;
|
||||
const vec3 BLUE = vec3(47.0, 75.0, 162.0) / 255.0;
|
||||
|
||||
mat2 rotate(float r) {
|
||||
return mat2(cos(r), sin(r), -sin(r), cos(r));
|
||||
}
|
||||
|
||||
vec3 background_color(vec2 uv) {
|
||||
vec3 col = vec3(0.0);
|
||||
|
||||
float y = sin(uv.x - 0.2) * 0.3 - 0.1;
|
||||
float m = uv.y - y;
|
||||
|
||||
col += mix(BLUE, BLACK, smoothstep(0.0, 1.0, abs(m)));
|
||||
col += mix(PINK, BLACK, smoothstep(0.0, 1.0, abs(m - 0.8)));
|
||||
return col * 0.5;
|
||||
}
|
||||
|
||||
vec3 getLineColor(float t, vec3 baseColor) {
|
||||
if (lineGradientCount <= 0) {
|
||||
return baseColor;
|
||||
|
|
@ -108,7 +97,7 @@ vec3 getLineColor(float t, vec3 baseColor) {
|
|||
gradientColor = mix(c1, c2, f);
|
||||
}
|
||||
|
||||
return gradientColor * 0.5;
|
||||
return gradientColor;
|
||||
}
|
||||
|
||||
vec3 drawCircle(vec2 uv, vec2 center, float r, vec3 color) {
|
||||
|
|
@ -163,25 +152,9 @@ float bezierClosestT(vec2 q, vec2 p0, vec2 pc, vec2 p1) {
|
|||
return t;
|
||||
}
|
||||
|
||||
float waveFocal(vec2 uv, float fi, float totalLines, vec2 sp, vec2 ep) {
|
||||
// Bézier-Kontrollpunkt: Mittelpunkt + senkrechter Versatz
|
||||
vec2 seg = ep - sp;
|
||||
float segLen = length(seg);
|
||||
if (segLen < 0.001) return 0.0;
|
||||
vec2 segDir = seg / segLen;
|
||||
vec2 segPerp = vec2(-segDir.y, segDir.x);
|
||||
vec2 pc = (sp + ep) * 0.5 + segPerp * segLen * bezierCurvature;
|
||||
|
||||
float t = bezierClosestT(uv, sp, pc, ep);
|
||||
float mt = 1.0 - t;
|
||||
|
||||
// Position und Tangente auf der Kurve
|
||||
vec2 curvePos = mt*mt*sp + 2.0*mt*t*pc + t*t*ep;
|
||||
vec2 tang = normalize(2.0*mt*(pc - sp) + 2.0*t*(ep - pc));
|
||||
vec2 norm = vec2(-tang.y, tang.x);
|
||||
|
||||
// Senkrechter Abstand von der Kurve
|
||||
float s = dot(uv - curvePos, norm);
|
||||
// Accepts precomputed bezier values (bt, bPos, bNorm) — computed once per segment
|
||||
float waveFocal(vec2 uv, float fi, float totalLines, float t, vec2 bPos, vec2 bNorm) {
|
||||
float s = dot(uv - bPos, bNorm);
|
||||
|
||||
float time = iTime * animationSpeed;
|
||||
float normalizedI = totalLines > 1.0 ? fi / (totalLines - 1.0) : 0.5;
|
||||
|
|
@ -227,7 +200,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|||
|
||||
vec3 col = vec3(0.0);
|
||||
|
||||
vec3 b = lineGradientCount > 0 ? bgColorCenter : background_color(baseUv);
|
||||
vec3 b = bgColorCenter;
|
||||
|
||||
vec2 mouseUv = vec2(0.0);
|
||||
if (interactive) {
|
||||
|
|
@ -269,33 +242,34 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|||
vec2 sp = vec2(x0, pointY[s]);
|
||||
vec2 ep = vec2(x1, pointY[s + 1]);
|
||||
|
||||
// Gradient: globaler t-Bereich [s, s+1] / (numPoints-1)
|
||||
vec2 pd = ep - sp;
|
||||
float pl = length(pd);
|
||||
vec2 pa = pl > 0.001 ? pd / pl : vec2(1.0, 0.0);
|
||||
float t_seg = clamp(dot(baseUv - sp, pa) / pl, 0.0, 1.0);
|
||||
// Segment-Geometrie (einmalig berechnet, von Gradient + Bézier genutzt)
|
||||
vec2 seg = ep - sp;
|
||||
float segL = length(seg);
|
||||
vec2 segDir = segL > 0.001 ? seg / segL : vec2(1.0, 0.0);
|
||||
vec2 sPerp = vec2(-segDir.y, segDir.x);
|
||||
vec2 pc = (sp + ep) * 0.5 + sPerp * segL * bezierCurvature;
|
||||
|
||||
// Gradient
|
||||
float t_seg = clamp(dot(baseUv - sp, segDir) / segL, 0.0, 1.0);
|
||||
float t_global = (float(s) + t_seg) * tScale;
|
||||
vec3 lineCol = getLineColor(t_global, b);
|
||||
vec3 lineCol = getLineColor(t_global, b);
|
||||
|
||||
// Bézier-Kontrollpunkt für Nebel (gleiche Logik wie in waveFocal)
|
||||
vec2 segD = ep - sp;
|
||||
float segL = length(segD);
|
||||
vec2 segDir = segL > 0.001 ? segD / segL : vec2(1.0, 0.0);
|
||||
vec2 sPerp = vec2(-segDir.y, segDir.x);
|
||||
vec2 pc = (sp + ep) * 0.5 + sPerp * segL * bezierCurvature;
|
||||
// Bézier einmal pro Segment — geteilt von Nebel + allen Linien
|
||||
float bt = bezierClosestT(baseUv, sp, pc, ep);
|
||||
float bmt = 1.0 - bt;
|
||||
vec2 bPos = bmt*bmt*sp + 2.0*bmt*bt*pc + bt*bt*ep;
|
||||
vec2 bTang = normalize(2.0*bmt*(pc - sp) + 2.0*bt*(ep - pc));
|
||||
vec2 bNorm = vec2(-bTang.y, bTang.x);
|
||||
|
||||
// Weicher Nebel entlang der Bézier-Kurve → füllt dunkle Winkel organisch
|
||||
float bt = bezierClosestT(baseUv, sp, pc, ep);
|
||||
float bmt = 1.0 - bt;
|
||||
vec2 bPos = bmt*bmt*sp + 2.0*bmt*bt*pc + bt*bt*ep;
|
||||
float bDist = length(baseUv - bPos);
|
||||
// Weicher Nebel entlang der Kurve
|
||||
float bDist = length(baseUv - bPos);
|
||||
float fogFade = smoothstep(-0.06, 0.05, bt) * smoothstep(1.06, 0.95, bt);
|
||||
float fogEnv = sin(bt * 3.14159265359);
|
||||
float segFog = fogFade * fogEnv * 0.0018 / max(bDist * bDist * 4.0 + 0.012, 0.001);
|
||||
col += lineCol * segFog;
|
||||
|
||||
for (int i = 0; i < middleLineCount; ++i) {
|
||||
col += lineCol * waveFocal(baseUv, float(i), float(middleLineCount), sp, ep);
|
||||
col += lineCol * waveFocal(baseUv, float(i), float(middleLineCount), bt, bPos, bNorm);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -304,7 +278,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|||
if (p >= numPoints) break;
|
||||
float px = pointsOffsetX + (float(p) - float(numPoints - 1) * 0.5) * pointSpacingX;
|
||||
float t_pt = numPoints > 1 ? float(p) * tScale : 0.0;
|
||||
vec3 circCol = getLineColor(t_pt, b) * 2.5;
|
||||
vec3 circCol = getLineColor(t_pt, b) * 1.5;
|
||||
col += drawCircle(baseUv, vec2(px, pointY[p]), r, circCol);
|
||||
}
|
||||
}
|
||||
|
|
@ -328,9 +302,33 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|||
}
|
||||
}
|
||||
|
||||
col *= lineBrightness;
|
||||
|
||||
// Hintergrundverlauf: radial von bgColorCenter (Mitte) nach bgColorEdge (Rand)
|
||||
float dist = length(baseUv) / 1.8;
|
||||
vec3 bg = mix(bgColorCenter, bgColorEdge, clamp(dist, 0.0, 1.0));
|
||||
|
||||
if (horizonMode == 1) {
|
||||
// Nebel: breites weiches Gaussband in Gradient-Mittelfarbe
|
||||
float band = exp(-baseUv.y * baseUv.y * 5.0);
|
||||
vec3 fogColor = getLineColor(0.5, bg) * 2.0;
|
||||
col += fogColor * band * horizonOpacity;
|
||||
} else if (horizonMode == 2) {
|
||||
// Farbtrennung: vertikaler Split an Y=0
|
||||
// bgColorCenter → oben (positives UV-Y), bgColorEdge → unten
|
||||
// horizonBlend: 0=harter Schnitt, 1=sehr weicher Übergang
|
||||
float blendW = max(horizonBlend * 0.7, 0.001);
|
||||
float t = smoothstep(-blendW, blendW, baseUv.y);
|
||||
bg = mix(bgColorEdge, bgColorCenter, t);
|
||||
} else if (horizonMode == 3) {
|
||||
// Glow: konzentriertes Leuchten in Gradient-Mittelfarbe
|
||||
float d2 = baseUv.y * baseUv.y;
|
||||
float softGlow = exp(-d2 * 10.0);
|
||||
float coreGlow = exp(-d2 * 70.0) * 0.7;
|
||||
vec3 glowColor = getLineColor(0.5, bg) * 3.0;
|
||||
col += glowColor * (softGlow + coreGlow) * horizonOpacity;
|
||||
}
|
||||
|
||||
fragColor = vec4(clamp(bg + col, 0.0, 1.0), 1.0);
|
||||
}
|
||||
|
||||
|
|
@ -376,7 +374,6 @@ export default class FloatingLines {
|
|||
lineCount = [6],
|
||||
lineDistance = [5],
|
||||
topWavePosition,
|
||||
middleWavePosition,
|
||||
bottomWavePosition = { x: 2.0, y: -0.7, rotate: -1 },
|
||||
numPoints = 4,
|
||||
pointSpacingX = 0.8,
|
||||
|
|
@ -389,6 +386,7 @@ export default class FloatingLines {
|
|||
bezierCurvature = 0.3,
|
||||
circleRadiusPx = 50,
|
||||
animationSpeed = 1,
|
||||
lineBrightness = 1.0,
|
||||
interactive = true,
|
||||
bendRadius = 5.0,
|
||||
bendStrength = -0.5,
|
||||
|
|
@ -397,6 +395,11 @@ export default class FloatingLines {
|
|||
parallaxStrength = 0.2,
|
||||
circleGlowSize = 18.0,
|
||||
circleGlowStrength = 1.5,
|
||||
horizonMode = 'off',
|
||||
horizonOpacity = 0.5,
|
||||
horizonBlend = 0.2,
|
||||
bgColorCenter = '#0a0514',
|
||||
bgColorEdge = '#000000',
|
||||
mixBlendMode = 'screen',
|
||||
} = {},
|
||||
) {
|
||||
|
|
@ -427,14 +430,12 @@ export default class FloatingLines {
|
|||
return lineDistance[index] ?? 0.1
|
||||
}
|
||||
|
||||
const topLineCount = enabledWaves.includes('top') ? getLineCount('top') : 0
|
||||
const middleLineCount = enabledWaves.includes('middle') ? getLineCount('middle') : 0
|
||||
const bottomLineCount = enabledWaves.includes('bottom') ? getLineCount('bottom') : 0
|
||||
const topLineCount = getLineCount('top')
|
||||
const middleLineCount = getLineCount('middle')
|
||||
const bottomLineCount = getLineCount('bottom')
|
||||
|
||||
const topLineDistance = enabledWaves.includes('top') ? getLineDistance('top') * 0.01 : 0.01
|
||||
const bottomLineDistance = enabledWaves.includes('bottom')
|
||||
? getLineDistance('bottom') * 0.01
|
||||
: 0.01
|
||||
const topLineDistance = getLineDistance('top') * 0.01
|
||||
const bottomLineDistance = getLineDistance('bottom') * 0.01
|
||||
|
||||
this.scene = new Scene()
|
||||
this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1)
|
||||
|
|
@ -451,6 +452,7 @@ export default class FloatingLines {
|
|||
iTime: { value: 0 },
|
||||
iResolution: { value: new Vector3(1, 1, 1) },
|
||||
animationSpeed: { value: animationSpeed },
|
||||
lineBrightness: { value: lineBrightness },
|
||||
|
||||
enableTop: { value: enabledWaves.includes('top') },
|
||||
enableMiddle: { value: enabledWaves.includes('middle') },
|
||||
|
|
@ -497,6 +499,10 @@ export default class FloatingLines {
|
|||
bendStrength: { value: bendStrength },
|
||||
bendInfluence: { value: 0 },
|
||||
|
||||
horizonMode: { value: { off: 0, fog: 1, split: 2, glow: 3 }[horizonMode] ?? 0 },
|
||||
horizonOpacity: { value: horizonOpacity },
|
||||
horizonBlend: { value: horizonBlend },
|
||||
|
||||
parallax: { value: parallax },
|
||||
parallaxStrength: { value: parallaxStrength },
|
||||
parallaxOffset: { value: new Vector2(0, 0) },
|
||||
|
|
@ -505,7 +511,8 @@ export default class FloatingLines {
|
|||
value: Array.from({ length: MAX_GRADIENT_STOPS }, () => new Vector3(1, 1, 1)),
|
||||
},
|
||||
lineGradientCount: { value: 0 },
|
||||
bgColor: { value: new Vector3(0, 0, 0) },
|
||||
bgColorCenter: { value: new Vector3(0, 0, 0) },
|
||||
bgColorEdge: { value: new Vector3(0, 0, 0) },
|
||||
}
|
||||
|
||||
if (linesGradient && linesGradient.length > 0) {
|
||||
|
|
@ -517,6 +524,11 @@ export default class FloatingLines {
|
|||
})
|
||||
}
|
||||
|
||||
const center = hexToVec3(bgColorCenter)
|
||||
this.uniforms.bgColorCenter.value.set(center.x, center.y, center.z)
|
||||
const edge = hexToVec3(bgColorEdge)
|
||||
this.uniforms.bgColorEdge.value.set(edge.x, edge.y, edge.z)
|
||||
|
||||
const material = new ShaderMaterial({
|
||||
uniforms: this.uniforms,
|
||||
vertexShader,
|
||||
|
|
@ -529,7 +541,7 @@ export default class FloatingLines {
|
|||
|
||||
this.geometry = geometry
|
||||
this.material = material
|
||||
this.clock = new Clock()
|
||||
this._startTime = performance.now()
|
||||
|
||||
this._setSize = () => {
|
||||
const width = container.clientWidth || 1
|
||||
|
|
@ -571,7 +583,7 @@ export default class FloatingLines {
|
|||
|
||||
this.raf = 0
|
||||
const renderLoop = () => {
|
||||
this.uniforms.iTime.value = this.clock.getElapsedTime()
|
||||
this.uniforms.iTime.value = (performance.now() - this._startTime) * 0.001
|
||||
|
||||
if (this.interactive) {
|
||||
this.currentMouse.lerp(this.targetMouse, this.mouseDamping)
|
||||
|
|
@ -589,12 +601,24 @@ export default class FloatingLines {
|
|||
this.renderer.render(this.scene, this.camera)
|
||||
this.raf = requestAnimationFrame(renderLoop)
|
||||
}
|
||||
|
||||
this._handleVisibility = () => {
|
||||
if (document.hidden) {
|
||||
cancelAnimationFrame(this.raf)
|
||||
this.raf = 0
|
||||
} else if (!this.raf) {
|
||||
renderLoop()
|
||||
}
|
||||
}
|
||||
document.addEventListener('visibilitychange', this._handleVisibility)
|
||||
|
||||
renderLoop()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
cancelAnimationFrame(this.raf)
|
||||
if (this.ro) this.ro.disconnect()
|
||||
document.removeEventListener('visibilitychange', this._handleVisibility)
|
||||
|
||||
this.renderer.domElement.removeEventListener('pointermove', this._handlePointerMove)
|
||||
this.renderer.domElement.removeEventListener('pointerleave', this._handlePointerLeave)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
border-top: 1px solid #222;
|
||||
padding: 10px 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 2fr 1.2fr;
|
||||
grid-template-columns: 1fr 1fr 2fr 0.8fr 1.2fr;
|
||||
gap: 10px 16px;
|
||||
max-height: 230px;
|
||||
overflow-y: auto;
|
||||
|
|
@ -287,6 +287,27 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Col 3.5: Horizont -->
|
||||
<div class="ctrl-group" style="grid-column: span 1">
|
||||
<h3>Horizont</h3>
|
||||
<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px;">
|
||||
<button class="img-btn active" data-mode="0">Aus</button>
|
||||
<button class="img-btn" data-mode="1">Nebel</button>
|
||||
<button class="img-btn" data-mode="2">Trennung</button>
|
||||
<button class="img-btn" data-mode="3">Glow</button>
|
||||
</div>
|
||||
<div class="row" id="row-horizonOpacity">
|
||||
<label for="horizonOpacity">Deckkraft</label>
|
||||
<input type="range" id="horizonOpacity" min="0.05" max="1" step="0.05" value="0.5" />
|
||||
<span class="val" id="horizonOpacity-val">0.50</span>
|
||||
</div>
|
||||
<div class="row" id="row-horizonBlend" style="display:none">
|
||||
<label for="horizonBlend">Übergang</label>
|
||||
<input type="range" id="horizonBlend" min="0" max="1" step="0.02" value="0.2" />
|
||||
<span class="val" id="horizonBlend-val">0.20</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Col 4: Hintergrundbild + Farben -->
|
||||
<div class="ctrl-group">
|
||||
<h3>Hintergrundbild</h3>
|
||||
|
|
@ -501,6 +522,27 @@
|
|||
hexToUniformVec3(e.target.value, fl.uniforms.bgColorEdge)
|
||||
})
|
||||
|
||||
// ── Horizont ─────────────────────────────────────────────────────
|
||||
const rowOpacity = document.getElementById('row-horizonOpacity')
|
||||
const rowBlend = document.getElementById('row-horizonBlend')
|
||||
|
||||
function updateHorizonRows(mode) {
|
||||
rowOpacity.style.display = mode === 2 ? 'none' : 'flex'
|
||||
rowBlend.style.display = mode === 2 ? 'flex' : 'none'
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-mode]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-mode]').forEach((b) => b.classList.remove('active'))
|
||||
btn.classList.add('active')
|
||||
const mode = parseInt(btn.dataset.mode)
|
||||
fl.uniforms.horizonMode.value = mode
|
||||
updateHorizonRows(mode)
|
||||
})
|
||||
})
|
||||
slider('horizonOpacity', 2, (v) => (fl.uniforms.horizonOpacity.value = v))
|
||||
slider('horizonBlend', 2, (v) => (fl.uniforms.horizonBlend.value = v))
|
||||
|
||||
// ── Gradient ──────────────────────────────────────────────────────
|
||||
const MAX_STOPS = 8
|
||||
function applyGradient() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue