キーフレーム

2 値だけのトゥイーンではなく、複数の節(key)を時系列に並べて滑らかにつなぐ仕組み。 CSS の @keyframes 相当を JS で自作する。

キーフレームとは

「0% でこの値、25% でこの値、100% でこの値」のように時間軸上の節を指定し、 その間を補間する手法。

時間 (0〜1)        値
0.0  ─────  100
0.25 ─────  300
0.6  ─────  150
1.0  ─────  500

       0.25  0.5  0.6
        ┃    ┃    ┃
   ▆▅▄▃▂▁▂▃▄▅▆▇█
   ↑              ↑
   100            500
      

API 設計

Web Animations API(WAAPI)に近い形が使いやすい:

animateKeyframes(target, [
  { x: 0,   opacity: 0 },         // 0%
  { x: 200, opacity: 1 },         // 50%
  { x: 100, opacity: 0.5 },       // 100%
], { duration: 1500 })

// オフセット指定
animateKeyframes(target, [
  { offset: 0,    x: 0   },
  { offset: 0.3,  x: 200 },       // 30% で 200
  { offset: 1,    x: 0   },
], { duration: 2000 })

最小実装

function animateKeyframes(target, frames, options = {}) {
  const duration = options.duration ?? 1000
  const easing = options.easing ?? ((t) => t)

  // offset を正規化
  const normalized = frames.map((f, i) => ({
    ...f,
    offset: f.offset ?? i / (frames.length - 1),
  }))

  // どのプロパティを扱うか
  const keys = new Set()
  for (const f of normalized) {
    for (const k in f) if (k !== "offset" && k !== "easing") keys.add(k)
  }

  return new Promise((resolve) => {
    const start = performance.now()
    function tick(now) {
      const t = Math.min((now - start) / duration, 1)
      const e = easing(t)

      for (const key of keys) {
        // 該当する区間を探す
        for (let i = 0; i < normalized.length - 1; i++) {
          const a = normalized[i]
          const b = normalized[i + 1]
          if (e >= a.offset && e <= b.offset) {
            const localT = (e - a.offset) / (b.offset - a.offset)
            const segEase = b.easing ?? ((t) => t)
            const finalT = segEase(localT)
            target[key] = a[key] + (b[key] - a[key]) * finalT
            break
          }
        }
      }

      if (t < 1) requestAnimationFrame(tick)
      else resolve()
    }
    requestAnimationFrame(tick)
  })
}

区間ごとに違うイージング

各キーフレームに easing を持たせて、その区間の入り方を変えられる:

animateKeyframes(target, [
  { x: 0 },
  { x: 200, easing: Easings.easeOutCubic },   // 0→200 の区間に適用
  { x: 100, easing: Easings.easeInOutCubic },
  { x: 500, easing: Easings.easeOutBack },
], { duration: 2000 })

プロパティごとに独立した keyframes

WAAPI の「prop ベースの配列」形式(GSAP の keyframes も同様):

animatePropKeyframes(target, {
  x: [0, 200, 100, 500],
  opacity: [0, 1, 0.5],
}, { duration: 2000 })

// 内部実装
function animatePropKeyframes(target, propMap, options) {
  // 各 prop で長さが違うことがある → それぞれ補間
  return new Promise((resolve) => {
    const start = performance.now()
    const duration = options.duration ?? 1000

    function tick(now) {
      const t = Math.min((now - start) / duration, 1)

      for (const key in propMap) {
        const arr = propMap[key]
        const seg = (arr.length - 1) * t
        const i = Math.min(Math.floor(seg), arr.length - 2)
        const localT = seg - i
        target[key] = arr[i] + (arr[i + 1] - arr[i]) * localT
      }

      if (t < 1) requestAnimationFrame(tick)
      else resolve()
    }
    requestAnimationFrame(tick)
  })
}

WAAPI に直接渡す(自作不要パターン)

単に DOM をアニメさせるならWeb Animations API が完成度高い:

box.animate([
  { transform: "translate(0, 0)",       opacity: 0,   offset: 0 },
  { transform: "translate(200px, 0)",   opacity: 1,   offset: 0.5 },
  { transform: "translate(100px, 50px)", opacity: 0.5, offset: 1 },
], {
  duration: 1500,
  easing: "ease-out",
  fill: "forwards",
  iterations: 1,
})

ブラウザがネイティブで補間するためパフォーマンスが最高。 ただし JS 側で値を読みたい / Canvas / WebGL に流すなら自作の方が良い。

スプライン補間(カトマルロム)

単純な「区間ごとの線形補間」だと、節をまたぐ瞬間に角が立つ。 滑らかにしたいならカトマルロムスプライン

function catmullRom(p0, p1, p2, p3, t) {
  const t2 = t * t, t3 = t2 * t
  return 0.5 * (
    (2 * p1) +
    (-p0 + p2) * t +
    (2*p0 - 5*p1 + 4*p2 - p3) * t2 +
    (-p0 + 3*p1 - 3*p2 + p3) * t3
  )
}

function smoothKeyframes(values, t) {
  const seg = (values.length - 1) * t
  const i = Math.floor(seg)
  const localT = seg - i
  const p0 = values[Math.max(i - 1, 0)]
  const p1 = values[i]
  const p2 = values[Math.min(i + 1, values.length - 1)]
  const p3 = values[Math.min(i + 2, values.length - 1)]
  return catmullRom(p0, p1, p2, p3, localT)
}

2D / 3D の経路補間

座標を持つ点の配列をスプラインでつなぐ:

function smoothPath(points, t) {
  return {
    x: smoothKeyframes(points.map(p => p.x), t),
    y: smoothKeyframes(points.map(p => p.y), t),
  }
}

const path = [
  { x: 0,   y: 0 },
  { x: 100, y: 50 },
  { x: 50,  y: 200 },
  { x: 300, y: 150 },
]

function tick(now) {
  const t = ((now / 3000) % 1)
  const p = smoothPath(path, t)
  box.style.transform = `translate(${p.x}px, ${p.y}px)`
  requestAnimationFrame(tick)
}

SVG パスに沿わせる

SVG パスは数学的に正確な経路。標準 API で点を取得できる:

const path = document.querySelector("path#guide")
const total = path.getTotalLength()

function tick(now) {
  const t = ((now / 4000) % 1)
  const pt = path.getPointAtLength(t * total)

  // 接線方向で向きも合わせる
  const lookahead = path.getPointAtLength(t * total + 1)
  const angle = Math.atan2(lookahead.y - pt.y, lookahead.x - pt.x)

  el.style.transform = `translate(${pt.x}px, ${pt.y}px) rotate(${angle}rad)`
  requestAnimationFrame(tick)
}

ベジエ手書きの経路

SVG が無くてもベジエ曲線を直接実装:

function bezier3(p0, p1, p2, p3, t) {
  const u = 1 - t
  return {
    x: u**3*p0.x + 3*u**2*t*p1.x + 3*u*t*t*p2.x + t**3*p3.x,
    y: u**3*p0.y + 3*u**2*t*p1.y + 3*u*t*t*p2.y + t**3*p3.y,
  }
}

const start = { x: 0,   y: 0 }
const ctrl1 = { x: 100, y: -100 }
const ctrl2 = { x: 200, y: 100 }
const end   = { x: 300, y: 0 }

const pos = bezier3(start, ctrl1, ctrl2, end, t)

イージング付きキーフレームの実用例

ローディングのドット 3 つ

document.querySelectorAll(".dot").forEach((dot, i) => {
  setInterval(() => {
    animateKeyframes(dot.style, [
      { opacity: 0.3 },
      { opacity: 1, easing: Easings.easeOutCubic },
      { opacity: 0.3, easing: Easings.easeInCubic },
    ], { duration: 900 })
  }, 1200)
  // delay でずらしても OK
})

ハートビート

animatePropKeyframes(state, {
  scale: [1, 1.15, 1, 1.1, 1],
}, {
  duration: 1000,
  // 心拍 ↑↓↑↓ を表現
})

カードフリップ

animatePropKeyframes(state, {
  rotateY: [0, 90, 90, 180],
  scale:   [1, 1.1, 1.1, 1],
}, { duration: 800 })

キーフレーム + 物理(ハイブリッド)

着地のような物理的な動きを再現したい場合、最後だけスプリング、それ以前はキーフレームというハイブリッドも有効:

// 1) 70% までキーフレームで派手に
await animateKeyframes(...)
// 2) 残りはスプリングで揺れる(→ spring.html)
spring(state, { y: 0 }, { stiffness: 150 })

キーフレームのデバッグ

パフォーマンスメモ

WAAPI vs 自作の早見表

条件選択
DOM 要素のみWAAPI
Canvas / WebGL の値自作
フレームごとに値を取りたい自作(onUpdate)
ブラウザで最速WAAPI(コンポジタ層で動く)
イベント連動 / 動的に値変更自作 or motion
DOM だけなら CSS @keyframes

ループ系はそもそも CSS の @keyframes で書ける場合が多い。JS で fadeIn / spin / bounce を作る前に、 CSS で済まないか考える。複雑な制御 / シーケンス / Canvas のときに JS の出番。