thats-me/frontend/dev/init-fl.html
2026-04-30 14:54:39 +02:00

564 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FloatingLines Dev</title>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js"
}
}
</script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
background: #000;
overflow: hidden;
font-family: ui-monospace, 'Cascadia Code', monospace;
}
#app {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
#container {
flex: 1;
min-height: 0;
position: relative;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* ── Footer ──────────────────────────────────────────────────────── */
#controls {
flex-shrink: 0;
background: #0d0d0f;
border-top: 1px solid #222;
padding: 10px 14px;
display: grid;
grid-template-columns: 1fr 1fr 2fr 0.8fr 1.2fr;
gap: 10px 16px;
max-height: 230px;
overflow-y: auto;
}
.ctrl-group {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 0;
}
.ctrl-group h3 {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #555;
padding-bottom: 3px;
border-bottom: 1px solid #1e1e1e;
margin-bottom: 2px;
}
.row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.row label {
font-size: 10px;
color: #777;
white-space: nowrap;
min-width: 60px;
flex-shrink: 0;
}
.row input[type='range'] {
flex: 1;
min-width: 0;
height: 3px;
accent-color: #a855f7;
cursor: pointer;
}
.row .val {
font-size: 10px;
color: #bbb;
min-width: 36px;
text-align: right;
flex-shrink: 0;
}
/* Punkthöhen-Grid */
#point-sliders {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 10px;
}
.point-row {
display: flex;
align-items: center;
gap: 5px;
min-width: 0;
}
.point-row label {
font-size: 10px;
flex-shrink: 0;
min-width: 22px;
}
.point-row input[type='range'] {
flex: 1;
min-width: 0;
height: 3px;
accent-color: #a855f7;
cursor: pointer;
}
.point-row .val {
font-size: 10px;
color: #bbb;
min-width: 32px;
text-align: right;
flex-shrink: 0;
}
.point-row.inactive {
opacity: 0.25;
pointer-events: none;
}
.img-btn {
background: #1a1a2e;
border: 1px solid #333;
color: #888;
font-family: inherit;
font-size: 10px;
padding: 3px 7px;
border-radius: 3px;
cursor: pointer;
transition: all 0.15s;
}
.img-btn:hover {
border-color: #a855f7;
color: #ccc;
}
.img-btn.active {
border-color: #a855f7;
color: #a855f7;
background: #2a1a3e;
}
</style>
</head>
<body>
<div id="app">
<div id="container"></div>
<footer id="controls">
<!-- Col 1: Linien -->
<div class="ctrl-group">
<h3>Linien</h3>
<div class="row">
<label for="speed">Speed</label>
<input type="range" id="speed" min="0.1" max="3" step="0.05" value="1" />
<span class="val" id="speed-val">1.00</span>
</div>
<div class="row">
<label for="lineCount">Anzahl</label>
<input type="range" id="lineCount" min="1" max="40" step="1" value="10" />
<span class="val" id="lineCount-val">10</span>
</div>
<div class="row">
<label for="spread">Wellen-Amp</label>
<input type="range" id="spread" min="0.01" max="1" step="0.01" value="0.5" />
<span class="val" id="spread-val">0.05</span>
</div>
<div class="row">
<label for="fanSpread">Fächerbreite</label>
<input type="range" id="fanSpread" min="0.01" max="0.5" step="0.005" value="0.05" />
<span class="val" id="fanSpread-val">0.5</span>
</div>
<div class="row">
<label for="lineSharpness">Feinheit</label>
<input type="range" id="lineSharpness" min="0.3" max="10" step="0.1" value="8" />
<span class="val" id="lineSharpness-val">8.0</span>
</div>
<div class="row">
<label for="waveFreq">Welligkeit</label>
<input type="range" id="waveFreq" min="1" max="30" step="0.5" value="7" />
<span class="val" id="waveFreq-val">7.0</span>
</div>
<div class="row">
<label for="bezierCurv">Kurve</label>
<input type="range" id="bezierCurv" min="-1" max="1" step="0.05" value="0.2" />
<span class="val" id="bezierCurv-val">0.20</span>
</div>
<div class="row">
<label for="circleRadius">Kreis</label>
<input type="range" id="circleRadius" min="10" max="200" step="5" value="75" />
<span class="val" id="circleRadius-val">75px</span>
</div>
<div class="row">
<label for="glowSize">Glow Größe</label>
<input type="range" id="glowSize" min="5" max="100" step="1" value="18" />
<span class="val" id="glowSize-val">18px</span>
</div>
<div class="row">
<label for="glowStrength">Glow Stärke</label>
<input type="range" id="glowStrength" min="0.5" max="12" step="0.5" value="1.5" />
<span class="val" id="glowStrength-val">1.5</span>
</div>
</div>
<!-- Col 2: Raster -->
<div class="ctrl-group">
<h3>Raster</h3>
<div class="row">
<label for="numPoints">Punkte</label>
<input type="range" id="numPoints" min="2" max="8" step="1" value="4" />
<span class="val" id="numPoints-val">4</span>
</div>
<div class="row">
<label for="spacing">Abstand</label>
<input type="range" id="spacing" min="0.1" max="3" step="0.05" value="0.8" />
<span class="val" id="spacing-val">0.80</span>
</div>
<div class="row">
<label for="offsetX">Versatz X</label>
<input type="range" id="offsetX" min="-2" max="2" step="0.05" value="0" />
<span class="val" id="offsetX-val">0.00</span>
</div>
</div>
<!-- Col 3: Punkt-Höhen (2×4 Grid) -->
<div class="ctrl-group">
<h3>Punkt-Höhen</h3>
<div id="point-sliders">
<!-- P1P8, je Zeile: Label + Slider + Wert -->
<div class="point-row" id="pr0">
<label style="color: #a78bfa">P1</label>
<input type="range" id="py0" min="-1.2" max="1.2" step="0.02" value="-0.75" />
<span class="val" id="py0-val">-0.75</span>
</div>
<div class="point-row" id="pr1">
<label style="color: #a78bfa">P2</label>
<input type="range" id="py1" min="-1.2" max="1.2" step="0.02" value="0.5" />
<span class="val" id="py1-val">0.50</span>
</div>
<div class="point-row" id="pr2">
<label style="color: #818cf8">P3</label>
<input type="range" id="py2" min="-1.2" max="1.2" step="0.02" value="-0.3" />
<span class="val" id="py2-val">-0.30</span>
</div>
<div class="point-row" id="pr3">
<label style="color: #818cf8">P4</label>
<input type="range" id="py3" min="-1.2" max="1.2" step="0.02" value="0.75" />
<span class="val" id="py3-val">0.75</span>
</div>
<div class="point-row inactive" id="pr4">
<label style="color: #6366f1">P5</label>
<input type="range" id="py4" min="-1.2" max="1.2" step="0.02" value="-0.6" />
<span class="val" id="py4-val">-0.60</span>
</div>
<div class="point-row inactive" id="pr5">
<label style="color: #6366f1">P6</label>
<input type="range" id="py5" min="-1.2" max="1.2" step="0.02" value="0.4" />
<span class="val" id="py5-val">0.40</span>
</div>
<div class="point-row inactive" id="pr6">
<label style="color: #4f46e5">P7</label>
<input type="range" id="py6" min="-1.2" max="1.2" step="0.02" value="-0.2" />
<span class="val" id="py6-val">-0.20</span>
</div>
<div class="point-row inactive" id="pr7">
<label style="color: #4f46e5">P8</label>
<input type="range" id="py7" min="-1.2" max="1.2" step="0.02" value="0.8" />
<span class="val" id="py7-val">0.80</span>
</div>
</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>
<div style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px">
<button class="img-btn active" data-img="">Keins</button>
<button class="img-btn" data-img="../public/images/bg-image-1.jpg">1</button>
<button class="img-btn" data-img="../public/images/bg-image-2.jpg">2</button>
<button class="img-btn" data-img="../public/images/bg-image-3.jpg">3</button>
<button class="img-btn" data-img="../public/images/bg-image-4.jpg">4</button>
<button class="img-btn" data-img="../public/images/bg-image-5.jpg">5</button>
<button class="img-btn" data-img="../public/images/bg-image-6.jpg">6</button>
<button class="img-btn" data-img="../public/images/bg-image-7.jpg">7</button>
<button class="img-btn" data-img="../public/images/bg-image-8.jpg">8</button>
<button class="img-btn" data-img="../public/images/bg-image-9.jpg">9</button>
<button class="img-btn" data-img="../public/images/bg-image-10.jpg">10</button>
</div>
<h3>Hintergrundfarbe</h3>
<div class="row">
<label for="bgCenter">BG Mitte</label>
<input
type="color"
id="bgCenter"
value="#0a0514"
style="
width: 36px;
height: 18px;
border: none;
padding: 0;
background: none;
cursor: pointer;
"
/>
</div>
<div class="row">
<label for="bgEdge">BG Rand</label>
<input
type="color"
id="bgEdge"
value="#000000"
style="
width: 36px;
height: 18px;
border: none;
padding: 0;
background: none;
cursor: pointer;
"
/>
</div>
<div class="row" style="flex-direction: column; align-items: flex-start; gap: 4px">
<label style="color: #555; font-size: 9px">Farbstopps (je Zeile ein Hex)</label>
<textarea
id="gradientInput"
spellcheck="false"
style="
width: 100%;
height: 70px;
background: #111;
border: 1px solid #2a2a2a;
color: #ccc;
font-family: inherit;
font-size: 10px;
padding: 4px 6px;
resize: none;
border-radius: 3px;
outline: none;
line-height: 1.5;
"
>
#e947f5
#2f4ba2
#0a0a12</textarea
>
<button
id="applyGradient"
style="
background: #1e1e2e;
border: 1px solid #333;
color: #a855f7;
font-family: inherit;
font-size: 10px;
padding: 3px 10px;
border-radius: 3px;
cursor: pointer;
align-self: flex-end;
"
>
Anwenden
</button>
</div>
</div>
</footer>
</div>
<script type="module">
import FloatingLines from './floating-lines.js'
const container = document.getElementById('container')
// Slider-Y ist screen-intuitiv: +1 = oben = shader-Y negativ → Y-Flip beim Setzen
const initPointY = [-0.75, 0.5, -0.3, 0.75, -0.6, 0.4, -0.2, 0.8].map((v) => -v) // flip für shader
const fl = new FloatingLines(container, {
enabledWaves: ['middle'],
lineCount: [10],
numPoints: 4,
pointSpacingX: 0.8,
pointsOffsetX: 0.0,
pointYValues: initPointY,
lineSpread: 0.05,
fanSpread: 0.05,
lineSharpness: 8.0,
waveFrequency: 7.0,
circleRadiusPx: 75,
circleGlowSize: 18,
circleGlowStrength: 1.5,
interactive: false,
parallax: false,
linesGradient: ['#e947f5', '#2f4ba2', '#0a0a12'],
})
// ── Helpers ───────────────────────────────────────────────────────
function slider(id, decimals, onChange) {
const input = document.getElementById(id)
const disp = document.getElementById(id + '-val')
input.addEventListener('input', () => {
const v = parseFloat(input.value)
if (disp) disp.textContent = v.toFixed(decimals)
onChange(v)
})
}
function hexToVec3(hex) {
let v = hex.trim().replace('#', '')
if (v.length === 3) v = v[0] + v[0] + v[1] + v[1] + v[2] + v[2]
return [
parseInt(v.slice(0, 2), 16) / 255,
parseInt(v.slice(2, 4), 16) / 255,
parseInt(v.slice(4, 6), 16) / 255,
]
}
// ── Linien ────────────────────────────────────────────────────────
slider('speed', 2, (v) => (fl.uniforms.animationSpeed.value = v))
slider('lineCount', 0, (v) => (fl.uniforms.middleLineCount.value = Math.round(v)))
slider('spread', 2, (v) => (fl.uniforms.lineSpread.value = v))
slider('fanSpread', 2, (v) => (fl.uniforms.fanSpread.value = v))
slider('lineSharpness', 1, (v) => (fl.uniforms.lineSharpness.value = v))
slider('waveFreq', 1, (v) => (fl.uniforms.waveFrequency.value = v))
slider('bezierCurv', 2, (v) => (fl.uniforms.bezierCurvature.value = v))
slider('glowSize', 1, (v) => (fl.uniforms.circleGlowSize.value = v))
slider('glowStrength', 1, (v) => (fl.uniforms.circleGlowStrength.value = v))
const crInput = document.getElementById('circleRadius')
const crVal = document.getElementById('circleRadius-val')
crInput.addEventListener('input', () => {
const px = parseFloat(crInput.value)
crVal.textContent = px + 'px'
fl.uniforms.circleRadiusPx.value = px
})
// ── Raster ────────────────────────────────────────────────────────
function updateActivePoints(n) {
for (let i = 0; i < 8; i++) {
document.getElementById('pr' + i).classList.toggle('inactive', i >= n)
}
}
const numPointsInput = document.getElementById('numPoints')
const numPointsVal = document.getElementById('numPoints-val')
numPointsInput.addEventListener('input', () => {
const n = parseInt(numPointsInput.value)
numPointsVal.textContent = n
fl.uniforms.numPoints.value = n
updateActivePoints(n)
})
slider('spacing', 2, (v) => (fl.uniforms.pointSpacingX.value = v))
slider('offsetX', 2, (v) => (fl.uniforms.pointsOffsetX.value = v))
// ── Punkt-Höhen (Y-Flip) ──────────────────────────────────────────
for (let i = 0; i < 8; i++) {
const input = document.getElementById('py' + i)
const disp = document.getElementById('py' + i + '-val')
input.addEventListener('input', () => {
const v = parseFloat(input.value)
disp.textContent = v.toFixed(2)
fl.uniforms.pointY.value[i] = -v // Y-Flip
})
}
// ── Hintergrundbild ───────────────────────────────────────────────
document.querySelectorAll('.img-btn').forEach((btn) => {
btn.addEventListener('click', () => {
document.querySelectorAll('.img-btn').forEach((b) => b.classList.remove('active'))
btn.classList.add('active')
const img = btn.dataset.img
container.style.backgroundImage = img ? `url('${img}')` : 'none'
})
})
// ── Hintergrundverlauf ────────────────────────────────────────────
function hexToUniformVec3(hex, uniform) {
const [r, g, b] = hexToVec3(hex)
uniform.value.set(r, g, b)
}
document.getElementById('bgCenter').addEventListener('input', (e) => {
hexToUniformVec3(e.target.value, fl.uniforms.bgColorCenter)
})
document.getElementById('bgEdge').addEventListener('input', (e) => {
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() {
const lines = document
.getElementById('gradientInput')
.value.split('\n')
.map((s) => s.trim())
.filter((s) => s.length > 0)
const stops = lines.slice(0, MAX_STOPS)
fl.uniforms.lineGradientCount.value = stops.length
stops.forEach((hex, i) => {
const [r, g, b] = hexToVec3(hex)
fl.uniforms.lineGradient.value[i].set(r, g, b)
})
}
document.getElementById('applyGradient').addEventListener('click', applyGradient)
</script>
</body>
</html>