Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Playable In-Ear Device Protocols (B&W)</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://d3js.org/d3.v7.min.js"></script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* Custom styles */ | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #f9fafb; /* Light gray background */ | |
| } | |
| .protocol-container { | |
| margin-bottom: 1.5rem; /* Space between protocols */ | |
| padding: 1.5rem; | |
| background-color: white; | |
| border-radius: 0.5rem; /* Rounded corners */ | |
| box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); /* Subtle shadow */ | |
| position: relative; /* Needed for absolute positioning of progress line */ | |
| } | |
| .protocol-title { | |
| font-size: 1.1rem; /* Slightly smaller title */ | |
| font-weight: 600; | |
| margin-bottom: 1rem; | |
| color: #1f2937; /* Dark gray text */ | |
| } | |
| svg { | |
| display: block; | |
| margin: auto; | |
| overflow: visible; | |
| } | |
| .bar { | |
| stroke: #6b7280; /* Medium Gray border for bars */ | |
| stroke-width: 0.5; | |
| transition: fill 0.1s ease-in-out, stroke-width 0.1s ease-in-out; /* Smooth transition for highlight */ | |
| } | |
| .bar.active { | |
| fill: #000000 ; /* Black for active */ | |
| stroke-width: 1.5; | |
| stroke: #000000; | |
| } | |
| .tooltip { | |
| position: absolute; | |
| text-align: center; | |
| padding: 6px 10px; | |
| font-size: 12px; | |
| background: #374151; /* Darker gray background */ | |
| color: white; | |
| border-radius: 4px; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| white-space: nowrap; | |
| z-index: 10; /* Ensure tooltip is above elements */ | |
| } | |
| .axis path, | |
| .axis line { | |
| fill: none; | |
| stroke: #9ca3af; /* Lighter gray axis */ | |
| shape-rendering: crispEdges; | |
| } | |
| .axis text { | |
| font-size: 10px; | |
| fill: #6b7280; /* Medium gray text for axis */ | |
| } | |
| .progress-line { | |
| stroke: #ef4444; /* Red color for progress */ | |
| stroke-width: 1.5; | |
| pointer-events: none; /* Prevent line from interfering */ | |
| } | |
| /* Control Buttons Styling */ | |
| .control-button { | |
| background-color: #3b82f6; /* Blue */ | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border: none; | |
| border-radius: 0.375rem; /* Rounded */ | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background-color 0.2s; | |
| margin: 0 0.25rem; /* Spacing between buttons */ | |
| } | |
| .control-button:hover { | |
| background-color: #2563eb; /* Darker Blue */ | |
| } | |
| .control-button:disabled { | |
| background-color: #9ca3af; /* Gray when disabled */ | |
| cursor: not-allowed; | |
| } | |
| .time-display { | |
| font-weight: 600; | |
| color: #1f2937; | |
| min-width: 100px; /* Ensure space for time */ | |
| text-align: right; | |
| } | |
| </style> | |
| </head> | |
| <body class="p-6 bg-gray-50"> | |
| <h1 class="text-2xl font-bold text-center mb-6 text-gray-800">In-Ear Recording Protocols Player</h1> | |
| <div class="flex justify-center items-center mb-6 p-4 bg-white rounded-lg shadow"> | |
| <button id="play-pause-button" class="control-button">Play</button> | |
| <button id="reset-button" class="control-button">Reset</button> | |
| <div class="ml-4 time-display"> | |
| Time: <span id="current-time">0.0</span>s / <span id="total-duration">0.0</span>s | |
| </div> | |
| <input type="range" id="speed-slider" min="0.1" max="5" step="0.1" value="1" class="ml-4 w-32 cursor-pointer"> | |
| <label for="speed-slider" class="ml-2 text-sm text-gray-600">Speed: <span id="speed-value">1.0</span>x</label> | |
| </div> | |
| <div id="jaw-protocol" class="protocol-container"> | |
| <h2 class="protocol-title">Jaw Movements Protocol</h2> | |
| <svg id="jaw-chart"></svg> | |
| </div> | |
| <div id="tongue-protocol" class="protocol-container"> | |
| <h2 class="protocol-title">Tongue Movements Protocol</h2> | |
| <svg id="tongue-chart"></svg> | |
| </div> | |
| <div id="spoken-aloud-protocol" class="protocol-container"> | |
| <h2 class="protocol-title">Spoken Words Protocol (Aloud)</h2> | |
| <svg id="spoken-aloud-chart"></svg> | |
| </div> | |
| <div id="spoken-whisper-protocol" class="protocol-container"> | |
| <h2 class="protocol-title">Spoken Words Protocol (Whisper)</h2> | |
| <svg id="spoken-whisper-chart"></svg> | |
| </div> | |
| <div id="spoken-closed-protocol" class="protocol-container"> | |
| <h2 class="protocol-title">Spoken Words Protocol (Mouth Closed)</h2> | |
| <svg id="spoken-closed-chart"></svg> | |
| </div> | |
| <div id="eyes-protocol" class="protocol-container"> | |
| <h2 class="protocol-title">Eyes Open/Closed Protocol</h2> | |
| <svg id="eyes-chart"></svg> | |
| </div> | |
| <div id="clench-protocol" class="protocol-container"> | |
| <h2 class="protocol-title">Clench Jaw Protocol</h2> | |
| <svg id="clench-chart"></svg> | |
| </div> | |
| <div id="tooltip" class="tooltip"></div> | |
| <script> | |
| // --- Protocol Data (Keep as is) --- | |
| const JAW_PROTOCOL = [ | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Post-Repetition Rest", "duration": 15, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Post-Repetition Rest", "duration": 15, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, | |
| {"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"} | |
| ]; | |
| const TONGUE_PROTOCOL = [ | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Right", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Left", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Upper Teeth", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Lower Teeth", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Retraction", "duration": 6, "color": "#CC99FF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Right", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Left", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Upper Teeth", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Lower Teeth", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Retraction", "duration": 6, "color": "#CC99FF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Right", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Left", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Upper Teeth", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Lower Teeth", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Retraction", "duration": 6, "color": "#CC99FF"} | |
| ]; | |
| const SPOKEN_ALOUD_PROTOCOL = [ | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Later'", "duration": 6, "color": "#99CCFF"} | |
| ]; | |
| const SPOKEN_WHISPER_PROTOCOL = [ | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Later'", "duration": 6, "color": "#99CCFF"} | |
| ]; | |
| const SPOKEN_CLOSED_PROTOCOL = [ | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"}, | |
| {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Later'", "duration": 6, "color": "#99CCFF"} | |
| ]; | |
| const EYES_PROTOCOL = [ | |
| {"name": "Rest", "duration": 5, "color": "#CCCCCC"}, | |
| {"name": "Eyes Closed", "duration": 30, "color": "#FF9999"}, {"name": "Eyes Open", "duration": 30, "color": "#99FF99"}, | |
| {"name": "Eyes Closed", "duration": 30, "color": "#FF9999"}, {"name": "Eyes Open", "duration": 30, "color": "#99FF99"}, | |
| {"name": "Eyes Closed", "duration": 30, "color": "#FF9999"}, {"name": "Eyes Open", "duration": 30, "color": "#99FF99"} | |
| ]; | |
| const CLENCH_PROTOCOL = [ | |
| {"name": "Rest", "duration": 15, "color": "#99FF99"}, {"name": "Clench", "duration": 15, "color": "#FF9999"}, | |
| {"name": "Rest", "duration": 15, "color": "#99FF99"}, {"name": "Clench", "duration": 15, "color": "#FF9999"}, | |
| {"name": "Rest", "duration": 15, "color": "#99FF99"}, {"name": "Clench", "duration": 15, "color": "#FF9999"} | |
| ]; | |
| const ALL_PROTOCOLS = { | |
| "jaw-chart": JAW_PROTOCOL, | |
| "tongue-chart": TONGUE_PROTOCOL, | |
| "spoken-aloud-chart": SPOKEN_ALOUD_PROTOCOL, | |
| "spoken-whisper-chart": SPOKEN_WHISPER_PROTOCOL, | |
| "spoken-closed-chart": SPOKEN_CLOSED_PROTOCOL, | |
| "eyes-chart": EYES_PROTOCOL, | |
| "clench-chart": CLENCH_PROTOCOL | |
| }; | |
| // --- Global State --- | |
| let timerInterval = null; | |
| let currentTime = 0; // Current time in seconds | |
| let isPlaying = false; | |
| let playbackSpeed = 1.0; | |
| const timeStep = 0.1; // Update interval in seconds (100ms) | |
| let maxDuration = 0; // Maximum duration across all protocols | |
| const scales = {}; // Store scales for each chart | |
| const processedDataStore = {}; // Store processed data for each chart | |
| // --- DOM Elements --- | |
| const playPauseButton = document.getElementById('play-pause-button'); | |
| const resetButton = document.getElementById('reset-button'); | |
| const currentTimeDisplay = document.getElementById('current-time'); | |
| const totalDurationDisplay = document.getElementById('total-duration'); | |
| const tooltip = d3.select("#tooltip"); | |
| const speedSlider = document.getElementById('speed-slider'); | |
| const speedValueDisplay = document.getElementById('speed-value'); | |
| // --- Helper Functions --- | |
| // Function to calculate cumulative duration and add start/end times | |
| function processProtocolData(protocolData) { | |
| let cumulativeDuration = 0; | |
| return protocolData.map(d => { | |
| const start = cumulativeDuration; | |
| cumulativeDuration += d.duration; | |
| return { ...d, start: start, end: cumulativeDuration }; | |
| }); | |
| } | |
| // Function to calculate total duration of a processed protocol | |
| function calculateTotalDuration(processedData) { | |
| return processedData.length > 0 ? processedData[processedData.length - 1].end : 0; | |
| } | |
| // --- Visualization Function --- | |
| function createTimeline(chartId, processedData, globalMaxDuration) { | |
| const container = d3.select(`#${chartId}`); | |
| if (container.empty()) { | |
| console.error(`Container element #${chartId} not found.`); | |
| return; | |
| } | |
| const containerWidth = container.node().getBoundingClientRect().width; | |
| const margin = { top: 5, right: 15, bottom: 25, left: 15 }; | |
| const width = containerWidth - margin.left - margin.right; | |
| const height = 60 - margin.top - margin.bottom; // Reduced height | |
| const barHeight = 25; // Reduced bar height | |
| // --- Scales --- | |
| // Use the global max duration for the domain | |
| const xScale = d3.scaleLinear() | |
| .domain([0, globalMaxDuration]) | |
| .range([0, width]); | |
| scales[chartId] = xScale; // Store the scale | |
| // --- SVG Setup --- | |
| container.selectAll("*").remove(); // Clear previous chart | |
| const svg = container | |
| .attr("width", width + margin.left + margin.right) | |
| .attr("height", height + margin.top + margin.bottom) | |
| .append("g") | |
| .attr("transform", `translate(${margin.left},${margin.top})`); | |
| // --- Draw Bars --- | |
| svg.selectAll(".bar") | |
| .data(processedData) | |
| .enter() | |
| .append("rect") | |
| .attr("class", "bar") | |
| .attr("data-chart-id", chartId) // Add chart ID for easier selection later | |
| .attr("x", d => xScale(d.start)) | |
| .attr("y", (height - barHeight) / 2) | |
| .attr("width", d => Math.max(0, xScale(d.end) - xScale(d.start))) // Ensure width is non-negative | |
| .attr("height", barHeight) | |
| .attr("fill", d => d.name.toLowerCase().includes("rest") ? "#D3D3D3" : "#696969") // Initial colors | |
| .on("mouseover", function(event, d) { | |
| tooltip.style("opacity", 1); | |
| }) | |
| .on("mousemove", function(event, d) { | |
| tooltip.html(`${d.name}<br>Duration: ${d.duration}s<br>Time: ${d.start.toFixed(1)}s - ${d.end.toFixed(1)}s`) | |
| .style("left", (event.pageX + 10) + "px") | |
| .style("top", (event.pageY - 28) + "px"); | |
| }) | |
| .on("mouseout", function() { | |
| tooltip.style("opacity", 0); | |
| }); | |
| // --- Add Time Axis --- | |
| const xAxis = d3.axisBottom(xScale) | |
| .ticks(Math.min(10, Math.ceil(globalMaxDuration / 15))) // Adjust ticks | |
| .tickFormat(d => `${d}s`); | |
| svg.append("g") | |
| .attr("class", "axis x-axis") | |
| .attr("transform", `translate(0,${height})`) | |
| .call(xAxis); | |
| // --- Add Progress Line --- | |
| svg.append("line") | |
| .attr("class", "progress-line") | |
| .attr("x1", xScale(0)) | |
| .attr("x2", xScale(0)) | |
| .attr("y1", -margin.top / 2) // Position slightly above the bars | |
| .attr("y2", height + margin.bottom / 2) // Extend slightly below axis | |
| .style("opacity", 0); // Initially hidden | |
| } | |
| // --- Update Highlighting and Progress --- | |
| function updateVisuals(time) { | |
| currentTimeDisplay.textContent = time.toFixed(1); | |
| // Update highlights for all bars | |
| d3.selectAll(".bar").each(function(d) { | |
| const bar = d3.select(this); | |
| const isActive = time >= d.start && time < d.end; | |
| bar.classed("active", isActive) | |
| // Reset fill if not active (needed because !important overrides direct fill) | |
| .style("fill", isActive ? null : (d.name.toLowerCase().includes("rest") ? "#D3D3D3" : "#696969")); | |
| }); | |
| // Update progress lines for all charts | |
| d3.selectAll(".progress-line").each(function() { | |
| const line = d3.select(this); | |
| const chartId = d3.select(this.parentNode).select('.bar').attr('data-chart-id'); // Get chart ID from sibling bar | |
| if (chartId && scales[chartId]) { | |
| const xScale = scales[chartId]; | |
| line.attr("x1", xScale(time)) | |
| .attr("x2", xScale(time)) | |
| .style("opacity", 1); // Make visible | |
| } | |
| }); | |
| } | |
| // --- Playback Control Functions --- | |
| function play() { | |
| if (isPlaying) return; | |
| isPlaying = true; | |
| playPauseButton.textContent = 'Pause'; | |
| resetButton.disabled = true; // Disable reset while playing | |
| // Clear existing interval if any | |
| if (timerInterval) clearInterval(timerInterval); | |
| let lastTimestamp = performance.now(); | |
| function tick(currentTimestamp) { | |
| if (!isPlaying) return; // Stop if paused | |
| const elapsedRealTime = (currentTimestamp - lastTimestamp) / 1000; // Time since last frame in seconds | |
| lastTimestamp = currentTimestamp; | |
| currentTime += elapsedRealTime * playbackSpeed; // Increment time based on speed | |
| if (currentTime >= maxDuration) { | |
| currentTime = maxDuration; // Clamp to end | |
| pause(); // Automatically pause at the end | |
| resetButton.disabled = false; // Re-enable reset | |
| } | |
| updateVisuals(currentTime); | |
| // Continue the loop only if playing | |
| if (isPlaying) { | |
| requestAnimationFrame(tick); | |
| } | |
| } | |
| requestAnimationFrame(tick); // Start the loop | |
| } | |
| function pause() { | |
| if (!isPlaying) return; | |
| isPlaying = false; | |
| playPauseButton.textContent = 'Play'; | |
| resetButton.disabled = false; // Re-enable reset | |
| // No need to clear interval with requestAnimationFrame | |
| } | |
| function reset() { | |
| pause(); // Ensure it's paused | |
| currentTime = 0; | |
| updateVisuals(currentTime); | |
| d3.selectAll(".progress-line").style("opacity", 0); // Hide progress line on reset | |
| } | |
| // --- Initialization --- | |
| function initialize() { | |
| // Process all data and find max duration | |
| let currentMax = 0; | |
| for (const chartId in ALL_PROTOCOLS) { | |
| const processedData = processProtocolData(ALL_PROTOCOLS[chartId]); | |
| processedDataStore[chartId] = processedData; // Store processed data | |
| const duration = calculateTotalDuration(processedData); | |
| if (duration > currentMax) { | |
| currentMax = duration; | |
| } | |
| } | |
| maxDuration = currentMax; | |
| totalDurationDisplay.textContent = maxDuration.toFixed(1); | |
| // Create all timelines using the global max duration | |
| for (const chartId in processedDataStore) { | |
| createTimeline(chartId, processedDataStore[chartId], maxDuration); | |
| } | |
| // Set initial visuals | |
| updateVisuals(0); | |
| d3.selectAll(".progress-line").style("opacity", 0); // Ensure lines are hidden initially | |
| // Setup Speed Slider | |
| speedSlider.addEventListener('input', (event) => { | |
| playbackSpeed = parseFloat(event.target.value); | |
| speedValueDisplay.textContent = playbackSpeed.toFixed(1); | |
| }); | |
| speedValueDisplay.textContent = playbackSpeed.toFixed(1); // Initial display | |
| // Add event listeners | |
| playPauseButton.addEventListener('click', () => { | |
| if (isPlaying) { | |
| pause(); | |
| } else { | |
| if (currentTime >= maxDuration) { // If at end, reset before playing | |
| reset(); | |
| } | |
| play(); | |
| } | |
| }); | |
| resetButton.addEventListener('click', reset); | |
| } | |
| // --- Run Initialization on Load --- | |
| window.addEventListener('load', initialize); | |
| // --- Redraw charts on resize --- | |
| // Debounce resize event for performance | |
| let resizeTimeout; | |
| window.addEventListener('resize', () => { | |
| clearTimeout(resizeTimeout); | |
| resizeTimeout = setTimeout(() => { | |
| // Re-initialize everything on resize to recalculate widths/scales | |
| // Store current state | |
| const wasPlaying = isPlaying; | |
| const timeBeforeResize = currentTime; | |
| // Pause before re-initializing | |
| pause(); | |
| // Re-initialize | |
| initialize(); | |
| // Restore state | |
| currentTime = timeBeforeResize; | |
| updateVisuals(currentTime); | |
| if (wasPlaying) { | |
| play(); // Resume playing if it was playing before | |
| } | |
| }, 250); // Wait 250ms after last resize event | |
| }); | |
| </script> | |
| </body> | |
| </html> | |