/** * Audio Wave Analyzer UI Module (Refactored) * Main coordinator that orchestrates all UI functionality through modular components */ import { AudioAnalyzerControls } from './audio_analyzer_controls.js'; import { AudioAnalyzerWidgets } from './audio_analyzer_widgets.js'; import { AudioAnalyzerLayout } from './audio_analyzer_layout.js'; export class AudioAnalyzerUI { constructor(core) { this.core = core; this.container = null; this.canvas = null; this.playButton = null; this.stopButton = null; this.timeDisplay = null; this.selectionDisplay = null; this.statusDisplay = null; this.messageDisplay = null; this.controlsContainer = null; this.widget = null; // Initialize modular components this.controlsModule = new AudioAnalyzerControls(core); this.widgets = new AudioAnalyzerWidgets(core); this.layout = new AudioAnalyzerLayout(core); // Make components available to core for cross-component communication this.core.controls = this.controlsModule; this.core.widgets = this.widgets; this.core.layout = this.layout; } createInterface() { // Create Audio Wave Analyzer UI interface using modular components // Remove existing interface const existingInterface = this.core.node.widgets?.find(w => w.name === 'audio_analyzer_interface'); if (existingInterface) { const existingContainer = existingInterface.element; if (existingContainer && existingContainer.parentNode) { existingContainer.parentNode.removeChild(existingContainer); } const widgetIndex = this.core.node.widgets.indexOf(existingInterface); if (widgetIndex >= 0) { this.core.node.widgets.splice(widgetIndex, 1); } } const existingSpacerIndex = this.core.node.widgets?.findIndex(w => w.name === 'audio_analyzer_spacer') ?? -1; if (existingSpacerIndex >= 0) { this.core.node.widgets.splice(existingSpacerIndex, 1); } const existingButtonIndex = this.core.node.widgets?.findIndex(w => w.name === 'audio_analyzer_button_row') ?? -1; if (existingButtonIndex >= 0) { const existingButton = this.core.node.widgets[existingButtonIndex]; if (existingButton.element && existingButton.element.parentNode) { existingButton.element.parentNode.removeChild(existingButton.element); } this.core.node.widgets.splice(existingButtonIndex, 1); } // Create main container using layout module this.container = this.layout.createMainContainer(); this.core.container = this.container; // Create canvas using layout module this.canvas = this.layout.createCanvas(); // Get canvas context this.core.canvas = this.canvas; this.core.ctx = this.canvas.getContext('2d'); // Create controls container using layout module this.controlsContainer = this.layout.createControlsContainer(); // Create UI components using controls module const playbackControls = this.controlsModule.createPlaybackControls(); const speedSlider = this.controlsModule.createSpeedSlider(); const mainControls = this.controlsModule.createMainControls(); const zoomControls = this.controlsModule.createZoomControls(); const statusDisplays = this.controlsModule.createStatusDisplays(); // Add zoom controls to the first row playbackControls.appendChild(zoomControls); // Assemble controls this.controlsContainer.appendChild(playbackControls); this.controlsContainer.appendChild(mainControls); this.controlsContainer.appendChild(statusDisplays); // Assemble interface this.container.appendChild(this.canvas); this.container.appendChild(this.controlsContainer); // Create analyze button for its own node-reserved DOM row. this.addFloatingAnalyzeButton(); // Add floating speed slider to canvas this.addFloatingSpeedSlider(); // Add container to node using layout module const success = this.layout.addContainerToNode(this.container); if (success) { // Setup initial canvas size using controls module this.controlsModule.setupCanvasSize(); // Setup canvas resize observer using controls module this.controlsModule.setupCanvasResize(); // Setup drag and drop using controls module this.controlsModule.setupDragAndDrop(); // console.log('🌊 Audio Wave Analyzer: Interface setup complete - spacer reserves space, interface positioned over it'); // Debug: setup complete } } addFloatingAnalyzeButton() { // Create floating analyze button const floatingAnalyzeButton = document.createElement('button'); floatingAnalyzeButton.textContent = '🔍 Analyze'; floatingAnalyzeButton.onclick = () => this.core.onParametersChanged(); // Place the button in the node-colored reserved gap above the analyzer panel. floatingAnalyzeButton.style.cssText = ` position: relative; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; color: white; background: #28a745; box-shadow: 0 2px 8px rgba(40, 167, 69, 0.4); `; this.analyzeButton = floatingAnalyzeButton; this.core.analyzeButton = floatingAnalyzeButton; // console.log('🌊 Audio Wave Analyzer: Added floating analyze button at canvas center'); // Debug: button placement } addFloatingSpeedSlider() { // Create floating speed slider container backdrop-filter: blur(4px); const floatingSliderContainer = document.createElement('div'); floatingSliderContainer.style.cssText = ` position: absolute; bottom: 107px; left: 0; right: 0; z-index: 99; display: flex; align-items: center; justify-content: center; gap: 12px; `; const speedLabel = document.createElement('span'); speedLabel.textContent = 'Speed:'; speedLabel.style.cssText = ` color: #fff; font-size: 11px; font-weight: bold; `; this.core.ui.speedSlider = document.createElement('input'); this.core.ui.speedSlider.type = 'range'; this.core.ui.speedSlider.min = '0.00'; this.core.ui.speedSlider.max = '2.00'; this.core.ui.speedSlider.step = '0.025'; this.core.ui.speedSlider.value = '1'; this.core.ui.speedSlider.style.cssText = ` flex: 1; height: 20px; background: transparent; outline: none; cursor: pointer; -webkit-appearance: none; appearance: none; border: none; position: relative; z-index: 2; `; // Simple CSS for just the thumb const thumbStyle = document.createElement('style'); thumbStyle.textContent = ` input[type="range"].speed-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 2px; height: 20px; background: white; border: none; border-radius: 0; cursor: pointer; } input[type="range"].speed-slider::-moz-range-thumb { -moz-appearance: none; width: 2px; height: 20px; background: white; border: none; border-radius: 0; cursor: pointer; } `; document.head.appendChild(thumbStyle); this.core.ui.speedSlider.className = 'speed-slider'; // Create custom track line behind the slider const customTrack = document.createElement('div'); customTrack.style.cssText = ` position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: #666; transform: translateY(-50%); pointer-events: none; z-index: 1; `; this.core.ui.speedValue = document.createElement('span'); this.core.ui.speedValue.textContent = '1.00x'; this.core.ui.speedValue.style.cssText = ` color: #fff; font-size: 11px; font-weight: bold; min-width: 40px; text-align: center; `; // Track extended dragging beyond slider limits let extendedSpeed = 1.0; let isDragging = false; let lastMouseX = 0; this.core.ui.speedSlider.addEventListener('mousedown', (e) => { isDragging = true; lastMouseX = e.clientX; extendedSpeed = parseFloat(this.core.ui.speedSlider.value); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - lastMouseX; lastMouseX = e.clientX; // Calculate base sensitivity based on slider width const sliderRect = this.core.ui.speedSlider.getBoundingClientRect(); const baseSensitivity = 2.0 / sliderRect.width; // 2.0 is the normal range (0.0 to 2.0) // Calculate how far we are from normal bounds for acceleration let acceleration = 1.0; if (extendedSpeed < 0.0) { // Left side acceleration: the more negative, the faster it goes const outOfBounds = Math.abs(extendedSpeed); acceleration = 1.0 + (outOfBounds * 3.0); // Much stronger linear acceleration } else if (extendedSpeed > 2.0) { // Right side acceleration: the higher above 2.0, the faster it goes const outOfBounds = extendedSpeed - 2.0; acceleration = 1.0 + (outOfBounds * 2.5); // Much stronger linear acceleration } // Apply accelerated sensitivity const sensitivity = baseSensitivity * acceleration; // Update extended speed based on mouse movement extendedSpeed += deltaX * sensitivity; // Clamp to absolute limits extendedSpeed = Math.max(-8.0, Math.min(8.0, extendedSpeed)); // Update slider position (clamped to visual range) const clampedValue = Math.max(0.0, Math.min(2.0, extendedSpeed)); this.core.ui.speedSlider.value = clampedValue; // Update display and speed with extended value this.core.ui.speedValue.textContent = `${extendedSpeed.toFixed(2)}x`; this.core.setPlaybackSpeed(extendedSpeed); }); document.addEventListener('mouseup', () => { isDragging = false; // Force sync when drag ends to ensure consistency extendedSpeed = parseFloat(this.core.ui.speedSlider.value); this.core.ui.speedValue.textContent = `${extendedSpeed.toFixed(2)}x`; this.core.setPlaybackSpeed(extendedSpeed); }); this.core.ui.speedSlider.oninput = () => { const currentSliderValue = parseFloat(this.core.ui.speedSlider.value); if (!isDragging) { // Normal slider input (not during extended drag) extendedSpeed = currentSliderValue; } else { // During extended drag, ensure we don't lose sync // If slider is at edge but we're dragging, keep extended value if ((currentSliderValue === 2.0 && extendedSpeed > 2.0) || (currentSliderValue === 0.0 && extendedSpeed < 0.0)) { // Keep the extended speed, don't reset to slider value } else { // We're within normal range, sync with slider extendedSpeed = currentSliderValue; } } this.core.ui.speedValue.textContent = `${extendedSpeed.toFixed(2)}x`; this.core.setPlaybackSpeed(extendedSpeed); }; // Create slider wrapper for proper positioning of track and slider const sliderWrapper = document.createElement('div'); sliderWrapper.style.cssText = ` position: relative; flex: 1; height: 20px; display: flex; align-items: center; `; sliderWrapper.appendChild(customTrack); sliderWrapper.appendChild(this.core.ui.speedSlider); floatingSliderContainer.appendChild(speedLabel); floatingSliderContainer.appendChild(sliderWrapper); floatingSliderContainer.appendChild(this.core.ui.speedValue); // Add to canvas container this.container.appendChild(floatingSliderContainer); // console.log('🌊 Audio Wave Analyzer: Added floating speed slider spanning canvas width'); // Debug: slider placement } // Delegate methods to appropriate modules // Time and selection display methods (delegated to controls) updateTimeDisplay() { this.controlsModule.updateTimeDisplay(); } updateSelectionDisplay() { this.controlsModule.updateSelectionDisplay(); } showMessage(message, level = 'info') { this.controlsModule.showMessage(message, level); } updateStatus(status) { this.controlsModule.updateStatus(status); } // Canvas methods (delegated to controls) setupCanvasSize() { this.controlsModule.setupCanvasSize(); } setupCanvasResize() { this.controlsModule.setupCanvasResize(); } // Layout methods (delegated to layout) resizeNodeForInterface() { this.layout.resizeNodeForInterface(); } updateNodeLayout() { this.layout.updateNodeLayout(); } setupNodeResizeHandling() { this.layout.setupNodeResizeHandling(); } // Widget methods (delegated to widgets) setupWidgetHeight(widget) { this.widgets.setupWidgetHeight(widget); } insertSpacerWidget() { return this.widgets.insertSpacerWidget(); } positionInterfaceOverSpacer() { this.widgets.positionInterfaceOverSpacer(); } findInsertPosition() { return this.widgets.findInsertPosition(); } setupMultilineWidgetWatchers() { this.widgets.setupMultilineWidgetWatchers(); } recalculateNodeHeight() { this.widgets.recalculateNodeHeight(); } ensureUIVisible() { this.widgets.ensureUIVisible(); } // Cleanup destroy() { this.layout.destroy(); console.log('🌊 Audio Wave Analyzer UI destroyed and cleaned up'); } }