Image Perspective Distortion in JavaScript

Javed Baloch
Bits and Pieces
Published in
10 min readMar 10, 2023

--

Image by kjpargeter on Freepik

Perspective distortion in programming involves altering the appearance of an object and its surroundings to make it appear as if it was viewed from a different perspective than the standard focal length.

This effect is achieved by manipulating the coordinates of the image pixels, resulting in an image that simulates 3D space, depth, and distance. The distorted image can be used for various purposes such as creating realistic 3D effects or transforming images in unique ways.

It is widely used in image and video editing, computer graphics, and game development to create visually interesting effects and enhance the overall realism of a scene.

In this article, we’ll explore the basics of some of the popular techniques for creating perspective distortion effects using JavaScript and the HTML canvas element.

We will start by setting up the canvas and image elements essential for executing our perspective distortion algorithms.

<html>
<head>
<title>Image Perspective Distortion in JavaScript</title>
</head>
<body>
<canvas id="canvas"></canvas>
<img id="image" src="https://source.unsplash.com/random/800x800">
</body>
</html>

Vanishing Point Perspective

For our first perspective distortion effect we will explore one of the fundamental perspective distortion algorithms — Vanishing Point Perspective. It is a type of linear perspective that creates a sense of depth and distance in a 2D image.

The algorithm works by manipulating the x and y coordinates of an image based on the distance from the vanishing point, hence creates an illusion of 3D in a 2D image by giving the appearance of receding lines that meet at a single point in the distance.

We can implement Vanishing Point Perspective on an image in canvas by defining a focal point and apply scaling to the image based on the distance from the focal point.

The focal point is the point in the image where all lines converge. Or more simply put, think of it as the point on the horizon where the ground and sky appear to meet.

We will first define the canvas element and its context, as well as the image element. The image dimensions are also specified and set as the canvas size.

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const image = document.getElementById('image');
const imageWidth = 800;
const imageHeight = 800;
canvas.width = imageWidth;
canvas.height = imageHeight;

Next, several variables are defined to specify the center of the image, the horizon line (which is the line separating the sky and ground), the distance of the viewer from the image, the scaling factor, and the maximum angle of rotation.

The angleIncrement variable is used to control the rate at which the angle changes as the distance from the viewer changes. In this case, it's set to zero so the angle doesn't change.

const centerX = imageWidth / 2;
const centerY = 0;
const horizon = 0;
const distance = 2000;
const scalingFactor = 1;
const maxAngle = 0;
const angleIncrement = 0;

The applyVanishingPointPerspective function is then defined, which loops over each pixel in the image and applies the vanishing point perspective transformation using avanishingPointPerspective function.

The drawImage method is used to draw each pixel of the original image onto the canvas with the transformed coordinates.

function applyVanishingPointPerspective() {
for (let x = 0; x < imageWidth; x++) {
for (let y = 0; y < imageHeight; y++) {
const { x: xPrime, y: yPrime } = vanishingPointPerspective(x, y, centerX, centerY, horizon, distance, scalingFactor, angleIncrement);
context.drawImage(image, x, y, 1, 1, xPrime, yPrime, 1, 1);
}
}
}

Finally, the vanishingPointPerspective function is defined. This function takes the x and y coordinates of a pixel and calculates its new position after applying the vanishing point perspective transformation.

function vanishingPointPerspective(x, y, centerX, centerY, horizon, distance, scalingFactor, angleIncrement) {
const theta = Math.atan2(y - centerY, x - centerX);
const distanceFromViewer = Math.sqrt(Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2));
const distanceFromHorizon = Math.abs(y - horizon);
const angle = Math.min(maxAngle, angleIncrement * distanceFromViewer);
const scaling = distance / (distance + distanceFromViewer);
const z = distanceFromHorizon * scaling * scalingFactor;
const xPrime = centerX + z * Math.cos(theta + angle);
const yPrime = centerY + z * Math.sin(theta + angle);
return { x: xPrime, y: yPrime };
}

The algorithm involves calculating the angle between the viewer’s line of sight and the horizontal line passing through the pixel. The distance from the viewer to the pixel is also calculated, as well as the distance from the horizon to the pixel.

The angle of rotation is calculated based on the distance from the viewer, and the scaling factor is used to adjust the size of the image based on the viewer’s distance.

Finally, the new x and y coordinates are calculated using trigonometric functions and returned as an object.

An onload event listener is also added to the image element in order to invoke applyVanishingPointPerspective function.

image.addEventListener('load', () => {
applyFishEyePerspective();
});

Checkout the comparison between the original image on the left vs image with Vanishing Point Perspective applied on the right:

Photo by Diego Jimenez on Unsplash

Vanishing point perspective is a simple but powerful technique for applying a sense of depth and distance on any given image. It allows us to create stunning visual effects by using a canvas element and defining a focal point and applying scaling based on distance.

Fish-Eye Perspective

Fish-Eye perspective technique is often used in photography to create dramatic, stylized images and visually creates a wide-angle, distorted view of an image.

Programmatically speaking, the core of the Fish-Eye Perspective algorithm revolves around taking each pixel in the original image and mapping it to a new position in the distorted image based on its distance from the center of the image and its angle relative to the center.

The fish-eye effect is a popular distortion effect that simulates a wide-angle view or a fish-eye lens effect. In this case, we will be creating the fish-eye effect using JavaScript on an HTML canvas element.

First, we declare and initialize the canvas element, its context, and the image element. We also set canvas dimensions to be equal to the image’s width and height.

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const image = document.getElementById('image');
const imageWidth = 800;
const imageHeight = 800;
canvas.width = imageWidth;
canvas.height = imageHeight;

The centerX and centerY variables are then set to the center of the canvas element. The strength variable is set to 1, which determines the degree of the fisheye effect, and radius is calculated as the minimum of centerX and centerY multiplied by strength.

const centerX = imageWidth / 2;
const centerY = imageHeight / 2;
const strength = 1;
const radius = Math.min(centerX, centerY) * strength;

The applyFishEyePerspective function is where the actual fisheye effect is applied. The function first draws the original image on the canvas using the drawImage() method.

function applyFishEyePerspective() {
context.drawImage(image,0,0);
// ...
}

The function then loops through each pixel in the canvas and applies the fisheye effect to the pixel if it is within the specified radius.

The dx and dy variables represent the distance of the pixel from the center of the canvas, while the distance variable represents the overall distance from the center of the canvas to the pixel.

for (let x = 0; x < imageWidth; x++) {
for (let y = 0; y < imageHeight; y++) {
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
// ...
}
}

If the pixel is within the specified radius, the function calculates the angle of the pixel relative to the center of the canvas, as well as a new distance for the pixel based on the radius and strength of the effect. The newX and newY variables are then calculated using trigonometry and the drawImage() method is used to draw the pixel at its new position on the canvas.

if (distance < radius) {
const angle = Math.atan2(dy, dx);
const r = distance / radius;
const newDistance = r * r * radius;
const newX = centerX + newDistance * Math.cos(angle);
const newY = centerY + newDistance * Math.sin(angle);
context.drawImage(image, x, y, 1, 1, newX, newY, 1, 1);
}

Finally, the image element is set to call the applyFishEyePerspective() function when it finishes loading.

image.addEventListener('load', () => {
applyFishEyePerspective();
});

See the difference between the original image on the left against the image with Fish-Eye Perspective on the right:

Photo by Samson on Unsplash

It’s important to note that the Fish-Eye Perspective algorithm works by manipulating the image data pixel by pixel. This can be a slow operation for large images, so it’s recommended to use smaller images or to optimize the code for performance.

Stereographic Projection

Stereographic projection is a technique used in computer graphics to create a 3D effect by projecting a 3D object onto a 2D plane. In this technique, a point in 3D space is projected onto a sphere, which is then projected onto a plane.

This results in a distorted image that appears to be in 3D. Stereographic projection can be used to create a variety of effects, such as fish-eye lenses and panoramic views.

The projection is derived from a mathematical formula that takes into account the distance between the observer and the object being projected.

We will define applyStereographicProjection() function to perform the stereographic projection on the image. We start by getting the width and height of the image, and setting the canvas dimensions to match.

function applyStereographicProjection() {
const imageWidth = image.width;
const imageHeight = image.height;
canvas.width = imageWidth;
canvas.height = imageHeight;

Next, we will set the center of the projection to be the center of the image.

const centerX = imageWidth / 2;
const centerY = imageHeight / 2;

Then draw the original image onto the canvas using drawImage().

context.drawImage(image, 0, 0);

Next, we define the distance from the center of the projection as d. We loop through each pixel in the image and calculate its distance r from the center of the image.

const d = 1;

for (let x = 0; x < imageWidth; x++) {
for (let y = 0; y < imageHeight; y++) {
const dx = x - centerX;
const dy = y - centerY;
const r = Math.sqrt(dx * dx + dy * dy);
if (r < centerX) {
const theta = Math.atan2(dy, dx);
const phi = 4 * Math.atan(d / (4 * r));
const newX = centerX + Math.sin(phi) * Math.cos(theta) * d / (1 - Math.cos(phi));
const newY = centerY + Math.sin(phi) * Math.sin(theta) * d / (1 - Math.cos(phi));
context.drawImage(image, x, y, 1, 1, newX, newY, 1, 1);
}
}
}

We also need to check if the distance r is less than the radius of the image, so that we only apply the projection to the pixels within the circle. Next, we calculate the angle theta and the projection angle phi using the Math.atan2() and Math.atan() methods.

Finally, we calculate the new x and y positions using the formulas for stereographic projection, and draw the pixel at the new position.

Here is the complete code of applyStereographicProjection() function.

function applyStereographicProjection() {

const imageWidth = image.width;
const imageHeight = image.height;
canvas.width = imageWidth;
canvas.height = imageHeight;
const centerX = imageWidth / 2;
const centerY = imageHeight/ 2;
context.drawImage(image, 0, 0);

const d = 1;

for (let x = 0; x < imageWidth; x++) {
for (let y = 0; y < imageHeight; y++) {
const dx = x - centerX;
const dy = y - centerY;
const r = Math.sqrt(dx * dx + dy * dy);
if (r < centerX) {
const theta = Math.atan2(dy, dx);
const phi = 4 * Math.atan(d / (4 * r));
const newX = centerX + Math.sin(phi) * Math.cos(theta) * d / (1 - Math.cos(phi));
const newY = centerY + Math.sin(phi) * Math.sin(theta) * d / (1 - Math.cos(phi));
context.drawImage(image, x, y, 1, 1, newX, newY, 1, 1);
}
}
}

}

With some tweaks to the distance d and other variables, you can add further variations to the effect on the image.

💡 Tip: You can now use Bit to share and reuse your Stereographic Projection logic as utility functions between apps. This way, you’ll have independent versioning, tests, and documentation for them too, making it easier for others to understand and use your code. No more copy/pasting repeatable code from repos. Find out more here.

Finally, we add an event listener to the image element to call the applyStereographicProjection() function when the image has loaded.

image.addEventListener('load', () => {
applyStereographicProjection();
});

See the Stereographic Projection in action with the image on right as compared to its original on the left.

Photo by Alberto Frías on Unsplash

Conclusion

While this article has covered only the basics and provided a guideline for implementing some popular perspective distortion effects in JavaScript, there is still a lot more to explore and readers can continue to experiment and build upon these techniques to create even more compelling visual experiences.

Overall, the world of image perspective distortion is a fascinating and ever-evolving field, and there is always more to learn and discover.

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

E-Commerce, AI Driven DIY Applications, Graphics Programming, Blockchain