ImageRotator

This repository contains a JavaScript code capable of rotate an given image ImageData.

Summary

The algorithm to rate an ImageData is implemented in the Rotator class and it is composed of five different steps:

  1. Create a Pixel matrix;
  2. Scale the matrix;
  3. Rotate the matrix;
  4. Antialiasing the matrix;
  5. Create a new ImageData.

Create a Pixel Matrix

The first step is converting the original ImageData to an matrix of Pixel.

This step also ensures that the resulting matrix is an square matrix, in order to avoid problems of resizing the image.

Before After
Before After

For square images, the result will be the same as the input:

Before After
Before After

Scale the matrix

This step is responsible of scale the Pixel's matrix according the rotation angle.

The algorithm uses the triangle properties with cos(θ) and sin(θ), like the image below:

Scale

let cos = Math.cos(angle);
let sin = Math.sin(angle);
let newHeight, newWidth;

newWidth = (width * cos) + (height * sin);
newHeight = (width * sin) + (height * cos);

It is important to notice that, the rotation itself is not calculated yet. The picture shown above is just an illustration to help in the problem understanding.

After this step the Pixel's matrix scale to the right size.

Before After (30 degrees)
Original After
Before After (30 degrees)
Original After

Rotate the matrix

Rotate the matrix around its central point is the main algorithm of this class. It works following the concept of a rotation matrix.

This algorithm transverse all image's matrix, calculating the new X,Y position for each Pixel.

The center of the image is defined as 0,0 position, then, the new position is calculated based on the rotation matrix:

Image

let cos = Math.cos(radians),
    sin = Math.sin(radians),
    newX = (cos * (row - centralRow)) + (sin * (column - centralColumn)) + centralRow,
    newY = (cos * (column - centralColumn)) - (sin * (row - centralRow)) + centralColumn;
return [this.normalizeNumber(newX), this.normalizeNumber(newY)];
Before After (30 degrees)
Original After
Before After (30 degrees)
Original After

Since there is a rounding (normalizeNumber uses Math.round), some positions in the matrix are not filled. it can prejudice the final result.

Antialiasing the matrix

As above mentioned the previous step can create some "holes" in the image.

In order to enhance the result, the antialiasing algorithm is responsbile to transverse the image's matrix and calculates the average of the neighbors pixels RGBA for each transparent pixel that contains at least four valid neighbors.

A invalid neighbor is defined as: A pixel transparent, out of the matrix bounds or undefined.

The image below shows the algorithm calculating a single pixel:

Calculated Antialiasing

static antialiasing(matrix, row, column) {
    if (Pixel.isOutOfBounds(matrix,row, column)) throw new RangeError();

    let pixel = matrix[row][column];
    let neighbors = Pixel.getNeighbors(matrix,row,column);

    if (neighbors.length > 3) {
        pixel = Pixel.getAverageRGB(neighbors);
    }

    return pixel;
    }
Before After (30 degrees)
Original After
Before After (30 degrees)
Original After

Create a new ImageData

This process simply transverse the whole image matrix and create a new object.

createNewImageArray(antialisedMatrix) {
    const newimageData = {
        width: antialisedMatrix.length,
        height: antialisedMatrix[0].length,
        data: []
    };

    let dataArray = this.toDataArray(antialisedMatrix);
    for (let i = 0; i < dataArray.length; i++) {
        newimageData.data[i] = dataArray[i];
    }

    return newimageData;
}

In order to be able to run it at NodeJS, I didn't use the actual ImageData type, since it is part of the Canvas API.