{"id":1219,"date":"2025-09-05T22:11:33","date_gmt":"2025-09-05T22:11:33","guid":{"rendered":"https:\/\/www.healthgrowers.co\/soy60\/?page_id=1219"},"modified":"2025-09-06T11:45:43","modified_gmt":"2025-09-06T11:45:43","slug":"mvp26","status":"publish","type":"page","link":"https:\/\/www.healthgrowers.co\/soy60\/mvp26\/","title":{"rendered":"mvp26"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"1219\" class=\"elementor elementor-1219\">\n\t\t\t\t<div class=\"elementor-element elementor-element-bccdbfc e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-equal-height-no e-con e-parent\" data-id=\"bccdbfc\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-aeec8fc elementor-widget elementor-widget-html\" data-id=\"aeec8fc\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\n<html lang=\"es\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Colibr\u00ed Rehab - Rehabilitaci\u00f3n Inteligente<\/title>\n<style>\n* {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n  overflow: auto;\n}\n\nbody { \n  font-family: 'Arial', sans-serif; \n  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n  min-height: 100vh;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 10px;\n}\n\n.container { \n  display: grid;\n  grid-template-columns: 1fr 300px;\n  gap: 20px;\n  width: 100%;\n  max-width: 1200px;\n  background: rgba(255, 255, 255, 0.1);\n  border-radius: 20px;\n  padding: 20px;\n  backdrop-filter: blur(10px);\n  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n}\n\n.game-section {\n  background: rgba(255, 255, 255, 0.9);\n  border-radius: 15px;\n  padding: 20px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 600px;\n  position: relative;\n}\n\n.dashboard {\n  background: rgba(255, 255, 255, 0.95);\n  border-radius: 15px;\n  padding: 20px;\n  display: flex;\n  flex-direction: column;\n  gap: 15px;\n}\n\ncanvas {\n  background: linear-gradient(to bottom, #e3f2fd, #ffffff);\n  border: 3px solid #2196F3;\n  border-radius: 10px;\n  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);\n  max-width: 100%;\n  height: auto;\n}\n\n.camera-preview {\n  position: absolute;\n  top: 20px;\n  right: 20px;\n  width: 160px;\n  height: 120px;\n  border: 2px solid #2196F3;\n  border-radius: 8px;\n  background: #000;\n  overflow: hidden;\n  z-index: 10;\n}\n\n.camera-preview video {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  transform: scaleX(-1);\n}\n\n.camera-status {\n  position: absolute;\n  top: 5px;\n  left: 5px;\n  background: rgba(0, 0, 0, 0.7);\n  color: white;\n  padding: 2px 6px;\n  border-radius: 4px;\n  font-size: 10px;\n  z-index: 11;\n}\n\n.controls {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n\n.control-group {\n  display: flex;\n  flex-direction: column;\n  gap: 5px;\n}\n\n.control-group label {\n  font-weight: bold;\n  color: #333;\n  font-size: 14px;\n}\n\n.control-group select,\n.control-group input[type=\"number\"],\n.control-group input[type=\"range\"] {\n  padding: 8px;\n  border: 2px solid #ddd;\n  border-radius: 8px;\n  font-size: 14px;\n  transition: border-color 0.3s ease;\n}\n\n.control-group select:focus,\n.control-group input:focus {\n  outline: none;\n  border-color: #2196F3;\n}\n\n.checkbox-group {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  margin: 10px 0;\n}\n\n.checkbox-group input[type=\"checkbox\"] {\n  transform: scale(1.2);\n}\n\n.btn {\n  background: linear-gradient(45deg, #2196F3, #21CBF3);\n  color: white;\n  border: none;\n  padding: 12px 20px;\n  border-radius: 10px;\n  font-size: 16px;\n  font-weight: bold;\n  cursor: pointer;\n  transition: all 0.3s ease;\n  text-transform: uppercase;\n  letter-spacing: 1px;\n}\n\n.btn:hover:not(:disabled) {\n  transform: translateY(-2px);\n  box-shadow: 0 6px 20px rgba(33, 150, 243, 0.4);\n}\n\n.btn:disabled {\n  opacity: 0.6;\n  cursor: not-allowed;\n  transform: none;\n}\n\n.btn-success {\n  background: linear-gradient(45deg, #4CAF50, #45a049);\n}\n\n.btn-warning {\n  background: linear-gradient(45deg, #FF9800, #F57C00);\n}\n\n.btn-danger {\n  background: linear-gradient(45deg, #F44336, #D32F2F);\n}\n\n.stats {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 15px;\n  margin: 15px 0;\n}\n\n.stat-card {\n  background: rgba(33, 150, 243, 0.1);\n  border: 2px solid rgba(33, 150, 243, 0.3);\n  border-radius: 10px;\n  padding: 15px;\n  text-align: center;\n}\n\n.stat-value {\n  font-size: 24px;\n  font-weight: bold;\n  color: #2196F3;\n  margin-bottom: 5px;\n}\n\n.stat-label {\n  font-size: 12px;\n  color: #666;\n  text-transform: uppercase;\n  letter-spacing: 1px;\n}\n\n.message-area {\n  background: rgba(76, 175, 80, 0.1);\n  border-left: 4px solid #4CAF50;\n  padding: 15px;\n  border-radius: 0 8px 8px 0;\n  margin: 15px 0;\n  min-height: 50px;\n  display: flex;\n  align-items: center;\n  font-weight: 500;\n}\n\n.progress-section {\n  margin-top: 20px;\n}\n\n.progress-bar {\n  background: #e0e0e0;\n  height: 25px;\n  border-radius: 12px;\n  overflow: hidden;\n  position: relative;\n}\n\n.progress-fill {\n  background: linear-gradient(45deg, #4CAF50, #45a049);\n  height: 100%;\n  width: 0%;\n  border-radius: 12px;\n  transition: width 0.5s ease;\n  position: relative;\n}\n\n.progress-text {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  font-weight: bold;\n  font-size: 12px;\n  color: white;\n  text-shadow: 1px 1px 2px rgba(0,0,0,0.5);\n}\n\n.calibration-panel {\n  background: rgba(255, 193, 7, 0.1);\n  border: 2px solid rgba(255, 193, 7, 0.3);\n  border-radius: 10px;\n  padding: 15px;\n  text-align: center;\n}\n\n.calibration-counter {\n  font-size: 36px;\n  font-weight: bold;\n  color: #FF9800;\n  margin: 10px 0;\n  text-shadow: 2px 2px 4px rgba(0,0,0,0.3);\n}\n\n.mobility-indicator {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 10px;\n  margin: 10px 0;\n  font-weight: bold;\n}\n\n.status-dot {\n  width: 12px;\n  height: 12px;\n  border-radius: 50%;\n  animation: pulse 2s infinite;\n}\n\n.status-good { background: #4CAF50; }\n.status-warning { background: #FF9800; }\n.status-error { background: #F44336; }\n\n.history-section {\n  max-height: 200px;\n  overflow-y: auto;\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 10px;\n}\n\n.history-item {\n  background: rgba(33, 150, 243, 0.05);\n  border-radius: 6px;\n  padding: 8px;\n  margin: 5px 0;\n  font-size: 13px;\n  border-left: 3px solid #2196F3;\n}\n\n.report-section {\n  display: none;\n  margin-top: 20px;\n  padding-top: 20px;\n  border-top: 2px solid #eee;\n}\n\n.report-buttons {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n}\n\n.camera-error {\n  background: rgba(244, 67, 54, 0.1);\n  border: 2px solid rgba(244, 67, 54, 0.3);\n  border-radius: 10px;\n  padding: 15px;\n  text-align: center;\n  color: #d32f2f;\n  margin: 10px 0;\n}\n\n.achievement-notification {\n  position: fixed;\n  top: 20px;\n  right: 20px;\n  background: linear-gradient(45deg, #4CAF50, #45a049);\n  color: white;\n  padding: 15px 20px;\n  border-radius: 10px;\n  box-shadow: 0 4px 20px rgba(76, 175, 80, 0.4);\n  z-index: 1000;\n  max-width: 300px;\n  animation: slideInRight 0.5s ease-out;\n}\n\n@keyframes pulse {\n  0% { transform: scale(1); opacity: 1; }\n  50% { transform: scale(1.1); opacity: 0.7; }\n  100% { transform: scale(1); opacity: 1; }\n}\n\n@keyframes slideIn {\n  from { opacity: 0; transform: translateY(20px); }\n  to { opacity: 1; transform: translateY(0); }\n}\n\n@keyframes slideInRight {\n  from { transform: translateX(100%); opacity: 0; }\n  to { transform: translateX(0); opacity: 1; }\n}\n\n@keyframes slideOutRight {\n  from { transform: translateX(0); opacity: 1; }\n  to { transform: translateX(100%); opacity: 0; }\n}\n\n.slide-in {\n  animation: slideIn 0.3s ease-out;\n}\n\n@media (max-width: 768px) {\n  .container {\n    grid-template-columns: 1fr;\n    gap: 15px;\n    padding: 15px;\n  }\n  \n  .stats {\n    grid-template-columns: 1fr;\n  }\n  \n  canvas {\n    width: 100%;\n    max-width: 400px;\n    height: 400px;\n  }\n  \n  .camera-preview {\n    width: 120px;\n    height: 90px;\n  }\n}\n<\/style>\n<\/head>\n<body>\n\n<div class=\"container\">\n  <div class=\"game-section\">\n    <canvas id=\"gameCanvas\" width=\"500\" height=\"500\"><\/canvas>\n    \n    <div class=\"camera-preview\" id=\"cameraPreview\" style=\"display: none;\">\n      <div class=\"camera-status\" id=\"cameraStatus\">Sin c\u00e1mara<\/div>\n      <video id=\"cameraVideo\" autoplay muted><\/video>\n    <\/div>\n  <\/div>\n\n  <div class=\"dashboard\">\n    <div class=\"controls\" id=\"mainControls\">\n      <div class=\"control-group\">\n        <label for=\"armSelect\">Brazo a ejercitar:<\/label>\n        <select id=\"armSelect\">\n          <option value=\"right\">Brazo Derecho<\/option>\n          <option value=\"left\">Brazo Izquierdo<\/option>\n        <\/select>\n      <\/div>\n\n      <div class=\"control-group\">\n        <label for=\"levelSelect\">Nivel de dificultad:<\/label>\n        <select id=\"levelSelect\">\n          <option value=\"1\">Principiante (1)<\/option>\n          <option value=\"2\">F\u00e1cil (2)<\/option>\n          <option value=\"3\">Moderado (3)<\/option>\n          <option value=\"4\">Intermedio (4)<\/option>\n          <option value=\"5\">Avanzado (5)<\/option>\n        <\/select>\n      <\/div>\n\n      <div class=\"checkbox-group\">\n        <input type=\"checkbox\" id=\"audioToggle\" unlocked>\n        <label for=\"audioToggle\">Habilitar gu\u00eda de voz<\/label>\n      <\/div>\n\n      <button class=\"btn\" id=\"startBtn\">\n        Iniciar Rehabilitaci\u00f3n\n      <\/button>\n    <\/div>\n\n    <div class=\"calibration-panel\" id=\"calibrationPanel\" style=\"display: none;\">\n      <h4>Calibraci\u00f3n del Sistema<\/h4>\n      \n      <div class=\"control-group\">\n        <label for=\"sensitivity\">Sensibilidad:<\/label>\n        <input type=\"range\" id=\"sensitivity\" min=\"0.05\" max=\"0.3\" step=\"0.01\" value=\"0.15\">\n        <span id=\"sensitivityValue\">0.15<\/span>\n      <\/div>\n      \n      <div class=\"calibration-counter\" id=\"calibrationCounter\">3.0<\/div>\n      \n      <div class=\"mobility-indicator\">\n        <div class=\"status-dot status-warning\" id=\"statusDot\"><\/div>\n        <span id=\"mobilityText\">Preparando sensores...<\/span>\n      <\/div>\n    <\/div>\n\n    <div class=\"camera-error\" id=\"cameraError\" style=\"display: none;\">\n      <h4>Error de C\u00e1mara<\/h4>\n      <p>No se pudo acceder a la c\u00e1mara. Verifica los permisos del navegador.<\/p>\n      <button class=\"btn btn-warning\" id=\"retryCamera\">Reintentar<\/button>\n    <\/div>\n\n    <div class=\"stats\">\n      <div class=\"stat-card\">\n        <div class=\"stat-value\" id=\"scoreValue\">0<\/div>\n        <div class=\"stat-label\">Puntaje<\/div>\n      <\/div>\n      <div class=\"stat-card\">\n        <div class=\"stat-value\" id=\"levelValue\">1<\/div>\n        <div class=\"stat-label\">Nivel<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"message-area\" id=\"messageArea\">\n      <span>Bienvenido a Colibr\u00ed Rehab - Tu sistema de rehabilitaci\u00f3n inteligente<\/span>\n    <\/div>\n\n    <div class=\"progress-section\">\n      <h4>Progreso de Rehabilitaci\u00f3n<\/h4>\n      <div class=\"progress-bar\">\n        <div class=\"progress-fill\" id=\"progressFill\">\n          <div class=\"progress-text\" id=\"progressText\">0%<\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div>\n      <h4>Historial de Sesiones<\/h4>\n      <div class=\"history-section\" id=\"historySection\">\n        <div class=\"history-item\">\n          No hay sesiones previas. \u00a1Comienza tu primera sesi\u00f3n!\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"report-section\" id=\"reportSection\">\n      <h4>Generar Reporte<\/h4>\n      <div class=\"report-buttons\">\n        <button class=\"btn btn-success\" id=\"whatsappBtn\">\n          Enviar por WhatsApp\n        <\/button>\n        <button class=\"btn btn-danger\" id=\"emailBtn\">\n          Enviar por Email\n        <\/button>\n        <button class=\"btn btn-warning\" id=\"copyBtn\">\n          Copiar al Portapapeles\n        <\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n\/\/ Configuraci\u00f3n global\nconst CONFIG = {\n  CANVAS_WIDTH: 500,\n  CANVAS_HEIGHT: 500,\n  BIRD_SIZE: 15,\n  OBSTACLE_WIDTH: 50,\n  GRAVITY: 0.4,\n  CALIBRATION_TIME: 3000,\n  CALIBRATION_TOLERANCE: 40,\n  FPS: 60,\n  MOTION_THRESHOLD: 20,\n  SMOOTHING_FACTOR: 0.7,\n  MAX_MOTION_HISTORY: 30\n};\n\nconst GAME_STATES = {\n  MENU: 'menu',\n  TUTORIAL: 'tutorial', \n  CALIBRATION: 'calibration',\n  PLAYING: 'playing',\n  PAUSED: 'paused',\n  GAME_OVER: 'game_over'\n};\n\n\/\/ Variables globales\nlet gameState = GAME_STATES.MENU;\nlet canvas, ctx;\nlet gameData = {\n  score: 0,\n  level: 1,\n  selectedArm: 'right',\n  sensitivity: 0.15,\n  selectedLevel: 1\n};\n\nlet gameObjects = {\n  bird: { x: 100, y: 250, velocity: 0, rotation: 0 },\n  obstacles: [],\n  particles: []\n};\n\nlet calibration = {\n  targetY: 250,\n  currentY: 250,\n  startTime: null,\n  isCalibrated: false\n};\n\nlet audio = {\n  enabled: true,\n  voices: [],\n  currentVoice: null\n};\n\nlet session = {\n  startTime: null,\n  scores: [],\n  currentStreak: 0\n};\n\nlet cameraSystem = {\n  video: null,\n  canvas: null,\n  context: null,\n  stream: null,\n  isActive: false,\n  previousFrame: null,\n  smoothedMotion: 250,\n  motionHistory: [],\n  lastMotionTime: 0\n};\n\nlet simulation = {\n  armY: 250,\n  direction: 1,\n  speed: 1.5,\n  time: 0\n};\n\nlet achievements = {\n  unlocked: [],\n  definitions: {\n    FIRST_FLIGHT: { name: 'Primer Vuelo', description: 'Completa tu primera sesi\u00f3n', icon: 'target' },\n    SCORE_MASTER: { name: 'Maestro del Puntaje', description: 'Alcanza 50 puntos', icon: 'trophy' },\n    CONSISTENCY: { name: 'Constancia', description: 'Completa 5 sesiones', icon: 'muscle' },\n    LEVEL_EXPERT: { name: 'Experto', description: 'Alcanza el nivel 8', icon: 'star' },\n    CAMERA_MASTER: { name: 'Maestro de la C\u00e1mara', description: 'Completa 3 sesiones con c\u00e1mara', icon: 'camera' }\n  }\n};\n\n\/\/ Inicializaci\u00f3n\nfunction initializeApp() {\n  try {\n    console.log('Iniciando Colibr\u00ed Rehab...');\n    \n    canvas = document.getElementById('gameCanvas');\n    if (!canvas) {\n      throw new Error('Canvas no encontrado');\n    }\n    \n    ctx = canvas.getContext('2d');\n    if (!ctx) {\n      throw new Error('Contexto 2D no disponible');\n    }\n    \n    canvas.width = CONFIG.CANVAS_WIDTH;\n    canvas.height = CONFIG.CANVAS_HEIGHT;\n    \n    initializeAudio();\n    initializeCamera();\n    setupEventListeners();\n    updateUI();\n    \n    gameLoop();\n    \n    console.log('Aplicaci\u00f3n inicializada correctamente');\n    \n  } catch (error) {\n    console.error('Error de inicializaci\u00f3n:', error);\n    showMessage('Error: ' + error.message, 'error');\n  }\n}\n\n\/\/ Sistema de c\u00e1mara\nasync function initializeCamera() {\n  try {\n    console.log('Inicializando sistema de c\u00e1mara...');\n    \n    cameraSystem.video = document.getElementById('cameraVideo');\n    cameraSystem.canvas = document.createElement('canvas');\n    cameraSystem.context = cameraSystem.canvas.getContext('2d');\n    \n    cameraSystem.canvas.width = 320;\n    cameraSystem.canvas.height = 240;\n    \n    const constraints = {\n      video: {\n        width: { ideal: 640, max: 1280 },\n        height: { ideal: 480, max: 720 },\n        facingMode: 'user'\n      }\n    };\n    \n    cameraSystem.stream = await navigator.mediaDevices.getUserMedia(constraints);\n    cameraSystem.video.srcObject = cameraSystem.stream;\n    \n    await new Promise((resolve) => {\n      cameraSystem.video.onloadedmetadata = () => {\n        cameraSystem.video.play();\n        resolve();\n      };\n    });\n    \n    cameraSystem.isActive = true;\n    \n    document.getElementById('cameraPreview').style.display = 'block';\n    document.getElementById('cameraStatus').innerHTML = 'C\u00e1mara activa';\n    document.getElementById('cameraStatus').style.background = 'rgba(76, 175, 80, 0.8)';\n    document.getElementById('cameraError').style.display = 'none';\n    \n    console.log('C\u00e1mara inicializada correctamente');\n    showMessage('C\u00e1mara activada - Sistema listo para rehabilitaci\u00f3n');\n    \n    startMotionDetection();\n    \n  } catch (error) {\n    console.error('Error al inicializar c\u00e1mara:', error);\n    handleCameraError(error);\n  }\n}\n\nfunction handleCameraError(error) {\n  cameraSystem.isActive = false;\n  \n  document.getElementById('cameraError').style.display = 'block';\n  document.getElementById('cameraPreview').style.display = 'none';\n  \n  let errorMessage = 'Error desconocido';\n  \n  switch (error.name) {\n    case 'NotAllowedError':\n      errorMessage = 'Acceso a la c\u00e1mara denegado. Por favor, permite el acceso en tu navegador.';\n      break;\n    case 'NotFoundError':\n      errorMessage = 'No se encontr\u00f3 ninguna c\u00e1mara en tu dispositivo.';\n      break;\n    case 'NotReadableError':\n      errorMessage = 'La c\u00e1mara est\u00e1 siendo utilizada por otra aplicaci\u00f3n.';\n      break;\n    case 'OverconstrainedError':\n      errorMessage = 'La configuraci\u00f3n de c\u00e1mara solicitada no es compatible.';\n      break;\n    default:\n      errorMessage = error.message || 'Error desconocido';\n  }\n  \n  showMessage(`Error de c\u00e1mara: ${errorMessage}`, 'error');\n  speak(`Error de c\u00e1mara: ${errorMessage}`);\n  \n  activateSimulationMode();\n}\n\nfunction activateSimulationMode() {\n  console.log('Activando modo simulaci\u00f3n...');\n  showMessage('Usando modo simulaci\u00f3n - Movimiento autom\u00e1tico para demo', 'warning');\n  speak('C\u00e1mara no disponible, usando modo demostraci\u00f3n');\n}\n\nfunction startMotionDetection() {\n  if (!cameraSystem.isActive) return;\n  \n  const detectMotion = () => {\n    if (!cameraSystem.isActive || !cameraSystem.video.videoWidth) {\n      requestAnimationFrame(detectMotion);\n      return;\n    }\n    \n    const now = Date.now();\n    if (now - cameraSystem.lastMotionTime < 33) { \/\/ Limitar a ~30 FPS\n      requestAnimationFrame(detectMotion);\n      return;\n    }\n    cameraSystem.lastMotionTime = now;\n    \n    try {\n      cameraSystem.context.drawImage(\n        cameraSystem.video, \n        0, 0, \n        cameraSystem.canvas.width, \n        cameraSystem.canvas.height\n      );\n      \n      const currentFrame = cameraSystem.context.getImageData(\n        0, 0, \n        cameraSystem.canvas.width, \n        cameraSystem.canvas.height\n      );\n      \n      if (cameraSystem.previousFrame) {\n        const motionY = calculateVerticalMotion(currentFrame, cameraSystem.previousFrame);\n        updateArmPosition(motionY);\n      }\n      \n      cameraSystem.previousFrame = currentFrame;\n    } catch (error) {\n      console.warn('Error en detecci\u00f3n de movimiento:', error);\n    }\n    \n    requestAnimationFrame(detectMotion);\n  };\n  \n  detectMotion();\n}\n\nfunction calculateVerticalMotion(currentFrame, previousFrame) {\n  let totalMotion = 0;\n  let motionCount = 0;\n  let weightedY = 0;\n  \n  const data1 = currentFrame.data;\n  const data2 = previousFrame.data;\n  const width = currentFrame.width;\n  const height = currentFrame.height;\n  \n  const blockSize = 8;\n  \n  for (let y = 0; y < height - blockSize; y += blockSize) {\n    for (let x = 0; x < width - blockSize; x += blockSize) {\n      let blockMotion = 0;\n      \n      for (let by = 0; by < blockSize; by++) {\n        for (let bx = 0; bx < blockSize; bx++) {\n          const i = ((y + by) * width + (x + bx)) * 4;\n          \n          const gray1 = (data1[i] + data1[i + 1] + data1[i + 2]) \/ 3;\n          const gray2 = (data2[i] + data2[i + 1] + data2[i + 2]) \/ 3;\n          \n          blockMotion += Math.abs(gray1 - gray2);\n        }\n      }\n      \n      blockMotion \/= (blockSize * blockSize);\n      \n      if (blockMotion > CONFIG.MOTION_THRESHOLD) {\n        totalMotion += blockMotion;\n        weightedY += (y + blockSize \/ 2) * blockMotion;\n        motionCount++;\n      }\n    }\n  }\n  \n  if (motionCount > 0) {\n    const avgMotionY = weightedY \/ totalMotion;\n    return (avgMotionY \/ height) * CONFIG.CANVAS_HEIGHT;\n  }\n  \n  return cameraSystem.smoothedMotion;\n}\n\nfunction updateArmPosition(detectedY) {\n  if (!isFinite(detectedY)) {\n    detectedY = cameraSystem.smoothedMotion;\n  }\n  \n  cameraSystem.smoothedMotion = \n    CONFIG.SMOOTHING_FACTOR * cameraSystem.smoothedMotion + \n    (1 - CONFIG.SMOOTHING_FACTOR) * detectedY;\n  \n  cameraSystem.motionHistory.push(cameraSystem.smoothedMotion);\n  if (cameraSystem.motionHistory.length > CONFIG.MAX_MOTION_HISTORY) {\n    cameraSystem.motionHistory.shift();\n  }\n  \n  updateMobilityIndicator();\n}\n\nfunction updateMobilityIndicator() {\n  if (cameraSystem.motionHistory.length < 10) return;\n  \n  const recent = cameraSystem.motionHistory.slice(-10);\n  const avg = recent.reduce((a, b) => a + b, 0) \/ recent.length;\n  const variance = recent.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) \/ recent.length;\n  const mobility = Math.sqrt(variance);\n  \n  const statusDot = document.getElementById('statusDot');\n  const mobilityText = document.getElementById('mobilityText');\n  \n  if (statusDot && mobilityText) {\n    if (mobility > 15) {\n      statusDot.className = 'status-dot status-good';\n      mobilityText.textContent = 'Movimiento excelente';\n    } else if (mobility > 8) {\n      statusDot.className = 'status-dot status-warning';\n      mobilityText.textContent = 'Movimiento moderado';\n    } else {\n      statusDot.className = 'status-dot status-error';\n      mobilityText.textContent = 'Aumenta el movimiento';\n    }\n  }\n}\n\nfunction updateArmSimulation() {\n  if (cameraSystem.isActive) {\n    return cameraSystem.smoothedMotion;\n  }\n  \n  simulation.time += 0.016;\n  simulation.armY = CONFIG.CANVAS_HEIGHT \/ 2 + Math.sin(simulation.time) * 80 + Math.cos(simulation.time * 0.7) * 30;\n  simulation.armY = Math.max(50, Math.min(CONFIG.CANVAS_HEIGHT - 50, simulation.armY));\n  \n  return simulation.armY;\n}\n\n\/\/ Sistema de audio\nfunction initializeAudio() {\n  if ('speechSynthesis' in window) {\n    audio.enabled = true;\n    \n    const loadVoices = () => {\n      audio.voices = speechSynthesis.getVoices();\n      audio.currentVoice = audio.voices.find(voice => \n        voice.lang.startsWith('es')\n      ) || audio.voices[0];\n    };\n    \n    if (audio.voices.length === 0) {\n      speechSynthesis.addEventListener('voiceschanged', loadVoices);\n    } else {\n      loadVoices();\n    }\n  } else {\n    audio.enabled = false;\n    const audioToggle = document.getElementById('audioToggle');\n    if (audioToggle) {\n      audioToggle.disabled = true;\n    }\n  }\n}\n\nfunction speak(text, rate = 1.0, pitch = 1.0) {\n  if (!audio.enabled || !document.getElementById('audioToggle').checked) return;\n  \n  speechSynthesis.cancel();\n  \n  const utterance = new SpeechSynthesisUtterance(text);\n  if (audio.currentVoice) utterance.voice = audio.currentVoice;\n  utterance.rate = rate;\n  utterance.pitch = pitch;\n  utterance.volume = 0.8;\n  \n  speechSynthesis.speak(utterance);\n}\n\n\/\/ L\u00f3gica del juego\nfunction updateGame() {\n  switch (gameState) {\n    case GAME_STATES.MENU:\n      updateMenu();\n      break;\n      \n    case GAME_STATES.TUTORIAL:\n      updateTutorial();\n      break;\n      \n    case GAME_STATES.CALIBRATION:\n      updateCalibration();\n      break;\n      \n    case GAME_STATES.PLAYING:\n      updateGameplay();\n      break;\n      \n    case GAME_STATES.PAUSED:\n      updatePaused();\n      break;\n      \n    case GAME_STATES.GAME_OVER:\n      updateGameOver();\n      break;\n  }\n  \n  updateParticles();\n}\n\nfunction updateMenu() {\n  drawBackground();\n  \n  ctx.fillStyle = 'rgba(33, 150, 243, 0.8)';\n  ctx.font = 'bold 32px Arial';\n  ctx.textAlign = 'center';\n  ctx.fillText('COLIBR\u00cd REHAB', CONFIG.CANVAS_WIDTH \/ 2, 150);\n  \n  ctx.font = '18px Arial';\n  ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';\n  ctx.fillText('Sistema Inteligente de Rehabilitaci\u00f3n', CONFIG.CANVAS_WIDTH \/ 2, 180);\n  \n  drawBird(CONFIG.CANVAS_WIDTH \/ 2, CONFIG.CANVAS_HEIGHT \/ 2 + Math.sin(Date.now() \/ 1000) * 20, 0, 1);\n}\n\nfunction updateTutorial() {\n  drawBackground();\n  \n  const armY = updateArmSimulation();\n  drawBird(100, armY, 0, 0.8);\n  \n  ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';\n  ctx.font = 'bold 24px Arial';\n  ctx.textAlign = 'center';\n  ctx.fillText('TUTORIAL', CONFIG.CANVAS_WIDTH \/ 2, 50);\n  \n  ctx.font = '16px Arial';\n  const modeText = cameraSystem.isActive ? \n    'Mueve tu brazo - El colibr\u00ed te sigue' : \n    'Observa la demostraci\u00f3n autom\u00e1tica';\n  ctx.fillText(modeText, CONFIG.CANVAS_WIDTH \/ 2, 80);\n  \n  ctx.fillStyle = 'rgba(76, 175, 80, 0.6)';\n  ctx.fillRect(300, 50, CONFIG.OBSTACLE_WIDTH, 150);\n  ctx.fillRect(300, 300, CONFIG.OBSTACLE_WIDTH, 150);\n  \n  ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';\n  ctx.font = 'bold 14px Arial';\n  ctx.fillText('\u00a1Pasa por aqu\u00ed!', 325, 240);\n  \n  drawArrow(250, 225, 300, 225);\n}\n\nfunction updateCalibration() {\n  drawBackground();\n  \n  const armY = updateArmSimulation();\n  calibration.currentY = armY;\n  \n  const tolerance = CONFIG.CALIBRATION_TOLERANCE;\n  \n  ctx.fillStyle = 'rgba(76, 175, 80, 0.2)';\n  ctx.fillRect(0, calibration.targetY - tolerance, CONFIG.CANVAS_WIDTH, tolerance * 2);\n  \n  ctx.strokeStyle = '#4CAF50';\n  ctx.lineWidth = 4;\n  ctx.setLineDash([20, 10]);\n  ctx.beginPath();\n  ctx.moveTo(50, calibration.targetY);\n  ctx.lineTo(CONFIG.CANVAS_WIDTH - 50, calibration.targetY);\n  ctx.stroke();\n  ctx.setLineDash([]);\n  \n  drawBird(100, calibration.targetY, 0, 0.4, true);\n  drawBird(100, calibration.currentY, 0, 1, false);\n  \n  const distance = Math.abs(calibration.currentY - calibration.targetY);\n  const isInPosition = distance <= tolerance;\n  \n  if (isInPosition) {\n    if (!calibration.startTime) {\n      calibration.startTime = Date.now();\n      speak('Perfecto, mant\u00e9n esa posici\u00f3n');\n    }\n    \n    const elapsed = Date.now() - calibration.startTime;\n    const remaining = Math.max(0, CONFIG.CALIBRATION_TIME - elapsed);\n    const progress = elapsed \/ CONFIG.CALIBRATION_TIME;\n    \n    document.getElementById('calibrationCounter').textContent = \n      (remaining \/ 1000).toFixed(1);\n    \n    drawProgressCircle(CONFIG.CANVAS_WIDTH - 100, 100, 30, progress);\n    \n    if (remaining === 0) {\n      calibration.isCalibrated = true;\n      gameState = GAME_STATES.PLAYING;\n      startGame();\n    }\n  } else {\n    calibration.startTime = null;\n    document.getElementById('calibrationCounter').textContent = '3.0';\n    \n    const guidance = calibration.currentY < calibration.targetY ? \n      'Baja tu brazo' : 'Sube tu brazo';\n    \n    ctx.fillStyle = '#FF5722';\n    ctx.font = 'bold 18px Arial';\n    ctx.textAlign = 'center';\n    ctx.fillText(guidance, CONFIG.CANVAS_WIDTH \/ 2, calibration.targetY + 80);\n  }\n  \n  ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';\n  ctx.font = 'bold 20px Arial';\n  ctx.textAlign = 'center';\n  ctx.fillText('CALIBRACI\u00d3N', CONFIG.CANVAS_WIDTH \/ 2, 40);\n  \n  ctx.font = '14px Arial';\n  const instructionText = cameraSystem.isActive ? \n    'Mant\u00e9n el colibr\u00ed en la l\u00ednea verde por 3 segundos' :\n    'Calibraci\u00f3n autom\u00e1tica en progreso...';\n  ctx.fillText(instructionText, CONFIG.CANVAS_WIDTH \/ 2, 60);\n}\n\nfunction updateGameplay() {\n  drawBackground();\n  \n  if (!session.startTime) {\n    session.startTime = Date.now();\n  }\n  \n  const armY = updateArmSimulation();\n  const targetY = calibration.targetY + (armY - calibration.targetY) * gameData.sensitivity;\n  \n  gameObjects.bird.velocity += (targetY - gameObjects.bird.y) * 0.15;\n  gameObjects.bird.velocity *= 0.9;\n  gameObjects.bird.y += gameObjects.bird.velocity;\n  \n  gameObjects.bird.y = Math.max(CONFIG.BIRD_SIZE, \n    Math.min(CONFIG.CANVAS_HEIGHT - CONFIG.BIRD_SIZE, gameObjects.bird.y));\n  \n  gameObjects.bird.rotation = gameObjects.bird.velocity * 0.1;\n  \n  updateObstacles();\n  \n  drawObstacles();\n  drawBird(gameObjects.bird.x, gameObjects.bird.y, gameObjects.bird.rotation);\n  drawParticles();\n  \n  if (checkCollisions()) {\n    endGame();\n  }\n  \n  updateGameUI();\n}\n\nfunction updatePaused() {\n  ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';\n  ctx.fillRect(0, 0, CONFIG.CANVAS_WIDTH, CONFIG.CANVAS_HEIGHT);\n  \n  ctx.fillStyle = 'white';\n  ctx.font = 'bold 32px Arial';\n  ctx.textAlign = 'center';\n  ctx.fillText('PAUSADO', CONFIG.CANVAS_WIDTH \/ 2, CONFIG.CANVAS_HEIGHT \/ 2);\n  \n  ctx.font = '16px Arial';\n  ctx.fillText('Presiona ESPACIO para continuar', \n    CONFIG.CANVAS_WIDTH \/ 2, CONFIG.CANVAS_HEIGHT \/ 2 + 40);\n}\n\nfunction updateGameOver() {\n  drawBackground();\n  \n  ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';\n  ctx.fillRect(0, 0, CONFIG.CANVAS_WIDTH, CONFIG.CANVAS_HEIGHT);\n  \n  ctx.fillStyle = 'white';\n  ctx.font = 'bold 28px Arial';\n  ctx.textAlign = 'center';\n  ctx.fillText('SESI\u00d3N COMPLETADA', CONFIG.CANVAS_WIDTH \/ 2, 200);\n  \n  ctx.font = '20px Arial';\n  ctx.fillText(`Puntaje Final: ${gameData.score}`, CONFIG.CANVAS_WIDTH \/ 2, 250);\n  ctx.fillText(`Nivel Alcanzado: ${gameData.level}`, CONFIG.CANVAS_WIDTH \/ 2, 280);\n  \n  let evaluation = '';\n  if (gameData.score >= 50) evaluation = 'EXCEPCIONAL!';\n  else if (gameData.score >= 30) evaluation = 'EXCELENTE!';\n  else if (gameData.score >= 15) evaluation = 'BUEN TRABAJO!';\n  else evaluation = 'BUEN COMIENZO!';\n  \n  ctx.font = 'bold 24px Arial';\n  ctx.fillStyle = '#4CAF50';\n  ctx.fillText(evaluation, CONFIG.CANVAS_WIDTH \/ 2, 330);\n}\n\nfunction updateObstacles() {\n  const speed = 2 + gameData.level * 0.3;\n  \n  for (let i = gameObjects.obstacles.length - 1; i >= 0; i--) {\n    const obstacle = gameObjects.obstacles[i];\n    obstacle.x -= speed;\n    \n    if (!obstacle.passed && obstacle.x + CONFIG.OBSTACLE_WIDTH < gameObjects.bird.x) {\n      obstacle.passed = true;\n      gameData.score++;\n      session.currentStreak++;\n      \n      createCelebrationParticles(gameObjects.bird.x, gameObjects.bird.y);\n      updateLevel();\n      playScoreSound();\n    }\n    \n    if (obstacle.x + CONFIG.OBSTACLE_WIDTH < 0) {\n      gameObjects.obstacles.splice(i, 1);\n    }\n  }\n  \n  const lastObstacle = gameObjects.obstacles[gameObjects.obstacles.length - 1];\n  const spacing = 300 - gameData.level * 15;\n  \n  if (!lastObstacle || lastObstacle.x < CONFIG.CANVAS_WIDTH - spacing) {\n    createObstacle();\n  }\n}\n\nfunction createObstacle() {\n  const minGap = 120 + gameData.selectedLevel * 10;\n  const maxGap = 180 + gameData.selectedLevel * 5;\n  const gap = minGap + Math.random() * (maxGap - minGap);\n  \n  const minTop = 80;\n  const maxTop = CONFIG.CANVAS_HEIGHT - gap - 80;\n  const topHeight = minTop + Math.random() * (maxTop - minTop);\n  \n  gameObjects.obstacles.push({\n    x: CONFIG.CANVAS_WIDTH + 50,\n    topHeight: topHeight,\n    bottomY: topHeight + gap,\n    width: CONFIG.OBSTACLE_WIDTH,\n    passed: false,\n    color: `hsl(${120 + Math.random() * 40}, 70%, 50%)`\n  });\n}\n\nfunction checkCollisions() {\n  const bird = gameObjects.bird;\n  const birdRadius = CONFIG.BIRD_SIZE;\n  \n  if (bird.y - birdRadius <= 0 || bird.y + birdRadius >= CONFIG.CANVAS_HEIGHT) {\n    return true;\n  }\n  \n  for (const obstacle of gameObjects.obstacles) {\n    if (bird.x + birdRadius > obstacle.x && \n        bird.x - birdRadius < obstacle.x + obstacle.width) {\n      \n      if (bird.y - birdRadius < obstacle.topHeight || \n          bird.y + birdRadius > obstacle.bottomY) {\n        return true;\n      }\n    }\n  }\n  \n  return false;\n}\n\nfunction updateLevel() {\n  const newLevel = Math.min(Math.floor(gameData.score \/ 5) + 1, 10);\n  \n  if (newLevel > gameData.level) {\n    gameData.level = newLevel;\n    speak(`\u00a1Nivel ${newLevel} alcanzado!`, 0.9, 1.3);\n    createLevelUpEffect();\n  }\n}\n\nfunction createLevelUpEffect() {\n  for (let i = 0; i < 20; i++) {\n    gameObjects.particles.push({\n      x: CONFIG.CANVAS_WIDTH \/ 2 + (Math.random() - 0.5) * 200,\n      y: CONFIG.CANVAS_HEIGHT \/ 2 + (Math.random() - 0.5) * 100,\n      vx: (Math.random() - 0.5) * 8,\n      vy: -Math.random() * 6 - 2,\n      life: 120,\n      maxLife: 120,\n      color: '#FFD700',\n      size: 6,\n      type: 'star'\n    });\n  }\n}\n\nfunction updateParticles() {\n  for (let i = gameObjects.particles.length - 1; i >= 0; i--) {\n    const particle = gameObjects.particles[i];\n    \n    particle.x += particle.vx;\n    particle.y += particle.vy;\n    particle.vy += 0.2;\n    particle.vx *= 0.99;\n    particle.life--;\n    \n    if (particle.life <= 0) {\n      gameObjects.particles.splice(i, 1);\n    }\n  }\n}\n\nfunction createCelebrationParticles(x, y) {\n  const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8'];\n  \n  for (let i = 0; i < 8; i++) {\n    gameObjects.particles.push({\n      x: x + (Math.random() - 0.5) * 30,\n      y: y + (Math.random() - 0.5) * 30,\n      vx: (Math.random() - 0.5) * 6,\n      vy: (Math.random() - 0.5) * 6,\n      life: 60,\n      maxLife: 60,\n      color: colors[Math.floor(Math.random() * colors.length)],\n      size: 3 + Math.random() * 3,\n      type: 'circle'\n    });\n  }\n}\n\n\/\/ Funciones de dibujo\nfunction drawBackground() {\n  const gradient = ctx.createLinearGradient(0, 0, 0, CONFIG.CANVAS_HEIGHT);\n  gradient.addColorStop(0, '#87CEEB');\n  gradient.addColorStop(1, '#E0F6FF');\n  \n  ctx.fillStyle = gradient;\n  ctx.fillRect(0, 0, CONFIG.CANVAS_WIDTH, CONFIG.CANVAS_HEIGHT);\n  \n  drawCloud(80, 80, 0.8);\n  drawCloud(350, 120, 0.6);\n  drawCloud(200, 400, 0.7);\n}\n\nfunction drawCloud(x, y, alpha) {\n  ctx.save();\n  ctx.globalAlpha = alpha;\n  ctx.fillStyle = 'white';\n  \n  ctx.beginPath();\n  ctx.arc(x, y, 25, 0, 2 * Math.PI);\n  ctx.arc(x + 25, y, 35, 0, 2 * Math.PI);\n  ctx.arc(x + 50, y, 25, 0, 2 * Math.PI);\n  ctx.arc(x + 30, y - 25, 25, 0, 2 * Math.PI);\n  ctx.fill();\n  \n  ctx.restore();\n}\n\nfunction drawBird(x, y, rotation, alpha = 1, isTarget = false) {\n  ctx.save();\n  ctx.translate(x, y);\n  ctx.rotate(rotation);\n  ctx.globalAlpha = alpha;\n  \n  if (!isTarget) {\n    ctx.save();\n    ctx.translate(3, 3);\n    ctx.globalAlpha = alpha * 0.3;\n    ctx.fillStyle = 'rgba(0,0,0,0.3)';\n    ctx.beginPath();\n    ctx.ellipse(0, 0, CONFIG.BIRD_SIZE, CONFIG.BIRD_SIZE * 0.7, 0, 0, 2 * Math.PI);\n    ctx.fill();\n    ctx.restore();\n  }\n  \n  const bodyGradient = ctx.createRadialGradient(0, 0, 5, 0, 0, CONFIG.BIRD_SIZE);\n  if (isTarget) {\n    bodyGradient.addColorStop(0, '#90EE90');\n    bodyGradient.addColorStop(1, '#32CD32');\n  } else {\n    bodyGradient.addColorStop(0, '#FF6B6B');\n    bodyGradient.addColorStop(1, '#FF8E8E');\n  }\n  \n  ctx.fillStyle = bodyGradient;\n  ctx.beginPath();\n  ctx.ellipse(0, 0, CONFIG.BIRD_SIZE, CONFIG.BIRD_SIZE * 0.8, 0, 0, 2 * Math.PI);\n  ctx.fill();\n  \n  const wingFlap = Math.sin(Date.now() \/ 100) * 0.5;\n  \n  ctx.save();\n  ctx.rotate(wingFlap);\n  ctx.fillStyle = isTarget ? '#32CD32' : '#4ECDC4';\n  ctx.beginPath();\n  ctx.ellipse(-8, -8, 12, 6, Math.PI \/ 4, 0, 2 * Math.PI);\n  ctx.fill();\n  ctx.restore();\n  \n  ctx.save();\n  ctx.rotate(-wingFlap * 0.8);\n  ctx.fillStyle = isTarget ? '#228B22' : '#45B7D1';\n  ctx.beginPath();\n  ctx.ellipse(-6, 6, 10, 4, -Math.PI \/ 6, 0, 2 * Math.PI);\n  ctx.fill();\n  ctx.restore();\n  \n  ctx.fillStyle = '#FFA500';\n  ctx.beginPath();\n  ctx.moveTo(CONFIG.BIRD_SIZE - 2, 0);\n  ctx.lineTo(CONFIG.BIRD_SIZE + 8, -2);\n  ctx.lineTo(CONFIG.BIRD_SIZE + 8, 2);\n  ctx.closePath();\n  ctx.fill();\n  \n  ctx.fillStyle = 'black';\n  ctx.beginPath();\n  ctx.arc(4, -4, 3, 0, 2 * Math.PI);\n  ctx.fill();\n  \n  ctx.fillStyle = 'white';\n  ctx.beginPath();\n  ctx.arc(5, -5, 1, 0, 2 * Math.PI);\n  ctx.fill();\n  \n  ctx.restore();\n}\n\nfunction drawObstacles() {\n  gameObjects.obstacles.forEach(obstacle => {\n    const gradient = ctx.createLinearGradient(\n      obstacle.x, 0, obstacle.x + obstacle.width, 0\n    );\n    gradient.addColorStop(0, obstacle.color);\n    gradient.addColorStop(0.5, '#228B22');\n    gradient.addColorStop(1, obstacle.color);\n    \n    ctx.fillStyle = gradient;\n    ctx.strokeStyle = '#006400';\n    ctx.lineWidth = 2;\n    \n    ctx.fillRect(obstacle.x, 0, obstacle.width, obstacle.topHeight);\n    ctx.strokeRect(obstacle.x, 0, obstacle.width, obstacle.topHeight);\n    \n    ctx.fillRect(obstacle.x, obstacle.bottomY, obstacle.width, \n      CONFIG.CANVAS_HEIGHT - obstacle.bottomY);\n    ctx.strokeRect(obstacle.x, obstacle.bottomY, obstacle.width, \n      CONFIG.CANVAS_HEIGHT - obstacle.bottomY);\n    \n    ctx.save();\n    ctx.globalAlpha = 0.3;\n    ctx.fillStyle = 'white';\n    ctx.fillRect(obstacle.x + 2, 2, obstacle.width - 4, obstacle.topHeight - 4);\n    ctx.fillRect(obstacle.x + 2, obstacle.bottomY + 2, obstacle.width - 4, \n      CONFIG.CANVAS_HEIGHT - obstacle.bottomY - 4);\n    ctx.restore();\n  });\n}\n\nfunction drawParticles() {\n  gameObjects.particles.forEach(particle => {\n    ctx.save();\n    ctx.globalAlpha = particle.life \/ particle.maxLife;\n    ctx.fillStyle = particle.color;\n    \n    if (particle.type === 'star') {\n      drawStar(particle.x, particle.y, particle.size);\n    } else {\n      ctx.beginPath();\n      ctx.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI);\n      ctx.fill();\n    }\n    \n    ctx.restore();\n  });\n}\n\nfunction drawStar(x, y, size) {\n  ctx.save();\n  ctx.translate(x, y);\n  \n  ctx.beginPath();\n  for (let i = 0; i < 5; i++) {\n    ctx.lineTo(Math.cos(i * 2 * Math.PI \/ 5) * size, \n              Math.sin(i * 2 * Math.PI \/ 5) * size);\n    ctx.lineTo(Math.cos((i + 0.5) * 2 * Math.PI \/ 5) * size * 0.5,\n              Math.sin((i + 0.5) * 2 * Math.PI \/ 5) * size * 0.5);\n  }\n  ctx.closePath();\n  ctx.fill();\n  \n  ctx.restore();\n}\n\nfunction drawProgressCircle(x, y, radius, progress) {\n  ctx.strokeStyle = '#E0E0E0';\n  ctx.lineWidth = 6;\n  ctx.beginPath();\n  ctx.arc(x, y, radius, 0, 2 * Math.PI);\n  ctx.stroke();\n  \n  ctx.strokeStyle = '#4CAF50';\n  ctx.beginPath();\n  ctx.arc(x, y, radius, -Math.PI \/ 2, -Math.PI \/ 2 + (2 * Math.PI * progress));\n  ctx.stroke();\n  \n  ctx.fillStyle = '#4CAF50';\n  ctx.font = 'bold 14px Arial';\n  ctx.textAlign = 'center';\n  ctx.fillText(Math.round(progress * 100) + '%', x, y + 5);\n}\n\nfunction drawArrow(x1, y1, x2, y2) {\n  const headlen = 10;\n  const angle = Math.atan2(y2 - y1, x2 - x1);\n  \n  ctx.strokeStyle = '#FF9800';\n  ctx.lineWidth = 3;\n  ctx.beginPath();\n  ctx.moveTo(x1, y1);\n  ctx.lineTo(x2, y2);\n  ctx.lineTo(x2 - headlen * Math.cos(angle - Math.PI \/ 6), \n            y2 - headlen * Math.sin(angle - Math.PI \/ 6));\n  ctx.moveTo(x2, y2);\n  ctx.lineTo(x2 - headlen * Math.cos(angle + Math.PI \/ 6),\n            y2 - headlen * Math.sin(angle + Math.PI \/ 6));\n  ctx.stroke();\n}\n\n\/\/ Control de juego\nfunction startGame() {\n  gameState = GAME_STATES.PLAYING;\n  session.startTime = Date.now();\n  \n  gameObjects.bird = { \n    x: 100, \n    y: calibration.targetY, \n    velocity: 0, \n    rotation: 0 \n  };\n  gameObjects.obstacles = [];\n  gameObjects.particles = [];\n  \n  gameData.score = 0;\n  gameData.level = 1;\n  session.currentStreak = 0;\n  \n  const modeMessage = cameraSystem.isActive ? \n    'Controla el colibr\u00ed con tu brazo. \u00a1A volar!' :\n    'Sesi\u00f3n iniciada en modo demostraci\u00f3n';\n  \n  speak(modeMessage, 0.9, 1.2);\n  showMessage('Sesi\u00f3n de rehabilitaci\u00f3n iniciada');\n}\n\nfunction pauseGame() {\n  if (gameState === GAME_STATES.PLAYING) {\n    gameState = GAME_STATES.PAUSED;\n    speak('Juego pausado');\n    showMessage('Juego pausado. Presiona ESPACIO para continuar.');\n  }\n}\n\nfunction resumeGame() {\n  if (gameState === GAME_STATES.PAUSED) {\n    gameState = GAME_STATES.PLAYING;\n    speak('Continuando rehabilitaci\u00f3n');\n    showMessage('Continuando la sesi\u00f3n');\n  }\n}\n\nfunction endGame() {\n  gameState = GAME_STATES.GAME_OVER;\n  \n  session.scores.push({\n    score: gameData.score,\n    level: gameData.level,\n    arm: gameData.selectedArm,\n    date: new Date(),\n    duration: session.startTime ? Math.floor((Date.now() - session.startTime) \/ 1000) : 0,\n    streak: session.currentStreak,\n    cameraUsed: cameraSystem.isActive\n  });\n  \n  updateSessionHistory();\n  updateProgressBar();\n  showReportSection();\n  checkAchievements();\n  \n  let message, audioMessage;\n  if (gameData.score >= 50) {\n    message = 'RECUPERACI\u00d3N EXCEPCIONAL';\n    audioMessage = `Incre\u00edble! Has alcanzado ${gameData.score} puntos. Tu recuperaci\u00f3n es excepcional.`;\n  } else if (gameData.score >= 30) {\n    message = 'EXCELENTE PROGRESO';\n    audioMessage = `Excelente trabajo! ${gameData.score} puntos alcanzados.`;\n  } else if (gameData.score >= 15) {\n    message = 'BUEN AVANCE';\n    audioMessage = `Muy bien! Has conseguido ${gameData.score} puntos.`;\n  } else {\n    message = 'BUEN COMIENZO';\n    audioMessage = `Felicitaciones! ${gameData.score} puntos en tu sesi\u00f3n.`;\n  }\n  \n  showMessage(message);\n  speak(audioMessage, 0.8, 1.1);\n  \n  setTimeout(() => {\n    if (confirm('\u00bfDeseas realizar otra sesi\u00f3n de rehabilitaci\u00f3n?')) {\n      resetToCalibration();\n    } else {\n      showReportSection();\n;\n    }\n  }, 3000);\n}\n\nfunction resetToMenu() {\n  gameState = GAME_STATES.MENU;\n  document.getElementById('startBtn').style.display = 'block';\n  document.getElementById('calibrationPanel').style.display = 'none';\n  document.getElementById('reportSection').style.display = 'none';\n  \n  calibration.isCalibrated = false;\n  calibration.startTime = null;\n  \n  showMessage('Gracias por usar Colibr\u00ed Rehab');\n  speak('Sesi\u00f3n completada. Excelente trabajo en tu rehabilitaci\u00f3n');\n}\n\nfunction resetToCalibration() {\n  gameState = GAME_STATES.CALIBRATION;\n  \n  calibration.isCalibrated = false;\n  calibration.startTime = null;\n  calibration.targetY = CONFIG.CANVAS_HEIGHT \/ 2;\n  \n  showMessage('Comenzando nueva calibraci\u00f3n...');\n  speak('Iniciando nueva sesi\u00f3n. Comenzamos con la calibraci\u00f3n.');\n}\n\n\/\/ Interfaz de usuario\nfunction updateUI() {\n  const scoreEl = document.getElementById('scoreValue');\n  const levelEl = document.getElementById('levelValue');\n  const sensitivityEl = document.getElementById('sensitivityValue');\n  \n  if (scoreEl) scoreEl.textContent = gameData.score;\n  if (levelEl) levelEl.textContent = gameData.level;\n  if (sensitivityEl) sensitivityEl.textContent = gameData.sensitivity.toFixed(2);\n}\n\nfunction updateGameUI() {\n  updateUI();\n  \n  const statusDot = document.getElementById('statusDot');\n  const mobilityText = document.getElementById('mobilityText');\n  \n  if (statusDot && mobilityText && cameraSystem.motionHistory.length > 5) {\n    const recentMotion = cameraSystem.motionHistory.slice(-5);\n    const variance = calculateVariance(recentMotion);\n    \n    if (variance > 15) {\n      statusDot.className = 'status-dot status-good';\n      mobilityText.textContent = 'Movimiento excelente';\n    } else if (variance > 5) {\n      statusDot.className = 'status-dot status-warning';\n      mobilityText.textContent = 'Movimiento moderado';\n    } else {\n      statusDot.className = 'status-dot status-error';\n      mobilityText.textContent = 'Aumenta el movimiento';\n    }\n  }\n}\n\nfunction calculateVariance(arr) {\n  if (arr.length === 0) return 0;\n  const mean = arr.reduce((a, b) => a + b, 0) \/ arr.length;\n  return arr.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) \/ arr.length;\n}\n\nfunction showMessage(text, type = 'info') {\n  const messageArea = document.getElementById('messageArea');\n  if (!messageArea) return;\n  \n  messageArea.innerHTML = `<span>${text}<\/span>`;\n  messageArea.className = `message-area ${type}`;\n  \n  setTimeout(() => {\n    if (messageArea.innerHTML === `<span>${text}<\/span>`) {\n      messageArea.innerHTML = '<span>Sistema listo para rehabilitaci\u00f3n<\/span>';\n      messageArea.className = 'message-area';\n    }\n  }, 5000);\n}\n\nfunction updateSessionHistory() {\n  const historySection = document.getElementById('historySection');\n  if (!historySection) return;\n  \n  historySection.innerHTML = '';\n  \n  if (session.scores.length === 0) {\n    historySection.innerHTML = '<div class=\"history-item\">No hay sesiones previas<\/div>';\n    return;\n  }\n  \n  session.scores.slice(-5).forEach((score, index) => {\n    const date = score.date.toLocaleDateString('es-ES');\n    const time = score.date.toLocaleTimeString('es-ES', { \n      hour: '2-digit', \n      minute: '2-digit' \n    });\n    \n    const cameraIcon = score.cameraUsed ? '(C\u00e1mara)' : '(Demo)';\n    \n    const historyItem = document.createElement('div');\n    historyItem.className = 'history-item slide-in';\n    historyItem.innerHTML = `\n      <strong>Sesi\u00f3n ${session.scores.length - session.scores.slice(-5).length + index + 1}<\/strong> ${cameraIcon}<br>\n      ${date} ${time}<br>\n      ${score.score} pts | Nivel ${score.level} | ${score.duration}s<br>\n      ${score.arm === 'right' ? 'Brazo Derecho' : 'Brazo Izquierdo'} | Racha: ${score.streak}\n    `;\n    \n    historySection.appendChild(historyItem);\n  });\n}\n\nfunction updateProgressBar() {\n  if (session.scores.length === 0) return;\n  \n  const recentScores = session.scores.slice(-5);\n  const avgScore = recentScores.reduce((sum, s) => sum + s.score, 0) \/ recentScores.length;\n  const progress = Math.min(100, (avgScore \/ 40) * 100);\n  \n  const progressFill = document.getElementById('progressFill');\n  const progressText = document.getElementById('progressText');\n  \n  if (progressFill && progressText) {\n    progressFill.style.width = progress + '%';\n    progressText.textContent = `${Math.round(progress)}% - ${avgScore.toFixed(1)} pts promedio`;\n  }\n}\n\nfunction showReportSection() {\n  const reportSection = document.getElementById('reportSection');\n  if (reportSection) {\n    reportSection.style.display = 'block';\n  }\n}\n\n\/\/ Sistema de reportes\nfunction generateReport() {\n  if (session.scores.length === 0) return 'No hay datos de sesiones disponibles.';\n  \n  const now = new Date();\n  const totalSessions = session.scores.length;\n  const scores = session.scores.map(s => s.score);\n  const levels = session.scores.map(s => s.level);\n  const durations = session.scores.map(s => s.duration);\n  const cameraUsage = session.scores.filter(s => s.cameraUsed).length;\n  \n  const avgScore = (scores.reduce((a, b) => a + b, 0) \/ totalSessions).toFixed(1);\n  const maxScore = Math.max(...scores);\n  const maxLevel = Math.max(...levels);\n  const totalTime = durations.reduce((a, b) => a + b, 0);\n  const avgDuration = Math.round(totalTime \/ totalSessions);\n  \n  const improvement = totalSessions >= 3 ? \n    ((scores.slice(-3).reduce((a, b) => a + b, 0) \/ 3) - \n     (scores.slice(0, 3).reduce((a, b) => a + b, 0) \/ Math.min(3, totalSessions))) : 0;\n  \n  let progressLevel, recommendation;\n  if (avgScore >= 35) {\n    progressLevel = \"Excelente\";\n    recommendation = \"Felicitaciones! Mant\u00e9n la constancia para consolidar tu recuperaci\u00f3n.\";\n  } else if (avgScore >= 25) {\n    progressLevel = \"Avanzado\";\n    recommendation = \"Muy buen progreso! Contin\u00faa con sesiones regulares.\";\n  } else if (avgScore >= 15) {\n    progressLevel = \"Intermedio\";\n    recommendation = \"Buen avance. Aumenta gradualmente la frecuencia de las sesiones.\";\n  } else {\n    progressLevel = \"B\u00e1sico\";\n    recommendation = \"Progreso inicial positivo. La constancia es clave.\";\n  }\n  \n  return `REPORTE COLIBR\u00cd REHAB - REHABILITACI\u00d3N INTELIGENTE\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nFecha: ${now.toLocaleDateString('es-ES')} | ${now.toLocaleTimeString('es-ES')}\n\nINFORMACI\u00d3N DEL PACIENTE:\n\u2022 Extremidad: ${gameData.selectedArm === 'right' ? 'Brazo Derecho' : 'Brazo Izquierdo'}\n\u2022 Total de sesiones: ${totalSessions}\n\u2022 Sesiones con c\u00e1mara: ${cameraUsage}\/${totalSessions} (${Math.round(cameraUsage\/totalSessions*100)}%)\n\u2022 Tiempo total de rehabilitaci\u00f3n: ${Math.round(totalTime \/ 60)} minutos\n\nM\u00c9TRICAS DE RENDIMIENTO:\n\u2022 Puntaje promedio: ${avgScore} puntos\n\u2022 Puntaje m\u00e1ximo: ${maxScore} puntos\n\u2022 Nivel m\u00e1ximo alcanzado: ${maxLevel}\n\u2022 Duraci\u00f3n promedio por sesi\u00f3n: ${avgDuration} segundos\n\u2022 Mejora promedio: ${improvement > 0 ? '+' : ''}${improvement.toFixed(1)} puntos\n\nEVALUACI\u00d3N DEL PROGRESO:\n\u2022 Nivel de recuperaci\u00f3n: ${progressLevel}\n\u2022 Estado actual: ${avgScore >= 30 ? 'Recuperaci\u00f3n avanzada' : avgScore >= 15 ? 'Progreso constante' : 'Desarrollo inicial'}\n\nRECOMENDACI\u00d3N M\u00c9DICA:\n${recommendation}\n\nHISTORIAL DETALLADO:\n${session.scores.map((s, i) => {\n  const date = s.date.toLocaleDateString('es-ES');\n  const time = s.date.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });\n  const mode = s.cameraUsed ? 'C\u00e1mara' : 'Demo';\n  return `${i + 1}. ${date} ${time} ${mode} - ${s.score} pts (Nivel ${s.level}) - ${s.duration}s - Racha: ${s.streak}`;\n}).join('\\n')}\n\nPR\u00d3XIMOS OBJETIVOS:\n\u2022 ${avgScore < 20 ? 'Alcanzar 20 puntos promedio' : avgScore < 35 ? 'Mantener consistencia en nivel 5+' : 'Optimizar tiempo de recuperaci\u00f3n'}\n\u2022 Frecuencia recomendada: ${avgScore < 15 ? 'Diaria (10-15 min)' : 'Interdiaria (15-20 min)'}\n\u2022 Uso de c\u00e1mara: ${cameraUsage < totalSessions ? 'Activar c\u00e1mara para mejor precisi\u00f3n' : 'Continuar con detecci\u00f3n de movimiento'}\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nGenerado por Colibr\u00ed Rehab v2.1 - Sistema con Detecci\u00f3n de Movimiento\nPara m\u00e1s informaci\u00f3n: soporte@colibrirehab.com`;\n}\n\n\/\/ Efectos de sonido\nfunction playScoreSound() {\n  if (typeof AudioContext === 'undefined' && typeof webkitAudioContext === 'undefined') {\n    return;\n  }\n  \n  try {\n    const audioContext = new (AudioContext || webkitAudioContext)();\n    const oscillator = audioContext.createOscillator();\n    const gainNode = audioContext.createGain();\n    \n    oscillator.connect(gainNode);\n    gainNode.connect(audioContext.destination);\n    \n    oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime);\n    oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1);\n    oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2);\n    \n    gainNode.gain.setValueAtTime(0, audioContext.currentTime);\n    gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + 0.05);\n    gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.3);\n    \n    oscillator.start(audioContext.currentTime);\n    oscillator.stop(audioContext.currentTime + 0.3);\n  } catch (error) {\n    console.log('Audio Context no disponible:', error);\n  }\n}\n\n\/\/ Sistema de logros\nfunction checkAchievements() {\n  const newAchievements = [];\n  \n  if (session.scores.length >= 1 && !achievements.unlocked.includes('FIRST_FLIGHT')) {\n    newAchievements.push('FIRST_FLIGHT');\n  }\n  \n  if (gameData.score >= 50 && !achievements.unlocked.includes('SCORE_MASTER')) {\n    newAchievements.push('SCORE_MASTER');\n  }\n  \n  if (session.scores.length >= 5 && !achievements.unlocked.includes('CONSISTENCY')) {\n    newAchievements.push('CONSISTENCY');\n  }\n  \n  if (gameData.level >= 8 && !achievements.unlocked.includes('LEVEL_EXPERT')) {\n    newAchievements.push('LEVEL_EXPERT');\n  }\n  \n  const cameraSessionsCount = session.scores.filter(s => s.cameraUsed).length;\n  if (cameraSessionsCount >= 3 && !achievements.unlocked.includes('CAMERA_MASTER')) {\n    newAchievements.push('CAMERA_MASTER');\n  }\n  \n  newAchievements.forEach(achievementId => {\n    unlockAchievement(achievementId);\n  });\n}\n\nfunction unlockAchievement(achievementId) {\n  if (!achievements.unlocked.includes(achievementId)) {\n    achievements.unlocked.push(achievementId);\n    \n    const achievement = achievements.definitions[achievementId];\n    if (achievement) {\n      showAchievementNotification(achievement);\n      speak(`\u00a1Logro desbloqueado! ${achievement.name}`);\n    }\n  }\n}\n\nfunction showAchievementNotification(achievement) {\n  const notification = document.createElement('div');\n  notification.className = 'achievement-notification';\n  \n  notification.innerHTML = `\n    <div style=\"font-weight: bold; margin-bottom: 5px;\">\n      LOGRO DESBLOQUEADO!\n    <\/div>\n    <div style=\"font-size: 16px; margin-bottom: 3px;\">${achievement.name}<\/div>\n    <div style=\"font-size: 12px; opacity: 0.9;\">${achievement.description}<\/div>\n  `;\n  \n  document.body.appendChild(notification);\n  \n  setTimeout(() => {\n    notification.style.animation = 'slideOutRight 0.5s ease-in forwards';\n    setTimeout(() => {\n      if (document.body.contains(notification)) {\n        document.body.removeChild(notification);\n      }\n    }, 500);\n  }, 4000);\n}\n\n\/\/ Event listeners\nfunction setupEventListeners() {\n  const startBtn = document.getElementById('startBtn');\n  if (startBtn) {\n    startBtn.addEventListener('click', async () => {\n      gameData.selectedArm = document.getElementById('armSelect').value;\n      gameData.selectedLevel = parseInt(document.getElementById('levelSelect').value);\n      gameData.sensitivity = parseFloat(document.getElementById('sensitivity').value);\n      \n      if (!cameraSystem.isActive) {\n        await initializeCamera();\n      }\n      \n      gameState = GAME_STATES.TUTORIAL;\n      startBtn.style.display = 'none';\n      \n      const tutorialMessage = cameraSystem.isActive ? \n        'Tutorial activo - Mueve tu brazo para controlar el colibr\u00ed' :\n        'Observa el tutorial - Demostraci\u00f3n autom\u00e1tica';\n      \n      showMessage(tutorialMessage);\n      speak('Bienvenido a Colibr\u00ed Rehab. Observa c\u00f3mo funciona el sistema.', 0.9, 1.1);\n      \n      setTimeout(() => {\n        gameState = GAME_STATES.CALIBRATION;\n        document.getElementById('calibrationPanel').style.display = 'block';\n        calibration.targetY = CONFIG.CANVAS_HEIGHT \/ 2;\n        \n        const calibrationMessage = cameraSystem.isActive ?\n          'Calibraci\u00f3n iniciada - Posiciona tu brazo en el centro' :\n          'Calibraci\u00f3n autom\u00e1tica en progreso...';\n        \n        showMessage(calibrationMessage);\n        speak('Ahora comenzamos la calibraci\u00f3n. Mant\u00e9n el colibr\u00ed en la l\u00ednea verde.', 0.9, 1.1);\n      }, 5000);\n    });\n  }\n  \n  const retryCamera = document.getElementById('retryCamera');\n  if (retryCamera) {\n    retryCamera.addEventListener('click', async () => {\n      await initializeCamera();\n    });\n  }\n  \n  const sensitivity = document.getElementById('sensitivity');\n  if (sensitivity) {\n    sensitivity.addEventListener('input', (e) => {\n      gameData.sensitivity = parseFloat(e.target.value);\n      const sensitivityValue = document.getElementById('sensitivityValue');\n      if (sensitivityValue) {\n        sensitivityValue.textContent = gameData.sensitivity.toFixed(2);\n      }\n    });\n  }\n  \n  const audioToggle = document.getElementById('audioToggle');\n  if (audioToggle) {\n    audioToggle.addEventListener('change', (e) => {\n      if (e.target.checked && !audio.enabled) {\n        initializeAudio();\n      }\n      \n      if (e.target.checked) {\n        speak('Audio habilitado');\n      } else {\n        speechSynthesis.cancel();\n      }\n    });\n  }\n  \n  const whatsappBtn = document.getElementById('whatsappBtn');\n  if (whatsappBtn) {\n    whatsappBtn.addEventListener('click', () => {\n      const report = generateReport();\n      const whatsappUrl = `https:\/\/wa.me\/?text=${encodeURIComponent(report)}`;\n      window.open(whatsappUrl, '_blank');\n      showMessage('Enviando reporte por WhatsApp...', 'success');\n    });\n  }\n  \n  const emailBtn = document.getElementById('emailBtn');\n  if (emailBtn) {\n    emailBtn.addEventListener('click', () => {\n      const report = generateReport();\n      const subject = 'Reporte de Rehabilitaci\u00f3n - Colibr\u00ed Rehab';\n      const mailtoUrl = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(report)}`;\n      window.location.href = mailtoUrl;\n      showMessage('Abriendo cliente de email...', 'success');\n    });\n  }\n  \n  const copyBtn = document.getElementById('copyBtn');\n  if (copyBtn) {\n    copyBtn.addEventListener('click', async () => {\n      try {\n        const report = generateReport();\n        \n        if (navigator.clipboard && navigator.clipboard.writeText) {\n          await navigator.clipboard.writeText(report);\n        } else {\n          const textArea = document.createElement('textarea');\n          textArea.value = report;\n          textArea.style.position = 'fixed';\n          textArea.style.left = '-999999px';\n          textArea.style.top = '-999999px';\n          document.body.appendChild(textArea);\n          textArea.focus();\n          textArea.select();\n          document.execCommand('copy');\n          document.body.removeChild(textArea);\n        }\n        \n        showMessage('Reporte copiado al portapapeles', 'success');\n        speak('Reporte copiado exitosamente');\n        \n      } catch (error) {\n        console.error('Error al copiar:', error);\n        showMessage('Error al copiar el reporte', 'error');\n      }\n    });\n  }\n  \n  \/\/ Controles de teclado\n  document.addEventListener('keydown', (e) => {\n    switch (e.key) {\n      case ' ':\n      case 'Escape':\n        e.preventDefault();\n        if (gameState === GAME_STATES.PLAYING) {\n          pauseGame();\n        } else if (gameState === GAME_STATES.PAUSED) {\n          resumeGame();\n        }\n        break;\n        \n      case 'r':\n      case 'R':\n        if (e.ctrlKey && gameState === GAME_STATES.PLAYING) {\n          e.preventDefault();\n          if (confirm('\u00bfReiniciar la sesi\u00f3n actual?')) {\n            startGame();\n          }\n        }\n        break;\n        \n      case 'm':\n      case 'M':\n        if (gameState !== GAME_STATES.MENU) {\n          resetToMenu();\n        }\n        break;\n        \n      case 'c':\n      case 'C':\n        if (e.ctrlKey) {\n          e.preventDefault();\n          initializeCamera();\n        }\n        break;\n    }\n  });\n  \n  document.addEventListener('visibilitychange', () => {\n    if (document.hidden && gameState === GAME_STATES.PLAYING) {\n      pauseGame();\n    }\n  });\n  \n  \/\/ Prevenir zoom en m\u00f3viles\n  document.addEventListener('touchstart', (e) => {\n    if (e.touches.length > 1) {\n      e.preventDefault();\n    }\n  }, { passive: false });\n  \n  let lastTouchEnd = 0;\n  document.addEventListener('touchend', (e) => {\n    const now = Date.now();\n    if (now - lastTouchEnd <= 300) {\n      e.preventDefault();\n    }\n    lastTouchEnd = now;\n  }, { passive: false });\n}\n\n\/\/ Loop principal\nlet lastFrameTime = 0;\nlet animationId;\n\nfunction gameLoop(currentTime = 0) {\n  if (currentTime - lastFrameTime >= 1000 \/ CONFIG.FPS) {\n    try {\n      updateGame();\n    } catch (error) {\n      console.error('Error en game loop:', error);\n    }\n    lastFrameTime = currentTime;\n  }\n  \n  animationId = requestAnimationFrame(gameLoop);\n}\n\n\/\/ Funciones de c\u00e1mara adicionales\nfunction stopCamera() {\n  if (cameraSystem.stream) {\n    cameraSystem.stream.getTracks().forEach(track => track.stop());\n    cameraSystem.stream = null;\n    cameraSystem.isActive = false;\n    \n    document.getElementById('cameraPreview').style.display = 'none';\n    document.getElementById('cameraStatus').innerHTML = 'C\u00e1mara desactivada';\n    document.getElementById('cameraStatus').style.background = 'rgba(244, 67, 54, 0.8)';\n    \n    console.log('C\u00e1mara desactivada');\n  }\n}\n\nfunction toggleCamera() {\n  if (cameraSystem.isActive) {\n    stopCamera();\n    activateSimulationMode();\n  } else {\n    initializeCamera();\n  }\n}\n\n\/\/ Utilidades\nfunction formatTime(seconds) {\n  const mins = Math.floor(seconds \/ 60);\n  const secs = seconds % 60;\n  return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\nfunction getPerformanceLevel(score) {\n  if (score >= 50) return { level: 'Excepcional', color: '#4CAF50' };\n  if (score >= 35) return { level: 'Excelente', color: '#2196F3' };\n  if (score >= 20) return { level: 'Bueno', color: '#FF9800' };\n  if (score >= 10) return { level: 'Regular', color: '#FFC107' };\n  return { level: 'Inicial', color: '#9E9E9E' };\n}\n\nfunction calculateHealthMetrics() {\n  if (session.scores.length === 0) return null;\n  \n  const recentSessions = session.scores.slice(-10);\n  const avgScore = recentSessions.reduce((sum, s) => sum + s.score, 0) \/ recentSessions.length;\n  const consistency = recentSessions.filter(s => s.score >= avgScore * 0.8).length \/ recentSessions.length;\n  const improvement = recentSessions.length >= 5 ? \n    (recentSessions.slice(-3).reduce((sum, s) => sum + s.score, 0) \/ 3) -\n    (recentSessions.slice(0, 3).reduce((sum, s) => sum + s.score, 0) \/ 3) : 0;\n  \n  return {\n    avgScore: avgScore.toFixed(1),\n    consistency: (consistency * 100).toFixed(0),\n    improvement: improvement.toFixed(1),\n    totalSessions: session.scores.length,\n    totalTime: formatTime(recentSessions.reduce((sum, s) => sum + s.duration, 0))\n  };\n}\n\n\/\/ Manejo de errores\nwindow.addEventListener('error', (e) => {\n  console.error('Error capturado:', e.error);\n  showMessage('Se produjo un error. Recarga la p\u00e1gina si persiste.', 'error');\n});\n\nwindow.addEventListener('unhandledrejection', (e) => {\n  console.error('Promesa rechazada:', e.reason);\n  showMessage('Error de conexi\u00f3n. Verifica tu conexi\u00f3n a internet.', 'error');\n  e.preventDefault();\n});\n\n\/\/ Cleanup\nwindow.addEventListener('beforeunload', () => {\n  if (animationId) {\n    cancelAnimationFrame(animationId);\n  }\n  \n  if (cameraSystem.stream) {\n    cameraSystem.stream.getTracks().forEach(track => track.stop());\n  }\n  \n  if (typeof speechSynthesis !== 'undefined') {\n    speechSynthesis.cancel();\n  }\n});\n\n\/\/ Inicializaci\u00f3n final\nif (document.readyState === 'loading') {\n  document.addEventListener('DOMContentLoaded', initializeApp);\n} else {\n  initializeApp();\n}\n\nconsole.log('Colibr\u00ed Rehab v2.1 cargado - Sistema con detecci\u00f3n de movimiento');\nconsole.log('Controles: ESPACIO (pausa), R (reiniciar), M (men\u00fa), Ctrl+C (c\u00e1mara)');\n<\/script>\n\n<\/body>\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Colibr\u00ed Rehab &#8211; Rehabilitaci\u00f3n Inteligente Sin c\u00e1mara Brazo a ejercitar: Brazo DerechoBrazo Izquierdo Nivel de dificultad: Principiante (1)F\u00e1cil (2)Moderado (3)Intermedio (4)Avanzado (5) Habilitar gu\u00eda de voz Iniciar Rehabilitaci\u00f3n Calibraci\u00f3n del Sistema Sensibilidad: 0.15 3.0 Preparando sensores&#8230; Error de C\u00e1mara No se pudo acceder a la c\u00e1mara. Verifica los permisos del navegador. Reintentar 0 Puntaje 1 Nivel Bienvenido a Colibr\u00ed Rehab &#8211; Tu sistema de rehabilitaci\u00f3n inteligente Progreso de Rehabilitaci\u00f3n 0% Historial de Sesiones No hay sesiones previas. \u00a1Comienza tu primera sesi\u00f3n! Generar Reporte Enviar por WhatsApp Enviar por Email Copiar al Portapapeles<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"site-sidebar-layout":"no-sidebar","site-content-layout":"","ast-site-content-layout":"full-width-container","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"disabled","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"_joinchat":[],"footnotes":""},"class_list":["post-1219","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>mvp26 - HEALTH GROWERS SAS<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.healthgrowers.co\/soy60\/mvp26\/\" \/>\n<meta property=\"og:locale\" content=\"es_ES\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"mvp26 - HEALTH GROWERS SAS\" \/>\n<meta property=\"og:description\" content=\"Colibr\u00ed Rehab &#8211; Rehabilitaci\u00f3n Inteligente Sin c\u00e1mara Brazo a ejercitar: Brazo DerechoBrazo Izquierdo Nivel de dificultad: Principiante (1)F\u00e1cil (2)Moderado (3)Intermedio (4)Avanzado (5) Habilitar gu\u00eda de voz Iniciar Rehabilitaci\u00f3n Calibraci\u00f3n del Sistema Sensibilidad: 0.15 3.0 Preparando sensores&#8230; Error de C\u00e1mara No se pudo acceder a la c\u00e1mara. Verifica los permisos del navegador. Reintentar 0 Puntaje 1 Nivel Bienvenido a Colibr\u00ed Rehab &#8211; Tu sistema de rehabilitaci\u00f3n inteligente Progreso de Rehabilitaci\u00f3n 0% Historial de Sesiones No hay sesiones previas. \u00a1Comienza tu primera sesi\u00f3n! Generar Reporte Enviar por WhatsApp Enviar por Email Copiar al Portapapeles\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.healthgrowers.co\/soy60\/mvp26\/\" \/>\n<meta property=\"og:site_name\" content=\"HEALTH GROWERS SAS\" \/>\n<meta property=\"article:modified_time\" content=\"2025-09-06T11:45:43+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/mvp26\\\/\",\"url\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/mvp26\\\/\",\"name\":\"mvp26 - HEALTH GROWERS SAS\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/#website\"},\"datePublished\":\"2025-09-05T22:11:33+00:00\",\"dateModified\":\"2025-09-06T11:45:43+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/mvp26\\\/#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/mvp26\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/mvp26\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Portada\",\"item\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"mvp26\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/#website\",\"url\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/\",\"name\":\"HEALTH GROWERS SAS\",\"description\":\"Cultivamos Salud\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/#organization\",\"name\":\"HEALTH GROWERS SAS\",\"url\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"es\",\"@id\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/wp-content\\\/uploads\\\/2023\\\/12\\\/cropped-cropped-Copia-de-LOGO-HG.png\",\"contentUrl\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/wp-content\\\/uploads\\\/2023\\\/12\\\/cropped-cropped-Copia-de-LOGO-HG.png\",\"width\":759,\"height\":859,\"caption\":\"HEALTH GROWERS SAS\"},\"image\":{\"@id\":\"https:\\\/\\\/www.healthgrowers.co\\\/soy60\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"mvp26 - HEALTH GROWERS SAS","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.healthgrowers.co\/soy60\/mvp26\/","og_locale":"es_ES","og_type":"article","og_title":"mvp26 - HEALTH GROWERS SAS","og_description":"Colibr\u00ed Rehab &#8211; Rehabilitaci\u00f3n Inteligente Sin c\u00e1mara Brazo a ejercitar: Brazo DerechoBrazo Izquierdo Nivel de dificultad: Principiante (1)F\u00e1cil (2)Moderado (3)Intermedio (4)Avanzado (5) Habilitar gu\u00eda de voz Iniciar Rehabilitaci\u00f3n Calibraci\u00f3n del Sistema Sensibilidad: 0.15 3.0 Preparando sensores&#8230; Error de C\u00e1mara No se pudo acceder a la c\u00e1mara. Verifica los permisos del navegador. Reintentar 0 Puntaje 1 Nivel Bienvenido a Colibr\u00ed Rehab &#8211; Tu sistema de rehabilitaci\u00f3n inteligente Progreso de Rehabilitaci\u00f3n 0% Historial de Sesiones No hay sesiones previas. \u00a1Comienza tu primera sesi\u00f3n! Generar Reporte Enviar por WhatsApp Enviar por Email Copiar al Portapapeles","og_url":"https:\/\/www.healthgrowers.co\/soy60\/mvp26\/","og_site_name":"HEALTH GROWERS SAS","article_modified_time":"2025-09-06T11:45:43+00:00","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.healthgrowers.co\/soy60\/mvp26\/","url":"https:\/\/www.healthgrowers.co\/soy60\/mvp26\/","name":"mvp26 - HEALTH GROWERS SAS","isPartOf":{"@id":"https:\/\/www.healthgrowers.co\/soy60\/#website"},"datePublished":"2025-09-05T22:11:33+00:00","dateModified":"2025-09-06T11:45:43+00:00","breadcrumb":{"@id":"https:\/\/www.healthgrowers.co\/soy60\/mvp26\/#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.healthgrowers.co\/soy60\/mvp26\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.healthgrowers.co\/soy60\/mvp26\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Portada","item":"https:\/\/www.healthgrowers.co\/soy60\/"},{"@type":"ListItem","position":2,"name":"mvp26"}]},{"@type":"WebSite","@id":"https:\/\/www.healthgrowers.co\/soy60\/#website","url":"https:\/\/www.healthgrowers.co\/soy60\/","name":"HEALTH GROWERS SAS","description":"Cultivamos Salud","publisher":{"@id":"https:\/\/www.healthgrowers.co\/soy60\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.healthgrowers.co\/soy60\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"},{"@type":"Organization","@id":"https:\/\/www.healthgrowers.co\/soy60\/#organization","name":"HEALTH GROWERS SAS","url":"https:\/\/www.healthgrowers.co\/soy60\/","logo":{"@type":"ImageObject","inLanguage":"es","@id":"https:\/\/www.healthgrowers.co\/soy60\/#\/schema\/logo\/image\/","url":"https:\/\/www.healthgrowers.co\/soy60\/wp-content\/uploads\/2023\/12\/cropped-cropped-Copia-de-LOGO-HG.png","contentUrl":"https:\/\/www.healthgrowers.co\/soy60\/wp-content\/uploads\/2023\/12\/cropped-cropped-Copia-de-LOGO-HG.png","width":759,"height":859,"caption":"HEALTH GROWERS SAS"},"image":{"@id":"https:\/\/www.healthgrowers.co\/soy60\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/www.healthgrowers.co\/soy60\/wp-json\/wp\/v2\/pages\/1219","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.healthgrowers.co\/soy60\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.healthgrowers.co\/soy60\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.healthgrowers.co\/soy60\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.healthgrowers.co\/soy60\/wp-json\/wp\/v2\/comments?post=1219"}],"version-history":[{"count":13,"href":"https:\/\/www.healthgrowers.co\/soy60\/wp-json\/wp\/v2\/pages\/1219\/revisions"}],"predecessor-version":[{"id":1239,"href":"https:\/\/www.healthgrowers.co\/soy60\/wp-json\/wp\/v2\/pages\/1219\/revisions\/1239"}],"wp:attachment":[{"href":"https:\/\/www.healthgrowers.co\/soy60\/wp-json\/wp\/v2\/media?parent=1219"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}