インタラクティブ立方体

ポインタのドラッグで回転し、離すと慣性で滑らかに減速する6色立方体。製品の3Dプレビューや操作デモのUI部品として活用できます。

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

#webgl#threejs#interactive#drag

ライブデモ

使用例(お題: 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。