const canvas = document.getElementById('simCanvas'); const ctx = canvas.getContext('2d'); const size = 15; const rows = canvas.height / size; const cols = canvas.width / size; const stepDelay = 40; const slowDelay = stepDelay + 70; const fadeDurationDefault = 400; const fadeDurationSlow = 1200; let barrierMode = false, repolarMode = false, pacemakerMode = false, slowConductionMode = false, paceActive = false; let isMouseDown = false, paintAction = null; let hideObstacles = false; let selectPixelMode = false; let selectedPixel = { row: rows - 1, col: 0 }; const barrierToggle = document.getElementById('barrierToggle'); const repolarToggle = document.getElementById('repolarToggle'); const slowConductionToggle = document.getElementById('slowConductionToggle'); const pacemakerToggle = document.getElementById('pacemakerToggle'); const paceModeToggle = document.getElementById('paceModeToggle'); const visibilityToggle = document.getElementById('visibilityToggle'); const selectPixelButton = document.getElementById('selectPixelButton'); const pacemakerEntries = document.getElementById('pacemakerEntries'); const resetButton = document.getElementById('resetButton'); const circleBlockButton = document.getElementById('circleBlockButton'); const diamondBlockButton = document.getElementById('diamondBlockButton'); let grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => ({ value: 0, time: 0, barrier: false, repolarizeSlow: false, pacemaker: false, slowConduction: false, paceRate: 60, lastPaceTime: performance.now() })) ); barrierToggle.onclick = () => { barrierMode = !barrierMode; repolarMode = pacemakerMode = slowConductionMode = false; updateButtons(); }; repolarToggle.onclick = () => { repolarMode = !repolarMode; barrierMode = pacemakerMode = slowConductionMode = false; updateButtons(); }; slowConductionToggle.onclick = () => { slowConductionMode = !slowConductionMode; barrierMode = repolarMode = pacemakerMode = false; updateButtons(); }; pacemakerToggle.onclick = () => { pacemakerMode = !pacemakerMode; barrierMode = repolarMode = slowConductionMode = false; updateButtons(); }; paceModeToggle.onclick = () => { paceActive = !paceActive; updateButtons(); }; visibilityToggle.onclick = () => { hideObstacles = !hideObstacles; visibilityToggle.textContent = hideObstacles ? "👁️ Show Only Activation Waves: ON" : "👁️ Show Only Activation Waves: OFF"; }; selectPixelButton.onclick = () => { selectPixelMode = !selectPixelMode; selectPixelButton.textContent = selectPixelMode ? "📍 Select Pixel to Monitor: ON" : "📍 Select Pixel to Monitor: OFF"; }; circleBlockButton.onclick = () => { const centerX = Math.floor(cols / 2); const centerY = Math.floor(rows / 2); const radius = Math.floor(Math.min(cols, rows) / 2) - 2; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const dist = Math.sqrt((c - centerX) ** 2 + (r - centerY) ** 2); if (dist >= radius - 1 && dist <= radius + 1) { grid[r][c].barrier = true; grid[r][c].repolarizeSlow = false; grid[r][c].slowConduction = false; grid[r][c].pacemaker = false; } } } }; diamondBlockButton.onclick = () => { const centerX = Math.floor(cols / 2); const centerY = Math.floor(rows / 2); const maxDist = Math.floor((Math.min(cols, rows) / 2)) - 2; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const manhattan = Math.abs(r - centerY) + Math.abs(c - centerX); if (manhattan >= maxDist - 1 && manhattan <= maxDist + 1) { grid[r][c].barrier = true; grid[r][c].repolarizeSlow = false; grid[r][c].slowConduction = false; grid[r][c].pacemaker = false; } } } }; function updateButtons() { barrierToggle.textContent = barrierMode ? "🔴 Conduction blocks: ON" : "🔴 Conduction blocks: OFF"; repolarToggle.textContent = repolarMode ? "🟫 Slow repolarization: ON" : "🟫 Slow repolarization: OFF"; slowConductionToggle.textContent = slowConductionMode ? "🟩 Slow conduction: ON" : "🟩 Slow conduction: OFF"; pacemakerToggle.textContent = pacemakerMode ? "💙 Add Pacemakers: ON" : "💙 Add Pacemakers: OFF"; paceModeToggle.textContent = paceActive ? "⚡ PACE Mode: ON" : "⚡ PACE Mode: OFF"; } canvas.addEventListener('mousedown', (e) => { const { row, col } = getMousePos(e); if (selectPixelMode) { selectedPixel = { row, col }; selectPixelMode = false; selectPixelButton.textContent = "📍 Select Pixel to Monitor: OFF"; return; } isMouseDown = true; const cell = grid[row][col]; if (barrierMode) paintAction = cell.barrier ? "remove" : "add"; else if (repolarMode) paintAction = cell.repolarizeSlow ? "remove" : "add"; else if (slowConductionMode) paintAction = cell.slowConduction ? "remove" : "add"; else if (pacemakerMode) paintAction = cell.pacemaker ? "remove" : "add"; paintCell(row, col, paintAction); }); canvas.addEventListener('mousemove', (e) => { if (!isMouseDown) return; const { row, col } = getMousePos(e); paintCell(row, col, paintAction); }); window.addEventListener('mouseup', () => { isMouseDown = false; paintAction = null; }); function getMousePos(e) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; return { col: Math.floor(x / size), row: Math.floor(y / size) }; } function paintCell(row, col, overrideAction = null) { if (col < 0 || col >= cols || row < 0 || row >= rows) return; const cell = grid[row][col]; if (barrierMode) { const action = overrideAction || (cell.barrier ? "remove" : "add"); cell.barrier = (action === "add"); if (cell.barrier) cell.repolarizeSlow = cell.pacemaker = cell.slowConduction = false; } else if (repolarMode) { const action = overrideAction || (cell.repolarizeSlow ? "remove" : "add"); cell.repolarizeSlow = (action === "add"); if (cell.repolarizeSlow) cell.barrier = cell.pacemaker = cell.slowConduction = false; } else if (slowConductionMode) { const action = overrideAction || (cell.slowConduction ? "remove" : "add"); cell.slowConduction = (action === "add"); if (cell.slowConduction) cell.barrier = cell.repolarizeSlow = cell.pacemaker = false; } else if (pacemakerMode) { const action = overrideAction || (cell.pacemaker ? "remove" : "add"); cell.pacemaker = (action === "add"); if (cell.pacemaker) { cell.barrier = cell.repolarizeSlow = cell.slowConduction = false; cell.paceRate = 60; cell.lastPaceTime = performance.now(); } updatePacemakerList(); } else if (cell.value <= 0.2 && !cell.barrier) { startWave(row, col); } } function startWave(startRow, startCol) { const queue = [[startRow, startCol]]; const nextWaves = new Map(); function addToWave(delay, r, c) { if (!nextWaves.has(delay)) nextWaves.set(delay, []); nextWaves.get(delay).push([r, c]); } function propagateWave() { if (queue.length === 0 && nextWaves.size === 0) return; const current = queue.splice(0); for (const [r, c] of current) { const cell = grid[r][c]; if (cell.barrier || cell.value > 0.2) continue; cell.value = 1; cell.time = performance.now(); for (let dr = -1; dr <= 1; dr++) { for (let dc = -1; dc <= 1; dc++) { if (dr === 0 && dc === 0) continue; const nr = r + dr, nc = c + dc; if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue; const neighbor = grid[nr][nc]; if (neighbor.barrier || neighbor.value > 0.2) continue; const distance = Math.sqrt(dr * dr + dc * dc); const delay = Math.round((neighbor.slowConduction ? slowDelay * 1.5 : stepDelay) * distance); addToWave(delay, nr, nc); } } } for (const [delay, list] of nextWaves) { setTimeout(() => { queue.push(...list); propagateWave(); }, delay); } nextWaves.clear(); } propagateWave(); } function update() { const now = performance.now(); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const cell = grid[r][c]; if (cell.value > 0) { const duration = cell.repolarizeSlow ? fadeDurationSlow : fadeDurationDefault; const elapsed = now - cell.time; if (elapsed > duration) cell.value = 0; else cell.value = Math.max(0, 1 - elapsed / duration); } if (paceActive && cell.pacemaker && now - cell.lastPaceTime >= 1000 * 60 / cell.paceRate) { startWave(r, c); cell.lastPaceTime = now; } } } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const cell = grid[r][c]; if (cell.repolarizeSlow && !hideObstacles) ctx.fillStyle = "gray"; else if (cell.barrier && !hideObstacles) ctx.fillStyle = "red"; else if (cell.slowConduction && !hideObstacles) ctx.fillStyle = "green"; else { const val = Math.floor((1 - cell.value) * 255); ctx.fillStyle = `rgb(${val},${val},255)`; } ctx.fillRect(c * size + 0.5, r * size + 0.5, size - 1, size - 1); if (cell.pacemaker) { ctx.strokeStyle = "blue"; ctx.lineWidth = 2; ctx.strokeRect(c * size + 0.5, r * size + 0.5, size - 2, size - 2); } } } // Highlight selected pixel ctx.strokeStyle = "black"; ctx.lineWidth = 3; ctx.strokeRect( selectedPixel.col * size + 0.5, selectedPixel.row * size + 0.5, size - 1, size - 1 ); } function updatePacemakerList() { pacemakerEntries.innerHTML = ''; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const cell = grid[r][c]; if (cell.pacemaker) { const div = document.createElement('div'); div.className = 'pacemaker-entry'; div.innerHTML = `(${r},${c}) → ${cell.paceRate} BPM `; pacemakerEntries.appendChild(div); } } } } window.changeRate = function(r, c, delta) { const cell = grid[r][c]; cell.paceRate = Math.max(10, Math.min(300, cell.paceRate + delta)); updatePacemakerList(); }; const signalCanvas = document.getElementById('signalCanvas'); const signalCtx = signalCanvas.getContext('2d'); const signalData = []; const maxSignalLength = 300; function updateSignalPlot() { const val = grid[selectedPixel.row][selectedPixel.col].value; const last = signalData.at(-1) ?? val; const smoothed = last * 0.8 + val * 0.2; signalData.push(smoothed); if (signalData.length > maxSignalLength) signalData.shift(); signalCtx.clearRect(0, 0, signalCanvas.width, signalCanvas.height); signalCtx.beginPath(); signalCtx.moveTo(0, signalCanvas.height - signalData[0] * signalCanvas.height); for (let i = 1; i < signalData.length; i++) { const x = (i / maxSignalLength) * signalCanvas.width; const y = signalCanvas.height - signalData[i] * signalCanvas.height; signalCtx.lineTo(x, y); } signalCtx.strokeStyle = 'blue'; signalCtx.lineWidth = 2; signalCtx.stroke(); } function loop() { update(); draw(); updateSignalPlot(); requestAnimationFrame(loop); } resetButton.onclick = () => { for (let row of grid) for (let cell of row) Object.assign(cell, { barrier: false, repolarizeSlow: false, slowConduction: false, pacemaker: false, value: 0 }); updatePacemakerList(); }; updateButtons(); updatePacemakerList(); loop();