コンテンツにスキップ

Squash & Stretch(潰しと伸ばし)

物体は衝撃を受けると潰れ、加速すると伸びます。twelve-principles はこの変形を「体積を保つ scaleX / scaleY の組」として表し、UI では 3〜5% の控えめな弾みとして使えるようにしています。

LIVE
UI (±5%) と Cartoon (±30%) を押し比べて、同じ関数でも強さで印象が変わることを確認してください。jump() は個性の切り替えに追従します。

Squash & Stretch は12原則の中でも最も基本的で、最も効果の大きい原則とされています。代表例は弾むボールです。落下中のボールは速度に応じて縦に伸び、地面に当たった瞬間に横へ潰れ、跳ね返ると再び伸びてから丸い形に戻ります。硬いボウリングの球はほとんど変形せず、ゴムボールは大きく変形する、という違いがそのまま「素材の硬さ」と「重さ」として観客に伝わります。

重要なのは 体積を一定に保つ ことです。縦に潰れた分だけ横に膨らまなければ、物体は縮んだり膨らんだりしているように見え、変形ではなく大きさの変化として読まれてしまいます。

この原則はボールに限らず、キャラクターの顔(驚いて目を見開く、笑って頬が持ち上がる)や、筋肉の伸縮にも使われます。速い動きの途中で伸ばすことは、コマ間の飛びを埋めてモーションブラーの代わりにもなります。

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)で表現するほうが自然です。

function deform(amount: number, axis?: Axis): Pose; // axis 既定 "y"
type Axis = "x" | "y";

体積を保つ変形ポーズを返す基本関数です。axis 方向の倍率を 1 + amount、直交方向の倍率をその逆数にするので、常に scaleX × scaleY = 1 が成り立ちます。

  • amount > 0axis 方向に伸び、amount < 0 で潰れます。amount = 0 は静止状態(scaleX: 1, scaleY: 1)です。
  • amount-1 より大きい必要があり、それ以下では RangeError を投げます。
  • 例: deform(-0.05)scaleX ≈ 1.0526scaleY = 0.95deform(0.3, "x")scaleX = 1.3scaleY ≈ 0.769

戻り値は scaleX / scaleY だけを持つ Pose なので、x / y / scale など他のキーとスプレッドで合成できます(scale とは CSS 上で乗算されます)。

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 の読みやすい別名です。squashdeform(-|amount|, axis)stretchdeform(|amount|, axis) を返すため、amount の符号は無視されます。

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.05axis = "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 / stretchposeToPose のフレームに展開します。右へ素早く移動して壁で止まるチップの例です。移動中は進行方向に伸び、到着で潰れます。

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%" },
),
);
  • Anticipation — 跳ぶ前にしゃがむ「溜め」は、それ自体が潰れの形です。jump レシピはしゃがみ(squash)→ 離陸(stretch)→ 着地(squash)を1本にまとめています。
  • Straight Ahead & Pose to PoseposeToPose のフレームに deform を展開すれば、任意の位置で伸び縮みさせられます。物理シミュレーションなら straightAheaddraw 関数の中で速度から変形量を計算します。
  • Follow Through & Overlapping ActionsquashStretch の反発は、減衰していく follow through の一種です。
  • Exaggerationexaggerate(squashStretch(), 2) で変形量だけを2倍にできます。
  • Appeal — 個性の squash 値(natural 0.04、snappy 0.03、calm 0.02、playful 0.08、bouncy 0.12、cartoon 0.22)で、プロダクト全体の柔らかさを一か所で決められます。
  • 派手な動きrubberBand は体積を保ったまま大きく伸び縮みを繰り返し、fallIn は落下の速度で伸びて着地で潰れます。変形量は rubberBandexaggeration に、fallIn が個性の squash に比例します。
  • レシピjump の詳細。