アニメーション
FrameScript の動きはほぼここに集約される。useVariable でアニメ可能な値を作り、
useAnimation で「いつ・どう動くか」を async/await で記述する。
メンタルモデル
FrameScript のアニメーションは 「再生時に動く」のではなく「マウント時に未来を全部決める」。
useAnimation のコールバックは初回マウント時に1回だけ実行され、その中で
ctx.move(...).to(...) を呼ぶたびに「この変数はこの区間でこう動く」というセグメントが
変数に追加されていく。
実際の再生(フレームをスクラブ)では、variable.use() がそのフレームに対応する
セグメントを補間して値を返すだけ。イベント・分岐・ループのような実行時の挙動は持たない。
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通り:
variable.use()— JSX の中で「現在フレームの値」を取る(再レンダー対象になる)。variable.get(frame)— 任意フレームの値をその場で取る(フックを使えない場所、PSD 等で使う)。
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,
)
depsを変えると再計算される(普通は[]でよい)。- 戻り値の
readyは事前計算の完了フラグ。初フレームのチラつきが気になる時はif (!ready) return nullしておく。 - 戻り値の
durationFramesはそのシーケンスの長さ。Clip に自動報告されるので、Clip 側のdurationを省略できる。
ctx の操作
| メソッド | 意味 |
|---|---|
ctx.sleep(frames) | カーソルを n フレーム進める。await で「待つ」。 |
ctx.waitUntil(frame) | 絶対フレームまで進める(過去には戻らない)。 |
ctx.waitUntilClip(label) | 同名 Clip の開始フレームまで進める。Clip 越しの同期に使える。 |
ctx.move(v).to(value, frames, easing?) | 変数 v を value まで動かすセグメントを追加。 |
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_EASE | CSS の ease |
BEZIER_EASE_IN / BEZIER_EASE_OUT / BEZIER_EASE_IN_OUT | CSS 標準 |
BEZIER_SMOOTH | 柔らかい動き(推奨デフォルト) |
BEZIER_SHARP | 切れ味のある動き |
BEZIER_ACCELERATE / BEZIER_DECELERATE | 加速 / 減速 |
BEZIER_SNAPPY | 俊敏 |
BEZIER_OVERSHOOT / _SOFT / _HARD | 行き過ぎて戻る系 |
関数イージング
easeLinear/easeOutCubic/easeInOutCubic/easeOutExpocubicBezier(x1, y1, x2, y2)— 自前で作る
ヘルパー
frameProgress(frame, start, end, easing?)— 0..1 の進捗をその場で取るfadeInOut(frame, durationFrames, { in?, out? })— 簡易フェードstagger(index, eachFrames, base?)— リスト要素のスタガー開始フレーム計算
キーフレーム 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%" の値になっていないと段が切れて見える。
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}
/>