3Dドットグローブ
canvasに球面分布した点を透視投影で回転描画する地球儀風アニメーション。ドラッグ操作対応で、データ可視化やテック系の背景に向きます。
ライブデモ
使用例(お題: 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。