Follow Through & Overlapping Action(フォロースルーと重なり合う動作)
本体が止まっても、ゆるく付いた部分は慣性で行き過ぎてから戻り、各部位は少しずつ遅れて動く。followThrough は動きの終端をスプリングに置き換えて行き過ぎを作り、overlap は複数の要素を時間差で動かして、後ろの要素ほど大きく揺らします。
アニメーションにおける原則
Section titled “アニメーションにおける原則”Follow through と overlapping action は、ディズニーの 12 原則では一つの項目として扱われます。どちらも「キャラクターは一枚の硬い板ではなく、重さと柔らかさの違う部位の集まりである」という観察から来ています。
Follow through は、主体が止まった後も付随する部分が動き続けることです。走ってきたキャラクターが急停止すると、体は止まっても髪やマント、犬の耳は前へ流れ、行き過ぎてから元の位置に落ち着きます。投球動作で腕を振り切った後も体が回り続けるのも同じ現象です。
Overlapping action(drag とも呼ばれる)は、部位ごとに動き出しと止まりのタイミングがずれることです。腕を振るとき、上腕が先に動き、前腕、手首、指先の順に遅れてついていきます。末端の軽い部位ほど遅れ、大きく揺れます。全部位が同じフレームで動き出して同じフレームで止まると、動きは機械的で重さのないものに見えます。
UI への翻訳
Section titled “UI への翻訳”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 に始まります。祝福の演出のように時間差そのものを見せたいときは、cap と total を明示して広げます(派手な動き)。
followThrough
Section titled “followThrough”function followThrough(spec: MotionSpec, options?: FollowThroughOptions): MotionSpec;| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
bounce |
number |
0.3 |
終端での行き過ぎの強さ。0 = なし、0.3 = 生き生き、0.6 = ゆるい |
動作の詳細:
- フレームが 2 つ未満、または
bounceが 0 以下のときはspecをそのまま返します。 - まず
withoutGlobalEasingでspec.easing(イテレーション全体のイージング)をフレーム側へ移し、キーポーズを保ったまま平坦化します。 - 最後の区間(最後から 2 番目のフレームの
easing)をspring({ bounce })に置き換えます。その区間に元々付いていたイージングは捨てられます。2 フレームの spec なら動き全体がスプリングになります。 - スプリングはその区間の長さに正規化されるので、
spec.durationは変わりません。スプリングの知覚duration(400ms)は使われず、bounceによる曲線の形だけが効きます。 - JS 関数のイージングを含むため、
compile時に 60fps の線形キーフレーム(12〜120 枚)へ焼き込まれます。
overlap
Section titled “overlap”function overlap(spec: MotionSpec, count: number, options?: OverlapOptions): MotionSpec[];OverlapOptions は StaggerOptions を拡張しています。
| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
drag |
number |
0.1 |
起点から n ステップ離れたパートの bounce を drag × n にする(上限 0.8) |
each |
number |
30 |
隣り合うパートの遅延(ms)。cap でクランプ |
cap |
number |
50(MAX_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] になり、中央から外側へ広がります。
spring
Section titled “spring”function spring(options?: SpringOptions): Spring;
interface Spring extends EasingFn { readonly duration: number; // 静定時間(ms) readonly stiffness: number; readonly damping: number; readonly mass: number;}減衰調和振動子をイージング関数として返します。パラメータは「知覚」と「物理」の 2 通りで指定でき、stiffness か damping のどちらかを渡すと物理パラメータが優先されます。
| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
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)² × mass、damping = 4π × (1 − bounce) × mass / duration(durationは秒)に変換します。減衰比は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 通りあります。animateTo の transition に渡すと、.duration がそのまま再生時間になり、物理的な時間感覚が保たれます。PoseFrame.easing や MotionSpec.easing に渡すと、その区間やイテレーションの長さに曲線が引き伸ばされます。isSpring(value) で Spring かどうかを判定できます。
settleSpring
Section titled “settleSpring”function settleSpring(personality?: PersonalityInput): Spring;押下・ホバー・チルトの解除に使うスプリングです。spring({ duration: duration(350, personality.tempo), bounce: personality.bounce }) と等価で、pressable と tiltable が内部で使っています。
| 個性 | 知覚 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 |
useCascade(React)
Section titled “useCascade(React)”function useCascade<T extends HTMLElement = HTMLElement>( input?: EnterInput, options?: CascadeOptions,): RefCallback<T>;
interface CascadeOptions extends OverlapOptions { trigger?: unknown;}| 引数 / オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
input |
EnterInput |
"rise" |
TransitionKind(enter レシピ名)、MotionSpec、または個性を受け取って spec を返す関数 |
trigger |
unknown |
なし | この値が変わるとカスケードを再生し直す |
drag / each / cap / total / from |
overlap と同じ |
overlap にそのまま渡される |
返り値の ref をコンテナに付けると、マウント時と trigger が変わったときに、その時点の子要素(HTMLElement のみ)を数えて overlap を作り、各子に再生します。個性と reducedMotion は MotionProvider から読みます。子要素が後から増えても自動では再生しないので、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)});import { useState } from "react";import { spring } from "twelve-principles";import { useCascade, useMotion } from "twelve-principles/react";
type Result = { id: string; title: string };
export function ResultList({ results }: { results: Result[] }) { // results が差し替わるたびに、新しい子要素でカスケードを再生する const ref = useCascade<HTMLUListElement>("rise", { each: 40, drag: 0.1, trigger: results }); return ( <ul ref={ref}> {results.map((r) => ( <li key={r.id}>{r.title}</li> ))} </ul> );}
const KNOB = spring({ duration: 300, bounce: 0.3 });
export function Switch({ label }: { label: string }) { const [on, setOn] = useState(false); const knob = useMotion<HTMLSpanElement>(); const toggle = () => { const next = !on; setOn(next); knob.animateTo({ x: next ? 20 : 0 }, { transition: KNOB }); }; return ( <button role="switch" aria-checked={on} aria-label={label} className="switch" onClick={toggle}> <span ref={knob.ref} className="switch-knob" /> </button> );}ガイドラインと落とし穴
Section titled “ガイドラインと落とし穴”- 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/staggerDistancesとcap(既定MAX_STAGGER_MS)に従います。 - Secondary Action: 主動作の余韻として、別の要素に
accompanyを重ねます。 - Appeal: 個性の
bounceがsettleSpringとenter("pop")の弾みを決めます。 - 派手な動き:
rubberBand/jello/swing/wobbleは、振れ幅が毎回小さくなる減衰振動としての follow through です。bounceInはゆるいスプリング(bounce 0.5 以上)で到着します。