ワイヤーフレーム地形

平面メッシュの頂点を正弦波で変位させて波打たせるレトロな立体地形。シンセウェーブ調のビジュアルやダッシュボード背景に最適です。

外部ライブラリ: https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js

#webgl#threejs#wireframe#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

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

HTML
<!-- FlowDesk:波打つワイヤー地形を背景にしたダッシュボード概要 -->
<section class="fd-dash" aria-label="FlowDesk ダッシュボード">
  <!-- 背景に流れるワイヤーフレーム地形(データの起伏を象徴) -->
  <canvas id="scene" class="fd-dash__canvas" aria-hidden="true"></canvas>
  <div class="fd-dash__fallback" id="fd-fallback" hidden></div>

  <header class="fd-bar">
    <span class="fd-logo"><b>◆</b> FlowDesk</span>
    <span class="fd-bar__user">Analytics ・ 田中</span>
  </header>

  <div class="fd-dash__body">
    <div class="fd-headline">
      <span class="fd-kicker">REAL-TIME OVERVIEW</span>
      <h1 class="fd-title">今月のワークフロー稼働率</h1>
    </div>

    <div class="fd-cards">
      <div class="fd-card">
        <span class="fd-card__label">処理タスク</span>
        <strong class="fd-card__value">12,480</strong>
        <span class="fd-card__delta fd-up">▲ 8.2%</span>
      </div>
      <div class="fd-card">
        <span class="fd-card__label">平均処理時間</span>
        <strong class="fd-card__value">1.4<small>分</small></strong>
        <span class="fd-card__delta fd-up">▲ 12%</span>
      </div>
      <div class="fd-card">
        <span class="fd-card__label">エラー率</span>
        <strong class="fd-card__value">0.3<small>%</small></strong>
        <span class="fd-card__delta fd-down">▼ 0.1%</span>
      </div>
    </div>
  </div>
</section>
CSS
/* FlowDesk:紺地のダッシュボード+シンセウェーブ地形 */
:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
  --white: #ffffff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Segoe UI", system-ui, "Hiragino Kaku Gothic ProN", sans-serif;
  background: var(--navy);
}

.fd-dash {
  position: relative;
  width: 100%;
  height: 400px;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 110%, #18305e 0%, #0f1b34 55%, #0a1226 100%);
  color: var(--white);
}

/* 地形は画面下半分の背景として広がる */
.fd-dash__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* フォールバック:グリッド線の遠近背景 */
.fd-dash__fallback {
  position: absolute;
  inset: 50% 0 0 0;
  background:
    linear-gradient(transparent 0%, rgba(79, 124, 255, 0.25) 100%),
    repeating-linear-gradient(90deg, transparent, transparent 28px, rgba(79, 124, 255, 0.3) 29px),
    repeating-linear-gradient(0deg, transparent, transparent 22px, rgba(79, 124, 255, 0.25) 23px);
}

.fd-bar {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 15px 24px;
  border-bottom: 1px solid rgba(79, 124, 255, 0.16);
}
.fd-logo { font-size: 16px; font-weight: 700; letter-spacing: 0.04em; }
.fd-logo b { color: var(--blue); }
.fd-bar__user { font-size: 12px; color: rgba(255, 255, 255, 0.6); }

.fd-dash__body {
  position: relative;
  z-index: 2;
  padding: 22px 24px;
}
.fd-headline { margin-bottom: 20px; }
.fd-kicker {
  font-size: 10.5px;
  letter-spacing: 0.26em;
  color: var(--blue);
  font-weight: 700;
}
.fd-title {
  margin: 8px 0 0;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: 0.01em;
}

.fd-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  max-width: 560px;
}
.fd-card {
  padding: 14px 16px;
  border-radius: 14px;
  background: rgba(20, 36, 70, 0.66);
  border: 1px solid rgba(79, 124, 255, 0.22);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.fd-card__label {
  display: block;
  font-size: 11px;
  color: rgba(255, 255, 255, 0.6);
  margin-bottom: 8px;
}
.fd-card__value {
  display: block;
  font-size: 26px;
  font-weight: 700;
  line-height: 1;
}
.fd-card__value small { font-size: 13px; font-weight: 500; opacity: 0.7; }
.fd-card__delta {
  display: inline-block;
  margin-top: 8px;
  font-size: 11px;
  font-weight: 600;
}
.fd-up { color: #4fd99a; }
.fd-down { color: #ff7a90; }
JavaScript
// FlowDesk ダッシュボード背景:正弦波で波打つワイヤーフレーム地形(データの起伏を象徴)
(function () {
  "use strict";
  const canvas = document.getElementById("scene");
  const fallback = document.getElementById("fd-fallback");
  // Three.js未読込やcanvas不在なら安全にフォールバック表示
  if (!canvas || typeof THREE === "undefined") {
    if (fallback) fallback.hidden = false;
    return;
  }

  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  let renderer;
  try {
    renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  } catch (e) {
    if (fallback) fallback.hidden = false;
    return;
  }
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
  camera.position.set(0, 2.2, 6);
  camera.lookAt(0, 0, -4);

  // 平面メッシュをワイヤーフレームで奥に倒す
  const SEG = 48;
  const geometry = new THREE.PlaneGeometry(26, 26, SEG, SEG);
  geometry.rotateX(-Math.PI / 2);
  const material = new THREE.MeshBasicMaterial({
    color: 0x4f7cff,
    wireframe: true,
    transparent: true,
    opacity: 0.6,
  });
  const terrain = new THREE.Mesh(geometry, material);
  terrain.position.set(0, -1.6, -6);
  scene.add(terrain);

  // 元の頂点座標を保持(変位計算の基準)
  const base = geometry.attributes.position.array.slice();

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize);

  let raf = 0;
  let running = true;
  let t = 0;
  function animate() {
    if (!reduceMotion) t += 0.02;
    const pos = geometry.attributes.position;
    // x,zに応じた正弦波で高さyを変位させる
    for (let i = 0; i < pos.count; i++) {
      const x = base[i * 3];
      const z = base[i * 3 + 2];
      const y = Math.sin(x * 0.5 + t) * 0.6 + Math.cos(z * 0.4 + t * 0.8) * 0.5;
      pos.setY(i, y);
    }
    pos.needsUpdate = true;
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate();

  document.addEventListener("visibilitychange", () => {
    if (document.hidden) {
      if (running) { cancelAnimationFrame(raf); running = false; }
    } else if (!running) {
      running = true;
      raf = requestAnimationFrame(animate);
    }
  });

  window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();

コード

HTML
<!-- 波打つワイヤーフレーム地形。頂点を三角関数でアニメーション -->
<div class="stage">
  <canvas id="terrain" aria-label="波打つワイヤーフレーム地形"></canvas>
  <div class="caption">
    <span class="badge">Wireframe</span>
    <h2>Wireframe Terrain</h2>
    <p>頂点を時間で変位させるレトロな立体メッシュ</p>
  </div>
</div>
CSS
/* ネオンな配色 */
:root {
  --ink: #f3eaff;
  --accent: #ff5fa2;
}

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

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  overflow: hidden;
}

.stage {
  position: relative;
  width: 100%;
  height: 360px;
  /* 夕暮れのシンセウェーブ背景 */
  background: linear-gradient(180deg, #2b1055 0%, #7a1f6e 45%, #1a0830 100%);
}

#terrain {
  display: block;
  width: 100%;
  height: 100%;
}

.caption {
  position: absolute;
  left: 28px;
  bottom: 24px;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, .7);
  pointer-events: none;
}

.badge {
  display: inline-block;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(255, 95, 162, .2);
  border: 1px solid rgba(255, 95, 162, .5);
  color: var(--accent);
  margin-bottom: 10px;
}

.caption h2 {
  font-size: 22px;
  font-weight: 700;
}

.caption p {
  margin-top: 4px;
  font-size: 13px;
  opacity: .72;
}
JavaScript
// ワイヤーフレーム地形:平面の頂点を三角関数で波打たせる
(function () {
  "use strict";
  const canvas = document.getElementById("terrain");
  if (!canvas || typeof THREE === "undefined") return;

  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 100);
  camera.position.set(0, 4.2, 9);
  camera.lookAt(0, -1, 0);

  // 分割数の多い平面を地形のベースにする
  const SEG = 48;
  const geometry = new THREE.PlaneGeometry(20, 20, SEG, SEG);
  geometry.rotateX(-Math.PI / 2);
  const material = new THREE.MeshBasicMaterial({
    color: 0xff5fa2,
    wireframe: true,
    transparent: true,
    opacity: 0.85,
  });
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);

  // 初期高さを保存(基準として使用)
  const pos = geometry.attributes.position;
  const baseX = new Float32Array(pos.count);
  const baseZ = new Float32Array(pos.count);
  for (let i = 0; i < pos.count; i++) {
    baseX[i] = pos.getX(i);
    baseZ[i] = pos.getZ(i);
  }

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize);

  // 二つの正弦波を合成して高さを決める
  function deform(t) {
    for (let i = 0; i < pos.count; i++) {
      const x = baseX[i], z = baseZ[i];
      const y = Math.sin(x * 0.5 + t) * 0.9 + Math.cos(z * 0.4 + t * 0.7) * 0.7;
      pos.setY(i, y);
    }
    pos.needsUpdate = true;
  }
  deform(0);

  let raf = 0;
  const start = performance.now();
  function animate(now) {
    if (!reduceMotion) {
      const t = (now - start) * 0.0011;
      deform(t);
      mesh.rotation.y = Math.sin(t * 0.3) * 0.12;
    }
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate(start);

  window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();

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

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

# 追加してほしい効果
ワイヤーフレーム地形(WebGL / Three.js)
平面メッシュの頂点を正弦波で変位させて波打たせるレトロな立体地形。シンセウェーブ調のビジュアルやダッシュボード背景に最適です。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 波打つワイヤーフレーム地形。頂点を三角関数でアニメーション -->
<div class="stage">
  <canvas id="terrain" aria-label="波打つワイヤーフレーム地形"></canvas>
  <div class="caption">
    <span class="badge">Wireframe</span>
    <h2>Wireframe Terrain</h2>
    <p>頂点を時間で変位させるレトロな立体メッシュ</p>
  </div>
</div>

【CSS】
/* ネオンな配色 */
:root {
  --ink: #f3eaff;
  --accent: #ff5fa2;
}

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

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  overflow: hidden;
}

.stage {
  position: relative;
  width: 100%;
  height: 360px;
  /* 夕暮れのシンセウェーブ背景 */
  background: linear-gradient(180deg, #2b1055 0%, #7a1f6e 45%, #1a0830 100%);
}

#terrain {
  display: block;
  width: 100%;
  height: 100%;
}

.caption {
  position: absolute;
  left: 28px;
  bottom: 24px;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, .7);
  pointer-events: none;
}

.badge {
  display: inline-block;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(255, 95, 162, .2);
  border: 1px solid rgba(255, 95, 162, .5);
  color: var(--accent);
  margin-bottom: 10px;
}

.caption h2 {
  font-size: 22px;
  font-weight: 700;
}

.caption p {
  margin-top: 4px;
  font-size: 13px;
  opacity: .72;
}

【JavaScript】
// ワイヤーフレーム地形:平面の頂点を三角関数で波打たせる
(function () {
  "use strict";
  const canvas = document.getElementById("terrain");
  if (!canvas || typeof THREE === "undefined") return;

  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 100);
  camera.position.set(0, 4.2, 9);
  camera.lookAt(0, -1, 0);

  // 分割数の多い平面を地形のベースにする
  const SEG = 48;
  const geometry = new THREE.PlaneGeometry(20, 20, SEG, SEG);
  geometry.rotateX(-Math.PI / 2);
  const material = new THREE.MeshBasicMaterial({
    color: 0xff5fa2,
    wireframe: true,
    transparent: true,
    opacity: 0.85,
  });
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);

  // 初期高さを保存(基準として使用)
  const pos = geometry.attributes.position;
  const baseX = new Float32Array(pos.count);
  const baseZ = new Float32Array(pos.count);
  for (let i = 0; i < pos.count; i++) {
    baseX[i] = pos.getX(i);
    baseZ[i] = pos.getZ(i);
  }

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize);

  // 二つの正弦波を合成して高さを決める
  function deform(t) {
    for (let i = 0; i < pos.count; i++) {
      const x = baseX[i], z = baseZ[i];
      const y = Math.sin(x * 0.5 + t) * 0.9 + Math.cos(z * 0.4 + t * 0.7) * 0.7;
      pos.setY(i, y);
    }
    pos.needsUpdate = true;
  }
  deform(0);

  let raf = 0;
  const start = performance.now();
  function animate(now) {
    if (!reduceMotion) {
      const t = (now - start) * 0.0011;
      deform(t);
      mesh.rotation.y = Math.sin(t * 0.3) * 0.12;
    }
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate(start);

  window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();

# 外部ライブラリ
https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js

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