summaryrefslogtreecommitdiff
path: root/web-timeplot
diff options
context:
space:
mode:
Diffstat (limited to 'web-timeplot')
-rw-r--r--web-timeplot/PROTOTYPING.md146
-rw-r--r--web-timeplot/index.html81
-rw-r--r--web-timeplot/src/main.js275
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; }
+});