summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--web-timeplot/src/main.js179
-rw-r--r--web-timeplot/src/state.js2
2 files changed, 101 insertions, 80 deletions
diff --git a/web-timeplot/src/main.js b/web-timeplot/src/main.js
index b0ba032..31be4bb 100644
--- a/web-timeplot/src/main.js
+++ b/web-timeplot/src/main.js
@@ -1,31 +1,26 @@
import { Application } from 'pixi.js';
import { WaterfallGraph } from './waterfall.js';
import { PerformanceMetrics } from './metrics.js';
+import { StateManager } from './state.js';
// ============================================================================
// GLOBAL STATE
// ============================================================================
-let env = {};
-let cfg = {
- showGrid: true,
- showMetrics: true,
- rollingWindow: 60,
- historyCapacity: 10000,
-};
+// Centralized reactive state
+const state = new StateManager();
+// DOM references
let dom = {
container: null,
controls: {},
display: {},
};
+// Application instances
let app = null; // PixiJS Application
-let graphs = [];
let metrics = null;
-let time = 0;
-let frameCounter = 0;
-let metricsUpdateInterval = 10; // Update metrics display every N frames
+let graphs = []; // Keep PixiJS graph instances outside reactive state
// Vertical zoom dragging state
let isDraggingVerticalZoom = false;
@@ -42,9 +37,6 @@ document.addEventListener('DOMContentLoaded', async function() {
log('init DOM');
await initDOM();
- log('init config');
- await initCfg();
-
log('init PixiJS renderer');
await initRenderer();
@@ -57,6 +49,9 @@ document.addEventListener('DOMContentLoaded', async function() {
log('setup controls');
await setupControls();
+ log('setup state listeners');
+ await setupStateListeners();
+
log('TimePlot ready');
});
@@ -74,23 +69,6 @@ async function initDOM() {
dom.display.timeScale = document.getElementById('time-scale');
}
-async function initCfg() {
- const localCfg = localStorage.getItem('timeplot-cfg');
- if (localCfg) {
- try {
- const parsed = JSON.parse(localCfg);
- cfg = { ...cfg, ...parsed };
- log('Loaded config from localStorage');
- } catch (e) {
- log('Failed to parse config, using defaults');
- }
- }
-}
-
-function saveCfg() {
- localStorage.setItem('timeplot-cfg', JSON.stringify(cfg));
-}
-
async function initRenderer() {
// Check WebGPU availability
let preference = 'webgpu';
@@ -114,11 +92,16 @@ async function initRenderer() {
dom.container.appendChild(app.canvas);
- // Display actual renderer type
+ // 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);
@@ -152,16 +135,22 @@ async function initGraphs() {
color: 0x66ff66,
});
- graphs.push(graph1, graph2);
+ // 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(cfg.showGrid);
+ graph.setGridVisible(state.state.userPrefs.showGrid);
});
}
async function initServices() {
// Initialize performance metrics
- metrics = new PerformanceMetrics(cfg.rollingWindow, cfg.historyCapacity);
+ metrics = new PerformanceMetrics(
+ state.state.userPrefs.rollingWindow,
+ state.state.userPrefs.historyCapacity
+ );
// Start animation loop
app.ticker.add(update);
@@ -170,13 +159,23 @@ async function initServices() {
}
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', toggleGrid);
- dom.controls.metricsBtn.addEventListener('click', toggleMetrics);
- dom.controls.exportBtn.addEventListener('click', exportMetrics);
+ 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
- window.addEventListener('keydown', handleKeyboard);
+ // 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);
@@ -188,30 +187,51 @@ function setupControls() {
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 handleKeyboard(e) {
- switch(e.key.toLowerCase()) {
- case 'g':
- toggleGrid();
- break;
- case 'm':
- toggleMetrics();
- break;
- case 'e':
- exportMetrics();
- break;
- }
-}
-
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);
@@ -223,7 +243,7 @@ function handleMouseDown(e) {
e.preventDefault();
isDraggingVerticalZoom = true;
dragStartY = e.clientY;
- dragStartZoom = graphs[0]?.getVerticalScale() || 1.0;
+ dragStartZoom = state.state.time.verticalScale;
dom.container.style.cursor = 'ns-resize';
}
}
@@ -235,13 +255,8 @@ function handleMouseMove(e) {
const sensitivity = 0.005; // Adjust sensitivity
const newZoom = dragStartZoom + (deltaY * sensitivity);
- // Apply to all graphs
- graphs.forEach(graph => {
- graph.setVerticalScale(newZoom);
- });
-
- // Update display
- updateVerticalZoomDisplay();
+ // 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) {
@@ -259,12 +274,15 @@ function update() {
metrics.beginFrame();
metrics.beginUpdate();
- time += 0.016; // ~60fps increment
- frameCounter++;
+ // 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(time, idx);
+ graph.update(state.state.time.current, idx);
});
const updateMs = metrics.endUpdate();
@@ -279,8 +297,18 @@ function update() {
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
- if (cfg.showMetrics && frameCounter % metricsUpdateInterval === 0) {
+ const frameCounter = state.state.rendering.frameCounter;
+ const interval = state.state.userPrefs.metricsUpdateInterval;
+ if (state.state.userPrefs.showMetrics && frameCounter % interval === 0) {
updateMetricsDisplay();
}
}
@@ -290,18 +318,11 @@ function update() {
// ============================================================================
function toggleGrid() {
- cfg.showGrid = !cfg.showGrid;
- graphs.forEach(graph => graph.setGridVisible(cfg.showGrid));
- updateControlButtons();
- saveCfg();
- log(`Grid: ${cfg.showGrid ? 'ON' : 'OFF'}`);
+ state.togglePref('showGrid');
}
function toggleMetrics() {
- cfg.showMetrics = !cfg.showMetrics;
- updateControlButtons();
- saveCfg();
- log(`Metrics: ${cfg.showMetrics ? 'ON' : 'OFF'}`);
+ state.togglePref('showMetrics');
}
function exportMetrics() {
@@ -323,13 +344,13 @@ function updateMetricsDisplay() {
}
function updateControlButtons() {
- dom.controls.gridBtn.classList.toggle('active', cfg.showGrid);
- dom.controls.metricsBtn.classList.toggle('active', cfg.showMetrics);
+ 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 && graphs[0]) {
- const zoom = graphs[0].getVerticalScale();
+ 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
diff --git a/web-timeplot/src/state.js b/web-timeplot/src/state.js
index a38dc98..53d8279 100644
--- a/web-timeplot/src/state.js
+++ b/web-timeplot/src/state.js
@@ -98,8 +98,8 @@ export class StateManager extends EventEmitter {
rendering: {
rendererType: 'unknown', // 'webgpu' | 'webgl' | 'canvas'
- graphs: [], // Array of graph instances
frameCounter: 0,
+ // Note: graph instances are NOT stored here to avoid proxy wrapping
},
health: {