// PoseDetectionCanvas Component for WiFi-DensePose UI
import { PoseRenderer } from '../utils/pose-renderer.js';
import { poseService } from '../services/pose.service.js';
import { SettingsPanel } from './SettingsPanel.js';
export class PoseDetectionCanvas {
constructor(containerId, options = {}) {
this.containerId = containerId;
this.container = document.getElementById(containerId);
if (!this.container) {
throw new Error(`Container with ID '${containerId}' not found`);
}
this.config = {
width: 800,
height: 600,
autoResize: true,
enableStats: true,
enableControls: true,
zoneId: 'zone_1',
updateInterval: 50, // ms
...options
};
this.state = {
isActive: false,
connectionState: 'disconnected',
lastPoseData: null,
errorMessage: null,
frameCount: 0,
startTime: Date.now()
};
this.callbacks = {
onStateChange: null,
onPoseUpdate: null,
onError: null,
onConnectionChange: null
};
this.logger = this.createLogger();
this.unsubscribeFunctions = [];
// Initialize settings panel
this.settingsPanel = null;
// Initialize component
this.initializeComponent();
}
createLogger() {
return {
debug: (...args) => console.debug('[CANVAS-DEBUG]', new Date().toISOString(), ...args),
info: (...args) => console.info('[CANVAS-INFO]', new Date().toISOString(), ...args),
warn: (...args) => console.warn('[CANVAS-WARN]', new Date().toISOString(), ...args),
error: (...args) => console.error('[CANVAS-ERROR]', new Date().toISOString(), ...args)
};
}
initializeComponent() {
this.logger.info('Initializing PoseDetectionCanvas component', { containerId: this.containerId });
// Create DOM structure
this.createDOMStructure();
// Initialize canvas and renderer
this.initializeCanvas();
// Set up event handlers
this.setupEventHandlers();
// Set up pose service subscription
this.setupPoseServiceSubscription();
this.logger.info('PoseDetectionCanvas component initialized successfully');
}
createDOMStructure() {
this.container.innerHTML = `
`;
// Add CSS styles
this.addComponentStyles();
}
addComponentStyles() {
const style = document.createElement('style');
style.textContent = `
.pose-detection-canvas-wrapper {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
background: #f9f9f9;
font-family: Arial, sans-serif;
}
.pose-canvas-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background: #f0f0f0;
border-bottom: 1px solid #ddd;
}
.pose-canvas-title {
display: flex;
align-items: center;
gap: 15px;
}
.pose-canvas-title h3 {
margin: 0;
color: #333;
font-size: 16px;
}
.connection-status {
display: flex;
align-items: center;
gap: 5px;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: #ccc;
transition: background-color 0.3s;
}
.status-indicator.connected { background: #28a745; }
.status-indicator.connecting { background: #ffc107; }
.status-indicator.error { background: #dc3545; }
.status-indicator.disconnected { background: #6c757d; }
.status-text {
font-size: 12px;
color: #666;
min-width: 80px;
}
.pose-canvas-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.primary-controls {
flex: 1;
}
.secondary-controls {
flex-shrink: 0;
}
.btn {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 6px;
background: #ffffff;
color: #333333;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
text-decoration: none;
display: inline-block;
min-width: 80px;
text-align: center;
}
.btn:hover:not(:disabled) {
background: #f8f9fa;
border-color: #adb5bd;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
transform: translateY(-1px);
}
.btn:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
background: #e9ecef;
color: #6c757d;
transform: none !important;
box-shadow: none !important;
}
.btn-start {
background: #28a745;
color: white;
border-color: #28a745;
}
.btn-start:hover:not(:disabled) {
background: #218838;
border-color: #1e7e34;
}
.btn-stop {
background: #dc3545;
color: white;
border-color: #dc3545;
}
.btn-stop:hover:not(:disabled) {
background: #c82333;
border-color: #bd2130;
}
.btn-reconnect {
background: #17a2b8;
color: white;
border-color: #17a2b8;
}
.btn-reconnect:hover:not(:disabled) {
background: #138496;
border-color: #117a8b;
}
.btn-demo {
background: #6f42c1;
color: white;
border-color: #6f42c1;
}
.btn-demo:hover:not(:disabled) {
background: #5a32a3;
border-color: #512a97;
}
.btn-settings {
background: #6c757d;
color: white;
border-color: #6c757d;
}
.btn-settings:hover:not(:disabled) {
background: #5a6268;
border-color: #545b62;
}
.mode-select {
padding: 5px 8px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
font-size: 12px;
}
.pose-canvas-container {
position: relative;
background: #000;
}
.pose-canvas {
display: block;
width: 100%;
height: auto;
background: #000;
}
.pose-canvas-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 10;
}
.pose-stats {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px;
border-radius: 4px;
font-size: 11px;
line-height: 1.4;
font-family: monospace;
max-width: 200px;
}
.pose-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(220, 53, 69, 0.9);
color: white;
padding: 15px;
border-radius: 4px;
font-size: 14px;
text-align: center;
max-width: 80%;
}
`;
if (!document.querySelector('#pose-canvas-styles')) {
style.id = 'pose-canvas-styles';
document.head.appendChild(style);
}
}
initializeCanvas() {
this.canvas = document.getElementById(`pose-canvas-${this.containerId}`);
this.canvas.width = this.config.width;
this.canvas.height = this.config.height;
// Initialize renderer
this.renderer = new PoseRenderer(this.canvas, {
showDebugInfo: this.config.enableStats,
mode: 'skeleton'
});
this.logger.debug('Canvas and renderer initialized', {
width: this.config.width,
height: this.config.height
});
// Handle auto-resize
if (this.config.autoResize) {
this.setupAutoResize();
}
}
setupAutoResize() {
const resizeObserver = new ResizeObserver(entries => {
const entry = entries[0];
const { width } = entry.contentRect;
const height = Math.round(width * 0.75); // 4:3 aspect ratio
this.renderer.resize(width, height);
this.logger.debug('Canvas auto-resized', { width, height });
});
resizeObserver.observe(this.container);
this.resizeObserver = resizeObserver;
}
setupEventHandlers() {
// Start button
const startBtn = document.getElementById(`start-btn-${this.containerId}`);
startBtn.addEventListener('click', () => this.start());
// Stop button
const stopBtn = document.getElementById(`stop-btn-${this.containerId}`);
stopBtn.addEventListener('click', () => this.stop());
// Reconnect button
const reconnectBtn = document.getElementById(`reconnect-btn-${this.containerId}`);
reconnectBtn.addEventListener('click', () => this.reconnect());
// Demo button
const demoBtn = document.getElementById(`demo-btn-${this.containerId}`);
demoBtn.addEventListener('click', () => this.toggleDemo());
// Settings button
const settingsBtn = document.getElementById(`settings-btn-${this.containerId}`);
settingsBtn.addEventListener('click', () => this.showSettings());
// Mode selector
const modeSelect = document.getElementById(`mode-select-${this.containerId}`);
modeSelect.addEventListener('change', (event) => {
this.setRenderMode(event.target.value);
});
this.logger.debug('Event handlers set up');
}
setupPoseServiceSubscription() {
// Subscribe to pose updates
const unsubscribePose = poseService.subscribeToPoseUpdates((update) => {
this.handlePoseUpdate(update);
});
this.unsubscribeFunctions.push(unsubscribePose);
this.logger.debug('Pose service subscription set up');
}
handlePoseUpdate(update) {
try {
switch (update.type) {
case 'pose_update':
this.state.lastPoseData = update.data;
this.state.frameCount++;
this.renderPoseData(update.data);
this.updateStats();
this.notifyCallback('onPoseUpdate', update.data);
break;
case 'connected':
this.setConnectionState('connected');
this.clearError();
break;
case 'disconnected':
this.setConnectionState('disconnected');
break;
case 'connecting':
this.setConnectionState('connecting');
break;
case 'connection_state':
this.setConnectionState(update.state);
break;
case 'error':
this.setConnectionState('error');
this.showError(update.error?.message || 'Connection error');
this.notifyCallback('onError', update.error);
break;
default:
this.logger.debug('Unhandled pose update type', { type: update.type });
}
} catch (error) {
this.logger.error('Error handling pose update', { error: error.message, update });
this.showError(`Update error: ${error.message}`);
}
}
renderPoseData(poseData) {
if (!this.renderer || !this.state.isActive) {
return;
}
try {
this.renderer.render(poseData, {
frameCount: this.state.frameCount,
connectionState: this.state.connectionState
});
} catch (error) {
this.logger.error('Render error', { error: error.message });
this.showError(`Render error: ${error.message}`);
}
}
setConnectionState(state) {
if (this.state.connectionState !== state) {
this.logger.debug('Connection state changed', { from: this.state.connectionState, to: state });
this.state.connectionState = state;
this.updateConnectionIndicator();
this.updateControls();
this.notifyCallback('onConnectionChange', state);
}
}
updateConnectionIndicator() {
const indicator = document.getElementById(`status-indicator-${this.containerId}`);
const text = document.getElementById(`status-text-${this.containerId}`);
if (indicator && text) {
indicator.className = `status-indicator ${this.state.connectionState}`;
text.textContent = this.state.connectionState.charAt(0).toUpperCase() +
this.state.connectionState.slice(1);
}
}
updateControls() {
const startBtn = document.getElementById(`start-btn-${this.containerId}`);
const stopBtn = document.getElementById(`stop-btn-${this.containerId}`);
const reconnectBtn = document.getElementById(`reconnect-btn-${this.containerId}`);
if (startBtn && stopBtn && reconnectBtn) {
const isConnected = this.state.connectionState === 'connected';
const isActive = this.state.isActive;
startBtn.disabled = isActive || isConnected;
stopBtn.disabled = !isActive;
reconnectBtn.disabled = !isActive || this.state.connectionState === 'connecting';
}
}
updateStats() {
if (!this.config.enableStats) return;
const statsEl = document.getElementById(`stats-${this.containerId}`);
if (!statsEl) return;
const uptime = Math.round((Date.now() - this.state.startTime) / 1000);
const fps = this.renderer.getPerformanceMetrics().averageFps;
const persons = this.state.lastPoseData?.persons?.length || 0;
const zones = Object.keys(this.state.lastPoseData?.zone_summary || {}).length;
statsEl.innerHTML = `
Connection: ${this.state.connectionState}
Frames: ${this.state.frameCount}
FPS: ${fps.toFixed(1)}
Persons: ${persons}
Zones: ${zones}
Uptime: ${uptime}s
`;
}
showError(message) {
this.state.errorMessage = message;
const errorEl = document.getElementById(`error-${this.containerId}`);
if (errorEl) {
errorEl.textContent = message;
errorEl.style.display = 'block';
}
this.logger.error('Component error', { message });
}
clearError() {
this.state.errorMessage = null;
const errorEl = document.getElementById(`error-${this.containerId}`);
if (errorEl) {
errorEl.style.display = 'none';
}
}
// Public API methods
async start() {
try {
this.logger.info('Starting pose detection');
this.state.isActive = true;
this.state.frameCount = 0;
this.state.startTime = Date.now();
this.clearError();
this.updateControls();
await poseService.startPoseStream({
zoneIds: [this.config.zoneId],
minConfidence: 0.3,
maxFps: 30
});
this.notifyCallback('onStateChange', { isActive: true });
this.logger.info('Pose detection started successfully');
} catch (error) {
this.logger.error('Failed to start pose detection', { error: error.message });
this.state.isActive = false;
this.updateControls();
this.showError(`Failed to start: ${error.message}`);
this.notifyCallback('onError', error);
}
}
stop() {
try {
this.logger.info('Stopping pose detection');
this.state.isActive = false;
poseService.stopPoseStream();
this.setConnectionState('disconnected');
this.clearError();
this.updateControls();
// Clear canvas
if (this.renderer) {
this.renderer.clearCanvas();
}
this.notifyCallback('onStateChange', { isActive: false });
this.logger.info('Pose detection stopped');
} catch (error) {
this.logger.error('Error stopping pose detection', { error: error.message });
this.showError(`Stop error: ${error.message}`);
}
}
async reconnect() {
try {
this.logger.info('Reconnecting pose stream');
await poseService.reconnectStream();
} catch (error) {
this.logger.error('Reconnection failed', { error: error.message });
this.showError(`Reconnection failed: ${error.message}`);
}
}
setRenderMode(mode) {
if (this.renderer) {
this.renderer.setMode(mode);
this.logger.info('Render mode changed', { mode });
}
}
// Toggle demo mode
toggleDemo() {
if (this.demoState && this.demoState.isRunning) {
this.stopDemo();
this.updateDemoButton(false);
} else {
this.runDemo();
this.updateDemoButton(true);
}
}
// Demo mode - renders animated test pose data
runDemo() {
this.logger.info('Running animated demo mode');
// Stop any existing demo animation
this.stopDemo();
// Force enable all visual elements for demo
this.originalConfig = { ...this.renderer.config };
this.renderer.updateConfig({
showKeypoints: true,
showSkeleton: true,
showBoundingBox: true,
showConfidence: true,
confidenceThreshold: 0.1,
keypointConfidenceThreshold: 0.1
});
// Initialize animation state
this.demoState = {
isRunning: true,
frameCount: 0,
startTime: Date.now(),
animations: {
person1: { type: 'walking', phase: 0, centerX: 150, centerY: 250 },
person2: { type: 'waving', phase: 0, centerX: 350, centerY: 270 },
person3: { type: 'dancing', phase: 0, centerX: 550, centerY: 260 }
}
};
// Start animation loop
this.startDemoAnimation();
// Show demo notification
this.showDemoNotification('🎭 Animated Demo Active - Walking, Waving & Dancing');
}
stopDemo() {
if (this.demoState && this.demoState.isRunning) {
this.demoState.isRunning = false;
if (this.demoAnimationFrame) {
cancelAnimationFrame(this.demoAnimationFrame);
}
if (this.originalConfig) {
this.renderer.updateConfig(this.originalConfig);
}
// Clear canvas
if (this.renderer) {
this.renderer.clearCanvas();
}
this.logger.info('Demo stopped');
}
}
updateDemoButton(isRunning) {
const demoBtn = document.getElementById(`demo-btn-${this.containerId}`);
if (demoBtn) {
demoBtn.textContent = isRunning ? 'Stop Demo' : 'Demo';
demoBtn.style.background = isRunning ? '#dc3545' : '#6f42c1';
demoBtn.style.borderColor = isRunning ? '#dc3545' : '#6f42c1';
}
}
startDemoAnimation() {
if (!this.demoState || !this.demoState.isRunning) return;
this.demoState.frameCount++;
const elapsed = (Date.now() - this.demoState.startTime) / 1000;
// Generate animated pose data
const animatedPoseData = this.generateAnimatedPoseData(elapsed);
// Render the animated data
this.renderPoseData(animatedPoseData);
// Continue animation
this.demoAnimationFrame = requestAnimationFrame(() => this.startDemoAnimation());
}
generateAnimatedPoseData(time) {
const persons = [];
// Person 1: Walking animation
const person1 = this.generateWalkingPerson(
this.demoState.animations.person1.centerX,
this.demoState.animations.person1.centerY,
time * 2 // Walking speed
);
persons.push(person1);
// Person 2: Waving animation
const person2 = this.generateWavingPerson(
this.demoState.animations.person2.centerX,
this.demoState.animations.person2.centerY,
time * 3 // Waving speed
);
persons.push(person2);
// Person 3: Dancing animation
const person3 = this.generateDancingPerson(
this.demoState.animations.person3.centerX,
this.demoState.animations.person3.centerY,
time * 2.5 // Dancing speed
);
persons.push(person3);
return {
timestamp: new Date().toISOString(),
frame_id: `demo_frame_${this.demoState.frameCount.toString().padStart(6, '0')}`,
persons: persons,
zone_summary: {
demo_zone: persons.length
},
processing_time_ms: 12 + Math.random() * 8,
metadata: {
mock_data: true,
source: 'animated_demo',
fps: Math.round(this.demoState.frameCount / ((Date.now() - this.demoState.startTime) / 1000))
}
};
}
generateWalkingPerson(centerX, centerY, time) {
// Walking cycle parameters
const walkCycle = Math.sin(time) * 0.3;
const stepPhase = Math.sin(time * 2) * 0.2;
// Base keypoint positions for walking
const keypoints = [
// Head (nose, eyes, ears) - slight bob
{ x: centerX, y: centerY - 80 + Math.sin(time * 4) * 2, confidence: 0.95 },
{ x: centerX - 8, y: centerY - 85 + Math.sin(time * 4) * 2, confidence: 0.92 },
{ x: centerX + 8, y: centerY - 85 + Math.sin(time * 4) * 2, confidence: 0.93 },
{ x: centerX - 15, y: centerY - 82 + Math.sin(time * 4) * 2, confidence: 0.88 },
{ x: centerX + 15, y: centerY - 82 + Math.sin(time * 4) * 2, confidence: 0.89 },
// Shoulders - subtle movement
{ x: centerX - 35 + walkCycle * 5, y: centerY - 40 + Math.sin(time * 4) * 1, confidence: 0.94 },
{ x: centerX + 35 - walkCycle * 5, y: centerY - 40 + Math.sin(time * 4) * 1, confidence: 0.95 },
// Elbows - arm swing
{ x: centerX - 25 + walkCycle * 20, y: centerY + 10 + walkCycle * 10, confidence: 0.91 },
{ x: centerX + 25 - walkCycle * 20, y: centerY + 10 - walkCycle * 10, confidence: 0.92 },
// Wrists - follow elbows
{ x: centerX - 15 + walkCycle * 25, y: centerY + 55 + walkCycle * 15, confidence: 0.87 },
{ x: centerX + 15 - walkCycle * 25, y: centerY + 55 - walkCycle * 15, confidence: 0.88 },
// Hips - slight movement
{ x: centerX - 18 + walkCycle * 3, y: centerY + 60, confidence: 0.96 },
{ x: centerX + 18 - walkCycle * 3, y: centerY + 60, confidence: 0.96 },
// Knees - walking motion
{ x: centerX - 20 + stepPhase * 15, y: centerY + 120 - Math.abs(stepPhase) * 10, confidence: 0.93 },
{ x: centerX + 20 - stepPhase * 15, y: centerY + 120 - Math.abs(-stepPhase) * 10, confidence: 0.94 },
// Ankles - foot placement
{ x: centerX - 22 + stepPhase * 20, y: centerY + 180, confidence: 0.90 },
{ x: centerX + 22 - stepPhase * 20, y: centerY + 180, confidence: 0.91 }
];
return {
person_id: 'demo_walker',
confidence: 0.94 + Math.sin(time) * 0.03,
bbox: this.calculateBoundingBox(keypoints),
keypoints: keypoints,
zone_id: 'demo_zone',
activity: 'walking'
};
}
generateWavingPerson(centerX, centerY, time) {
// Waving parameters
const wavePhase = Math.sin(time) * 0.8;
const armWave = Math.sin(time * 1.5) * 30;
const keypoints = [
// Head - stable
{ x: centerX, y: centerY - 80, confidence: 0.96 },
{ x: centerX - 8, y: centerY - 85, confidence: 0.94 },
{ x: centerX + 8, y: centerY - 85, confidence: 0.94 },
{ x: centerX - 15, y: centerY - 82, confidence: 0.90 },
{ x: centerX + 15, y: centerY - 82, confidence: 0.91 },
// Shoulders
{ x: centerX - 35, y: centerY - 40, confidence: 0.95 },
{ x: centerX + 35, y: centerY - 40, confidence: 0.95 },
// Elbows - left arm stable, right arm waving
{ x: centerX - 55, y: centerY + 10, confidence: 0.92 },
{ x: centerX + 65 + armWave * 0.3, y: centerY - 10 - Math.abs(armWave) * 0.5, confidence: 0.93 },
// Wrists - dramatic wave motion
{ x: centerX - 60, y: centerY + 60, confidence: 0.88 },
{ x: centerX + 45 + armWave, y: centerY - 30 - Math.abs(armWave) * 0.8, confidence: 0.89 },
// Hips - stable
{ x: centerX - 18, y: centerY + 60, confidence: 0.97 },
{ x: centerX + 18, y: centerY + 60, confidence: 0.97 },
// Knees - slight movement
{ x: centerX - 20, y: centerY + 120 + Math.sin(time * 0.5) * 5, confidence: 0.94 },
{ x: centerX + 20, y: centerY + 120 + Math.sin(time * 0.5) * 5, confidence: 0.95 },
// Ankles - stable
{ x: centerX - 22, y: centerY + 180, confidence: 0.92 },
{ x: centerX + 22, y: centerY + 180, confidence: 0.93 }
];
return {
person_id: 'demo_waver',
confidence: 0.91 + Math.sin(time * 0.7) * 0.05,
bbox: this.calculateBoundingBox(keypoints),
keypoints: keypoints,
zone_id: 'demo_zone',
activity: 'waving'
};
}
generateDancingPerson(centerX, centerY, time) {
// Dancing parameters - more complex movement
const dancePhase1 = Math.sin(time * 1.2) * 0.6;
const dancePhase2 = Math.cos(time * 1.8) * 0.4;
const bodyBob = Math.sin(time * 3) * 8;
const hipSway = Math.sin(time * 1.5) * 15;
const keypoints = [
// Head - dancing bob
{ x: centerX + dancePhase1 * 5, y: centerY - 80 + bodyBob, confidence: 0.96 },
{ x: centerX - 8 + dancePhase1 * 5, y: centerY - 85 + bodyBob, confidence: 0.94 },
{ x: centerX + 8 + dancePhase1 * 5, y: centerY - 85 + bodyBob, confidence: 0.94 },
{ x: centerX - 15 + dancePhase1 * 5, y: centerY - 82 + bodyBob, confidence: 0.90 },
{ x: centerX + 15 + dancePhase1 * 5, y: centerY - 82 + bodyBob, confidence: 0.91 },
// Shoulders - dance movement
{ x: centerX - 35 + dancePhase1 * 10, y: centerY - 40 + bodyBob * 0.5, confidence: 0.95 },
{ x: centerX + 35 + dancePhase2 * 10, y: centerY - 40 + bodyBob * 0.5, confidence: 0.95 },
// Elbows - both arms dancing
{ x: centerX - 45 + dancePhase1 * 25, y: centerY + 0 + dancePhase1 * 20, confidence: 0.92 },
{ x: centerX + 45 + dancePhase2 * 25, y: centerY + 0 + dancePhase2 * 20, confidence: 0.93 },
// Wrists - expressive arm movements
{ x: centerX - 40 + dancePhase1 * 35, y: centerY + 50 + dancePhase1 * 30, confidence: 0.88 },
{ x: centerX + 40 + dancePhase2 * 35, y: centerY + 50 + dancePhase2 * 30, confidence: 0.89 },
// Hips - dancing sway
{ x: centerX - 18 + hipSway * 0.3, y: centerY + 60 + bodyBob * 0.3, confidence: 0.97 },
{ x: centerX + 18 + hipSway * 0.3, y: centerY + 60 + bodyBob * 0.3, confidence: 0.97 },
// Knees - dancing steps
{ x: centerX - 20 + hipSway * 0.5 + Math.sin(time * 2.5) * 10, y: centerY + 120 + Math.abs(Math.sin(time * 2.5)) * 15, confidence: 0.94 },
{ x: centerX + 20 + hipSway * 0.5 + Math.cos(time * 2.5) * 10, y: centerY + 120 + Math.abs(Math.cos(time * 2.5)) * 15, confidence: 0.95 },
// Ankles - feet positioning
{ x: centerX - 22 + hipSway * 0.6 + Math.sin(time * 2.5) * 12, y: centerY + 180, confidence: 0.92 },
{ x: centerX + 22 + hipSway * 0.6 + Math.cos(time * 2.5) * 12, y: centerY + 180, confidence: 0.93 }
];
return {
person_id: 'demo_dancer',
confidence: 0.89 + Math.sin(time * 1.3) * 0.07,
bbox: this.calculateBoundingBox(keypoints),
keypoints: keypoints,
zone_id: 'demo_zone',
activity: 'dancing'
};
}
calculateBoundingBox(keypoints) {
const validPoints = keypoints.filter(kp => kp.confidence > 0.1);
if (validPoints.length === 0) return { x: 0, y: 0, width: 50, height: 50 };
const xs = validPoints.map(kp => kp.x);
const ys = validPoints.map(kp => kp.y);
const minX = Math.min(...xs) - 10;
const maxX = Math.max(...xs) + 10;
const minY = Math.min(...ys) - 10;
const maxY = Math.max(...ys) + 10;
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
};
}
generateDemoKeypoints(centerX, centerY) {
// COCO keypoint order: nose, left_eye, right_eye, left_ear, right_ear,
// left_shoulder, right_shoulder, left_elbow, right_elbow, left_wrist, right_wrist,
// left_hip, right_hip, left_knee, right_knee, left_ankle, right_ankle
const offsets = [
[0, -80], // nose
[-10, -90], // left_eye
[10, -90], // right_eye
[-20, -85], // left_ear
[20, -85], // right_ear
[-40, -40], // left_shoulder
[40, -40], // right_shoulder
[-60, 10], // left_elbow
[60, 10], // right_elbow
[-65, 60], // left_wrist
[65, 60], // right_wrist
[-20, 60], // left_hip
[20, 60], // right_hip
[-25, 120], // left_knee
[25, 120], // right_knee
[-25, 180], // left_ankle
[25, 180] // right_ankle
];
return offsets.map(([dx, dy]) => ({
x: centerX + dx,
y: centerY + dy,
confidence: 0.8 + (Math.random() * 0.2)
}));
}
showDemoNotification(message = '🎭 Demo Mode Active') {
const notification = document.createElement('div');
notification.style.cssText = `
position: absolute;
top: 10px;
left: 10px;
background: rgba(111, 66, 193, 0.9);
color: white;
padding: 10px 15px;
border-radius: 4px;
font-size: 14px;
z-index: 20;
pointer-events: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
`;
notification.textContent = message;
const overlay = document.getElementById(`overlay-${this.containerId}`);
// Remove any existing notifications
const existingNotifications = overlay.querySelectorAll('div[style*="background: rgba(111, 66, 193"]');
existingNotifications.forEach(n => n.remove());
overlay.appendChild(notification);
// Remove notification after 3 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
// Configuration methods
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
if (this.renderer) {
this.renderer.updateConfig(newConfig);
}
this.logger.debug('Component configuration updated', { config: this.config });
}
// Callback management
setCallback(eventName, callback) {
if (eventName in this.callbacks) {
this.callbacks[eventName] = callback;
}
}
notifyCallback(eventName, data) {
if (this.callbacks[eventName]) {
try {
this.callbacks[eventName](data);
} catch (error) {
this.logger.error('Callback error', { eventName, error: error.message });
}
}
}
// Utility methods
getState() {
return { ...this.state };
}
getPerformanceMetrics() {
return this.renderer ? this.renderer.getPerformanceMetrics() : null;
}
exportFrame(format = 'png') {
return this.renderer ? this.renderer.exportFrame(format) : null;
}
// Test method for debugging
renderTestShape() {
if (this.renderer) {
this.renderer.renderTestShape();
}
}
// Show settings modal
showSettings() {
this.logger.info('Opening settings modal');
if (!this.settingsPanel) {
this.createSettingsModal();
}
this.settingsPanel.show();
}
createSettingsModal() {
// Create a temporary container for the settings panel
const modalContainer = document.createElement('div');
modalContainer.id = `settings-modal-${this.containerId}`;
modalContainer.className = 'settings-modal-wrapper';
modalContainer.innerHTML = `
`;
document.body.appendChild(modalContainer);
// Create the settings panel inside the modal
this.settingsPanel = new SettingsPanel(`settings-container-${this.containerId}`, {
enableAdvancedSettings: true,
enableDebugControls: true,
enableExportFeatures: true,
allowConfigPersistence: true,
initialSettings: this.getInitialSettings()
});
// Set up settings panel callbacks
this.settingsPanel.setCallback('onSettingsChange', (data) => {
this.handleSettingsChange(data);
});
this.settingsPanel.setCallback('onRenderModeChange', (mode) => {
this.setRenderMode(mode);
});
// Set up modal event handlers
this.setupModalEventHandlers(modalContainer);
// Add modal styles
this.addModalStyles();
// Add show/hide methods to the modal
modalContainer.show = () => {
modalContainer.style.display = 'flex';
modalContainer.classList.add('active');
document.body.style.overflow = 'hidden';
};
modalContainer.hide = () => {
modalContainer.style.display = 'none';
modalContainer.classList.remove('active');
document.body.style.overflow = '';
};
this.settingsPanel.show = () => modalContainer.show();
this.settingsPanel.hide = () => modalContainer.hide();
this.logger.debug('Settings modal created');
}
setupModalEventHandlers(modalContainer) {
// Close button
const closeBtn = modalContainer.querySelector('.settings-modal-close');
closeBtn.addEventListener('click', () => {
this.settingsPanel.hide();
});
// Overlay click to close
const overlay = modalContainer.querySelector('.settings-modal-overlay');
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.settingsPanel.hide();
}
});
// Escape key to close
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modalContainer.classList.contains('active')) {
this.settingsPanel.hide();
}
});
}
addModalStyles() {
if (document.querySelector('#pose-canvas-modal-styles')) return;
const style = document.createElement('style');
style.id = 'pose-canvas-modal-styles';
style.textContent = `
.settings-modal-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.settings-modal-wrapper.active {
opacity: 1;
}
.settings-modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
backdrop-filter: blur(5px);
}
.settings-modal-dialog {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 800px;
max-height: 90vh;
overflow: hidden;
transform: scale(0.9);
transition: transform 0.3s ease;
display: flex;
flex-direction: column;
}
.settings-modal-wrapper.active .settings-modal-dialog {
transform: scale(1);
}
.settings-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e9ecef;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.settings-modal-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.settings-modal-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 8px;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
backdrop-filter: blur(10px);
}
.settings-modal-close:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.settings-modal-body {
flex: 1;
overflow-y: auto;
padding: 0;
}
/* Override settings panel styles for modal */
.settings-modal-body .settings-panel {
border: none;
border-radius: 0;
box-shadow: none;
}
.settings-modal-body .settings-header {
display: none;
}
.settings-modal-body .settings-content {
max-height: none;
padding: 24px;
}
/* Custom scrollbar for modal */
.settings-modal-body::-webkit-scrollbar {
width: 8px;
}
.settings-modal-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.settings-modal-body::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.settings-modal-body::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Mobile responsive */
@media (max-width: 768px) {
.settings-modal-overlay {
padding: 10px;
}
.settings-modal-dialog {
max-width: 100%;
max-height: 95vh;
}
.settings-modal-header {
padding: 15px 20px;
}
.settings-modal-body .settings-content {
padding: 20px;
}
}
`;
document.head.appendChild(style);
}
getInitialSettings() {
return {
// Get current renderer config
...(this.renderer ? this.renderer.getConfig() : {}),
// Add other relevant settings
currentZone: this.config.zoneId || 'zone_1',
maxFps: 30,
autoReconnect: true,
connectionTimeout: 10000
};
}
handleSettingsChange(data) {
this.logger.debug('Settings changed', data);
if (data.settings && this.renderer) {
// Apply render settings
const renderConfig = {
mode: data.settings.renderMode,
showKeypoints: data.settings.showKeypoints,
showSkeleton: data.settings.showSkeleton,
showBoundingBox: data.settings.showBoundingBox,
showConfidence: data.settings.showConfidence,
showZones: data.settings.showZones,
showDebugInfo: data.settings.showDebugInfo,
skeletonColor: data.settings.skeletonColor,
keypointColor: data.settings.keypointColor,
boundingBoxColor: data.settings.boundingBoxColor,
confidenceThreshold: data.settings.confidenceThreshold,
keypointConfidenceThreshold: data.settings.keypointConfidenceThreshold,
enableSmoothing: data.settings.enableSmoothing
};
this.renderer.updateConfig(renderConfig);
this.logger.info('Renderer config updated from settings');
}
}
// Cleanup
dispose() {
this.logger.info('Disposing PoseDetectionCanvas component');
try {
// Stop pose detection
if (this.state.isActive) {
this.stop();
}
// Stop demo animation
this.stopDemo();
// Dispose settings panel
if (this.settingsPanel) {
this.settingsPanel.dispose();
const modalContainer = document.getElementById(`settings-modal-${this.containerId}`);
if (modalContainer) {
modalContainer.remove();
}
}
// Unsubscribe from pose service
this.unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
this.unsubscribeFunctions = [];
// Clean up resize observer
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
// Clear DOM
if (this.container) {
this.container.innerHTML = '';
}
this.logger.info('PoseDetectionCanvas component disposed successfully');
} catch (error) {
this.logger.error('Error during disposal', { error: error.message });
}
}
}