Straight Ahead & Pose to Pose(ストレート・アヘッドとポーズ・トゥ・ポーズ)
動きの作り方には、最初から順に1コマずつ描いていく方法と、要所のポーズを先に決めて間を埋める方法の2つがあります。twelve-principles では前者を straightAhead、後者を poseToPose として提供し、どちらも同じ MotionSpec を返します。
アニメーションにおける原則
Section titled “アニメーションにおける原則”ストレート・アヘッド(Straight Ahead Action) は、最初のコマから順番に、前のコマを見ながら次のコマを描いていく手法です。先の形を決めずに描き進めるため、炎、水しぶき、煙、髪のなびきのような、予測しにくく流動的な動きに生命感が出ます。その代わり、プロポーションが少しずつずれたり、最後にどこへ着地するかを制御しにくかったりします。
ポーズ・トゥ・ポーズ(Pose to Pose) は、動作の要となるキーポーズ(始まり、頂点、終わり)を先に描き、あとから中割り(in-between)でその間を埋める手法です。構図とタイミングを最初に決められるので、演技や感情表現のように「どの瞬間にどの形であるべきか」が重要な場面に向いています。
実際の制作では、この2つを組み合わせます。体の大きな動きはポーズ・トゥ・ポーズで設計し、その上に髪や布のような二次的な部分をストレート・アヘッドで描き足す、といった使い分けです。
UI への翻訳
Section titled “UI への翻訳”UI の遷移は、ほぼすべてがポーズ・トゥ・ポーズです。「閉じた状態」と「開いた状態」という2つのキーポーズがあり、その間はブラウザが補間します。CSS transition も、WAAPI のキーフレームも、この考え方です。poseToPose は、キーポーズを並べるだけで各区間に slow in / slow out(easings.inOut)を付けるので、2〜5個のキーで大半の UI モーションが書けます。
ストレート・アヘッドが必要になるのは、キーポーズで表すより 規則(物理法則や数式)で表すほうが簡単な動き です。重力で落ちて弾むボール、減衰しながら揺れるベル、ノイズで漂う要素などがこれにあたります。straightAhead は、時間を受け取ってポーズを返す関数を fps(既定 60)でサンプリングし、密なキーフレーム列にします。サンプリングは spec を作るときに一度だけ行われ、再生は通常の WAAPI アニメーションです。requestAnimationFrame で毎フレーム計算してスタイルを書き換える方式と違い、再生中に JavaScript は走らず、transform と opacity だけの動きならコンポジタで再生されます。
poseToPose |
straightAhead |
|
|---|---|---|
| 書くもの | 2つ以上のキーポーズ | 時間 → ポーズの関数 |
| フレーム数 | キーの数だけ | round(duration / 1000 × fps) + 1(1400ms・60fps で 85) |
| 区間のイージング | 未指定の区間は easings.inOut |
なし(サンプル間は線形) |
| 既定の尺 | durations.slow(300ms) |
必須 |
| 向いている動き | 状態遷移、演技、ほぼすべての UI | 物理シミュレーション、減衰振動、ノイズ |
poseToPose
Section titled “poseToPose”function poseToPose(keys: readonly PoseFrame[], options?: TimelineOptions): MotionSpec;キーポーズの配列から MotionSpec を作ります。
- キーは2つ以上必要で、1つ以下なら
RangeErrorを投げます。 - offset の並びは作成時に検証され、0〜1 の範囲で昇順でなければ
RangeErrorを投げます(再生時ではなく、spec を作った時点で気付けます)。 - 最後のキーを除き、
easingを指定していないキーにはeasings.inOut(cubic-bezier(0.65, 0, 0.35, 1))が付きます。各キーポーズに向かって減速し、そこから加速して離れる動きになります。
各キーは PoseFrame、つまり Pose に次の2つを加えたものです。
| フィールド | 型 | 既定値 | 説明 |
|---|---|---|---|
offset |
number |
WAAPI と同じく均等配置 | タイムライン上の位置(0〜1)。省略すると、両端は 0 / 1 に固定され、間は指定済みのキーの間に均等に並ぶ。 |
easing |
Easing |
easings.inOut(最後のキーを除く) |
このキーから始まる区間 のイージング。CSS 文字列または JS 関数。 |
TimelineOptions:
| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
duration |
number |
durations.slow(300) |
1イテレーションの尺(ms)。 |
delay |
number |
0 |
開始までの遅延(ms)。 |
easing |
Easing |
なし | イテレーション全体に掛かるイージング。各キーの区間イージングの上に重なる。通常は未指定にする。 |
iterations |
number |
1 |
繰り返し回数。Infinity で無限。 |
direction |
PlaybackDirection |
"normal" |
"reverse" / "alternate" / "alternate-reverse"。 |
origin |
string |
なし | transform-origin。例: "50% 100%"。 |
perspective |
number |
なし | perspective(px) を付けて rotateX / rotateY / z を立体的に描く。 |
straightAhead
Section titled “straightAhead”function straightAhead( draw: (t: number, elapsedMs: number) => Pose, options: StraightAheadOptions,): MotionSpec;draw を一定間隔で呼び出し、その結果をキーフレームとして並べます。t は進捗(0〜1)、elapsedMs は開始からの経過時間(ms)です。区間数は Math.max(Math.round(duration / 1000 × fps), 1) で、draw はその数 + 1 回、t = 0 から t = 1 まで等間隔に呼ばれます。各フレームにはイージングが付かず、サンプルの間は線形に補間されます。
StraightAheadOptions は TimelineOptions から easing を除いたもの(時間の歪みは draw の中で表現する)に、次の変更を加えたものです。
| オプション | 型 | 既定値 | 説明 |
|---|---|---|---|
duration |
number |
なし(必須) | 尺(ms)。0 以下なら RangeError。 |
fps |
number |
60 |
サンプリングレート。0 以下なら RangeError。 |
delay / iterations / direction / origin / perspective |
— | TimelineOptions と同じ |
そのまま spec に渡される。 |
「いいね」ボタンのハートをキーポーズで設計します。一度縮んでから大きくふくらみ、最後の区間だけ ease-out で落ち着きます。
import { easings, play, poseToPose } from "twelve-principles";
const heart = document.querySelector<HTMLElement>(".like-heart")!;
const like = poseToPose( [ { scale: 1 }, { scale: 0.85, offset: 0.2 }, { scale: 1.2, offset: 0.55, easing: easings.out }, { scale: 1 }, ], { duration: 300 },);
heart.addEventListener("click", () => play(heart, like));通知ベルを減衰振動で揺らします。キーポーズで書くと山の数だけキーが必要ですが、式なら1行です。上端を支点にし、t = 1 で sin(8π) = 0 になるので静止状態で終わります。
import { play, straightAhead } from "twelve-principles";
const bell = document.querySelector<HTMLElement>(".bell-icon")!;
const ring = straightAhead( (t) => ({ rotate: 14 * Math.exp(-4 * t) * Math.sin(t * Math.PI * 8) }), { duration: 800, origin: "50% 0%" },);
export function notify() { play(bell, ring);}放物線を描いて投げる動きも、式で直接書けます。
import { straightAhead } from "twelve-principles";
const toss = straightAhead((t) => ({ x: t * 200, y: -300 * t + 300 * t * t }), { duration: 600 });useMotion の play に個性を受け取る関数を渡し、尺を tempo で調整します。
import { duration, easings, poseToPose } from "twelve-principles";import { useMotion } from "twelve-principles/react";
export function LikeButton({ liked, onToggle }: { liked: boolean; onToggle: () => void }) { const { ref, play } = useMotion<HTMLSpanElement>();
const toggle = () => { onToggle(); if (liked) return; // 取り消しでは動かさない play((p) => poseToPose( [ { scale: 1 }, { scale: 0.85, offset: 0.2 }, { scale: 1.2, offset: 0.55, easing: easings.out }, { scale: 1 }, ], { duration: duration("slow", p.tempo) }, ), ); };
return ( <button type="button" aria-pressed={liked} onClick={toggle}> <span ref={ref} className="like-heart" aria-hidden="true" /> いいね </button> );}ストレート・アヘッドの spec は描画関数を毎回呼ぶので、入力が変わらないならコンポーネントの外で一度だけ作ります。
import { straightAhead } from "twelve-principles";import { useMotion } from "twelve-principles/react";
const ring = straightAhead( (t) => ({ rotate: 14 * Math.exp(-4 * t) * Math.sin(t * Math.PI * 8) }), { duration: 800, origin: "50% 0%" },);
export function BellButton({ unread }: { unread: number }) { const { ref, play } = useMotion<HTMLSpanElement>(); return ( <button type="button" onClick={() => play(ring)} aria-label={`通知 ${unread} 件`}> <span ref={ref} className="bell-icon" aria-hidden="true" /> </button> );}ガイドラインと落とし穴
Section titled “ガイドラインと落とし穴”- Squash & Stretch — デモのボールは、
drawの中で速度からdeformの量を計算しています。poseToPoseのキーに...stretch(0.1)のように展開することもできます。 - Follow Through & Overlapping Action —
followThrough(poseToPose(keys))で最後の区間をスプリングにし、到着で行き過ぎて落ち着かせます。 - Slow In & Slow Out —
poseToPoseの既定の区間イージングeasings.inOutは、この原則そのものです。 - Arcs —
arcは曲線を既定 24 区間(25 点)でサンプリングし、キーポーズの列として返します。ストレート・アヘッドと同じく「規則からフレームを作る」方式です。 - Timing — キーの
offsetの配置が、どの形にどれだけ時間を使うかを決めます。 - Solid Drawing —
perspectiveオプションかsolid()で、rotateYのキーポーズを立体的に描けます。