CSS
<style>
.line-body {
margin: 0;
font-family: 'Arial', sans-serif;
background-color: #1a1a1a; /* Fallback */
color: white;
}
.line-body-hero-panel { /* Renamed to background-container for clarity, or keep as hero-panel if preferred */
width: 100%;
height: 100vh;
min-height:800px;
position: relative; /* For positioning svg and controls */
background: linear-gradient(to bottom, #333333, #0a0a0a);
overflow: hidden; /* Crucial */
}
.background-waves {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1; /* Lines are on top of gradient, below controls */
}
.background-waves svg {
width: 100%;
height: 100%;
}
.controls-panel {
position: fixed;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(40, 40, 40, 0.85);
padding: 10px 20px;
border-radius: 8px;
z-index: 100; /* Controls on top of everything */
display: flex;
flex-wrap: wrap;
gap: 15px;
font-size: 12px;
color: #eee;
}
.controls-panel > div {
display: flex;
flex-direction: column;
align-items: center;
}
.controls-panel label {
margin-bottom: 3px;
white-space: nowrap;
}
.controls-panel input[type="range"] {
width: 100px; /* Slightly smaller width for more controls */
}
.controls-panel .value-display {
min-width: 30px;
text-align: center;
font-weight: bold;
margin-top: 2px;
}
</style>
JS
<script>
document.addEventListener('DOMContentLoaded', () => {
const svgNS = "http://www.w3.org/2000/svg";
const svgContainer = document.getElementById('wavy-lines-svg');
const panel = document.querySelector('.line-body-hero-panel'); // Main container
// --- Simulation Configuration ---
const SIM_CONFIG = {
numLines: 35,
numPointsPerLine: 70,
lineBaseStrokeWidth: 0.7, // Default, will be controlled by slider
mass: 1.2,
stiffnessReturn: 0.03,
stiffnessLink: 0.05,
damping: 0.92,
mouseInfluenceRadiusX: 120,
mouseVerticalProximityThreshold: 25,
maxSimultaneousInteractions: 3,
mousePullStrength: 0.15,
maxDisplacementForColor: 20,
originalLineColor: [5, 5, 5],
hotLineColor: [200, 0, 0],
};
const MASTER_WAVE = {
amplitude: 0.0,
frequency: 0.6,
phase: Math.PI / 2.5,
verticalShift: 0.0,
};
let lines = [];
let mousePos = { x: -1000, y: -1000, isActive: false };
let activelyInteractingLineIds = new Set();
// --- UI Control Elements ---
const pullStrengthSlider = document.getElementById('pullStrength');
const pullStrengthValueDisplay = document.getElementById('pullStrengthValue');
const snapBackSlider = document.getElementById('snapBack');
const snapBackValueDisplay = document.getElementById('snapBackValue');
const dampingSlider = document.getElementById('damping');
const dampingValueDisplay = document.getElementById('dampingValue');
const interactionRangeYSlider = document.getElementById('interactionRangeY');
const interactionRangeYValueDisplay = document.getElementById('interactionRangeYValue');
const lineWidthSlider = document.getElementById('lineWidth'); // New
const lineWidthValueDisplay = document.getElementById('lineWidthValue'); // New
function setupControls() {
pullStrengthSlider.value = SIM_CONFIG.mousePullStrength;
pullStrengthValueDisplay.textContent = SIM_CONFIG.mousePullStrength.toFixed(2);
pullStrengthSlider.addEventListener('input', (e) => {
SIM_CONFIG.mousePullStrength = parseFloat(e.target.value);
pullStrengthValueDisplay.textContent = SIM_CONFIG.mousePullStrength.toFixed(2);
});
snapBackSlider.value = SIM_CONFIG.stiffnessReturn;
snapBackValueDisplay.textContent = SIM_CONFIG.stiffnessReturn.toFixed(3);
snapBackSlider.addEventListener('input', (e) => {
SIM_CONFIG.stiffnessReturn = parseFloat(e.target.value);
snapBackValueDisplay.textContent = SIM_CONFIG.stiffnessReturn.toFixed(3);
});
dampingSlider.value = SIM_CONFIG.damping;
dampingValueDisplay.textContent = SIM_CONFIG.damping.toFixed(2);
dampingSlider.addEventListener('input', (e) => {
SIM_CONFIG.damping = parseFloat(e.target.value);
dampingValueDisplay.textContent = SIM_CONFIG.damping.toFixed(2);
});
interactionRangeYSlider.value = SIM_CONFIG.mouseVerticalProximityThreshold;
interactionRangeYValueDisplay.textContent = SIM_CONFIG.mouseVerticalProximityThreshold;
interactionRangeYSlider.addEventListener('input', (e) => {
SIM_CONFIG.mouseVerticalProximityThreshold = parseInt(e.target.value, 10);
interactionRangeYValueDisplay.textContent = SIM_CONFIG.mouseVerticalProximityThreshold;
});
// New control for Line Width
lineWidthSlider.value = SIM_CONFIG.lineBaseStrokeWidth;
lineWidthValueDisplay.textContent = SIM_CONFIG.lineBaseStrokeWidth.toFixed(1);
lineWidthSlider.addEventListener('input', (e) => {
SIM_CONFIG.lineBaseStrokeWidth = parseFloat(e.target.value);
lineWidthValueDisplay.textContent = SIM_CONFIG.lineBaseStrokeWidth.toFixed(1);
});
}
function initLines() {
lines = [];
const viewWidth = panel.clientWidth;
const viewHeight = panel.clientHeight;
MASTER_WAVE.amplitude = viewHeight * 0.08;
MASTER_WAVE.verticalShift = viewHeight * 0.5;
const xMultiplier = Math.PI * 2 / viewWidth;
for (let i = 0; i < SIM_CONFIG.numLines; i++) {
const line = {
id: `line-${i}`,
points: [],
baseY: viewHeight * (0.1 + (i / (SIM_CONFIG.numLines -1 )) * 0.8)
};
for (let j = 0; j < SIM_CONFIG.numPointsPerLine; j++) {
const x = (viewWidth / (SIM_CONFIG.numPointsPerLine - 1)) * j;
const masterWaveYOffset = MASTER_WAVE.amplitude *
Math.sin(x * xMultiplier * MASTER_WAVE.frequency + MASTER_WAVE.phase);
const originalY = line.baseY + masterWaveYOffset;
line.points.push({ x: x, y: originalY, originalY: originalY, vy: 0, ay: 0 });
}
lines.push(line);
}
}
function updatePhysics() {
const dt = 1;
const svgRect = svgContainer.getBoundingClientRect();
const relativeMouseX = mousePos.x - svgRect.left;
const relativeMouseY = mousePos.y - svgRect.top;
let candidateLines = [];
if (mousePos.isActive) {
for (const line of lines) {
let currentLineCenterY = line.points.reduce((sum, p) => sum + p.y, 0) / line.points.length || line.baseY;
const distYToLine = Math.abs(relativeMouseY - currentLineCenterY);
if (distYToLine < SIM_CONFIG.mouseVerticalProximityThreshold) {
candidateLines.push({ line, dist: distYToLine });
}
}
candidateLines.sort((a, b) => a.dist - b.dist);
activelyInteractingLineIds.clear();
candidateLines.slice(0, SIM_CONFIG.maxSimultaneousInteractions).forEach(c => activelyInteractingLineIds.add(c.line.id));
} else {
activelyInteractingLineIds.clear();
}
for (const line of lines) {
const isLineActiveForMouse = activelyInteractingLineIds.has(line.id);
for (let i = 0; i < line.points.length; i++) {
const p = line.points[i];
let forceY = -SIM_CONFIG.stiffnessReturn * (p.y - p.originalY);
if (i > 0) forceY += SIM_CONFIG.stiffnessLink * (line.points[i-1].y - p.y);
if (i < line.points.length - 1) forceY += SIM_CONFIG.stiffnessLink * (line.points[i+1].y - p.y);
if (mousePos.isActive && isLineActiveForMouse) {
const dx = p.x - relativeMouseX;
if (Math.abs(dx) < SIM_CONFIG.mouseInfluenceRadiusX) {
const influenceX = 1 - (Math.abs(dx) / SIM_CONFIG.mouseInfluenceRadiusX);
forceY += (relativeMouseY - p.y) * SIM_CONFIG.mousePullStrength * influenceX;
}
}
p.ay = forceY / SIM_CONFIG.mass;
p.vy = (p.vy + p.ay * dt) * SIM_CONFIG.damping;
p.y += p.vy * dt;
}
}
}
function lerpColor(colorA_rgb, colorB_rgb, t) {
t = Math.max(0, Math.min(1, t));
return `rgb(${Math.round(colorA_rgb[0] + (colorB_rgb[0] - colorA_rgb[0]) * t)},${Math.round(colorA_rgb[1] + (colorB_rgb[1] - colorA_rgb[1]) * t)},${Math.round(colorA_rgb[2] + (colorB_rgb[2] - colorA_rgb[2]) * t)})`;
}
function renderLines() {
svgContainer.innerHTML = '';
for (const line of lines) {
if (line.points.length < 2) continue;
for (let i = 0; i < line.points.length - 1; i++) {
const p1 = line.points[i], p2 = line.points[i+1];
const segmentPath = document.createElementNS(svgNS, "path");
segmentPath.setAttribute("d", `M ${p1.x.toFixed(1)} ${p1.y.toFixed(1)} L ${p2.x.toFixed(1)} ${p2.y.toFixed(1)}`);
const avgDisplacement = (Math.abs(p1.y - p1.originalY) + Math.abs(p2.y - p2.originalY)) / 2;
const colorIntensity = Math.min(1, avgDisplacement / SIM_CONFIG.maxDisplacementForColor);
segmentPath.setAttribute("stroke", lerpColor(SIM_CONFIG.originalLineColor, SIM_CONFIG.hotLineColor, colorIntensity));
// Use the dynamic SIM_CONFIG.lineBaseStrokeWidth here
segmentPath.setAttribute("stroke-width", SIM_CONFIG.lineBaseStrokeWidth.toString());
segmentPath.setAttribute("fill", "none");
segmentPath.setAttribute("stroke-linecap", "round");
svgContainer.appendChild(segmentPath);
}
}
}
function gameLoop() {
updatePhysics();
renderLines();
requestAnimationFrame(gameLoop);
}
panel.addEventListener('mousemove', (e) => { mousePos.x = e.clientX; mousePos.y = e.clientY; mousePos.isActive = true; });
panel.addEventListener('mouseleave', () => { mousePos.isActive = false; });
panel.addEventListener('mouseenter', (e) => { mousePos.x = e.clientX; mousePos.y = e.clientY; mousePos.isActive = true; });
let resizeTimer;
window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(initLines, 250); });
setupControls();
initLines();
gameLoop();
});
</script>