Add zoom & pan for all video modes
- Zoom now works for livestream, timelapse and daily videos - Added pan function: drag to move zoomed area with mouse - Added touch support for mobile pan - Added +/- zoom buttons and reset button - Reduced max zoom from 100x to 4x - Dynamically detects active video element - Pan limits based on zoom level - Cursor changes to grab when zoomed
This commit is contained in:
@@ -1553,10 +1553,41 @@ nav ul li a:hover { color: #4CAF50; }
|
|||||||
}
|
}
|
||||||
.zoom-value {
|
.zoom-value {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
min-width: 60px;
|
min-width: 50px;
|
||||||
text-align: right;
|
text-align: center;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
.zoom-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.zoom-btn:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
.zoom-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
.video-container {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.video-container.zoomed {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.video-container.zoomed:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
.tech-stat { justify-self: start; font-family: monospace; color: #555; }
|
.tech-stat { justify-self: start; font-family: monospace; color: #555; }
|
||||||
.bitrate-icon { color: #4CAF50; }
|
.bitrate-icon { color: #4CAF50; }
|
||||||
@@ -2031,9 +2062,11 @@ body.theme-neo footer {
|
|||||||
<!--
|
<!--
|
||||||
CONTROLS -->
|
CONTROLS -->
|
||||||
<div id="zoom-controls" class="zoom-controls" aria-label="Zoom Steuerung">
|
<div id="zoom-controls" class="zoom-controls" aria-label="Zoom Steuerung">
|
||||||
<label for="zoom-range">Zoom</label>
|
<button type="button" onclick="adjustZoom(-0.5)" class="zoom-btn" title="Zoom out">−</button>
|
||||||
<input type="range" id="zoom-range" class="zoom-slider" min="1" max="100" value="1" step="1">
|
<input type="range" id="zoom-range" class="zoom-slider" min="1" max="4" value="1" step="0.5">
|
||||||
<span id="zoom-value" class="zoom-value">1x</span>
|
<span id="zoom-value" class="zoom-value">1.0x</span>
|
||||||
|
<button type="button" onclick="adjustZoom(0.5)" class="zoom-btn" title="Zoom in">+</button>
|
||||||
|
<button type="button" onclick="resetZoom()" class="zoom-btn" title="Reset">⟲</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VIDEO PLAYER CONTROLS (für Tagesvideos) -->
|
<!-- VIDEO PLAYER CONTROLS (für Tagesvideos) -->
|
||||||
@@ -2260,7 +2293,7 @@ document.getElementById('qrcode')?.addEventListener('click', function() {
|
|||||||
window.zoomConfig = {
|
window.zoomConfig = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
minZoom: 1,
|
minZoom: 1,
|
||||||
maxZoom: 100,
|
maxZoom: 4,
|
||||||
defaultZoom: 1
|
defaultZoom: 1
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+192
-22
@@ -1,37 +1,207 @@
|
|||||||
|
/**
|
||||||
|
* Video Zoom & Pan Controller
|
||||||
|
* - Zoom für alle Video-Modi (Live, Timelapse, Tagesvideo)
|
||||||
|
* - Pan-Funktion: Mit Maus den gezoomten Bereich verschieben
|
||||||
|
*/
|
||||||
(() => {
|
(() => {
|
||||||
const config = window.zoomConfig || {};
|
const config = window.zoomConfig || {};
|
||||||
if (!config.enabled) return;
|
if (!config.enabled) return;
|
||||||
|
|
||||||
const slider = document.getElementById('zoom-range');
|
let currentZoom = 1;
|
||||||
const valueEl = document.getElementById('zoom-value');
|
let panX = 0;
|
||||||
if (!slider || !valueEl) return;
|
let panY = 0;
|
||||||
|
let isDragging = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
|
||||||
const minZoom = Number(config.minZoom || 1);
|
const minZoom = Number(config.minZoom || 1);
|
||||||
const maxZoom = Number(config.maxZoom || 100);
|
const maxZoom = Number(config.maxZoom || 4);
|
||||||
const defaultZoom = Number(config.defaultZoom || 1);
|
const defaultZoom = Number(config.defaultZoom || 1);
|
||||||
|
|
||||||
slider.min = minZoom;
|
const slider = document.getElementById('zoom-range');
|
||||||
slider.max = maxZoom;
|
const valueEl = document.getElementById('zoom-value');
|
||||||
slider.value = defaultZoom;
|
|
||||||
|
|
||||||
const targets = [
|
// Finde das aktuell aktive Video-Element
|
||||||
document.getElementById('webcam-player'),
|
function getActiveTarget() {
|
||||||
document.getElementById('timelapse-image'),
|
const webcam = document.getElementById('webcam-player');
|
||||||
document.getElementById('daily-video')
|
const timelapse = document.getElementById('timelapse-image');
|
||||||
].filter(Boolean);
|
const daily = document.getElementById('daily-video');
|
||||||
|
const timelapseViewer = document.getElementById('timelapse-viewer');
|
||||||
|
const dailyPlayer = document.getElementById('daily-video-player');
|
||||||
|
|
||||||
const applyZoom = (zoomValue) => {
|
// Prüfe welches Element sichtbar ist
|
||||||
const zoom = Math.max(minZoom, Math.min(maxZoom, zoomValue));
|
if (dailyPlayer && dailyPlayer.style.display !== 'none' && daily) {
|
||||||
valueEl.textContent = `${zoom}x`;
|
return daily;
|
||||||
targets.forEach((el) => {
|
}
|
||||||
el.style.transform = `scale(${zoom})`;
|
if (timelapseViewer && timelapseViewer.style.display !== 'none' && timelapse) {
|
||||||
el.style.transformOrigin = 'center center';
|
return timelapse;
|
||||||
|
}
|
||||||
|
if (webcam) {
|
||||||
|
return webcam;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wende Zoom und Pan auf das aktive Element an
|
||||||
|
function applyTransform() {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
// Bei Zoom 1x: Kein Pan erlaubt
|
||||||
|
if (currentZoom <= 1) {
|
||||||
|
panX = 0;
|
||||||
|
panY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begrenzen der Pan-Werte basierend auf Zoom
|
||||||
|
const maxPan = (currentZoom - 1) * 50; // Prozent
|
||||||
|
panX = Math.max(-maxPan, Math.min(maxPan, panX));
|
||||||
|
panY = Math.max(-maxPan, Math.min(maxPan, panY));
|
||||||
|
|
||||||
|
target.style.transform = `scale(${currentZoom}) translate(${panX}%, ${panY}%)`;
|
||||||
|
target.style.transformOrigin = 'center center';
|
||||||
|
target.style.transition = isDragging ? 'none' : 'transform 0.2s ease';
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
if (valueEl) valueEl.textContent = `${currentZoom.toFixed(1)}x`;
|
||||||
|
if (slider) slider.value = currentZoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom setzen
|
||||||
|
function setZoom(value) {
|
||||||
|
currentZoom = Math.max(minZoom, Math.min(maxZoom, value));
|
||||||
|
applyTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom anpassen
|
||||||
|
function adjustZoom(delta) {
|
||||||
|
setZoom(currentZoom + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom zurücksetzen
|
||||||
|
function resetZoom() {
|
||||||
|
currentZoom = 1;
|
||||||
|
panX = 0;
|
||||||
|
panY = 0;
|
||||||
|
applyTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse Events für Pan
|
||||||
|
function setupPanEvents() {
|
||||||
|
const container = document.querySelector('.video-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.addEventListener('mousedown', (e) => {
|
||||||
|
if (currentZoom <= 1) return;
|
||||||
|
isDragging = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
container.style.cursor = 'grabbing';
|
||||||
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
applyZoom(defaultZoom);
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
slider.addEventListener('input', (event) => {
|
const dx = (e.clientX - startX) / 5; // Sensitivität anpassen
|
||||||
applyZoom(Number(event.target.value));
|
const dy = (e.clientY - startY) / 5;
|
||||||
|
|
||||||
|
panX += dx / currentZoom;
|
||||||
|
panY += dy / currentZoom;
|
||||||
|
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
|
||||||
|
applyTransform();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
const container = document.querySelector('.video-container');
|
||||||
|
if (container) container.style.cursor = currentZoom > 1 ? 'grab' : 'default';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch Events für Mobile
|
||||||
|
container.addEventListener('touchstart', (e) => {
|
||||||
|
if (currentZoom <= 1 || e.touches.length !== 1) return;
|
||||||
|
isDragging = true;
|
||||||
|
startX = e.touches[0].clientX;
|
||||||
|
startY = e.touches[0].clientY;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
container.addEventListener('touchmove', (e) => {
|
||||||
|
if (!isDragging || e.touches.length !== 1) return;
|
||||||
|
|
||||||
|
const dx = (e.touches[0].clientX - startX) / 5;
|
||||||
|
const dy = (e.touches[0].clientY - startY) / 5;
|
||||||
|
|
||||||
|
panX += dx / currentZoom;
|
||||||
|
panY += dy / currentZoom;
|
||||||
|
|
||||||
|
startX = e.touches[0].clientX;
|
||||||
|
startY = e.touches[0].clientY;
|
||||||
|
|
||||||
|
applyTransform();
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
container.addEventListener('touchend', () => {
|
||||||
|
isDragging = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cursor anpassen bei Zoom
|
||||||
|
container.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slider Events
|
||||||
|
function setupSlider() {
|
||||||
|
if (!slider) return;
|
||||||
|
|
||||||
|
slider.min = minZoom;
|
||||||
|
slider.max = maxZoom;
|
||||||
|
slider.step = 0.5;
|
||||||
|
slider.value = defaultZoom;
|
||||||
|
|
||||||
|
slider.addEventListener('input', (e) => {
|
||||||
|
setZoom(Number(e.target.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Globale Funktionen für Buttons
|
||||||
|
window.adjustZoom = adjustZoom;
|
||||||
|
window.resetZoom = resetZoom;
|
||||||
|
window.setZoom = setZoom;
|
||||||
|
|
||||||
|
// Initialisierung
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setupSlider();
|
||||||
|
setupPanEvents();
|
||||||
|
currentZoom = defaultZoom;
|
||||||
|
|
||||||
|
// Warte kurz, damit Video-Elemente geladen sind
|
||||||
|
setTimeout(() => {
|
||||||
|
applyTransform();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Update Cursor bei Zoom-Änderung
|
||||||
|
const container = document.querySelector('.video-container');
|
||||||
|
if (container) {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
container.style.cursor = currentZoom > 1 ? 'grab' : 'default';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bei Moduswechsel Pan zurücksetzen
|
||||||
|
window.addEventListener('click', (e) => {
|
||||||
|
if (e.target.id === 'timelapse-button' ||
|
||||||
|
e.target.closest('#timelapse-button') ||
|
||||||
|
e.target.id === 'dvp-back-live' ||
|
||||||
|
e.target.closest('.play-link')) {
|
||||||
|
panX = 0;
|
||||||
|
panY = 0;
|
||||||
|
setTimeout(applyTransform, 100);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user