これは、農工大2025アドベントカレンダーの記事です。 animejsをreactで頑張った話です。

はじめに

去年からWeb開発を始め、アプリやサイトを作成してきました。主にNext.jsを使っており、技術選定の幅を広げたいと思いつつも、慣れた技術に頼りがちです。

最近では、見た目にもこだわりたいという思いが強くなり、アニメーションに挑戦することにしました。アニメーションを使った動きのあるサイトはとてもかっこいいので、私もそんなサイトを作りたいと思っています。

そこで、友人から教えてもらった「anime.js」というライブラリを使ってみることにしました。この記事では、初めてのアニメーション制作の挑戦を通じて学んだことを共有します。


anime.jsとは

anime.jsは、アニメーションを簡単に追加できるJavaScriptライブラリです。 公式サイトには多くのサンプルやドキュメントがあり、初心者でも取り組みやすかったです。

公式ドキュメントにはVanilla JSでのサンプルコードが多く掲載されていますが、私は普段Next.jsとReactを使っているため、React向けにコードを調整する必要がありました。

Reactでanime.jsを使う

幸い、公式ドキュメントにはReact用のサンプルコードも1つだけ掲載されていました。 これを参考に、React環境でanime.jsを使う方法を学びました。

"use client";

import { animate } from 'animejs';
import { useEffect, useRef } from 'react';

export default function Example() {
  const demoRef = useRef<HTMLDivErement | null>(null);

  useEffect(() => {
    if (!demoRef.current) return;

    const squares = demoRef.current.querySelectorAll(".square");
    animate(squares, { x: "23rem" });
  }, []);

return (
  <div
    id="selector-demo"
    ref={demoRef}
    className="flex flex-col gap-4 p-8 bg-slate-900 min-h-screen"
  >
      <div className="medium row flex justify-center">
        <div className="square h-16 w-16 rounded-xl bg-pink-500" />
      </div>
      <div className="medium row flex justify-center">
        <div className="square h-16 w-16 rounded-xl bg-pink-500" />
      </div>
      <div className="medium row flex justify-center">
        <div className="square h-16 w-16 rounded-xl bg-pink-500" />
      </div>
  </div>
);
}

このコードでは、以下の手順でアニメーションを実現しています:

  1. HTML要素を参照するための変数をuseRefで用意。
  2. anime.jsのanimate関数でアニメーションを適用。

Next.jsではデフォルトでSSR(サーバーサイドレンダリング)が有効なため、"use client"を指定する必要があります。 また、useRefで取得した参照がサーバー側でnullになる問題を回避するため、適切な条件分岐を追加しています。


スクロールアニメーション

次に挑戦したのは、スクロール位置に応じてアニメーションを実行する機能です。 公式ドキュメントのonScrollを参考に、以下のコードを作成しました。

"use client";

import { onScroll, animate } from 'animejs';
import { useEffect, useRef } from "react";

export default function ScrollTest() {
  const ref = useRef<HTMLDivElement | null>(null);;
  const container = useRef<HTMLDivElement | null>(null);;

  useEffect(() => {
    if (!ref.current) return;
    if (!container.current) return;
    
    animate(ref.current, { 
      x: "23rem",
      autoplay: onScroll({
        container: container.current,
        enter: "55% 0%",
        leave: "45% 100%",
        sync: true,
        debug: true,
      }),
    });
  }, []);

  return (
    <div ref={container} className='h-screen overflow-auto'>
        <div className="container min-h-screen bg-slate-950 text-slate-100 overflow-x-hidden">
          <section className="flex h-screen items-center justify-center">
            <p className="text-sm uppercase tracking-[0.4em] text-slate-500">
              scroll down
            </p>
          </section>
          
          <div ref={ref}>
            <h2 className="h-24 block bg-amber-300">hello scroll</h2>
          </div>
    
          <section className="flex h-screen items-center justify-center">
            <p className="text-sm uppercase tracking-[0.4em] text-slate-500">
              scroll up
            </p>
          </section>
        </div>
    </div>
  );
}

このコードでは、onScrollを使ってスクロール位置に応じたアニメーションを実現しています。 enterleaveの値を調整することで、アニメーションが発生する位置を細かく設定できます。


タイムラインアニメーション

最後に挑戦したのは、タイムラインを使ったアニメーションです。 以下のコードでは、スクロールイベントに応じて複数の要素を連動して動かすアニメーションを実現しました。

"use client";

import { createTimeline, onScroll } from 'animejs';
import { useEffect, useRef } from "react";

export default function Testt() {
  const container = useRef<HTMLDivElement | null>(null);
  const target = useRef<HTMLDivElement | null>(null);

  const leftbox = useRef<HTMLDivElement | null>(null);
  const rightbox = useRef<HTMLDivElement | null>(null);



  useEffect(() => {
    if (!leftbox.current) return;
    if (!rightbox.current) return;
    if (!container.current) return;
    if (!target.current) return;

    const leftboxTimeline = createTimeline({
      autoplay: false,
    }).add(leftbox.current, { 
      width: "*= 0.8",
      height: "*= 0.8",
      zIndex: [2,0],
      translateX: target.current.offsetWidth - leftbox.current.offsetWidth * 0.8,
      translateY: target.current.offsetHeight - leftbox.current.offsetHeight * 0.8,
      backgroundColor: ['var(--theme-gold)',`var(--theme-alpha)`],
      duration: 500,
    });

    const rightboxTimeline = createTimeline({
      autoplay: false,
    }).add(rightbox.current, { 
      width: "*= 1.25",
      height: "*= 1.25",
      zIndex: [0,2],
      translateX: -(target.current.offsetWidth - rightbox.current.offsetWidth * 1.25),
      translateY: -(target.current.offsetHeight - rightbox.current.offsetHeight * 1.25),
      backgroundColor: ['var(--theme-alpha)',  'var(--theme-muted)'],
      duration: 500,
    });

    onScroll({
      container: container.current, 
      target: target.current,
      enter: "50% 50%",
      leave: "0% 50%",
      debug: true,
      onEnter: () => {
        leftboxTimeline.play();
        rightboxTimeline.play();
      },
      onLeaveBackward: () => {
        leftboxTimeline.reverse();
        rightboxTimeline.reverse();
      },
    });
  }, []);

  return (
    <div ref={container} className=' h-screen overflow-y-auto'>
    <div className="min-h-screen bg-slate-950 text-slate-100">
      <section className="flex h-screen items-center justify-center">
        <p className="text-sm uppercase tracking-[0.4em] text-slate-500">
          scroll down
        </p>
      </section>
      
      <div ref={target} className='relative w-[350px] mx-auto h-[250px]'>
        <div ref={leftbox} className="absolute   top-0 left-0 border border-theme-gold/30"
          style={{ width: "300px", height: "200px",top:0,left:0 }}
        >
          ああ
        </div>
        <div
          ref={rightbox}
          className="absolute bg-blue-300 bottom-0 right-0 border border-theme-gold/30"
          style={{ width: "240px", height: "160px" }}
        >
          いい
        </div>
      </div>
      
      <section className="flex h-screen items-center justify-center">
        <p className="text-sm uppercase tracking-[0.4em] text-slate-500">
          scroll up
        </p>
      </section>
    </div>
    
    </div>
  );
}

このコードでは、createTimelineを使って複数のアニメーションを連動させています。onScrollonEnteronLeaveBackwardを活用することで、スクロール位置に応じた再生や逆再生を実現しました。


おわりに

初めてのアニメーション制作は試行錯誤の連続でしたが、anime.jsを使うことで、比較的簡単に動きのあるWebサイトを作ることができました。この記事が、これからアニメーションに挑戦する方の参考になれば幸いです。