3Dドットグローブ

canvasに球面分布した点を透視投影で回転描画する地球儀風アニメーション。ドラッグ操作対応で、データ可視化やテック系の背景に向きます。

#javascript#canvas#3d#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk:グローバル稼働を表すドットグローブのヒーロー -->
<section class="fd-globe" aria-label="FlowDesk グローバルインフラ">
  <div class="fd-globe__copy">
    <span class="fd-globe__brand">▰ FlowDesk Cloud</span>
    <h1 class="fd-globe__title">世界中の<br>チームと、同時に。</h1>
    <p class="fd-globe__lead">12リージョンの分散インフラで、99.99%の稼働率を実現。</p>
    <div class="fd-globe__stats">
      <div><b>99.99%</b><span>稼働率</span></div>
      <div><b>12</b><span>リージョン</span></div>
      <div><b>48ms</b><span>平均応答</span></div>
    </div>
    <button class="fd-globe__btn" type="button">無料で試す</button>
  </div>

  <div class="fd-globe__stage">
    <!-- 球面ドットを透視投影で回転描画。ドラッグ対応 -->
    <canvas id="fdGlobe" width="300" height="300" aria-label="回転するドット地球儀"></canvas>
    <span class="fd-globe__hint">ドラッグで回転</span>
  </div>
</section>
CSS
/* FlowDesk:ドットグローブのSaaSヒーロー */
:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
  --white: #ffffff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  align-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background:
    radial-gradient(circle at 75% 30%, #16264a 0%, var(--navy) 70%);
  color: var(--white);
  overflow: hidden;
}

/* 左:コピー */
.fd-globe__copy { padding: 0 12px 0 30px; }
.fd-globe__brand { font-size: 12px; font-weight: 800; letter-spacing: 0.1em; color: var(--blue); }
.fd-globe__title { margin: 10px 0 8px; font-size: 28px; font-weight: 900; line-height: 1.25; }
.fd-globe__lead { margin: 0 0 14px; font-size: 12.5px; line-height: 1.7; color: rgba(255,255,255,.72); }

.fd-globe__stats { display: flex; gap: 10px; margin-bottom: 16px; }
.fd-globe__stats > div {
  text-align: center;
  padding: 7px 10px;
  border-radius: 10px;
  background: rgba(79,124,255,.14);
  border: 1px solid rgba(79,124,255,.28);
}
.fd-globe__stats b { display: block; font-size: 16px; color: #8fb0ff; }
.fd-globe__stats span { font-size: 10px; color: rgba(255,255,255,.6); }

.fd-globe__btn {
  font: inherit; font-size: 13px; font-weight: 700; color: #fff;
  background: linear-gradient(135deg, #6e93ff, var(--blue));
  border: none; padding: 11px 24px; border-radius: 999px; cursor: pointer;
  box-shadow: 0 8px 20px rgba(79,124,255,.45);
  transition: transform 0.2s ease;
}
.fd-globe__btn:hover { transform: translateY(-2px); }

/* 右:canvasステージ */
.fd-globe__stage {
  position: relative;
  height: 100%;
  display: grid;
  place-items: center;
}
#fdGlobe {
  width: 300px; height: 300px;
  max-width: 100%;
  cursor: grab;
  touch-action: none;
  filter: drop-shadow(0 10px 30px rgba(79,124,255,.35));
}
#fdGlobe:active { cursor: grabbing; }
.fd-globe__hint {
  position: absolute; bottom: 16px;
  font-size: 11px; color: rgba(255,255,255,.45);
}
JavaScript
// FlowDesk ドットグローブ:球面の点を透視投影で回転描画。ドラッグ操作対応
(() => {
  const canvas = document.getElementById("fdGlobe");
  if (!canvas || !canvas.getContext) return; // null安全
  const ctx = canvas.getContext("2d");
  if (!ctx) return;

  const W = canvas.width, H = canvas.height;
  const cx = W / 2, cy = H / 2;
  const R = 120;          // 球の半径
  const N = 480;          // 点の数
  const FOCAL = 360;      // 透視投影の焦点距離

  // フィボナッチ球で点を均等配置
  const points = [];
  const golden = Math.PI * (3 - Math.sqrt(5));
  for (let i = 0; i < N; i++) {
    const y = 1 - (i / (N - 1)) * 2;     // -1〜1
    const r = Math.sqrt(1 - y * y);
    const theta = golden * i;
    points.push({ x: Math.cos(theta) * r, y, z: Math.sin(theta) * r });
  }

  // データセンターを示す強調ノード(緑の点)をいくつか選ぶ
  const hubs = new Set([20, 80, 140, 200, 260, 320, 380, 440]);

  let rotY = 0, rotX = 0.3;
  let auto = 0.004;
  let dragging = false, lastX = 0, lastY = 0;
  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  const onDown = (e) => {
    dragging = true; auto = 0;
    lastX = e.clientX; lastY = e.clientY;
    canvas.setPointerCapture?.(e.pointerId);
  };
  const onMove = (e) => {
    if (!dragging) return;
    rotY += (e.clientX - lastX) * 0.008;
    rotX += (e.clientY - lastY) * 0.008;
    lastX = e.clientX; lastY = e.clientY;
  };
  const onUp = () => {
    if (!dragging) return;
    dragging = false;
    setTimeout(() => { if (!dragging && !reduce) auto = 0.004; }, 1200);
  };

  canvas.addEventListener("pointerdown", onDown);
  window.addEventListener("pointermove", onMove);
  window.addEventListener("pointerup", onUp);

  const draw = () => {
    if (!reduce) rotY += auto;
    ctx.clearRect(0, 0, W, H);

    const sx = Math.sin(rotX), cxr = Math.cos(rotX);
    const sy = Math.sin(rotY), cyr = Math.cos(rotY);

    // 回転+透視投影し、奥から描画
    const proj = points.map((p, i) => {
      let x = p.x * cyr - p.z * sy;
      let z = p.x * sy + p.z * cyr;
      let y = p.y * cxr - z * sx;
      z = p.y * sx + z * cxr;
      const scale = FOCAL / (FOCAL + z * R);
      return { px: cx + x * R * scale, py: cy + y * R * scale, depth: z, hub: hubs.has(i) };
    }).sort((a, b) => a.depth - b.depth);

    for (const q of proj) {
      const t = (q.depth + 1) / 2;            // 0(奥)〜1(手前)
      const alpha = 0.2 + t * 0.8;
      ctx.beginPath();
      if (q.hub) {
        // データセンター:緑で大きめに
        const rad = 2.4 + t * 2.4;
        ctx.fillStyle = `hsla(150, 90%, ${55 + t * 12}%, ${alpha})`;
        ctx.arc(q.px, q.py, rad, 0, Math.PI * 2);
        ctx.fill();
        // 淡いハロー
        ctx.beginPath();
        ctx.fillStyle = `hsla(150, 90%, 60%, ${alpha * 0.18})`;
        ctx.arc(q.px, q.py, rad + 4, 0, Math.PI * 2);
        ctx.fill();
      } else {
        const rad = 1 + t * 1.6;
        const hue = 215 + t * 20;             // 紺〜青
        ctx.fillStyle = `hsla(${hue}, 90%, ${58 + t * 14}%, ${alpha})`;
        ctx.arc(q.px, q.py, rad, 0, Math.PI * 2);
        ctx.fill();
      }
    }
    requestAnimationFrame(draw);
  };

  draw();
})();

コード

HTML
<div class="globe-wrap" aria-label="3Dドットグローブのデモ">
  <!-- canvasに点を球面配置して回転投影 -->
  <canvas id="globe" width="320" height="320" role="img" aria-label="回転する点の球体"></canvas>
  <p class="globe-hint">ドラッグで回転</p>
</div>
CSS
/* ===== 3Dドットグローブ(canvas) ===== */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background:
    radial-gradient(circle at 50% 45%, #0b2238 0%, #05101d 55%, #02060c 100%);
  overflow: hidden;
  user-select: none;
}

.globe-wrap { display: grid; place-items: center; gap: 10px; }

#globe {
  width: 260px;
  height: 260px;
  cursor: grab;
  filter: drop-shadow(0 0 30px rgba(80,180,255,.25));
  touch-action: none;
}
#globe:active { cursor: grabbing; }

.globe-hint {
  margin: 0;
  font-size: 12px; letter-spacing: .2em;
  color: rgba(140,200,255,.6);
}
JavaScript
// 3Dドットグローブ: 球面に点を分布させ、回転・透視投影してcanvasに描画
(() => {
  const canvas = document.getElementById('globe');
  if (!canvas || !canvas.getContext) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

  const W = canvas.width, H = canvas.height;
  const cx = W / 2, cy = H / 2;
  const R = 120;          // 球の半径
  const N = 520;          // 点の数
  const FOCAL = 360;      // 透視投影の焦点距離

  // フィボナッチ球で点を均等配置
  const points = [];
  const golden = Math.PI * (3 - Math.sqrt(5));
  for (let i = 0; i < N; i++) {
    const y = 1 - (i / (N - 1)) * 2;     // -1〜1
    const r = Math.sqrt(1 - y * y);
    const theta = golden * i;
    points.push({ x: Math.cos(theta) * r, y, z: Math.sin(theta) * r });
  }

  let rotY = 0, rotX = 0.35;
  let auto = 0.004;
  let dragging = false, lastX = 0, lastY = 0;
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  const onDown = (e) => {
    dragging = true; auto = 0;
    lastX = e.clientX; lastY = e.clientY;
    canvas.setPointerCapture?.(e.pointerId);
  };
  const onMove = (e) => {
    if (!dragging) return;
    rotY += (e.clientX - lastX) * 0.008;
    rotX += (e.clientY - lastY) * 0.008;
    lastX = e.clientX; lastY = e.clientY;
  };
  const onUp = () => {
    if (!dragging) return;
    dragging = false;
    setTimeout(() => { if (!dragging && !reduce) auto = 0.004; }, 1200);
  };

  canvas.addEventListener('pointerdown', onDown);
  window.addEventListener('pointermove', onMove);
  window.addEventListener('pointerup', onUp);

  const draw = () => {
    if (!reduce) rotY += auto;
    ctx.clearRect(0, 0, W, H);

    const sx = Math.sin(rotX), cxr = Math.cos(rotX);
    const sy = Math.sin(rotY), cyr = Math.cos(rotY);

    // zでソートして奥の点から描く(簡易的に配列を再利用)
    const proj = points.map((p) => {
      // Y軸回転 → X軸回転
      let x = p.x * cyr - p.z * sy;
      let z = p.x * sy + p.z * cyr;
      let y = p.y * cxr - z * sx;
      z = p.y * sx + z * cxr;
      const scale = FOCAL / (FOCAL + z * R);
      return {
        px: cx + x * R * scale,
        py: cy + y * R * scale,
        depth: z,        // -1(奥)〜1(手前)
        scale
      };
    }).sort((a, b) => a.depth - b.depth);

    for (const q of proj) {
      const t = (q.depth + 1) / 2;              // 0〜1
      const rad = 1.1 + t * 2.0;                // 手前ほど大きく
      const alpha = 0.25 + t * 0.75;            // 手前ほど濃く
      const hue = 190 + t * 40;                 // 青→水色
      ctx.beginPath();
      ctx.fillStyle = `hsla(${hue}, 95%, ${55 + t * 15}%, ${alpha})`;
      ctx.arc(q.px, q.py, rad, 0, Math.PI * 2);
      ctx.fill();
    }

    requestAnimationFrame(draw);
  };

  draw();
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「3Dドットグローブ」の効果を追加してください。

# 追加してほしい効果
3Dドットグローブ(3D & パースペクティブ)
canvasに球面分布した点を透視投影で回転描画する地球儀風アニメーション。ドラッグ操作対応で、データ可視化やテック系の背景に向きます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="globe-wrap" aria-label="3Dドットグローブのデモ">
  <!-- canvasに点を球面配置して回転投影 -->
  <canvas id="globe" width="320" height="320" role="img" aria-label="回転する点の球体"></canvas>
  <p class="globe-hint">ドラッグで回転</p>
</div>

【CSS】
/* ===== 3Dドットグローブ(canvas) ===== */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background:
    radial-gradient(circle at 50% 45%, #0b2238 0%, #05101d 55%, #02060c 100%);
  overflow: hidden;
  user-select: none;
}

.globe-wrap { display: grid; place-items: center; gap: 10px; }

#globe {
  width: 260px;
  height: 260px;
  cursor: grab;
  filter: drop-shadow(0 0 30px rgba(80,180,255,.25));
  touch-action: none;
}
#globe:active { cursor: grabbing; }

.globe-hint {
  margin: 0;
  font-size: 12px; letter-spacing: .2em;
  color: rgba(140,200,255,.6);
}

【JavaScript】
// 3Dドットグローブ: 球面に点を分布させ、回転・透視投影してcanvasに描画
(() => {
  const canvas = document.getElementById('globe');
  if (!canvas || !canvas.getContext) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

  const W = canvas.width, H = canvas.height;
  const cx = W / 2, cy = H / 2;
  const R = 120;          // 球の半径
  const N = 520;          // 点の数
  const FOCAL = 360;      // 透視投影の焦点距離

  // フィボナッチ球で点を均等配置
  const points = [];
  const golden = Math.PI * (3 - Math.sqrt(5));
  for (let i = 0; i < N; i++) {
    const y = 1 - (i / (N - 1)) * 2;     // -1〜1
    const r = Math.sqrt(1 - y * y);
    const theta = golden * i;
    points.push({ x: Math.cos(theta) * r, y, z: Math.sin(theta) * r });
  }

  let rotY = 0, rotX = 0.35;
  let auto = 0.004;
  let dragging = false, lastX = 0, lastY = 0;
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  const onDown = (e) => {
    dragging = true; auto = 0;
    lastX = e.clientX; lastY = e.clientY;
    canvas.setPointerCapture?.(e.pointerId);
  };
  const onMove = (e) => {
    if (!dragging) return;
    rotY += (e.clientX - lastX) * 0.008;
    rotX += (e.clientY - lastY) * 0.008;
    lastX = e.clientX; lastY = e.clientY;
  };
  const onUp = () => {
    if (!dragging) return;
    dragging = false;
    setTimeout(() => { if (!dragging && !reduce) auto = 0.004; }, 1200);
  };

  canvas.addEventListener('pointerdown', onDown);
  window.addEventListener('pointermove', onMove);
  window.addEventListener('pointerup', onUp);

  const draw = () => {
    if (!reduce) rotY += auto;
    ctx.clearRect(0, 0, W, H);

    const sx = Math.sin(rotX), cxr = Math.cos(rotX);
    const sy = Math.sin(rotY), cyr = Math.cos(rotY);

    // zでソートして奥の点から描く(簡易的に配列を再利用)
    const proj = points.map((p) => {
      // Y軸回転 → X軸回転
      let x = p.x * cyr - p.z * sy;
      let z = p.x * sy + p.z * cyr;
      let y = p.y * cxr - z * sx;
      z = p.y * sx + z * cxr;
      const scale = FOCAL / (FOCAL + z * R);
      return {
        px: cx + x * R * scale,
        py: cy + y * R * scale,
        depth: z,        // -1(奥)〜1(手前)
        scale
      };
    }).sort((a, b) => a.depth - b.depth);

    for (const q of proj) {
      const t = (q.depth + 1) / 2;              // 0〜1
      const rad = 1.1 + t * 2.0;                // 手前ほど大きく
      const alpha = 0.25 + t * 0.75;            // 手前ほど濃く
      const hue = 190 + t * 40;                 // 青→水色
      ctx.beginPath();
      ctx.fillStyle = `hsla(${hue}, 95%, ${55 + t * 15}%, ${alpha})`;
      ctx.arc(q.px, q.py, rad, 0, Math.PI * 2);
      ctx.fill();
    }

    requestAnimationFrame(draw);
  };

  draw();
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。