diff options
| author | grothedev <grothedev@gmail.com> | 2025-11-26 00:19:54 -0500 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2025-11-26 00:19:54 -0500 |
| commit | a1c95e72bea26f554eb05916d6fc584927367159 (patch) | |
| tree | 4b6a5e25b9f1d15c64bf91ace252700c20b578d0 /web-timeplot | |
| parent | 43420f2987b76aa7ede0012e1998ba8d61419bc9 (diff) | |
making this into a prototyping framework
Diffstat (limited to 'web-timeplot')
| -rw-r--r-- | web-timeplot/PROTOTYPING.md | 146 | ||||
| -rw-r--r-- | web-timeplot/index.html | 81 | ||||
| -rw-r--r-- | web-timeplot/src/main.js | 275 |
3 files changed, 175 insertions, 327 deletions
diff --git a/web-timeplot/PROTOTYPING.md b/web-timeplot/PROTOTYPING.md new file mode 100644 index 0000000..e220f9a --- /dev/null +++ b/web-timeplot/PROTOTYPING.md @@ -0,0 +1,146 @@ +# PixiJS Prototyping Framework + +A minimal PixiJS framework with core architecture patterns (DOM initialization, Service initialization, State management) for rapid prototyping. + +## Quick Start + +```bash +npm run dev +``` + +Open browser to `http://localhost:5173/` + +## Architecture + +The framework follows a clean initialization pattern: + +1. **DOM Initialization** - Reference DOM elements +2. **Renderer Initialization** - Set up PixiJS with WebGPU/WebGL +3. **Services Initialization** - Start the update loop + +## Global Objects (Available in Console) + +- `window.PIXI` - Complete PixiJS namespace +- `window.pixiApp` - PixiJS Application instance +- `window.state` - StateManager instance (reactive state) +- `window.log` - Logger function + +## Rapid Prototyping Examples + +### Example 1: Draw a Rectangle + +Open browser console: + +```javascript +const graphics = new PIXI.Graphics(); +graphics.rect(100, 100, 200, 150); +graphics.fill(0xff0000); +pixiApp.stage.addChild(graphics); +``` + +### Example 2: Animated Sprite + +```javascript +const graphics = new PIXI.Graphics(); +graphics.circle(0, 0, 50); +graphics.fill(0x00ff00); +pixiApp.stage.addChild(graphics); + +// Add to update loop in main.js: +// graphics.x = Math.sin(state.state.time.current) * 200 + pixiApp.screen.width / 2; +// graphics.y = pixiApp.screen.height / 2; +``` + +### Example 3: Using State System + +The framework includes a reactive state manager: + +```javascript +// Listen to state changes +state.on('time.current', ({ value }) => { + console.log('Time:', value); +}); + +// Modify state (triggers listeners) +state.state.time.speed = 2.0; // Double speed + +// Toggle pause +state.togglePause(); +``` + +### Example 4: Register Input Actions + +```javascript +// In main.js, add to setupControls(): +state.registerAction('myAction', () => { + log('Action triggered!'); +}); + +state.mapKey('KeyP', 'myAction'); +``` + +## Modifying the Update Loop + +Edit `/src/main.js` function `update()`: + +```javascript +function update() { + state.incrementTime(0.016); // ~60fps increment + state.updateRealElapsed(); + state.state.rendering.frameCounter++; + + // YOUR PROTOTYPE CODE GOES HERE + // Example: + mySprite.rotation += 0.01; + myGraphics.x = Math.sin(state.state.time.current) * 100; +} +``` + +## State Structure + +```javascript +state.state = { + userPrefs: { + showGrid: true, + showMetrics: true, + theme: 'dark', + // ... persisted to localStorage + }, + + uiConfig: { + canvasWidth: number, + canvasHeight: number, + // ... + }, + + time: { + current: number, // Increments every frame + realElapsed: number, // Real seconds since start + speed: number, // Time multiplier + isPaused: boolean, + }, + + rendering: { + rendererType: 'webgpu' | 'webgl', + frameCounter: number, + }, + + health: { + fps: number, + updateMs: number, + renderMs: number, + }, +} +``` + +## Tips + +1. **Use the console** - All major objects are exposed globally +2. **Hot reload** - Vite will automatically reload on file changes +3. **State persistence** - userPrefs automatically save to localStorage +4. **Responsive** - Canvas automatically resizes with window +5. **WebGPU fallback** - Automatically falls back to WebGL if WebGPU unavailable + +## Clean Slate + +The framework intentionally draws nothing by default. Start adding your PixiJS objects and see results immediately. diff --git a/web-timeplot/index.html b/web-timeplot/index.html index 37702a2..6f0d26d 100644 --- a/web-timeplot/index.html +++ b/web-timeplot/index.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>TimePlot - PixiJS Waterfall Display</title> + <title>PixiJS Prototyping Framework</title> <style> * { margin: 0; @@ -21,92 +21,17 @@ #app { width: 100vw; height: 100vh; - display: flex; - flex-direction: column; } #canvas-container { - flex: 1; - position: relative; - } - - #controls { - background: #2a2a2f; - padding: 10px 20px; - border-top: 2px solid #3a3a4f; - display: flex; - gap: 15px; - align-items: center; - flex-wrap: wrap; - } - - .control-group { - display: flex; - gap: 10px; - align-items: center; - } - - button { - background: #4a4a5f; - color: #fff; - border: none; - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - transition: background 0.2s; - } - - button:hover { - background: #5a5a7f; - } - - button.active { - background: #6a6aff; - } - - .info { - font-family: monospace; - font-size: 12px; - color: #6f6; - margin-left: auto; - min-width: 600px; - text-align: right; - } - - kbd { - background: #3a3a4f; - padding: 2px 6px; - border-radius: 3px; - font-size: 11px; - border: 1px solid #4a4a5f; + width: 100%; + height: 100%; } </style> </head> <body> <div id="app"> <div id="canvas-container"></div> - <div id="controls"> - <div class="control-group"> - <button id="toggle-grid"> - Grid: <kbd>G</kbd> - </button> - <button id="toggle-metrics"> - Metrics: <kbd>M</kbd> - </button> - <button id="export-metrics"> - Export: <kbd>E</kbd> - </button> - </div> - <div class="control-group"> - <span>Renderer: <span id="renderer-type">Loading...</span></span> - <span style="margin-left: 15px;">Time Range Zoom: <span id="time-scale">1.0x</span></span> - <span style="font-size: 11px; color: #888;">(Middle-drag ↕)</span> - </div> - <div class="info" id="metrics-display"> - Starting... - </div> - </div> </div> <script type="module" src="/src/main.js"></script> </body> diff --git a/web-timeplot/src/main.js b/web-timeplot/src/main.js index c9d11fb..e435193 100644 --- a/web-timeplot/src/main.js +++ b/web-timeplot/src/main.js @@ -1,8 +1,6 @@ import { Application } from 'pixi.js'; -import { WaterfallGraph } from './waterfall.js'; -import { PerformanceMetrics } from './metrics.js'; +import * as PIXI from 'pixi.js'; import { StateManager } from './state.js'; -import { example8_InteractiveDemo} from './example-usage.js'; // ============================================================================ // GLOBAL STATE @@ -14,26 +12,17 @@ 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('Framework starting...'); log('init DOM'); await initDOM(); @@ -41,19 +30,10 @@ document.addEventListener('DOMContentLoaded', async function() { 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'); + log('Framework ready - start prototyping!'); }); // ============================================================================ @@ -62,12 +42,10 @@ document.addEventListener('DOMContentLoaded', async function() { 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'); + + if (!dom.container) { + throw new Error('Canvas container not found'); + } } async function initRenderer() { @@ -84,7 +62,7 @@ async function initRenderer() { await app.init({ preference: preference, width: window.innerWidth, - height: window.innerHeight - 60, // Account for controls + height: window.innerHeight, backgroundColor: 0x1a1a26, antialias: true, autoDensity: true, @@ -96,7 +74,6 @@ async function initRenderer() { // 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 @@ -112,159 +89,26 @@ async function initRenderer() { } } -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; + const height = window.innerHeight; 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'; - } } // ============================================================================ @@ -272,103 +116,36 @@ function handleMouseUp(e) { // ============================================================================ 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(); - } + // YOUR PROTOTYPE CODE GOES HERE + // Example: + // yourSprite.x += 1; + // yourGraphics.rotation += 0.01; } // ============================================================================ -// UI CONTROL FUNCTIONS +// UTILITIES // ============================================================================ -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'; - } - } +function log(msg) { + console.log(`[Framework] ${msg}`); } // ============================================================================ -// UTILITIES +// EXPORTS FOR PROTOTYPING // ============================================================================ -function log(msg) { - console.log(`[TimePlot] ${msg}`); -} +// Export immediately available objects +window.PIXI = PIXI; +window.state = state; +window.log = log; + +// Export app after initialization (using a getter) +Object.defineProperty(window, 'pixiApp', { + get() { return app; } +}); |
