Skip to content

Virtual Viewport

The virtual viewport represents how the 'real' part of an image, (the part that actually contains color pixel data), fits in a larger context of a 'canvas'.

Viewport class is used in Lens to represent image virtual viewport and provides some convenient helper methods for common viewport operations.

Virtual viewport is defined by two points:

  • (x1, y1) — coords of top-left pixel in 'canvas' coordinate space. You also can think of it as of image's origin offset in that space.
  • (x2, y2) — coords of bottom-left pixel in 'canvas' coordinate space.

In a fresh, not distorted image these points are (0, 0) and (imageWidth - 1, imageHeight - 1).

Distortion functions are taking in account virtual viewport and operate with viewport coords, not the image coords. However, in general case these coords are same.

Resolving Output Image Viewport

Under the hood Lens first makes new blank image and then loops through it's viewport coords, mapping the source image viewport coords to take color from.

There are three ways of how we get the new image viewport coords (and thus it's size).

Using source image's viewport

The most common way is just to take source image's viewport. It is used as default method and is supported by about all distortions. However, it doesn't guarantee that result image will contain the whole distortion result. In fact, there may be situations when it will be completely empty.

Have a look at this code:

ts
import { distort, Canvas } from "@alxcube/lens";
// example image of size 100x100
const imgUrl = "some-image-100x100.png";
const adapter = await Canvas.createFromUrl(imgUrl);
const { x1: sx1, y1: sy1, x2: sx2, y2: sy2 } = adapter.getViewport();
console.log(sx1, sy1, sx2, sy2); // 0, 0, 99, 99

// Move image 100px to the right, using Affine distortion
const { image } = await distort(adapter, "Affine", [0, 0, 100, 0]);
const { x1: dx1, y1: dy1, x2: dx2, y2: dy2 } = image.getViewport();
console.log(dx1, dy1, dx2, dy2); // 0, 0, 99, 99 -- source and result viewports are equal

// display image
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext("2d").drawImage(image.getResource(), 0, 0);
document.body.appendChild(canvas);

The distorted image will be empty since we moved all the pixels out of viewport bounds.

Calculating 'best-fit' viewport

If you set viewport option to true or "bestFit", Lens will calculate the 'best-fit' viewport, which will contain whole distorted image.

Unfortunately, not all distortions supports this. If selected distortion doesn't support best fit viewport calculation, this option will be ignored, and source image viewport will be used instead.

Also, some distortions may force best fit calculation regardless of viewport option value.

Let's modify the code above to see how 'best-fit' calculation works:

ts
import { distort, Canvas } from "@alxcube/lens";
// example image of size 100x100
const imgUrl = "some-image-100x100.png";
const adapter = await Canvas.createFromUrl(imgUrl);
const { x1: sx1, y1: sy1, x2: sx2, y2: sy2 } = adapter.getViewport();
console.log(sx1, sy1, sx2, sy2); // 0, 0, 99, 99

// Move image 100px to the right, using Affine distortion
const { image } = await distort(adapter, "Affine", [0, 0, 100, 0]); ;
const { image } = await distort(adapter, "Affine", [0, 0, 100, 0], { 
  viewport: true, 
}); 
const { x1: dx1, y1: dy1, x2: dx2, y2: dy2 } = image.getViewport();
console.log(dx1, dy1, dx2, dy2); // 0, 0, 99, 99 -- source and result viewports are equal
console.log(dx1, dy1, dx2, dy2); // 100, 0, 199, 99 -- source and result viewports are now different

// display image
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext("2d").drawImage(image.getResource(), 0, 0);
document.body.appendChild(canvas);

Now if we'll look at distorted image, it will look the same as original image (well, almost the same, since it will be resampled). But if you look at viewport — you'll see that it is different.

Knowing that viewport offset, you can properly compose distorted image with other images.

Using user-provided viewport

At last, you can set viewport option to specified viewport which will be used as viewport of output image. You can use it e.g. for cropping result or for enlarging output image to make sure they contain whole image for distortions that can't calculate best fit.

You can set specified viewport with one of the following ways:

By providing Viewport class instance:

ts
import { distort, Viewport } from "@alxcube/lens";

// ...

const result = await distort(img, distortionName, args, {
  viewport: new Viewport(topLeftX, topLeftY, bottomRightX, bottomRightY),
});

By providing viewport literal object:

ts
distort(image, distortionName, args, {
  viewport: {
    x1: topLeftX,
    y1: topLeftY,
    x2: bottomRightX,
    y2: bottomRightY,
  },
});

// or

distort(image, distortionName, args, {
  viewport: {
    width: outputWidth,
    height: outputHeight,
    x: topLeftX, // optional, equals 0 if omitted
    y: topLeftY, // optional, equals 0 if omitted
  },
});

Let's modify our code to see how it works:

ts
import { distort, Canvas } from "@alxcube/lens"; 
import { distort, Canvas, Viewport } from "@alxcube/lens"; 
// example image of size 100x100
const imgUrl = "some-image-100x100.png";
const adapter = await Canvas.createFromUrl(imgUrl);
const { x1: sx1, y1: sy1, x2: sx2, y2: sy2 } = adapter.getViewport();
console.log(sx1, sy1, sx2, sy2); // 0, 0, 99, 99

// Move image 100px to the right, using Affine distortion
const { image } = await distort(adapter, "Affine", [0, 0, 100, 0], {
  viewport: true, 
  viewport: new Viewport(0, 0, 199, 99), 
});
const { x1: dx1, y1: dy1, x2: dx2, y2: dy2 } = image.getViewport();
console.log(dx1, dy1, dx2, dy2); // 100, 0, 199, 99 -- source and result viewports are now different
console.log(dx1, dy1, dx2, dy2); // 0, 0, 199, 99 -- same as was provided

// display image
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext("2d").drawImage(image.getResource(), 0, 0);
document.body.appendChild(canvas);

Now the distorted image will have size of 200x100 px and will contain empty area in it's left half, and translated image in it's right half.