HTMLのテキストやボタンと連動!PixiJSでWebサイトの背景をすいすい動かす

Webサイトの背景にCanvasアニメーションを敷く際、ただ背景がループ再生されているだけでは「単なる動画」と変わりません。

この記事では、前面にあるHTMLのテキストやボタンに対する操作(マウスカーソルの移動やボタンへのホバー)に応じて、背景のPixiJSグラフィックが追従・反応する組み込み手法を解説します。

全体構造とレイヤー分離

基本的なアプローチは以下の通りです。

  1. 背景Canvas
    CSSで position: fixed; z-index: -1; を指定し、画面全体に広げます。CSSで pointer-events: none; を付与し、通常のクリックやスクロールを邪魔しないようにします。
  2. 前面HTML
    通常通りテキストやボタンをレイアウトします。
  3. イベント連携
    JS側で windowmousemove や、HTML要素の mouseenter / mouseleave を監視し、その座標や状態フラグをPixiJSの描画ループ(Ticker)へ渡します。

実装コード(Vanilla JS & Pixi.js v8)

以下のコードをそのままコピペしてブラウザで開くと動作を確認できます。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>PixiJS Interlocking Background</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>

  <!-- 背面Canvas用コンテナ -->
  <div id="pixi-bg"></div>

  <!-- 前面HTMLコンテンツ -->
  <main class="content">
    <h1>PixiJS × DOM Interaction</h1>
    <p>マウスカーソルを動かしたり、下のボタンにホバーすると背景の粒子が反応します。</p>
    <button id="target-btn" class="interactive-btn">ホバーで引き寄せる</button>
  </main>

  <!-- Pixi.js v8 CDN -->
  <script src="https://pixijs.download/v8.0.0/pixi.min.js"></script>
  <script src="main.js"></script>
</body>
</html>

style.css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: sans-serif;
  color: #ffffff;
  background-color: #0d0e15;
  min-height: 100vh;
  overflow-x: hidden;
}

/* 最背面Canvas */
#pixi-bg {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: -1;
  pointer-events: none; /* 前面要素の操作を阻害しない */
  overflow: hidden;
}

/* 前面コンテンツ */
.content {
  position: relative;
  z-index: 1;
  max-width: 600px;
  margin: 0 auto;
  padding: 120px 20px;
  text-align: center;
}

h1 {
  font-size: 2.5rem;
  margin-bottom: 1rem;
}

p {
  color: #a0a5c0;
  line-height: 1.6;
  margin-bottom: 2rem;
}

.interactive-btn {
  padding: 14px 28px;
  font-size: 1rem;
  font-weight: bold;
  color: #0d0e15;
  background-color: #00ffcc;
  border: none;
  border-radius: 30px;
  cursor: pointer;
  transition: transform 0.2s, background-color 0.2s;
}

.interactive-btn:hover {
  transform: scale(1.05);
  background-color: #33ffdd;
}

main.js

(async () => {
  const container = document.getElementById('pixi-bg');
  const targetBtn = document.getElementById('target-btn');

  // 1. Pixi.js Application の初期化
  const app = new PIXI.Application();
  await app.init({
    resizeTo: window,
    backgroundAlpha: 0,
    resolution: window.devicePixelRatio || 1,
    autoDensity: true,
  });

  container.appendChild(app.canvas);

  // 2. 状態管理用オブジェクト
  const pointer = {
    x: app.screen.width / 2,
    y: app.screen.height / 2,
    targetX: app.screen.width / 2,
    targetY: app.screen.height / 2,
    isHovered: false, // ボタンホバー状態フラグ
  };

  // 3. パーティクルの生成
  const particleCount = 40;
  const particles = [];
  const graphics = new PIXI.Graphics();
  app.stage.addChild(graphics);

  for (let i = 0; i < particleCount; i++) {
    particles.push({
      x: Math.random() * app.screen.width,
      y: Math.random() * app.screen.height,
      vx: (Math.random() - 0.5) * 1.5,
      vy: (Math.random() - 0.5) * 1.5,
      radius: Math.random() * 4 + 2,
      baseAlpha: Math.random() * 0.4 + 0.2,
    });
  }

  // 4. イベント監視
  // (A) マウス位置の追従
  window.addEventListener('mousemove', (e) => {
    if (!pointer.isHovered) {
      pointer.targetX = e.clientX;
      pointer.targetY = e.clientY;
    }
  });

  // (B) ボタンへのホバー検知
  targetBtn.addEventListener('mouseenter', () => {
    pointer.isHovered = true;
    const rect = targetBtn.getBoundingClientRect();
    // ボタンの中心座標をターゲットに設定
    pointer.targetX = rect.left + rect.width / 2;
    pointer.targetY = rect.top + rect.height / 2;
  });

  targetBtn.addEventListener('mouseleave', () => {
    pointer.isHovered = false;
  });

  // 5. 描画ループ (Ticker)
  app.ticker.add((ticker) => {
    // ポインター位置の遅延補間(イージング)
    pointer.x += (pointer.targetX - pointer.x) * 0.08;
    pointer.y += (pointer.targetY - pointer.y) * 0.08;

    graphics.clear();

    particles.forEach((p) => {
      // 位置の更新
      p.x += p.vx;
      p.y += p.vy;

      // 画面端の折り返し
      if (p.x < 0) p.x = app.screen.width;
      if (p.x > app.screen.width) p.x = 0;
      if (p.y < 0) p.y = app.screen.height;
      if (p.y > app.screen.height) p.y = 0;

      // ポインター(またはボタン位置)との距離を計算
      const dx = pointer.x - p.x;
      const dy = pointer.y - p.y;
      const dist = Math.sqrt(dx * dx + dy * dy);

      // ボタンホバー時は強力に吸い寄せ、通常時は近くの粒子と線を繋ぐ
      if (pointer.isHovered && dist < 180) {
        p.x += dx * 0.012;
        p.y += dy * 0.012;
      }

      // 描画:一定距離内の粒子とポインター間に引く線
      if (dist < 150) {
        const lineAlpha = (1 - dist / 150) * (pointer.isHovered ? 0.8 : 0.3);
        graphics.moveTo(p.x, p.y);
        graphics.lineTo(pointer.x, pointer.y);
        graphics.stroke({ width: 1, color: 0x00ffcc, alpha: lineAlpha });
      }

      // 粒子の描画
      graphics.circle(p.x, p.y, p.radius);
      graphics.fill({ color: 0x00ffcc, alpha: p.baseAlpha });
    });
  });
})();

PixiJSの主要記述

今回のコードで使用しているPixiJS(v8)固有のオブジェクトやAPIの役割一覧です。

初期化・キャンバス

  • new PIXI.Application()
    PixiJSの全体を管理するメインアプリケーションを作成します。
  • app.init({ ... })
    レンダーサイズや解像度、背景透過を設定して非同期で初期化します。
  • app.canvas
    生成された <canvas> DOM要素を参照します。
  • app.screen
    描画領域の幅や高さを取得します(app.screen.width / height)。

画面構造

  • new PIXI.Graphics()
    円や線などのベクター図形を描画するためのオブジェクトを作成します。
  • app.stage.addChild(...)
    画面の描画ツリー(ステージ)に図形オブジェクトを追加して表示対象にします。

アニメーション

  • app.ticker.add((ticker) => { ... })
    毎フレーム(約60fps)呼び出される描画ループ処理を登録します。

図形・線の描画

  • graphics.clear()
    前のフレームで描画した図形パスをクリアし、再描画の準備をします。
  • graphics.circle(x, y, r)
    指定した座標と半径で円のパスを作成します。
  • graphics.fill({ color, alpha })
    作成したパスを指定した色と透明度で塗りつぶします。
  • graphics.moveTo(x, y) / lineTo(x, y)
    直線を引くための開始点と終了点を設定します。
  • graphics.stroke({ width, color, alpha })
    設定した線のパスに対して輪郭線を描画します。

実装のポイント解説

① 座標のイージング処理(すいすい動く理由)

マウス移動イベント(mousemove)の座標に直接パーティクルを合わせると、動きが硬く機械的な印象になります。

// 目標座標(targetX/Y)に向けて、毎フレーム8%ずつ近づける
pointer.x += (pointer.targetX - pointer.x) * 0.08;
pointer.y += (pointer.targetY - pointer.y) * 0.08;

この補間計算を1行挟むことで、カーソルの動きに対して遅れて滑らかについてくるような質感が生まれます。

② HTMLの要素位置を取得する

ボタンにホバーした際、ボタンの画面上の絶対座標を getBoundingClientRect() で取得します。

const rect = targetBtn.getBoundingClientRect();
pointer.targetX = rect.left + rect.width / 2;
pointer.targetY = rect.top + rect.height / 2;

取得した中心座標に引き寄せるフラグ(pointer.isHovered)を立て、Ticker内で粒子を移動させることで、ボタンと背景アニメーションが物理的に連動しているような見た目を作ることができます。

まとめ

  1. pointer-events: none でイベント干渉を防ぐ
  2. DOMイベント(マウス・ホバー)から座標と状態を取得する
  3. PixiJSのTicker内でイージング補間を行い、スムーズに描画に反映する

このパターンを押さえておけば、テキスト入力欄のフォーカスやスクロール位置に応じた背景の変化など、さまざまなHTML操作と背景演出を無理なく連動させることができます。