コンテンツにスキップ

Straight Ahead & Pose to Pose(ストレート・アヘッドとポーズ・トゥ・ポーズ)

動きの作り方には、最初から順に1コマずつ描いていく方法と、要所のポーズを先に決めて間を埋める方法の2つがあります。twelve-principles では前者を straightAhead、後者を poseToPose として提供し、どちらも同じ MotionSpec を返します。

LIVE
straightAhead() は重力シミュレーションでボールを落とし、速度に応じて伸び、接地で潰れながら4回弾ませます。poseToPose() は4つのキーポーズだけで箱を跳ね上げ、空中で回転させて着地させます。

ストレート・アヘッド(Straight Ahead Action) は、最初のコマから順番に、前のコマを見ながら次のコマを描いていく手法です。先の形を決めずに描き進めるため、炎、水しぶき、煙、髪のなびきのような、予測しにくく流動的な動きに生命感が出ます。その代わり、プロポーションが少しずつずれたり、最後にどこへ着地するかを制御しにくかったりします。

ポーズ・トゥ・ポーズ(Pose to Pose) は、動作の要となるキーポーズ(始まり、頂点、終わり)を先に描き、あとから中割り(in-between)でその間を埋める手法です。構図とタイミングを最初に決められるので、演技や感情表現のように「どの瞬間にどの形であるべきか」が重要な場面に向いています。

実際の制作では、この2つを組み合わせます。体の大きな動きはポーズ・トゥ・ポーズで設計し、その上に髪や布のような二次的な部分をストレート・アヘッドで描き足す、といった使い分けです。

UI の遷移は、ほぼすべてがポーズ・トゥ・ポーズです。「閉じた状態」と「開いた状態」という2つのキーポーズがあり、その間はブラウザが補間します。CSS transition も、WAAPI のキーフレームも、この考え方です。poseToPose は、キーポーズを並べるだけで各区間に slow in / slow out(easings.inOut)を付けるので、2〜5個のキーで大半の UI モーションが書けます。

ストレート・アヘッドが必要になるのは、キーポーズで表すより 規則(物理法則や数式)で表すほうが簡単な動き です。重力で落ちて弾むボール、減衰しながら揺れるベル、ノイズで漂う要素などがこれにあたります。straightAhead は、時間を受け取ってポーズを返す関数を fps(既定 60)でサンプリングし、密なキーフレーム列にします。サンプリングは spec を作るときに一度だけ行われ、再生は通常の WAAPI アニメーションです。requestAnimationFrame で毎フレーム計算してスタイルを書き換える方式と違い、再生中に JavaScript は走らず、transformopacity だけの動きならコンポジタで再生されます。

poseToPose straightAhead
書くもの 2つ以上のキーポーズ 時間 → ポーズの関数
フレーム数 キーの数だけ round(duration / 1000 × fps) + 1(1400ms・60fps で 85)
区間のイージング 未指定の区間は easings.inOut なし(サンプル間は線形)
既定の尺 durations.slow(300ms) 必須
向いている動き 状態遷移、演技、ほぼすべての UI 物理シミュレーション、減衰振動、ノイズ
function poseToPose(keys: readonly PoseFrame[], options?: TimelineOptions): MotionSpec;

キーポーズの配列から MotionSpec を作ります。

  • キーは2つ以上必要で、1つ以下なら RangeError を投げます。
  • offset の並びは作成時に検証され、0〜1 の範囲で昇順でなければ RangeError を投げます(再生時ではなく、spec を作った時点で気付けます)。
  • 最後のキーを除き、easing を指定していないキーには easings.inOutcubic-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 を立体的に描く。
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 まで等間隔に呼ばれます。各フレームにはイージングが付かず、サンプルの間は線形に補間されます。

StraightAheadOptionsTimelineOptions から 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 = 1sin(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 });
  • Squash & Stretch — デモのボールは、draw の中で速度から deform の量を計算しています。poseToPose のキーに ...stretch(0.1) のように展開することもできます。
  • Follow Through & Overlapping ActionfollowThrough(poseToPose(keys)) で最後の区間をスプリングにし、到着で行き過ぎて落ち着かせます。
  • Slow In & Slow OutposeToPose の既定の区間イージング easings.inOut は、この原則そのものです。
  • Arcsarc は曲線を既定 24 区間(25 点)でサンプリングし、キーポーズの列として返します。ストレート・アヘッドと同じく「規則からフレームを作る」方式です。
  • Timing — キーの offset の配置が、どの形にどれだけ時間を使うかを決めます。
  • Solid Drawingperspective オプションか solid() で、rotateY のキーポーズを立体的に描けます。