コンテンツにスキップ

Arcs(弧)

生き物の動きは直線ではなく弧を描きます。arc は 2 つのポーズの間を二次ベジェ曲線に沿って動く MotionSpec を作り、arcPoint は同じ曲線上の任意の点を返します。

LIVE
「直線 vs arc()」を押すと、下の要素は 220px を直線で、上の要素は同じ 700ms で最大 66px 上に膨らむ弧を描いて進む。上の要素は orient により進行方向へ機首を向ける。

人や動物の関節は回転運動をするので、手足の先端は必ず円弧を描きます。腕を振る、振り返る、歩くときに頭が上下するといった動きは、どれも中割りを直線上ではなく弧の上に置くことで自然になります。直線上に中割りを置くと、動きは硬く、機械的に見えます。

投げたボールは放物線を描き、振り子は円弧を往復します。顔を左から右へ向けるときも、頭は水平に滑るのではなく、途中でわずかに下がる(または上がる)弧をたどります。アニメーターはキーポーズの間に弧のガイド線を引き、中割りをその上に配置します。

例外は機械です。ピストンやエレベーター、ロボットの直線的な動きは、直線であることがそのまま「機械らしさ」の表現になります。また動きが速いほど弧は平らになり、ゆっくりした動きほど弧がはっきり見えます。

UI では、要素がある場所から離れた別の場所へ「移る」ときに弧が効きます。

  • カートへの追加、お気に入りへの保存: 商品画像のサムネイルがカートアイコンへ飛び込む。投げた物のように上に膨らむ弧が合います。
  • FAB やメニューの展開: ボタンから項目が弧を描いて広がると、どこから出てきたかが追いやすくなります。
  • ドラッグの取り消し: ドロップ先が無効なとき、元の位置へ弧を描いて戻ります。

bend は移動距離に対する膨らみの割合で、既定値 0.2 は 320px の移動で 64px の膨らみです。UI では 0.1〜0.3 が目安で、デモは 0.3 を使っています。移動距離が 100px 前後以下の小さな動き、画面内の多くの要素が同時に動くレイアウト変化(並べ替えやグリッドの再配置)、スクロール連動の動きでは直線のままにします。弧は視線を引くので、同時に何本も走ると散らかって見えます。

function arc(from: Pose, to: Pose, options?: ArcOptions): MotionSpec;
オプション 既定値 説明
bend number 0.2 直線からの膨らみの最大量を、移動距離に対する割合で指定。正で画面上方向、負で下方向
orient boolean false 経路の接線に合わせて要素を回転させる(投げた物、紙飛行機)
samples number 24 経路の分割数。フレーム数は samples + 1
duration number travelDuration(距離) 再生時間(ms)。既定は 2 点間の直線距離から算出(150〜300ms)
delay number なし 開始までの遅延(ms)
easing Easing easings.inOut 経路上の進み具合。イテレーション全体に掛かる

動作の詳細:

  • from / tox / y を端点に使います。省略した軸は 0 とみなします。
  • 曲線上を t = i / samples で等分割し、各フレームの x / y を曲線上の点に置きます。scaleopacity など他のキーは from から to へ線形に補間され、同じ easing で一緒に進みます。
  • フレーム自体には easing を付けず、spec の easing で全体を 1 回だけ加減速します。easing が文字列なら焼き込みなしで 25 枚のキーフレームとして WAAPI に渡ります。
  • 分割は曲線のパラメータ t で等間隔で、弧長では等間隔ではありません。二次曲線は端点付近で速く、頂点付近で遅く進むため、投げた物が頂点で一瞬とどまるような間が自然に生まれます。bend が大きいほど顕著です。

既定の durationtravelDuration による距離依存の値です。平方根で伸びるので、長い移動でも間延びしません。

直線距離 既定の duration
50px 188ms
220px 229ms
400px 256ms
800px 以上 300ms

弧の長さではなく直線距離から求めるため、bend を大きくしても既定の尺は変わりません。

曲線の制御点は、直線の中点から直線に垂直な方向へ置かれます。その向きは移動方向によらず決まります。

  • 正の bend画面の上方向y が小さくなる側)へ膨らみます。左へ移動しても右へ移動しても上です。
  • 真上・真下への移動では「上」がないので、正の bend左方向x が小さくなる側)へ膨らみます。
  • 負の bend はその反対(下、または右)へ膨らみます。
  • 膨らみの頂点は直線の中点から bend × 距離 の位置です(制御点はその 2 倍離れた位置に置かれます)。
始点 → 終点 bend 曲線の中点(t = 0.5
(0, 0) → (220, 0) 0.3 (110, -66)
(220, 0) → (0, 0) 0.2 (110, -44)
(0, 0) → (0, 200) 0.2 (-40, 100)

orient: true のとき、各フレームの rotate を「経路の接線が直線(始点から終点への向き)からどれだけずれているか」に設定します。値は -180〜180 度の範囲に収まり、from.rotate に加算されます。デモの設定(右へ 220px、bend: 0.3)では、出発時に -50.2 度(機首が上)、頂点で 0 度、到着時に 50.2 度(機首が下)です。

  • 回転は直線の向きを基準にした相対値なので、要素は「移動方向を向いた状態」で描いておきます。右へ飛ばす紙飛行機なら機首が右を向いた絵です。
  • to.rotateorient の回転で上書きされるため効きません。
  • 始点と終点が同じ位置のときは回転しません。
function arcPoint(a: Point, b: Point, bend: number, t: number): Point;
引数 説明
a Point 始点 { x, y }
b Point 終点 { x, y }
bend number arc と同じ膨らみの割合と符号
t number 曲線のパラメータ(0 = a、1 = b

arc が内部で使うのと同じ曲線上の点を返します。経路を SVG で描いてプレビューする、軌跡にパーティクルを置く、キャンバスに描画するといった用途に使えます。ab が同じ点なら常に a を返します。

import { arc, arcPoint, play } from "twelve-principles";
// サムネイルの複製をカートアイコンへ弧を描いて飛ばす
export async function flyToCart(thumb: HTMLElement, cart: HTMLElement) {
const from = thumb.getBoundingClientRect();
const to = cart.getBoundingClientRect();
const ghost = thumb.cloneNode(true) as HTMLElement;
Object.assign(ghost.style, {
position: "fixed",
left: `${from.left}px`,
top: `${from.top}px`,
width: `${from.width}px`,
height: `${from.height}px`,
margin: "0",
pointerEvents: "none",
});
document.body.append(ghost);
const dx = to.left + to.width / 2 - (from.left + from.width / 2);
const dy = to.top + to.height / 2 - (from.top + from.height / 2);
// 投げた物のように上へ膨らませ、縮みながら消える
const spec = arc({ x: 0, y: 0 }, { x: dx, y: dy, scale: 0.2, opacity: 0 }, { bend: 0.25 });
await play(ghost, spec).finished;
ghost.remove();
}
// 同じ曲線を SVG の path として描く(デバッグ・デザイン確認用)
export function drawArc(path: SVGPathElement, a: { x: number; y: number }, b: { x: number; y: number }, bend = 0.25) {
const points = Array.from({ length: 25 }, (_, i) => arcPoint(a, b, bend, i / 24));
path.setAttribute("d", points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" "));
}
  • Slow In & Slow Out: easing が経路上の進み具合を決めます。入場なら easings.out、退場なら easings.in を渡します。
  • Timing: 既定の durationtravelDuration です。距離が変わっても一貫した速度感になります。
  • Exaggeration: exaggerate(arc(...), 1.3) は REST 基準で逸脱量を拡大するので、移動距離と膨らみがまとめて 1.3 倍になります。
  • Squash & Stretch: 到着時の潰れは別の要素(または後続の play)で表現します。
  • Follow Through & Overlapping: 弧全体を弾ませるにはスプリングを easing に使います。