• Web Developer
  • Posts
  • Creating a React Hook for Rotating Images at Any Angle

Creating a React Hook for Rotating Images at Any Angle

Build a React hook to rotate images at any angle while maintaining their aspect ratio, ensuring flexible and distortion-free image manipulation.

In web development, you may need to rotate an image, which is easy to do in CSS. Simple code like this transform: rotate(90deg);. But what if we want to do it in JS?

TLDR

Draws the image to the canvas in the browser environment and rotates it. But before that, we need to do some math to keep the original image aspect ratio.

Core

Assuming that we have loaded the image, calculating the rotated image can be done as follows:

const { PI, sin, cos, abs } = Math;
const angle = (degree * PI) / 180;
const sinAngle = sin(angle);
const cosAngle = cos(angle);

const rotatedWidth = abs(imageWidth * cosAngle) + abs(imageHeight * sinAngle);

const rotatedHeight = abs(imageWidth * sinAngle) + abs(imageHeight * cosAngle);

And next, we use some of the canvas APIs to do the actual rotation:

const canvas = document.createElement('canvas');

const { width: canvasWidth, height: canvasHeight } = canvas;
const canvasCtx2D = canvas.getContext('2d');

canvasCtx2D.clearRect(0, 0, canvasWidth, canvasHeight);
canvasCtx2D.translate(canvasWidth / 2, canvasHeight / 2);
canvasCtx2D.rotate(angle);

canvasCtx2D.drawImage(
  image,
  -imageWidth / 2,
  -imageHeight / 2,
  imageWidth,
  imageHeight,
);

return canvas.toDataURL('image/png');

Wrap up

With the core code in place, we can make some optimizations and write dedicated React hooks to use it:

import { useEffect, useRef, useState } from 'react';

type RotatedImage = {
  src: string;
  width: number;
  height: number;
} | null;

let canvas: HTMLCanvasElement | null = null;
let canvasCtx2D: CanvasRenderingContext2D | null = null;

const getRotatedImage = (
  image: HTMLImageElement | null,
  rotation: number,
): RotatedImage => {
  canvas ??= document.createElement('canvas');
  canvasCtx2D ??= canvas.getContext('2d');

  if (!image || !canvasCtx2D) return null;

  const { width: imageWidth, height: imageHeight, currentSrc } = image;
  const degree = rotation % 360;
  if (!degree) {
    return {
      src: currentSrc,
      width: imageWidth,
      height: imageHeight,
    };
  }

  const { PI, sin, cos, abs } = Math;
  const angle = (degree * PI) / 180;
  const sinAngle = sin(angle);
  const cosAngle = cos(angle);

  canvas.width = abs(imageWidth * cosAngle) + abs(imageHeight * sinAngle);
  canvas.height = abs(imageWidth * sinAngle) + abs(imageHeight * cosAngle);

  // The width and height of the canvas will be automatically rounded.
  const { width: canvasWidth, height: canvasHeight } = canvas;

  canvasCtx2D.clearRect(0, 0, canvasWidth, canvasHeight);
  canvasCtx2D.translate(canvasWidth / 2, canvasHeight / 2);
  canvasCtx2D.rotate(angle);

  canvasCtx2D.drawImage(
    image,
    -imageWidth / 2,
    -imageHeight / 2,
    imageWidth,
    imageHeight,
  );

  const src = canvas.toDataURL('image/png');
  canvas.width = 0;
  canvas.height = 0;

  return {
    src,
    width: canvasWidth,
    height: canvasHeight,
  };
};

const useRotateImage = (imageSrc: string, rotation?: number): RotatedImage => {
  const imageEle = useRef<HTMLImageElement | null>(null);
  const [rotatedImage, setRotatedImage] = useState<RotatedImage>(null);

  useEffect(() => {
    if (typeof rotation === 'number') {
      let currImage = imageEle.current;

      if (currImage?.currentSrc !== imageSrc) {
        currImage = new Image();
        imageEle.current = currImage;

        currImage.src = imageSrc;
      }

      currImage.decode().then(
        () => setRotatedImage(getRotatedImage(currImage, rotation)),
        () => setRotatedImage(null),
      );
    }
  }, [imageSrc, rotation]);

  return rotatedImage;
};

export default useRotateImage;

Here I reuse the same canvas element to reduce repeated creation. Secondly, it should be noted that I set its width and height to 0 after each rotation to reduce memory usage. By the way, I also did the operation of clearing the canvas. This is because in the HTML spec when you modify the width and height of the canvas (whether it is the same as before) will clear the canvas, which is the same as canvasCtx2D.clearRect(0, 0, canvasWidth, canvasHeight), which is supported by modern browsers.

In useRotateImage, I keep a reference to the image element and set the rotated image state after image.decode(), which is resolved after the image data is ready.

Below is a online use case:

If you found this helpful, please consider subscribing to my newsletter for more useful articles and tools about web development. Thanks for reading!

Reply

or to participate.