262 lines
7.7 KiB
HTML
262 lines
7.7 KiB
HTML
<html>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Video Compare</title>
|
|
<style>
|
|
body {
|
|
background: #333;
|
|
margin: 4px;
|
|
}
|
|
#video-compare-container {
|
|
display: inline-block;
|
|
line-height: 0;
|
|
position: relative;
|
|
width: 100%;
|
|
padding-top: 42.3%;
|
|
overflow: hidden;
|
|
}
|
|
.zoom-layer {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
transform-origin: 0 0;
|
|
will-change: transform;
|
|
}
|
|
#video-compare-container > div > video {
|
|
width: 100%;
|
|
position: absolute;
|
|
top: 0;
|
|
height: 100%;
|
|
}
|
|
#video-clipper {
|
|
width: 50%;
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
overflow: hidden;
|
|
border-right: 1px dashed #ccc;
|
|
}
|
|
#video-clipper video {
|
|
width: 200%;
|
|
position: absolute;
|
|
height: 100%;
|
|
}
|
|
#title1 {
|
|
color: #ccc;
|
|
float: right;
|
|
}
|
|
#title2 {
|
|
color: #ccc;
|
|
float: left;
|
|
}
|
|
.controls {
|
|
margin: 5px 0;
|
|
text-align: center;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.controls button {
|
|
padding: 5px;
|
|
background: #444;
|
|
color: white;
|
|
border: 1px solid #666;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
.controls button:hover {
|
|
background: #555;
|
|
}
|
|
.controls input {
|
|
padding: 4px;
|
|
flex: 1;
|
|
max-width: 49%;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body translate="no">
|
|
<div class="controls">
|
|
<button id="reload-btn">Reload</button>
|
|
<button id="pause-btn">Pause</button>
|
|
<input
|
|
type="text"
|
|
id="videoA-url"
|
|
placeholder="Video A URL"
|
|
value="https://s3-us-west-2.amazonaws.com/s.cdpn.io/4273/floodplain-dirty.mp4"
|
|
/>
|
|
<input
|
|
type="text"
|
|
id="videoB-url"
|
|
placeholder="Video B URL"
|
|
value="https://s3-us-west-2.amazonaws.com/s.cdpn.io/4273/floodplain-clean.mp4"
|
|
/>
|
|
</div>
|
|
|
|
<div id="video-compare-container">
|
|
<div id="zoom-layer" class="zoom-layer">
|
|
<video loop="" autoplay="" muted="">
|
|
<source
|
|
src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/4273/floodplain-clean.mp4"
|
|
/>
|
|
</video>
|
|
<div id="video-clipper">
|
|
<video loop="" autoplay="" muted="" style="width: 200%; z-index: 3">
|
|
<source
|
|
src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/4273/floodplain-dirty.mp4"
|
|
/>
|
|
</video>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script id="rendered-js">
|
|
var videoContainer = document.getElementById("video-compare-container"),
|
|
zoomLayer = document.getElementById("zoom-layer"),
|
|
videoClipper = document.getElementById("video-clipper"),
|
|
clippedVideo = videoClipper.getElementsByTagName("video")[0];
|
|
|
|
var videoFg = clippedVideo; // Foreground/Clipper (Left) -> Video A
|
|
var videoBg = videoContainer.getElementsByTagName("video")[0]; // Background (Right) -> Video B
|
|
|
|
var videoA = videoFg;
|
|
var videoB = videoBg;
|
|
|
|
function reloadVideos() {
|
|
var videoAUrl = document.getElementById('videoA-url').value;
|
|
var videoBUrl = document.getElementById('videoB-url').value;
|
|
|
|
videoA.getElementsByTagName('source')[0].src = videoAUrl;
|
|
videoB.getElementsByTagName('source')[0].src = videoBUrl;
|
|
|
|
videoA.load();
|
|
videoB.load();
|
|
pauseBtn.innerText = "Pause";
|
|
scale = 1; panX = 0; panY = 0; updateTransform();
|
|
}
|
|
|
|
// Unified Sync Logic
|
|
function linkVideos(vMaster, vSlave) {
|
|
// Event Listeners
|
|
vMaster.addEventListener('play', () => { vSlave.play(); updatePlayBtn(); });
|
|
vMaster.addEventListener('pause', () => { vSlave.pause(); updatePlayBtn(); });
|
|
vMaster.addEventListener('seeking', () => { vSlave.currentTime = vMaster.currentTime; });
|
|
vMaster.addEventListener('seeked', () => { vSlave.currentTime = vMaster.currentTime; });
|
|
vMaster.addEventListener('ratechange', () => { vSlave.playbackRate = vMaster.playbackRate; });
|
|
|
|
// Sync Loop
|
|
function loop() {
|
|
if (!vMaster.paused && !vSlave.paused) {
|
|
const diff = vSlave.currentTime - vMaster.currentTime;
|
|
if (Math.abs(diff) > 0.04) {
|
|
vSlave.currentTime = vMaster.currentTime;
|
|
}
|
|
}
|
|
requestAnimationFrame(loop);
|
|
}
|
|
loop();
|
|
}
|
|
|
|
// Initialize Sync
|
|
linkVideos(videoFg, videoBg); // Fg is master (A), Bg is slave (B)
|
|
|
|
const pauseBtn = document.getElementById('pause-btn');
|
|
function updatePlayBtn() {
|
|
pauseBtn.innerText = videoFg.paused ? "Play" : "Pause";
|
|
}
|
|
function togglePause() {
|
|
if (videoFg.paused) videoFg.play();
|
|
else videoFg.pause();
|
|
}
|
|
pauseBtn.addEventListener('click', togglePause);
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.code === 'Space') {
|
|
e.preventDefault();
|
|
togglePause();
|
|
}
|
|
});
|
|
|
|
let scale = 1,
|
|
panX = 0,
|
|
panY = 0,
|
|
isDragging = false,
|
|
lastX = 0,
|
|
lastY = 0;
|
|
|
|
function updateTransform() {
|
|
zoomLayer.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
|
}
|
|
|
|
function trackLocation(e) {
|
|
if (isDragging) return;
|
|
var rect = zoomLayer.getBoundingClientRect(),
|
|
clientX = e.touches ? e.touches[0].clientX : e.clientX,
|
|
position = ((clientX - rect.left) / rect.width) * 100;
|
|
if (position >= 0 && position <= 100) {
|
|
videoClipper.style.width = position + "%";
|
|
videoFg.style.width = (100 / position) * 100 + "%";
|
|
videoFg.style.zIndex = 3;
|
|
}
|
|
}
|
|
|
|
videoContainer.addEventListener("mousemove", (e) => {
|
|
if (isDragging) {
|
|
const dx = e.clientX - lastX;
|
|
const dy = e.clientY - lastY;
|
|
lastX = e.clientX;
|
|
lastY = e.clientY;
|
|
panX += dx;
|
|
panY += dy;
|
|
updateTransform();
|
|
} else {
|
|
trackLocation(e);
|
|
}
|
|
});
|
|
videoContainer.addEventListener("touchstart", trackLocation, false);
|
|
videoContainer.addEventListener("touchmove", trackLocation, false);
|
|
|
|
videoContainer.addEventListener("mousedown", (e) => {
|
|
if (e.button === 2) {
|
|
// Right click
|
|
isDragging = true;
|
|
lastX = e.clientX;
|
|
lastY = e.clientY;
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
videoContainer.addEventListener("mouseup", () => {
|
|
isDragging = false;
|
|
});
|
|
videoContainer.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
videoContainer.addEventListener(
|
|
"wheel",
|
|
(e) => {
|
|
e.preventDefault();
|
|
const zoomSpeed = 0.1;
|
|
const oldScale = scale;
|
|
if (e.deltaY < 0) scale *= 1 + zoomSpeed;
|
|
else scale /= 1 + zoomSpeed;
|
|
scale = Math.max(0.1, Math.min(scale, 10));
|
|
|
|
const rect = videoContainer.getBoundingClientRect();
|
|
const mx = e.clientX - rect.left;
|
|
const my = e.clientY - rect.top;
|
|
const rx = (mx - panX) / oldScale;
|
|
const ry = (my - panY) / oldScale;
|
|
panX = mx - rx * scale;
|
|
panY = my - ry * scale;
|
|
updateTransform();
|
|
},
|
|
{ passive: false },
|
|
);
|
|
|
|
document
|
|
.getElementById("reload-btn")
|
|
.addEventListener("click", reloadVideos);
|
|
// cleanup old sync
|
|
// setInterval(syncVideos, 100);
|
|
</script>
|
|
</body>
|
|
</html>
|