万華鏡コラージュ

カラフルな図形の破片が万華鏡のように鏡像対称で回り続ける背景です。1セクターだけ描いて12回フォーミラーするだけで、キャンペーンサイトのような華やかな漫然模様が生まれます。

#kaleidoscope#canvas#generative#mandala

ライブデモ

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

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

HTML
<!-- Sakura: ライブ演出のビジュアル。桜ネオンの破片が万華鏡状に回り続ける -->
<div class="stage">
  <canvas class="kaleido" data-kaleido aria-label="桜ネオンの万華鏡ビジュアル"></canvas>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #1a0626; }

.stage {
  width: 100%; height: 100vh; min-height: 240px; max-height: 100%;
  overflow: hidden; background: #1a0626;
}
.kaleido { display: block; width: 100%; height: 100%; }
JavaScript
// 万華鏡(デモと同じロジック)。配色と中央テキストを Sakura テーマに差し替え。
(() => {
  const cv = document.querySelector('[data-kaleido]');
  if (!cv) return;
  const ctx = cv.getContext('2d');
  const off = document.createElement('canvas');
  off.width = off.height = 512;
  const octx = off.getContext('2d');

  const COLORS = ['#FF7DA8', '#FFC2D6', '#9B6BFF', '#FFE3B0', '#FF5EA0'];
  const SECTORS = 12;
  const rand = (a, b) => a + (b - a) * Math.random();

  const parts = [];
  for (let i = 0; i < 15; i++) {
    parts.push({
      shape: i % 4, color: COLORS[i % COLORS.length],
      size: rand(20, 90),
      bx: rand(140, 372), by: rand(140, 372),
      amp: rand(12, 30), ph: rand(0, Math.PI * 2), spd: rand(0.4, 1.0),
      rot: rand(0, Math.PI * 2), rotSpd: rand(-0.3, 0.3), x: 256, y: 256
    });
  }

  let dpr = 1, W = 0, H = 0, cx = 0, cy = 0, R = 0, global = 0, last = 0;
  const resize = () => {
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    W = cv.clientWidth; H = cv.clientHeight;
    cv.width = W * dpr; cv.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    cx = W / 2; cy = H / 2; R = Math.hypot(W, H) / 2 + 20;
    ctx.fillStyle = '#1a0626'; ctx.fillRect(0, 0, W, H);
  };
  window.addEventListener('resize', resize);
  resize();

  const drawParticle = (p) => {
    octx.save();
    octx.translate(p.x, p.y);
    octx.rotate(p.rot);
    octx.fillStyle = p.color; octx.strokeStyle = p.color;
    const s = p.size;
    if (p.shape === 0) { octx.beginPath(); octx.arc(0, 0, s / 2, 0, Math.PI * 2); octx.fill(); }
    else if (p.shape === 1) { octx.beginPath(); octx.moveTo(0, -s / 2); octx.lineTo(s / 2, s / 2); octx.lineTo(-s / 2, s / 2); octx.closePath(); octx.fill(); }
    else if (p.shape === 2) { octx.lineWidth = s * 0.18; octx.beginPath(); octx.arc(0, 0, s / 2, 0.3, Math.PI * 1.3); octx.stroke(); }
    else { octx.fillRect(-s / 2, -s * 0.12, s, s * 0.24); }
    octx.restore();
  };

  const frame = (now) => {
    const dt = last ? Math.min((now - last) / 1000, 0.05) : 0;
    last = now;
    global += 0.05 * dt;
    const t = now / 1000;

    octx.clearRect(0, 0, 512, 512);
    for (const p of parts) {
      p.rot += p.rotSpd * dt;
      p.x = p.bx + Math.sin(t * p.spd + p.ph) * p.amp;
      p.y = p.by + Math.cos(t * p.spd * 0.9 + p.ph) * p.amp;
      drawParticle(p);
    }

    ctx.fillStyle = 'rgba(26,6,38,0.25)';
    ctx.fillRect(0, 0, W, H);

    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(global);
    const half = Math.PI / SECTORS;
    for (let i = 0; i < SECTORS; i++) {
      ctx.save();
      ctx.rotate(i * 2 * half);
      if (i % 2 === 1) ctx.scale(-1, 1);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.arc(0, 0, R, -half - 0.01, half + 0.01);
      ctx.closePath();
      ctx.clip();
      ctx.drawImage(off, -R, -R, 2 * R, 2 * R);
      ctx.restore();
    }
    ctx.restore();

    ctx.beginPath(); ctx.arc(cx, cy, 56, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill();
    ctx.fillStyle = '#1a0626';
    ctx.font = '700 18px system-ui, sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('SAKURA', cx, cy);

    requestAnimationFrame(frame);
  };
  requestAnimationFrame(frame);
})();

実装ガイド

使いどころ

キャンペーンやイベントの華やかな背景・ビジュアルに。生成AI画像を使わずにカラフルな漫然模様を作りたいときに。

実装時の注意点

canvas 2Dで、オフスクリーンに素材1面を描き、メインへ扇形clip+ミラーで12回スタンプします。奇数セクターのscale(-1,1)が鏡像の核です。残像はclearせず半透明で全面を塗って尾を引きます。canvasはdevicePixelRatio(上限2)対応でリサイズ時に再設定。WebGL不要です。

対応ブラウザ

Canvas 2D・clip・drawImageは全モダンブラウザで安定動作します。DPRスケールでぼやけを防ぎ、常時描画のため低スペック端末では負荷に注意します。対応は実機で確認してください。

よくある失敗

フォーミラー(奇数セクター反転)を忘れると回転コピーになり万華鏡に見えません。clipの境界に微小オーバーラップ(+0.01rad)を入れないと継ぎ目が出ます。残像の透明度を上げすぎると尾が消え、下げすぎると滲みます。

応用例

素材をロゴ片や写真に、セクター数や回転速度を調整、中央テキストをブランドに、マウスで回転速度を変えるなどに発展できます。

コード

HTML
<!-- 万華鏡コラージュ:素材1面を扇形clip+ミラーで12回スタンプし鏡像対称に回す -->
<div class="stage">
  <canvas class="kaleido" data-kaleido aria-label="万華鏡状に回り続けるコラージュ背景"></canvas>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #0F1117; }

.stage {
  width: 100%;
  height: 100vh;
  min-height: 240px;
  max-height: 100%;
  overflow: hidden;
  background: #0F1117;
}
.kaleido { display: block; width: 100%; height: 100%; }
JavaScript
// オフスクリーンに15個の図形パーティクルを描き、メインへ12セクターのミラーclipでスタンプ。
(() => {
  const cv = document.querySelector('[data-kaleido]');
  if (!cv) return; // null安全
  const ctx = cv.getContext('2d');
  const off = document.createElement('canvas');
  off.width = off.height = 512;
  const octx = off.getContext('2d');

  const COLORS = ['#FF5C5C', '#FFC83D', '#4ED1A1', '#4D7CFE', '#9B6BFF'];
  const SECTORS = 12;
  const rand = (a, b) => a + (b - a) * Math.random();

  // 素材パーティクル15個(円/三角/円弧/細長矩形)
  const parts = [];
  for (let i = 0; i < 15; i++) {
    parts.push({
      shape: i % 4, color: COLORS[i % COLORS.length],
      size: rand(20, 90),
      bx: rand(140, 372), by: rand(140, 372),
      amp: rand(12, 30), ph: rand(0, Math.PI * 2), spd: rand(0.4, 1.0),
      rot: rand(0, Math.PI * 2), rotSpd: rand(-0.3, 0.3),
      x: 256, y: 256
    });
  }

  let dpr = 1, W = 0, H = 0, cx = 0, cy = 0, R = 0, global = 0, last = 0;
  const resize = () => {
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    W = cv.clientWidth; H = cv.clientHeight;
    cv.width = W * dpr; cv.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    cx = W / 2; cy = H / 2; R = Math.hypot(W, H) / 2 + 20;
    ctx.fillStyle = '#0F1117'; ctx.fillRect(0, 0, W, H);
  };
  window.addEventListener('resize', resize);
  resize();

  const drawParticle = (p) => {
    octx.save();
    octx.translate(p.x, p.y);
    octx.rotate(p.rot);
    octx.fillStyle = p.color; octx.strokeStyle = p.color;
    const s = p.size;
    if (p.shape === 0) { octx.beginPath(); octx.arc(0, 0, s / 2, 0, Math.PI * 2); octx.fill(); }
    else if (p.shape === 1) { octx.beginPath(); octx.moveTo(0, -s / 2); octx.lineTo(s / 2, s / 2); octx.lineTo(-s / 2, s / 2); octx.closePath(); octx.fill(); }
    else if (p.shape === 2) { octx.lineWidth = s * 0.18; octx.beginPath(); octx.arc(0, 0, s / 2, 0.3, Math.PI * 1.3); octx.stroke(); }
    else { octx.fillRect(-s / 2, -s * 0.12, s, s * 0.24); }
    octx.restore();
  };

  const frame = (now) => {
    const dt = last ? Math.min((now - last) / 1000, 0.05) : 0;
    last = now;
    global += 0.05 * dt;
    const t = now / 1000;

    // 素材面を再描画
    octx.clearRect(0, 0, 512, 512);
    for (const p of parts) {
      p.rot += p.rotSpd * dt;
      p.x = p.bx + Math.sin(t * p.spd + p.ph) * p.amp;
      p.y = p.by + Math.cos(t * p.spd * 0.9 + p.ph) * p.amp;
      drawParticle(p);
    }

    // 残像(clearせず半透明で塗る)
    ctx.fillStyle = 'rgba(15,17,23,0.25)';
    ctx.fillRect(0, 0, W, H);

    // 12セクターをミラー配置でスタンプ
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(global);
    const half = Math.PI / SECTORS; // 15°
    for (let i = 0; i < SECTORS; i++) {
      ctx.save();
      ctx.rotate(i * 2 * half);
      if (i % 2 === 1) ctx.scale(-1, 1);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.arc(0, 0, R, -half - 0.01, half + 0.01); // 微オーバーラップで継ぎ目消し
      ctx.closePath();
      ctx.clip();
      ctx.drawImage(off, -R, -R, 2 * R, 2 * R);
      ctx.restore();
    }
    ctx.restore();

    // 中央の白円+HOLIDAYS
    ctx.beginPath(); ctx.arc(cx, cy, 56, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill();
    ctx.fillStyle = '#0F1117';
    ctx.font = '700 16px system-ui, sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('HOLIDAYS', cx, cy);

    requestAnimationFrame(frame);
  };
  requestAnimationFrame(frame);
})();

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

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

# 追加してほしい効果
万華鏡コラージュ(Canvas エフェクト)
カラフルな図形の破片が万華鏡のように鏡像対称で回り続ける背景です。1セクターだけ描いて12回フォーミラーするだけで、キャンペーンサイトのような華やかな漫然模様が生まれます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 万華鏡コラージュ:素材1面を扇形clip+ミラーで12回スタンプし鏡像対称に回す -->
<div class="stage">
  <canvas class="kaleido" data-kaleido aria-label="万華鏡状に回り続けるコラージュ背景"></canvas>
</div>

【CSS】
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #0F1117; }

.stage {
  width: 100%;
  height: 100vh;
  min-height: 240px;
  max-height: 100%;
  overflow: hidden;
  background: #0F1117;
}
.kaleido { display: block; width: 100%; height: 100%; }

【JavaScript】
// オフスクリーンに15個の図形パーティクルを描き、メインへ12セクターのミラーclipでスタンプ。
(() => {
  const cv = document.querySelector('[data-kaleido]');
  if (!cv) return; // null安全
  const ctx = cv.getContext('2d');
  const off = document.createElement('canvas');
  off.width = off.height = 512;
  const octx = off.getContext('2d');

  const COLORS = ['#FF5C5C', '#FFC83D', '#4ED1A1', '#4D7CFE', '#9B6BFF'];
  const SECTORS = 12;
  const rand = (a, b) => a + (b - a) * Math.random();

  // 素材パーティクル15個(円/三角/円弧/細長矩形)
  const parts = [];
  for (let i = 0; i < 15; i++) {
    parts.push({
      shape: i % 4, color: COLORS[i % COLORS.length],
      size: rand(20, 90),
      bx: rand(140, 372), by: rand(140, 372),
      amp: rand(12, 30), ph: rand(0, Math.PI * 2), spd: rand(0.4, 1.0),
      rot: rand(0, Math.PI * 2), rotSpd: rand(-0.3, 0.3),
      x: 256, y: 256
    });
  }

  let dpr = 1, W = 0, H = 0, cx = 0, cy = 0, R = 0, global = 0, last = 0;
  const resize = () => {
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    W = cv.clientWidth; H = cv.clientHeight;
    cv.width = W * dpr; cv.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    cx = W / 2; cy = H / 2; R = Math.hypot(W, H) / 2 + 20;
    ctx.fillStyle = '#0F1117'; ctx.fillRect(0, 0, W, H);
  };
  window.addEventListener('resize', resize);
  resize();

  const drawParticle = (p) => {
    octx.save();
    octx.translate(p.x, p.y);
    octx.rotate(p.rot);
    octx.fillStyle = p.color; octx.strokeStyle = p.color;
    const s = p.size;
    if (p.shape === 0) { octx.beginPath(); octx.arc(0, 0, s / 2, 0, Math.PI * 2); octx.fill(); }
    else if (p.shape === 1) { octx.beginPath(); octx.moveTo(0, -s / 2); octx.lineTo(s / 2, s / 2); octx.lineTo(-s / 2, s / 2); octx.closePath(); octx.fill(); }
    else if (p.shape === 2) { octx.lineWidth = s * 0.18; octx.beginPath(); octx.arc(0, 0, s / 2, 0.3, Math.PI * 1.3); octx.stroke(); }
    else { octx.fillRect(-s / 2, -s * 0.12, s, s * 0.24); }
    octx.restore();
  };

  const frame = (now) => {
    const dt = last ? Math.min((now - last) / 1000, 0.05) : 0;
    last = now;
    global += 0.05 * dt;
    const t = now / 1000;

    // 素材面を再描画
    octx.clearRect(0, 0, 512, 512);
    for (const p of parts) {
      p.rot += p.rotSpd * dt;
      p.x = p.bx + Math.sin(t * p.spd + p.ph) * p.amp;
      p.y = p.by + Math.cos(t * p.spd * 0.9 + p.ph) * p.amp;
      drawParticle(p);
    }

    // 残像(clearせず半透明で塗る)
    ctx.fillStyle = 'rgba(15,17,23,0.25)';
    ctx.fillRect(0, 0, W, H);

    // 12セクターをミラー配置でスタンプ
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(global);
    const half = Math.PI / SECTORS; // 15°
    for (let i = 0; i < SECTORS; i++) {
      ctx.save();
      ctx.rotate(i * 2 * half);
      if (i % 2 === 1) ctx.scale(-1, 1);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.arc(0, 0, R, -half - 0.01, half + 0.01); // 微オーバーラップで継ぎ目消し
      ctx.closePath();
      ctx.clip();
      ctx.drawImage(off, -R, -R, 2 * R, 2 * R);
      ctx.restore();
    }
    ctx.restore();

    // 中央の白円+HOLIDAYS
    ctx.beginPath(); ctx.arc(cx, cy, 56, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill();
    ctx.fillStyle = '#0F1117';
    ctx.font = '700 16px system-ui, sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('HOLIDAYS', cx, cy);

    requestAnimationFrame(frame);
  };
  requestAnimationFrame(frame);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。