After Effectsのエクスプレッションは、評価結果をキーフレームへ変換(ベイク)できます。選んだプロパティだけを変換するなら標準機能を使います。この記事では、数値の式をコンポ全体の各フレームで処理するJSXも紹介し、1秒・30fpsの回転でキー数、値、Undoを検証します。
標準機能でベイクする手順
- レイヤーを展開し、エクスプレッションが適用されているプロパティを選択します。
- メニューの「アニメーション」>「キーフレーム補助」>「エクスプレッションをキーフレームに変換」を実行します。複数のプロパティを同時に選択して変換することも可能です。
Adobeの公式説明「Convert an expression to keyframes」では、各フレームにキーを作り、元の式を残して無効にする動作が説明されています。以下の検証は、標準機能との比較ではなく掲載JSXの実行結果です。
掲載JSXを試すための準備と適用条件
- プロジェクトを別名保存し、1秒・30fpsの練習用コンポジションを作ります。検証では960×540に200×100のオレンジ色の平面を中央配置しました。
- 平面の回転を表示し、Altキーを押しながらストップウォッチをクリックして、式にtime * 10;を入力します。回転のキーフレームは0個にします。
- このコンポをアクティブにし、後掲コードをbake-numeric.jsxとしてUTF-8で保存します。
- 「ファイル」→「スクリプト」→「スクリプトファイルを実行」でJSXを開きます。
- レイヤーを選んでUキーを押し、回転のキーと0秒・0.5秒・最終フレームの値を確認します。
対象は、有効なエクスプレッションが付いた数値1つ、2D・3Dの数値配列、色の4要素配列です。コードはコンポ内の全レイヤーを調べます。選択プロパティだけ、ワークエリアだけを処理するものではありません。上限は3000フレーム・20プロパティ・合計20000サンプルです。
有効な式の付いたプロパティに既存キー・エラー・非対応の値型がある場合や、そのレイヤーがロックされている場合は、書き込み前に停止する設計です。文字列やシェイプパスを返す式は対象外ですが、文字やシェイプのレイヤーが存在するだけで停止するわけではありません。コードは式を持つプロパティの型を調べています。
1秒30fpsでの実機検証:30キー・値の差・Undo
2026年10月14日、Windows 11 Pro(10.0.26200)/After Effects 26.5(Build 89、日本語表示)で、掲載JSXを変更せず実行しました。回転のtime * 10;をベイクした結果は次のとおりです。
| 確認項目 | 実機結果 |
|---|---|
| キー数と範囲 | 30個。最初は0秒、最後は29/30秒。1秒の終了境界にはキーなし |
| 回転の値 | 0秒=0度、0.5秒=5度、29/30秒=約9.666667度 |
| 全30フレームの数値比較 | 元の式との差は最大約0.000000318度。厳密な数値一致ではない |
| 0.5秒の描画比較 | 前後に出力したPNGの全画素・全チャンネルが一致 |
| 式の状態 | 文字列time * 10;を保持し、式は無効 |
| Ctrl+Zを1回 | キー0個・式有効へ復帰。0.5秒は5度 |
| 回転に既存キー1個を追加して再実行 | Nothing changedの警告で停止。キー1個・式有効を維持 |

今回の単純な回転では、各フレームの値の差はごく小さく、0.5秒の画像は一致しました。ただし、全フレームの画像、曲線的な式、位置の空間軌道、モーションブラーのサブフレーム評価まで一致したという結果ではありません。これらは未検証です。
スクリプトで一括ベイクするコード
全対象の値を先に読み取ってからキーを書き込む構成です。元の式の文字列は保持し、式を無効化して、キーの時間方向の補間をリニアに設定します。APIの補足にはコミュニティで保守されているProperty参照を利用できます。
(function () {
var comp = app.project && app.project.activeItem;
if (!(comp instanceof CompItem)) { alert("Open a composition."); return; }
var targets = [], plans = [], times = [], frame = comp.frameDuration, i, j;
function dimensions(type) {
if (type === PropertyValueType.OneD) { return 1; }
if (type === PropertyValueType.TwoD || type === PropertyValueType.TwoD_SPATIAL) { return 2; }
if (type === PropertyValueType.ThreeD || type === PropertyValueType.ThreeD_SPATIAL) { return 3; }
if (type === PropertyValueType.COLOR) { return 4; }
return 0;
}
function collect(group, locked) {
for (var k = 1; k <= group.numProperties; k++) {
var prop = group.property(k);
if (prop.propertyType !== PropertyType.PROPERTY) { collect(prop, locked); continue; }
if (!prop.canSetExpression || !prop.expressionEnabled) { continue; }
if (locked || prop.numKeys !== 0 || prop.expressionError || !dimensions(prop.propertyValueType)) {
throw new Error("Only unlocked numeric expressions without existing keys are supported.");
}
targets.push(prop);
}
}
try {
if (!isFinite(frame) || frame <= 0 || !isFinite(comp.duration) || comp.duration <= 0) { throw new Error("Invalid duration."); }
var count = Math.ceil(comp.duration / frame - 0.000001);
if (count < 1 || count > 3000) { throw new Error("Use 1 to 3000 frames."); }
for (i = 1; i <= comp.numLayers; i++) { collect(comp.layer(i), comp.layer(i).locked); }
if (!targets.length || targets.length > 20 || targets.length * count > 20000) { throw new Error("Too many properties or samples, or no target."); }
for (i = 0; i < count; i++) { times.push(i * frame); }
// Sample every target before writing keys, including dependent expressions.
for (i = 0; i < targets.length; i++) {
var prop = targets[i], values = [], size = dimensions(prop.propertyValueType);
for (j = 0; j < times.length; j++) {
var value = prop.valueAtTime(times[j], false), copy;
if (prop.expressionError) { throw new Error(prop.expressionError); }
if (size === 1) {
if (typeof value !== "number" || !isFinite(value)) { throw new Error("Invalid scalar sample."); }
copy = value;
} else {
if (!value || value.length !== size) { throw new Error("Invalid vector sample."); }
copy = [];
for (var n = 0; n < size; n++) {
if (typeof value[n] !== "number" || !isFinite(value[n])) { throw new Error("Invalid vector sample."); }
copy.push(value[n]);
}
}
values.push(copy);
}
plans.push({prop:prop, values:values});
}
} catch (error) { alert("Nothing changed: " + error.toString()); return; }
app.beginUndoGroup("Bake numeric expressions at comp frames");
try {
for (i = 0; i < plans.length; i++) {
var plan = plans[i]; plan.prop.setValuesAtTimes(times, plan.values);
for (j = 1; j <= plan.prop.numKeys; j++) {
plan.prop.setInterpolationTypeAtKey(j, KeyframeInterpolationType.LINEAR, KeyframeInterpolationType.LINEAR);
}
plan.prop.expressionEnabled = false; // Keep the original expression text.
}
} catch (error) { alert("Stopped: " + error.toString() + "\nUndo or reopen your practice copy."); }
finally { app.endUndoGroup(); }
})();
停止したとき・元の式に戻すとき
「Only unlocked numeric expressions without existing keys are supported.」と表示された場合は、有効な式が付くプロパティの既存キー、レイヤーロック、式のエラー、値型を確認します。今回の既存キー1個の試験では、追加のベイクキーを作らず停止しました。ほかの停止条件や複数プロパティ間の依存関係は、今回の実機試験には含めていません。
元の状態へ戻すには、実行直後にCtrl+Zを押して、キーが0個、式が有効、値が元どおりであることを確認します。式を有効にするだけではベイクしたキーが残ります。処理途中のエラーで一部だけ書き込まれた場合の復旧は未検証なので、保存した練習用コピーと比較してください。
コンポジションの長さを延ばす場合も再確認が必要です。このコードは実行時の最終フレームまでしかキーを作らず、式は無効のままです。処理時間の短縮効果は測定していません。
参考:25fpsで位置と回転をベイクした既存の画面
以下は2026年10月13日の別条件の画面です。25fpsのコンポジションで、位置にvalue + [Math.sin(time * 2 * Math.PI) * 180, 0]、回転にtime * 120を設定しています。6フレームの表示値は前後とも位置819.6, 360.0、回転28.8度です。今回検証した1秒・30fpsの例とは区別してください。この既存例のキー数は数えていません。


After Effectsのほかの操作は、まとめからご覧いただけます。

旧実装の参考コードは、GitHubのMOTOKI-LLC/cg-method-codeにもまとめています。
エクスプレッションの書き方や、スクリプト化に関する記事は、ほかにもあります。




