fix(web): предпросмотр правок работает и без зума

Клиентский предпросмотр был включён только в зуме, поэтому в обычном виде
просмотра фото оставалось без правок. Теперь холст рисует ту часть картинки,
что попала во вьюпорт, в любом режиме, а под ним лежит неотредактированная
версия фото — так правки не применяются дважды.
This commit is contained in:
iu
2026-10-21 00:11:17 +03:00
parent 520886a11d
commit 2adcb1374b
+41 -18
View File
@@ -77,6 +77,8 @@
''
);
const originalSrc = $derived(fresh?.original_url ?? current?.original_url ?? '');
// Базовые (неотредактированные) версии: на них браузер рисует предпросмотр.
const basePreviewSrc = $derived(fresh?.preview_url ?? current?.preview_url ?? '');
// Пока сервер не отрендерил текущие правки, видимую область фото рисует
// сам браузер: правки видны сразу, без ожидания рендера.
@@ -91,12 +93,15 @@
JSON.stringify(edit.params) !== JSON.stringify(savedParams)
);
const needClientRender = $derived(hasEdits && (pending || !editUrl));
const clientPreview = $derived(zoomed && needClientRender);
const clientPreview = $derived(needClientRender);
// Пока браузер рисует правки сам, под ним лежит исходная версия фото.
const clientSource = $derived(
zoomed ? originalSrc || basePreviewSrc : basePreviewSrc
);
const displaySrc = $derived(
clientPreview
? originalSrc || previewSrc
? clientSource || previewSrc
: zoomed && hiResLoaded && fullSrc
? fullSrc
: previewSrc
@@ -176,7 +181,10 @@
let canvas: HTMLCanvasElement | undefined = $state();
let viewportFrame: number | null = null;
/** Рисует видимую область фото с применёнными правками. */
/**
* Рисует видимую область фото с применёнными правками. Работает и в зуме,
* и в обычном виде: берём ту часть картинки, что попала во вьюпорт.
*/
function renderViewport() {
const c = canvas;
const img = imgEl;
@@ -184,8 +192,20 @@
if (!c || !img || !el) return;
const natural = img.naturalWidth;
if (!natural) return;
const ratio = img.clientWidth / natural;
if (!ratio) return;
const imgRect = img.getBoundingClientRect();
const viewRect = el.getBoundingClientRect();
if (!imgRect.width || !viewRect.width) return;
const ratio = imgRect.width / natural;
// Видимая часть картинки.
const left = Math.max(imgRect.left, viewRect.left);
const top = Math.max(imgRect.top, viewRect.top);
const right = Math.min(imgRect.right, viewRect.right);
const bottom = Math.min(imgRect.bottom, viewRect.bottom);
const visibleW = right - left;
const visibleH = bottom - top;
if (visibleW <= 0 || visibleH <= 0) return;
const width = el.clientWidth;
const height = el.clientHeight;
if (!width || !height) return;
@@ -195,23 +215,25 @@
}
c.style.width = `${width}px`;
c.style.height = `${height}px`;
const ctx = c.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, width, height);
ctx.drawImage(
img,
el.scrollLeft / ratio,
el.scrollTop / ratio,
width / ratio,
height / ratio,
0,
0,
width,
height
);
const data = ctx.getImageData(0, 0, width, height);
// Исходный кусок в пикселях картинки и его место на холсте.
const sx = (left - imgRect.left) / ratio;
const sy = (top - imgRect.top) / ratio;
const sw = visibleW / ratio;
const sh = visibleH / ratio;
const dx = Math.round(left - viewRect.left);
const dy = Math.round(top - viewRect.top);
const dw = Math.round(visibleW);
const dh = Math.round(visibleH);
ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
const data = ctx.getImageData(dx, dy, dw, dh);
applyLight(data.data, liveParams.light);
ctx.putImageData(data, 0, 0);
ctx.putImageData(data, dx, dy);
}
function scheduleViewport() {
@@ -684,6 +706,7 @@
src={displaySrc}
alt={current.original_filename}
draggable="false"
onload={scheduleViewport}
style={zoomed ? `width: ${baseWidth() * scale}px; height: auto;` : ''}
class="m-auto block {zoomed
? 'max-w-none shrink-0 select-none'