脈動する二十面体
頂点を法線方向に呼吸させたワイヤー二十面体を、加算合成の光の粒で包んだ幻想的な造形。テック系やAIプロダクトの象徴ビジュアルに最適です。
外部ライブラリ: https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
ライブデモ
使用例(お題: 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。