fix(web): починить зум в просмотре
Механика зума переписана: - Пробел зумит фото до 100% от оригинала (и обратно к вписанному виду). - Колесо мыши зумит к курсору: точка под указателем остаётся на месте при любом изменении масштаба. - Стрелки влево/вправо переключают фото и в зуме — панорамирование осталось за мышью. - Масштаб и позиция считаются от размеров оригинала, а не от текущей рамки, поэтому зум не «уезжает» и картинка корректно центрируется.
This commit is contained in:
@@ -337,15 +337,9 @@
|
||||
return fresh?.height || current?.height || imgEl?.naturalHeight || 0;
|
||||
}
|
||||
|
||||
/** Элемент, занимающий весь кадр фото. */
|
||||
function frameElement(): HTMLElement | undefined {
|
||||
return imgEl ?? undefined;
|
||||
}
|
||||
|
||||
function prev() {
|
||||
if (index > 0) onIndexChange(index - 1);
|
||||
}
|
||||
|
||||
/** Масштаб, при котором фото целиком помещается в просмотр. */
|
||||
function fitScale(): number {
|
||||
if (!scroller) return 1;
|
||||
@@ -355,72 +349,95 @@
|
||||
return Math.min(scroller.clientWidth / natW, scroller.clientHeight / natH, 1);
|
||||
}
|
||||
|
||||
/** Текущий масштаб показа относительно оригинала. */
|
||||
function currentScale(): number {
|
||||
return zoomed ? scale : fitScale();
|
||||
}
|
||||
|
||||
/** Смещение картинки в содержимом: картинка центрируется, если влезает. */
|
||||
function imageOffset(width: number, height: number): { x: number; y: number } {
|
||||
if (!scroller) return { x: 0, y: 0 };
|
||||
return {
|
||||
x: Math.max(0, (scroller.clientWidth - width) / 2),
|
||||
y: Math.max(0, (scroller.clientHeight - height) / 2)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Зум клавишами: `+`/`-`. Точка в центре просмотра остаётся на месте,
|
||||
* как при зуме колесом мыши.
|
||||
* Масштабирует фото, удерживая точку под курсором (или в центре вьюпорта)
|
||||
* на месте. cursorX/cursorY — координаты точки в области просмотра.
|
||||
*/
|
||||
function zoomBy(factor: number) {
|
||||
function zoomTo(next: number, cursorX: number, cursorY: number) {
|
||||
const el = scroller;
|
||||
const frame = frameElement();
|
||||
if (!el || !frame) return;
|
||||
if (!el) return;
|
||||
const natW = baseWidth();
|
||||
const natH = baseHeight();
|
||||
if (!natW || !natH) return;
|
||||
|
||||
const fit = fitScale();
|
||||
const cur = zoomed ? scale : fit;
|
||||
const next = Math.min(MAX_ZOOM, Math.max(fit, cur * factor));
|
||||
const cur = currentScale();
|
||||
if (Math.abs(next - cur) < 1e-4) return;
|
||||
|
||||
const relX = frame.offsetWidth
|
||||
? (el.scrollLeft + el.clientWidth / 2 - frame.offsetLeft) / frame.offsetWidth
|
||||
: 0.5;
|
||||
const relY = frame.offsetHeight
|
||||
? (el.scrollTop + el.clientHeight / 2 - frame.offsetTop) / frame.offsetHeight
|
||||
: 0.5;
|
||||
// Положение точки на картинке до изменения масштаба.
|
||||
const oldWidth = natW * cur;
|
||||
const oldHeight = natH * cur;
|
||||
const oldOffset = imageOffset(oldWidth, oldHeight);
|
||||
const relX = oldWidth > 0 ? (el.scrollLeft + cursorX - oldOffset.x) / oldWidth : 0.5;
|
||||
const relY = oldHeight > 0 ? (el.scrollTop + cursorY - oldOffset.y) / oldHeight : 0.5;
|
||||
|
||||
const fit = fitScale();
|
||||
zoomed = next > fit * 1.001;
|
||||
scale = next;
|
||||
|
||||
tick().then(() => {
|
||||
const frame = frameElement();
|
||||
const iw = frame?.offsetWidth ?? 0;
|
||||
const ih = frame?.offsetHeight ?? 0;
|
||||
if (!iw || !ih) return;
|
||||
const offX = Math.max(0, (el.clientWidth - iw) / 2);
|
||||
const offY = Math.max(0, (el.clientHeight - ih) / 2);
|
||||
el.scrollLeft = offX + relX * iw - el.clientWidth / 2;
|
||||
el.scrollTop = offY + relY * ih - el.clientHeight / 2;
|
||||
const width = natW * next;
|
||||
const height = natH * next;
|
||||
const offset = imageOffset(width, height);
|
||||
el.scrollLeft = offset.x + relX * width - cursorX;
|
||||
el.scrollTop = offset.y + relY * height - cursorY;
|
||||
scheduleViewport();
|
||||
});
|
||||
}
|
||||
|
||||
/** Зум клавишами `+`/`-`: точка в центре просмотра остаётся на месте. */
|
||||
function zoomBy(factor: number) {
|
||||
const el = scroller;
|
||||
if (!el) return;
|
||||
const fit = fitScale();
|
||||
const next = Math.min(MAX_ZOOM, Math.max(fit, currentScale() * factor));
|
||||
zoomTo(next, el.clientWidth / 2, el.clientHeight / 2);
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (index < photos.length - 1) onIndexChange(index + 1);
|
||||
}
|
||||
|
||||
/** Пробел: 100% от оригинала и обратно к вписанному виду. */
|
||||
function toggleActual() {
|
||||
const el = scroller;
|
||||
if (!el) return;
|
||||
if (zoomed) {
|
||||
zoomed = false;
|
||||
scale = 1;
|
||||
if (scroller) {
|
||||
scroller.scrollLeft = 0;
|
||||
scroller.scrollTop = 0;
|
||||
}
|
||||
} else {
|
||||
zoomed = true;
|
||||
scale = 1;
|
||||
tick().then(() => {
|
||||
if (!scroller) return;
|
||||
scroller.scrollLeft = (scroller.scrollWidth - scroller.clientWidth) / 2;
|
||||
scroller.scrollTop = (scroller.scrollHeight - scroller.clientHeight) / 2;
|
||||
scheduleViewport();
|
||||
});
|
||||
el.scrollLeft = 0;
|
||||
el.scrollTop = 0;
|
||||
scheduleViewport();
|
||||
return;
|
||||
}
|
||||
// 100% от оригинала.
|
||||
zoomed = true;
|
||||
scale = 1;
|
||||
tick().then(() => {
|
||||
const natW = baseWidth();
|
||||
const natH = baseHeight();
|
||||
const offset = imageOffset(natW, natH);
|
||||
el.scrollLeft = offset.x;
|
||||
el.scrollTop = offset.y;
|
||||
scheduleViewport();
|
||||
});
|
||||
}
|
||||
|
||||
function onWheel(ev: WheelEvent) {
|
||||
const frame = frameElement();
|
||||
if (!current || !scroller || !frame) return;
|
||||
if (!current || !scroller) return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
@@ -428,26 +445,13 @@
|
||||
const natH = baseHeight();
|
||||
if (!natW || !natH) return;
|
||||
|
||||
const rect = scroller.getBoundingClientRect();
|
||||
const cursorX = ev.clientX - rect.left;
|
||||
const cursorY = ev.clientY - rect.top;
|
||||
const factor = ev.deltaY < 0 ? 1.15 : 1 / 1.15;
|
||||
const fit = fitScale();
|
||||
const curScale = zoomed ? scale : fit;
|
||||
const newScale = Math.min(MAX_ZOOM, Math.max(fit, curScale * factor));
|
||||
if (Math.abs(newScale - curScale) < 1e-4) return;
|
||||
|
||||
const r = frame.getBoundingClientRect();
|
||||
const fx = r.width ? (ev.clientX - r.left) / r.width : 0.5;
|
||||
const fy = r.height ? (ev.clientY - r.top) / r.height : 0.5;
|
||||
|
||||
zoomed = true;
|
||||
scale = newScale;
|
||||
|
||||
tick().then(() => {
|
||||
if (!scroller) return;
|
||||
const sr = scroller.getBoundingClientRect();
|
||||
scroller.scrollLeft = fx * natW * newScale - (ev.clientX - sr.left);
|
||||
scroller.scrollTop = fy * natH * newScale - (ev.clientY - sr.top);
|
||||
scheduleViewport();
|
||||
});
|
||||
const next = Math.min(MAX_ZOOM, Math.max(fit, currentScale() * factor));
|
||||
zoomTo(next, cursorX, cursorY);
|
||||
}
|
||||
|
||||
function onPointerDown(ev: PointerEvent) {
|
||||
@@ -534,7 +538,12 @@
|
||||
zoomBy(1 / ZOOM_STEP);
|
||||
return;
|
||||
}
|
||||
if (zoomed) return;
|
||||
if (zoomed) {
|
||||
// В зуме стрелки переключают фото, а панорамирование — мышью.
|
||||
if (ev.key === 'ArrowLeft') prev();
|
||||
else if (ev.key === 'ArrowRight') next();
|
||||
return;
|
||||
}
|
||||
if (ev.key === 'ArrowLeft') prev();
|
||||
else if (ev.key === 'ArrowRight') next();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user