インタラクティブ立方体
ポインタのドラッグで回転し、離すと慣性で滑らかに減速する6色立方体。製品の3Dプレビューや操作デモのUI部品として活用できます。
外部ライブラリ: https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:ドラッグで回せる立方体を製品プレビューにした機能紹介 -->
<section class="fd-feature" aria-label="FlowDesk 連携プレビュー">
<div class="fd-feature__text">
<span class="fd-kicker">INTEGRATIONS</span>
<h1 class="fd-title">あらゆるツールを、<br>ひとつの面に。</h1>
<p class="fd-lead">チャット・カレンダー・ストレージ。<br>立方体をドラッグして連携面を確かめてください。</p>
<ul class="fd-points">
<li>200以上のアプリと双方向同期</li>
<li>ノーコードで自動化ルールを設定</li>
</ul>
<button class="fd-btn" type="button">連携を試す</button>
</div>
<div class="fd-feature__view">
<!-- ドラッグで回り、離すと慣性で減速する立方体 -->
<canvas id="scene" class="fd-feature__canvas" aria-label="3D製品プレビュー"></canvas>
<div class="fd-feature__fallback" id="fd-fallback" hidden></div>
<span class="fd-feature__hint">ドラッグで回転</span>
</div>
</section>
CSS
/* FlowDesk:左にテキスト、右に3Dプレビューの2カラム */
: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-feature {
position: relative;
width: 100%;
height: 400px;
display: grid;
grid-template-columns: 1fr 0.85fr;
align-items: center;
overflow: hidden;
background:
radial-gradient(100% 120% at 100% 50%, #1a3066 0%, #0f1b34 60%, #0a1226 100%);
color: var(--white);
}
.fd-feature__text { padding: 0 30px 0 36px; }
.fd-kicker {
font-size: 10.5px;
letter-spacing: 0.26em;
color: var(--blue);
font-weight: 700;
}
.fd-title {
margin: 10px 0 14px;
font-size: 28px;
line-height: 1.32;
font-weight: 700;
}
.fd-lead {
margin: 0 0 16px;
font-size: 13.5px;
line-height: 1.8;
color: rgba(255, 255, 255, 0.78);
}
.fd-points {
margin: 0 0 20px;
padding: 0;
list-style: none;
font-size: 12.5px;
color: rgba(255, 255, 255, 0.86);
}
.fd-points li {
position: relative;
padding-left: 20px;
margin-bottom: 8px;
}
.fd-points li::before {
content: "✓";
position: absolute;
left: 0;
color: var(--blue);
font-weight: 700;
}
.fd-btn {
font: inherit;
font-size: 14px;
font-weight: 700;
color: var(--white);
background: linear-gradient(135deg, #6a93ff, var(--blue));
border: none;
padding: 12px 28px;
border-radius: 10px;
cursor: pointer;
box-shadow: 0 8px 22px rgba(79, 124, 255, 0.45);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.fd-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(79, 124, 255, 0.6); }
.fd-btn:active { transform: translateY(0); }
/* 右の3Dビュー領域 */
.fd-feature__view {
position: relative;
width: 100%;
height: 100%;
}
.fd-feature__canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
cursor: grab;
touch-action: none;
}
.fd-feature__canvas:active { cursor: grabbing; }
.fd-feature__fallback {
position: absolute;
left: 50%;
top: 50%;
width: 130px;
height: 130px;
transform: translate(-50%, -50%) rotate(12deg);
background: linear-gradient(135deg, #6a93ff, var(--blue));
border-radius: 18px;
box-shadow: 0 20px 50px rgba(79, 124, 255, 0.5);
}
.fd-feature__hint {
position: absolute;
left: 50%;
bottom: 18px;
transform: translateX(-50%);
font-size: 11px;
letter-spacing: 0.1em;
color: rgba(255, 255, 255, 0.55);
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.fd-btn { transition: none; }
}
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(45, 1, 0.1, 100);
camera.position.set(0, 0, 4.2);
// 6面それぞれを連携アプリ色に
const faceColors = [0x4f7cff, 0x6a93ff, 0x3a5fd9, 0x8aa9ff, 0x2f4dbf, 0x5c83ff];
const geometry = new THREE.BoxGeometry(1.7, 1.7, 1.7);
const materials = faceColors.map((c) => new THREE.MeshStandardMaterial({
color: c, metalness: 0.3, roughness: 0.45,
}));
const cube = new THREE.Mesh(geometry, materials);
cube.rotation.set(0.5, 0.6, 0);
scene.add(cube);
scene.add(new THREE.AmbientLight(0x223355, 1.5));
const key = new THREE.DirectionalLight(0xffffff, 1.5);
key.position.set(3, 4, 5);
scene.add(key);
// ドラッグ操作と慣性
let dragging = false;
let lastX = 0, lastY = 0;
let velX = 0, velY = 0;
canvas.addEventListener("pointerdown", (e) => {
dragging = true;
lastX = e.clientX; lastY = e.clientY;
velX = velY = 0;
canvas.setPointerCapture?.(e.pointerId);
});
canvas.addEventListener("pointermove", (e) => {
if (!dragging) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
velY = dx * 0.005;
velX = dy * 0.005;
cube.rotation.y += velY;
cube.rotation.x += velX;
});
function endDrag() { dragging = false; }
canvas.addEventListener("pointerup", endDrag);
canvas.addEventListener("pointercancel", endDrag);
canvas.addEventListener("pointerleave", endDrag);
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;
function animate() {
if (!dragging) {
// 慣性で減速、止まったら自動でゆっくり回す
cube.rotation.y += velY;
cube.rotation.x += velX;
velX *= 0.94;
velY *= 0.94;
if (!reduceMotion && Math.abs(velY) < 0.0008) cube.rotation.y += 0.004;
}
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="cube" aria-label="ドラッグで回せる立方体"></canvas>
<div class="caption">
<span class="badge">Drag</span>
<h2>Interactive Cube</h2>
<p>ドラッグで回転、離すと慣性で滑らかに減速</p>
</div>
<div class="hint" id="hint">ドラッグして回す</div>
</div>
CSS
/* 配色変数 */
:root {
--ink: #eef2ff;
--accent: #5eead4;
}
* { 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% 40%, #123a3a 0%, #0a1820 60%, #050b10 100%);
}
#cube {
display: block;
width: 100%;
height: 100%;
cursor: grab;
touch-action: none; /* ドラッグ時のスクロールを抑止 */
}
#cube:active {
cursor: grabbing;
}
.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(94, 234, 212, .18);
border: 1px solid rgba(94, 234, 212, .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;
}
.hint {
position: absolute;
top: 18px;
right: 22px;
font-size: 12px;
color: var(--ink);
background: rgba(255, 255, 255, .08);
border: 1px solid rgba(255, 255, 255, .15);
padding: 6px 12px;
border-radius: 999px;
pointer-events: none;
transition: opacity .5s ease;
}
.hint.hide {
opacity: 0;
}
JavaScript
// インタラクティブ立方体:ポインタドラッグで回転+慣性
(function () {
"use strict";
const canvas = document.getElementById("cube");
const hint = document.getElementById("hint");
if (!canvas || typeof THREE === "undefined") return;
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(45, 1, 0.1, 100);
camera.position.z = 4.6;
// 6面それぞれに異なる色を割り当てた立方体
const faceColors = [0x5eead4, 0x60a5fa, 0xa78bfa, 0xf472b6, 0xfbbf24, 0x34d399];
const materials = faceColors.map((c) =>
new THREE.MeshStandardMaterial({ color: c, metalness: 0.3, roughness: 0.4 })
);
const cube = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.8, 1.8), materials);
scene.add(cube);
// 角を引き締めるエッジライン
const edges = new THREE.LineSegments(
new THREE.EdgesGeometry(cube.geometry),
new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.35 })
);
cube.add(edges);
scene.add(new THREE.AmbientLight(0xffffff, 0.85));
const dir = new THREE.DirectionalLight(0xffffff, 0.9);
dir.position.set(2, 3, 4);
scene.add(dir);
// ドラッグ状態の管理
let dragging = false, lastX = 0, lastY = 0;
let velX = 0.004, velY = 0.006; // 慣性用の角速度
let interacted = false;
function onDown(e) {
dragging = true;
lastX = e.clientX;
lastY = e.clientY;
if (!interacted) {
interacted = true;
if (hint) hint.classList.add("hide");
}
}
function onMove(e) {
if (!dragging) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
lastX = e.clientX;
lastY = e.clientY;
velY = dx * 0.005;
velX = dy * 0.005;
cube.rotation.y += velY;
cube.rotation.x += velX;
}
function onUp() { dragging = false; }
canvas.addEventListener("pointerdown", onDown);
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
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;
function animate() {
if (!dragging) {
// 慣性で回し続け、少しずつ減衰させる
cube.rotation.x += velX;
cube.rotation.y += velY;
velX *= 0.96;
velY *= 0.96;
// ほぼ止まったら一定の自動回転に戻す
if (Math.abs(velX) < 0.0005 && Math.abs(velY) < 0.0005) {
velX = 0.0015;
velY = 0.0025;
}
}
renderer.render(scene, camera);
raf = requestAnimationFrame(animate);
}
animate();
window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「インタラクティブ立方体」の効果を追加してください。
# 追加してほしい効果
インタラクティブ立方体(WebGL / Three.js)
ポインタのドラッグで回転し、離すと慣性で滑らかに減速する6色立方体。製品の3Dプレビューや操作デモのUI部品として活用できます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ドラッグで自由に回せるインタラクティブ立方体 -->
<div class="stage">
<canvas id="cube" aria-label="ドラッグで回せる立方体"></canvas>
<div class="caption">
<span class="badge">Drag</span>
<h2>Interactive Cube</h2>
<p>ドラッグで回転、離すと慣性で滑らかに減速</p>
</div>
<div class="hint" id="hint">ドラッグして回す</div>
</div>
【CSS】
/* 配色変数 */
:root {
--ink: #eef2ff;
--accent: #5eead4;
}
* { 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% 40%, #123a3a 0%, #0a1820 60%, #050b10 100%);
}
#cube {
display: block;
width: 100%;
height: 100%;
cursor: grab;
touch-action: none; /* ドラッグ時のスクロールを抑止 */
}
#cube:active {
cursor: grabbing;
}
.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(94, 234, 212, .18);
border: 1px solid rgba(94, 234, 212, .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;
}
.hint {
position: absolute;
top: 18px;
right: 22px;
font-size: 12px;
color: var(--ink);
background: rgba(255, 255, 255, .08);
border: 1px solid rgba(255, 255, 255, .15);
padding: 6px 12px;
border-radius: 999px;
pointer-events: none;
transition: opacity .5s ease;
}
.hint.hide {
opacity: 0;
}
【JavaScript】
// インタラクティブ立方体:ポインタドラッグで回転+慣性
(function () {
"use strict";
const canvas = document.getElementById("cube");
const hint = document.getElementById("hint");
if (!canvas || typeof THREE === "undefined") return;
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(45, 1, 0.1, 100);
camera.position.z = 4.6;
// 6面それぞれに異なる色を割り当てた立方体
const faceColors = [0x5eead4, 0x60a5fa, 0xa78bfa, 0xf472b6, 0xfbbf24, 0x34d399];
const materials = faceColors.map((c) =>
new THREE.MeshStandardMaterial({ color: c, metalness: 0.3, roughness: 0.4 })
);
const cube = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.8, 1.8), materials);
scene.add(cube);
// 角を引き締めるエッジライン
const edges = new THREE.LineSegments(
new THREE.EdgesGeometry(cube.geometry),
new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.35 })
);
cube.add(edges);
scene.add(new THREE.AmbientLight(0xffffff, 0.85));
const dir = new THREE.DirectionalLight(0xffffff, 0.9);
dir.position.set(2, 3, 4);
scene.add(dir);
// ドラッグ状態の管理
let dragging = false, lastX = 0, lastY = 0;
let velX = 0.004, velY = 0.006; // 慣性用の角速度
let interacted = false;
function onDown(e) {
dragging = true;
lastX = e.clientX;
lastY = e.clientY;
if (!interacted) {
interacted = true;
if (hint) hint.classList.add("hide");
}
}
function onMove(e) {
if (!dragging) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
lastX = e.clientX;
lastY = e.clientY;
velY = dx * 0.005;
velX = dy * 0.005;
cube.rotation.y += velY;
cube.rotation.x += velX;
}
function onUp() { dragging = false; }
canvas.addEventListener("pointerdown", onDown);
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
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;
function animate() {
if (!dragging) {
// 慣性で回し続け、少しずつ減衰させる
cube.rotation.x += velX;
cube.rotation.y += velY;
velX *= 0.96;
velY *= 0.96;
// ほぼ止まったら一定の自動回転に戻す
if (Math.abs(velX) < 0.0005 && Math.abs(velY) < 0.0005) {
velX = 0.0015;
velY = 0.0025;
}
}
renderer.render(scene, camera);
raf = requestAnimationFrame(animate);
}
animate();
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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。