動きのあるWebサイトはこう作る|PixiJS・GSAP・Locomotive Scrollの役割と実装

背景のモチーフがふわふわと漂い、スクロールが滑らかに追従する。そうした「動きのあるWebサイト」は、CSSアニメーションだけで作られているわけではありません。多くの場合、描画・アニメーション・スクロールをそれぞれ専門のライブラリに任せ、組み合わせて実現しています。この記事では、実際に公開されているサイトの構成を読み解きながら、PixiJS・GSAP・Locomotive Scrollがそれぞれ何を担当しているのか、そして最も再現しやすい「漂う背景」をどう実装するのかを、動作検証済みのコードとあわせて解説します。

実例で使われていた4つの技術

lines of HTML codes
Photo by Florian Olivo on Unsplash

今回参考にしたのは、クラウド歯科・矯正歯科のサイトです。公開されているJavaScriptを確認したところ、次の4つが組み合わされていました。役割がきれいに分かれている点が参考になります。

ライブラリ担当している役割CSSだけでは難しい理由
PixiJS 6.4.2背景に漂うモチーフをWebGLで描画要素数が増えるとDOMでは描画負荷が上がる
GSAP 3.10.4アニメーション全般の時間制御ランダム値や複雑な順序制御が書きにくい
ScrollTrigger 3.10.4スクロール位置と動きの連動スクロール量に応じた進行度の管理が煩雑
Locomotive Scrollスムーススクロール慣性のあるスクロールは標準機能にない

なお、このサイトはWordPressではなく、ビルド済みのJavaScriptを配信する静的サイトとして構築されていました。動きを重視するサイトでは、CMSの制約を受けにくいこの構成が選ばれることがよくあります。

背景をWebGLで漂わせる(PixiJS)

この構成の中心はPixiJSです。PixiJSは、WebGLを使ってブラウザ上に高速に絵を描くための描画ライブラリで、今回のサイトでは画面全体を覆うcanvasを敷き、その上に雲のかたちのSVGを何枚も浮かべる用途で使われていました。

読み込まれていたのはbg1.svgからbg3.svgの3種類だけで、いずれも0.2〜0.5KBという軽さです。この3種類を拡大・縮小し、位置と速度を変えて何枚も配置することで、画面の情報量を増やしています。画像を何十枚も用意するのではなく、少ない素材を動きで使い回すという考え方です。

実際の初期化では、resizeTo: windowで画面サイズに追従させ、resolution: 2autoDensity: trueで高解像度ディスプレイに対応し、背景色は0xF0F0F0という淡いグレーが指定されていました。次のコードは同じ考え方を、現行のPixiJS 8で書き直したものです。ブラウザで実際に動かして確認しています。

<script src="https://cdn.jsdelivr.net/npm/pixi.js@8/dist/pixi.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>

<script>
(async () => {
  const app = new PIXI.Application();
  await app.init({
    resizeTo: window,      // 画面サイズに追従
    antialias: true,
    backgroundColor: 0xf0f0f0,
    resolution: 2,         // 高解像度ディスプレイ対応
    autoDensity: true,
  });
  document.getElementById('bg').appendChild(app.canvas);

  const texture = await PIXI.Assets.load('/assets/img/cloud.svg');
  const layer = new PIXI.Container();
  app.stage.addChild(layer);

  const parts = [];
  for (let i = 0; i < 10; i++) {
    const sp = new PIXI.Sprite(texture);
    sp.anchor.set(0.5);
    sp.x = (i % 5) * 260 + 150;
    sp.y = Math.floor(i / 5) * 300 + 180;
    sp.scale.set(0.6 + (i % 3) * 0.25);   // 大きさをばらけさせる
    layer.addChild(sp);
    parts.push(sp);

    // 1枚ずつ違う速さでゆっくり傾け続ける
    gsap.to(sp, {
      rotation: 'random(-0.4, 0.4)',
      duration: 'random(3, 5.5)',
      ease: 'sine.inOut',
      repeat: -1,
      yoyo: true,
    });
  }

  // マウス位置に合わせて全体をわずかにずらす(視差)
  const mouse = { x: 0, y: 0 };
  window.addEventListener('pointermove', (e) => {
    mouse.x = (e.clientX / window.innerWidth - 0.5) * 2;
    mouse.y = (e.clientY / window.innerHeight - 0.5) * 2;
  });

  app.ticker.add(() => {
    layer.x += ((-mouse.x * 40) - layer.x) * 0.05;   // 少しずつ近づけて滑らかに
    layer.y += ((-mouse.y * 25) - layer.y) * 0.05;
    parts.forEach((sp) => {
      sp.x -= 0.3;                                    // ゆっくり横に流す
      if (sp.x < -150) sp.x = window.innerWidth + 150; // 画面外に出たら反対側へ
    });
  });
})();
</script>

バージョンには注意が必要です。参考サイトが使っているPixiJS 6のPIXI.Loaderはバージョン7で廃止されており、現行の8ではPIXI.Assets.load()に置き換わっています。初期化もawait app.init()が必須になり、canvas要素の取得もapp.viewからapp.canvasに変わりました。古い記事のコードをそのまま貼っても動かないのは、この違いが原因です。

「自然な動き」をつくっているのはランダム値

漂う動きが機械的に見えないのは、動きの値をひとつずつばらけさせているからです。参考サイトでは、GSAPの文字列によるランダム指定が使われていました。

GSAPではrotation: "random(-0.4, 0.4)"のように書くと、対象ごとに範囲内の値がランダムに割り当てられます。duration: "random(3, 5.5)"と組み合わせれば、傾く角度も速さも1枚ずつ変わります。すべてが同じ周期で動くと人工的に見えますが、周期をずらすだけで一気に自然になります。ここが、CSSアニメーションで同じことをやろうとすると面倒になる部分です。

ローディング完了を起点に本編を動かす設計

a laptop computer sitting on top of a desk
Photo by Bernd Dittrich on Unsplash

参考サイトで最も参考になったのが、ローディング画面から本編への受け渡し方です。演出の開始点を1か所にまとめることで、バラバラに動き出す事故を防いでいます。

ローディング画面では、複数のSVGパーツを4つのグループに分け、それぞれをシャッフルしてから順に上へ飛ばしていました。1枚あたりの動く時間は2秒+0〜0.3秒のランダム、開始の遅れは並び順×0.045秒、移動量はtranslate3d(0, -100vh, 0)に回転を加えたものです。順番をシャッフルしている点がポイントで、DOMの並び順どおりに飛ばすと規則的すぎて機械的に見えてしまいます。

そして全パーツが飛び終わるタイミング(PCで1.65秒後、スマートフォンで1.3秒後)にshow-after-loadingというイベントを発火し、これを合図に背景のコンテナが画面下から上がってくる、という流れになっていました。ローディングの終了を各所で個別に判定するのではなく、1つのイベントに集約して全員がそれを待つ設計です。制作の現場では、この作りにしておくと後から演出を足すときに壊れにくくなります。

スムーススクロールとScrollTriggerをつなぐ

スムーススクロールを導入すると、GSAPのScrollTriggerがそのままでは正しく動きません。ブラウザ本来のスクロール位置と、ライブラリが管理している位置がずれるためです。

参考サイトでは、Locomotive ScrollのスクロールイベントでScrollTrigger.updateを呼び、ScrollTrigger.scrollerProxy()でスクロール位置の取得方法そのものをLocomotive側に差し替えていました。さらにScrollTrigger.addEventListener('refresh')でLocomotive側の再計算も走らせています。この3点セットは、スムーススクロールとScrollTriggerを併用するときの定番の書き方です。併用してアニメーションの発火位置がずれる場合は、まずここが実装されているかを確認してください。

実務で採用する前に確認したいこと

見栄えは良い構成ですが、案件に持ち込む前に判断しておきたい点が4つあります。

  1. 読み込むJavaScriptの量:この構成を採ると、ライブラリ一式だけで数百KB規模になります。動きを主役に据えるサイトでは妥当な投資ですが、表示速度を最優先するサイトでは重い選択になります。どちらを取るかを設計の段階で決めておくと、後から議論が戻りません。
  2. スマートフォンでの扱い:参考サイトでも、マウス視差はPCのみ有効にするなど、端末で処理を分けていました。常時描画は電池消費にも影響します。
  3. 動きを減らしたい利用者への配慮:OS側で視差効果を減らす設定をしている利用者に向けて、prefers-reduced-motionで動きを止める分岐を用意しておくと安全です。
  4. 更新の担当者:この構成はCMSからの更新と相性が良くありません。文章や写真を頻繁に差し替えるサイトなら、動かす範囲をトップページだけに限定するといった線引きが必要です。

サイト制作の観点では、全ページに動きを入れるより、トップページのファーストビューだけに集中投資するほうが費用対効果は高くなります。参考サイトも、凝った描画は背景とローディングに絞り、下層の情報はカードとモーダルで淡々と見せる構成になっていました。

よくある質問

Q. 背景の動きはCSSアニメーションでは作れませんか?
A. 数枚であればCSSでも作れます。ただし十枚以上を別々の速さで動かし、マウスやスクロールにも連動させる場合は、DOM要素として動かすより描画専用のcanvasにまとめたほうが動作が安定します。判断の目安は「同時に動かす要素が10個を超えるかどうか」です。

Q. PixiJSは無料で使えますか?
A. PixiJSはMITライセンスで公開されており、商用利用も可能です。一方でGSAPは、一部の有料プラグイン(ScrollSmootherなど)に条件があるため、使用するプラグインごとにライセンスを確認してください。ScrollTriggerは無料で利用できます。

Q. WordPressのサイトでも同じことはできますか?
A. できます。テーマにライブラリを読み込み、トップページ用のテンプレートでcanvasを設置すれば同じ実装が可能です。ただし他のプラグインが読み込むJavaScriptと競合しやすいため、必要なページだけで読み込むよう条件分岐させることをおすすめします。

Q. こうした動きのあるサイトは制作費が高くなりますか?
A. 通常のコーポレートサイトより費用はかかります。動きの設計・実装・端末ごとの調整が加わるためです。予算を抑えたい場合は、動かす範囲をファーストビューに限定する方法が現実的です。

まとめ

動きのあるサイトは、ひとつの魔法で作られているわけではなく、描画はPixiJS、時間制御はGSAP、スクロール連動はScrollTrigger、スクロールの質感はLocomotive Scrollというように、役割ごとに道具を分けて組み立てられています。まず取り入れやすいのは、少ない素材をランダムな速さで動かす背景です。値をひとつずつばらけさせるだけで、印象は大きく変わります。THREE D PLUSでは、こうした動きのあるサイトの設計・実装から、既存サイトへの部分的な導入までご相談を承っています。お気軽にお問い合わせください。

最新情報をチェックしよう!

LET’S CONTACT

お問い合わせ

デザインと発信の力で、お客様の強みや想いを形にし、
成果につながるコンテンツを制作します。
「うちの場合はどうなる?」という段階からでも、どうぞお気軽にご相談ください。