スクロール速度スキュー

rAFでスクロール速度を算出し、速くスクロールするほど要素をskewY/scaleYで歪ませます。手を止めると線形補間でなめらかに元へ戻る慣性演出。

#scroll#velocity#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

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

HTML
<!-- FlowDesk リリースノート。素早くスクロールすると各更新カードが歪む -->
<div class="fdv-scroller" id="vskScroller">
  <header class="fdv-hero">
    <span class="fdv-kicker">CHANGELOG</span>
    <h1>FlowDesk 更新履歴</h1>
    <p class="fdv-lead">最新リリースのハイライト。<br>勢いよくスクロールしてみてください。</p>
    <span class="fdv-arrow" aria-hidden="true">&#8595;</span>
  </header>

  <!-- これらの更新カードが速度に応じて歪む -->
  <section class="vsk-list">
    <article class="fdv-card" style="--h:#4f7cff">
      <span class="fdv-ver">v3.8</span>
      <span class="fdv-date">2026.06.01</span>
      <span class="fdv-tag fdv-new">新機能</span>
      <h2>AIタスク要約</h2>
      <p>長い案件スレッドをワンクリックで要約。要点と次のアクションを自動抽出します。</p>
    </article>
    <article class="fdv-card" style="--h:#22d3ee">
      <span class="fdv-ver">v3.7</span>
      <span class="fdv-date">2026.05.18</span>
      <span class="fdv-tag fdv-imp">改善</span>
      <h2>ダッシュボード高速化</h2>
      <p>初回表示を最大40%高速化。大量カードでもスクロールがなめらかになりました。</p>
    </article>
    <article class="fdv-card" style="--h:#a78bfa">
      <span class="fdv-ver">v3.6</span>
      <span class="fdv-date">2026.05.02</span>
      <span class="fdv-tag fdv-new">新機能</span>
      <h2>カレンダー連携</h2>
      <p>外部カレンダーと双方向同期。期限の変更が自動で反映されます。</p>
    </article>
    <article class="fdv-card" style="--h:#34d399">
      <span class="fdv-ver">v3.5</span>
      <span class="fdv-date">2026.04.20</span>
      <span class="fdv-tag fdv-fix">修正</span>
      <h2>通知の重複を解消</h2>
      <p>特定条件で通知が二重に届く不具合を修正しました。</p>
    </article>
    <article class="fdv-card" style="--h:#f472b6">
      <span class="fdv-ver">v3.4</span>
      <span class="fdv-date">2026.04.06</span>
      <span class="fdv-tag fdv-imp">改善</span>
      <h2>権限設定UIを刷新</h2>
      <p>ロールごとの権限がツリー表示に。設定ミスが起きにくくなりました。</p>
    </article>
  </section>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
  --white: #ffffff;
}

body {
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
  background: var(--navy);
  color: var(--white);
  -webkit-font-smoothing: antialiased;
}

/* 内部スクロール領域。ここで速度を測る */
.fdv-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--blue) transparent;
}

.fdv-hero {
  height: 240px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  text-align: center;
  padding: 0 20px;
  background:
    radial-gradient(circle at 20% 20%, rgba(79,124,255,.4), transparent 55%),
    radial-gradient(circle at 85% 85%, rgba(79,124,255,.22), transparent 55%),
    var(--navy);
}
.fdv-kicker { letter-spacing: .3em; font-size: .64rem; color: #9db5ff; }
.fdv-hero h1 { font-size: 1.9rem; font-weight: 800; line-height: 1.25; }
.fdv-lead { font-size: .82rem; line-height: 1.65; color: #aeb9da; }
.fdv-arrow { font-size: 1.4rem; color: var(--blue); animation: fdvBob 1.6s ease-in-out infinite; }
@keyframes fdvBob {
  0%, 100% { transform: translateY(0); opacity: .6; }
  50% { transform: translateY(8px); opacity: 1; }
}

/* JSが書き込む歪み量の受け皿。デフォは無歪み */
.vsk-list {
  max-width: 540px;
  margin: 0 auto;
  padding: 30px 24px 110px;
  display: grid;
  gap: 18px;
  --skew: 0deg;
  --stretch: 1;
}
.fdv-card {
  position: relative;
  padding: 20px 20px 20px 22px;
  border-radius: 16px;
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.09);
  border-left: 4px solid var(--h, var(--blue));
  box-shadow: 0 16px 38px -26px rgba(0,0,0,.9);
  /* 速度に応じた歪み。原点を上にして伸び感を出す */
  transform-origin: center top;
  transform: skewY(var(--skew)) scaleY(var(--stretch));
  will-change: transform;
}
.fdv-ver {
  font-weight: 800;
  font-size: 1rem;
  color: var(--h, var(--blue));
  margin-right: 8px;
}
.fdv-date { font-size: .72rem; color: #8595c0; }
.fdv-tag {
  float: right;
  font-size: .62rem;
  letter-spacing: .06em;
  padding: 3px 9px;
  border-radius: 999px;
}
.fdv-new { background: rgba(79,124,255,.2); color: #aac4ff; }
.fdv-imp { background: rgba(52,211,153,.18); color: #8df0c4; }
.fdv-fix { background: rgba(244,114,182,.18); color: #ffb4d6; }
.fdv-card h2 { font-size: 1.05rem; font-weight: 800; margin: 10px 0 6px; }
.fdv-card p { font-size: .86rem; line-height: 1.7; color: #c0c8e4; }

/* 動きを減らす設定では歪みを無効化 */
@media (prefers-reduced-motion: reduce) {
  .fdv-card { transform: none !important; }
  .fdv-arrow { animation: none; }
}
JavaScript
// FlowDesk リリースノート:スクロール速度を rAF で算出し、skewY / scaleY に反映。止まると戻る。
(() => {
  const scroller = document.getElementById('vskScroller');
  const list = document.querySelector('.vsk-list');
  if (!scroller || !list) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return; // 動きを減らす設定では何もしない

  // チューニング用パラメータ
  const SKEW_MAX = 7;       // 最大スキュー角(度)
  const STRETCH_MAX = 0.12; // 最大の縦伸び量
  const SKEW_GAIN = 0.06;   // 速度→スキューの感度
  const EASE = 0.12;        // 復帰の補間係数

  let lastTop = scroller.scrollTop;
  let velocity = 0; // 直近フレームの速度(px/frame)
  let skew = 0;     // 現在の表示スキュー
  let stretch = 1;  // 現在の縦スケール
  let ticking = false;

  const clamp = (v, min, max) => Math.max(min, Math.min(max, v));

  function loop() {
    // スクロール差分=速度
    const top = scroller.scrollTop;
    velocity = top - lastTop;
    lastTop = top;

    // 速度から目標の歪みを決定(符号で傾く向きが変わる)
    const targetSkew = clamp(velocity * SKEW_GAIN, -SKEW_MAX, SKEW_MAX);
    const targetStretch = 1 + clamp(Math.abs(velocity) * SKEW_GAIN * 0.02, 0, STRETCH_MAX);

    // 線形補間でなめらかに追従&復帰
    skew += (targetSkew - skew) * EASE;
    stretch += (targetStretch - stretch) * EASE;

    // 微小値はゼロに丸めて無駄な描画を抑える
    if (Math.abs(skew) < 0.01) skew = 0;

    list.style.setProperty('--skew', skew.toFixed(3) + 'deg');
    list.style.setProperty('--stretch', stretch.toFixed(4));

    // ほぼ静止したらループ停止。スクロールで再開
    if (Math.abs(velocity) < 0.05 && Math.abs(skew) < 0.02 && Math.abs(stretch - 1) < 0.001) {
      ticking = false;
      return;
    }
    requestAnimationFrame(loop);
  }

  function start() {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(loop);
  }

  scroller.addEventListener('scroll', start, { passive: true });

  // 操作前でも演出が伝わるよう、最初だけ軽く自動スクロールして見せる
  let auto = true;
  const stopAuto = () => { auto = false; };
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
    scroller.addEventListener(ev, stopAuto, { passive: true }));

  setTimeout(function demo() {
    if (!auto) return;
    const max = scroller.scrollHeight - scroller.clientHeight;
    if (scroller.scrollTop >= max - 1) return; // 最下部で停止
    scroller.scrollTop += 6; // やや速めに動かして歪みを見せる
    requestAnimationFrame(demo);
  }, 800);
})();

コード

HTML
<!-- スクロール速度スキュー。内部の縦スクロール領域で速度を測る -->
<div class="vsk-scroller" id="vskScroller">
  <header class="vsk-hero">
    <p class="vsk-kicker">VELOCITY SKEW</p>
    <h1>速くスクロールすると<br>歪む</h1>
    <p class="vsk-lead">スクロール速度を rAF で算出し<br>skewY と scaleY に反映。止まると戻る</p>
    <span class="vsk-arrow" aria-hidden="true">&#8595;</span>
  </header>

  <!-- これらのカードが速度に応じて歪む -->
  <section class="vsk-list">
    <article class="vsk-card" style="--h:#f97316">
      <span class="vsk-num">01</span>
      <h2>慣性のある質感</h2>
      <p>勢いよくスクロールするほど大きく傾き、伸びます。</p>
    </article>
    <article class="vsk-card" style="--h:#22d3ee">
      <span class="vsk-num">02</span>
      <h2>rAFで速度算出</h2>
      <p>毎フレームのスクロール差分から速度を求めています。</p>
    </article>
    <article class="vsk-card" style="--h:#a78bfa">
      <span class="vsk-num">03</span>
      <h2>イージング復帰</h2>
      <p>手を止めると線形補間でゆっくり元の形へ戻ります。</p>
    </article>
    <article class="vsk-card" style="--h:#34d399">
      <span class="vsk-num">04</span>
      <h2>軽量実装</h2>
      <p>transform変更のみ。再レイアウトを起こしません。</p>
    </article>
    <article class="vsk-card" style="--h:#f472b6">
      <span class="vsk-num">05</span>
      <h2>体感のリッチさ</h2>
      <p>わずかな歪みでもスクロールが生き生きと感じられます。</p>
    </article>
  </section>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --bg: #0a0a12;
  --ink: #f3f4ff;
  --accent: #f97316;
}

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

/* 内部スクロール領域。ここで速度を測る */
.vsk-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
}

.vsk-hero {
  height: 260px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  text-align: center;
  background:
    radial-gradient(circle at 20% 20%, rgba(249,115,22,.4), transparent 55%),
    radial-gradient(circle at 85% 80%, rgba(167,139,250,.34), transparent 55%),
    var(--bg);
}
.vsk-kicker { letter-spacing: .34em; font-size: .68rem; color: var(--accent); }
.vsk-hero h1 { font-size: 2.1rem; font-weight: 800; line-height: 1.2; }
.vsk-lead { font-size: .86rem; line-height: 1.6; color: #bcc0e0; }
.vsk-arrow { font-size: 1.5rem; animation: vskBob 1.6s ease-in-out infinite; }
@keyframes vskBob {
  0%, 100% { transform: translateY(0); opacity: .6; }
  50% { transform: translateY(8px); opacity: 1; }
}

.vsk-list {
  max-width: 540px;
  margin: 0 auto;
  padding: 30px 24px 110px;
  display: grid;
  gap: 20px;
  /* JSが書き込む歪み量。デフォは無歪み */
  --skew: 0deg;
  --stretch: 1;
}
.vsk-card {
  position: relative;
  padding: 24px 24px 24px 62px;
  border-radius: 16px;
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.09);
  border-left: 4px solid var(--h, var(--accent));
  box-shadow: 0 16px 38px -26px rgba(0,0,0,.9);
  /* 速度に応じた歪み。原点を上にして伸び感を出す */
  transform-origin: center top;
  transform: skewY(var(--skew)) scaleY(var(--stretch));
  will-change: transform;
}
.vsk-num {
  position: absolute;
  left: 22px; top: 24px;
  font-weight: 800;
  font-size: 1.05rem;
  color: var(--h, var(--accent));
}
.vsk-card h2 { font-size: 1.08rem; margin-bottom: 6px; }
.vsk-card p { font-size: .9rem; line-height: 1.6; color: #c5c8e6; }

/* 動きを減らす設定では歪みを無効化 */
@media (prefers-reduced-motion: reduce) {
  .vsk-card { transform: none !important; }
  .vsk-arrow { animation: none; }
}
JavaScript
// スクロール速度を rAF で算出し、skewY / scaleY に反映。止まると戻る。
(() => {
  const scroller = document.getElementById('vskScroller');
  const list = document.querySelector('.vsk-list');
  if (!scroller || !list) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return; // 動きを減らす設定では何もしない

  // チューニング用パラメータ
  const SKEW_MAX = 7;     // 最大スキュー角(度)
  const STRETCH_MAX = 0.12; // 最大の縦伸び量
  const SKEW_GAIN = 0.06; // 速度→スキューの感度
  const EASE = 0.12;      // 復帰の補間係数

  let lastTop = scroller.scrollTop;
  let velocity = 0;   // 直近フレームの速度(px/frame)
  let skew = 0;       // 現在の表示スキュー
  let stretch = 1;    // 現在の縦スケール
  let ticking = false;

  const clamp = (v, min, max) => Math.max(min, Math.min(max, v));

  function loop() {
    // スクロール差分=速度
    const top = scroller.scrollTop;
    velocity = top - lastTop;
    lastTop = top;

    // 速度から目標の歪みを決定(符号で傾く向きが変わる)
    const targetSkew = clamp(velocity * SKEW_GAIN, -SKEW_MAX, SKEW_MAX);
    const targetStretch = 1 + clamp(Math.abs(velocity) * SKEW_GAIN * 0.02, 0, STRETCH_MAX);

    // 線形補間でなめらかに追従&復帰
    skew += (targetSkew - skew) * EASE;
    stretch += (targetStretch - stretch) * EASE;

    // 微小値はゼロに丸めて無駄な描画を抑える
    if (Math.abs(skew) < 0.01) skew = 0;

    list.style.setProperty('--skew', skew.toFixed(3) + 'deg');
    list.style.setProperty('--stretch', stretch.toFixed(4));

    // ほぼ静止したらループ停止。スクロールで再開
    if (Math.abs(velocity) < 0.05 && Math.abs(skew) < 0.02 && Math.abs(stretch - 1) < 0.001) {
      ticking = false;
      return;
    }
    requestAnimationFrame(loop);
  }

  function start() {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(loop);
  }

  scroller.addEventListener('scroll', start, { passive: true });

  // 操作前でも演出が伝わるよう、最初だけ軽く自動スクロールして見せる
  let auto = true;
  const stopAuto = () => { auto = false; };
  scroller.addEventListener('wheel', stopAuto, { passive: true });
  scroller.addEventListener('touchstart', stopAuto, { passive: true });
  scroller.addEventListener('pointerdown', stopAuto);

  setTimeout(function demo() {
    if (!auto) return;
    const max = scroller.scrollHeight - scroller.clientHeight;
    if (scroller.scrollTop >= max - 1) return; // 最下部で停止
    scroller.scrollTop += 6; // やや速めに動かして歪みを見せる
    requestAnimationFrame(demo);
  }, 800);
})();

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

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

# 追加してほしい効果
スクロール速度スキュー(スクロール演出)
rAFでスクロール速度を算出し、速くスクロールするほど要素をskewY/scaleYで歪ませます。手を止めると線形補間でなめらかに元へ戻る慣性演出。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- スクロール速度スキュー。内部の縦スクロール領域で速度を測る -->
<div class="vsk-scroller" id="vskScroller">
  <header class="vsk-hero">
    <p class="vsk-kicker">VELOCITY SKEW</p>
    <h1>速くスクロールすると<br>歪む</h1>
    <p class="vsk-lead">スクロール速度を rAF で算出し<br>skewY と scaleY に反映。止まると戻る</p>
    <span class="vsk-arrow" aria-hidden="true">&#8595;</span>
  </header>

  <!-- これらのカードが速度に応じて歪む -->
  <section class="vsk-list">
    <article class="vsk-card" style="--h:#f97316">
      <span class="vsk-num">01</span>
      <h2>慣性のある質感</h2>
      <p>勢いよくスクロールするほど大きく傾き、伸びます。</p>
    </article>
    <article class="vsk-card" style="--h:#22d3ee">
      <span class="vsk-num">02</span>
      <h2>rAFで速度算出</h2>
      <p>毎フレームのスクロール差分から速度を求めています。</p>
    </article>
    <article class="vsk-card" style="--h:#a78bfa">
      <span class="vsk-num">03</span>
      <h2>イージング復帰</h2>
      <p>手を止めると線形補間でゆっくり元の形へ戻ります。</p>
    </article>
    <article class="vsk-card" style="--h:#34d399">
      <span class="vsk-num">04</span>
      <h2>軽量実装</h2>
      <p>transform変更のみ。再レイアウトを起こしません。</p>
    </article>
    <article class="vsk-card" style="--h:#f472b6">
      <span class="vsk-num">05</span>
      <h2>体感のリッチさ</h2>
      <p>わずかな歪みでもスクロールが生き生きと感じられます。</p>
    </article>
  </section>
</div>

【CSS】
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --bg: #0a0a12;
  --ink: #f3f4ff;
  --accent: #f97316;
}

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

/* 内部スクロール領域。ここで速度を測る */
.vsk-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
}

.vsk-hero {
  height: 260px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  text-align: center;
  background:
    radial-gradient(circle at 20% 20%, rgba(249,115,22,.4), transparent 55%),
    radial-gradient(circle at 85% 80%, rgba(167,139,250,.34), transparent 55%),
    var(--bg);
}
.vsk-kicker { letter-spacing: .34em; font-size: .68rem; color: var(--accent); }
.vsk-hero h1 { font-size: 2.1rem; font-weight: 800; line-height: 1.2; }
.vsk-lead { font-size: .86rem; line-height: 1.6; color: #bcc0e0; }
.vsk-arrow { font-size: 1.5rem; animation: vskBob 1.6s ease-in-out infinite; }
@keyframes vskBob {
  0%, 100% { transform: translateY(0); opacity: .6; }
  50% { transform: translateY(8px); opacity: 1; }
}

.vsk-list {
  max-width: 540px;
  margin: 0 auto;
  padding: 30px 24px 110px;
  display: grid;
  gap: 20px;
  /* JSが書き込む歪み量。デフォは無歪み */
  --skew: 0deg;
  --stretch: 1;
}
.vsk-card {
  position: relative;
  padding: 24px 24px 24px 62px;
  border-radius: 16px;
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.09);
  border-left: 4px solid var(--h, var(--accent));
  box-shadow: 0 16px 38px -26px rgba(0,0,0,.9);
  /* 速度に応じた歪み。原点を上にして伸び感を出す */
  transform-origin: center top;
  transform: skewY(var(--skew)) scaleY(var(--stretch));
  will-change: transform;
}
.vsk-num {
  position: absolute;
  left: 22px; top: 24px;
  font-weight: 800;
  font-size: 1.05rem;
  color: var(--h, var(--accent));
}
.vsk-card h2 { font-size: 1.08rem; margin-bottom: 6px; }
.vsk-card p { font-size: .9rem; line-height: 1.6; color: #c5c8e6; }

/* 動きを減らす設定では歪みを無効化 */
@media (prefers-reduced-motion: reduce) {
  .vsk-card { transform: none !important; }
  .vsk-arrow { animation: none; }
}

【JavaScript】
// スクロール速度を rAF で算出し、skewY / scaleY に反映。止まると戻る。
(() => {
  const scroller = document.getElementById('vskScroller');
  const list = document.querySelector('.vsk-list');
  if (!scroller || !list) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return; // 動きを減らす設定では何もしない

  // チューニング用パラメータ
  const SKEW_MAX = 7;     // 最大スキュー角(度)
  const STRETCH_MAX = 0.12; // 最大の縦伸び量
  const SKEW_GAIN = 0.06; // 速度→スキューの感度
  const EASE = 0.12;      // 復帰の補間係数

  let lastTop = scroller.scrollTop;
  let velocity = 0;   // 直近フレームの速度(px/frame)
  let skew = 0;       // 現在の表示スキュー
  let stretch = 1;    // 現在の縦スケール
  let ticking = false;

  const clamp = (v, min, max) => Math.max(min, Math.min(max, v));

  function loop() {
    // スクロール差分=速度
    const top = scroller.scrollTop;
    velocity = top - lastTop;
    lastTop = top;

    // 速度から目標の歪みを決定(符号で傾く向きが変わる)
    const targetSkew = clamp(velocity * SKEW_GAIN, -SKEW_MAX, SKEW_MAX);
    const targetStretch = 1 + clamp(Math.abs(velocity) * SKEW_GAIN * 0.02, 0, STRETCH_MAX);

    // 線形補間でなめらかに追従&復帰
    skew += (targetSkew - skew) * EASE;
    stretch += (targetStretch - stretch) * EASE;

    // 微小値はゼロに丸めて無駄な描画を抑える
    if (Math.abs(skew) < 0.01) skew = 0;

    list.style.setProperty('--skew', skew.toFixed(3) + 'deg');
    list.style.setProperty('--stretch', stretch.toFixed(4));

    // ほぼ静止したらループ停止。スクロールで再開
    if (Math.abs(velocity) < 0.05 && Math.abs(skew) < 0.02 && Math.abs(stretch - 1) < 0.001) {
      ticking = false;
      return;
    }
    requestAnimationFrame(loop);
  }

  function start() {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(loop);
  }

  scroller.addEventListener('scroll', start, { passive: true });

  // 操作前でも演出が伝わるよう、最初だけ軽く自動スクロールして見せる
  let auto = true;
  const stopAuto = () => { auto = false; };
  scroller.addEventListener('wheel', stopAuto, { passive: true });
  scroller.addEventListener('touchstart', stopAuto, { passive: true });
  scroller.addEventListener('pointerdown', stopAuto);

  setTimeout(function demo() {
    if (!auto) return;
    const max = scroller.scrollHeight - scroller.clientHeight;
    if (scroller.scrollTop >= max - 1) return; // 最下部で停止
    scroller.scrollTop += 6; // やや速めに動かして歪みを見せる
    requestAnimationFrame(demo);
  }, 800);
})();

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

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