twelve-principles
v0.1.1
Published
Disney's 12 principles of animation as composable TypeScript primitives for UI components. Web Animations API based, zero runtime dependencies, optional React bindings.
Maintainers
Readme
twelve-principles
ディズニーの「アニメーションの12原則」を、UI コンポーネント向けの合成可能なプリミティブとして実装した TypeScript ライブラリです。各原則を「ポーズを返す関数」「モーションを変換する関数」「要素に振る舞いを付ける関数」に落とし込み、Web Animations API (WAAPI) で再生します。ランタイム依存はゼロ、React バインディングは twelve-principles/react から任意で使えます(react >= 18 は optional peer dependency)。
npm i twelve-principles解説サイト(ライブデモ付き): https://twelve-principles.iru-yo.com
目次
クイックスタート
Vanilla
import { enter, play, pressable } from "twelve-principles";
const card = document.querySelector<HTMLElement>(".card")!;
play(card, enter("rise")); // 下からフェードイン(ease-out)
const button = document.querySelector<HTMLButtonElement>("button")!;
const cleanup = pressable(button); // 押下で縮み、離すとスプリングで戻る
// cleanup(); // リスナーを外し、静止状態に戻すReact
import { jump } from "twelve-principles";
import { Motion, MotionProvider, Presence, useMotion } from "twelve-principles/react";
function Mascot() {
const { ref, play } = useMotion<HTMLDivElement>();
return <div ref={ref} onClick={() => play((p) => jump({ personality: p }))}>!</div>;
}
export function App({ open }: { open: boolean }) {
return (
<MotionProvider personality="playful">
<Motion as="button" press hover enter="pop">
Save
</Motion>
<Presence show={open} enter="pop" exit="pop">
Dialog
</Presence>
<Mascot />
</MotionProvider>
);
}MotionProvider—personality(既定"natural")とreducedMotion(既定"auto")をサブツリー全体に配る。Motion—as(既定"div")、press/hover/tilt(booleanまたはオプション、既定false)、enter(レシピ名・spec・(personality) => spec、既定false)。Presence—showがfalseになっても退場モーションが終わるまでアンマウントしない。enter既定"rise"、exit既定"fade"、initial既定true(初回表示時にも入場を再生)。useMotion()—{ ref, play, animateTo, stop }。playは spec か(personality) => specを受け取る。
同等のフック: usePress / useHover / useTilt(引数 false で無効化)、useEnter(input = "rise")、usePresence(show, { enter, exit, initial }) → { present, ref }、useStage(active, options)、useCascade(input = "rise", options)、useMotionConfig()。
メンタルモデル
Pose → PoseFrame → MotionSpec → compile(spec) → WAAPI keyframes/options
└→ play(el, spec) → AnimationPose— 要素の見た目の状態。x/y/z(px)、scale/scaleX/scaleY、rotate/rotateX/rotateY/skewX/skewY(deg)、opacity、blur(px)、elevation(0 = 面に接地、~24 = 高く浮く)。PoseFrame—Poseにoffset(0..1、省略時は WAAPI と同じく均等配置)とeasing(そのフレームから始まる区間に適用)を加えたもの。MotionSpec—frames,duration(ms),delay,easing(イテレーション全体),iterations,direction,origin(transform-origin),perspective(px)。compile(spec)—{ keyframes, options }を返す純関数。play(el, spec, options)がそれをelement.animate()で再生しAnimationを返す。
設計上の約束:
- ポーズは静止状態に対する絶対値。
{ x: 10 }は「今の位置から +10px」ではなく「無変形の状態から 10px」。省略したキーは静止値(REST:x: 0,scale: 1,opacity: 1…)として扱われる。 - アニメーション対象の要素では、ライブラリが
transform/transform-origin/opacity/filter/box-shadowを所有する。 自前の CSS transform が必要なら、ラッパー要素に分けること。 - 1要素につき1モーション。 新しい
playは再生中のモーションを中断して置き換える。途切れなく繋ぎたいときはcurrentPose(el)(再生中ならその瞬間のポーズ、終了後なら最後の静止ポーズ)を起点にするか、それを内部で行うanimateTo(el, pose, options)を使う。 - 静止状態で終わるモーションはインラインスタイルを消す。 終端ポーズが
RESTと等しければ上記の所有プロパティをインラインから除去し、スタイルシートに制御を返す。それ以外はcommitStyles()で終端をインラインに固定してアニメーションを解放する(playのpersist: falseなら終了後に元へ戻る)。stop(el)はその場で止める。 - レイヤーで合成する。
setLayer(el, name, pose | null, options)は名前付きレイヤーを設定・解除し、全レイヤーの合成ポーズへ遷移する(平行移動・回転・elevation は加算、scale・opacity は乗算 —composePoses)。pressable/hoverable/tiltable/stageはそれぞれ"press"/"hover"/"tilt"/"stage"レイヤーを使うので、同じ要素に重ねても互いを上書きしない。
import { animateTo, currentPose, poseToPose, play, setLayer, spring } from "twelve-principles";
animateTo(el, { x: 120 }, { transition: spring({ bounce: 0.3 }) }); // 現在位置から中断なしに遷移
play(el, poseToPose([currentPose(el), { x: 0 }])); // 同じことを明示的に
setLayer(el, "drag", { rotate: 4 }); // 他のレイヤーと合成される12原則と API の対応
| # | 原則 | API |
| --- | --- | --- |
| 1 | Squash & stretch | deform, squash, stretch, squashStretch |
| 2 | Anticipation | anticipate, windUp |
| 3 | Staging | stagingPoses, stage, useStage |
| 4 | Straight ahead & pose to pose | straightAhead, poseToPose |
| 5 | Follow through & overlapping action | followThrough, overlap, spring, useCascade |
| 6 | Slow in & slow out | easings, cubicBezier |
| 7 | Arcs | arc, arcPoint |
| 8 | Secondary action | secondaryAction, accompany |
| 9 | Timing | durations, duration, travelDuration, staggerDelays, staggerDistances |
| 10 | Exaggeration | exaggerate |
| 11 | Solid drawing | lift, tiltToward, solid, shadowForElevation, tiltable |
| 12 | Appeal | personalities, definePersonality, MotionProvider |
1. Squash & stretch
物体は衝撃で潰れ、加速すると伸びる。体積を保ったまま変形させることで、重さと柔らかさが伝わる。UI では着地・押下・通知バッジの弾みなど、ごく小さな変形(3〜5%)として使う。
deform(amount, axis = "y")—scaleX × scaleY = 1を保つ変形ポーズ。amount > 0で伸び、< 0で潰れ(> -1が必要)。squash(amount = 0.05, axis = "y")/stretch(amount = 0.05, axis = "y")squashStretch({ intensity = 0.05, axis = "y", duration = 300, origin })— 接地 → 潰れ → 反発で伸び → 小さく潰れ → 静止。originの既定はaxisが"y"なら"50% 100%"、"x"なら"0% 50%"。
play(badge, squashStretch({ intensity: 0.08 }));2. Anticipation
動作の前に逆方向の小さな「溜め」を入れ、これから何が起きるかを観客に予告する(ジャンプ前にしゃがむ、投げる前に腕を引く)。UI では削除・送信など大きな動きの直前に短い逆方向の動きを置く。
anticipate(spec, { amount = 0.15, share = 0.3, fit = "compress" })— 最初の区間の前に溜めフレームを挿入。shareはタイムライン中の溜めの割合(0〜1 の開区間)。"compress"は総尺を維持(ユーザー起点の時間予算を守る)、"extend"は元の動作の尺を維持して全体を延ばす。windUp(from, to, amount = 0.15)—from → toに対する溜めポーズ(空間的なキーのみ。opacity は引き戻さない)。
play(card, anticipate(poseToPose([{ x: 0 }, { x: 240, opacity: 0 }]), { amount: 0.2 }));3. Staging
見せたいものを一つに絞り、構図・光・動きで視線を誘導する。UI では選択中のカードを前へ出し、周囲を暗く・ぼかし・後退させる。
stagingPoses({ dim = 0.5, blur = 2, recede = 0.02, lift = 12 })→{ focus, surroundings }(ポーズのみ)。stage(focus, options)→{ release(): Promise<void> }。surroundingsの既定はfocusの兄弟要素。personality/reducedMotionも受け取る。適用中はfocusにz-index: 10(position: staticならrelative)を付け、release()完了時に戻す。useStage(active, options)—activeの間だけステージングする React フック。surroundingsはステージング開始時に読まれる(インライン配列でも毎レンダーで再ステージしない)。
const handle = stage(selectedCard, { dim: 0.6 });
await handle.release();4. Straight ahead & pose to pose
ストレートアヘッドは最初から順に1コマずつ描く手法、ポーズ・トゥ・ポーズは要所のキーポーズを先に決めて中割りを埋める手法。UI では通常ポーズ・トゥ・ポーズ(中割りはブラウザが担当)を使い、物理シミュレーションなどキーでは表せない動きだけを時間関数から直接サンプリングする。
poseToPose(keys, { duration = 300, delay, easing, iterations, direction, origin, perspective })— キーは2つ以上。easingのない区間はeasings.inOutになる。straightAhead(draw, { duration, fps = 60, ... })—draw(t, elapsedMs)をfpsでサンプリング。durationは必須。
const toss = straightAhead((t) => ({ x: t * 200, y: -300 * t + 300 * t * t }), { duration: 600 });
const nod = poseToPose([{ rotate: 0 }, { rotate: -6 }, { rotate: 0 }], { duration: 250 });5. Follow through & overlapping action
本体が止まっても髪や服は慣性で行き過ぎてから落ち着き(follow through)、体の各部は少しずつずれて動く(overlapping action)。UI では到着時の小さなオーバーシュートや、リスト項目の時間差入場として現れる。
followThrough(spec, { bounce = 0.3 })— 最終区間をスプリングで駆動し、終端を行き過ぎて戻らせる。overlap(spec, count, { drag = 0.1, each = 30, cap = 50, total = 300, from = "first" })→MotionSpec[]。各パートをstaggerDelaysの間隔でずらし、起点からnステップ後ろのパートにbounce = drag × n(上限 0.8)の follow through を付ける(staggerDistances(count, from)がnを返す)。spring({ duration = 400, bounce = 0.2 })(知覚パラメータ)またはspring({ stiffness, damping, mass })(物理パラメータ。どちらかを指定するとstiffness既定 170、damping既定 26、mass既定 1)。velocityも指定可。返り値はイージング関数で、.durationに静定時間(ms)を持つ。useCascade(input = "rise", { trigger, ...OverlapOptions })— コンテナの子要素を順に入場させる。triggerが変わると再生し直す(capなどのオプションを変えただけでは再生しない)。
const items = Array.from(list.children) as HTMLElement[];
const specs = overlap(enter("rise"), items.length, { each: 40 });
await playAll(items.map((element, i) => ({ element, spec: specs[i]! })));6. Slow in & slow out
現実の物体は動き出しで加速し、止まる前に減速する。キーポーズ付近にコマを多く置くことで自然な重さが出る。UI では空間的な動きには必ずイージングを付け、linear は進捗表示だけに使う。
easings:
| 名前 | 値 | 用途 |
| --- | --- | --- |
| linear | "linear" | プログレスバー・スピナー・スクラブのみ |
| inOut | cubic-bezier(0.65, 0, 0.35, 1) | 画面内の2状態間の移動 |
| out | cubic-bezier(0.22, 1, 0.36, 1) | 入場 |
| in | cubic-bezier(0.55, 0, 1, 0.45) | 退場 |
| anticipate | cubic-bezier(0.36, 0, 0.66, -0.56) | 一度後ろに下がってから進む |
| overshoot | cubic-bezier(0.34, 1.56, 0.64, 1) | 行き過ぎて戻る |
cubicBezier(x1, y1, x2, y2) は CSS の cubic-bezier() と同じ曲線を JS 関数として返す。Easing には CSS 文字列と JS 関数のどちらも渡せ、resolveEasing で関数に解決できる。
play(panel, { duration: 200, easing: easings.out, frames: [{ y: 16, opacity: 0 }, { y: 0, opacity: 1 }] });7. Arcs
生き物の動きは直線ではなく弧を描く。直線移動は機械的に見える。UI ではカートに飛び込むアイテムや FAB からの展開など、2点間の移動を緩やかな曲線にする。
arc(from, to, { bend = 0.2, orient = false, samples = 24, duration, delay, easing = easings.inOut })— 二次ベジェ曲線上をサンプリング。bendは移動距離に対する膨らみの割合(正で画面上方向)。orient: trueで進行方向に回転する。durationの既定は移動距離からtravelDurationで算出。x/y以外のキーは並行して補間される。arcPoint(a, b, bend, t)— 同じ曲線上の点。
play(chip, arc({ x: 0, y: 0 }, { x: 320, y: -40, scale: 0.4, opacity: 0 }, { bend: 0.3, orient: true }));8. Secondary action
主動作を補強する副次的な動き(歩きながら腕を振る等)。主動作より目立ってはいけない。UI ではボタンを押したときのアイコンの揺れ、待機中の小さな浮遊など。
secondaryAction(kind, { amplitude = 1, duration = 500, delay, iterations })—kindは"wiggle"/"pulse"/"float"/"sway"。amplitudeは 1 未満を推奨。accompany(primary, kind, { attenuation = 0.6, lag = 0.15 })— 主動作のlag割合だけ遅れて始まり、残りの尺に収まる副次動作。
const main = jump();
play(button, main);
play(icon, accompany(main, "wiggle"));9. Timing
同じ動きでもコマ数(時間)で重さや感情が変わる。UI では時間が応答性そのものであり、ユーザー起点の動きは短く保つ。
durations—instant: 100,fast: 150,base: 200,slow: 300,deliberate: 500(deliberateはオンボーディングなどシステム起点の演出用)。duration(value, tempo = 1)— トークンまたは ms にtempoを掛けて丸める。travelDuration(distancePx, { min = 150, max = 300, reference = 800 })— 距離の平方根に比例(referencepx でmaxに到達)。staggerDelays(count, { each = 30, cap = MAX_STAGGER_MS, total = 300, from = "first" })— 要素ごとの遅延。fromは"first"/"last"/"center"/ インデックス。1 ステップはmin(each, cap, total / 最大距離)。cap(既定 50ms)を上げると派手なカスケードにできる(負ならRangeError)。
const ms = travelDuration(Math.hypot(dx, dy));
play(el, poseToPose([{ x: 0, y: 0 }, { x: dx, y: dy }], { duration: ms }));10. Exaggeration
現実をそのまま写すと弱く見えるため、本質を強調して誇張する。UI ではブランドの性格に合わせて動きの振れ幅を強めたり、逆に控えめにしたりする。
exaggerate(target, factor, { rest = REST, keys })—PoseまたはMotionSpec(全フレーム)の、restからの偏差をfactor倍する。factor > 1で誇張、< 1で抑制、負数で反転。keysの既定は opacity 以外の全キー。
play(el, exaggerate(squashStretch(), 2));11. Solid drawing
描かれたものに体積・重さ・奥行きを持たせ、一貫した3D空間に置く。UI では一つの光源を前提に、浮いた要素ほど影が大きく・柔らかく・薄くなるよう統一し、傾きは本物のパースペクティブで描く。
lift(level = 4)→{ y: -level * 0.5, elevation: level }tiltToward(point, maxDeg = 8)—pointは要素に正規化した座標(左上(-0.5, -0.5)〜 右下(0.5, 0.5))。solid(spec, perspective = 800)—perspective(px)を付け、rotateX/rotateY/zを立体的に描画する。shadowForElevation(elevation)—elevationから2層のbox-shadow文字列を作る(Pose.elevationが内部で使う)。tiltable(el, { max = 8, perspective = 800, personality, reducedMotion })— ポインタに追従する3Dチルト。離れるとスプリングで平らに戻る。hoverable(el, { level = 6, pose, personality, reducedMotion })— ホバーで浮き上がる(タッチでは発火しない)。
hoverable(card, { pose: { scale: 1.02 } });
tiltable(card, { max: 6 });
play(card, solid(poseToPose([{ rotateY: 0 }, { rotateY: 180 }])));12. Appeal
観客が惹かれるキャラクターには一貫した個性がある。UI ではプロダクト全体が一つの「キャラクター」として動くよう、テンポ・誇張・弾性を一か所で決める。
Personality は name, tempo(尺の倍率), exaggeration(振れ幅の倍率), bounce(0〜0.95), anticipation(0〜1), squash(0〜0.5), guardrails(boolean。true で押下などを UI ガイドライン内に収める)を持つ。
| personalities | 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 |
definePersonality(overrides, base = "natural")— 範囲検証済みの個性を作る(tempoは 0.1〜4、exaggerationは 0〜4 など。範囲外はRangeError、guardrailsが boolean でなければTypeError)。nameとguardrailsは省略するとbaseから引き継ぐ。resolvePersonality(input = "natural")— 名前またはPersonalityを解決する。MotionProvider— React サブツリー全体に個性を配る。レシピと振る舞い(pressable等)はすべてpersonalityオプションを受け取る。
const brand = definePersonality({ name: "brand", tempo: 0.9, bounce: 0.3 }, "snappy");
<MotionProvider personality={brand}>{/* ... */}</MotionProvider>;React で個性オブジェクトを使う場合は、上のようにコンポーネントの外で一度だけ作ること(毎レンダー新しいオブジェクトを渡すとコンテキスト値が毎回変わる)。
レシピと原則の合成
レシピは複数の原則を組み合わせ、personality で調整済みの MotionSpec を返す。共通オプションは RecipeOptions: { personality, duration, distance }。
| レシピ | 内容 | 既定 |
| --- | --- | --- |
| enter(kind = "rise", options) | 入場。easings.out。"pop" はスプリングでオーバーシュート(bounce は max(personality.bounce, 0.25)) | 尺 base(200ms)、"pop" は slow(300ms)、いずれも × tempo。distance 12 |
| exit(kind = "fade", options) | 退場。easings.in。"pop" は anticipation > 0 の個性なら一度膨らんでから消える | 尺 fast(150ms)× tempo。distance 12 |
| jump({ height = 16, ... }) | しゃがみ → 伸びて離陸 → 頂点で滞空 → 着地で潰れ → 静止。origin: "50% 100%" | 尺 450ms × tempo |
| shake({ distance = 6, ... }) | x 方向の減衰振動(エラー表示) | 尺 400ms × tempo |
| pressDepth(personality) | 押下時の縮小量(guardrails: true なら 0.02〜0.05、false なら 0.02〜0.2 にクランプ) | squash × 0.75 |
| settleSpring(personality) | 押下・ホバー解除用のスプリング | duration 350 × tempo、bounce は個性の値 |
TransitionKind: "fade" / "rise" / "drop" / "slideLeft" / "slideRight" / "zoom" / "pop"。
原則の関数はすべて MotionSpec → MotionSpec(またはポーズ)なので、そのまま合成できる。
import { anticipate, arc, exaggerate, followThrough, play, poseToPose, secondaryAction, solid } from "twelve-principles";
// 溜めてから動き、行き過ぎて落ち着く
const toss = followThrough(anticipate(poseToPose([{ x: 0 }, { x: 200 }])), { bounce: 0.4 });
// 弧を描く移動を 1.3 倍に誇張(REST 基準なので移動距離そのものも 1.3 倍になる)
const swoop = exaggerate(arc({ x: 0, y: 0 }, { x: 200, y: 0 }), 1.3);
// 3D の揺れを無限ループで
const idle = solid({ ...secondaryAction("sway", { amplitude: 0.5 }), iterations: Infinity });
play(el, toss);派手な動き(Expressive)
既定値は UI ガイドラインに沿っているが、祝福・オンボーディング・ゲームなど動きそのものを見せたい場面では、次の 3 つで意図して外へ出られる。どれも既定では無効。
guardrails: falseの個性 —bouncy/cartoon、またはdefinePersonality({ guardrails: false, ... })。pressDepthの上限が 0.05 から 0.2 になり、押下が深く潰れる(bouncyは scale 0.91、cartoonは 0.835)。- スタッガーの
cap—staggerDelays/overlap/useCascadeにcapを渡すと 1 項目 50ms の上限を超えられる。total(既定 300ms)も効くので、必要なら一緒に上げる。 - 派手なレシピ — 下表。どれも
RecipeOptionsを受け取り、尺は基準 ×tempo、振れ幅はexaggerationに比例し、静止状態で終わる。
| レシピ | 内容 | 既定 |
| --- | --- | --- |
| rubberBand({ axis = "x", ... }) | 体積を保って伸び縮みを繰り返す(Squash & stretch + Follow through) | 尺 800ms × tempo |
| jello(options) | skew の減衰振動 | 尺 900ms × tempo |
| tada(options) | 縮んで溜め → 膨らんで左右に揺れる → 静止(Anticipation + Secondary action) | 尺 1000ms × tempo |
| heartbeat(options) | 2 回の鼓動と間 | 尺 1300ms × tempo |
| swing(options) | 上端(origin: "50% 0%")から吊られた振り子 | 尺 1000ms × tempo |
| wobble({ distance = 25, ... }) | x と回転の減衰振動 | 尺 1000ms × tempo |
| flip(options) | perspective 400 で Y 軸 1 回転(rotateY -360 → 0 なので静止で終わる) | 尺 1000ms × tempo |
| bounceIn(options) | opacity 0・縮小から、bounce 0.5 以上のスプリングで登場 | 尺はスプリングの静定時間 |
| fallIn({ height = 80, ... }) | 重力で落ちて 3 回跳ねる(straightAhead)。着地で潰れる | 尺は物理的な所要時間 × tempo |
expressive は名前からレシピを引く辞書(Record<ExpressiveRecipe, (options?) => MotionSpec>)。
import { exaggerate, rubberBand, staggerDelays, tada } from "twelve-principles";
import { MotionProvider, useMotion } from "twelve-principles/react";
const louder = exaggerate(tada(), 1.5); // tada の振れ幅をさらに 1.5 倍
const delays = staggerDelays(6, { each: 120, cap: 150, total: 900 }); // [0, 120, 240, 360, 480, 600]
function Trophy() {
const { ref, play } = useMotion<HTMLDivElement>();
return <div ref={ref} onClick={() => play((p) => rubberBand({ personality: p }))}>Trophy</div>;
}
// 祝福画面だけ cartoon にして、アプリ全体のガードレールは保つ
<MotionProvider personality="cartoon"><Trophy /></MotionProvider>;reduced motion は派手なレシピにもそのまま効く(静止で始まり静止で終わるものは動かず、bounceIn / fallIn は opacity のフェードになる)。iterations: Infinity でループさせる場合は停止手段を用意すること。
アクセシビリティ
play / animateTo / setLayer / 各振る舞い / stage / React の MotionProvider はすべて reducedMotion を受け取る(ReducedMotion 型)。
| 値 | 挙動 |
| --- | --- |
| "auto"(既定) | prefers-reduced-motion: reduce のときだけ "fade"、それ以外は "full" |
| "fade" | 終端の形状へ即座に移り、opacity だけを最大 150ms でクロスフェードする |
| "skip" | 終端状態へ即座に移る(尺 0) |
| "full" | 常にフル再生。意味を伝えるのに不可欠なモーションに限る |
prefersReducedMotion() で現在の設定を、reduceSpec(spec, "fade" | "skip") で置き換え後の spec を取得できる。pressable はキーボード(Enter / Space)にも反応し、hoverable / tiltable はタッチ入力を無視する。
既定値に組み込まれた UI ガイドライン
- ユーザー起点の動きは 300ms 以内(
USER_INITIATED_MAX_MS = 300)。押下 100ms、ホバー 200ms、入場 200ms(popは 300ms)、退場 150ms、travelDurationの上限 300ms。これらはtempo倍されるため、calm(1.25)では上回ることがある。jump(450ms)とsecondaryAction(500ms)は注意喚起・演出用。 - スタッガーは 1 項目あたり 50ms 以内(
MAX_STAGGER_MS = 50)。staggerDelaysのeachはcap(既定はこの値)でクランプされ、全体はtotal(既定 300ms)に収まる。 - 入場は ease-out、退場は ease-in。
enterはeasings.out、exitはeasings.inを使い、退場のほうが短い。 - linear は進捗表示専用。 空間的な動きの既定は
easings.inOut(poseToPose/animateTo/arc)。 - 押下の縮小は scale 0.95〜0.98。
pressDepthはguardrails: trueの個性(natural/snappy/calm/playful)では 0.02〜0.05 にクランプする。bouncy/cartoonのようなguardrails: falseの個性では 0.2 まで深くなる。 - オーバーシュートはスプリングで。 解除時の戻り(
settleSpring)、followThrough、enter("pop")はspring()で行き過ぎて静定する。
ブラウザ対応
WAAPI と Animation.prototype.commitStyles() が必要(Chrome 84+、Firefox 75+、Safari 13.1+)。CSS のイージング文字列はそのまま WAAPI に渡し、JS のイージング(スプリングや cubicBezier など)は compile 時に約 60fps の線形キーフレームへ焼き込むため、CSS の linear() 関数には依存しない。
開発
npm run dev # dev/ のデモを Vite で起動
npm test # vitest run
npm run build # tsup で dist/ を生成(ESM + CJS + 型定義)
npm run typecheck # tsc --noEmit
npm run docs:dev # 解説サイト(docs/、Astro + Starlight)を起動
npm run docs:build # 静的サイトを docs/dist/ に出力(検索インデックス込み)
npm run skill:install -- <skills-dir> # エージェント用スキルをコピー(例: ../my-app/.claude/skills)エージェント用スキル
skills/ui-motion-principles/ は、コーディングエージェント(Claude Code など)にこのライブラリの使い方を教える Agent Skill です。解説サイトの「エージェント用スキル」ページで全ファイルを公開し、/skill/ui-motion-principles.zip として配布しています(ビルド時にこのディレクトリから生成)。npm パッケージにも同梱されるので、cp -r node_modules/twelve-principles/skills/ui-motion-principles .claude/skills/ でも導入できます。テンプレートの .tsx は npm run typecheck の対象です。
