import { Application } from 'pixi.js'; import { WaterfallGraph } from './waterfall.js'; import { PerformanceMetrics } from './metrics.js'; import { StateManager } from './state.js'; import { example8_InteractiveDemo} from './example-usage.js'; // ============================================================================ // GLOBAL STATE // ============================================================================ // Centralized reactive state const state = new StateManager(); // DOM references let dom = { container: null, controls: {}, display: {}, }; // Application instances let app = null; // PixiJS Application let metrics = null; let graphs = []; // Keep PixiJS graph instances outside reactive state // Vertical zoom dragging state let isDraggingVerticalZoom = false; let dragStartY = 0; let dragStartZoom = 1.0; // ============================================================================ // APPLICATION ENTRY POINT // ============================================================================ document.addEventListener('DOMContentLoaded', async function() { log('TimePlot starting...'); log('init DOM'); await initDOM(); log('init PixiJS renderer'); await initRenderer(); log('init graphs'); await initGraphs(); log('init services'); await initServices(); log('setup controls'); await setupControls(); log('setup state listeners'); await setupStateListeners(); log('TimePlot ready'); }); // ============================================================================ // INITIALIZATION FUNCTIONS // ============================================================================ async function initDOM() { dom.container = document.getElementById('canvas-container'); dom.controls.gridBtn = document.getElementById('toggle-grid'); dom.controls.metricsBtn = document.getElementById('toggle-metrics'); dom.controls.exportBtn = document.getElementById('export-metrics'); dom.display.rendererType = document.getElementById('renderer-type'); dom.display.metrics = document.getElementById('metrics-display'); dom.display.timeScale = document.getElementById('time-scale'); } async function initRenderer() { // Check WebGPU availability let preference = 'webgpu'; if (!navigator.gpu) { log('WebGPU not available, using WebGL'); preference = 'webgl'; } try { app = new Application(); await app.init({ preference: preference, width: window.innerWidth, height: window.innerHeight - 60, // Account for controls backgroundColor: 0x1a1a26, antialias: true, autoDensity: true, resolution: window.devicePixelRatio || 1, }); dom.container.appendChild(app.canvas); // Store renderer info in state const rendererType = app.renderer.type; state.state.rendering.rendererType = rendererType; dom.display.rendererType.textContent = rendererType; log(`Using renderer: ${rendererType}`); // Store canvas dimensions in state state.state.uiConfig.canvasWidth = app.screen.width; state.state.uiConfig.canvasHeight = app.screen.height; // Handle window resize window.addEventListener('resize', handleResize); } catch (error) { log(`Failed to initialize renderer: ${error}`); throw error; } } async function initGraphs() { const width = app.screen.width; const height = app.screen.height; // Left graph const graph1 = new WaterfallGraph({ x: 0, y: 0, width: width / 2, height: height, title: 'Frequency vs Time', color: 0xff6666, }); // Right graph const graph2 = new WaterfallGraph({ x: width / 2, y: 0, width: width / 2, height: height, title: 'Position vs Time', color: 0x66ff66, }); // Store graphs locally (PixiJS objects shouldn't be proxied) graphs = [graph1, graph2]; // Add to stage graphs.forEach(graph => { app.stage.addChild(graph.container); graph.setGridVisible(state.state.userPrefs.showGrid); }); } async function initServices() { // Initialize performance metrics metrics = new PerformanceMetrics( state.state.userPrefs.rollingWindow, state.state.userPrefs.historyCapacity ); // Start animation loop app.ticker.add(update); log('Services initialized'); } function setupControls() { // Register input actions state.registerAction('toggleGrid', toggleGrid); state.registerAction('toggleMetrics', toggleMetrics); state.registerAction('exportMetrics', exportMetrics); // Map keyboard keys to actions state.mapKey('KeyG', 'toggleGrid'); state.mapKey('KeyM', 'toggleMetrics'); state.mapKey('KeyE', 'exportMetrics'); // Button controls dom.controls.gridBtn.addEventListener('click', () => state.executeAction('toggleGrid')); dom.controls.metricsBtn.addEventListener('click', () => state.executeAction('toggleMetrics')); dom.controls.exportBtn.addEventListener('click', () => state.executeAction('exportMetrics')); // Keyboard controls via state's input action system window.addEventListener('keydown', (e) => state.handleKeyboardEvent(e)); // Mouse controls for time scaling dom.container.addEventListener('mousedown', handleMouseDown); window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); dom.container.addEventListener('contextmenu', (e) => e.preventDefault()); // Prevent context menu // Update button states updateControlButtons(); } function setupStateListeners() { // React to showGrid changes state.on('userPrefs.showGrid', ({ value }) => { graphs.forEach(graph => graph.setGridVisible(value)); updateControlButtons(); log(`Grid: ${value ? 'ON' : 'OFF'}`); }); // React to showMetrics changes state.on('userPrefs.showMetrics', ({ value }) => { updateControlButtons(); log(`Metrics: ${value ? 'ON' : 'OFF'}`); }); // React to vertical scale changes state.on('time.verticalScale', ({ value }) => { graphs.forEach(graph => graph.setVerticalScale(value)); updateVerticalZoomDisplay(); }); // React to pause state changes state.on('time.isPaused', ({ value }) => { log(`Time ${value ? 'PAUSED' : 'RESUMED'}`); }); // React to speed changes state.on('time.speed', ({ value }) => { log(`Time speed: ${value.toFixed(1)}x`); }); } // ============================================================================ // EVENT HANDLERS // ============================================================================ function handleResize() { const width = window.innerWidth; const height = window.innerHeight - 60; app.renderer.resize(width, height); // Update state state.state.uiConfig.canvasWidth = width; state.state.uiConfig.canvasHeight = height; // Update graphs if (graphs[0]) graphs[0].resize(0, 0, width / 2, height); if (graphs[1]) graphs[1].resize(width / 2, 0, width / 2, height); } function handleMouseDown(e) { // Middle mouse button (button = 1) if (e.button === 1) { e.preventDefault(); isDraggingVerticalZoom = true; dragStartY = e.clientY; dragStartZoom = state.state.time.verticalScale; dom.container.style.cursor = 'ns-resize'; } } function handleMouseMove(e) { if (!isDraggingVerticalZoom) return; const deltaY = dragStartY - e.clientY; // Inverted: drag up = zoom in const sensitivity = 0.005; // Adjust sensitivity const newZoom = dragStartZoom + (deltaY * sensitivity); // Update state (which will trigger graph updates via state listener) state.state.time.verticalScale = Math.max(0.2, Math.min(3.0, newZoom)); } function handleMouseUp(e) { if (e.button === 1) { isDraggingVerticalZoom = false; dom.container.style.cursor = 'default'; } } // ============================================================================ // MAIN UPDATE LOOP // ============================================================================ function update() { metrics.beginFrame(); metrics.beginUpdate(); // Update time using state manager state.incrementTime(0.016); // ~60fps increment state.updateRealElapsed(); state.state.rendering.frameCounter++; // Update each graph graphs.forEach((graph, idx) => { graph.update(state.state.time.current, idx); }); const updateMs = metrics.endUpdate(); metrics.beginRender(); // Rendering happens automatically via PixiJS const renderMs = metrics.endRender(); // Calculate stats const vertexCount = graphs.reduce((sum, g) => sum + g.getVertexCount(), 0); const lineCount = graphs.reduce((sum, g) => sum + g.getLineCount(), 0); metrics.endFrame(updateMs, renderMs, vertexCount, lineCount); // Update health metrics in state (silently to avoid spamming events) const currentHealth = state.state.health; currentHealth.updateMs = updateMs; currentHealth.renderMs = renderMs; currentHealth.vertexCount = vertexCount; currentHealth.lineCount = lineCount; currentHealth.fps = metrics.getFPS(); // Update UI less frequently to prevent flickering const frameCounter = state.state.rendering.frameCounter; const interval = state.state.userPrefs.metricsUpdateInterval; if (state.state.userPrefs.showMetrics && frameCounter % interval === 0) { updateMetricsDisplay(); } } // ============================================================================ // UI CONTROL FUNCTIONS // ============================================================================ function toggleGrid() { state.togglePref('showGrid'); } function toggleMetrics() { state.togglePref('showMetrics'); } function exportMetrics() { const csv = metrics.exportToCSV(); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `timeplot-metrics-${Date.now()}.csv`; a.click(); URL.revokeObjectURL(url); log('Metrics exported'); } function updateMetricsDisplay() { if (dom.display.metrics) { dom.display.metrics.textContent = metrics.formatSummary(); } } function updateControlButtons() { dom.controls.gridBtn.classList.toggle('active', state.state.userPrefs.showGrid); dom.controls.metricsBtn.classList.toggle('active', state.state.userPrefs.showMetrics); } function updateVerticalZoomDisplay() { if (dom.display.timeScale) { const zoom = state.state.time.verticalScale; dom.display.timeScale.textContent = `${zoom.toFixed(2)}x`; // Color code: zoomed out = blue, normal = white, zoomed in = orange if (zoom < 0.8) { dom.display.timeScale.style.color = '#6af'; // Zoomed out (see more history) } else if (zoom > 1.2) { dom.display.timeScale.style.color = '#fa6'; // Zoomed in (see less history) } else { dom.display.timeScale.style.color = '#fff'; } } } // ============================================================================ // UTILITIES // ============================================================================ function log(msg) { console.log(`[TimePlot] ${msg}`); }