コンテンツにスキップ

基本概念

twelve-principles は「動き」をデータとして扱います。データなので原則を関数として重ねられ、再生中の姿勢も正確に計算できます。

Pose ─▶ PoseFrame ─▶ MotionSpec ─compile()─▶ WAAPI keyframes ─play()─▶ Element
▲ │
└────┘ anticipate / followThrough / exaggerate / ...

Pose は要素の見た目を、変形していない「静止状態(REST)」を基準に表します。

キー 単位 静止値
x y z px 0
scale scaleX scaleY 倍率 1
rotate rotateX rotateY skewX skewY deg 0
opacity 0〜1 1
blur px 0
elevation 抽象的な高さ(0〜24 目安) 0

elevation は影に変換されます(Solid Drawing)。ポーズは絶対値です。{ y: -4 } は「今から 4px 上」ではなく「静止位置から 4px 上」を意味します。

2. MotionSpec — 時間軸に並べたポーズ

Section titled “2. MotionSpec — 時間軸に並べたポーズ”
interface MotionSpec {
frames: PoseFrame[]; // Pose + offset(0..1) + easing
duration: number; // ms
delay?: number;
easing?: Easing; // 全体にかかるイージング
iterations?: number;
direction?: PlaybackDirection;
origin?: string; // transform-origin
perspective?: number; // perspective(px)
}

PoseFrame.easing はそのフレームから次のフレームまでの区間に効きます(WAAPI と同じ規則)。offset を省くと WAAPI と同様に均等配置されます。イージングは CSS 文字列でも JS 関数(スプリングなど)でもよく、関数は compile() 時に 60fps 相当(12〜120 フレーム)のキーフレームへ焼き込まれます。そのため CSS の linear() に対応していないブラウザでもスプリングが動きます。

3. 原則は MotionSpec → MotionSpec の関数

Section titled “3. 原則は MotionSpec → MotionSpec の関数”
const base = enter("rise"); // レシピ
const wound = anticipate(base); // 2. 予備動作を前置
const loose = followThrough(wound); // 5. 終端でオーバーシュート
const loud = exaggerate(loose, 1.4); // 10. 振れ幅を 1.4 倍
play(card, loud);

どれも入力を変更せず新しい spec を返します。全体イージング付きの spec にフレームを差し込む処理(anticipate / followThrough)は、先に withoutGlobalEasing() でイージングを各区間へ移してから行うため、キーポーズの形が崩れません。

4. 再生 — 1 要素に 1 モーション

Section titled “4. 再生 — 1 要素に 1 モーション”

play(el, spec) は Web Animations API で再生し、Animation を返します。

  • 置き換え: 同じ要素で新しいモーションを始めると、前のモーションは止まります。
  • 割り込み: currentPose(el) は再生中の姿勢を JS で正確に返します。animateTo(el, pose) はそこから始めるので、途中で方向を変えても飛びません。
  • 終了時: 終点が静止状態なら、ライブラリが管理するインラインスタイル(transform transform-origin opacity filter box-shadow)を消し、要素をスタイルシートに返します。それ以外の終点は commitStyles() でインラインに固定し、アニメーションを解放します。

5. レイヤー — 独立した状態を合成する

Section titled “5. レイヤー — 独立した状態を合成する”

ホバーで持ち上がり、押すと潰れ、ポインタに向かって傾く。これらは別々のビヘイビアですが、同じ transform を奪い合います。setLayer(el, name, pose) は名前付きレイヤーを持ち、平行移動・回転・高さは足し算、拡大率・不透明度は掛け算で合成してから遷移します。

setLayer(card, "hover", { y: -3, elevation: 6 });
setLayer(card, "press", { scale: 0.97 }); // hover を保ったまま縮む
setLayer(card, "press", null); // press だけ外す
LIVE
ホバーで持ち上がり、ポインタに向かって傾く
hover レイヤー(持ち上げ)と tilt レイヤー(傾き)が同時に効いている。

Personality はテンポ・誇張・弾み・予備動作・潰しの 5 値と、UI ガイドラインの上限を守るかどうかを決める guardrails の組で、レシピとビヘイビアはすべてここから値を読みます。アプリ全体で 1 つの性格を使うことが、12 番目の原則 Appeal の実装です。

名前 tempo exaggeration bounce anticipation squash guardrails
natural 1 1 0.2 0.15 0.04 true
snappy 0.8 0.9 0.1 0.08 0.03 true
calm 1.25 0.7 0 0 0.02 true
playful 1 1.4 0.45 0.25 0.08 true
bouncy 0.95 1.7 0.55 0.3 0.12 false
cartoon 1.15 2.4 0.65 0.45 0.22 false

既定の guardrails: true では、押下の縮みなどのフィードバックが UI ガイドラインの範囲に収まります。bouncycartoon はそれを外した派手な用途向けのプリセットです(派手な動き)。