Introduction

Today, we’ll explore two important CSS properties: background-size and background-position. The idea is to take one image and split it into multiple pieces. Why do this? By arranging the pieces properly, we can make it look like one complete image. No magic—just simple CSS techniques that anyone can learn!

Image-Pieces

But wait, here’s the twist! We won’t actually be dividing the image. Instead, we’ll create several image divs and arrange them side by side. By setting their background position and size carefully, they will appear as one complete image.

In reality, it’s just an illusion of a single image split into multiple pieces. To make it more interesting, we’ll add animations, making it look like the pieces are moving apart and then coming back together, creating a captivating visual effect.

There might be other, more optimised ways to do this, but I found this approach fun and challenging.

Let’s start coding:

Number of rows and columns:

This function calculates the optimal number of rows and columns for arranging a grid layout based on the total number of elements.

export function calculateGridDimensions(
  numberOfPieces: number
): GridDimensions {
  const idealGridSize = Math.ceil(Math.sqrt(numberOfPieces))

  let columns = idealGridSize
  let rows = Math.ceil(numberOfPieces / columns)

  while (columns * rows > numberOfPieces) {
    columns -= 1
    rows = Math.ceil(numberOfPieces / columns)
  }

  return { rows, columns }
}

Piece Dimension

This function calculates the dimensions for each piece in a grid layout based on the total width and height of the grid and the number of pieces. It first determines the grid dimensions using the calculateGridDimensions function, then divides the total width and height by the number of rows and columns respectively to get the dimensions for each piece. Finally, it returns the width and height of each piece as an object.

export function calculatePieceDimensions(
  width: number,
  height: number,
  numberOfPieces: number
): PieceDimensions {
  const { rows, columns } = calculateGridDimensions(numberOfPieces)

  const pieceWidth = width / rows
  const pieceHeight = height / columns

  return { width: pieceWidth, height: pieceHeight }
}

Piece Position

This function figures out where a piece should be placed within a grid based on its index, the total number of pieces, and the grid’s dimensions. It first checks if the index is valid, and then calculates the position of the piece in the grid—like top-left, bottom-right, and so on.

export function calculatePiecePosition(
  index: number,
  numberOfPieces: number,
  gridDimensions: GridDimensions
): PiecePosition | null {
  if (index < 0 || index >= numberOfPieces) {
    console.error('Invalid index.')
    return null
  }

  const row = Math.floor(index / gridDimensions.rows)
  const col = index % gridDimensions.rows

  const isTop = row === 0
  const isBottom = row === gridDimensions.columns - 1
  const isLeft = col === 0
  const isRight = col === gridDimensions.rows - 1

  if (isTop && isLeft) return PiecePosition.TOP_LEFT
  if (isTop && isRight) return PiecePosition.TOP_RIGHT
  if (isBottom && isLeft) return PiecePosition.BOTTOM_LEFT
  if (isBottom && isRight) return PiecePosition.BOTTOM_RIGHT
  if (isLeft) return PiecePosition.LEFT
  if (isTop) return PiecePosition.TOP
  if (isRight) return PiecePosition.RIGHT
  if (isBottom) return PiecePosition.BOTTOM

  return PiecePosition.CENTER
}

Validation Check:

  • It first checks if the given index is valid. If the index is less than 0 or greater than the total number of pieces, it logs an error and returns null.

Calculating Row and Column:

  • It calculates the row and column of the piece based on its index and the grid dimensions.
  • The row is calculated by dividing the index by the total number of rows, and the column is found using the remainder of the index divided by the number of rows.

Determining the Position:

  • It checks if the piece is located at a specific edge or corner of the grid, like:
  • Top-Left, Top-Right, Bottom-Left, Bottom-Right for corners. Top, Bottom, Left, Right for edges.
  • If it’s not at an edge or corner, it must be in the center.

Returning the Position:

  • Based on these conditions, it returns the corresponding position from the PiecePosition enum.

Piece Animation based on its position

Adding a GitHub link for this to keep the blog short.

Piece Style

This function figures out the background position and size for each piece in a grid. It starts by calculating how many rows and columns the grid should have and the size of each piece. Then, it sets where each piece’s background should be placed and how big it should be so that all pieces fit together correctly.

export function calculatePieceStyles(
  index: number,
  width: number,
  height: number,
  numberOfPieces: number
): PieceStyles | null {
  const gridDimensions = calculateGridDimensions(numberOfPieces)

  const pieceDimensions = calculatePieceDimensions(
    width,
    height,
    numberOfPieces
  )

  const { rows: columns, columns: rows } = gridDimensions
  const { width: pieceWidth, height: pieceHeight } = pieceDimensions

  const row = Math.floor(index / gridDimensions.rows)
  const col = index % gridDimensions.rows

  const backgroundSizeX = pieceWidth * columns
  const backgroundSizeY = pieceHeight * rows
  const backgroundSize = `${backgroundSizeX}px ${backgroundSizeY}px`

  const shiftXBy = columns > 1 ? 100 / (columns - 1) : 100
  const shiftYBy = rows > 1 ? 100 / (rows - 1) : 100
  const backgroundPositionX = col * shiftXBy
  const backgroundPositionY = row * shiftYBy
  const backgroundPosition = `${backgroundPositionX}% ${backgroundPositionY}%`

  return {
    backgroundPosition,
    backgroundSize
  }
}

Finally, let’s join the pieces together!

import React from 'react'
import styles from './Animations/styles.module.css'
import {
  calculateGridDimensions,
  calculatePieceDimensions,
  calculatePiecePosition,
  calculatePieceStyles
} from './Animations/utils'
import { type AnimationsComponentProps } from './types'

export const ImagePiece = (
  props: AnimationsComponentProps
): React.JSX.Element => {
  const {
    imgSrc,
    height: CONTAINER_HEIGHT,
    width: CONTAINER_WIDTH,
    pieces: PIECES,
    animationDirection = 'normal',
    animationDuration = '1s',
    animationIterationCount = 'infinite'
  } = props

  function createNumberArray(): number[] {
    const result: number[] = []
    for (let i = 1; i <= PIECES; i += 1) {
      result.push(i)
    }
    return result
  }

  const pieceDimensions = calculatePieceDimensions(
    CONTAINER_WIDTH,
    CONTAINER_HEIGHT,
    PIECES
  )

  return (
    <div>
      <div
        className={styles.container}
        style={{ width: CONTAINER_WIDTH, height: CONTAINER_HEIGHT }}
      >
        {createNumberArray().map((item, index) => {
          const { width, height } = pieceDimensions

          return (
            <div
              key={item}
              style={{
                width,
                height,
                animationIterationCount,
                animationDuration,
                animationDirection,
                backgroundImage: `url(${imgSrc})`,
                ...calculatePieceStyles(
                  index,
                  CONTAINER_WIDTH,
                  CONTAINER_HEIGHT,
                  PIECES
                )
              }}
              className={`${styles.piece} ${
                styles[
                  `piece_${calculatePiecePosition(
                    index,
                    PIECES,
                    calculateGridDimensions(PIECES)
                  )}`
                ]
              }`}
            />
          )
        })}
      </div>
    </div>
  )
}

Conclusion

In conclusion, we learned how CSS properties like background-size and background-position can turn a single image into a creative set of moving pieces. By using these properties smartly, we made images look alive with cool motion effects.

If you want to try this out, check out react-img-pieces, an npm package I made to make these animations easier in React projects.

Warning

But keep in mind, if you’re working with thousands of image pieces, it might slow down performance because each piece is a separate HTML element (like a div). Thousands of elements increase the complexity of the DOM, making rendering and layout calculations heavier for the browser. Moving or resizing elements (like changing background-position or transform) can trigger repaints and reflows, which are expensive operations in the rendering pipeline.

GitHub: react-img-pieces

NPM: react-img-pieces

Thanks for reading, and happy coding! 🚀