From 6457cbd710206768101fa617ad7844c66c22be79 Mon Sep 17 00:00:00 2001 From: Micah Galizia Date: Sun, 25 Aug 2024 21:32:10 -0400 Subject: [PATCH] fix: maximize displayed portion of zoom image (#285) * fix: correct source map on api * fix: respect image bounds when scaling iamge from viewport --- packages/api/src/server.ts | 2 +- packages/api/webpack.common.js | 2 - packages/api/webpack.dev.js | 4 +- packages/api/webpack.prod.js | 6 +- packages/mui/src/utils/contentworker.ts | 62 +++------- packages/mui/src/utils/geometry.ts | 146 +++++++++++++++--------- packages/mui/test/geometry.test.ts | 83 ++++++-------- 7 files changed, 150 insertions(+), 155 deletions(-) diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index dfeeb63..6d88cf6 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -19,7 +19,7 @@ import mongoose from "mongoose"; import { WebSocketServer } from "ws"; import { ValueType, metrics } from "@opentelemetry/api"; -// mongoose.set('debug', true); // +// mongoose.set('debug', true); log.info(`System starting in ${process.env.NODE_ENV}`); diff --git a/packages/api/webpack.common.js b/packages/api/webpack.common.js index 9f91a3b..8a80452 100644 --- a/packages/api/webpack.common.js +++ b/packages/api/webpack.common.js @@ -25,7 +25,6 @@ module.exports = { resolve: { extensions: [".webpack.js", ".web.js", ".ts", ".js"], }, - devtool: "eval-source-map", module: { rules: [ { @@ -34,7 +33,6 @@ module.exports = { }, ], }, - externals: nodeModules, ignoreWarnings: [ { module: /opentelemetry/, diff --git a/packages/api/webpack.dev.js b/packages/api/webpack.dev.js index 021d94e..be6df09 100644 --- a/packages/api/webpack.dev.js +++ b/packages/api/webpack.dev.js @@ -4,10 +4,10 @@ const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'development', - devtool: 'inline-source-map', + devtool: 'eval-source-map', plugins: [ new webpack.DefinePlugin({ PRODUCTION: JSON.stringify(false), }), ], -}); +}); \ No newline at end of file diff --git a/packages/api/webpack.prod.js b/packages/api/webpack.prod.js index 20c4850..555ef8a 100644 --- a/packages/api/webpack.prod.js +++ b/packages/api/webpack.prod.js @@ -4,14 +4,10 @@ const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'development', - devtool: 'inline-source-map', + devtool: 'source-map', plugins: [ new webpack.DefinePlugin({ PRODUCTION: JSON.stringify(true), }), ], }); - -// build the prod js with tree shaking node modules -// to avoid installing in the docker image -delete module.exports.externals; diff --git a/packages/mui/src/utils/contentworker.ts b/packages/mui/src/utils/contentworker.ts index 7287fe5..da141ca 100644 --- a/packages/mui/src/utils/contentworker.ts +++ b/packages/mui/src/utils/contentworker.ts @@ -16,6 +16,8 @@ import { scalePoints, translatePoints, copyRect, + zoomFromViewport, + adjustImageToViewport, } from "./geometry"; import { Rect } from "@micahg/tbltp-common"; @@ -122,22 +124,19 @@ function renderImage( ctx.restore(); } -function calculateViewport( - angle: number, - zoom: number, - containerWidth: number, - containerHeight: number, -) { - const [cw, ch] = [containerWidth, containerHeight]; - [_vp.width, _vp.height] = rotatedWidthAndHeight(-angle, cw, ch); - [_img.width, _img.height] = [zoom * _vp.width, zoom * _vp.height]; - if (_img.width > backgroundImage.width) { - _img.width = backgroundImage.width; - _vp.width = Math.round((_vp.height * _img.width) / _img.height); - } else if (_img.height > backgroundImage.height) { - _img.height = backgroundImage.height; - _vp.height = Math.round((_vp.width * _img.height) / _img.width); - } +function calculateViewport() { + // REMEMBER THIS METHOD UPDATES THE _vp and the _img + adjustImageToViewport( + _angle, + _zoom, + _canvas.width, + _canvas.height, + backgroundImage.width, + backgroundImage.height, + _vp, + _img, + ); + return; } /** @@ -156,30 +155,7 @@ function adjustZoomFromViewport() { // set our viewport to the initial value requested copyRect(_img_orig, _img); - // unrotate canvas - const [cW, cH] = rotatedWidthAndHeight( - -_angle, - _canvas.width, - _canvas.height, - ); - const zW = _img.width / cW; - const zH = _img.height / cH; - - // set zoom and offset x or y to compensate for viewport - // aspect ratios that are different from the screen - if (zH > zW) { - _zoom = zH; - const adj = cW * _zoom; - if (adj < _fullRotW) { - _img.x -= (adj - _img.width) / 2; - } - } else { - _zoom = zW; - const adj = cH * _zoom; - if (adj < _fullRotH) { - _img.y -= (adj - _img.height) / 2; - } - } + _zoom = zoomFromViewport(_angle, _canvas.width, _canvas.height, _img); } /** @@ -398,7 +374,7 @@ function fullRerender(zoomOut = false) { _img.y = 0; } adjustZoomFromViewport(); - calculateViewport(_angle, _zoom, _canvas.width, _canvas.height); + calculateViewport(); renderAllCanvasses(backgroundImage); } @@ -453,7 +429,7 @@ function adjustZoom(zoom: number, x: number, y: number) { q.x += _img.x; q.y += _img.y; _zoom = zoom; - calculateViewport(_angle, _zoom, _canvas.width, _canvas.height); + calculateViewport(); // calculate any offsets for where we are completely zoomed in in one dimension // note that we accommodate for the rotation const yOffset = _vp.height < cH ? cH - _vp.height : 0; @@ -590,7 +566,7 @@ self.onmessage = async (evt) => { _canvas.height = evt.data.height; if (backgroundImage) { adjustZoomFromViewport(); - calculateViewport(_angle, _zoom, _canvas.width, _canvas.height); + calculateViewport(); trimPanning(); fullRerender(); postMessage({ diff --git a/packages/mui/src/utils/geometry.ts b/packages/mui/src/utils/geometry.ts index 8b474e7..c772122 100644 --- a/packages/mui/src/utils/geometry.ts +++ b/packages/mui/src/utils/geometry.ts @@ -1,4 +1,3 @@ -import { getRect } from "./drawing"; import { Rect } from "@micahg/tbltp-common"; export interface Point { @@ -205,67 +204,104 @@ export function translatePoints(points: Point[], x: number, y: number) { } /** - * rotate and fill viewport to fit screen/window/canvas - * @param screen screen [width, height] - * @param image image [width, height] (actual -- might get shrunk by browser) - * @param oImage image [width, height] (original -- as the editor saw it -- possibly shrunk but we don't handle that yet) - * @param angle angle of rotation - * @param viewport viewport withing the original image {x, y, w, h} - * @returns + * Given a desired viewport, set our current viewport accordingly, set the zoom, + * and then center the request viewport within our screen, extending its short + * side to fit our screen. + * + * This is used when the remote client is told which region to display, rather + * than in the editor, where (IIRC) you calculate the viewpoint given a point, + * a zoom level and the canvas size. + */ +export function zoomFromViewport( + angle: number, + containerWidth: number, + containerHeight: number, + img: Rect, +) { + // un-rotate canvas + const [cW, cH] = rotatedWidthAndHeight( + -angle, + containerWidth, + containerHeight, + ); + const zW = img.width / cW; + const zH = img.height / cH; + return Math.max(zW, zH); +} + +/** + * Given an angle of rotation, zoom factor, container (canvas) size, and requested image, + * set the viewport to the same aspect of the screen and adjust the image to fill that viewport. + * @param angle the angle of rotation + * @param zoom the zoom factor + * @param containerWidth container (canvas) width + * @param containerHeight container (canvas) height + * @param backgroundWidth background image width + * @param backgroundHeight background image height + * @param viewport viewport (will be updated) + * @param image image area (will be updated) */ -export function fillRotatedViewport( - screen: number[], - image: number[], - oImage: number[], +export function adjustImageToViewport( angle: number, + zoom: number, + containerWidth: number, + containerHeight: number, + backgroundWidth: number, + backgroundHeight: number, viewport: Rect, + image: Rect, ) { - if ( - viewport.x === 0 && - viewport.y === 0 && - viewport.width === oImage[0] && - viewport.height === oImage[1] - ) { - return getRect(0, 0, image[0], image[1]); - } - const rScreen = rotatedWidthAndHeight(angle, screen[0], screen[1]); - const selR = viewport.width / viewport.height; - const scrR = rScreen[0] / rScreen[1]; - let { x, y, width: w, height: h } = viewport; + // screen w/h + const [cw, ch] = [containerWidth, containerHeight]; + const [rw, rh] = rotatedWidthAndHeight( + angle, + backgroundWidth, + backgroundHeight, + ); - // const newVP = { x: viewport.x, y: viewport.y, width: viewport.width, height: viewport.height }; - if (scrR > selR) { - const offset = Math.round((h * scrR - w) / 2); - w = Math.round(h * scrR); - if (x - offset < 0) - x = 0; // shunt to left screen bound rather than render a partial image - else if (x + w > oImage[0]) x = oImage[0] - w; - // shunt to right screen bound rather than render a partial image - else x -= offset; + // center the image - this can put the image x and y into the negatives or + // increase them so x + width or y + height are greater than the source image + // the following if block down below corrects those over/under adjustments + if (image.height / containerHeight > image.width / containerWidth) { + const adj = cw * zoom; + if (adj < rw) { + image.x -= (adj - image.width) / 2; + } } else { - const offset = Math.round((w / scrR - h) / 2); - h = Math.round(w / scrR); - if (y - offset < 0) y = 0; - else if (y + h + offset > oImage[1]) y = oImage[1] - h; - else y -= offset; + const adj = ch * zoom; + if (adj < rh) { + image.y -= (adj - image.height) / 2; + } } - // calculate coefficient for browser-resized images - // We shouldn't need to square (**2) the scaling value; however, I - // think due to a browser bug, squaring silkScale below is what works. - // FWIW, the bug was filed here: - // https://bugs.chromium.org/p/chromium/issues/detail?id=1494756 - // - // Some time before the end of March of 2024, the workaround stopped being - // necessary - // - // const silkScale = (image[0] / oImage[0]) ** 2; - const silkScale = image[0] / oImage[0]; - return { - x: x * silkScale, - y: y * silkScale, - width: w * silkScale, - height: h * silkScale, - }; + + // vp = rotated screen w/h + [viewport.width, viewport.height] = rotatedWidthAndHeight(-angle, cw, ch); + + // multiply viewport by zoom factor WHICH CAN LEAD TO IMAGE SIZES GREATER THAN ACTUAL IMAGE SIZE + [image.width, image.height] = [zoom * viewport.width, zoom * viewport.height]; + if (image.width > backgroundWidth) { + // img (scaled viewport) greater than actual image, so shrink it down and adjust the viewport to fit it + image.width = backgroundWidth; + viewport.width = Math.round((viewport.height * image.width) / image.height); + } else if (image.height > backgroundHeight) { + // one side of the displayed image region fits into our viewport + image.height = backgroundHeight; + viewport.height = Math.round((viewport.width * image.height) / image.width); + } else if (image.y < 0) { + // remember, our "image" dimensions are based on our viewport and zoom so if we're off the page, just slide and we'll still fit + image.y = 0; + } else if (image.x < 0) { + // remember, our "image" dimensions are based on our viewport and zoom so if we're off the page, just slide and we'll still fit + image.x = 0; + } else if (image.x + image.width > backgroundWidth) { + image.x = backgroundWidth - image.width; + } else if (image.y + image.height > backgroundHeight) { + image.y = backgroundHeight - image.height; + } + image.x = Math.round(image.x); + image.y = Math.round(image.y); + image.width = Math.round(image.width); + image.height = Math.round(image.height); } export function copyRect(source: Rect, destination: Rect) { diff --git a/packages/mui/test/geometry.test.ts b/packages/mui/test/geometry.test.ts index d9bdad4..faa4a1d 100644 --- a/packages/mui/test/geometry.test.ts +++ b/packages/mui/test/geometry.test.ts @@ -6,10 +6,10 @@ import { calculateBounds, rotatedWidthAndHeight, rotateBackToBackgroundOrientation, - fillRotatedViewport, normalizeRect, createRect, Point, + adjustImageToViewport, } from "../src/utils/geometry"; describe("Geometry", () => { @@ -124,53 +124,42 @@ describe("Geometry", () => { }); }); - describe("Rotate and Fill Viewport", () => { - it("Should rotate and fill the viewport horizontally", () => { - const screen = [960, 540]; - const image = [2008, 4160]; - const angle = 90; - const viewport = { x: 100, y: 100, width: 100, height: 100 }; - const result = fillRotatedViewport(screen, image, image, angle, viewport); - expect(result.width).toBe(100); - expect(result.height).toBe(178); - expect(result.x).toBe(100); - expect(result.y).toBe(61); - }); - - it("Should rotate and fill the viewport", () => { - const screen = [960, 540]; - const image = [2008, 4160]; - const angle = 90; - const viewport = { x: 100, y: 100, width: 100, height: 10 }; - const result = fillRotatedViewport(screen, image, image, angle, viewport); - expect(result.width).toBe(100); - expect(result.height).toBe(178); - expect(result.x).toBe(100); - expect(result.y).toBe(16); - }); - - it("BRAIN MELTING", () => { - const screen = [1420, 641]; - const image = [2008, 4160]; - const angle = 90; - const viewport = { x: 300, y: 1294, width: 72, height: 448 }; - const result = fillRotatedViewport(screen, image, image, angle, viewport); - expect(result.width).toBe(202); - expect(result.height).toBe(448); - expect(result.x).toBe(235); - expect(result.y).toBe(1294); - }); - - it("should retain viewport when not zoomed", () => { - const screen = [1420, 642]; - const image = [2888, 1838]; + describe("Adjust image and viewport to screen and background image size", () => { + it("Should not extend past the background width", () => { const angle = 0; - const viewport = { x: 0, y: 0, width: 2888, height: 1838 }; - const result = fillRotatedViewport(screen, image, image, angle, viewport); - expect(result.width).toBe(2888); - expect(result.height).toBe(1838); - expect(result.x).toBe(0); - expect(result.y).toBe(0); + const zoom = 0.41371241501522343; + const [cw, ch] = [2037, 1162]; + const [bw, bh] = [2888, 1839]; + const vp = { x: 0, y: 0, width: 0, height: 0 }; + const img = { x: 2773, y: 1341, width: 95, height: 480 }; + adjustImageToViewport(angle, zoom, cw, ch, bw, bh, vp, img); + expect(vp.x).toBe(0); + expect(vp.y).toBe(0); + expect(vp.width).toBe(2037); + expect(vp.height).toBe(1162); + expect(img.x).toBe(2045); + expect(img.y).toBe(1341); + expect(img.width).toBe(843); + expect(img.height).toBe(481); + return; + }); + it("Should extend before 0,0", () => { + const angle = 90; + const zoom = 0.3603392688134574; + const [cw, ch] = [2037, 1162]; + const [bw, bh] = [2888, 1839]; + const vp = { x: 0, y: 0, width: 0, height: 0 }; + const img = { x: 16, y: 1097, width: 27, height: 734 }; + adjustImageToViewport(angle, zoom, cw, ch, bw, bh, vp, img); + expect(vp.x).toBe(0); + expect(vp.y).toBe(0); + expect(vp.width).toBe(1162); + expect(vp.height).toBe(2037); + expect(img.x).toBe(0); + expect(img.y).toBe(1097); + expect(img.width).toBe(419); + expect(img.height).toBe(734); + return; }); });