Squash & Stretch(潰しと伸ばし)
物体は衝撃を受けると潰れ、加速すると伸びます。twelve-principles はこの変形を「体積を保つ scaleX / scaleY の組」として表し、UI では 3〜5% の控えめな弾みとして使えるようにしています。
アニメーションにおける原則
Section titled “アニメーションにおける原則”Squash & Stretch は12原則の中でも最も基本的で、最も効果の大きい原則とされています。代表例は弾むボールです。落下中のボールは速度に応じて縦に伸び、地面に当たった瞬間に横へ潰れ、跳ね返ると再び伸びてから丸い形に戻ります。硬いボウリングの球はほとんど変形せず、ゴムボールは大きく変形する、という違いがそのまま「素材の硬さ」と「重さ」として観客に伝わります。
重要なのは 体積を一定に保つ ことです。縦に潰れた分だけ横に膨らまなければ、物体は縮んだり膨らんだりしているように見え、変形ではなく大きさの変化として読まれてしまいます。
この原則はボールに限らず、キャラクターの顔(驚いて目を見開く、笑って頬が持ち上がる)や、筋肉の伸縮にも使われます。速い動きの途中で伸ばすことは、コマ間の飛びを埋めてモーションブラーの代わりにもなります。
UI への翻訳
Section titled “UI への翻訳”UI の要素はキャラクターではないため、変形量はずっと小さくします。変形量の目安は次のとおりです。
| 用途 | 変形量 | 例 |
|---|---|---|
| 通常の UI | 0.03〜0.05(±3〜5%) | カートのバッジ、通知の着地、ドロップ完了 |
| 遊び心のあるブランド | 0.08 前後 | playful 個性の squash 値 |
| 派手な演出 | 0.12 前後 | bouncy 個性の squash 値。祝福やキャンペーンページ |
| キャラクター・マスコット | 0.2 以上 | cartoon 個性(0.22)、イラスト、空の状態のキャラクター |
UI で効果が高いのは「何かが到着した」「何かが当たった」瞬間です。カートに商品が入ったときのバッジ、ドラッグしたカードがドロップ位置に収まったとき、トーストが画面下端に着地したときなどです。変形は 300ms 程度で完全に元へ戻るため、レイアウトには影響しません。
一方で、テキストが主役の要素(段落、入力欄、テーブル)を潰すと文字まで歪み、読みにくく安っぽく見えます。大きなカードやモーダル全体に使うのも避け、アイコンやバッジ、ボールのような「物体らしい」小さな要素に限定するのが安全です。押下のフィードバックは Squash & Stretch ではなく、pressable の一様な縮小(ガードレールのある Personality では scale 0.95〜0.98)で表現するほうが自然です。
deform
Section titled “deform”function deform(amount: number, axis?: Axis): Pose; // axis 既定 "y"type Axis = "x" | "y";体積を保つ変形ポーズを返す基本関数です。axis 方向の倍率を 1 + amount、直交方向の倍率をその逆数にするので、常に scaleX × scaleY = 1 が成り立ちます。
amount > 0でaxis方向に伸び、amount < 0で潰れます。amount = 0は静止状態(scaleX: 1, scaleY: 1)です。amountは-1より大きい必要があり、それ以下ではRangeErrorを投げます。- 例:
deform(-0.05)はscaleX ≈ 1.0526、scaleY = 0.95。deform(0.3, "x")はscaleX = 1.3、scaleY ≈ 0.769。
戻り値は scaleX / scaleY だけを持つ Pose なので、x / y / scale など他のキーとスプレッドで合成できます(scale とは CSS 上で乗算されます)。
squash / stretch
Section titled “squash / stretch”function squash(amount?: number, axis?: Axis): Pose; // amount 既定 0.05、axis 既定 "y"function stretch(amount?: number, axis?: Axis): Pose; // amount 既定 0.05、axis 既定 "y"deform の読みやすい別名です。squash は deform(-|amount|, axis)、stretch は deform(|amount|, axis) を返すため、amount の符号は無視されます。
squashStretch
Section titled “squashStretch”function squashStretch(options?: ImpactOptions): MotionSpec;衝突の反応を1本のモーションとして返します。接地 → 潰れ → 反発して伸びる → 小さく潰れる → 静止、の順に、反発のたびにエネルギーが減衰していくので、重さとして読まれます。
| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
intensity |
number |
0.05 |
最大の変形量。UI は 0.03〜0.05、キャラクターは 0.2 以上が目安。 |
axis |
"x" | "y" |
"y" |
衝撃の方向(力が進んできた軸)。 |
duration |
number |
300 |
尺(ms)。 |
origin |
string |
axis が "y" なら "50% 100%"、"x" なら "0% 50%" |
transform-origin。物体が面に接する点。既定は下端(y)/ 左端(x)。 |
生成されるキーフレームは次のとおりです(intensity = 0.05、axis = "y" の場合)。
| offset | ポーズ | 次区間のイージング |
|---|---|---|
| 0 | 静止(deform(0)) |
easings.out |
| 0.25 | squash(intensity) = scaleY 0.95 |
easings.inOut |
| 0.55 | stretch(intensity × 0.5) = scaleY 1.025 |
easings.inOut |
| 0.8 | squash(intensity × 0.15) = scaleY 0.9925 |
easings.inOut |
| 1 | 静止 | — |
カートに商品を追加したとき、バッジを弾ませます。バッジは床に接していないので、原点は中央にします。
import { play, squashStretch } from "twelve-principles";
const badge = document.querySelector<HTMLElement>(".cart-badge")!;
export function setCartCount(count: number) { badge.textContent = String(count); play(badge, squashStretch({ intensity: 0.08, duration: 250, origin: "50% 50%" }));}キーポーズを自分で組む場合は、deform / squash / stretch を poseToPose のフレームに展開します。右へ素早く移動して壁で止まるチップの例です。移動中は進行方向に伸び、到着で潰れます。
import { deform, play, poseToPose, squash, stretch } from "twelve-principles";
const chip = document.querySelector<HTMLElement>(".chip")!;
play( chip, poseToPose( [ { x: 0, ...deform(0, "x") }, { x: 160, ...stretch(0.1, "x"), offset: 0.55 }, { x: 240, ...squash(0.06, "x"), offset: 0.8 }, { x: 240, ...deform(0, "x") }, ], { duration: 300, origin: "100% 50%" }, ),);useMotion の play には、個性(Personality)を受け取って spec を返す関数を渡せます。MotionProvider の個性に合わせて変形量が変わります(natural は 0.04、playful は 0.08)。
import { useEffect, useRef } from "react";import { squashStretch } from "twelve-principles";import { useMotion } from "twelve-principles/react";
export function CartBadge({ count }: { count: number }) { const { ref, play } = useMotion<HTMLSpanElement>(); const previous = useRef(count);
useEffect(() => { if (count > previous.current) { play((p) => squashStretch({ intensity: Math.max(p.squash, 0.03), origin: "50% 50%" })); } previous.current = count; }, [count, play]);
return ( <span ref={ref} className="cart-badge" aria-live="polite"> {count} </span> );}注意を引くための「跳ねる」動きなら、Anticipation と Squash & Stretch をまとめた jump レシピが使えます。
import { jump } from "twelve-principles";import { useMotion } from "twelve-principles/react";
export function Mascot() { const { ref, play } = useMotion<HTMLSpanElement>(); return ( <button type="button" onClick={() => play((p) => jump({ personality: p }))}> <span ref={ref} className="mascot" aria-hidden="true" /> 話しかける </button> );}ガイドラインと落とし穴
Section titled “ガイドラインと落とし穴”- Anticipation — 跳ぶ前にしゃがむ「溜め」は、それ自体が潰れの形です。
jumpレシピはしゃがみ(squash)→ 離陸(stretch)→ 着地(squash)を1本にまとめています。 - Straight Ahead & Pose to Pose —
poseToPoseのフレームにdeformを展開すれば、任意の位置で伸び縮みさせられます。物理シミュレーションならstraightAheadのdraw関数の中で速度から変形量を計算します。 - Follow Through & Overlapping Action —
squashStretchの反発は、減衰していく follow through の一種です。 - Exaggeration —
exaggerate(squashStretch(), 2)で変形量だけを2倍にできます。 - Appeal — 個性の
squash値(natural0.04、snappy0.03、calm0.02、playful0.08、bouncy0.12、cartoon0.22)で、プロダクト全体の柔らかさを一か所で決められます。 - 派手な動き —
rubberBandは体積を保ったまま大きく伸び縮みを繰り返し、fallInは落下の速度で伸びて着地で潰れます。変形量はrubberBandがexaggerationに、fallInが個性のsquashに比例します。 - レシピ —
jumpの詳細。