コンテンツにスキップ

Follow Through & Overlapping Action(フォロースルーと重なり合う動作)

本体が止まっても、ゆるく付いた部分は慣性で行き過ぎてから戻り、各部位は少しずつ遅れて動く。followThrough は動きの終端をスプリングに置き換えて行き過ぎを作り、overlap は複数の要素を時間差で動かして、後ろの要素ほど大きく揺らします。

LIVE
overlap() で5本のバーが 40ms 間隔で入場し、後ろのバーほど大きく行き過ぎる。followThrough() は箱を 180px 動かし、約 23px 行き過ぎてから止める(押すたびに往復)。

Follow through と overlapping action は、ディズニーの 12 原則では一つの項目として扱われます。どちらも「キャラクターは一枚の硬い板ではなく、重さと柔らかさの違う部位の集まりである」という観察から来ています。

Follow through は、主体が止まった後も付随する部分が動き続けることです。走ってきたキャラクターが急停止すると、体は止まっても髪やマント、犬の耳は前へ流れ、行き過ぎてから元の位置に落ち着きます。投球動作で腕を振り切った後も体が回り続けるのも同じ現象です。

Overlapping action(drag とも呼ばれる)は、部位ごとに動き出しと止まりのタイミングがずれることです。腕を振るとき、上腕が先に動き、前腕、手首、指先の順に遅れてついていきます。末端の軽い部位ほど遅れ、大きく揺れます。全部位が同じフレームで動き出して同じフレームで止まると、動きは機械的で重さのないものに見えます。

UI 要素には髪も尻尾もありませんが、「到着時の小さな行き過ぎ」と「複数要素の時間差」は同じ効果を生みます。

  • 到着の行き過ぎ: トグルのつまみ、ポップオーバー、トースト、enter("pop") のような入場。移動量の数 % だけ行き過ぎることで、要素に質量があるように見えます。
  • 押下からの復帰: pressable / usePress は押下を 100ms の ease-out で潰し、離したときに settleSpring で戻します。
  • リストの時間差入場: 検索結果、カードグリッド、メニュー項目。overlap / useCascade は項目ごとに遅延をずらし、起点から遠い項目ほど強い follow through を付けます。

スプリングの bounce と見た目の関係は次の通りです(spring({ bounce })、知覚 duration 400ms での実測値)。

bounce 最大の行き過ぎ 静定時間 .duration 用途の目安
0 0% 592ms 臨界減衰。行き過ぎなし
0.2 約 1.5% 542ms natural の個性。ほぼ気づかない程度の弾み
0.3 約 4.6% 658ms followThrough の既定値
0.45 約 12.6% 804ms playful の個性。デモの箱(180px で約 23px)
0.6 約 25% 1112ms カートゥーン調。bouncy(0.55)と cartoon(0.65)の個性がこの前後
0.8 約 53% 2104ms overlap の上限。UI ではほぼ使わない

業務系の UI では 0.1〜0.3、注目を集めたい一回限りの演出で 0.45 前後が目安です。タイピング中の候補表示、ドラッグ中の追従、数値やチャートなど「正確な位置」を読ませる要素には付けないでください。行き過ぎた一瞬、値や位置が誤って見えます。画面全体を覆うパネルも、行き過ぎると端から背景が覗くため避けます。

時間差は短く保ちます。staggerDelays は 1 項目あたりの遅延を cap(既定 MAX_STAGGER_MS = 50ms)で、カスケード全体を total(既定 300ms)で頭打ちにします。項目数が多いほど間隔は自動的に詰まり、12 項目・既定値では約 27ms 間隔で最後の項目が 300ms に始まります。祝福の演出のように時間差そのものを見せたいときは、captotal を明示して広げます(派手な動き)。

function followThrough(spec: MotionSpec, options?: FollowThroughOptions): MotionSpec;
オプション 既定値 説明
bounce number 0.3 終端での行き過ぎの強さ。0 = なし、0.3 = 生き生き、0.6 = ゆるい

動作の詳細:

  • フレームが 2 つ未満、または bounce が 0 以下のときは spec をそのまま返します。
  • まず withoutGlobalEasingspec.easing(イテレーション全体のイージング)をフレーム側へ移し、キーポーズを保ったまま平坦化します。
  • 最後の区間(最後から 2 番目のフレームの easing)を spring({ bounce }) に置き換えます。その区間に元々付いていたイージングは捨てられます。2 フレームの spec なら動き全体がスプリングになります。
  • スプリングはその区間の長さに正規化されるので、spec.duration は変わりません。スプリングの知覚 duration(400ms)は使われず、bounce による曲線の形だけが効きます。
  • JS 関数のイージングを含むため、compile 時に 60fps の線形キーフレーム(12〜120 枚)へ焼き込まれます。
function overlap(spec: MotionSpec, count: number, options?: OverlapOptions): MotionSpec[];

OverlapOptionsStaggerOptions を拡張しています。

オプション 既定値 説明
drag number 0.1 起点から n ステップ離れたパートの bouncedrag × n にする(上限 0.8)
each number 30 隣り合うパートの遅延(ms)。cap でクランプ
cap number 50MAX_STAGGER_MS 1 パートあたりの遅延の上限(ms)。負なら RangeError
total number 300 カスケード全体の遅延の上限(ms)
from "first" | "last" | "center" | number "first" カスケードの起点。数値はインデックス(小数も可)

戻り値は count 個の MotionSpec の配列で、i 番目を i 番目の要素に再生します。

  • 遅延は staggerDelays(count, options) で決まります。間隔は min(each, cap, total / 最大距離) で、各パートの遅延は round(距離 × 間隔) です。元の spec.delay に加算されます。
  • 距離は staggerDistances(count, from) で求めます。起点のパート(距離 0)は bounce が 0 になるので、spec をそのまま使います(follow through なし)。
  • count が 0 以上の整数でなければ RangeError を投げます。

デモと同じ each: 40, drag: 0.12 で 5 パートの場合:

パート 距離 遅延 bounce
0 0 0ms なし(元の spec)
1 1 40ms 0.12
2 2 80ms 0.24
3 3 120ms 0.36
4 4 160ms 0.48

from: "center" なら 5 パートの距離は [2, 1, 0, 1, 2]、既定の each で遅延は [60, 30, 0, 30, 60] になり、中央から外側へ広がります。

function spring(options?: SpringOptions): Spring;
interface Spring extends EasingFn {
readonly duration: number; // 静定時間(ms)
readonly stiffness: number;
readonly damping: number;
readonly mass: number;
}

減衰調和振動子をイージング関数として返します。パラメータは「知覚」と「物理」の 2 通りで指定でき、stiffnessdamping のどちらかを渡すと物理パラメータが優先されます。

オプション 既定値 説明
duration number 400 知覚上の長さ(ms)。減衰がないときの振動の周期
bounce number 0.2 0 = 臨界減衰(行き過ぎなし)、0.3 = 生き生き、0.6 以上 = カートゥーン調。0〜0.95 にクランプ
stiffness number 170(物理モード時) ばね定数
damping number 26(物理モード時) 減衰係数
mass number 1 質量
velocity number 0 初速。1 = 1 秒あたり全距離
  • 知覚モードでは stiffness = (2π / duration)² × massdamping = 4π × (1 − bounce) × mass / durationduration は秒)に変換します。減衰比は 1 − bounce です。
  • 返り値の関数は t が 0 以下で 0、1 以上で 1 を返し、その間は行き過ぎて 1 を超えることがあります。
  • .duration は値が目標の 0.1% 以内に収まり続けるまでの時間(ms)です。240Hz で最大 10 秒まで走査して求めます。知覚 duration とは別物で、既定の spring() なら 542ms です。
  • 物理モードの既定値(170 / 26 / 1)は減衰比が約 0.997 で、ほぼ行き過ぎず 704ms で静定します。
  • mass / stiffness / damping が正の有限値にならない場合(duration: 0 など)は RangeError を投げます。

使い方は 2 通りあります。animateTotransition に渡すと、.duration がそのまま再生時間になり、物理的な時間感覚が保たれます。PoseFrame.easingMotionSpec.easing に渡すと、その区間やイテレーションの長さに曲線が引き伸ばされます。isSpring(value)Spring かどうかを判定できます。

function settleSpring(personality?: PersonalityInput): Spring;

押下・ホバー・チルトの解除に使うスプリングです。spring({ duration: duration(350, personality.tempo), bounce: personality.bounce }) と等価で、pressabletiltable が内部で使っています。

個性 知覚 duration bounce 静定時間
natural(既定) 350ms 0.2 475ms
snappy 280ms 0.1 375ms
calm 438ms 0 646ms
playful 350ms 0.45 704ms
bouncy 333ms 0.55 808ms
cartoon 402ms 0.65 1167ms
function useCascade<T extends HTMLElement = HTMLElement>(
input?: EnterInput,
options?: CascadeOptions,
): RefCallback<T>;
interface CascadeOptions extends OverlapOptions {
trigger?: unknown;
}
引数 / オプション 既定値 説明
input EnterInput "rise" TransitionKindenter レシピ名)、MotionSpec、または個性を受け取って spec を返す関数
trigger unknown なし この値が変わるとカスケードを再生し直す
drag / each / cap / total / from overlap と同じ overlap にそのまま渡される

返り値の ref をコンテナに付けると、マウント時と trigger が変わったときに、その時点の子要素(HTMLElement のみ)を数えて overlap を作り、各子に再生します。個性と reducedMotionMotionProvider から読みます。子要素が後から増えても自動では再生しないので、trigger を変えてください。

import { animateTo, easings, enter, followThrough, overlap, play, playAll, spring } from "twelve-principles";
// トースト: 下から上がり、わずかに行き過ぎて止まる
const toast = document.querySelector<HTMLElement>(".toast")!;
play(
toast,
followThrough(
{ duration: 300, frames: [{ y: 24, opacity: 0, easing: easings.out }, { y: 0, opacity: 1 }] },
{ bounce: 0.25 },
),
);
// 検索結果: 上から順に 40ms ずつ遅れて入場し、後ろほど弾む
const list = document.querySelector<HTMLElement>("#results")!;
const items = Array.from(list.children) as HTMLElement[];
const specs = overlap(enter("rise"), items.length, { each: 40, drag: 0.08 });
await playAll(items.map((element, i) => ({ element, spec: specs[i]! })));
// トグル: スプリングはモジュールで一度だけ作る
const KNOB = spring({ duration: 300, bounce: 0.3 });
const knob = document.querySelector<HTMLElement>(".switch-knob")!;
let on = false;
knob.parentElement!.addEventListener("click", () => {
on = !on;
animateTo(knob, { x: on ? 20 : 0 }, { transition: KNOB }); // 再生時間は KNOB.duration(492ms)
});
  • Anticipation: followThrough(anticipate(spec), { bounce: 0.4 }) で「溜め → 動作 → 行き過ぎて落ち着く」の一連が作れます。anticipate は先頭に、followThrough は末尾に作用するので互いに干渉しません。
  • Squash & Stretch: pressable は押下で潰し、settleSpring で弾みながら戻します。
  • Slow In & Slow Out: スプリングは減速を内包したイージングです。焼き込みを避けたい場合は、行き過ぎ量が約 9.8% に固定された CSS 曲線 easings.overshoot が代わりになります。
  • Timing: overlap の遅延は staggerDelays / staggerDistancescap(既定 MAX_STAGGER_MS)に従います。
  • Secondary Action: 主動作の余韻として、別の要素に accompany を重ねます。
  • Appeal: 個性の bouncesettleSpringenter("pop") の弾みを決めます。
  • 派手な動き: rubberBand / jello / swing / wobble は、振れ幅が毎回小さくなる減衰振動としての follow through です。bounceIn はゆるいスプリング(bounce 0.5 以上)で到着します。