Anticipation(予備動作)
大きな動きの直前に、逆方向へ小さく「溜める」ことで、見る人はこれから何が起きるかを一瞬早く理解できます。twelve-principles では、既存のモーションの先頭に溜めのポーズを挿入する anticipate として提供しています。
アニメーションにおける原則
Section titled “アニメーションにおける原則”Anticipation は、主動作の前に置く準備動作です。ジャンプの前にしゃがむ、ボールを投げる前に腕を後ろへ引く、走り出す前に体を沈める、といった動きがこれにあたります。現実の体は力を溜めないと大きく動けないので、溜めがない動きは唐突で、重さのない物体が瞬間移動したように見えます。
もう一つの役割は 観客の視線の誘導 です。アニメーションの観客は、画面のどこで何が起きるかを知りません。溜めによって「ここで何かが起きる」と予告しておけば、主動作が速くても見逃されません。キャラクターが画面外を見やってから別のキャラクターが登場する、というのも同じ原則の応用です。
溜めの大きさと長さは、主動作の大きさとつり合っている必要があります。大きな跳躍には深いしゃがみが、軽い会釈にはわずかな沈み込みが対応します。
UI への翻訳
Section titled “UI への翻訳”UI では、溜めは「これから要素が動く方向」を先に示す合図になります。溜めの大きさは主動作の移動量に対する割合で決めるのが扱いやすく、anticipate の既定値は 移動量の 15%、溜めに使う時間は タイムラインの 30% です。180px の移動なら、先に逆方向へ 27px 下がってから進みます。
向いている場面:
- 削除・送信・アーカイブなど、要素が画面から去る動き(去る方向を予告する)。
- ポップアップが消える前に一瞬ふくらむ
exit("pop")のような、注意を引く退場。 - オンボーディングやマスコットなど、演出として見せる動き。
避けるべき場面:
- ドラッグ、スライダー、トグルなど、ユーザーの操作に直接追従する動き。溜めの間は目標と逆方向に動くので、遅延や誤作動に見えます。
- 頻繁に繰り返す小さな遷移(タブ切り替え、ホバー)。毎回の溜めが疲労につながります。
溜めを入れると、主動作に使える時間が減るか、全体の尺が延びるかのどちらかになります。fit: "compress"(既定)は総尺を保つので、300ms の移動なら溜めに 90ms、主動作に 210ms を使います。fit: "extend" は主動作の 300ms を保ち、全体が Math.round(300 / (1 - 0.3)) = 429ms に延びます。ユーザー起点の動きは 300ms 以内(USER_INITIATED_MAX_MS)に収めるのが原則なので、UI では "compress" を基本にしてください。
anticipate
Section titled “anticipate”function anticipate(spec: MotionSpec, options?: AnticipationOptions): MotionSpec;モーションの最初の区間の前に、溜めのフレームを挿入した新しい MotionSpec を返します。
| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
amount |
number |
0.15 |
溜めの大きさ。最初の移動量に対する割合。 |
share |
number |
0.3 |
タイムラインのうち溜めに使う割合。0 と 1 を含まない開区間。 |
fit |
"compress" | "extend" |
"compress" |
"compress" は総尺を維持(主動作が短くなる)。"extend" は元の尺を主動作に残し、総尺を Math.round(duration / (1 - share)) に延ばす。 |
動作の詳細:
- 溜めのポーズは 最初の2フレーム から
windUp(frames[0], frames[1], amount)で計算します。複数のキーポーズを持つ spec でも、溜めは最初の区間の逆方向だけです。 - 結果のフレームは「元の先頭フレーム(offset 0、イージング
easings.inOut)→ 溜めのポーズ(offsetshare)→ 元の2フレーム目以降」になります。元のフレームの offset はshareから 1 の範囲へ詰め直されます。 - 溜めのフレームには元の先頭フレームのイージング(未指定なら
easings.inOut)が付くので、主動作の加減速は元の spec のまま保たれます。 - spec 全体のイージング(
spec.easing)は、フレームを挿入する前に各区間のイージングへ畳み込まれます。溜めの区間が全体のイージングで引き伸ばされることはありません。 amountが0、またはフレームが2つ未満なら、spec をそのまま返します。shareが 0 より大きく 1 より小さい範囲にない場合はRangeErrorを投げます。
windUp
Section titled “windUp”function windUp(from: Pose, to: Pose, amount?: number): Pose; // amount 既定 0.15from から to へ動くときの溜めのポーズを返します。anticipate が内部で使っている関数で、キーポーズを手で組むときに使えます。
| 引数 | 型 | 既定値 | 説明 |
|---|---|---|---|
from |
Pose |
— | 動き出す前のポーズ。 |
to |
Pose |
— | 最初の目標ポーズ。 |
amount |
number |
0.15 |
from からの逆方向の偏差の割合。 |
to に含まれる空間的なキー(opacity 以外)それぞれについて from + (to − from) × (−amount) を計算します。{ x: 0 } から { x: 240, opacity: 0 } への溜めは { x: -48 }(amount = 0.2)です。溜めは空間的な動きなので、opacity は引き戻しません。from に opacity があればその値を保ち、なければ結果から取り除きます。blur と elevation は 0 未満にならないようクランプされます。
easings.anticipate
Section titled “easings.anticipate”easings.anticipate; // "cubic-bezier(0.36, 0, 0.66, -0.56)"溜めをイージングカーブに埋め込んだものです。進捗は一度マイナス側へ下がり(最小で約 −0.098、時間の 43% 付近)、その後加速して終端に達します。終端で減速しないため、退場のように「勢いをつけて去る」動きに向いています。
anticipate との違い:
anticipate() |
easings.anticipate |
|
|---|---|---|
| 溜めの表現 | 独立したポーズフレームを挿入 | 1区間のイージングに内蔵 |
| 溜めの大きさ・時間 | amount / share で個別に調整 |
移動量の約 10%、固定 |
| 主動作の加減速 | 元の spec のイージングを維持 | 終端で減速しない(ease-in 的) |
| 対象キー | 空間的なキーのみ(opacity は引き戻さない) | 区間内のすべてのプロパティ |
exit(“pop”) に組み込まれた溜め
Section titled “exit(“pop”) に組み込まれた溜め”レシピ exit("pop") は、個性の anticipation が 0 より大きいとき、anticipate(spec, { amount: p.anticipation, share: 0.35 }) で一度ふくらんでから縮んで消えます。
| 個性 | 尺 | 溜め(offset 0.35) | 終端 |
|---|---|---|---|
natural |
150ms | scale 1.0225 | scale 0.85、opacity 0 |
snappy |
120ms | scale 1.0108 | scale 0.865、opacity 0 |
calm |
188ms | なし(anticipation が 0) |
scale 0.895、opacity 0 |
playful |
150ms | scale 1.0525 | scale 0.79、opacity 0 |
スワイプで消すカード。右へ去る前に左へ少し引き、去る方向を予告します。総尺は durations.slow(300ms)のままです。
import { anticipate, durations, easings, play, poseToPose } from "twelve-principles";
export async function dismiss(card: HTMLElement) { const away = poseToPose( [ { x: 0, opacity: 1, easing: easings.in }, { x: 320, opacity: 0 }, ], { duration: durations.slow }, ); // 溜めは x: -32。opacity は 1 のまま保たれる await play(card, anticipate(away, { amount: 0.1 })).finished; card.remove();}トーストを閉じるときは、レシピの exit("pop") がそのまま使えます。
import { exit, play } from "twelve-principles";
export async function closeToast(toast: HTMLElement) { await play(toast, exit("pop", { personality: "playful" })).finished; toast.hidden = true;}送信ボタンの紙飛行機アイコン。後ろへ少し下がってから右上へ飛び去り、persist: false で再生後に元の位置へ戻ります。溜めの大きさは個性の anticipation を使うので、calm では溜めなしになります。
import { anticipate, easings } from "twelve-principles";import { useMotion } from "twelve-principles/react";
export function SendButton({ onSend }: { onSend: () => void }) { const { ref, play } = useMotion<HTMLSpanElement>();
const send = async () => { const flight = play( (p) => anticipate( { duration: 250, frames: [ { x: 0, y: 0, rotate: 0, easing: easings.in }, { x: 24, y: -24, rotate: -10, opacity: 0 }, ], }, { amount: p.anticipation }, ), { persist: false }, ); await flight?.finished; onSend(); };
return ( <button type="button" onClick={send}> <span ref={ref} className="icon icon-send" aria-hidden="true" /> 送信 </button> );}マウント・アンマウントを伴う要素は Presence の exit="pop" に任せられます。退場が終わるまでアンマウントされません。
import { Presence } from "twelve-principles/react";
export function SavedToast({ open }: { open: boolean }) { return ( <Presence show={open} enter="pop" exit="pop" className="toast" role="status"> 保存しました </Presence> );}ガイドラインと落とし穴
Section titled “ガイドラインと落とし穴”- Squash & Stretch — しゃがむ溜めは潰れそのものです。
jumpレシピは、個性にanticipationがあればしゃがみを深くします。 - Follow Through & Overlapping Action — 溜めて動き、行き過ぎて落ち着く:
followThrough(anticipate(poseToPose([{ x: 0 }, { x: 200 }])), { bounce: 0.4 })。 - Slow In & Slow Out —
easings.anticipateはイージングとしての溜め。anticipate()は元のイージングを保ったまま溜めを足します。 - Timing —
fitの選び方は時間予算の問題です。 - Appeal — 個性の
anticipation(natural0.15、snappy0.08、calm0、playful0.25)でプロダクト全体の溜めの強さを揃えられます。 - レシピ —
exit("pop")とjumpの詳細。