DNA二重らせん

らせん上に球を並べ横棒で対を結んだ回転する二重らせんモデル。科学・医療・バイオ系サイトのキービジュアルに向きます。

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

#webgl#threejs#3d#animation

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura:二重らせんを花びらの渦に見立てた楽曲MVティザー -->
<section class="sk-mv" aria-label="Sakura 楽曲MV">
  <!-- 背景:回転する桜色の二重らせん(つながりの象徴) -->
  <canvas id="scene" class="sk-mv__canvas" aria-hidden="true"></canvas>
  <div class="sk-mv__fallback" id="sk-fallback" hidden></div>

  <header class="sk-bar">
    <span class="sk-logo">🌸 Sakura</span>
    <nav class="sk-nav">
      <a href="#">ニュース</a>
      <a href="#">ディスコグラフィ</a>
    </nav>
  </header>

  <div class="sk-mv__body">
    <span class="sk-kicker">MUSIC VIDEO TEASER</span>
    <h1 class="sk-title">『はなびらリンク』</h1>
    <p class="sk-lead">7人の歌声がひとつに重なる新曲MV。<br>春の夜空に舞う、桜のらせんがテーマです。</p>
    <div class="sk-mv__meta">
      <span class="sk-mv__views">▶ 公開3日で 120万回再生</span>
    </div>
    <button class="sk-btn" type="button">フルMVを再生</button>
  </div>
</section>
CSS
/* Sakura:夜桜トーンのMVティザー */
:root {
  --pink: #ffd1e0;
  --pink-deep: #ff8fb3;
  --white: #ffffff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  background: #1d1424;
}

.sk-mv {
  position: relative;
  width: 100%;
  height: 400px;
  overflow: hidden;
  /* 夜桜の空 */
  background:
    radial-gradient(95% 95% at 80% 50%, #4a2747 0%, #2c1a35 55%, #1a1022 100%);
  color: #fff;
}

.sk-mv__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* フォールバック:らせん風の二重カーブ */
.sk-mv__fallback {
  position: absolute;
  right: 18%;
  top: 50%;
  width: 90px;
  height: 240px;
  transform: translateY(-50%);
  background:
    radial-gradient(8px 8px at 20% 10%, var(--pink), transparent),
    radial-gradient(8px 8px at 80% 30%, var(--pink-deep), transparent),
    radial-gradient(8px 8px at 20% 50%, var(--pink), transparent),
    radial-gradient(8px 8px at 80% 70%, var(--pink-deep), transparent),
    radial-gradient(8px 8px at 20% 90%, var(--pink), transparent);
}

.sk-bar {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 26px;
}
.sk-logo { font-size: 18px; font-weight: 800; letter-spacing: 0.04em; }
.sk-nav { display: flex; gap: 18px; }
.sk-nav a {
  color: rgba(255, 255, 255, 0.82);
  text-decoration: none;
  font-size: 13px;
  font-weight: 600;
  transition: color 0.2s ease;
}
.sk-nav a:hover { color: var(--pink); }

.sk-mv__body {
  position: relative;
  z-index: 2;
  max-width: 430px;
  padding: 34px 26px;
}
.sk-kicker {
  font-size: 11px;
  letter-spacing: 0.26em;
  font-weight: 700;
  color: var(--pink-deep);
}
.sk-title {
  margin: 12px 0 14px;
  font-size: 32px;
  font-weight: 800;
  letter-spacing: 0.02em;
  text-shadow: 0 2px 18px rgba(255, 143, 179, 0.35);
}
.sk-lead {
  margin: 0 0 18px;
  font-size: 13.5px;
  line-height: 1.85;
  color: rgba(255, 255, 255, 0.82);
}
.sk-mv__meta { margin-bottom: 22px; }
.sk-mv__views {
  font-size: 12px;
  font-weight: 600;
  color: var(--pink);
  letter-spacing: 0.04em;
}

.sk-btn {
  font: inherit;
  font-size: 14px;
  font-weight: 800;
  color: #fff;
  background: linear-gradient(135deg, var(--pink-deep), #ff6f9c);
  border: none;
  padding: 12px 28px;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 8px 24px rgba(255, 111, 156, 0.5);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.sk-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 30px rgba(255, 111, 156, 0.65); }
.sk-btn:active { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .sk-btn { transition: none; }
}
JavaScript
// Sakura MVティザー:らせん上に球を並べ横棒で対を結んだ二重らせん(つながりの象徴・桜色)
(function () {
  "use strict";
  const canvas = document.getElementById("scene");
  const fallback = document.getElementById("sk-fallback");
  // THREE未読込や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, 9);

  // らせん全体を右に寄せて前景テキストを避ける
  const helix = new THREE.Group();
  helix.position.x = 2.4;
  scene.add(helix);

  const RUNGS = 22;       // 段数
  const TURN = 0.5;       // 1段ごとの回転量
  const RADIUS = 1.1;
  const SPAN = 5.4;       // 縦の長さ

  const pink = new THREE.MeshStandardMaterial({ color: 0xff8fb3, metalness: 0.2, roughness: 0.5 });
  const white = new THREE.MeshStandardMaterial({ color: 0xffe3ee, metalness: 0.2, roughness: 0.5 });
  const barMat = new THREE.MeshStandardMaterial({ color: 0xffd1e0, transparent: true, opacity: 0.55 });
  const ballGeo = new THREE.SphereGeometry(0.16, 16, 16);

  for (let i = 0; i < RUNGS; i++) {
    const a = i * TURN;
    const y = (i / (RUNGS - 1) - 0.5) * SPAN;
    const x1 = Math.cos(a) * RADIUS;
    const z1 = Math.sin(a) * RADIUS;
    const x2 = Math.cos(a + Math.PI) * RADIUS;
    const z2 = Math.sin(a + Math.PI) * RADIUS;

    // 対になる2つの球
    const b1 = new THREE.Mesh(ballGeo, pink);
    b1.position.set(x1, y, z1);
    const b2 = new THREE.Mesh(ballGeo, white);
    b2.position.set(x2, y, z2);
    helix.add(b1, b2);

    // 2球を結ぶ横棒(花びらのリンク)
    const bar = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, RADIUS * 2, 6), barMat);
    bar.position.set(0, y, 0);
    bar.rotation.z = Math.PI / 2;
    bar.rotation.y = -a; // らせんに合わせて向きを回す
    helix.add(bar);
  }

  scene.add(new THREE.AmbientLight(0x553344, 1.6));
  const key = new THREE.DirectionalLight(0xffffff, 1.3);
  key.position.set(3, 4, 5);
  scene.add(key);
  const rim = new THREE.PointLight(0xff8fb3, 1.4, 30);
  rim.position.set(-3, 0, 3);
  scene.add(rim);

  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 (!reduceMotion) helix.rotation.y += 0.01;
    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
<!-- 二重らせん構造を回転表示するDNAヘリックス -->
<div class="stage">
  <canvas id="dna" aria-label="回転するDNA二重らせん"></canvas>
  <div class="caption">
    <span class="badge">Instances</span>
    <h2>DNA Helix</h2>
    <p>球と棒を配置して構築する二重らせんモデル</p>
  </div>
</div>
CSS
/* 配色変数 */
:root {
  --ink: #eafffb;
  --accent: #45e0c8;
}

* { 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% 50%, #04303a 0%, #021820 60%, #010a10 100%);
}

#dna {
  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(69, 224, 200, .18);
  border: 1px solid rgba(69, 224, 200, .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
// DNAヘリックス:らせん上に球を並べ、横棒で対を結ぶ
(function () {
  "use strict";
  const canvas = document.getElementById("dna");
  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.set(0, 0, 11);

  // らせん全体をまとめるグループ
  const helix = new THREE.Group();
  scene.add(helix);

  const PAIRS = 22;        // 塩基対の数
  const RADIUS = 2.2;      // らせんの半径
  const SPAN = 9;          // 縦方向の長さ
  const TURN = Math.PI * 4; // 全体のねじれ量

  const ballGeo = new THREE.SphereGeometry(0.28, 18, 18);
  const matA = new THREE.MeshStandardMaterial({ color: 0x45e0c8, metalness: 0.3, roughness: 0.35 });
  const matB = new THREE.MeshStandardMaterial({ color: 0xff6f91, metalness: 0.3, roughness: 0.35 });
  const rungMat = new THREE.MeshStandardMaterial({ color: 0xaecbff, metalness: 0.2, roughness: 0.5 });

  for (let i = 0; i < PAIRS; i++) {
    const t = i / (PAIRS - 1);
    const angle = t * TURN;
    const y = (t - 0.5) * SPAN;
    const x = Math.cos(angle) * RADIUS;
    const z = Math.sin(angle) * RADIUS;

    // 二本の鎖の球
    const s1 = new THREE.Mesh(ballGeo, matA);
    s1.position.set(x, y, z);
    const s2 = new THREE.Mesh(ballGeo, matB);
    s2.position.set(-x, y, -z);
    helix.add(s1, s2);

    // 二点を結ぶ横棒(円柱)
    const a = s1.position, b = s2.position;
    const dist = a.distanceTo(b);
    const rung = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, dist, 8), rungMat);
    rung.position.copy(a).lerp(b, 0.5);
    rung.lookAt(b);
    rung.rotateX(Math.PI / 2); // 円柱の軸を向きに合わせる
    helix.add(rung);
  }

  scene.add(new THREE.AmbientLight(0xffffff, 0.7));
  const key = new THREE.DirectionalLight(0xffffff, 1.0);
  key.position.set(4, 5, 6);
  scene.add(key);
  const rim = new THREE.PointLight(0x45e0c8, 1.2, 40);
  rim.position.set(-5, 0, 4);
  scene.add(rim);

  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 (!reduceMotion) helix.rotation.y += 0.009;
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate();

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

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

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

# 追加してほしい効果
DNA二重らせん(WebGL / Three.js)
らせん上に球を並べ横棒で対を結んだ回転する二重らせんモデル。科学・医療・バイオ系サイトのキービジュアルに向きます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 二重らせん構造を回転表示するDNAヘリックス -->
<div class="stage">
  <canvas id="dna" aria-label="回転するDNA二重らせん"></canvas>
  <div class="caption">
    <span class="badge">Instances</span>
    <h2>DNA Helix</h2>
    <p>球と棒を配置して構築する二重らせんモデル</p>
  </div>
</div>

【CSS】
/* 配色変数 */
:root {
  --ink: #eafffb;
  --accent: #45e0c8;
}

* { 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% 50%, #04303a 0%, #021820 60%, #010a10 100%);
}

#dna {
  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(69, 224, 200, .18);
  border: 1px solid rgba(69, 224, 200, .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】
// DNAヘリックス:らせん上に球を並べ、横棒で対を結ぶ
(function () {
  "use strict";
  const canvas = document.getElementById("dna");
  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.set(0, 0, 11);

  // らせん全体をまとめるグループ
  const helix = new THREE.Group();
  scene.add(helix);

  const PAIRS = 22;        // 塩基対の数
  const RADIUS = 2.2;      // らせんの半径
  const SPAN = 9;          // 縦方向の長さ
  const TURN = Math.PI * 4; // 全体のねじれ量

  const ballGeo = new THREE.SphereGeometry(0.28, 18, 18);
  const matA = new THREE.MeshStandardMaterial({ color: 0x45e0c8, metalness: 0.3, roughness: 0.35 });
  const matB = new THREE.MeshStandardMaterial({ color: 0xff6f91, metalness: 0.3, roughness: 0.35 });
  const rungMat = new THREE.MeshStandardMaterial({ color: 0xaecbff, metalness: 0.2, roughness: 0.5 });

  for (let i = 0; i < PAIRS; i++) {
    const t = i / (PAIRS - 1);
    const angle = t * TURN;
    const y = (t - 0.5) * SPAN;
    const x = Math.cos(angle) * RADIUS;
    const z = Math.sin(angle) * RADIUS;

    // 二本の鎖の球
    const s1 = new THREE.Mesh(ballGeo, matA);
    s1.position.set(x, y, z);
    const s2 = new THREE.Mesh(ballGeo, matB);
    s2.position.set(-x, y, -z);
    helix.add(s1, s2);

    // 二点を結ぶ横棒(円柱)
    const a = s1.position, b = s2.position;
    const dist = a.distanceTo(b);
    const rung = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, dist, 8), rungMat);
    rung.position.copy(a).lerp(b, 0.5);
    rung.lookAt(b);
    rung.rotateX(Math.PI / 2); // 円柱の軸を向きに合わせる
    helix.add(rung);
  }

  scene.add(new THREE.AmbientLight(0xffffff, 0.7));
  const key = new THREE.DirectionalLight(0xffffff, 1.0);
  key.position.set(4, 5, 6);
  scene.add(key);
  const rim = new THREE.PointLight(0x45e0c8, 1.2, 40);
  rim.position.set(-5, 0, 4);
  scene.add(rim);

  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 (!reduceMotion) helix.rotation.y += 0.009;
    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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。