イージング関数

進行 0〜1 を「歪めて」緩急をつける関数。linear から elastic まで 30 種類を実装。 easings.net 互換。

イージングとは

トゥイーンの本体は lerp(a, b, t)t をそのまま渡すと等速直線。 これを「最初ゆっくり、後で速く」みたいに歪めるのがイージング。

   t (入力)         linear         easeInOut
   ────             ─────          ─────
   0   ──→  0      0   ──→  0     0    ──→  0
   0.5 ──→  0.5    0.5 ──→  0.5   0.5  ──→  0.5
   0.25──→  0.25   0.25──→  0.25  0.25 ──→  0.10
   0.75──→  0.75   0.75──→  0.75  0.75 ──→  0.90

      

入力 t も出力も0〜1を保つのが約束。0 と 1 を必ず通り、その間で曲がる。

命名規則

典型的な選択

linear

const linear = (t) => t

べき乗系(quad / cubic / quart / quint)

最もよく使う。n が大きいほど急峻。

// Quad (n=2)
const easeInQuad     = (t) => t * t
const easeOutQuad    = (t) => 1 - (1 - t) * (1 - t)
const easeInOutQuad  = (t) => t < 0.5
  ? 2 * t * t
  : 1 - Math.pow(-2 * t + 2, 2) / 2

// Cubic (n=3) — UI で最も汎用的
const easeInCubic    = (t) => t * t * t
const easeOutCubic   = (t) => 1 - Math.pow(1 - t, 3)
const easeInOutCubic = (t) => t < 0.5
  ? 4 * t * t * t
  : 1 - Math.pow(-2 * t + 2, 3) / 2

// Quart (n=4)
const easeInQuart    = (t) => t * t * t * t
const easeOutQuart   = (t) => 1 - Math.pow(1 - t, 4)
const easeInOutQuart = (t) => t < 0.5
  ? 8 * t * t * t * t
  : 1 - Math.pow(-2 * t + 2, 4) / 2

// Quint (n=5)
const easeInQuint    = (t) => t * t * t * t * t
const easeOutQuint   = (t) => 1 - Math.pow(1 - t, 5)
const easeInOutQuint = (t) => t < 0.5
  ? 16 * t * t * t * t * t
  : 1 - Math.pow(-2 * t + 2, 5) / 2

sin(最もマイルド)

const easeInSine    = (t) => 1 - Math.cos((t * Math.PI) / 2)
const easeOutSine   = (t) => Math.sin((t * Math.PI) / 2)
const easeInOutSine = (t) => -(Math.cos(Math.PI * t) - 1) / 2

circ(円弧)

const easeInCirc    = (t) => 1 - Math.sqrt(1 - t * t)
const easeOutCirc   = (t) => Math.sqrt(1 - Math.pow(t - 1, 2))
const easeInOutCirc = (t) => t < 0.5
  ? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
  : (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2

expo(指数)

const easeInExpo    = (t) => t === 0 ? 0 : Math.pow(2, 10 * t - 10)
const easeOutExpo   = (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t)
const easeInOutExpo = (t) => {
  if (t === 0) return 0
  if (t === 1) return 1
  return t < 0.5
    ? Math.pow(2, 20 * t - 10) / 2
    : (2 - Math.pow(2, -20 * t + 10)) / 2
}

back(行き過ぎて戻る)

ボタンが押された感、ポップアップの登場で人気。

const c1 = 1.70158
const c2 = c1 * 1.525
const c3 = c1 + 1

const easeInBack    = (t) => c3 * t * t * t - c1 * t * t
const easeOutBack   = (t) => 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2)
const easeInOutBack = (t) => t < 0.5
  ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
  : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2

1.70158 は元論文 Robert Penner の標準値(10% オーバーに対応)。強くしたければ大きく。

elastic(ゴムのように振動)

const c4 = (2 * Math.PI) / 3
const c5 = (2 * Math.PI) / 4.5

const easeInElastic = (t) => {
  if (t === 0) return 0
  if (t === 1) return 1
  return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4)
}
const easeOutElastic = (t) => {
  if (t === 0) return 0
  if (t === 1) return 1
  return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1
}
const easeInOutElastic = (t) => {
  if (t === 0) return 0
  if (t === 1) return 1
  return t < 0.5
    ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
    : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1
}

bounce(地面に弾む)

function easeOutBounce(t) {
  const n1 = 7.5625
  const d1 = 2.75
  if (t < 1 / d1) {
    return n1 * t * t
  } else if (t < 2 / d1) {
    return n1 * (t -= 1.5 / d1) * t + 0.75
  } else if (t < 2.5 / d1) {
    return n1 * (t -= 2.25 / d1) * t + 0.9375
  } else {
    return n1 * (t -= 2.625 / d1) * t + 0.984375
  }
}

const easeInBounce    = (t) => 1 - easeOutBounce(1 - t)
const easeInOutBounce = (t) => t < 0.5
  ? (1 - easeOutBounce(1 - 2 * t)) / 2
  : (1 + easeOutBounce(2 * t - 1)) / 2

30 種類まとめ(Easings オブジェクト)

export const Easings = {
  linear: (t) => t,

  easeInQuad,    easeOutQuad,    easeInOutQuad,
  easeInCubic,   easeOutCubic,   easeInOutCubic,
  easeInQuart,   easeOutQuart,   easeInOutQuart,
  easeInQuint,   easeOutQuint,   easeInOutQuint,
  easeInSine,    easeOutSine,    easeInOutSine,
  easeInCirc,    easeOutCirc,    easeInOutCirc,
  easeInExpo,    easeOutExpo,    easeInOutExpo,
  easeInBack,    easeOutBack,    easeInOutBack,
  easeInElastic, easeOutElastic, easeInOutElastic,
  easeInBounce,  easeOutBounce,  easeInOutBounce,
}

逆関数 / 反転

easeIneaseOut は数学的に対称:

const flip = (t) => 1 - t

const easeOutXxx = (t) => 1 - easeInXxx(flip(t))

独自の easeIn を作れば、easeOuteaseInOut は機械的に作れる:

function makeOut(easeIn) {
  return (t) => 1 - easeIn(1 - t)
}

function makeInOut(easeIn) {
  return (t) => t < 0.5
    ? easeIn(2 * t) / 2
    : 1 - easeIn(2 * (1 - t)) / 2
}

cubic-bezier(CSS と同じ)

CSS の cubic-bezier(.25, .1, .25, 1) を JS で動かす:

function cubicBezier(x1, y1, x2, y2) {
  // Newton-Raphson で x → t を解く
  return function(x) {
    if (x <= 0) return 0
    if (x >= 1) return 1

    const sampleX = (t) => 3*(1-t)*(1-t)*t*x1 + 3*(1-t)*t*t*x2 + t*t*t
    const sampleY = (t) => 3*(1-t)*(1-t)*t*y1 + 3*(1-t)*t*t*y2 + t*t*t
    const dxdt = (t) =>
      3*(1-t)*(1-t)*x1 + 6*(1-t)*t*(x2-x1) + 3*t*t*(1-x2)

    let t = x
    for (let i = 0; i < 8; i++) {
      const cur = sampleX(t) - x
      if (Math.abs(cur) < 1e-6) break
      const d = dxdt(t)
      if (Math.abs(d) < 1e-6) break
      t -= cur / d
    }
    return sampleY(t)
  }
}

const easeInOut = cubicBezier(0.42, 0, 0.58, 1)
easeInOut(0.5)  // ~0.5

step / staircase(離散)

パラパラ漫画的に階段状に進む:

function steps(n, position = "end") {
  return (t) => {
    if (position === "start") return Math.ceil(t * n) / n
    return Math.floor(t * n) / n
  }
}

const step3 = steps(3)
step3(0.0)  // 0
step3(0.5)  // 0.333...
step3(1.0)  // 1.0

カスタム合成

2 つのイージングを切り替える / 重ねる:

// 前半 easeIn、後半 easeOut のような組み合わせ
function chain(easeA, easeB, breakpoint = 0.5) {
  return (t) => t < breakpoint
    ? easeA(t / breakpoint) * breakpoint
    : breakpoint + easeB((t - breakpoint) / (1 - breakpoint)) * (1 - breakpoint)
}

イージングの選び方

感じたい印象おすすめ
素早く、上品easeOutCubic / easeOutQuart
機械的linear(実は不自然なので滅多に使わない)
軽快なポップeaseOutBack(c1=2 にすると派手)
ふわっとeaseOutSine / easeInOutSine
勢いよく入って減速easeOutQuart / easeOutExpo
ゲームの跳ね返りeaseOutBounce / easeOutElastic
遷移(A → B 滑らか)easeInOutCubic

視覚的な目安(横軸 t、縦軸 出力)

linear      ▁▂▃▄▅▆▇█

easeIn      ▁▁▁▂▃▄▆█

easeOut     █▆▄▃▂▁▁▁  ↑反転

easeInOut   ▁▁▂▄▆██▇▆

easeOutBack ▁▃▆██▇█    ↑1 を超える

easeOutBounce ▁▃▇▅▇██  ↑何度か跳ねる
      

WAAPI / CSS との対応

CSS近似 JS
easecubicBezier(0.25, 0.1, 0.25, 1.0)
ease-incubicBezier(0.42, 0, 1, 1)
ease-outcubicBezier(0, 0, 0.58, 1)
ease-in-outcubicBezier(0.42, 0, 0.58, 1)

1 を超えるイージング(Anticipation)

easeInBack や easeOutElastic は途中で1 を超えたり、マイナスになる。 これは仕様で、生き物っぽい動きの肝。値の範囲を厳密に縛る場合は注意。

イージングの可視化

function plotEasing(easing, canvas) {
  const ctx = canvas.getContext("2d")
  const w = canvas.width, h = canvas.height
  ctx.clearRect(0, 0, w, h)
  ctx.beginPath()
  for (let x = 0; x <= w; x++) {
    const t = x / w
    const y = h - easing(t) * h
    if (x === 0) ctx.moveTo(x, y)
    else ctx.lineTo(x, y)
  }
  ctx.stroke()
}

plotEasing(Easings.easeOutBack, canvas)

パフォーマンス

どのイージングも 1 フレームに数十回呼んでも無視できるコスト。気にしなくてよい。 cubicBezier の Newton 法も 8 反復程度なので速い。

参考実装と検証

迷ったら

UI のほぼすべては easeOutCubiceaseInOutCubic でいける。 演出を強めたいときに back / elastic を足す、というのが定石。