HOME / BLOG
BLOG

JavaScriptでバスケットボールをバウンドさせる

デフォルト画像

JavaScriptとCSSアニメーションを組み合わせると、短いコードでも「動くミニデモ」を作れます。今回は、バスケットボールが床に当たってバウンドする表現を作ります。

アニメーションの本体はCSSの@keyframesで作り、JavaScriptではボタンを押した時にバウンド速度を変えるだけにします。CodePenへ貼って動かせるコードも用意しました。

作るもの

  • バスケットボールが上下にバウンドするデモ
  • 床に近い時だけボールが少し潰れる表現
  • 影の大きさと透明度がボールの高さに合わせて変わる表現
  • Slow / Normal / Fastボタンで速度を切り替える操作
  • prefers-reduced-motionで動きを減らす設定にも対応

実装の考え方

ボールの上下移動はtransform: translateY()で表現します。床に当たる瞬間だけscale(1.08, .92)のように横へ少し広げ、縦へ少し潰すと、軽い弾性が出ます。

@keyframes bounce {
  0%, 100% {
    transform: translate(-50%, 0) scale(1.08, .92);
  }
  55% {
    transform: translate(-50%, -250px) scale(1);
  }
}

影はボールとは逆に、高く跳ねている時ほど小さく薄くします。これだけで、床との距離がかなり分かりやすくなります。

CodePen埋め込み用エリア

JavaScriptでバスケットボールをバウンドさせる
ここにCodePenの埋め込みタグを貼り付けます。CodePenでPenを作成後、Embedコードをこのブロックと差し替えてください。

CodePenでPenを作ったら、上の枠をCodePenの埋め込みタグに差し替えてください。下のコードはHTML、CSS、JSの各パネルへそのまま貼れます。

HTML

<main class="bounce-demo">
  <section class="stage" aria-label="バウンドするバスケットボールのデモ">
    <div class="ball" aria-hidden="true"></div>
    <div class="shadow" aria-hidden="true"></div>
  </section>

  <div class="controls">
    <button type="button" data-speed="slow">Slow</button>
    <button type="button" data-speed="normal" class="is-active">Normal</button>
    <button type="button" data-speed="fast">Fast</button>
  </div>
</main>

CSS

* {
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  margin: 0;
  display: grid;
  place-items: center;
  padding: 24px;
  background: #111;
  color: #fff;
  font-family: system-ui, sans-serif;
}

.bounce-demo {
  width: min(100%, 760px);
}

.stage {
  position: relative;
  height: min(62vh, 460px);
  overflow: hidden;
  border: 2px solid rgb(255 255 255 / .18);
  border-radius: 28px;
  background:
    radial-gradient(circle at 50% 20%, rgb(255 211 41 / .16), transparent 28%),
    linear-gradient(#202020, #111);
}

.stage::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 72px;
  left: 0;
  height: 3px;
  background: #ff2f63;
  box-shadow: 0 0 24px rgb(255 47 99 / .5);
}

.ball {
  --duration: .88s;
  position: absolute;
  left: 50%;
  bottom: 76px;
  width: clamp(82px, 16vw, 132px);
  aspect-ratio: 1;
  transform: translateX(-50%);
  border-radius: 50%;
  background:
    radial-gradient(circle at 32% 24%, #ffbd62 0 9%, transparent 10%),
    radial-gradient(circle at 35% 28%, #f28b2d, #c8601b 68%, #7a330c 100%);
  box-shadow: inset -18px -22px 28px rgb(0 0 0 / .25);
  animation: bounce var(--duration) cubic-bezier(.34, .02, .62, 1) infinite;
}

.ball::before,
.ball::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
}

.ball::before {
  border: 4px solid #2a1307;
  clip-path: polygon(0 44%, 100% 44%, 100% 58%, 0 58%);
}

.ball::after {
  background:
    linear-gradient(90deg, transparent 47%, #2a1307 48% 52%, transparent 53%),
    radial-gradient(circle at 50% 50%, transparent 0 42%, #2a1307 43% 46%, transparent 47%);
  opacity: .95;
}

.shadow {
  --duration: .88s;
  position: absolute;
  left: 50%;
  bottom: 52px;
  width: clamp(88px, 17vw, 142px);
  height: 18px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: rgb(0 0 0 / .42);
  filter: blur(4px);
  animation: shadow var(--duration) ease-in-out infinite;
}

.controls {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-top: 20px;
}

.controls button {
  min-width: 88px;
  min-height: 44px;
  border: 2px solid #fff;
  border-radius: 999px;
  background: transparent;
  color: #fff;
  font-weight: 800;
  cursor: pointer;
}

.controls button:hover,
.controls button:focus-visible,
.controls button.is-active {
  border-color: #ffd329;
  background: #ffd329;
  color: #111;
}

@keyframes bounce {
  0%, 100% {
    transform: translate(-50%, 0) scale(1.08, .92);
  }
  12% {
    transform: translate(-50%, -8px) scale(.96, 1.04);
  }
  55% {
    transform: translate(-50%, -250px) scale(1);
  }
}

@keyframes shadow {
  0%, 100% {
    opacity: .55;
    transform: translateX(-50%) scaleX(1);
  }
  55% {
    opacity: .16;
    transform: translateX(-50%) scaleX(.48);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ball,
  .shadow {
    animation: none;
  }
}

JS

const ball = document.querySelector(".ball");
const shadow = document.querySelector(".shadow");
const buttons = document.querySelectorAll("[data-speed]");

const speeds = {
  slow: "1.2s",
  normal: ".88s",
  fast: ".58s",
};

buttons.forEach((button) => {
  button.addEventListener("click", () => {
    const duration = speeds[button.dataset.speed] || speeds.normal;

    ball.style.setProperty("--duration", duration);
    shadow.style.setProperty("--duration", duration);

    buttons.forEach((item) => {
      item.classList.toggle("is-active", item === button);
    });
  });
});

JavaScriptの役割

JavaScriptでは、ボタンのdata-speedを読み取り、CSSカスタムプロパティ--durationを書き換えています。動きそのものをJavaScriptで毎フレーム計算するより、CSSアニメーションへ任せる方がシンプルです。

const speeds = {
  slow: '1.2s',
  normal: '.88s',
  fast: '.58s',
};

ball.style.setProperty('--duration', speeds.fast);

もっと自然に見せる調整

  • 最高到達点:translateY(-250px)の値を大きくすると高く跳ねます。
  • 速度:--durationを短くするとテンポが速くなります。
  • 重さ:床に当たる時間を短くし、上昇・落下を速くすると重く見えます。
  • 影:高い時に小さく薄く、低い時に大きく濃くすると距離感が出ます。
  • 床ライン:ボールの接地点にラインや影を置くと、着地位置が分かりやすくなります。

よくある失敗

  • topやbottomをアニメーションする:レイアウト計算が発生しやすいので、基本はtransformを使います。
  • 影を固定したままにする:ボールだけ動いて見え、浮遊感が弱くなります。
  • ボールの模様を複雑にしすぎる:小さい表示では潰れるので、線は少なめで十分です。
  • 速度切り替えをJSだけで作る:CSS変数へ渡すと、CSS側のアニメーション設計を保てます。
  • 動きを止める配慮がない:prefers-reduced-motionでアニメーションを停止できるようにします。

まとめ

バウンド表現は、上下移動、着地時の潰れ、影の変化を合わせると一気にそれっぽくなります。JavaScriptは速度変更などの状態管理に絞り、実際の動きはCSSアニメーションへ任せると、コードも読みやすくなります。

CodePenで試す時は、まず高さと速度を触ってみてください。少し値を変えるだけでも、軽いボール、重いボール、コミカルな動きなど、かなり印象が変わります。