May 26, 2023

Interactive Web Pages with React and Anime.js

By Daniel H Kim

blog post image

Anime.js is a lightweight JavaScript animation library that allows you to create smooth and powerful animations for web applications. It provides a simple syntax and supports a wide range of animation properties, easing functions, and timelines for creating dynamic and interactive user experiences. Below, we'll explore the implementation of Anime.js on this portfolio site.

Custom hook for window resizes

hook.js

"use client"
import React, { useLayoutEffect, useState } from 'react';

export function useWindowSize() {
  const [size, setSize] = useState([0, 0]);
  useLayoutEffect(() => {
    function updateSize() {
      setSize([window.innerWidth, window.innerHeight]);
    }
    window.addEventListener('resize', updateSize);
    updateSize();
    return () => window.removeEventListener('resize', updateSize);
  }, []);
  return size;
}

First, we need to create a custom hook to fetch the window size of the browser and update the width and height depending on window resizes. The `useLayoutEffect` is used to add an event listener ('resize') to run the `updateSize` callback function. The function will update the height and width to the current layout of browser window. To top off the `useLayoutEffect`, we add in a clean up function to remove the event listener.

Setting up tiles

Background.js

//...
const [width, height] = useWindowSize();

  const createTiles = (quantity) => {
    return Array.from(Array(quantity)).map((tile, index) => {
      return <Tile key={index} index={index} color={color} />
    })
  }

  useEffect(() => {
    anime({
      targets: '.tile',
      backgroundColor: 'rgb(65, 90, 119)',
      delay: anime.stagger(40, {
        grid: [grid.columns, grid.rows],
        from: 1,
      })
    })
  }, [grid])

  useEffect(() => {
    let columns = Math.floor(width / 30);
    let rows = Math.floor(height / 30);

    setGrid({ columns, rows });
  }, [height, width, setGrid]);

  const gridStyles = () => ({
    gridTemplateRows: `repeat(${grid.rows}, 1fr)`,
    gridTemplateColumns: `repeat(${grid.columns}, 1fr)`,
    overflow: 'hidden',
  });

//...

  <div
    ref={gridRef}
    className={`absolute tile-grid grid h-full w-full opacity-80`}
    style={gridStyles()}
  >
    {createTiles(grid.columns * grid.rows)}
  </div>
//...

In our `Background.js` file, we have the logic for creating and implementing the JSX of the tiles that will be animated. Here we also initiated the custom hook we built previously. The width and height is used later to calculate the amount of squares to create. The function `createTiles` creates an array of `<Tiles/>` depending on the quantity (which is calculated by the columns multiplied by rows).

We then implement two `useEffects`: one for the initial animation on page load and the other for setting the height and width on the `setGrid` useState (the grid is then used to animate tiles in the click handler further down below). The styles of the grid are also implemented here, using the CSS grid feature.

Click Handler

//..
  const [grid, setGrid] = useState({ columns: 0, rows: 0 });

  const handleClick = (event) => {
    const x = event.clientX;
    const y = event.clientY;
    const elements = document.elementsFromPoint(x, y);
    const index = findTileIndex(elements)
    setColor(colors[count % (colors.length - 1)])

    setCount(prev => prev + 1)

    anime({
      targets: '.tile',
      backgroundColor: color,
      delay: anime.stagger(40, {
        grid: [grid.columns, grid.rows],
        from: index,
      })
    })
  };

  const findTileIndex = (elementArray) => {
    const elementWithId123 = elementArray.find(el => el.className === 'tile h-8 w-8');
    return elementWithId123.getAttribute('data-index');
  };

//..

The `handleClick` function is triggered when the user clicks anywhere on the page. It retrieves the click coordinates and identifies elements at that point. The `findTileIndex` function locates a specific element with the class "tile h-8 w-8" and retrieves its data index. The count is incremented, and an Anime.js animation is triggered.

The anime() function

anime function

anime({
      targets: '.tile',
      backgroundColor: colors[count % (colors.length - 1)],
      delay: anime.stagger(40, {
        grid: [grid.columns, grid.rows],
        from: index,
      })

The `anime` function takes in an object for its argument. The keys in the argument include `targets`, `backgroundColor`, and `delay`. The `targets` key is used to specify which HTML element the animation function should target. The `backgroundColor` key is used to specify which color the element should be. In our case, the color will rotate among the color array based on the current count.

The ` delay` key has a value that is a function from the `anime` object. It is passed in a second counter for how long the animation will last. Then another object argument is passed in that specifies the grid and the index of the element where the click originated from.

The Anime.js animation modifies the background color of elements with the class "tile" based on the count value and predefined colors. The animation is delayed and staggered, creating an appealing effect. The grid dimensions and index determine the animation order.

onClick

  return (
    <>
      <div
        className="absolute flex flex-col w-[94.2vw] h-[82.5vh] sm:h-[93.5vh] m-auto left-0 right-0 top-0 bottom-0 z-[2] sm:flex-row sm:p-9"
        onClick={handleClick}
      >
        <Navbar author={author} />
        <div className="h-40 w-full sm:hidden"></div>
        <div className="flex h-full w-full px-5 pb-5 sm:p-0">
          {children}
        </div>
      </div>
      <Background setGrid={setGrid} />
    </>
  )

The component renders JSX elements for the page. It includes a container div with specific dimensions and styling, an onClick event handler for user clicks. Additional content is displayed using the "children" prop, and a separate "Background" component is rendered.

In this blog post, we explored a code snippet showcasing the use of React and Anime.js to build interactive web pages. The component effectively combines user interaction, dynamic animation, and flexible layout design. By leveraging React's component-based structure and Anime.js's animation capabilities, developers can create captivating and engaging web experiences with ease.