Exaggeration(誇張)
現実どおりの動きは画面上では弱く見えます。誇張は動きの本質を強調するための道具であり、UI ではブランドの性格に合わせて振れ幅を上げ下げするつまみとして使います。
アニメーションにおける原則
Section titled “アニメーションにおける原則”誇張は、現実の動きや形を強めて、観客に意図を確実に伝える技法です。驚いた顔は目を実際より大きく見開き、重い荷物を持ち上げる動作は体を大きく反らせます。現実どおりに描いた動きは、画面上では単調で退屈に見えがちです。
誇張は「何でも大きくする」ことではありません。その場面で伝えたい一点(驚き、重さ、勢い)を選び、そこだけを強めます。すべてを誇張すると、どこを見ればよいかがかえってわからなくなります。
誇張は他の原則の強さを決めるつまみでもあります。潰れと伸び(Squash & Stretch)を深くする、溜め(Anticipation)を大きくする、軌道の弧(Arcs)を膨らませる、といった形で現れます。
UI への翻訳
Section titled “UI への翻訳”UI の誇張は、一つのモーションの「振れ幅」を倍率で調整することです。本ライブラリでは、静止状態(REST)からの偏差に倍率を掛ける演算として実装しています。
factor > 1で強調、factor < 1で抑制、factor = 0で静止状態、負の値で反転します。反転は Anticipation の溜めポーズの作り方そのものです。- 既定では
opacityを対象から外します。見え方(透明度)の変化は誇張すべき「動き」ではないためです。 - 尺(
duration)やイージングは変えません。同じ時間で移動量が増えるので、倍率を上げると速度も上がります。
用途ごとの目安は次のとおりです。
| 用途 | 倍率の目安 | 理由 |
|---|---|---|
| 管理画面・データの多い画面 | 0.5〜0.8 | 動きを情報より目立たせない |
| 一般的なボタン・カード | 0.9〜1.1 | 既定値のまま |
| 達成・成功・オンボーディング | 1.3〜2 | 一度きりの場面で感情を伝える。bouncy 個性は 1.7 |
| ゲーム的な演出 | 2 以上 | 常用すると疲れる。cartoon 個性は 2.4 |
Personality の exaggeration(natural 1、snappy 0.9、calm 0.7、playful 1.4、bouncy 1.7、cartoon 2.4)は、この倍率をプロダクト全体で揃えるための値です。レシピと振る舞いは次の量にこの値を掛けます。
| 対象 | 既定の量 | calm(0.7) |
playful(1.4) |
bouncy(1.7) |
cartoon(2.4) |
|---|---|---|---|---|---|
enter / exit の移動距離 |
12px | 8.4px | 16.8px | 20.4px | 28.8px |
enter("pop") の開始スケール |
0.85 | 0.895 | 0.79 | 0.745 | 0.64 |
jump の高さ |
16px | 11.2px | 22.4px | 27.2px | 38.4px |
shake の振幅 |
6px | 4.2px | 8.4px | 10.2px | 14.4px |
hoverable の浮上量 |
level 6 |
4.2 | 8.4 | 10.2 | 14.4 |
tiltable の最大角度 |
8deg | 5.6deg | 11.2deg | 13.6deg | 19.2deg |
派手な動き のレシピ(rubberBand / jello / tada / heartbeat / swing / wobble / flip / bounceIn / fallIn)も、振れ幅を exaggeration に比例させています。たとえば swing の最大角度は natural で 15deg、cartoon で 36deg です。ただし要素が壊れないよう、flip の手前への浮き上がりは倍率 2、bounceIn の開始スケールは 0.05、rubberBand の潰れは -0.9 で頭打ちになります。
レシピは exaggerate を内部で使っているので、入場時の opacity: 0 は Personality に関係なく 0 のままです。
exaggerate
Section titled “exaggerate”function exaggerate(target: MotionSpec, factor: number, options?: ExaggerateOptions): MotionSpec;function exaggerate<P extends Pose>(target: P, factor: number, options?: ExaggerateOptions): P;target に frames があれば MotionSpec として全フレームを、なければ Pose として 1 つのポーズを変換します。各キーは rest + (value - rest) * factor になります。factor が有限数でなければ RangeError を投げます。
| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
rest |
Pose |
REST |
偏差を測る基準ポーズ。指定しないキーは REST の値を使う |
keys |
readonly PoseKey[] |
opacity 以外の全キー |
倍率を掛けるキー |
MotionSpec を渡した場合、duration・delay・easing・origin などのフィールドと、各フレームの offset・easing はそのまま残ります。ポーズに存在しないキーは追加されません。
exaggerate({ y: -10, scale: 1.1, opacity: 0.5 }, 2);// { y: -20, scale: 1.2, opacity: 0.5 } opacity は既定で対象外
exaggerate({ y: -10, scale: 1.1 }, -0.5);// { y: 5, scale: 0.95 } 反転して半分
exaggerate({ x: 110 }, 2, { rest: { x: 100 } });// { x: 120 } 基準 100 からの偏差 10 を 2 倍scaleDeviation
Section titled “scaleDeviation”function scaleDeviation<P extends Pose>( pose: P, factor: number, rest?: Pose, // 既定 REST keys?: readonly PoseKey[], // 既定 opacity 以外の全キー): P;exaggerate がポーズ 1 つに対して使う下位関数です。オプションオブジェクトではなく位置引数を取ります。factor の検証はしません。
結果は次の値にクランプされます。
| キー | クランプ |
|---|---|
opacity |
0〜1 |
blur |
0 以上 |
elevation |
0 以上 |
scale / scaleX / scaleY はクランプされません(落とし穴を参照)。
const REST: Readonly<Required<Pose>>;// x, y, z, rotate, rotateX, rotateY, skewX, skewY, blur, elevation は 0// scale, scaleX, scaleY, opacity は 1場面によって振れ幅を変える
Section titled “場面によって振れ幅を変える”同じ jump でも、通知バッジでは控えめに、初めての実績解除では大きく跳ばせます。
import { enter, exaggerate, jump, play } from "twelve-principles";
// 通常の通知は控えめにexport function nudge(badge: HTMLElement): void { play(badge, exaggerate(jump(), 0.6));}
// 初めての実績解除だけは大きくexport function celebrate(trophy: HTMLElement): void { play(trophy, exaggerate(jump({ height: 20 }), 1.8));}
// 密度の高い画面では pop の縮小を半分に(開始スケール 0.85 → 0.925)export function showRow(row: HTMLElement): void { play(row, exaggerate(enter("pop"), 0.5));}import { exaggerate, jump } from "twelve-principles";import { useMotion } from "twelve-principles/react";
export function Trophy({ firstTime }: { firstTime: boolean }) { const { ref, play } = useMotion<HTMLButtonElement>(); const factor = firstTime ? 1.8 : 0.8; return ( <button ref={ref} type="button" className="trophy" onClick={() => play((p) => exaggerate(jump({ personality: p }), factor))} > 実績を見る </button> );}ポーズを誇張する
Section titled “ポーズを誇張する”ポーズを返す関数(lift、tiltToward、squash など)の結果にも使えます。
import { exaggerate, hoverable, lift } from "twelve-principles";
// lift(4) = { y: -2, elevation: 4 } → { y: -4, elevation: 8 }hoverable(featuredCard, { level: 0, pose: exaggerate(lift(4), 2) });import type { ReactNode } from "react";import { exaggerate, lift } from "twelve-principles";import { Motion } from "twelve-principles/react";
const featuredLift = exaggerate(lift(4), 2); // { y: -4, elevation: 8 }
export function FeaturedCard({ children }: { children: ReactNode }) { return ( <Motion as="article" className="card" hover={{ level: 0, pose: featuredLift }}> {children} </Motion> );}hoverable の pose は lift(level * exaggeration) の上に展開されるため、level: 0 にしておくと pose の値だけが使われます。
ガイドラインと落とし穴
Section titled “ガイドラインと落とし穴”- Squash & Stretch —
exaggerate(squashStretch(), 2)で衝撃の潰れを深くします。 - Anticipation —
windUpは負の倍率のscaleDeviationで溜めのポーズを作ります。反転と誇張は同じ演算です。 - Follow Through —
followThroughを適用したあとにexaggerateしても、スプリングのイージングはフレームに残ります。 - Timing — 振れ幅を変えたら尺も見直します。
- Appeal — Personality の
exaggerationが、すべてのレシピと振る舞いの基準倍率になります。 - 派手な動き —
exaggerate(tada(), 1.5)のように、派手なレシピにさらに倍率を掛けられます。cartoon(2.4)の振れ幅にさらに大きな倍率を掛けると、スケールが 0 を下回ることがあるので結果を確認してください。