コンテンツにスキップ

Anticipation(予備動作)

大きな動きの直前に、逆方向へ小さく「溜める」ことで、見る人はこれから何が起きるかを一瞬早く理解できます。twelve-principles では、既存のモーションの先頭に溜めのポーズを挿入する anticipate として提供しています。

LIVE
なし と anticipate() を交互に押して比べてください。押すたびに箱が左右 180px を往復し、anticipate() では動き出す前に逆方向へ小さく下がります。

Anticipation は、主動作の前に置く準備動作です。ジャンプの前にしゃがむ、ボールを投げる前に腕を後ろへ引く、走り出す前に体を沈める、といった動きがこれにあたります。現実の体は力を溜めないと大きく動けないので、溜めがない動きは唐突で、重さのない物体が瞬間移動したように見えます。

もう一つの役割は 観客の視線の誘導 です。アニメーションの観客は、画面のどこで何が起きるかを知りません。溜めによって「ここで何かが起きる」と予告しておけば、主動作が速くても見逃されません。キャラクターが画面外を見やってから別のキャラクターが登場する、というのも同じ原則の応用です。

溜めの大きさと長さは、主動作の大きさとつり合っている必要があります。大きな跳躍には深いしゃがみが、軽い会釈にはわずかな沈み込みが対応します。

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" を基本にしてください。

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)→ 溜めのポーズ(offset share)→ 元の2フレーム目以降」になります。元のフレームの offset は share から 1 の範囲へ詰め直されます。
  • 溜めのフレームには元の先頭フレームのイージング(未指定なら easings.inOut)が付くので、主動作の加減速は元の spec のまま保たれます。
  • spec 全体のイージング(spec.easing)は、フレームを挿入する前に各区間のイージングへ畳み込まれます。溜めの区間が全体のイージングで引き伸ばされることはありません。
  • amount0、またはフレームが2つ未満なら、spec をそのまま返します。
  • share が 0 より大きく 1 より小さい範囲にない場合は RangeError を投げます。
function windUp(from: Pose, to: Pose, amount?: number): Pose; // amount 既定 0.15

from から 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 があればその値を保ち、なければ結果から取り除きます。blurelevation は 0 未満にならないようクランプされます。

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;
}
  • Squash & Stretch — しゃがむ溜めは潰れそのものです。jump レシピは、個性に anticipation があればしゃがみを深くします。
  • Follow Through & Overlapping Action — 溜めて動き、行き過ぎて落ち着く: followThrough(anticipate(poseToPose([{ x: 0 }, { x: 200 }])), { bounce: 0.4 })
  • Slow In & Slow Outeasings.anticipate はイージングとしての溜め。anticipate() は元のイージングを保ったまま溜めを足します。
  • Timingfit の選び方は時間予算の問題です。
  • Appeal — 個性の anticipationnatural 0.15、snappy 0.08、calm 0、playful 0.25)でプロダクト全体の溜めの強さを揃えられます。
  • レシピexit("pop")jump の詳細。