feat: compare slider UI for photo enhance modal
- Side-by-side before/after comparison with draggable divider - Zoom (scroll wheel) and pan when zoomed in, double-click to reset - Sliders moved to sidebar panel, responsive layout - Original/Result badges on the comparison view - Helper functions: setEnhanceClip, showEnhanceResult, applyEnhanceTransform
This commit is contained in:
+110
-14
@@ -427,6 +427,107 @@ let enhanceEngineLoaded = false;
|
||||
let photoAiEnabled = false;
|
||||
let photoAiPollTimer = null;
|
||||
let enhanceAiResultPath = null;
|
||||
let enhanceClipPos = 50;
|
||||
|
||||
function setEnhanceClip(p) {
|
||||
enhanceClipPos = Math.min(100, Math.max(0, p));
|
||||
document.getElementById('enhanceTop').style.clipPath = `inset(0 0 0 ${enhanceClipPos}%)`;
|
||||
document.getElementById('enhanceDivider').style.left = enhanceClipPos + '%';
|
||||
}
|
||||
|
||||
function showEnhanceResult(show) {
|
||||
document.getElementById('enhanceTop').style.display = show ? '' : 'none';
|
||||
document.getElementById('enhanceLoading').style.display = show ? 'none' : '';
|
||||
}
|
||||
|
||||
function setEnhanceLoadingText(text) {
|
||||
const el = document.getElementById('enhanceLoading');
|
||||
el.style.display = '';
|
||||
el.textContent = text;
|
||||
}
|
||||
|
||||
let enhanceZoom = 1;
|
||||
let enhancePanX = 0;
|
||||
let enhancePanY = 0;
|
||||
|
||||
function applyEnhanceTransform() {
|
||||
const t = `translate(${enhancePanX}px, ${enhancePanY}px) scale(${enhanceZoom})`;
|
||||
document.getElementById('enhanceStage').style.transform = t;
|
||||
document.getElementById('enhanceInner').style.transform = t;
|
||||
const area = document.getElementById('enhanceCompare');
|
||||
area.classList.toggle('zoomed', enhanceZoom > 1);
|
||||
if (enhanceZoom <= 1) area.classList.remove('panning');
|
||||
}
|
||||
|
||||
function clampEnhancePan() {
|
||||
const rect = document.getElementById('enhanceCompare').getBoundingClientRect();
|
||||
const minX = rect.width * (1 - enhanceZoom);
|
||||
const minY = rect.height * (1 - enhanceZoom);
|
||||
enhancePanX = Math.min(0, Math.max(minX, enhancePanX));
|
||||
enhancePanY = Math.min(0, Math.max(minY, enhancePanY));
|
||||
}
|
||||
|
||||
function resetEnhanceZoom() {
|
||||
enhanceZoom = 1;
|
||||
enhancePanX = 0;
|
||||
enhancePanY = 0;
|
||||
applyEnhanceTransform();
|
||||
}
|
||||
|
||||
(function initEnhanceDrag() {
|
||||
const area = document.getElementById('enhanceCompare');
|
||||
let dragging = false;
|
||||
let dividerDrag = false;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
const move = ev => {
|
||||
const rect = area.getBoundingClientRect();
|
||||
setEnhanceClip(((ev.clientX - rect.left) / rect.width) * 100);
|
||||
};
|
||||
area.addEventListener('pointerdown', ev => {
|
||||
dividerDrag = !!ev.target.closest('#enhanceDivider');
|
||||
dragging = true;
|
||||
lastX = ev.clientX;
|
||||
lastY = ev.clientY;
|
||||
area.setPointerCapture(ev.pointerId);
|
||||
if (!dividerDrag && enhanceZoom === 1) move(ev);
|
||||
});
|
||||
area.addEventListener('pointermove', ev => {
|
||||
if (!dragging) return;
|
||||
if (dividerDrag || enhanceZoom === 1) {
|
||||
move(ev);
|
||||
} else {
|
||||
area.classList.add('panning');
|
||||
enhancePanX += ev.clientX - lastX;
|
||||
enhancePanY += ev.clientY - lastY;
|
||||
lastX = ev.clientX;
|
||||
lastY = ev.clientY;
|
||||
clampEnhancePan();
|
||||
applyEnhanceTransform();
|
||||
}
|
||||
});
|
||||
const end = () => {
|
||||
dragging = false;
|
||||
area.classList.remove('panning');
|
||||
};
|
||||
area.addEventListener('pointerup', end);
|
||||
area.addEventListener('pointercancel', end);
|
||||
area.addEventListener('wheel', ev => {
|
||||
ev.preventDefault();
|
||||
const rect = area.getBoundingClientRect();
|
||||
const cx = ev.clientX - rect.left;
|
||||
const cy = ev.clientY - rect.top;
|
||||
const prev = enhanceZoom;
|
||||
const next = Math.min(5, Math.max(1, enhanceZoom * (ev.deltaY < 0 ? 1.15 : 1 / 1.15)));
|
||||
if (next === prev) return;
|
||||
enhanceZoom = next;
|
||||
enhancePanX = cx - (cx - enhancePanX) * (next / prev);
|
||||
enhancePanY = cy - (cy - enhancePanY) * (next / prev);
|
||||
clampEnhancePan();
|
||||
applyEnhanceTransform();
|
||||
}, { passive: false });
|
||||
area.addEventListener('dblclick', resetEnhanceZoom);
|
||||
})();
|
||||
|
||||
async function loadEnhanceEngine() {
|
||||
if (enhanceEngineLoaded) return;
|
||||
@@ -446,8 +547,6 @@ async function runPhotoAi() {
|
||||
enhanceSaving = true;
|
||||
const btn = document.getElementById('enhanceAiBtn');
|
||||
btn.disabled = true;
|
||||
const loading = document.getElementById('enhanceLoading');
|
||||
const canvas = document.getElementById('enhanceCanvas');
|
||||
try {
|
||||
const res = await fetch(`${API}/api/entries/${enhanceEntryId}/photo/enhance-ai`, { method: 'POST', headers: hdr() });
|
||||
if (!res.ok) {
|
||||
@@ -455,9 +554,8 @@ async function runPhotoAi() {
|
||||
throw new Error(err.error || 'Ошибка запуска ИИ');
|
||||
}
|
||||
const { jobId } = await res.json();
|
||||
canvas.style.display = 'none';
|
||||
loading.style.display = '';
|
||||
loading.textContent = 'ИИ обрабатывает фото... (может занять до минуты)';
|
||||
showEnhanceResult(false);
|
||||
setEnhanceLoadingText('ИИ обрабатывает фото... (может занять до минуты)');
|
||||
btn.textContent = '⏳ ИИ...';
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
photoAiPollTimer = setInterval(async () => {
|
||||
@@ -488,8 +586,7 @@ async function runPhotoAi() {
|
||||
aiImg.onerror = () => { showToast('Не удалось загрузить результат ИИ'); };
|
||||
aiImg.src = `${API}${result.photo_path}`;
|
||||
} catch (e) {
|
||||
loading.style.display = 'none';
|
||||
canvas.style.display = 'block';
|
||||
showEnhanceResult(true);
|
||||
alert(e.message);
|
||||
} finally {
|
||||
enhanceSaving = false;
|
||||
@@ -545,17 +642,17 @@ function openEnhance(id) {
|
||||
document.getElementById('enhanceAiBtn').style.display = photoAiEnabled ? '' : 'none';
|
||||
});
|
||||
const canvas = document.getElementById('enhanceCanvas');
|
||||
canvas.style.display = 'none';
|
||||
const loading = document.getElementById('enhanceLoading');
|
||||
loading.style.display = '';
|
||||
loading.textContent = 'Загрузка...';
|
||||
setEnhanceClip(50);
|
||||
resetEnhanceZoom();
|
||||
showEnhanceResult(false);
|
||||
setEnhanceLoadingText('Загрузка...');
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
enhanceSourceImg = img;
|
||||
renderEnhancePreview();
|
||||
};
|
||||
img.onerror = () => {
|
||||
loading.textContent = 'Не удалось загрузить фото';
|
||||
setEnhanceLoadingText('Не удалось загрузить фото');
|
||||
};
|
||||
img.src = `${API}${mp}`;
|
||||
document.getElementById('enhanceModal').classList.add('open');
|
||||
@@ -651,8 +748,7 @@ function renderEnhancePreview() {
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
drawEnhanced(ctx, img, w, h);
|
||||
canvas.style.display = 'block';
|
||||
document.getElementById('enhanceLoading').style.display = 'none';
|
||||
showEnhanceResult(true);
|
||||
}
|
||||
|
||||
function scheduleEnhancePreview() {
|
||||
|
||||
Reference in New Issue
Block a user