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