Add separate zoom wrapper layers for all video modes
- Added live-video-wrapper around webcam-player - Added timelapse-wrapper inside timelapse-viewer - Added daily-video-wrapper inside daily-video-player - Zoom now applies to wrapper divs, not video elements directly - Pan works by dragging inside video container when zoomed - Double-click to reset zoom - Cursor changes to grab when zoomed > 1x - Touch support for mobile pan
This commit is contained in:
+38
-15
@@ -152,16 +152,18 @@ class WebcamManager {
|
|||||||
// Zeigt NUR das Video ohne Schnickschnack
|
// Zeigt NUR das Video ohne Schnickschnack
|
||||||
public function displayWebcam() {
|
public function displayWebcam() {
|
||||||
return '
|
return '
|
||||||
<video id="webcam-player"
|
<div id="live-video-wrapper" class="video-zoom-wrapper">
|
||||||
autoplay
|
<video id="webcam-player"
|
||||||
muted
|
autoplay
|
||||||
playsinline
|
muted
|
||||||
webkit-playsinline
|
playsinline
|
||||||
x-webkit-airplay="allow"
|
webkit-playsinline
|
||||||
x5-video-player-type="h5"
|
x-webkit-airplay="allow"
|
||||||
x5-video-player-fullscreen="true"
|
x5-video-player-type="h5"
|
||||||
style="width: 100%; height: 100%; object-fit: contain;">
|
x5-video-player-fullscreen="true"
|
||||||
</video>';
|
style="width: 100%; height: 100%; object-fit: contain;">
|
||||||
|
</video>
|
||||||
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Das ist die neue Anzeige für unten links
|
// Das ist die neue Anzeige für unten links
|
||||||
@@ -1495,13 +1497,30 @@ nav ul li a:hover { color: #4CAF50; }
|
|||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
#webcam-player, #timelapse-viewer, #daily-video-player {
|
#live-video-wrapper, #timelapse-viewer, #daily-video-player {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-zoom-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#webcam-player, #timelapse-image, #daily-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-info-bar {
|
.video-info-bar {
|
||||||
@@ -2044,15 +2063,19 @@ body.theme-neo footer {
|
|||||||
|
|
||||||
<!-- Timelapse Overlay -->
|
<!-- Timelapse Overlay -->
|
||||||
<div id="timelapse-viewer" style="display: none;">
|
<div id="timelapse-viewer" style="display: none;">
|
||||||
<img id="timelapse-image" src="" alt="Timelapse Image" style="width: 100%; height: 100%; object-fit: cover;">
|
<div id="timelapse-wrapper" class="video-zoom-wrapper">
|
||||||
|
<img id="timelapse-image" src="" alt="Timelapse Image" style="width: 100%; height: 100%; object-fit: cover;">
|
||||||
|
</div>
|
||||||
<div id="timelapse-time-overlay"></div>
|
<div id="timelapse-time-overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Daily Video Player (für Tagesvideos) -->
|
<!-- Daily Video Player (für Tagesvideos) -->
|
||||||
<div id="daily-video-player" style="display: none;">
|
<div id="daily-video-player" style="display: none;">
|
||||||
<video id="daily-video" controls playsinline style="width: 100%; height: 100%; object-fit: contain;">
|
<div id="daily-video-wrapper" class="video-zoom-wrapper">
|
||||||
<source src="" type="video/mp4">
|
<video id="daily-video" controls playsinline style="width: 100%; height: 100%; object-fit: contain;">
|
||||||
</video>
|
<source src="" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Video Zoom & Pan Controller
|
* Video Zoom & Pan Controller
|
||||||
* - Zoom für alle Video-Modi (Live, Timelapse, Tagesvideo)
|
* Zoomt auf Wrapper-Layer statt direkt auf Video-Elemente
|
||||||
* - Pan-Funktion: Mit Maus den gezoomten Bereich verschieben
|
|
||||||
*/
|
*/
|
||||||
(() => {
|
(() => {
|
||||||
const config = window.zoomConfig || {};
|
const config = window.zoomConfig || {};
|
||||||
@@ -11,60 +10,75 @@
|
|||||||
let panX = 0;
|
let panX = 0;
|
||||||
let panY = 0;
|
let panY = 0;
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let startX = 0;
|
let lastX = 0;
|
||||||
let startY = 0;
|
let lastY = 0;
|
||||||
|
|
||||||
const minZoom = Number(config.minZoom || 1);
|
const minZoom = Number(config.minZoom || 1);
|
||||||
const maxZoom = Number(config.maxZoom || 4);
|
const maxZoom = Number(config.maxZoom || 4);
|
||||||
const defaultZoom = Number(config.defaultZoom || 1);
|
|
||||||
|
|
||||||
const slider = document.getElementById('zoom-range');
|
const slider = document.getElementById('zoom-range');
|
||||||
const valueEl = document.getElementById('zoom-value');
|
const valueEl = document.getElementById('zoom-value');
|
||||||
|
|
||||||
// Finde das aktuell aktive Video-Element
|
// Wrapper-IDs für jeden Modus
|
||||||
function getActiveTarget() {
|
const wrapperIds = ['live-video-wrapper', 'timelapse-wrapper', 'daily-video-wrapper'];
|
||||||
const webcam = document.getElementById('webcam-player');
|
|
||||||
const timelapse = document.getElementById('timelapse-image');
|
|
||||||
const daily = document.getElementById('daily-video');
|
|
||||||
const timelapseViewer = document.getElementById('timelapse-viewer');
|
|
||||||
const dailyPlayer = document.getElementById('daily-video-player');
|
|
||||||
|
|
||||||
// Prüfe welches Element sichtbar ist
|
// Finde den aktuell sichtbaren Wrapper
|
||||||
if (dailyPlayer && dailyPlayer.style.display !== 'none' && daily) {
|
function getActiveWrapper() {
|
||||||
return daily;
|
// Prüfe daily-video-player
|
||||||
|
const dailyPlayer = document.getElementById('daily-video-player');
|
||||||
|
if (dailyPlayer && dailyPlayer.style.display !== 'none') {
|
||||||
|
return document.getElementById('daily-video-wrapper');
|
||||||
}
|
}
|
||||||
if (timelapseViewer && timelapseViewer.style.display !== 'none' && timelapse) {
|
|
||||||
return timelapse;
|
// Prüfe timelapse-viewer
|
||||||
|
const timelapseViewer = document.getElementById('timelapse-viewer');
|
||||||
|
if (timelapseViewer && timelapseViewer.style.display !== 'none') {
|
||||||
|
return document.getElementById('timelapse-wrapper');
|
||||||
}
|
}
|
||||||
if (webcam) {
|
|
||||||
return webcam;
|
// Fallback: Live-Video
|
||||||
}
|
return document.getElementById('live-video-wrapper');
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wende Zoom und Pan auf das aktive Element an
|
// Wende Transform auf ALLE Wrapper an (damit beim Wechsel der Zoom erhalten bleibt)
|
||||||
function applyTransform() {
|
function applyTransform() {
|
||||||
const target = getActiveTarget();
|
// Bei Zoom 1x: Kein Pan
|
||||||
if (!target) return;
|
|
||||||
|
|
||||||
// Bei Zoom 1x: Kein Pan erlaubt
|
|
||||||
if (currentZoom <= 1) {
|
if (currentZoom <= 1) {
|
||||||
panX = 0;
|
panX = 0;
|
||||||
panY = 0;
|
panY = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begrenzen der Pan-Werte basierend auf Zoom
|
// Pan begrenzen basierend auf Zoom
|
||||||
const maxPan = (currentZoom - 1) * 50; // Prozent
|
const maxPan = (currentZoom - 1) * 50;
|
||||||
panX = Math.max(-maxPan, Math.min(maxPan, panX));
|
panX = Math.max(-maxPan, Math.min(maxPan, panX));
|
||||||
panY = Math.max(-maxPan, Math.min(maxPan, panY));
|
panY = Math.max(-maxPan, Math.min(maxPan, panY));
|
||||||
|
|
||||||
target.style.transform = `scale(${currentZoom}) translate(${panX}%, ${panY}%)`;
|
// Transform auf alle Wrapper anwenden
|
||||||
target.style.transformOrigin = 'center center';
|
wrapperIds.forEach(id => {
|
||||||
target.style.transition = isDragging ? 'none' : 'transform 0.2s ease';
|
const wrapper = document.getElementById(id);
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.style.transform = `scale(${currentZoom}) translate(${panX}%, ${panY}%)`;
|
||||||
|
wrapper.style.transition = isDragging ? 'none' : 'transform 0.15s ease-out';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update UI
|
// UI Update
|
||||||
if (valueEl) valueEl.textContent = `${currentZoom.toFixed(1)}x`;
|
if (valueEl) valueEl.textContent = `${currentZoom.toFixed(1)}x`;
|
||||||
if (slider) slider.value = currentZoom;
|
if (slider) slider.value = currentZoom;
|
||||||
|
|
||||||
|
// Cursor Update
|
||||||
|
updateCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCursor() {
|
||||||
|
const container = document.querySelector('.video-container');
|
||||||
|
if (container) {
|
||||||
|
if (currentZoom > 1) {
|
||||||
|
container.classList.add('zoomed');
|
||||||
|
} else {
|
||||||
|
container.classList.remove('zoomed');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zoom setzen
|
// Zoom setzen
|
||||||
@@ -91,57 +105,68 @@
|
|||||||
const container = document.querySelector('.video-container');
|
const container = document.querySelector('.video-container');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
// Mousedown - Start dragging
|
||||||
container.addEventListener('mousedown', (e) => {
|
container.addEventListener('mousedown', (e) => {
|
||||||
if (currentZoom <= 1) return;
|
if (currentZoom <= 1) return;
|
||||||
|
// Ignoriere Klicks auf Controls
|
||||||
|
if (e.target.closest('.zoom-controls, button, a')) return;
|
||||||
|
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
startX = e.clientX;
|
lastX = e.clientX;
|
||||||
startY = e.clientY;
|
lastY = e.clientY;
|
||||||
container.style.cursor = 'grabbing';
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mousemove - Dragging
|
||||||
document.addEventListener('mousemove', (e) => {
|
document.addEventListener('mousemove', (e) => {
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
const dx = (e.clientX - startX) / 5; // Sensitivität anpassen
|
const deltaX = e.clientX - lastX;
|
||||||
const dy = (e.clientY - startY) / 5;
|
const deltaY = e.clientY - lastY;
|
||||||
|
|
||||||
panX += dx / currentZoom;
|
// Sensitivität basierend auf Zoom
|
||||||
panY += dy / currentZoom;
|
const sensitivity = 0.15 / currentZoom;
|
||||||
|
panX += deltaX * sensitivity;
|
||||||
|
panY += deltaY * sensitivity;
|
||||||
|
|
||||||
startX = e.clientX;
|
lastX = e.clientX;
|
||||||
startY = e.clientY;
|
lastY = e.clientY;
|
||||||
|
|
||||||
applyTransform();
|
applyTransform();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mouseup - Stop dragging
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => {
|
||||||
if (isDragging) {
|
isDragging = false;
|
||||||
isDragging = false;
|
});
|
||||||
const container = document.querySelector('.video-container');
|
|
||||||
if (container) container.style.cursor = currentZoom > 1 ? 'grab' : 'default';
|
// Mouse leave
|
||||||
}
|
document.addEventListener('mouseleave', () => {
|
||||||
|
isDragging = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Touch Events für Mobile
|
// Touch Events für Mobile
|
||||||
container.addEventListener('touchstart', (e) => {
|
container.addEventListener('touchstart', (e) => {
|
||||||
if (currentZoom <= 1 || e.touches.length !== 1) return;
|
if (currentZoom <= 1 || e.touches.length !== 1) return;
|
||||||
|
if (e.target.closest('.zoom-controls, button, a')) return;
|
||||||
|
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
startX = e.touches[0].clientX;
|
lastX = e.touches[0].clientX;
|
||||||
startY = e.touches[0].clientY;
|
lastY = e.touches[0].clientY;
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
container.addEventListener('touchmove', (e) => {
|
container.addEventListener('touchmove', (e) => {
|
||||||
if (!isDragging || e.touches.length !== 1) return;
|
if (!isDragging || e.touches.length !== 1) return;
|
||||||
|
|
||||||
const dx = (e.touches[0].clientX - startX) / 5;
|
const deltaX = e.touches[0].clientX - lastX;
|
||||||
const dy = (e.touches[0].clientY - startY) / 5;
|
const deltaY = e.touches[0].clientY - lastY;
|
||||||
|
|
||||||
panX += dx / currentZoom;
|
const sensitivity = 0.15 / currentZoom;
|
||||||
panY += dy / currentZoom;
|
panX += deltaX * sensitivity;
|
||||||
|
panY += deltaY * sensitivity;
|
||||||
|
|
||||||
startX = e.touches[0].clientX;
|
lastX = e.touches[0].clientX;
|
||||||
startY = e.touches[0].clientY;
|
lastY = e.touches[0].clientY;
|
||||||
|
|
||||||
applyTransform();
|
applyTransform();
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
@@ -150,25 +175,28 @@
|
|||||||
isDragging = false;
|
isDragging = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cursor anpassen bei Zoom
|
// Doppelklick zum Zurücksetzen
|
||||||
container.style.cursor = 'default';
|
container.addEventListener('dblclick', (e) => {
|
||||||
|
if (e.target.closest('.zoom-controls, button, a')) return;
|
||||||
|
resetZoom();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slider Events
|
// Slider Setup
|
||||||
function setupSlider() {
|
function setupSlider() {
|
||||||
if (!slider) return;
|
if (!slider) return;
|
||||||
|
|
||||||
slider.min = minZoom;
|
slider.min = minZoom;
|
||||||
slider.max = maxZoom;
|
slider.max = maxZoom;
|
||||||
slider.step = 0.5;
|
slider.step = 0.5;
|
||||||
slider.value = defaultZoom;
|
slider.value = 1;
|
||||||
|
|
||||||
slider.addEventListener('input', (e) => {
|
slider.addEventListener('input', (e) => {
|
||||||
setZoom(Number(e.target.value));
|
setZoom(Number(e.target.value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globale Funktionen für Buttons
|
// Globale Funktionen
|
||||||
window.adjustZoom = adjustZoom;
|
window.adjustZoom = adjustZoom;
|
||||||
window.resetZoom = resetZoom;
|
window.resetZoom = resetZoom;
|
||||||
window.setZoom = setZoom;
|
window.setZoom = setZoom;
|
||||||
@@ -177,31 +205,11 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
setupSlider();
|
setupSlider();
|
||||||
setupPanEvents();
|
setupPanEvents();
|
||||||
currentZoom = defaultZoom;
|
|
||||||
|
|
||||||
// Warte kurz, damit Video-Elemente geladen sind
|
// Initial State
|
||||||
setTimeout(() => {
|
currentZoom = 1;
|
||||||
applyTransform();
|
applyTransform();
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// Update Cursor bei Zoom-Änderung
|
console.log('Video Zoom & Pan initialized');
|
||||||
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