mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-09-25 23:06:51 +00:00
CSS/JS For image comparison
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Interactive Image Comparison Slider
|
||||
*
|
||||
* Allows users to drag a vertical slider to reveal differences between
|
||||
* two images (before/after comparison).
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function initComparisonSlider() {
|
||||
const container = document.getElementById('comparison-container');
|
||||
const slider = document.getElementById('comparison-slider');
|
||||
const afterWrapper = container ? container.querySelector('.comparison-after') : null;
|
||||
const handle = slider ? slider.querySelector('.comparison-handle') : null;
|
||||
|
||||
if (!container || !slider || !afterWrapper || !handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isDragging = false;
|
||||
let isMouseOverContainer = false;
|
||||
let mouseY = 0;
|
||||
|
||||
/**
|
||||
* Update handle position - follows mouse if hovering, or viewport center
|
||||
*/
|
||||
function updateHandlePosition() {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
let handleTop;
|
||||
|
||||
if (isMouseOverContainer) {
|
||||
// Mouse is over container - follow the mouse Y position
|
||||
handleTop = mouseY - containerRect.top;
|
||||
} else {
|
||||
// Mouse not over container - use viewport center
|
||||
const viewportCenter = window.innerHeight / 2;
|
||||
|
||||
if (containerRect.top > viewportCenter) {
|
||||
// Container is below viewport center - position handle at container top
|
||||
handleTop = 0;
|
||||
} else if (containerRect.bottom < viewportCenter) {
|
||||
// Container is above viewport center - position handle at container bottom
|
||||
handleTop = containerRect.height;
|
||||
} else {
|
||||
// Container spans viewport center - position handle at viewport center
|
||||
handleTop = viewportCenter - containerRect.top;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure handle stays within container bounds
|
||||
handleTop = Math.max(24, Math.min(handleTop, containerRect.height - 24));
|
||||
|
||||
handle.style.top = handleTop + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Track mouse position over container
|
||||
*/
|
||||
function onMouseMoveContainer(e) {
|
||||
mouseY = e.clientY;
|
||||
updateHandlePosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse enters container
|
||||
*/
|
||||
function onMouseEnter() {
|
||||
isMouseOverContainer = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse leaves container
|
||||
*/
|
||||
function onMouseLeave() {
|
||||
isMouseOverContainer = false;
|
||||
updateHandlePosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update slider position and clip-path
|
||||
*/
|
||||
function updateSlider(clientX) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const pos = Math.max(0, Math.min(clientX - rect.left, rect.width));
|
||||
const percentage = (pos / rect.width) * 100;
|
||||
|
||||
slider.style.left = percentage + '%';
|
||||
afterWrapper.style.clipPath = `inset(0 0 0 ${percentage}%)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle move events (mouse or touch)
|
||||
*/
|
||||
function onMove(e) {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
|
||||
const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
|
||||
updateSlider(clientX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start dragging
|
||||
*/
|
||||
function onStart(e) {
|
||||
isDragging = true;
|
||||
const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
|
||||
updateSlider(clientX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop dragging
|
||||
*/
|
||||
function onEnd() {
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click anywhere on container to jump slider to that position
|
||||
*/
|
||||
function onClick(e) {
|
||||
if (e.target !== slider && !slider.contains(e.target)) {
|
||||
const clientX = e.clientX;
|
||||
updateSlider(clientX);
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse events
|
||||
slider.addEventListener('mousedown', onStart);
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onEnd);
|
||||
|
||||
// Touch events
|
||||
slider.addEventListener('touchstart', onStart, { passive: false });
|
||||
document.addEventListener('touchmove', onMove, { passive: false });
|
||||
document.addEventListener('touchend', onEnd);
|
||||
|
||||
// Click anywhere on container to move slider
|
||||
container.addEventListener('click', onClick);
|
||||
|
||||
// Track mouse position over container for handle following
|
||||
container.addEventListener('mouseenter', onMouseEnter);
|
||||
container.addEventListener('mouseleave', onMouseLeave);
|
||||
container.addEventListener('mousemove', onMouseMoveContainer, { passive: true });
|
||||
|
||||
// Update handle position on scroll and resize
|
||||
window.addEventListener('scroll', updateHandlePosition, { passive: true });
|
||||
window.addEventListener('resize', updateHandlePosition, { passive: true });
|
||||
|
||||
// Initial position
|
||||
updateHandlePosition();
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', function() {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onEnd);
|
||||
document.removeEventListener('touchmove', onMove);
|
||||
document.removeEventListener('touchend', onEnd);
|
||||
container.removeEventListener('mouseenter', onMouseEnter);
|
||||
container.removeEventListener('mouseleave', onMouseLeave);
|
||||
container.removeEventListener('mousemove', onMouseMoveContainer);
|
||||
window.removeEventListener('scroll', updateHandlePosition);
|
||||
window.removeEventListener('resize', updateHandlePosition);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initComparisonSlider);
|
||||
} else {
|
||||
initComparisonSlider();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1 @@
|
||||
.comparison-score{padding:1em;background:var(--color-table-stripe);border-radius:4px;margin:1em 0;border:1px solid var(--color-border-table-cell);color:var(--color-text)}.change-detected{color:#d32f2f;font-weight:bold}.no-change{color:#388e3c;font-weight:bold}.comparison-grid{display:grid;grid-template-columns:1fr 1fr;gap:1em;margin:1em 1em}@media(max-width: 1200px){.comparison-grid{grid-template-columns:1fr}}.image-comparison{position:relative;width:100%;overflow:hidden;border:1px solid var(--color-border-table-cell);box-shadow:0 2px 4px rgba(0,0,0,.1);user-select:none}.image-comparison img{display:block;width:100%;height:auto;max-width:100%;border:none;box-shadow:none}.comparison-image-wrapper{position:relative;width:100%;display:flex;align-items:flex-start;justify-content:center;background-color:var(--color-background);background-image:linear-gradient(45deg, var(--color-table-stripe) 25%, transparent 25%),linear-gradient(-45deg, var(--color-table-stripe) 25%, transparent 25%),linear-gradient(45deg, transparent 75%, var(--color-table-stripe) 75%),linear-gradient(-45deg, transparent 75%, var(--color-table-stripe) 75%);background-size:20px 20px;background-position:0 0,0 10px,10px -10px,-10px 0px}.comparison-after{position:absolute;top:0;left:0;width:100%;height:100%;clip-path:inset(0 0 0 50%)}.comparison-slider{position:absolute;top:0;left:50%;width:4px;height:100%;background:#0078e7;cursor:ew-resize;transform:translateX(-2px);z-index:10}.comparison-handle{position:absolute;top:50%;left:50%;width:48px;height:48px;background:#0078e7;border:3px solid #fff;border-radius:50%;transform:translate(-50%, -50%);box-shadow:0 2px 8px rgba(0,0,0,.3);display:flex;align-items:center;justify-content:center;cursor:ew-resize;transition:top .1s ease-out}.comparison-handle::after{content:"⇄";color:#fff;font-size:24px;font-weight:bold;pointer-events:none}.comparison-labels{position:absolute;top:10px;width:100%;display:flex;justify-content:space-between;padding:0 0px;z-index:5;pointer-events:none}.comparison-label{background:rgba(0,0,0,.7);color:#fff;padding:.5em 1em;border-radius:4px;font-size:.9em;font-weight:bold}.screenshot-panel{text-align:center;background:var(--color-background);border:1px solid var(--color-border-table-cell);border-radius:4px;padding:1em;box-shadow:0 2px 4px rgba(0,0,0,.05)}.screenshot-panel h3{margin:0 0 1em 0;font-size:1.1em;color:var(--color-text);border-bottom:2px solid var(--color-background-button-primary);padding-bottom:.5em}.screenshot-panel.diff h3{border-bottom-color:#d32f2f}.screenshot-panel img{max-width:100%;height:auto;border:1px solid var(--color-border-table-cell);box-shadow:0 2px 4px rgba(0,0,0,.1)}.version-selector{display:inline-block;margin:0 .5em}.version-selector label{font-weight:bold;margin-right:.5em;color:var(--color-text)}#settings{background:var(--color-background);padding:1.5em;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,.05);margin-bottom:2em;border:1px solid var(--color-border-table-cell)}#settings h2{margin-top:0;color:var(--color-text)}.diff-fieldset{border:none;padding:0;margin:0}.edit-link{float:right;margin-top:-0.5em}.comparison-description{color:var(--color-text-input-description);font-size:.9em;margin-bottom:1em}.download-link{color:var(--color-link);text-decoration:none;display:inline-flex;align-items:center;gap:.3em;font-size:.85em}.download-link:hover{text-decoration:underline}.diff-section-header{color:#d32f2f;font-size:.9em;margin-bottom:1em;font-weight:bold;display:flex;align-items:center;justify-content:center;gap:1em}.comparison-history-section{margin-top:3em;padding:1em;background:var(--color-background);border:1px solid var(--color-border-table-cell);border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,.05)}.comparison-history-section h3{color:var(--color-text)}.comparison-history-section p{color:var(--color-text-input-description);font-size:.9em}.history-changed-yes{color:#d32f2f;font-weight:bold}.history-changed-no{color:#388e3c}
|
||||
Reference in New Issue
Block a user