T-CREATOR

Motion(旧 Framer Motion)で学ぶ物理ベースアニメ:バネ定数・減衰・質量の直感入門

Motion(旧 Framer Motion)で学ぶ物理ベースアニメ:バネ定数・減衰・質量の直感入門

React でリッチなアニメーションを実装したいとき、Motion(旧 Framer Motion)はとても強力な選択肢ですね。 特に物理ベースのアニメーションは、バネのような自然な動きを簡単に実装できるため、ユーザー体験を格段に向上させることができます。 本記事では、Motion の物理ベースアニメーションにおける「バネ定数(stiffness)」「減衰(damping)」「質量(mass)」という 3 つのパラメータを、初心者の方にもわかりやすく解説していきます。

背景

Web アニメーションには、大きく分けて「時間ベース」と「物理ベース」の 2 つのアプローチがあります。

時間ベースのアニメーションは、CSS の transitionanimation のように、開始から終了までの時間を指定して動きを制御します。 一方、物理ベースのアニメーションは、現実世界の物理法則を模倣し、バネや重力のような自然な動きを再現するものです。

Motion では、この物理ベースのアニメーションを spring という設定で実現できます。 バネの動きを思い浮かべてください。引っ張って離すと、目標位置に向かって振動しながら徐々に静止していきますね。 この動きを数値パラメータで制御することで、多様な表現が可能になります。

以下の図は、時間ベースと物理ベースのアニメーションの違いを示したものです。

mermaidflowchart LR
  anim["アニメーション<br/>手法"]
  time["時間ベース"]
  physics["物理ベース"]

  anim --> time
  anim --> physics

  time --> css["CSS transition<br/>duration指定"]
  time --> linear["一定速度or<br/>イージング関数"]

  physics --> spring["バネモデル"]
  physics --> natural["自然な加速<br/>減速・振動"]
  spring --> params["stiffness<br/>damping<br/>mass"]

この図から、物理ベースは「バネモデル」を使い、「stiffness」「damping」「mass」という 3 つのパラメータで制御されることがわかります。

課題

物理ベースアニメーションは魅力的ですが、初めて触れる方には以下のような課題があります。

パラメータの意味が直感的にわかりにくい

「stiffness(バネ定数)」「damping(減衰)」「mass(質量)」という用語は、物理学の知識がないと理解しづらいものです。 数値を変更したときにどのような動きになるのか、予測するのが難しいですね。

適切な値の選び方がわからない

Motion のドキュメントには、デフォルト値として stiffness: 100damping: 10mass: 1 が設定されていますが、これらをどう調整すれば望みの動きになるのか、最初はわかりません。 試行錯誤を繰り返すことになり、時間がかかってしまうでしょう。

複数パラメータの組み合わせが複雑

3 つのパラメータは互いに影響し合います。 例えば、バネ定数を上げると速くなりますが、減衰が低いと振動が激しくなりすぎることがあります。 このバランス調整が難しいのです。

以下の図は、パラメータ同士の相互作用を示したものです。

mermaidflowchart TD
  stiff["stiffness<br/>バネ定数"]
  damp["damping<br/>減衰"]
  m["mass<br/>質量"]
  motion["アニメーション<br/>の動き"]

  stiff -->|高い値| fast["速い動き<br/>振動しやすい"]
  stiff -->|低い値| slow["遅い動き<br/>なめらか"]

  damp -->|高い値| stable["振動を抑制<br/>すぐ静止"]
  damp -->|低い値| bounce["振動が続く<br/>弾む感じ"]

  m -->|大きい値| heavy["重い動き<br/>慣性が大きい"]
  m -->|小さい値| light["軽い動き<br/>機敏"]

  fast --> motion
  slow --> motion
  stable --> motion
  bounce --> motion
  heavy --> motion
  light --> motion

この図のように、各パラメータは動きに異なる影響を与え、組み合わせによって最終的な挙動が決まります。

解決策

物理ベースアニメーションを使いこなすには、各パラメータの役割を理解し、実際にコードで試してみることが大切です。 Motion では、motion コンポーネントの transition プロパティで物理パラメータを指定できます。

stiffness(バネ定数):バネの硬さを表す

バネ定数は、バネがどれくらい硬いかを示す値です。 値が大きいほど、バネは硬くなり、強い力で目標位置に戻ろうとします。

  • 高い値(200〜500): 素早く、キビキビとした動き
  • デフォルト値(100): バランスの取れた標準的な動き
  • 低い値(50〜80): ゆったりとした、柔らかい動き

damping(減衰):振動の収まり方を表す

減衰は、バネの振動がどれくらい早く収まるかを制御します。 摩擦や空気抵抗のようなイメージです。

  • 高い値(30〜50): 振動がほとんどなく、スムーズに静止
  • デフォルト値(10): 適度な振動で自然な動き
  • 低い値(0〜5): 何度も振動して、弾むような動き

mass(質量):オブジェクトの重さを表す

質量は、動くオブジェクトの重さを示します。 重いものは慣性が大きく、軽いものは機敏に動きます。

  • 大きい値(3〜5): 重厚感のある、ゆっくりとした動き
  • デフォルト値(1): 標準的な重さ
  • 小さい値(0.5〜0.8): 軽快で素早い動き

パラメータの組み合わせ指針

以下の表は、よくある動きのパターンとパラメータの組み合わせをまとめたものです。

#動きの種類stiffnessdampingmass特徴
1素早くキビキビ300251ボタンのホバー、クリック反応に最適
2弾むような動き20051通知バッジ、ポップアップに効果的
3なめらかで優雅80201モーダル表示、大きな要素の移動向け
4重厚感のある動き100153ドラッグ&ドロップ、大きなカードに
5デフォルト(標準)100101汎用的で自然なバランス

この表を参考に、実装したい動きに応じてパラメータを選択すると良いでしょう。

具体例

実際のコードで、各パラメータの違いを見ていきましょう。 Motion を使った基本的な実装から、パラメータを変更した複数のパターンまで、段階的に解説します。

環境構築とインストール

まず、Motion をプロジェクトにインストールします。

bashyarn add motion

Motion は、以前 framer-motion という名前でしたが、現在は motion というパッケージ名に変更されています。 すでに framer-motion を使用している場合は、そのまま動作しますが、新規プロジェクトでは motion を使用することをお勧めします。

基本的な物理アニメーションの実装

Motion で物理ベースアニメーションを実装する基本的な例を見てみましょう。

typescriptimport { motion } from 'motion/react';

Motion からコンポーネントをインポートします。 motion オブジェクトには、motion.divmotion.button など、HTML 要素に対応したコンポーネントが用意されています。

typescriptexport default function BasicSpringAnimation() {
  return (
    <motion.div
      initial={{ x: 0 }}
      animate={{ x: 100 }}
      transition={{
        type: 'spring',
        stiffness: 100,
        damping: 10,
        mass: 1,
      }}
      style={{
        width: 100,
        height: 100,
        backgroundColor: '#3b82f6',
        borderRadius: 8,
      }}
    />
  );
}

この基本例では、青い四角が右に 100px 移動します。 initial は開始位置、animate は終了位置を指定します。 transitiontype: "spring" を指定することで、物理ベースアニメーションが有効になります。

stiffness(バネ定数)の比較

バネ定数を変更すると、動きの速さがどう変わるか見てみましょう。

typescriptimport { motion } from "motion/react"
import { useState } from "react"

export default function StiffnessComparison() {
  const [isAnimated, setIsAnimated] = useState(false)

  return (
    <div style={{ padding: 20 }}>
      <button onClick={() => setIsAnimated(!isAnimated)}>
        アニメーション切り替え
      </button>

まず、状態管理とボタンを用意します。 ボタンをクリックするたびに、アニメーションのオン・オフが切り替わります。

typescript{
  /* 低いstiffness(柔らかいバネ) */
}
<motion.div
  animate={{ x: isAnimated ? 300 : 0 }}
  transition={{
    type: 'spring',
    stiffness: 50, // 低い値
    damping: 10,
    mass: 1,
  }}
  style={{
    width: 80,
    height: 80,
    backgroundColor: '#10b981',
    borderRadius: 8,
    marginTop: 20,
  }}
>
  <span style={{ color: 'white', padding: 8 }}>
    stiffness: 50
  </span>
</motion.div>;

stiffness を 50 に設定した例です。 バネが柔らかいため、ゆっくりとなめらかに移動します。

typescript{
  /* デフォルトのstiffness */
}
<motion.div
  animate={{ x: isAnimated ? 300 : 0 }}
  transition={{
    type: 'spring',
    stiffness: 100, // デフォルト値
    damping: 10,
    mass: 1,
  }}
  style={{
    width: 80,
    height: 80,
    backgroundColor: '#3b82f6',
    borderRadius: 8,
    marginTop: 20,
  }}
>
  <span style={{ color: 'white', padding: 8 }}>
    stiffness: 100
  </span>
</motion.div>;

stiffness を 100(デフォルト値)に設定した例です。 バランスの取れた標準的な速度で移動します。

typescript      {/* 高いstiffness(硬いバネ) */}
      <motion.div
        animate={{ x: isAnimated ? 300 : 0 }}
        transition={{
          type: "spring",
          stiffness: 300,  // 高い値
          damping: 10,
          mass: 1
        }}
        style={{
          width: 80,
          height: 80,
          backgroundColor: "#ef4444",
          borderRadius: 8,
          marginTop: 20
        }}
      >
        <span style={{ color: "white", padding: 8 }}>stiffness: 300</span>
      </motion.div>
    </div>
  )
}

stiffness を 300 に設定した例です。 バネが硬いため、素早くキビキビと移動します。

この例を実行すると、3 つの要素が異なる速度で移動する様子を確認できます。

damping(減衰)の比較

次に、減衰の違いを見てみましょう。 減衰は振動の収まり方を制御します。

typescriptimport { motion } from "motion/react"
import { useState } from "react"

export default function DampingComparison() {
  const [isAnimated, setIsAnimated] = useState(false)

  return (
    <div style={{ padding: 20 }}>
      <button onClick={() => setIsAnimated(!isAnimated)}>
        アニメーション切り替え
      </button>

前の例と同様に、状態管理とボタンを用意します。

typescript{
  /* 低いdamping(振動が続く) */
}
<motion.div
  animate={{ x: isAnimated ? 300 : 0 }}
  transition={{
    type: 'spring',
    stiffness: 200,
    damping: 3, // 低い値
    mass: 1,
  }}
  style={{
    width: 80,
    height: 80,
    backgroundColor: '#10b981',
    borderRadius: 8,
    marginTop: 20,
  }}
>
  <span style={{ color: 'white', padding: 8 }}>
    damping: 3
  </span>
</motion.div>;

damping を 3 に設定した例です。 減衰が小さいため、目標位置を何度も行き来して、弾むような動きになります。

typescript{
  /* デフォルトのdamping */
}
<motion.div
  animate={{ x: isAnimated ? 300 : 0 }}
  transition={{
    type: 'spring',
    stiffness: 200,
    damping: 10, // デフォルト値
    mass: 1,
  }}
  style={{
    width: 80,
    height: 80,
    backgroundColor: '#3b82f6',
    borderRadius: 8,
    marginTop: 20,
  }}
>
  <span style={{ color: 'white', padding: 8 }}>
    damping: 10
  </span>
</motion.div>;

damping を 10(デフォルト値)に設定した例です。 適度な振動で、自然な動きになります。

typescript      {/* 高いdamping(振動が少ない) */}
      <motion.div
        animate={{ x: isAnimated ? 300 : 0 }}
        transition={{
          type: "spring",
          stiffness: 200,
          damping: 40,  // 高い値
          mass: 1
        }}
        style={{
          width: 80,
          height: 80,
          backgroundColor: "#ef4444",
          borderRadius: 8,
          marginTop: 20
        }}
      >
        <span style={{ color: "white", padding: 8 }}>damping: 40</span>
      </motion.div>
    </div>
  )
}

damping を 40 に設定した例です。 減衰が大きいため、振動がほとんどなく、スムーズに静止します。

mass(質量)の比較

最後に、質量の違いを確認しましょう。 質量は、動くオブジェクトの重さを表現します。

typescriptimport { motion } from "motion/react"
import { useState } from "react"

export default function MassComparison() {
  const [isAnimated, setIsAnimated] = useState(false)

  return (
    <div style={{ padding: 20 }}>
      <button onClick={() => setIsAnimated(!isAnimated)}>
        アニメーション切り替え
      </button>

同様に、状態管理とボタンを用意します。

typescript{
  /* 軽い質量 */
}
<motion.div
  animate={{ x: isAnimated ? 300 : 0 }}
  transition={{
    type: 'spring',
    stiffness: 100,
    damping: 10,
    mass: 0.5, // 軽い
  }}
  style={{
    width: 80,
    height: 80,
    backgroundColor: '#10b981',
    borderRadius: 8,
    marginTop: 20,
  }}
>
  <span style={{ color: 'white', padding: 8 }}>
    mass: 0.5
  </span>
</motion.div>;

mass を 0.5 に設定した例です。 軽いため、機敏で素早い動きになります。

typescript{
  /* デフォルトの質量 */
}
<motion.div
  animate={{ x: isAnimated ? 300 : 0 }}
  transition={{
    type: 'spring',
    stiffness: 100,
    damping: 10,
    mass: 1, // デフォルト値
  }}
  style={{
    width: 80,
    height: 80,
    backgroundColor: '#3b82f6',
    borderRadius: 8,
    marginTop: 20,
  }}
>
  <span style={{ color: 'white', padding: 8 }}>
    mass: 1
  </span>
</motion.div>;

mass を 1(デフォルト値)に設定した例です。 標準的な重さで動きます。

typescript      {/* 重い質量 */}
      <motion.div
        animate={{ x: isAnimated ? 300 : 0 }}
        transition={{
          type: "spring",
          stiffness: 100,
          damping: 10,
          mass: 3  // 重い
        }}
        style={{
          width: 80,
          height: 80,
          backgroundColor: "#ef4444",
          borderRadius: 8,
          marginTop: 20
        }}
      >
        <span style={{ color: "white", padding: 8 }}>mass: 3</span>
      </motion.div>
    </div>
  )
}

mass を 3 に設定した例です。 重いため、慣性が大きく、ゆっくりとした重厚感のある動きになります。

実践的なユースケース:モーダルウィンドウ

実際のアプリケーションで使える、モーダルウィンドウの実装例を見てみましょう。

typescriptimport { motion, AnimatePresence } from "motion/react"
import { useState } from "react"

export default function ModalExample() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div style={{ padding: 40 }}>
      <button
        onClick={() => setIsOpen(true)}
        style={{
          padding: "12px 24px",
          fontSize: 16,
          backgroundColor: "#3b82f6",
          color: "white",
          border: "none",
          borderRadius: 8,
          cursor: "pointer"
        }}
      >
        モーダルを開く
      </button>

モーダルを開くボタンを用意します。 AnimatePresence を使うことで、要素の追加・削除時にアニメーションを適用できます。

typescript      <AnimatePresence>
        {isOpen && (
          <>
            {/* 背景のオーバーレイ */}
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              transition={{ duration: 0.2 }}
              onClick={() => setIsOpen(false)}
              style={{
                position: "fixed",
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                backgroundColor: "rgba(0, 0, 0, 0.5)",
                display: "flex",
                alignItems: "center",
                justifyContent: "center"
              }}
            >

オーバーレイ(半透明の背景)は、フェードイン・フェードアウトのみで表示します。 initialanimateexit を指定することで、表示時と非表示時のアニメーションを制御できます。

typescript              {/* モーダル本体 */}
              <motion.div
                initial={{ scale: 0.8, y: 50 }}
                animate={{ scale: 1, y: 0 }}
                exit={{ scale: 0.8, y: 50 }}
                transition={{
                  type: "spring",
                  stiffness: 300,  // 素早く反応
                  damping: 25,     // 振動を抑える
                  mass: 1
                }}
                onClick={(e) => e.stopPropagation()}
                style={{
                  backgroundColor: "white",
                  borderRadius: 16,
                  padding: 40,
                  maxWidth: 500,
                  boxShadow: "0 20px 60px rgba(0, 0, 0, 0.3)"
                }}
              >

モーダル本体には、物理ベースアニメーションを適用します。 stiffness: 300 で素早く、damping: 25 で振動を抑えた、キビキビとした動きになります。 onClick={(e) => e.stopPropagation()} で、モーダル内クリック時に背景クリックイベントが発火しないようにしています。

typescript                <h2 style={{ marginTop: 0 }}>物理ベースアニメーション</h2>
                <p>
                  このモーダルは、stiffness: 300、damping: 25 で実装されています。
                  素早く表示され、自然な動きで開閉します。
                </p>
                <button
                  onClick={() => setIsOpen(false)}
                  style={{
                    padding: "8px 16px",
                    backgroundColor: "#ef4444",
                    color: "white",
                    border: "none",
                    borderRadius: 6,
                    cursor: "pointer",
                    marginTop: 20
                  }}
                >
                  閉じる
                </button>
              </motion.div>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </div>
  )
}

モーダル内のコンテンツと閉じるボタンを配置します。 この実装により、プロフェッショナルな見た目と操作感のモーダルが実現できます。

実践的なユースケース:カードリストのホバー

カードリストでのホバー効果も、物理ベースアニメーションと相性が良い例です。

typescriptimport { motion } from "motion/react"

const cards = [
  { id: 1, title: "カード 1", description: "素早い反応" },
  { id: 2, title: "カード 2", description: "自然な動き" },
  { id: 3, title: "カード 3", description: "心地よい UX" }
]

export default function CardList() {
  return (
    <div style={{
      display: "grid",
      gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))",
      gap: 20,
      padding: 20
    }}>

カードをグリッドレイアウトで並べます。 repeat(auto-fit, minmax(250px, 1fr)) により、レスポンシブに配置されます。

typescript      {cards.map((card) => (
        <motion.div
          key={card.id}
          whileHover={{
            scale: 1.05,
            y: -8
          }}
          transition={{
            type: "spring",
            stiffness: 400,  // 非常に素早く
            damping: 20,     // 適度な減衰
            mass: 0.8        // やや軽く
          }}
          style={{
            backgroundColor: "white",
            borderRadius: 12,
            padding: 24,
            boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
            cursor: "pointer"
          }}
        >

whileHover プロパティで、ホバー時の状態を指定します。 scale: 1.05 で 5% 拡大し、y: -8 で 8px 上に移動します。 stiffness: 400 という高い値で、非常に素早く反応するようにしています。

typescript          <h3 style={{ marginTop: 0, color: "#1f2937" }}>{card.title}</h3>
          <p style={{ color: "#6b7280", margin: 0 }}>{card.description}</p>
        </motion.div>
      ))}
    </div>
  )
}

カードの内容を表示します。 ホバーすると、即座に拡大・浮き上がる動きが実現され、インタラクティブな体験を提供できます。

以下の図は、これらのユースケースにおける物理パラメータの選択フローを示したものです。

mermaidflowchart TD
  start["実装したい<br/>動きの種類"]
  quick["素早く反応<br/>させたい"]
  smooth["なめらかに<br/>動かしたい"]
  bounce["弾むような<br/>動きにしたい"]
  heavy["重厚感を<br/>出したい"]

  start --> quick
  start --> smooth
  start --> bounce
  start --> heavy

  quick --> q_params["stiffness: 300-400<br/>damping: 20-30<br/>mass: 0.8-1"]
  smooth --> s_params["stiffness: 70-100<br/>damping: 20-30<br/>mass: 1"]
  bounce --> b_params["stiffness: 200-300<br/>damping: 3-8<br/>mass: 1"]
  heavy --> h_params["stiffness: 80-120<br/>damping: 15-20<br/>mass: 2-4"]

  q_params --> example_q["例: ボタンホバー<br/>カードホバー"]
  s_params --> example_s["例: モーダル表示<br/>ページ遷移"]
  bounce --> example_b["例: 通知バッジ<br/>ツールチップ"]
  h_params --> example_h["例: ドラッグ&ドロップ<br/>大きな要素"]

この図を参考に、実装したい動きに応じて適切なパラメータを選択できます。

図で理解できる要点

  • モーダルは「素早く反応」パターンで、高い stiffness と適度な damping を組み合わせています
  • カードホバーは「素早く反応」パターンで、さらに高い stiffness を使用して即座に反応します
  • パラメータの選択は、ユーザー体験の目的(素早さ、なめらかさ、楽しさ、重厚感)に応じて決定します

まとめ

Motion の物理ベースアニメーションは、3 つのパラメータを理解することで、自在に制御できるようになります。

stiffness(バネ定数) は、動きの速さを決定します。 高い値ほど素早く、低い値ほどゆっくりとした動きになるのでしたね。 ボタンやカードのホバーなど、即座に反応してほしい UI には高い値を、モーダルや大きな要素には適度な値を使うと良いでしょう。

damping(減衰) は、振動の収まり方を制御します。 高い値ほど振動が少なく、低い値ほど弾むような動きになります。 プロフェッショナルな印象を与えたいなら高めに、楽しさを演出したいなら低めに設定すると効果的です。

mass(質量) は、オブジェクトの重さを表現します。 大きい値ほど重厚感が生まれ、小さい値ほど軽快な動きになるのです。 実装する UI の性質に応じて、適切な重さを選択することで、ユーザーに直感的な操作感を提供できます。

最初はデフォルト値(stiffness: 100、damping: 10、mass: 1)から始めて、実際に動きを見ながら調整していくのがお勧めです。 本記事で紹介した比較例やユースケースを参考に、ぜひご自身のプロジェクトで試してみてください。 物理ベースアニメーションを使いこなせば、ユーザー体験が格段に向上し、印象に残るインタラクションを実現できますよ。

関連リンク