Skip to content

Commit

Permalink
fix: maximize displayed portion of zoom image (#285)
Browse files Browse the repository at this point in the history
* fix: correct source map on api
* fix: respect image bounds when scaling iamge from viewport
  • Loading branch information
micahg authored Aug 26, 2024
1 parent 46288ec commit 6457cbd
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 155 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);

Expand Down
2 changes: 0 additions & 2 deletions packages/api/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ module.exports = {
resolve: {
extensions: [".webpack.js", ".web.js", ".ts", ".js"],
},
devtool: "eval-source-map",
module: {
rules: [
{
Expand All @@ -34,7 +33,6 @@ module.exports = {
},
],
},
externals: nodeModules,
ignoreWarnings: [
{
module: /opentelemetry/,
Expand Down
4 changes: 2 additions & 2 deletions packages/api/webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}),
],
});
});
6 changes: 1 addition & 5 deletions packages/api/webpack.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
62 changes: 19 additions & 43 deletions packages/mui/src/utils/contentworker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
scalePoints,
translatePoints,
copyRect,
zoomFromViewport,
adjustImageToViewport,
} from "./geometry";
import { Rect } from "@micahg/tbltp-common";

Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -398,7 +374,7 @@ function fullRerender(zoomOut = false) {
_img.y = 0;
}
adjustZoomFromViewport();
calculateViewport(_angle, _zoom, _canvas.width, _canvas.height);
calculateViewport();
renderAllCanvasses(backgroundImage);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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({
Expand Down
146 changes: 91 additions & 55 deletions packages/mui/src/utils/geometry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getRect } from "./drawing";
import { Rect } from "@micahg/tbltp-common";

export interface Point {
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 6457cbd

Please sign in to comment.