522 lines
19 KiB
HTML
522 lines
19 KiB
HTML
<!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 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">
|
||
<!-- P1–P8, 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 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)
|
||
})
|
||
|
||
// ── 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>
|