Update multiCameraViewer.htm

master
Alan 2025-08-15 08:42:37 +01:00 zatwierdzone przez GitHub
rodzic b0937eae7c
commit 2436ff6da4
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
1 zmienionych plików z 213 dodań i 112 usunięć

Wyświetl plik

@ -11,10 +11,11 @@
Add camera IPs under "ADD YOUR CAMERA IPs HERE"
Modify load settings keys under 'load settings with keypress'
default detection timer under "// Timer properties"
Specify config on url: http://x.x.x.x?config=xxx
03Jun25
14Aug25
-->
@ -34,7 +35,6 @@
overflow: hidden;
font-family: sans-serif;
}
/* Styles for the toolbar at the top */
#toolbar {
position: relative;
@ -43,7 +43,6 @@
height: 0;
overflow: visible;
}
/* Styles for buttons within the toolbar */
#toolbar button {
margin-right: 5px;
@ -53,7 +52,6 @@
border: none;
cursor: pointer;
}
/* Styles for the containers holding each camera image */
.img-container {
position: absolute;
@ -63,11 +61,9 @@
z-index: 1;
background: #000;
}
.img-container.active {
z-index: 1000;
}
/* Styles for the inner div used for zoom/pan */
.img-inner {
width: 100%;
@ -76,7 +72,6 @@
cursor: grab;
overflow: hidden;
}
.img-inner img {
position: absolute;
top: 0;
@ -85,7 +80,6 @@
user-select: none;
pointer-events: none;
}
/* Styles for the close button */
.close-btn {
position: absolute;
@ -98,7 +92,6 @@
cursor: pointer;
z-index: 10;
}
/* Styles for the camera title */
.cam-title {
position: absolute;
@ -111,7 +104,6 @@
z-index: 5;
pointer-events: none;
}
/* Styles for motion detection controls */
.motion-controls {
position: absolute;
@ -126,17 +118,14 @@
display: flex;
align-items: center;
}
.motion-controls label {
margin-right: 0px;
cursor: pointer;
}
.motion-controls input[type="checkbox"] {
margin-right: 0px;
cursor: pointer;
}
.motion-controls input[type="range"] {
width: 120px;
height: 8px;
@ -162,41 +151,71 @@
pointer-events: none; /* Allow clicks through */
animation: pulse 0.5s infinite alternate;
}
@keyframes pulse {
from { transform: translate(-50%, -50%) scale(1); opacity: 0.7; }
to { transform: translate(-50%, -50%) scale(1.2); opacity: 0.3; }
}
/* Timer styles */
.timer-controls {
position: absolute;
top: 44px;
left: 5px;
background: rgba(0,0,0,0.4);
padding: 5px;
border-radius: 3px;
z-index: 10;
color: #00A;
font-size: 12px;
display: none; /* Hidden by default */
}
.timer-controls.visible {
display: block;
}
.timer-controls input {
width: 40px;
margin-left: 5px;
background: #333;
color: white;
border: 1px solid #555;
}
.timer-display {
margin-left: 10px;
font-weight: bold;
color: #ff9900;
}
.timer-controls button {
margin-left: 5px;
padding: 2px 6px;
background: #444;
color: white;
border: none;
cursor: pointer;
font-size: 10px;
}
</style>
</head>
<body>
<div id="toolbar"></div>
<script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>
<script>
let isShiftPressed = false;
let audioContext = null; // Global AudioContext
let lastBeepTime = 0;
const beepCooldown = 1000; // 1 second cooldown
// --- DEBUGGING ---
const DEBUG_MOTION = true; // Set to true to enable detailed motion logs
function debugLog(message) {
if (DEBUG_MOTION) {
console.log(`[${new Date().toLocaleTimeString()}] ${message}`);
}
}
// --- END DEBUGGING ---
// load settings with keypress ('r' and 'b' unavailable)
const keyConfigMap = [
{ key: 'm', config: 'main' },
{ key: 's', config: 'small' },
{ key: 'f', config: 'front' },
];
window.addEventListener('keydown', e => {
if (e.key === 'Shift') isShiftPressed = true;
if (e.key === 'b') {
@ -206,7 +225,6 @@
tb.style.display = (currentDisplay === 'none' || tb.style.display === 'none') ? 'block' : 'none';
}
if (e.key === 'r') resetCameras();
const match = keyConfigMap.find(item => e.key === item.key);
if (match) {
const data = localStorage.getItem('cameraState_' + match.config);
@ -217,16 +235,13 @@
}
}
});
window.addEventListener('keyup', e => { if (e.key === 'Shift') isShiftPressed = false; });
window.addEventListener('wheel', e => { if (e.ctrlKey) e.preventDefault(); }, { passive: false });
// ADD YOUR CAMERA IPs HERE
const cameras = [
{ src: "http://192.168.1.100/jpg", id: "Front" },
{ src: "http://192.168.1.101/jpg", id: "Side" },
{ src: "http://192.168.1.102/jpg", id: "Back" }
];
{ src: "http://192.168.1.200/jpg", id: "Side" }
const toolbar = document.getElementById('toolbar');
const camButtonContainer = document.createElement('span');
@ -236,14 +251,12 @@
camButtonContainer.style.zIndex = '2000'; // Ensure toolbar buttons are above camera windows
toolbar.appendChild(camButtonContainer);
cameras.forEach(cam => {
const btn = document.createElement('button');
btn.textContent = cam.id;
btn.onclick = () => createCameraWindow(cam);
camButtonContainer.appendChild(btn);
});
function createButton(text, onClick, marginLeft = '5px') {
const btn = document.createElement('button');
btn.textContent = text;
@ -252,12 +265,10 @@
camButtonContainer.appendChild(btn);
return btn;
}
createButton('Save', () => {
const name = prompt('Enter config name:');
if (name) localStorage.setItem('cameraState_' + name, localStorage.getItem('cameraState'));
}, '20px');
createButton('Load', () => {
const name = prompt('Enter config name:');
if (!name) return;
@ -269,7 +280,6 @@
alert(`Config "${name}" not found.`);
}
});
createButton('Export', () => {
const data = localStorage.getItem('cameraState');
if (data) {
@ -283,7 +293,6 @@
alert('No current state to export.');
}
});
createButton('Import', () => {
const input = document.createElement('input');
input.type = 'file';
@ -306,10 +315,8 @@
};
input.click();
});
createButton('Reset', resetCameras, '20px');
createButton('About', showInstructions, '20px');
function showInstructions() {
alert(`CCTV Camera Viewer with Motion Detection
- Click camera button to open its feed.
@ -318,6 +325,8 @@
- SHIFT + Scroll to zoom, SHIFT + drag to pan.
- Motion Detection: Check box to enable;
use slider for sensitivity.
- Timer: Set countdown (1-120 mins) when motion is enabled.
Timer will auto-disable motion when it reaches zero.
- 'b' hide/show the toolbar.
- Quick load configs:
'm' Main, 's' Side, 'f' Front.
@ -325,23 +334,31 @@
- Specify config on URL: ?config=
`);
}
function closeCameraWindow(buttonElement) {
const container = buttonElement.parentElement;
if (container && container._zoomPanState) {
debugLog(`Closing camera ${container.id}. Setting isActive=false.`);
container._zoomPanState.isActive = false; // Signal to stop fetching
// Clear any active timer
if (container._zoomPanState.timerInterval) {
clearInterval(container._zoomPanState.timerInterval);
}
}
if (container) container.remove();
saveState();
}
function resetCameras() {
debugLog('Resetting all cameras.');
document.querySelectorAll('.img-container').forEach(el => {
if (el._zoomPanState) {
debugLog(`Reset: Setting isActive=false for ${el.id}`);
el._zoomPanState.isActive = false;
// Clear any active timer
if (el._zoomPanState.timerInterval) {
clearInterval(el._zoomPanState.timerInterval);
}
}
el.remove();
});
@ -353,7 +370,6 @@
// });
}
function saveState() {
const stateToSave = {};
document.querySelectorAll('.img-container').forEach(c => {
@ -367,37 +383,39 @@
scale: s.scale, panX: s.panX, panY: s.panY,
uniqueId: c.id,
motionEnabled: s.motionEnabled,
motionSensitivity: s.motionSensitivity
motionSensitivity: s.motionSensitivity,
timerMinutes: s.timerMinutes,
timerSeconds: s.timerSeconds,
timerActive: s.timerActive
});
});
localStorage.setItem('cameraState', JSON.stringify(stateToSave));
debugLog('State saved.');
}
function getQueryParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
function loadState(configName) {
debugLog('Loading state...');
// Clear existing cameras
document.querySelectorAll('.img-container').forEach(el => {
if (el._zoomPanState) el._zoomPanState.isActive = false;
if (el._zoomPanState) {
el._zoomPanState.isActive = false;
if (el._zoomPanState.timerInterval) {
clearInterval(el._zoomPanState.timerInterval);
}
}
el.remove();
});
// Get state from URL config or fallback to default
const stateString = configName
? localStorage.getItem('cameraState_' + configName)
: localStorage.getItem('cameraState');
if (!stateString) {
debugLog('No state found to load.');
return;
}
let stateToLoad;
try {
stateToLoad = JSON.parse(stateString);
@ -406,18 +424,14 @@
alert("Error loading saved state.");
return;
}
for (const camDef of cameras) {
(stateToLoad[camDef.id] || []).forEach(cfg => {
createCameraWindow(camDef, cfg);
});
}
debugLog('State loaded.');
}
function initAudio() {
if (!audioContext) {
try {
@ -433,37 +447,31 @@
.catch(e => console.error("Error resuming AudioContext:", e));
}
}
function playBeep(cameraIdentifier = "unknown") {
function playBeep(cameraIdentifier = "unknown", isWarning = false) {
const now = Date.now();
if (!audioContext || audioContext.state !== 'running') {
debugLog(`playBeep (${cameraIdentifier}): AudioContext not ready or not running. State: ${audioContext ? audioContext.state : 'null'}`);
return;
}
if (now - lastBeepTime < beepCooldown) {
if (now - lastBeepTime < beepCooldown && !isWarning) {
// debugLog(`playBeep (${cameraIdentifier}): Beep cooldown active.`); // Can be spammy
return;
}
lastBeepTime = now;
debugLog(`playBeep triggered by ${cameraIdentifier}`);
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, audioContext.currentTime);
// Different sound for warning
oscillator.frequency.setValueAtTime(isWarning ? 440 : 880, audioContext.currentTime);
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); // Reduced volume slightly
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
oscillator.stop(audioContext.currentTime + (isWarning ? 0.5 : 0.1)); // Longer beep for warning
}
document.body.addEventListener('click', initAudio, { once: true });
document.body.addEventListener('keydown', initAudio, { once: true });
function createCameraWindow(cam, saved = null) {
const uniqueId = saved?.uniqueId || `${cam.id}_${Date.now()}`;
// Check if a window with this uniqueId already exists (e.g., from a partial loadState)
@ -471,13 +479,11 @@
debugLog(`Window with ID ${uniqueId} already exists. Skipping creation.`);
return;
}
debugLog(`Creating window for ${cam.id} (unique: ${uniqueId}). Saved: ${!!saved}`);
const div = document.createElement('div');
div.className = 'img-container';
div.id = uniqueId;
div.setAttribute('data-cam-id', cam.id);
if (saved) {
div.style.left = saved.left;
div.style.top = saved.top;
@ -488,7 +494,6 @@
const y = Math.random() * 150 + 20;
div.style = `left:${x}px; top:${y}px; width:320px; height:240px;`;
}
div.innerHTML = `
<button class="close-btn" onclick="closeCameraWindow(this)">×</button>
<div class="cam-title">${cam.id}</div>
@ -499,33 +504,34 @@
<input type="checkbox" id="motion_check_${uniqueId}" class="motion-checkbox">
<input type="range" id="motion_slider_${uniqueId}" class="motion-slider" min="1" max="100" value="50">
</div>
<div class="timer-controls" id="timer_controls_${uniqueId}">
<label for="timer_input_${uniqueId}">Timer:</label>
<input type="number" id="timer_input_${uniqueId}" min="1" max="120" value="30">
<button id="timer_start_${uniqueId}">Start</button>
<button id="timer_stop_${uniqueId}">Stop</button>
<span class="timer-display" id="timer_display_${uniqueId}">00:00</span>
</div>
`;
document.body.appendChild(div);
initContainer(div, cam.src, saved);
}
function compareFrames(ctx1, ctx2, width, height, sensitivity, containerId = "unknown_cam") { // Added containerId for logging
const imgData1 = ctx1.getImageData(0, 0, width, height).data;
const imgData2 = ctx2.getImageData(0, 0, width, height).data;
let diffCount = 0;
// --- MODIFIED PARAMETERS ---
// Original: const threshold = 10;
const threshold = 8; // Lowered from 10. Defines how much a single pixel's grayscale value needs to change.
// This makes individual pixel changes easier to detect.
// Original calculation was: (101 - sensitivity) * (width * height / 400)
// New calculation aims for a less steep sensitivity curve and generally more sensitivity.
// N_scaler: Compresses the effect of the sensitivity slider.
// A higher N_scaler means the slider has a less dramatic effect (curve is flatter).
// Original formula implicitly had N_scaler around 1.0 relative to its 1-100 range.
const N_scaler = 2.0;
// baseDivisor: Affects overall sensitivity. Higher baseDivisor = more sensitive (fewer pixels needed).
// Original was 400.0.
const baseDivisor = 800.0;
// 'sensitivity' is the slider value from 1 (low sensitivity) to 100 (high sensitivity).
// We want 'effectiveSensitivityFactor' to be low for high slider sensitivity, and high for low slider sensitivity.
// When slider sensitivity = 100 (max), effectiveSensitivityFactor = ((100-100)/2.0) + 1.0 = 1.0 (most sensitive factor part).
@ -534,14 +540,12 @@
const requiredDiffPixels = effectiveSensitivityFactor * (width * height / baseDivisor);
// --- END MODIFIED PARAMETERS ---
for (let i = 0; i < imgData1.length; i += 4) {
const gray1 = (imgData1[i] + imgData1[i + 1] + imgData1[i + 2]) / 3;
const gray2 = (imgData2[i] + imgData2[i + 1] + imgData2[i + 2]) / 3;
if (Math.abs(gray1 - gray2) > threshold) diffCount++;
}
const motionDetected = diffCount > requiredDiffPixels;
// For debugging the new values:
// Only log ~10% of the calls to reduce console spam, if DEBUG_MOTION is true.
if (DEBUG_MOTION && Math.random() < 0.1) { // Adjust 0.1 (10%) as needed for logging frequency
@ -549,17 +553,25 @@
}
return motionDetected;
}
function initContainer(container, src, saved) {
const inner = container.querySelector('.img-inner');
const img = inner.querySelector('img');
const motionCheckbox = container.querySelector('.motion-checkbox');
const motionSlider = container.querySelector('.motion-slider');
const motionIndicator = container.querySelector('.motion-indicator');
const motionControlsDiv = container.querySelector('.motion-controls');
// Timer elements
const timerControls = container.querySelector('.timer-controls');
const timerInput = container.querySelector(`#timer_input_${container.id}`);
const timerStart = container.querySelector(`#timer_start_${container.id}`);
const timerStop = container.querySelector(`#timer_stop_${container.id}`);
const timerDisplay = container.querySelector(`#timer_display_${container.id}`);
motionControlsDiv.addEventListener('mousedown', (event) => event.stopPropagation());
motionControlsDiv.addEventListener('pointerdown', (event) => event.stopPropagation());
timerControls.addEventListener('mousedown', (event) => event.stopPropagation());
timerControls.addEventListener('pointerdown', (event) => event.stopPropagation());
const canvas1 = document.createElement('canvas');
const canvas2 = document.createElement('canvas');
@ -570,7 +582,6 @@
canvas1.width = canvas2.width = motionWidth;
canvas1.height = canvas2.height = motionHeight;
let prevImageLoaded = false;
const state = {
scale: saved?.scale || 1, panX: saved?.panX || 0, panY: saved?.panY || 0,
isPanning: false, startX: 0, startY: 0,
@ -579,15 +590,25 @@
motionEnabled: saved?.motionEnabled || false,
motionSensitivity: saved?.motionSensitivity || 50,
lastMotionTime: 0,
isActive: true // NEW: Flag to control the fetch loop
isActive: true, // NEW: Flag to control the fetch loop
// Timer properties
timerMinutes: saved?.timerMinutes || 30,
timerSeconds: saved?.timerSeconds || 0,
timerActive: saved?.timerActive || false,
timerInterval: null
};
container._zoomPanState = state; // Attach state to the DOM element
debugLog(`[${container.id}] Initializing. MotionEnabled: ${state.motionEnabled}, Sensitivity: ${state.motionSensitivity}, IsActive: ${state.isActive}`);
motionCheckbox.checked = state.motionEnabled;
motionSlider.value = state.motionSensitivity;
if (state.motionEnabled) motionSlider.classList.add('visible');
if (state.motionEnabled) {
motionSlider.classList.add('visible');
timerControls.classList.add('visible');
}
// Initialize timer display
updateTimerDisplay();
motionCheckbox.addEventListener('change', () => {
initAudio(); // Ensure audio is ready
@ -595,14 +616,21 @@
debugLog(`[${container.id}] Checkbox changed. Old motionEnabled: ${state.motionEnabled}, New: ${newMotionEnabledState}`);
state.motionEnabled = newMotionEnabledState;
motionSlider.classList.toggle('visible', state.motionEnabled);
timerControls.classList.toggle('visible', state.motionEnabled);
// Reset prevImageLoaded whenever motion is toggled on OR off
// This ensures a clean start when re-enabled and stops lingering comparisons
debugLog(`[${container.id}] Resetting prevImageLoaded to false due to checkbox change.`);
prevImageLoaded = false;
if (!state.motionEnabled) {
motionIndicator.style.display = 'none';
// Stop timer if motion is disabled
if (state.timerInterval) {
clearInterval(state.timerInterval);
state.timerInterval = null;
state.timerActive = false;
updateTimerDisplay();
}
}
saveState();
});
@ -611,16 +639,108 @@
state.motionSensitivity = parseInt(motionSlider.value, 10);
// No saveState() on input for performance, only on change
});
motionSlider.addEventListener('change', () => {
state.motionSensitivity = parseInt(motionSlider.value, 10);
debugLog(`[${container.id}] Sensitivity changed to ${state.motionSensitivity}`);
saveState();
});
// Timer event listeners
timerInput.value = state.timerMinutes;
timerInput.addEventListener('change', () => {
const value = parseInt(timerInput.value, 10);
if (value >= 1 && value <= 120) {
state.timerMinutes = value;
state.timerSeconds = 0;
updateTimerDisplay();
saveState();
} else {
timerInput.value = state.timerMinutes; // Reset to valid value
}
});
timerStart.addEventListener('click', () => {
if (!state.timerActive) {
const minutes = parseInt(timerInput.value, 10);
if (minutes >= 1 && minutes <= 120) {
state.timerMinutes = minutes;
state.timerSeconds = 0;
startTimer();
saveState();
}
}
});
timerStop.addEventListener('click', () => {
if (state.timerInterval) {
clearInterval(state.timerInterval);
state.timerInterval = null;
}
state.timerActive = false;
updateTimerDisplay();
saveState();
});
function startTimer() {
if (state.timerInterval) {
clearInterval(state.timerInterval);
}
state.timerActive = true;
updateTimerDisplay();
state.timerInterval = setInterval(() => {
if (state.timerSeconds > 0) {
state.timerSeconds--;
} else if (state.timerMinutes > 0) {
state.timerMinutes--;
state.timerSeconds = 59;
} else {
// Timer reached zero
clearInterval(state.timerInterval);
state.timerInterval = null;
state.timerActive = false;
// Turn off motion detection
state.motionEnabled = false;
motionCheckbox.checked = false;
motionSlider.classList.remove('visible');
timerControls.classList.remove('visible');
motionIndicator.style.display = 'none';
// Play warning sound
playBeep(container.id, true);
debugLog(`[${container.id}] Timer expired. Motion detection disabled.`);
saveState();
}
updateTimerDisplay();
}, 1000);
}
function updateTimerDisplay() {
const minutes = String(state.timerMinutes).padStart(2, '0');
const seconds = String(state.timerSeconds).padStart(2, '0');
timerDisplay.textContent = `${minutes}:${seconds}`;
// Change color when less than 1 minute remaining
if (state.timerActive && state.timerMinutes === 0 && state.timerSeconds < 60) {
timerDisplay.style.color = '#ff0000';
} else {
timerDisplay.style.color = '#ff9900';
}
}
// If timer was active when saved, restart it
if (state.timerActive) {
startTimer();
}
interact(container)
.draggable({ /* ... listeners ... */ }) // Keep existing interactjs setup
.resizable({ /* ... listeners ... */ }); // Keep existing interactjs setup
// ... (Keep existing interact, draggable, resizable, wheel, mousedown, mousemove, mouseup, clampPan, updateImageTransform, container mousedown listeners) ...
// The following are copy-pasted but should be verified if you made changes there not shown in the original problem
interact(container)
@ -658,7 +778,6 @@
end() { if (state.isActive) saveState(); }
}
});
inner.addEventListener('wheel', e => {
if (!isShiftPressed || !state.isActive) return;
e.preventDefault();
@ -674,7 +793,6 @@
updateImageTransform(img, state);
// No saveState() on wheel, usually saved on mouseup after pan or resize end
});
inner.addEventListener('mousedown', e => {
if (!isShiftPressed || !state.isActive) return;
e.preventDefault();
@ -683,7 +801,6 @@
state.startY = e.clientY - state.panY;
inner.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', e => { // Global listener
if (state.isPanning && state.isActive) { // Check state.isActive here too
state.panX = e.clientX - state.startX;
@ -692,7 +809,6 @@
updateImageTransform(img, state);
}
});
window.addEventListener('mouseup', () => { // Global listener
if (state.isPanning && state.isActive) { // Check state.isActive
state.isPanning = false;
@ -700,7 +816,6 @@
saveState();
}
});
function clampPan() {
if (!state.imgNaturalWidth || !state.imgNaturalHeight) return; // Avoid division by zero if image not loaded
const cw = container.clientWidth, ch = container.clientHeight;
@ -709,16 +824,13 @@
// Allow image to be smaller than container if zoomed out
const maxX = Math.max(0, iw - cw);
const maxY = Math.max(0, ih - ch);
state.panX = Math.max(Math.min(0, state.panX), cw - iw);
state.panY = Math.max(Math.min(0, state.panY), ch - ih);
// If image is smaller than container, center it or keep at 0,0
if (iw < cw) state.panX = (cw - iw) / 2; else state.panX = Math.max(cw - iw, Math.min(0, state.panX));
if (ih < ch) state.panY = (ch - ih) / 2; else state.panY = Math.max(ch - ih, Math.min(0, state.panY));
}
function updateImageTransform(image, s) {
image.style.left = s.panX + 'px';
image.style.top = s.panY + 'px';
@ -727,22 +839,18 @@
image.style.height = s.imgNaturalHeight ? `${s.imgNaturalHeight}px` : 'auto';
}
container.addEventListener('mousedown', () => {
if (!state.isActive) return;
document.querySelectorAll('.img-container').forEach(c => c.classList.remove('active'));
container.classList.add('active');
});
let fetchTimeoutId = null; // To clear timeout if container becomes inactive
function fetchNextFrame() {
if (!state.isActive) {
debugLog(`[${container.id}] fetchNextFrame: state.isActive is false. Stopping loop.`);
if (fetchTimeoutId) clearTimeout(fetchTimeoutId);
return;
}
const imgUrl = `${src}?t=${Date.now()}`;
let requestTimedOut = false; // Renamed from timedOut to avoid conflict
const requestTimeoutId = setTimeout(() => {
@ -750,7 +858,6 @@
debugLog(`[${container.id}] Image request timed out for ${imgUrl}. Retrying.`);
if (state.isActive) fetchNextFrame(); // Retry only if still active
}, 4000);
const tempImg = new Image();
tempImg.crossOrigin = "Anonymous";
tempImg.onload = () => {
@ -762,18 +869,18 @@
}
clearTimeout(requestTimeoutId);
img.src = tempImg.src; // Update the visible image
const existingOverlay = container.querySelector('.error-overlay');
if (existingOverlay) existingOverlay.remove();
state.imgNaturalWidth = tempImg.naturalWidth;
state.imgNaturalHeight = tempImg.naturalHeight;
if (state.motionEnabled && state.imgNaturalWidth > 0 && state.isActive) {
// debugLog(`[${container.id}] Motion detection ACTIVE. prevImageLoaded=${prevImageLoaded}.`); // Spammy
ctx1.drawImage(canvas2, 0, 0, motionWidth, motionHeight); // Old canvas2 -> canvas1
ctx2.drawImage(tempImg, 0, 0, motionWidth, motionHeight); // New image -> canvas2
const sx = -state.panX / state.scale;
const sy = -state.panY / state.scale;
const sw = container.clientWidth / state.scale;
const sh = container.clientHeight / state.scale;
ctx2.drawImage(tempImg, sx, sy, sw, sh, 0, 0, motionWidth, motionHeight);
if (prevImageLoaded) {
// debugLog(`[${container.id}] Comparing frames.`); // Spammy
if (compareFrames(ctx1, ctx2, motionWidth, motionHeight, state.motionSensitivity, container.id)) {
@ -787,11 +894,9 @@
prevImageLoaded = true;
}
}
if (motionIndicator.style.display === 'block' && (Date.now() - state.lastMotionTime > 500)) {
motionIndicator.style.display = 'none';
}
if (!state.initialized && state.imgNaturalWidth > 0) {
debugLog(`[${container.id}] First successful image load. Initializing scale and pan.`);
const scaleX = container.clientWidth / state.imgNaturalWidth;
@ -806,7 +911,6 @@
updateImageTransform(img, state); // Just update if already initialized
}
if (state.isActive) { // Schedule next fetch only if still active
fetchTimeoutId = setTimeout(fetchNextFrame, 600);
}
@ -833,14 +937,11 @@
};
tempImg.src = imgUrl;
}
fetchNextFrame(); // Start the loop
}
// Initial load of state from localStorage when the page loads
const startupConfig = getQueryParam('config');
loadState(startupConfig);
</script>
</body>
</html>