Add comprehensive CSS styles for UI components and dark mode support
This commit is contained in:
391
ui/components/LiveDemoTab.js
Normal file
391
ui/components/LiveDemoTab.js
Normal file
@@ -0,0 +1,391 @@
|
||||
// Live Demo Tab Component
|
||||
|
||||
import { poseService } from '../services/pose.service.js';
|
||||
import { streamService } from '../services/stream.service.js';
|
||||
|
||||
export class LiveDemoTab {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.isRunning = false;
|
||||
this.streamConnection = null;
|
||||
this.poseSubscription = null;
|
||||
this.signalCanvas = null;
|
||||
this.poseCanvas = null;
|
||||
this.signalCtx = null;
|
||||
this.poseCtx = null;
|
||||
this.animationFrame = null;
|
||||
this.signalTime = 0;
|
||||
this.poseData = null;
|
||||
}
|
||||
|
||||
// Initialize component
|
||||
init() {
|
||||
this.setupCanvases();
|
||||
this.setupControls();
|
||||
this.initializeDisplays();
|
||||
}
|
||||
|
||||
// Set up canvases
|
||||
setupCanvases() {
|
||||
this.signalCanvas = this.container.querySelector('#signalCanvas');
|
||||
this.poseCanvas = this.container.querySelector('#poseCanvas');
|
||||
|
||||
if (this.signalCanvas) {
|
||||
this.signalCtx = this.signalCanvas.getContext('2d');
|
||||
}
|
||||
|
||||
if (this.poseCanvas) {
|
||||
this.poseCtx = this.poseCanvas.getContext('2d');
|
||||
}
|
||||
}
|
||||
|
||||
// Set up control buttons
|
||||
setupControls() {
|
||||
const startButton = this.container.querySelector('#startDemo');
|
||||
const stopButton = this.container.querySelector('#stopDemo');
|
||||
|
||||
if (startButton) {
|
||||
startButton.addEventListener('click', () => this.startDemo());
|
||||
}
|
||||
|
||||
if (stopButton) {
|
||||
stopButton.addEventListener('click', () => this.stopDemo());
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize displays
|
||||
initializeDisplays() {
|
||||
// Initialize signal canvas
|
||||
if (this.signalCtx) {
|
||||
this.signalCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
this.signalCtx.fillRect(0, 0, this.signalCanvas.width, this.signalCanvas.height);
|
||||
}
|
||||
|
||||
// Initialize pose canvas
|
||||
if (this.poseCtx) {
|
||||
this.poseCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
this.poseCtx.fillRect(0, 0, this.poseCanvas.width, this.poseCanvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Start demo
|
||||
async startDemo() {
|
||||
if (this.isRunning) return;
|
||||
|
||||
try {
|
||||
// Update UI
|
||||
this.isRunning = true;
|
||||
this.updateControls();
|
||||
this.updateStatus('Starting...', 'info');
|
||||
|
||||
// Check stream status
|
||||
const streamStatus = await streamService.getStatus();
|
||||
|
||||
if (!streamStatus.is_active) {
|
||||
// Try to start streaming
|
||||
await streamService.start();
|
||||
}
|
||||
|
||||
// Start pose stream
|
||||
this.streamConnection = poseService.startPoseStream({
|
||||
minConfidence: 0.5,
|
||||
maxFps: 30
|
||||
});
|
||||
|
||||
// Subscribe to pose updates
|
||||
this.poseSubscription = poseService.subscribeToPoseUpdates(update => {
|
||||
this.handlePoseUpdate(update);
|
||||
});
|
||||
|
||||
// Start animations
|
||||
this.startAnimations();
|
||||
|
||||
// Update status
|
||||
this.updateStatus('Running', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start demo:', error);
|
||||
this.updateStatus('Failed to start', 'error');
|
||||
this.stopDemo();
|
||||
}
|
||||
}
|
||||
|
||||
// Stop demo
|
||||
stopDemo() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
// Update UI
|
||||
this.isRunning = false;
|
||||
this.updateControls();
|
||||
this.updateStatus('Stopped', 'info');
|
||||
|
||||
// Stop pose stream
|
||||
if (this.poseSubscription) {
|
||||
this.poseSubscription();
|
||||
this.poseSubscription = null;
|
||||
}
|
||||
|
||||
poseService.stopPoseStream();
|
||||
this.streamConnection = null;
|
||||
|
||||
// Stop animations
|
||||
if (this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update controls
|
||||
updateControls() {
|
||||
const startButton = this.container.querySelector('#startDemo');
|
||||
const stopButton = this.container.querySelector('#stopDemo');
|
||||
|
||||
if (startButton) {
|
||||
startButton.disabled = this.isRunning;
|
||||
}
|
||||
|
||||
if (stopButton) {
|
||||
stopButton.disabled = !this.isRunning;
|
||||
}
|
||||
}
|
||||
|
||||
// Update status display
|
||||
updateStatus(text, type) {
|
||||
const statusElement = this.container.querySelector('#demoStatus');
|
||||
if (statusElement) {
|
||||
statusElement.textContent = text;
|
||||
statusElement.className = `status status--${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle pose updates
|
||||
handlePoseUpdate(update) {
|
||||
switch (update.type) {
|
||||
case 'connected':
|
||||
console.log('Pose stream connected');
|
||||
break;
|
||||
|
||||
case 'pose_update':
|
||||
this.poseData = update.data;
|
||||
this.updateMetrics(update.data);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('Pose stream error:', update.error);
|
||||
this.updateStatus('Stream error', 'error');
|
||||
break;
|
||||
|
||||
case 'disconnected':
|
||||
console.log('Pose stream disconnected');
|
||||
if (this.isRunning) {
|
||||
this.updateStatus('Disconnected', 'warning');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update metrics display
|
||||
updateMetrics(poseData) {
|
||||
if (!poseData) return;
|
||||
|
||||
// Update signal strength (simulated based on detection confidence)
|
||||
const signalStrength = this.container.querySelector('#signalStrength');
|
||||
if (signalStrength) {
|
||||
const strength = poseData.persons?.length > 0
|
||||
? -45 - Math.random() * 10
|
||||
: -55 - Math.random() * 10;
|
||||
signalStrength.textContent = `${strength.toFixed(0)} dBm`;
|
||||
}
|
||||
|
||||
// Update latency
|
||||
const latency = this.container.querySelector('#latency');
|
||||
if (latency && poseData.processing_time) {
|
||||
latency.textContent = `${poseData.processing_time.toFixed(0)} ms`;
|
||||
}
|
||||
|
||||
// Update person count
|
||||
const personCount = this.container.querySelector('#personCount');
|
||||
if (personCount) {
|
||||
personCount.textContent = poseData.persons?.length || 0;
|
||||
}
|
||||
|
||||
// Update confidence
|
||||
const confidence = this.container.querySelector('#confidence');
|
||||
if (confidence && poseData.persons?.length > 0) {
|
||||
const avgConfidence = poseData.persons.reduce((sum, p) => sum + p.confidence, 0)
|
||||
/ poseData.persons.length * 100;
|
||||
confidence.textContent = `${avgConfidence.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
// Update keypoints
|
||||
const keypoints = this.container.querySelector('#keypoints');
|
||||
if (keypoints && poseData.persons?.length > 0) {
|
||||
const totalKeypoints = poseData.persons[0].keypoints?.length || 0;
|
||||
const detectedKeypoints = poseData.persons[0].keypoints?.filter(kp => kp.confidence > 0.5).length || 0;
|
||||
keypoints.textContent = `${detectedKeypoints}/${totalKeypoints}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Start animations
|
||||
startAnimations() {
|
||||
const animate = () => {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
// Update signal visualization
|
||||
this.updateSignalVisualization();
|
||||
|
||||
// Update pose visualization
|
||||
this.updatePoseVisualization();
|
||||
|
||||
this.animationFrame = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
// Update signal visualization
|
||||
updateSignalVisualization() {
|
||||
if (!this.signalCtx) return;
|
||||
|
||||
const ctx = this.signalCtx;
|
||||
const width = this.signalCanvas.width;
|
||||
const height = this.signalCanvas.height;
|
||||
|
||||
// Clear canvas with fade effect
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Draw amplitude signal
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#1FB8CD';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const hasData = this.poseData?.persons?.length > 0;
|
||||
const amplitude = hasData ? 30 : 10;
|
||||
const frequency = hasData ? 0.05 : 0.02;
|
||||
|
||||
const y = height / 2 +
|
||||
Math.sin(x * frequency + this.signalTime) * amplitude +
|
||||
Math.sin(x * 0.02 + this.signalTime * 1.5) * 15;
|
||||
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// Draw phase signal
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#FFC185';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const hasData = this.poseData?.persons?.length > 0;
|
||||
const amplitude = hasData ? 25 : 15;
|
||||
|
||||
const y = height / 2 +
|
||||
Math.cos(x * 0.03 + this.signalTime * 0.8) * amplitude +
|
||||
Math.cos(x * 0.01 + this.signalTime * 0.5) * 20;
|
||||
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
this.signalTime += 0.05;
|
||||
}
|
||||
|
||||
// Update pose visualization
|
||||
updatePoseVisualization() {
|
||||
if (!this.poseCtx || !this.poseData) return;
|
||||
|
||||
const ctx = this.poseCtx;
|
||||
const width = this.poseCanvas.width;
|
||||
const height = this.poseCanvas.height;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Draw each detected person
|
||||
if (this.poseData.persons) {
|
||||
this.poseData.persons.forEach((person, index) => {
|
||||
this.drawPerson(ctx, person, index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a person's pose
|
||||
drawPerson(ctx, person, index) {
|
||||
if (!person.keypoints) return;
|
||||
|
||||
// Define COCO keypoint connections
|
||||
const connections = [
|
||||
[0, 1], [0, 2], [1, 3], [2, 4], // Head
|
||||
[5, 6], [5, 7], [7, 9], [6, 8], [8, 10], // Arms
|
||||
[5, 11], [6, 12], [11, 12], // Body
|
||||
[11, 13], [13, 15], [12, 14], [14, 16] // Legs
|
||||
];
|
||||
|
||||
// Scale keypoints to canvas
|
||||
const scale = Math.min(this.poseCanvas.width, this.poseCanvas.height) / 2;
|
||||
const offsetX = this.poseCanvas.width / 2;
|
||||
const offsetY = this.poseCanvas.height / 2;
|
||||
|
||||
// Draw skeleton connections
|
||||
ctx.strokeStyle = `hsl(${index * 60}, 70%, 50%)`;
|
||||
ctx.lineWidth = 3;
|
||||
|
||||
connections.forEach(([i, j]) => {
|
||||
const kp1 = person.keypoints[i];
|
||||
const kp2 = person.keypoints[j];
|
||||
|
||||
if (kp1 && kp2 && kp1.confidence > 0.3 && kp2.confidence > 0.3) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(kp1.x * scale + offsetX, kp1.y * scale + offsetY);
|
||||
ctx.lineTo(kp2.x * scale + offsetX, kp2.y * scale + offsetY);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw keypoints
|
||||
ctx.fillStyle = `hsl(${index * 60}, 70%, 60%)`;
|
||||
|
||||
person.keypoints.forEach(kp => {
|
||||
if (kp.confidence > 0.3) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
kp.x * scale + offsetX,
|
||||
kp.y * scale + offsetY,
|
||||
5,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw confidence label
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText(
|
||||
`Person ${index + 1}: ${(person.confidence * 100).toFixed(1)}%`,
|
||||
10,
|
||||
20 + index * 20
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
this.stopDemo();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user