弾力ジェリーカーソル

移動速度から伸び量と角度を計算し、進行方向へ伸縮する粘性カーソル。体積保存風のscaleで“びよん”とした有機的な動きを表現します。

#js#animation#transform#physics

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk: SaaS機能紹介1画面。弾力ジェリーカーソルを主役に -->
<div class="fd" data-jelly-root>
  <header class="fd__bar">
    <span class="fd__logo"><span class="fd__mark"></span>FlowDesk</span>
    <nav class="fd__nav">
      <a href="#">機能</a>
      <a href="#">料金</a>
      <a href="#">ブログ</a>
    </nav>
  </header>

  <section class="fd__body">
    <div class="fd__intro">
      <p class="fd__kicker">FEATURES</p>
      <h1 class="fd__title">動きで、<br>使い心地が変わる。</h1>
      <p class="fd__lead">タスク・通知・対話をひとつに。<br>触れるほど馴染む、なめらかな操作感。</p>
    </div>
    <ul class="fd__cards">
      <li class="fd__card"><span class="fd__ico">⚡</span>高速タスク</li>
      <li class="fd__card"><span class="fd__ico">🔔</span>賢い通知</li>
      <li class="fd__card"><span class="fd__ico">📊</span>分析ボード</li>
      <li class="fd__card"><span class="fd__ico">🔗</span>連携豊富</li>
    </ul>
  </section>

  <p class="fd__hint">カーソルが進む方向へ“びよん”と伸びます</p>

  <!-- 主役: 弾力ジェリーカーソル -->
  <div class="jelly-cursor" data-jelly></div>
</div>
CSS
/* FlowDesk SaaSテーマ: 紺/青/白 */
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(circle at 82% 16%, rgba(79,124,255,.28) 0%, transparent 44%),
    radial-gradient(circle at 8% 90%, rgba(79,124,255,.14) 0%, transparent 48%),
    #0f1b34;
  color: #fff;
  overflow: hidden;
  cursor: none;
}

.fd {
  position: relative;
  height: 400px;
  display: flex;
  flex-direction: column;
  padding: 0 30px;
}

/* ヘッダー */
.fd__bar {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 15px 0;
  font-size: 14px;
}
.fd__logo {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-weight: 800;
  font-size: 16px;
}
.fd__mark {
  width: 18px;
  height: 18px;
  border-radius: 6px;
  background: linear-gradient(135deg, #4f7cff, #8fb0ff);
  box-shadow: 0 0 14px rgba(79,124,255,.6);
}
.fd__nav {
  display: flex;
  gap: 20px;
  margin-left: auto;
}
.fd__nav a { color: rgba(255,255,255,.72); text-decoration: none; }

/* 本文 */
.fd__body {
  flex: 1;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 26px;
  align-items: center;
}
.fd__kicker {
  margin: 0 0 8px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .22em;
  color: #8fb0ff;
}
.fd__title {
  margin: 0 0 12px;
  font-size: 28px;
  line-height: 1.3;
  font-weight: 800;
}
.fd__lead {
  margin: 0;
  font-size: 13px;
  line-height: 1.8;
  color: rgba(255,255,255,.66);
}

/* 機能カード */
.fd__cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.fd__card {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 14px;
  font-size: 14px;
  font-weight: 700;
  background: rgba(255,255,255,.06);
  border: 1px solid rgba(255,255,255,.12);
  border-radius: 14px;
}
.fd__ico { font-size: 20px; }

.fd__hint {
  margin: 0 0 14px;
  text-align: center;
  font-size: 12px;
  letter-spacing: .04em;
  color: rgba(255,255,255,.42);
}

/* 主役: 弾力ジェリーカーソル */
.jelly-cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #aac4ff, #4f7cff 70%);
  box-shadow: 0 0 18px rgba(79,124,255,.6);
  pointer-events: none;
  z-index: 50;
  opacity: 0;
  transition: opacity .3s ease;
}
.fd.is-active .jelly-cursor { opacity: 1; }
JavaScript
// FlowDesk: 弾力ジェリーカーソル。待機中は自動巡回で伸縮し、操作で本物に追従
(() => {
  const root = document.querySelector('[data-jelly-root]');
  const jelly = document.querySelector('[data-jelly]');
  if (!root || !jelly) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 現在位置・目標位置・前フレーム位置
  let x = window.innerWidth / 2, y = window.innerHeight / 2;
  let tx = x, ty = y;
  let px = x, py = y;
  let usePointer = false;
  let lastMove = 0;
  const IDLE = 1500;

  root.classList.add('is-active');

  root.addEventListener('pointermove', (e) => {
    usePointer = true;
    lastMove = performance.now();
    tx = e.clientX; ty = e.clientY;
  });
  root.addEventListener('pointerleave', () => { usePointer = false; });

  // 仮想カーソルの自動経路: 機能カード帯を横切るように動く
  const autoPos = (t) => {
    const r = root.getBoundingClientRect();
    const cx = r.left + r.width / 2;
    const cy = r.top + r.height * 0.5;
    return {
      x: cx + Math.sin(t * 0.0011) * r.width * 0.34,
      y: cy + Math.sin(t * 0.0017 + 1.0) * r.height * 0.22,
    };
  };

  const ease = reduce ? 1 : 0.2;
  const tick = (now) => {
    if (usePointer && now - lastMove > IDLE) usePointer = false;
    if (!usePointer) {
      const p = autoPos(now);
      tx = p.x; ty = p.y;
    }

    // 目標へ補間
    x += (tx - x) * ease;
    y += (ty - y) * ease;

    // 速度から伸びと回転を算出
    const vx = x - px;
    const vy = y - py;
    px = x; py = y;

    const speed = Math.min(Math.hypot(vx, vy), 80);
    const angle = Math.atan2(vy, vx) * (180 / Math.PI);
    const stretch = reduce ? 0 : speed / 120;

    const sx = 1 + stretch;
    const sy = 1 - stretch * 0.6;

    // CSSで中心合わせ済みなので位置はtranslateのみ
    jelly.style.transform =
      `translate(${x}px, ${y}px) rotate(${angle}deg) scale(${sx}, ${sy})`;

    requestAnimationFrame(tick);
  };
  requestAnimationFrame(tick);
})();

コード

HTML
<!-- 弾力ジェリーカーソル:速度に応じて伸び縮みする粘性カーソル -->
<div class="stage" data-jelly-root>
  <div class="content">
    <h1 class="title">ジェリーカーソル</h1>
    <p class="lead">素早く動かすと、進行方向に“びよん”と伸びます。</p>
    <div class="dots">
      <span></span><span></span><span></span>
    </div>
  </div>

  <!-- 弾力カーソル -->
  <div class="jelly" data-jelly></div>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }

.stage {
  position: relative;
  height: 360px;
  display: grid;
  place-items: center;
  overflow: hidden;
  background:
    radial-gradient(600px 360px at 30% 20%, #103b3a 0%, transparent 60%),
    radial-gradient(600px 360px at 80% 90%, #2a1140 0%, transparent 60%),
    #0a0d12;
  color: #eafff8;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  cursor: none;
}

.content { text-align: center; padding: 24px; z-index: 1; }
.title {
  margin: 0 0 10px;
  font-size: clamp(30px, 6vw, 48px);
  font-weight: 800;
  letter-spacing: .03em;
  background: linear-gradient(90deg, #6df0c2, #58c8ff);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.lead { margin: 0 0 22px; color: #9fc4bd; font-size: 14px; }

.dots { display: flex; gap: 14px; justify-content: center; }
.dots span {
  width: 12px; height: 12px;
  border-radius: 50%;
  background: rgba(109,240,194,.5);
  animation: pulse 1.8s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: .25s; }
.dots span:nth-child(3) { animation-delay: .5s; }
@keyframes pulse {
  0%, 100% { transform: scale(1); opacity: .5; }
  50% { transform: scale(1.5); opacity: 1; }
}

/* ジェリーカーソル本体:rotate+scaleで伸縮 */
.jelly {
  position: fixed;
  top: 0; left: 0;
  width: 34px; height: 34px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #aeffe0, #2fd6a6 60%, #18b48a);
  box-shadow: 0 0 20px rgba(47,214,166,.6);
  pointer-events: none;
  z-index: 9999;
  will-change: transform;
  opacity: 0; /* 初回移動まで非表示(中央の文字に重ならない) */
  transition: opacity .3s ease;
}
[data-jelly-root].is-active .jelly { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .dots span { animation: none; }
}
JavaScript
// 弾力ジェリーカーソル:速度ベクトルから伸び量と角度を求め、scale/rotateで変形
(() => {
  const root = document.querySelector('[data-jelly-root]');
  const jelly = document.querySelector('[data-jelly]');
  if (!root || !jelly) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 現在位置・目標位置・前フレーム位置
  let x = window.innerWidth / 2, y = window.innerHeight / 2;
  let tx = x, ty = y;
  let px = x, py = y;

  root.addEventListener('pointermove', (e) => {
    tx = e.clientX;
    ty = e.clientY;
    if (!root.classList.contains('is-active')) root.classList.add('is-active');
  });
  root.addEventListener('pointerleave', () => root.classList.remove('is-active'));

  const ease = reduce ? 1 : 0.2;
  const tick = () => {
    // 目標へ補間して滑らかに追従
    x += (tx - x) * ease;
    y += (ty - y) * ease;

    // 速度(移動量)から伸びと回転を算出
    const vx = x - px;
    const vy = y - py;
    px = x; py = y;

    const speed = Math.min(Math.hypot(vx, vy), 80); // 上限でクランプ
    const angle = Math.atan2(vy, vx) * (180 / Math.PI);
    const stretch = reduce ? 0 : speed / 120; // 0〜0.66程度

    // 進行方向に伸ばし、直交方向に縮める(体積保存風)
    const sx = 1 + stretch;
    const sy = 1 - stretch * 0.6;

    jelly.style.transform =
      `translate(${x}px, ${y}px) translate(-50%, -50%) ` +
      `rotate(${angle}deg) scale(${sx}, ${sy})`;

    requestAnimationFrame(tick);
  };
  requestAnimationFrame(tick);
})();

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

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

# 追加してほしい効果
弾力ジェリーカーソル(カスタムカーソル)
移動速度から伸び量と角度を計算し、進行方向へ伸縮する粘性カーソル。体積保存風のscaleで“びよん”とした有機的な動きを表現します。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 弾力ジェリーカーソル:速度に応じて伸び縮みする粘性カーソル -->
<div class="stage" data-jelly-root>
  <div class="content">
    <h1 class="title">ジェリーカーソル</h1>
    <p class="lead">素早く動かすと、進行方向に“びよん”と伸びます。</p>
    <div class="dots">
      <span></span><span></span><span></span>
    </div>
  </div>

  <!-- 弾力カーソル -->
  <div class="jelly" data-jelly></div>
</div>

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

.stage {
  position: relative;
  height: 360px;
  display: grid;
  place-items: center;
  overflow: hidden;
  background:
    radial-gradient(600px 360px at 30% 20%, #103b3a 0%, transparent 60%),
    radial-gradient(600px 360px at 80% 90%, #2a1140 0%, transparent 60%),
    #0a0d12;
  color: #eafff8;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  cursor: none;
}

.content { text-align: center; padding: 24px; z-index: 1; }
.title {
  margin: 0 0 10px;
  font-size: clamp(30px, 6vw, 48px);
  font-weight: 800;
  letter-spacing: .03em;
  background: linear-gradient(90deg, #6df0c2, #58c8ff);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.lead { margin: 0 0 22px; color: #9fc4bd; font-size: 14px; }

.dots { display: flex; gap: 14px; justify-content: center; }
.dots span {
  width: 12px; height: 12px;
  border-radius: 50%;
  background: rgba(109,240,194,.5);
  animation: pulse 1.8s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: .25s; }
.dots span:nth-child(3) { animation-delay: .5s; }
@keyframes pulse {
  0%, 100% { transform: scale(1); opacity: .5; }
  50% { transform: scale(1.5); opacity: 1; }
}

/* ジェリーカーソル本体:rotate+scaleで伸縮 */
.jelly {
  position: fixed;
  top: 0; left: 0;
  width: 34px; height: 34px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #aeffe0, #2fd6a6 60%, #18b48a);
  box-shadow: 0 0 20px rgba(47,214,166,.6);
  pointer-events: none;
  z-index: 9999;
  will-change: transform;
  opacity: 0; /* 初回移動まで非表示(中央の文字に重ならない) */
  transition: opacity .3s ease;
}
[data-jelly-root].is-active .jelly { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .dots span { animation: none; }
}

【JavaScript】
// 弾力ジェリーカーソル:速度ベクトルから伸び量と角度を求め、scale/rotateで変形
(() => {
  const root = document.querySelector('[data-jelly-root]');
  const jelly = document.querySelector('[data-jelly]');
  if (!root || !jelly) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 現在位置・目標位置・前フレーム位置
  let x = window.innerWidth / 2, y = window.innerHeight / 2;
  let tx = x, ty = y;
  let px = x, py = y;

  root.addEventListener('pointermove', (e) => {
    tx = e.clientX;
    ty = e.clientY;
    if (!root.classList.contains('is-active')) root.classList.add('is-active');
  });
  root.addEventListener('pointerleave', () => root.classList.remove('is-active'));

  const ease = reduce ? 1 : 0.2;
  const tick = () => {
    // 目標へ補間して滑らかに追従
    x += (tx - x) * ease;
    y += (ty - y) * ease;

    // 速度(移動量)から伸びと回転を算出
    const vx = x - px;
    const vy = y - py;
    px = x; py = y;

    const speed = Math.min(Math.hypot(vx, vy), 80); // 上限でクランプ
    const angle = Math.atan2(vy, vx) * (180 / Math.PI);
    const stretch = reduce ? 0 : speed / 120; // 0〜0.66程度

    // 進行方向に伸ばし、直交方向に縮める(体積保存風)
    const sx = 1 + stretch;
    const sy = 1 - stretch * 0.6;

    jelly.style.transform =
      `translate(${x}px, ${y}px) translate(-50%, -50%) ` +
      `rotate(${angle}deg) scale(${sx}, ${sy})`;

    requestAnimationFrame(tick);
  };
  requestAnimationFrame(tick);
})();

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

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