アニメーション

FrameScript の動きはほぼここに集約される。useVariable でアニメ可能な値を作り、 useAnimation で「いつ・どう動くか」を async/await で記述する。

メンタルモデル

FrameScript のアニメーションは 「再生時に動く」のではなく「マウント時に未来を全部決める」useAnimation のコールバックは初回マウント時に1回だけ実行され、その中で ctx.move(...).to(...) を呼ぶたびに「この変数はこの区間でこう動く」というセグメントが 変数に追加されていく。

実際の再生(フレームをスクラブ)では、variable.use() がそのフレームに対応する セグメントを補間して値を返すだけ。イベント・分岐・ループのような実行時の挙動は持たない

async/await は「タイムテーブル DSL」

await ctx.sleep(seconds(0.5))0.5秒待つのではなく、 「内部カーソルを 0.5 秒分進める」だけ。
つまり async/await はアニメーションの記述順を時間軸にマッピングする糖衣構文として読む。

useVariable

アニメ可能な値を1つ作るフック。受け取れるのは number / Vec2 / Vec3 / 16進カラー("#RRGGBB" または "#RRGGBBAA")。

const opacity   = useVariable(0)
const pos2      = useVariable({ x: 0, y: 0 })
const pos3      = useVariable({ x: 0, y: 0, z: 0 })
const colorRgb  = useVariable("#FFAA33")
const colorRgba = useVariable("#FFAA33CC")

値を読むときは2通り:

const Box = () => {
  const opacity = useVariable(0)
  useAnimation(async (ctx) => {
    await ctx.move(opacity).to(1, seconds(0.6))
  }, [])

  return <div style={{ opacity: opacity.use() }} />
}
変数を共有しない

1つの useVariable が動かせるのは 1つの useAnimation だけ。 別のシーケンスから同じ変数に ctx.move すると例外になる。
連動させたい時は、1つの useAnimation の中で複数変数を並列に動かす

useAnimation

シグネチャ

const { ready, durationFrames } = useAnimation(
  async (ctx: AnimationContext) => {
    // ここにシーケンスを書く
  },
  deps,
)

ctx の操作

メソッド意味
ctx.sleep(frames)カーソルを n フレーム進める。await で「待つ」。
ctx.waitUntil(frame)絶対フレームまで進める(過去には戻らない)。
ctx.waitUntilClip(label)同名 Clip の開始フレームまで進める。Clip 越しの同期に使える。
ctx.move(v).to(value, frames, easing?)変数 vvalue まで動かすセグメントを追加。
ctx.parallel([h1, h2, ...])複数のハンドルをまとめて、最後まで終わる時刻に揃える。

逐次と並列

逐次(順番に動かす)
useAnimation(async (ctx) => {
  await ctx.move(opacity).to(1, seconds(0.4))
  await ctx.sleep(seconds(0.2))
  await ctx.move(pos).to({ x: 100, y: 0 }, seconds(0.6))
}, [])
並列(同時に動かす)
useAnimation(async (ctx) => {
  const move = ctx.move(pos).to({ x: 240, y: 0 }, seconds(1.2), BEZIER_SMOOTH)
  const fade = ctx.move(opacity).to(1, seconds(0.6), BEZIER_SMOOTH)
  await ctx.parallel([move, fade])
}, [])

「先にキックして後で待つ」パターン

ctx.move(...).to(...) は呼んだ時点でセグメントを作って「ハンドル」を返す。 await は「カーソルをそのハンドルの終端まで進める」操作なので、await のタイミングを後ろにずらすと 重なりや先行スタートが作れる。

useAnimation(async (ctx) => {
  // 動きを先に開始(カーソルは進まない)
  const move = ctx.move(pos).to({ x: 300, y: 0 }, seconds(1), BEZIER_SMOOTH)

  // 別の待ち時間を先に消化
  await ctx.sleep(seconds(0.4))

  // 既に進んでいる move を最後に await(残り時間だけ待つ)
  await move
}, [])

イージング

ctx.move(...).to(value, duration, easing) の第3引数。easing(t: number) => number 型(0..1 → 0..1)。

プリセット(cubic-bezier)

src/lib/animation/functions.ts から import できる。

定数用途
BEZIER_LINEAR等速
BEZIER_EASECSS の ease
BEZIER_EASE_IN / BEZIER_EASE_OUT / BEZIER_EASE_IN_OUTCSS 標準
BEZIER_SMOOTH柔らかい動き(推奨デフォルト)
BEZIER_SHARP切れ味のある動き
BEZIER_ACCELERATE / BEZIER_DECELERATE加速 / 減速
BEZIER_SNAPPY俊敏
BEZIER_OVERSHOOT / _SOFT / _HARD行き過ぎて戻る系

関数イージング

ヘルパー

キーフレーム DSL(プロジェクト同梱)

サンプル付属の project/keyframes.ts を使うと、CSS の @keyframes 風に 多段アニメをまとめて書ける。値が省略された変数はトラック内で線形補間される。

await keyframes(ctx, {
  duration: seconds(1.2),
  easing: BEZIER_EASE_IN_OUT,
  vars: { tx, ty, rot },
  steps: {
    "0%":   { tx: 50,  ty: 3, rot: 30 },
    "50%":  { tx: -50,        rot: -35 }, // ty 省略OK
    "75%":  { tx: 10,  ty: 1, rot: 10 },
    "100%": { tx: 0,   ty: 0, rot: 0 },
  },
})
"0%" は初期値を「設定」しない

"0%" の値は補間の起点として使われるだけ。 呼び出し時点で変数が "0%" の値になっていないと段が切れて見える。
useVariable(initial)initial をきちんと "0%" に合わせるか、 直前のステップで明示的に ctx.move しておく。

シーンを Clip と同期させる

useAnimation はそのシーケンスの長さを親 Clip に自動で報告する。 だから「シーン側でアニメを書くだけで Clip の duration が自然に決まる」のが定石。

逆にいうと、Clip の長さよりアニメが長いと Clip がアニメ準拠で延びる。 固定したい時は Clip 側に duration を明示する。

初フレームの読み込みを待たせたい

useAnimation は内部で __frameScript.waitAnimationsReady という プロミスを発生させていて、レンダラはこれが解決するまで先のフレームを撮らない。 つまり書き出しはアニメ確定を待ってくれる。

Studio で初フレームのチラつきが気になる場合だけ、戻り値の ready を見て if (!ready) return null を入れる。

エフェクト系コンポーネント

<SpeedLines />

集中線オーバーレイ。フレームに合わせて微妙に揺れる。

import { SpeedLines } from "../src/lib/animation/effect/speed-lines"

<FillFrame><SpeedLines /></FillFrame>

<DrawText />

フォントファイルから SVG ストロークを生成して描画する。progress(0..1)で書き進める。

const Title = () => {
  const progress = useVariable(0)
  useAnimation(async (ctx) => {
    await ctx.move(progress).to(1, seconds(2))
  })

  return (
    <DrawText
      text="Hello"
      fontUrl="assets/Roboto.ttf"
      fontSize={96}
      progress={progress}
    />
  )
}

<DrawTex />

TeX 数式を MathJax で SVG にして同様に描く。

<DrawTex
  tex={"\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}"}
  fontSize={96}
  progress={progress}
/>