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:

```javascript
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:

```javascript
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:

```typescript
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](https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-set-bitmap-dimensions) 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](https://stackblitz.com/edit/vitejs-vite-6iarde?file=src%2FApp.tsx):

![React image rotation hook live demo showing image rotation at different angles with canvas manipulation](https://img.upweb.dev/content/2715edf6-1292x718.jpg)

_If you found this helpful, consider [subscribing to my newsletter](https://upweb.dev) for weekly web development insights and updates. Thanks for reading!_
