diff --git a/README.md b/README.md index e2677c8..26a94cd 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,8 @@ python -m http.server 8000 - `v0.5/` - Current version (plain HTML/JS, no build step required) - `v0.1/`, `v0.2/` - Previous versions with build systems + +## Features + +- Dynamic geometry controls for paths, cross-sections, and modifiers +- Shape animation controls (`Animate Geometry`, amplitude, speed) that pulse the tube radius live without reloading the scene diff --git a/v0.5/src/geometry/tube.js b/v0.5/src/geometry/tube.js index f667600..f7641f4 100644 --- a/v0.5/src/geometry/tube.js +++ b/v0.5/src/geometry/tube.js @@ -86,9 +86,21 @@ function computePosition(u, v, rTube, params, isInner = false) { radius = modifiers['taper'].apply(u, radius, params); } + // Apply shape animation offset (breathing/pulsing effect) + const animationOffset = params.shapeAnimationOffset || 0; + if (animationOffset !== 0) { + radius *= (1 + animationOffset); + } + + // Ensure radius stays within valid bounds + const wallThickness = params.wallThickness || 0; + const minRadius = params.rMin || 0.015; + const minOuterRadius = wallThickness > 0 ? minRadius + wallThickness : minRadius; + radius = Math.max(radius, minOuterRadius); + // Apply wall thickness for inner surface if (isInner) { - radius -= params.wallThickness; + radius = Math.max(radius - wallThickness, minRadius); } // Apply twist modifier diff --git a/v0.5/src/params.js b/v0.5/src/params.js index 667bc22..35d3134 100644 --- a/v0.5/src/params.js +++ b/v0.5/src/params.js @@ -72,6 +72,12 @@ const defaultParams = { waveFrequency: 1.0, // Wave frequency wavePhase: 0.0, // Wave phase + // Shape animation + animateShape: false, // Enable geometry animation + shapeAnimationSpeed: 0.5, // Cycles per second + shapeAnimationAmplitude: 0.15, // Scale factor applied to tube radius + shapeAnimationOffset: 0.0, // Internal state, updated at runtime + // Legacy parameters (for backward compatibility) mode: 'Exponential', // 'Exponential' or 'Linear' twist: 0.0, // Legacy twist (maps to twistAmount) diff --git a/v0.5/src/rendering/renderer.js b/v0.5/src/rendering/renderer.js index 6218135..af6d536 100644 --- a/v0.5/src/rendering/renderer.js +++ b/v0.5/src/rendering/renderer.js @@ -23,6 +23,11 @@ function setupAnimationLoop(renderer, scene, sceneState, params, updateMesh) { let needsOutlineUpdate = false; let lastUpdateTime = 0; const updateThrottle = 200; // Update outline at most every 200ms + const animationFrameInterval = 1000 / 30; // Target ~30 mesh rebuilds per second during animation + let lastGeometryUpdate = 0; + let previousTimestamp = performance.now(); + let shapeAnimationAngle = 0; + let animationWasActive = false; // Update outline when camera changes significantly function checkCameraChange() { @@ -41,12 +46,47 @@ function setupAnimationLoop(renderer, scene, sceneState, params, updateMesh) { function animate() { requestAnimationFrame(animate); sceneState.orbitControls.update(); + const now = performance.now(); + const deltaSeconds = (now - previousTimestamp) / 1000; + previousTimestamp = now; + + let requiresMeshUpdate = false; + const amplitude = params.shapeAnimationAmplitude || 0; + const animationEnabled = params.animateShape && amplitude > 0; + + if (animationEnabled) { + if (!animationWasActive) { + lastGeometryUpdate = 0; // Force immediate rebuild on activation + } + const speed = Math.max(params.shapeAnimationSpeed || 0, 0); + if (speed > 0) { + shapeAnimationAngle = (shapeAnimationAngle + deltaSeconds * speed * Math.PI * 2) % (Math.PI * 2); + } + const nextOffset = Math.sin(shapeAnimationAngle) * amplitude; + params.shapeAnimationOffset = nextOffset; + + if ((now - lastGeometryUpdate) >= animationFrameInterval) { + requiresMeshUpdate = true; + } + } else { + const currentOffset = params.shapeAnimationOffset || 0; + if (Math.abs(currentOffset) > 1e-4) { + params.shapeAnimationOffset = 0; + requiresMeshUpdate = true; + } + } + animationWasActive = animationEnabled; // Update outline if camera changed and outline is enabled (throttled) checkCameraChange(); if (needsOutlineUpdate) { + requiresMeshUpdate = true; + } + + if (requiresMeshUpdate) { updateMesh(sceneState.meshGroup, params, sceneState.currentCamera); needsOutlineUpdate = false; + lastGeometryUpdate = now; } renderer.render(scene, sceneState.currentCamera); diff --git a/v0.5/src/ui/dynamicControls.js b/v0.5/src/ui/dynamicControls.js index 96c6c07..57093b6 100644 --- a/v0.5/src/ui/dynamicControls.js +++ b/v0.5/src/ui/dynamicControls.js @@ -3,16 +3,16 @@ function createControlGroup(label, controlElement) { const group = document.createElement('div'); group.className = 'control-group'; - + const labelEl = document.createElement('label'); labelEl.textContent = label; if (controlElement.id) { labelEl.setAttribute('for', controlElement.id); } - + group.appendChild(labelEl); group.appendChild(controlElement); - + return group; } @@ -238,7 +238,7 @@ function generateGeometryControls(params, onUpdate) { toggleLabel.setAttribute('for', `modifier_${key}`); toggleLabel.textContent = modifier.name; toggleContainer.appendChild(toggleLabel); - + const toggle = document.createElement('input'); toggle.type = 'checkbox'; toggle.id = `modifier_${key}`; @@ -303,6 +303,35 @@ function generateGeometryControls(params, onUpdate) { container.appendChild(controlsDiv); }); + // Shape animation controls + const animationHeader = document.createElement('h4'); + animationHeader.textContent = 'Shape Animation'; + animationHeader.style.marginTop = '20px'; + animationHeader.style.marginBottom = '10px'; + container.appendChild(animationHeader); + + addControl(container, 'animateShape', { + type: 'checkbox', + label: 'Animate Geometry', + default: false + }, params, onUpdate); + + addControl(container, 'shapeAnimationAmplitude', { + type: 'range', + min: 0, + max: 0.5, + step: 0.01, + label: 'Animation Amplitude' + }, params, onUpdate); + + addControl(container, 'shapeAnimationSpeed', { + type: 'range', + min: 0.1, + max: 3, + step: 0.05, + label: 'Animation Speed (cycles/s)' + }, params, onUpdate); + // Mesh density controls const densityHeader = document.createElement('h4'); densityHeader.textContent = 'Mesh Density';