脈動する二十面体

頂点を法線方向に呼吸させたワイヤー二十面体を、加算合成の光の粒で包んだ幻想的な造形。テック系やAIプロダクトの象徴ビジュアルに最適です。

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

#webgl#threejs#particles#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

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

HTML
<!-- FlowDesk:脈動する二十面体をAIエンジンの象徴にしたCTAヒーロー -->
<section class="fd-ai" aria-label="FlowDesk AI">
  <!-- 背景で呼吸する二十面体+光の粒(AIコアの象徴) -->
  <canvas id="scene" class="fd-ai__canvas" aria-hidden="true"></canvas>
  <div class="fd-ai__fallback" id="fd-fallback" hidden></div>

  <header class="fd-bar">
    <span class="fd-logo"><b>◆</b> FlowDesk</span>
    <nav class="fd-nav">
      <a href="#">機能</a>
      <a href="#">料金</a>
      <a href="#">導入事例</a>
    </nav>
  </header>

  <div class="fd-ai__body">
    <span class="fd-badge">FlowDesk AI</span>
    <h1 class="fd-title">考えるより速く、<br>仕事が片づく。</h1>
    <p class="fd-lead">タスクの優先順位も、議事録の要約も。<br>AIエンジンがチームの判断を後押しします。</p>
    <div class="fd-cta">
      <button class="fd-btn fd-btn--primary" type="button">無料で始める</button>
      <button class="fd-btn fd-btn--ghost" type="button">デモを見る</button>
    </div>
  </div>
</section>
CSS
/* FlowDesk:中央にAIコアが浮かぶCTAヒーロー */
: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-ai {
  position: relative;
  width: 100%;
  height: 400px;
  overflow: hidden;
  background:
    radial-gradient(90% 80% at 75% 45%, #16294f 0%, #0f1b34 55%, #080f20 100%);
  color: var(--white);
}

/* AIコアは右寄りの背景に大きく */
.fd-ai__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.fd-ai__fallback {
  position: absolute;
  right: 14%;
  top: 50%;
  width: 200px;
  height: 200px;
  transform: translateY(-50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(79, 124, 255, 0.7), transparent 65%);
  filter: blur(6px);
}

.fd-bar {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 26px;
}
.fd-logo { font-size: 16px; font-weight: 700; }
.fd-logo b { color: var(--blue); }
.fd-nav { display: flex; gap: 20px; }
.fd-nav a {
  color: rgba(255, 255, 255, 0.78);
  text-decoration: none;
  font-size: 13px;
  transition: color 0.2s ease;
}
.fd-nav a:hover { color: var(--blue); }

.fd-ai__body {
  position: relative;
  z-index: 2;
  max-width: 440px;
  padding: 30px 26px;
}
.fd-badge {
  display: inline-block;
  font-size: 11px;
  letter-spacing: 0.14em;
  font-weight: 700;
  padding: 5px 14px;
  border-radius: 999px;
  color: #bcd0ff;
  background: rgba(79, 124, 255, 0.16);
  border: 1px solid rgba(79, 124, 255, 0.4);
}
.fd-title {
  margin: 16px 0 14px;
  font-size: 34px;
  line-height: 1.34;
  font-weight: 700;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.4);
}
.fd-lead {
  margin: 0 0 24px;
  font-size: 14px;
  line-height: 1.85;
  color: rgba(255, 255, 255, 0.8);
}

.fd-cta { display: flex; gap: 12px; }
.fd-btn {
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  padding: 12px 24px;
  border-radius: 10px;
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.fd-btn--primary {
  color: var(--white);
  background: linear-gradient(135deg, #6a93ff, var(--blue));
  border: none;
  box-shadow: 0 8px 22px rgba(79, 124, 255, 0.45);
}
.fd-btn--primary:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(79, 124, 255, 0.6); }
.fd-btn--ghost {
  color: #cdddff;
  background: transparent;
  border: 1px solid rgba(79, 124, 255, 0.5);
}
.fd-btn--ghost:hover { background: rgba(79, 124, 255, 0.12); }
.fd-btn:active { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .fd-btn { transition: none; }
}
JavaScript
// FlowDesk AIヒーロー:法線方向に呼吸するワイヤー二十面体+加算合成の光の粒(AIコアの象徴)
(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(45, 1, 0.1, 100);
  camera.position.set(0, 0, 5);

  // 全体を右に寄せて前景テキストを避けるグループ
  const core = new THREE.Group();
  core.position.x = 1.5;
  scene.add(core);

  // ワイヤー二十面体(呼吸させる)
  const geometry = new THREE.IcosahedronGeometry(1.3, 1);
  const base = geometry.attributes.position.array.slice(); // 基準頂点
  const normals = geometry.attributes.normal.array.slice();
  const material = new THREE.MeshBasicMaterial({
    color: 0x6a93ff, wireframe: true, transparent: true, opacity: 0.85,
  });
  const ico = new THREE.Mesh(geometry, material);
  core.add(ico);

  // 周囲を漂う光の粒(加算合成で発光感)
  const COUNT = 500;
  const pPos = new Float32Array(COUNT * 3);
  for (let i = 0; i < COUNT; i++) {
    const r = 1.6 + Math.random() * 1.4;
    const th = Math.random() * Math.PI * 2;
    const ph = Math.acos(2 * Math.random() - 1);
    pPos[i * 3] = r * Math.sin(ph) * Math.cos(th);
    pPos[i * 3 + 1] = r * Math.sin(ph) * Math.sin(th);
    pPos[i * 3 + 2] = r * Math.cos(ph);
  }
  const pGeo = new THREE.BufferGeometry();
  pGeo.setAttribute("position", new THREE.BufferAttribute(pPos, 3));
  const pMat = new THREE.PointsMaterial({
    color: 0x9ec0ff, size: 0.045, transparent: true, opacity: 0.9,
    blending: THREE.AdditiveBlending, depthWrite: false,
  });
  const particles = new THREE.Points(pGeo, pMat);
  core.add(particles);

  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;
      core.rotation.y += 0.004;
      core.rotation.x += 0.0016;
      particles.rotation.y -= 0.002;
      // 頂点を法線方向に呼吸させる
      const pos = geometry.attributes.position;
      const breath = Math.sin(t) * 0.12;
      for (let i = 0; i < pos.count; i++) {
        pos.array[i * 3] = base[i * 3] + normals[i * 3] * breath;
        pos.array[i * 3 + 1] = base[i * 3 + 1] + normals[i * 3 + 1] * breath;
        pos.array[i * 3 + 2] = base[i * 3 + 2] + normals[i * 3 + 2] * breath;
      }
      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="ico" aria-label="脈動する二十面体"></canvas>
  <div class="caption">
    <span class="badge">Pulse</span>
    <h2>Glowing Icosahedron</h2>
    <p>頂点を呼吸させた幾何体と加算合成の光の粒</p>
  </div>
</div>
CSS
/* 配色変数 */
:root {
  --ink: #eef6ff;
  --accent: #7aa2ff;
}

* { 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: radial-gradient(circle at 50% 45%, #161d3a 0%, #0a0e1f 60%, #04060d 100%);
}

#ico {
  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(122, 162, 255, .18);
  border: 1px solid rgba(122, 162, 255, .45);
  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("ico");
  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(50, 1, 0.1, 100);
  camera.position.z = 5;

  const group = new THREE.Group();
  scene.add(group);

  // 細分化した二十面体をワイヤーで表示
  const geometry = new THREE.IcosahedronGeometry(1.5, 4);
  const pos = geometry.attributes.position;
  // 元の頂点を保存(呼吸の基準)
  const base = new Float32Array(pos.array.length);
  base.set(pos.array);

  const wire = new THREE.MeshBasicMaterial({
    color: 0x7aa2ff,
    wireframe: true,
    transparent: true,
    opacity: 0.55,
  });
  const solid = new THREE.MeshBasicMaterial({ color: 0x0c1430 });
  // 内側に塗りつぶしを置いて奥のワイヤーを隠す
  group.add(new THREE.Mesh(geometry, solid));
  const mesh = new THREE.Mesh(geometry, wire);
  group.add(mesh);

  // 周囲を漂う光の粒
  const PCOUNT = 700;
  const parr = new Float32Array(PCOUNT * 3);
  for (let i = 0; i < PCOUNT; i++) {
    // 球面上にランダム配置(半径2.2〜3.4)
    const r = 2.2 + Math.random() * 1.2;
    const theta = Math.random() * Math.PI * 2;
    const phi = Math.acos(2 * Math.random() - 1);
    parr[i * 3] = r * Math.sin(phi) * Math.cos(theta);
    parr[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
    parr[i * 3 + 2] = r * Math.cos(phi);
  }
  const pgeo = new THREE.BufferGeometry();
  pgeo.setAttribute("position", new THREE.BufferAttribute(parr, 3));
  const pmat = new THREE.PointsMaterial({
    color: 0x9ec1ff,
    size: 0.05,
    transparent: true,
    opacity: 0.8,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
  });
  const dust = new THREE.Points(pgeo, pmat);
  scene.add(dust);

  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 pulse(t) {
    for (let i = 0; i < pos.count; i++) {
      const ix = i * 3;
      const x = base[ix], y = base[ix + 1], z = base[ix + 2];
      const len = Math.sqrt(x * x + y * y + z * z) || 1;
      const wave = 1 + Math.sin(t * 1.6 + len * 5 + x * 2) * 0.06;
      pos.array[ix] = x * wave;
      pos.array[ix + 1] = y * wave;
      pos.array[ix + 2] = z * wave;
    }
    pos.needsUpdate = true;
  }

  let raf = 0;
  const start = performance.now();
  function animate(now) {
    const t = (now - start) * 0.001;
    if (!reduceMotion) {
      pulse(t);
      group.rotation.x = t * 0.18;
      group.rotation.y = t * 0.25;
      dust.rotation.y = -t * 0.08;
    }
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate(start);

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

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

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

# 追加してほしい効果
脈動する二十面体(WebGL / Three.js)
頂点を法線方向に呼吸させたワイヤー二十面体を、加算合成の光の粒で包んだ幻想的な造形。テック系やAIプロダクトの象徴ビジュアルに最適です。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 脈動するワイヤー二十面体。粒子のオーラを伴う -->
<div class="stage">
  <canvas id="ico" aria-label="脈動する二十面体"></canvas>
  <div class="caption">
    <span class="badge">Pulse</span>
    <h2>Glowing Icosahedron</h2>
    <p>頂点を呼吸させた幾何体と加算合成の光の粒</p>
  </div>
</div>

【CSS】
/* 配色変数 */
:root {
  --ink: #eef6ff;
  --accent: #7aa2ff;
}

* { 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: radial-gradient(circle at 50% 45%, #161d3a 0%, #0a0e1f 60%, #04060d 100%);
}

#ico {
  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(122, 162, 255, .18);
  border: 1px solid rgba(122, 162, 255, .45);
  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("ico");
  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(50, 1, 0.1, 100);
  camera.position.z = 5;

  const group = new THREE.Group();
  scene.add(group);

  // 細分化した二十面体をワイヤーで表示
  const geometry = new THREE.IcosahedronGeometry(1.5, 4);
  const pos = geometry.attributes.position;
  // 元の頂点を保存(呼吸の基準)
  const base = new Float32Array(pos.array.length);
  base.set(pos.array);

  const wire = new THREE.MeshBasicMaterial({
    color: 0x7aa2ff,
    wireframe: true,
    transparent: true,
    opacity: 0.55,
  });
  const solid = new THREE.MeshBasicMaterial({ color: 0x0c1430 });
  // 内側に塗りつぶしを置いて奥のワイヤーを隠す
  group.add(new THREE.Mesh(geometry, solid));
  const mesh = new THREE.Mesh(geometry, wire);
  group.add(mesh);

  // 周囲を漂う光の粒
  const PCOUNT = 700;
  const parr = new Float32Array(PCOUNT * 3);
  for (let i = 0; i < PCOUNT; i++) {
    // 球面上にランダム配置(半径2.2〜3.4)
    const r = 2.2 + Math.random() * 1.2;
    const theta = Math.random() * Math.PI * 2;
    const phi = Math.acos(2 * Math.random() - 1);
    parr[i * 3] = r * Math.sin(phi) * Math.cos(theta);
    parr[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
    parr[i * 3 + 2] = r * Math.cos(phi);
  }
  const pgeo = new THREE.BufferGeometry();
  pgeo.setAttribute("position", new THREE.BufferAttribute(parr, 3));
  const pmat = new THREE.PointsMaterial({
    color: 0x9ec1ff,
    size: 0.05,
    transparent: true,
    opacity: 0.8,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
  });
  const dust = new THREE.Points(pgeo, pmat);
  scene.add(dust);

  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 pulse(t) {
    for (let i = 0; i < pos.count; i++) {
      const ix = i * 3;
      const x = base[ix], y = base[ix + 1], z = base[ix + 2];
      const len = Math.sqrt(x * x + y * y + z * z) || 1;
      const wave = 1 + Math.sin(t * 1.6 + len * 5 + x * 2) * 0.06;
      pos.array[ix] = x * wave;
      pos.array[ix + 1] = y * wave;
      pos.array[ix + 2] = z * wave;
    }
    pos.needsUpdate = true;
  }

  let raf = 0;
  const start = performance.now();
  function animate(now) {
    const t = (now - start) * 0.001;
    if (!reduceMotion) {
      pulse(t);
      group.rotation.x = t * 0.18;
      group.rotation.y = t * 0.25;
      dust.rotation.y = -t * 0.08;
    }
    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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。