BACK TO EXPERIMENTS

Strumming Lines

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>