From 3911666c52fb1e731d0bc298f9c26b80df3ff8b3 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 21 Jul 2021 02:13:30 -0400 Subject: [PATCH 1/6] Zoom images to where the cursor points Signed-off-by: Robin Townsend --- src/components/views/elements/ImageView.tsx | 83 +++++++++++++++------ 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 954c1ab783b..215c92671be 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -160,41 +160,82 @@ export default class ImageView extends React.Component { }); }; - private zoom(delta: number) { - const newZoom = this.state.zoom + delta; + private zoomDelta(delta: number, zoomX?: number, zoomY?: number) { + this.zoom(this.state.zoom + delta, zoomX, zoomY); + } + + private zoom(zoomLevel: number, zoomX?: number, zoomY?: 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 (zoomX === undefined && zoomY === undefined) { + // 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; + switch (((this.state.rotation % 360) + 360) % 360) { + case 0: + offsetX = this.image.current.clientWidth / 2 - zoomX; + offsetY = this.image.current.clientHeight / 2 - zoomY; + break; + case 90: + offsetX = zoomY - this.image.current.clientHeight / 2; + offsetY = this.image.current.clientWidth / 2 - zoomX; + break; + case 180: + offsetX = zoomX - this.image.current.clientWidth / 2; + offsetY = zoomY - this.image.current.clientHeight / 2; + break; + case 270: + offsetX = this.image.current.clientHeight / 2 - zoomY; + offsetY = zoomX - 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(); - const { deltaY } = normalizeWheelEvent(ev); - this.zoom(-(deltaY * ZOOM_COEFFICIENT)); + + if (ev.target === this.image.current) { + // Zoom in on the point on the image targeted by the cursor + this.zoomDelta(-deltaY * ZOOM_COEFFICIENT, ev.offsetX, ev.offsetY); + } else { + // The user is scrolling outside of the image, so we can't really + // get a targeted point. Instead, we'll just zoom in on the center. + this.zoomDelta(-deltaY * ZOOM_COEFFICIENT); + } }; - private onZoomInClick = () => { - this.zoom(ZOOM_STEP); + private onZoomInClick = (ev: MouseEvent) => { + this.zoomDelta(ZOOM_STEP); }; - private onZoomOutClick = () => { - this.zoom(-ZOOM_STEP); + private onZoomOutClick = (ev: MouseEvent) => { + this.zoomDelta(-ZOOM_STEP); }; private onKeyDown = (ev: KeyboardEvent) => { @@ -259,7 +300,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 +330,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; } From 0497e0864f74beddb5f432bea2e6a4691a44af18 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 21 Jul 2021 02:42:30 -0400 Subject: [PATCH 2/6] Fix types Signed-off-by: Robin Townsend --- src/components/views/elements/ImageView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 215c92671be..40003f734c3 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -230,11 +230,11 @@ export default class ImageView extends React.Component { } }; - private onZoomInClick = (ev: MouseEvent) => { + private onZoomInClick = () => { this.zoomDelta(ZOOM_STEP); }; - private onZoomOutClick = (ev: MouseEvent) => { + private onZoomOutClick = () => { this.zoomDelta(-ZOOM_STEP); }; From b99a6a8d5405532bb6bac11157f678db49f41ca5 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 22 Jul 2021 09:41:39 -0400 Subject: [PATCH 3/6] Use typeof to check for presence of parameters Signed-off-by: Robin Townsend --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 40003f734c3..eff0ad4d610 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -175,7 +175,7 @@ export default class ImageView extends React.Component { translationX: 0, translationY: 0, }); - } else if (zoomX === undefined && zoomY === undefined) { + } else if (typeof zoomX !== "number" && typeof zoomY !== "number") { // Zoom relative to the center of the view this.setState({ zoom: newZoom, From 875b46bacb0719e58d5ee6537f0cdf326d973986 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 22 Jul 2021 09:46:29 -0400 Subject: [PATCH 4/6] Don't zoom images when the cursor isn't over the image Signed-off-by: Robin Townsend --- src/components/views/elements/ImageView.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index eff0ad4d610..4059277ea3b 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -216,17 +216,13 @@ export default class ImageView extends React.Component { } private onWheel = (ev: WheelEvent) => { - ev.stopPropagation(); - ev.preventDefault(); - const { deltaY } = normalizeWheelEvent(ev); - if (ev.target === this.image.current) { + ev.stopPropagation(); + ev.preventDefault(); + + 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); - } else { - // The user is scrolling outside of the image, so we can't really - // get a targeted point. Instead, we'll just zoom in on the center. - this.zoomDelta(-deltaY * ZOOM_COEFFICIENT); } }; From 9e0720a6c42da82bc0a24d17b097361a4bf9fd6d Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 22 Jul 2021 09:48:56 -0400 Subject: [PATCH 5/6] Rename zoom anchor variables for clarity Signed-off-by: Robin Townsend --- src/components/views/elements/ImageView.tsx | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 4059277ea3b..1979da52956 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -160,11 +160,11 @@ export default class ImageView extends React.Component { }); }; - private zoomDelta(delta: number, zoomX?: number, zoomY?: number) { - this.zoom(this.state.zoom + delta, zoomX, zoomY); + private zoomDelta(delta: number, anchorX?: number, anchorY?: number) { + this.zoom(this.state.zoom + delta, anchorX, anchorY); } - private zoom(zoomLevel: number, zoomX?: number, zoomY?: number) { + private zoom(zoomLevel: number, anchorX?: number, anchorY?: number) { const oldZoom = this.state.zoom; const newZoom = Math.min(zoomLevel, this.state.maxZoom); @@ -175,7 +175,7 @@ export default class ImageView extends React.Component { translationX: 0, translationY: 0, }); - } else if (typeof zoomX !== "number" && typeof zoomY !== "number") { + } else if (typeof anchorX !== "number" && typeof anchorY !== "number") { // Zoom relative to the center of the view this.setState({ zoom: newZoom, @@ -190,20 +190,20 @@ export default class ImageView extends React.Component { let offsetY; switch (((this.state.rotation % 360) + 360) % 360) { case 0: - offsetX = this.image.current.clientWidth / 2 - zoomX; - offsetY = this.image.current.clientHeight / 2 - zoomY; + offsetX = this.image.current.clientWidth / 2 - anchorX; + offsetY = this.image.current.clientHeight / 2 - anchorY; break; case 90: - offsetX = zoomY - this.image.current.clientHeight / 2; - offsetY = this.image.current.clientWidth / 2 - zoomX; + offsetX = anchorY - this.image.current.clientHeight / 2; + offsetY = this.image.current.clientWidth / 2 - anchorX; break; case 180: - offsetX = zoomX - this.image.current.clientWidth / 2; - offsetY = zoomY - this.image.current.clientHeight / 2; + offsetX = anchorX - this.image.current.clientWidth / 2; + offsetY = anchorY - this.image.current.clientHeight / 2; break; case 270: - offsetX = this.image.current.clientHeight / 2 - zoomY; - offsetY = zoomX - this.image.current.clientWidth / 2; + offsetX = this.image.current.clientHeight / 2 - anchorY; + offsetY = anchorX - this.image.current.clientWidth / 2; } // Apply the zoom and offset From 5d4b293e0a189efd19dc755c5aad8e3d20452dba Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 22 Jul 2021 09:56:26 -0400 Subject: [PATCH 6/6] Add comment about modulo operator Signed-off-by: Robin Townsend --- src/components/views/elements/ImageView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 1979da52956..db63dbbfc20 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -188,6 +188,8 @@ export default class ImageView extends React.Component { // 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;