diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 954c1ab783b..db63dbbfc20 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -160,41 +160,80 @@ export default class ImageView extends React.Component { }); }; - private zoom(delta: number) { - const newZoom = this.state.zoom + delta; + private zoomDelta(delta: number, anchorX?: number, anchorY?: number) { + this.zoom(this.state.zoom + delta, anchorX, anchorY); + } + + private zoom(zoomLevel: number, anchorX?: number, anchorY?: number) { + const oldZoom = this.state.zoom; + const newZoom = Math.min(zoomLevel, this.state.maxZoom); if (newZoom <= this.state.minZoom) { + // Zoom out fully this.setState({ zoom: this.state.minZoom, translationX: 0, translationY: 0, }); - return; - } - if (newZoom >= this.state.maxZoom) { - this.setState({ zoom: this.state.maxZoom }); - return; - } + } else if (typeof anchorX !== "number" && typeof anchorY !== "number") { + // Zoom relative to the center of the view + this.setState({ + zoom: newZoom, + translationX: this.state.translationX * newZoom / oldZoom, + translationY: this.state.translationY * newZoom / oldZoom, + }); + } else { + // Zoom relative to the given point on the image. + // First we need to figure out the offset of the anchor point + // relative to the center of the image, accounting for rotation. + let offsetX; + let offsetY; + // The modulo operator can return negative values for some + // rotations, so we have to do some extra work to normalize it + switch (((this.state.rotation % 360) + 360) % 360) { + case 0: + offsetX = this.image.current.clientWidth / 2 - anchorX; + offsetY = this.image.current.clientHeight / 2 - anchorY; + break; + case 90: + offsetX = anchorY - this.image.current.clientHeight / 2; + offsetY = this.image.current.clientWidth / 2 - anchorX; + break; + case 180: + offsetX = anchorX - this.image.current.clientWidth / 2; + offsetY = anchorY - this.image.current.clientHeight / 2; + break; + case 270: + offsetX = this.image.current.clientHeight / 2 - anchorY; + offsetY = anchorX - this.image.current.clientWidth / 2; + } - this.setState({ - zoom: newZoom, - }); + // Apply the zoom and offset + this.setState({ + zoom: newZoom, + translationX: this.state.translationX + (newZoom - oldZoom) * offsetX, + translationY: this.state.translationY + (newZoom - oldZoom) * offsetY, + }); + } } private onWheel = (ev: WheelEvent) => { - ev.stopPropagation(); - ev.preventDefault(); + if (ev.target === this.image.current) { + ev.stopPropagation(); + ev.preventDefault(); - const { deltaY } = normalizeWheelEvent(ev); - this.zoom(-(deltaY * ZOOM_COEFFICIENT)); + const { deltaY } = normalizeWheelEvent(ev); + // Zoom in on the point on the image targeted by the cursor + this.zoomDelta(-deltaY * ZOOM_COEFFICIENT, ev.offsetX, ev.offsetY); + } }; private onZoomInClick = () => { - this.zoom(ZOOM_STEP); + this.zoomDelta(ZOOM_STEP); }; private onZoomOutClick = () => { - this.zoom(-ZOOM_STEP); + this.zoomDelta(-ZOOM_STEP); }; private onKeyDown = (ev: KeyboardEvent) => { @@ -259,7 +298,7 @@ export default class ImageView extends React.Component { // Zoom in if we are completely zoomed out if (this.state.zoom === this.state.minZoom) { - this.setState({ zoom: this.state.maxZoom }); + this.zoom(this.state.maxZoom, ev.nativeEvent.offsetX, ev.nativeEvent.offsetY); return; } @@ -289,11 +328,7 @@ export default class ImageView extends React.Component { Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE && Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE ) { - this.setState({ - zoom: this.state.minZoom, - translationX: 0, - translationY: 0, - }); + this.zoom(this.state.minZoom); this.initX = 0; this.initY = 0; }