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!
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! 🚀