playrig 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +106 -0
- package/LICENSE +37 -0
- package/README.md +77 -2
- package/bin/playrig.js +4 -0
- package/editions/ae/README.md +144 -0
- package/editions/ae/client/aeb.js +9 -0
- package/editions/ae/examples/hello.jsx +3 -0
- package/editions/ae/examples/inspect.jsx +7 -0
- package/editions/ae/examples/snap.jsx +15 -0
- package/editions/ae/lib/actions.json +159 -0
- package/editions/ae/lib/bridge-api.jsxinc +474 -0
- package/editions/ae/lib/dev.jsxinc +21 -0
- package/editions/ae/lib/icons/README.md +4 -0
- package/editions/ae/lib/icons/dot-error.svg +3 -0
- package/editions/ae/lib/icons/dot-idle.svg +3 -0
- package/editions/ae/lib/icons/dot-running.svg +3 -0
- package/editions/ae/lib/icons/make-png.js +25 -0
- package/editions/ae/lib/icons/png/dot-error.png +0 -0
- package/editions/ae/lib/icons/png/dot-error@2x.png +0 -0
- package/editions/ae/lib/icons/png/dot-idle.png +0 -0
- package/editions/ae/lib/icons/png/dot-idle@2x.png +0 -0
- package/editions/ae/lib/icons/png/dot-running.png +0 -0
- package/editions/ae/lib/icons/png/dot-running@2x.png +0 -0
- package/editions/ae/lib/icons/png/power-off.png +0 -0
- package/editions/ae/lib/icons/png/power-off@2x.png +0 -0
- package/editions/ae/lib/icons/png/power-on.png +0 -0
- package/editions/ae/lib/icons/png/power-on@2x.png +0 -0
- package/editions/ae/lib/icons/png/settings.png +0 -0
- package/editions/ae/lib/icons/png/settings@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-error.png +0 -0
- package/editions/ae/lib/icons/png/status-error@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-ok.png +0 -0
- package/editions/ae/lib/icons/png/status-ok@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-rejected.png +0 -0
- package/editions/ae/lib/icons/png/status-rejected@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-timeout.png +0 -0
- package/editions/ae/lib/icons/png/status-timeout@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-unknown.png +0 -0
- package/editions/ae/lib/icons/png/status-unknown@2x.png +0 -0
- package/editions/ae/lib/icons/power-off.svg +4 -0
- package/editions/ae/lib/icons/power-on.svg +4 -0
- package/editions/ae/lib/icons/settings.svg +4 -0
- package/editions/ae/lib/icons/status-error.svg +4 -0
- package/editions/ae/lib/icons/status-ok.svg +4 -0
- package/editions/ae/lib/icons/status-rejected.svg +4 -0
- package/editions/ae/lib/icons/status-timeout.svg +4 -0
- package/editions/ae/lib/icons/status-unknown.svg +5 -0
- package/editions/ae/lib/panel-core.jsxinc +249 -0
- package/editions/ae/lib/update-key.pem +11 -0
- package/editions/ae/lib/updater.jsxinc +148 -0
- package/editions/ae/library-starter/INDEX.md +5 -0
- package/editions/ae/library-starter/README.md +161 -0
- package/editions/ae/library-starter/_common.jsx +187 -0
- package/editions/ae/library-starter/categories.json +9 -0
- package/editions/ae/library-starter/recipes/.gitkeep +0 -0
- package/editions/ae/package.json +7 -0
- package/editions/ae/panel/Playrig.jsx +39 -0
- package/editions/ae/panel/core.jsx +1026 -0
- package/editions/ae/skills/create-ae-recipe/SKILL.md +85 -0
- package/editions/ae/skills/create-ae-recipe/references/ae-patterns.md +88 -0
- package/editions/ae/skills/create-ae-recipe/references/video-analysis.md +72 -0
- package/editions/ae/skills/create-ae-recipe/scripts/media +5 -0
- package/editions/ae/skills/create-ae-recipe/scripts/media.py +253 -0
- package/editions/ae/skills/create-ae-recipe/scripts/setup.sh +17 -0
- package/editions/ae/skills/create-ae-video/SKILL.md +88 -0
- package/editions/ae/skills/create-ae-video/references/templates.md +90 -0
- package/editions/ae/skills/create-ae-video/scripts/preflight.js +99 -0
- package/editions/ae/skills/use-ae-recipes/SKILL.md +99 -0
- package/editions/ae/skills/use-ae-recipes/scripts/contact-sheet.jsx +10 -0
- package/editions/ae/skills/use-ae-recipes/scripts/edit-text.jsx +12 -0
- package/editions/ae/skills/use-ae-recipes/scripts/remove-comp.jsx +18 -0
- package/editions/pr/README.md +60 -0
- package/editions/pr/SCORE_PROCESS.md +41 -0
- package/editions/pr/client/aeb.js +9 -0
- package/editions/pr/examples/frame.js +6 -0
- package/editions/pr/examples/hello.js +4 -0
- package/editions/pr/organize.default.json +245 -0
- package/editions/pr/package.json +7 -0
- package/editions/pr/plugin/actions.js +79 -0
- package/editions/pr/plugin/dev.js +19 -0
- package/editions/pr/plugin/icons/README.md +4 -0
- package/editions/pr/plugin/icons/file-code.svg +6 -0
- package/editions/pr/plugin/icons/folder.svg +3 -0
- package/editions/pr/plugin/icons/power-off.svg +4 -0
- package/editions/pr/plugin/icons/power-on.svg +4 -0
- package/editions/pr/plugin/icons/settings.svg +4 -0
- package/editions/pr/plugin/icons/status-error.svg +4 -0
- package/editions/pr/plugin/icons/status-ok.svg +4 -0
- package/editions/pr/plugin/icons/status-rejected.svg +4 -0
- package/editions/pr/plugin/icons/status-timeout.svg +4 -0
- package/editions/pr/plugin/icons/status-unknown.svg +5 -0
- package/editions/pr/plugin/incremental.js +212 -0
- package/editions/pr/plugin/index.html +195 -0
- package/editions/pr/plugin/index.js +710 -0
- package/editions/pr/plugin/manifest.json +45 -0
- package/editions/pr/plugin/organize.js +223 -0
- package/editions/pr/skills/score-video/SKILL.md +69 -0
- package/editions/pr/skills/score-video/scripts/mix.py +112 -0
- package/editions/pr/skills/score-video/scripts/place.js +63 -0
- package/lib/ae.js +754 -0
- package/lib/common.js +132 -0
- package/lib/install.js +148 -0
- package/lib/main.js +107 -0
- package/lib/payload.js +60 -0
- package/lib/pr.js +239 -0
- package/lib/update.js +118 -0
- package/package.json +35 -4
- package/skills/create-video/SKILL.md +72 -0
- package/skills/create-video/references/lessons.md +31 -0
- package/skills/create-video/references/project-conventions.md +46 -0
- package/skills/create-video/scripts/ae/list-project.jsx +8 -0
- package/skills/create-video/scripts/ae/move-chips.jsx +14 -0
- package/skills/create-video/scripts/ae/organize-project.jsx +25 -0
- package/skills/create-video/scripts/ae/quadrant-chips.jsx +143 -0
- package/skills/create-video/scripts/ae/render-comps.jsx +16 -0
- package/skills/create-video/scripts/premiere/export-sequences.js +22 -0
- package/skills/create-video/scripts/premiere/make-sequences.js +24 -0
- package/skills/create-video/scripts/premiere/swap-media.js +43 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Playrig for After Effects panel core: everything the panel needs that is NOT user interface. ES3 only.
|
|
2
|
+
// Loaded once by the panel (lib/ folder) and defines $.global.PlayrigCore. Kept free of ScriptUI on purpose so it can move to a UXP
|
|
3
|
+
// port nearly unchanged: actions + job-header parsing/inference (data in actions.json), the per-project history reader and the
|
|
4
|
+
// detail-text builder. File access goes through File/Folder only.
|
|
5
|
+
(function () {
|
|
6
|
+
var G = $.global;
|
|
7
|
+
var C = {};
|
|
8
|
+
var DATA = null;
|
|
9
|
+
var cache = {}; // result.json path -> { t, size, row }
|
|
10
|
+
var FALLBACK = { logFolder: "EDIT_LOGS", legacyLogFolder: "AE-BRIDGE", recipePrefix: "recipe.", actions: {
|
|
11
|
+
"custom": { label: "Custom action", summary: "A job with no declared action." },
|
|
12
|
+
"job.rejected": { label: "Rejected job", summary: "The job was refused before it ran." } }, inferById: [], inferBySource: [] };
|
|
13
|
+
|
|
14
|
+
function readText(f) {
|
|
15
|
+
var s;
|
|
16
|
+
f.encoding = "UTF-8";
|
|
17
|
+
if (!f.open("r")) { throw new Error("Cannot open " + f.fsName); }
|
|
18
|
+
s = f.read();
|
|
19
|
+
f.close();
|
|
20
|
+
return s;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Strict JSON parser: B.json.parse from bridge-api when loaded, else eval of a validated-by-shape string is NOT used.
|
|
24
|
+
function parse(text) {
|
|
25
|
+
if (G.PLAYRIG && G.PLAYRIG.json) { return G.PLAYRIG.json.parse(text); }
|
|
26
|
+
throw new Error("JSON parser not loaded");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// dir: folder containing actions.json. Safe to call again.
|
|
30
|
+
C.init = function (dir) {
|
|
31
|
+
var f = new File(dir + "/actions.json"), d;
|
|
32
|
+
try { d = parse(readText(f)); } catch (e) { d = null; C.initError = e.message; }
|
|
33
|
+
DATA = d && d.actions ? d : FALLBACK;
|
|
34
|
+
return DATA === d;
|
|
35
|
+
};
|
|
36
|
+
C.data = function () { return DATA || FALLBACK; };
|
|
37
|
+
C.logFolder = function () { return C.data().logFolder || "EDIT_LOGS"; };
|
|
38
|
+
C.legacyLogFolder = function () { return C.data().legacyLogFolder || "AE-BRIDGE"; };
|
|
39
|
+
|
|
40
|
+
// ------------------------------------------------------------ actions
|
|
41
|
+
function isRecipe(id) {
|
|
42
|
+
var p = C.data().recipePrefix || "recipe.";
|
|
43
|
+
return typeof id === "string" && id.length > p.length && id.substr(0, p.length) === p;
|
|
44
|
+
}
|
|
45
|
+
C.isKnownAction = function (id) { return !!(id && (C.data().actions[id] || isRecipe(id))); };
|
|
46
|
+
|
|
47
|
+
// Label of a result/row: a recipe job carries its recipe's title in actionLabel; unknown ids are "Custom action".
|
|
48
|
+
C.actionLabel = function (action, actionLabel) {
|
|
49
|
+
var a = C.data().actions;
|
|
50
|
+
if (actionLabel) { return String(actionLabel); }
|
|
51
|
+
if (a[action]) { return a[action].label; }
|
|
52
|
+
if (isRecipe(action)) { return "Recipe: " + action.substr((C.data().recipePrefix || "recipe.").length); }
|
|
53
|
+
return a.custom.label;
|
|
54
|
+
};
|
|
55
|
+
C.actionSummary = function (action) {
|
|
56
|
+
var a = C.data().actions;
|
|
57
|
+
if (a[action]) { return a[action].summary; }
|
|
58
|
+
if (isRecipe(action)) { return a["recipe.run"] ? a["recipe.run"].summary : ""; }
|
|
59
|
+
return a.custom.summary;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// First `// ...` lines of a job script: `// @action <id>` declares the action; the first other comment line is the description.
|
|
63
|
+
C.parseJobHeader = function (src) {
|
|
64
|
+
var out = { action: null, description: null }, lines = String(src || "").split(/\r\n|\r|\n/), i, m, t, a;
|
|
65
|
+
for (i = 0; i < lines.length && i < 10; i++) {
|
|
66
|
+
m = /^\s*\/\/\s?(.*)$/.exec(lines[i]);
|
|
67
|
+
if (!m) { if (/\S/.test(lines[i])) { break; } else { continue; } }
|
|
68
|
+
t = m[1].replace(/^\s+|\s+$/g, "");
|
|
69
|
+
a = /^@action\s+([A-Za-z0-9_.\-]+)/.exec(t);
|
|
70
|
+
if (a) { out.action = a[1]; }
|
|
71
|
+
else if (t && !out.description) { out.description = t.substr(0, 240); }
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Best guess from the job id and script. Ordered rules, first match wins.
|
|
77
|
+
C.inferAction = function (src, id) {
|
|
78
|
+
var d = C.data(), s = String(src || ""), i, r;
|
|
79
|
+
for (i = 0; i < d.inferById.length; i++) {
|
|
80
|
+
r = d.inferById[i];
|
|
81
|
+
if (new RegExp(r.re, r.flags || "").test(String(id || ""))) { return r.action; }
|
|
82
|
+
}
|
|
83
|
+
for (i = 0; i < d.inferBySource.length; i++) {
|
|
84
|
+
r = d.inferBySource[i];
|
|
85
|
+
if (new RegExp(r.re, r.flags || "").test(s)) { return r.action; }
|
|
86
|
+
}
|
|
87
|
+
return "custom";
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Decides {action, actionSource, actionLabel, description} for a job from options (aeb --action/--desc) and its script.
|
|
91
|
+
C.classify = function (opts, src, id) {
|
|
92
|
+
var h = C.parseJobHeader(src), out = { action: null, actionSource: "inferred", actionLabel: null, description: null }, declared;
|
|
93
|
+
opts = opts || {};
|
|
94
|
+
declared = opts.action || h.action;
|
|
95
|
+
if (declared) {
|
|
96
|
+
out.actionSource = "declared";
|
|
97
|
+
out.action = C.isKnownAction(String(declared)) ? String(declared) : "custom";
|
|
98
|
+
} else {
|
|
99
|
+
out.action = C.inferAction(src, id);
|
|
100
|
+
}
|
|
101
|
+
if (opts.actionLabel) { out.actionLabel = String(opts.actionLabel).substr(0, 120); }
|
|
102
|
+
out.description = opts.description ? String(opts.description).substr(0, 240) : (h.description || null);
|
|
103
|
+
return out;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ------------------------------------------------------------ formatting
|
|
107
|
+
function pad2(n) { return (n < 10 ? "0" : "") + n; }
|
|
108
|
+
// "2026-10-02T17:42:05+02:00" -> "02-10-26 17:42" (the clock time the job was started at, not converted).
|
|
109
|
+
C.formatTime = function (iso) {
|
|
110
|
+
var m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(String(iso || ""));
|
|
111
|
+
return m ? (m[3] + "-" + m[2] + "-" + m[1].substr(2) + " " + m[4] + ":" + m[5]) : "";
|
|
112
|
+
};
|
|
113
|
+
C.formatDuration = function (ms) {
|
|
114
|
+
if (ms === null || ms === undefined || isNaN(Number(ms))) { return ""; }
|
|
115
|
+
return ms < 1000 ? (ms + " ms") : ((Math.round(ms / 100) / 10) + " s");
|
|
116
|
+
};
|
|
117
|
+
C.statusGlyph = function (status) {
|
|
118
|
+
return { ok: "\u2713", error: "\u2715", rejected: "\u26A0", timeout: "\u23F1" }[status] || "?";
|
|
119
|
+
};
|
|
120
|
+
C.statusIconName = function (status) {
|
|
121
|
+
return { ok: "status-ok", error: "status-error", rejected: "status-rejected", timeout: "status-timeout" }[status] || "status-unknown";
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ------------------------------------------------------------ project log folder
|
|
125
|
+
// Returns the Folder holding the project's log (EDIT_LOGS, or the legacy AE-BRIDGE until it is migrated), or null.
|
|
126
|
+
C.logDir = function (projectFile) {
|
|
127
|
+
var dir, f;
|
|
128
|
+
if (!projectFile) { return null; }
|
|
129
|
+
dir = projectFile.parent;
|
|
130
|
+
f = new Folder(dir.fsName + "/" + C.logFolder());
|
|
131
|
+
if (f.exists) { return f; }
|
|
132
|
+
f = new Folder(dir.fsName + "/" + C.legacyLogFolder());
|
|
133
|
+
return f.exists ? f : null;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ------------------------------------------------------------ history
|
|
137
|
+
// One light row per result.json: only what the table and tooltip need. The full result is re-read for the detail window.
|
|
138
|
+
function toRow(id, resultFile, logDir, projectPath) {
|
|
139
|
+
var r = parse(readText(resultFile)), row, src = null, cls;
|
|
140
|
+
if (projectPath && r.project && r.project.path && r.project.path !== projectPath) { return null; } // another project/app sharing the folder
|
|
141
|
+
row = { id: id, status: r.status || "?", startedAt: r.startedAt || "", durationMs: r.durationMs, action: r.action || null,
|
|
142
|
+
actionSource: r.actionSource || "declared", actionLabel: r.actionLabel || null, description: r.description || null,
|
|
143
|
+
error: r.error && r.error.message ? String(r.error.message) : null, framesOk: 0, resultPath: resultFile.fsName, projectPath: r.project ? r.project.path : null };
|
|
144
|
+
for (var i = 0; r.snapshots && i < r.snapshots.length; i++) { if (r.snapshots[i].ok) { row.framesOk++; } }
|
|
145
|
+
if (!row.action || !C.isKnownAction(row.action)) {
|
|
146
|
+
// Older job without an action: infer from the script kept in the project log.
|
|
147
|
+
try {
|
|
148
|
+
src = new File(logDir.fsName + "/jobs/" + id + ".jsx");
|
|
149
|
+
src = src.exists ? readText(src) : "";
|
|
150
|
+
} catch (e) { src = ""; }
|
|
151
|
+
cls = C.classify({}, src, id);
|
|
152
|
+
row.action = row.action ? "custom" : cls.action;
|
|
153
|
+
row.actionSource = "inferred";
|
|
154
|
+
if (!row.description) { row.description = cls.description; }
|
|
155
|
+
}
|
|
156
|
+
return row;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Latest `limit` jobs of the project, newest first. {rows, total, dir}. total = result folders found.
|
|
160
|
+
C.loadHistory = function (projectFile, limit) {
|
|
161
|
+
var logDir = C.logDir(projectFile), res, dirs, i, list = [], f, key, c, rows = [], row, projectPath, total;
|
|
162
|
+
if (!logDir) { return { rows: [], total: 0, dir: null }; }
|
|
163
|
+
projectPath = projectFile.fsName;
|
|
164
|
+
res = new Folder(logDir.fsName + "/results");
|
|
165
|
+
if (!res.exists) { return { rows: [], total: 0, dir: logDir.fsName }; }
|
|
166
|
+
dirs = res.getFiles(function (x) { return x instanceof Folder; });
|
|
167
|
+
for (i = 0; i < dirs.length; i++) {
|
|
168
|
+
f = new File(dirs[i].fsName + "/result.json");
|
|
169
|
+
if (f.exists) { list.push({ id: dirs[i].displayName, f: f, t: f.modified ? f.modified.getTime() : 0 }); }
|
|
170
|
+
}
|
|
171
|
+
list.sort(function (a, b) { return (b.t - a.t) || (a.id < b.id ? 1 : (a.id > b.id ? -1 : 0)); });
|
|
172
|
+
for (i = 0; i < list.length; i++) {
|
|
173
|
+
key = list[i].f.fsName;
|
|
174
|
+
c = cache[key];
|
|
175
|
+
if (c && c.t === list[i].t) { row = c.row; }
|
|
176
|
+
else {
|
|
177
|
+
try { row = toRow(list[i].id, list[i].f, logDir, projectPath); } catch (e) { row = { id: list[i].id, status: "?", startedAt: "", action: "custom", actionSource: "inferred", error: "unreadable result.json: " + e.message, resultPath: key }; }
|
|
178
|
+
cache[key] = { t: list[i].t, row: row };
|
|
179
|
+
}
|
|
180
|
+
if (row) { rows.push(row); }
|
|
181
|
+
}
|
|
182
|
+
total = rows.length;
|
|
183
|
+
// Most recent start first (file times can tie when results are copied).
|
|
184
|
+
rows.sort(function (a, b) { return a.startedAt < b.startedAt ? 1 : (a.startedAt > b.startedAt ? -1 : 0); });
|
|
185
|
+
if (rows.length > limit) { rows.length = limit; }
|
|
186
|
+
return { rows: rows, total: total, dir: logDir.fsName };
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// A light row built from a finished in-memory result (used for unsaved projects).
|
|
190
|
+
C.rowFromResult = function (r, resultPath) {
|
|
191
|
+
return { id: r.id, status: r.status, startedAt: r.startedAt, durationMs: r.durationMs, action: r.action || "custom", actionSource: r.actionSource || "inferred",
|
|
192
|
+
actionLabel: r.actionLabel || null, description: r.description || null, error: r.error && r.error.message ? String(r.error.message) : null,
|
|
193
|
+
framesOk: 0, resultPath: resultPath, projectPath: r.project ? r.project.path : null };
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
C.readResult = function (row) {
|
|
197
|
+
var f = new File(row.resultPath);
|
|
198
|
+
return f.exists ? parse(readText(f)) : null;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// ------------------------------------------------------------ tooltip / summary line
|
|
202
|
+
C.summaryText = function (row) {
|
|
203
|
+
var t = row.description || C.actionSummary(row.action), bits = [t];
|
|
204
|
+
if (row.actionSource === "inferred") { bits.push("Action: " + C.actionLabel(row.action, row.actionLabel) + " (inferred from the script)"); }
|
|
205
|
+
bits.push("Result: " + row.status + (row.durationMs !== undefined && row.durationMs !== null ? " in " + C.formatDuration(row.durationMs) : ""));
|
|
206
|
+
if (row.error) { bits.push(row.error.substr(0, 160)); }
|
|
207
|
+
if (row.framesOk) { bits.push(row.framesOk + " frame" + (row.framesOk === 1 ? "" : "s") + " exported"); }
|
|
208
|
+
return bits.join(" | ");
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ------------------------------------------------------------ detail text
|
|
212
|
+
// Sections for the job detail window, from a full result.json: [{ title, text }]. Dev-only extras are added by the panel.
|
|
213
|
+
C.detail = function (r, row) {
|
|
214
|
+
var out = [], i, s, sn, t, ret;
|
|
215
|
+
out.push({ title: "What happened", text: (r.description || C.actionSummary(r.action || row.action)) });
|
|
216
|
+
out.push({ title: "Action", text: C.actionLabel(r.action || row.action, r.actionLabel || row.actionLabel) + (r.actionSource === "inferred" || (!r.actionSource && row.actionSource === "inferred") ? " (inferred)" : "") });
|
|
217
|
+
out.push({ title: "Job id", text: String(r.id) });
|
|
218
|
+
out.push({ title: "Started", text: (C.formatTime(r.startedAt) || "?") + (r.durationMs !== undefined ? " (" + C.formatDuration(r.durationMs) + ")" : "") });
|
|
219
|
+
out.push({ title: "Project", text: r.project && r.project.path ? r.project.path + (r.project.dirty ? " (unsaved changes)" : "") : "(unsaved project)" });
|
|
220
|
+
if (r.aeVersion) { out.push({ title: "After Effects", text: String(r.aeVersion) }); }
|
|
221
|
+
if (r.error) {
|
|
222
|
+
t = String(r.error.message);
|
|
223
|
+
if (r.error.line) { t += "\nline " + r.error.line; }
|
|
224
|
+
if (r.error.source) { t += ": " + String(r.error.source).replace(/^\s+|\s+$/g, ""); }
|
|
225
|
+
out.push({ title: "Error", text: t, multi: true });
|
|
226
|
+
}
|
|
227
|
+
if (r.log && r.log.length) {
|
|
228
|
+
s = [];
|
|
229
|
+
for (i = 0; i < r.log.length && i < 100; i++) { s.push(String(r.log[i])); }
|
|
230
|
+
out.push({ title: "Log", text: s.join("\n") + (r.log.length > 100 ? "\n... " + (r.log.length - 100) + " more" : ""), multi: true });
|
|
231
|
+
}
|
|
232
|
+
if (r.snapshots && r.snapshots.length) {
|
|
233
|
+
s = [];
|
|
234
|
+
for (i = 0; i < r.snapshots.length; i++) {
|
|
235
|
+
sn = r.snapshots[i];
|
|
236
|
+
s.push(sn.ok ? (sn.comp + " @ " + sn.time + " s: " + sn.file) : ("FAILED " + (sn.comp || "") + " @ " + (sn.time === undefined ? "?" : sn.time) + ": " + (sn.error || "unknown")));
|
|
237
|
+
}
|
|
238
|
+
out.push({ title: "Frames exported", text: s.join("\n"), multi: true });
|
|
239
|
+
}
|
|
240
|
+
if (r.returnValue !== null && r.returnValue !== undefined) {
|
|
241
|
+
ret = (G.PLAYRIG && G.PLAYRIG.json) ? G.PLAYRIG.json.stringify(r.returnValue) : String(r.returnValue);
|
|
242
|
+
out.push({ title: "Returned", text: ret.length > 600 ? ret.substr(0, 600) + " ... (truncated)" : ret, multi: true });
|
|
243
|
+
}
|
|
244
|
+
out.push({ title: "Files", text: row.resultPath });
|
|
245
|
+
return out;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
G.PlayrigCore = C;
|
|
249
|
+
})();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-----BEGIN PUBLIC KEY-----
|
|
2
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAqLgsqpSkR8hPJA6pKLsp
|
|
3
|
+
Rxsu189MMharZQOJa+jS/cyatLklNebjrxhh41n4iHCiB5WZ7Ua6Z/yJFPs3FtmA
|
|
4
|
+
anRE8o86ZJuo16zq0+xrQklAjZWzwi20IODce6+hnv0imtQ84EXi0pp7NYOYTXCX
|
|
5
|
+
3ialfXf5LHR3xtGe6xc49TkMesaFhJsb5Ip270o67tCNFo4PdtO81sG9PuYGRlEM
|
|
6
|
+
d4bd7AFQhT+g00QjWu6cDWHF9X2XzkNRGwpXwAkKmwxU/r2/JW4cRl5V37uIUB2q
|
|
7
|
+
6X6f25YYi7wBXqzEE6ubR+26Y9wz7tqr4xS9R2dvnagQSUY81C0harK/M5RhQ2zo
|
|
8
|
+
Gpp2SixYwsieS60gTO75FQE2RBQhNdBpYQdb3Jc1hz2HlIWSV9WCmg7cPEpqIN1l
|
|
9
|
+
RZYyrTv/c6W+TFhnbbPhvFvUHxmDycFGs/ti3cLIFVI0esIiDAJpmlJVclIQmFnU
|
|
10
|
+
77s3AK5DbbV51KH49YzTzoykrDkfWFffSC1uIlluGEXjAgMBAAE=
|
|
11
|
+
-----END PUBLIC KEY-----
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Playrig updater: checks the public update feed and installs a newer core into the user's data folder. ES3 only, no UI.
|
|
2
|
+
// Defines $.global.PlayrigUpdater. The panel calls configure() first (folder of the running core, settings, JSON parser).
|
|
3
|
+
//
|
|
4
|
+
// Why curl: ExtendScript sockets have no TLS, so HTTPS goes through the system's curl via system.callSystem. This needs
|
|
5
|
+
// "Allow Scripts to Write Files and Access Network" (already required by Playrig).
|
|
6
|
+
// Trust: an update is installed only if (1) its URL is on the pinned GitHub releases path, (2) its SHA-256 matches the feed and
|
|
7
|
+
// (3) its RSA-SHA256 signature verifies against lib/update-key.pem, which ships with the installed panel (never downloaded).
|
|
8
|
+
// Step 3 needs `openssl` (macOS has it; stock Windows does not): without it the panel only reports the update and tells the
|
|
9
|
+
// user to run `playrig update ae`, which verifies natively.
|
|
10
|
+
(function () {
|
|
11
|
+
var G = $.global;
|
|
12
|
+
var FEED = "https://raw.githubusercontent.com/viveknigam3003/playrig-releases/main/versions.json";
|
|
13
|
+
var ASSET_PREFIX = "https://github.com/viveknigam3003/playrig-releases/releases/download/";
|
|
14
|
+
var STUB_API = 1;
|
|
15
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
var WIN = String($.os).indexOf("Windows") >= 0;
|
|
17
|
+
var env = null;
|
|
18
|
+
var U = {};
|
|
19
|
+
|
|
20
|
+
function q(s) {
|
|
21
|
+
s = String(s);
|
|
22
|
+
return WIN ? '"' + s.replace(/"/g, "") + '"' : "'" + s.replace(/'/g, "'\\''") + "'";
|
|
23
|
+
}
|
|
24
|
+
function sh(cmd) {
|
|
25
|
+
var out;
|
|
26
|
+
try { out = system.callSystem(WIN ? "cmd /s /c \"" + cmd + "\"" : cmd); } catch (e) { return ""; }
|
|
27
|
+
return String(out === undefined || out === null ? "" : out);
|
|
28
|
+
}
|
|
29
|
+
var CURL = WIN ? "curl.exe" : "/usr/bin/curl";
|
|
30
|
+
|
|
31
|
+
function parseVer(v) {
|
|
32
|
+
var m = /(\d+)\.(\d+)\.(\d+)/.exec(String(v || ""));
|
|
33
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
34
|
+
}
|
|
35
|
+
function isNewer(a, b) {
|
|
36
|
+
var i;
|
|
37
|
+
a = parseVer(a); b = parseVer(b);
|
|
38
|
+
if (!a || !b) { return false; }
|
|
39
|
+
for (i = 0; i < 3; i++) { if (a[i] !== b[i]) { return a[i] > b[i]; } }
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
function readText(f) { var s; f.encoding = "UTF-8"; if (!f.open("r")) { return null; } s = f.read(); f.close(); return s; }
|
|
43
|
+
function writeText(f, s) { f.encoding = "UTF-8"; if (!f.open("w")) { return false; } f.write(s); f.close(); return true; }
|
|
44
|
+
function ensure(dir) { var f = new Folder(dir); if (!f.exists) { f.create(); } return f.exists; }
|
|
45
|
+
function baseDir() { return Folder.userData.fsName + "/Playrig/ae"; }
|
|
46
|
+
|
|
47
|
+
U.configure = function (e) { env = e; };
|
|
48
|
+
U.parseVersion = parseVer;
|
|
49
|
+
U.isNewer = isNewer;
|
|
50
|
+
|
|
51
|
+
U.current = function () {
|
|
52
|
+
var f = new File(env.here + "/version.json"), s, m;
|
|
53
|
+
s = f.exists ? readText(f) : null;
|
|
54
|
+
m = s ? /"version"\s*:\s*"([^"]+)"/.exec(s) : null;
|
|
55
|
+
return m ? m[1] : null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
U.feedUrl = function () { return env.getSet("updateFeed", "") || FEED; };
|
|
59
|
+
// A custom feed (development only) may point at file:// or another host; the signature check still applies.
|
|
60
|
+
function pinned(url) { return !!env.getSet("updateFeed", "") || String(url).indexOf(ASSET_PREFIX) === 0; }
|
|
61
|
+
|
|
62
|
+
U.due = function () {
|
|
63
|
+
var last = Number(env.getSet("updateCheckedAt", "0")) || 0;
|
|
64
|
+
return new Date().getTime() - last > DAY_MS;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
U.canVerify = function () { return /(OpenSSL|LibreSSL)/.test(sh("openssl version")); };
|
|
68
|
+
|
|
69
|
+
// Reads the feed. Returns { ok, current, latest, newer, entry, error }.
|
|
70
|
+
U.check = function (timeoutSec) {
|
|
71
|
+
var cur = U.current(), out, feed, entry, r;
|
|
72
|
+
env.setSet("updateCheckedAt", String(new Date().getTime()));
|
|
73
|
+
out = sh(CURL + " -fsL --connect-timeout 5 --max-time " + (timeoutSec || 15) + " " + q(U.feedUrl()));
|
|
74
|
+
if (!out) { return { ok: false, current: cur, error: "Could not reach the update server." }; }
|
|
75
|
+
if (out.indexOf("{") !== 0) { return { ok: false, current: cur, error: "Could not read the update feed. No release has been published yet, or the repository is not public." }; }
|
|
76
|
+
try { feed = env.parse(out); } catch (e) { return { ok: false, current: cur, error: "The update feed is not valid." }; }
|
|
77
|
+
entry = feed && feed.ae;
|
|
78
|
+
if (!entry || !parseVer(entry.version)) { return { ok: false, current: cur, error: "The update feed has no After Effects entry." }; }
|
|
79
|
+
r = { ok: true, current: cur, latest: entry.version, newer: isNewer(entry.version, cur), entry: entry, notes: entry.notes || "", page: entry.page || "" };
|
|
80
|
+
if (r.newer && entry.stubApi && Number(entry.stubApi) > STUB_API) {
|
|
81
|
+
r.newer = false; r.needsReinstall = true;
|
|
82
|
+
r.error = "Version " + entry.version + " needs a newer Playrig.jsx: run `playrig install ae`.";
|
|
83
|
+
}
|
|
84
|
+
return r;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function sha256(file) {
|
|
88
|
+
var out, m;
|
|
89
|
+
if (WIN) {
|
|
90
|
+
out = sh("certutil -hashfile " + q(file) + " SHA256").split(/\r?\n/);
|
|
91
|
+
return out.length > 1 ? out[1].replace(/\s+/g, "").toLowerCase() : "";
|
|
92
|
+
}
|
|
93
|
+
out = sh("shasum -a 256 " + q(file));
|
|
94
|
+
m = /^([0-9a-fA-F]{64})/.exec(out);
|
|
95
|
+
return m ? m[1].toLowerCase() : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function fail(msg, tmp) {
|
|
99
|
+
if (tmp) { sh(WIN ? "rmdir /s /q " + q(tmp) : "rm -rf " + q(tmp)); }
|
|
100
|
+
return { ok: false, error: msg };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Downloads, verifies and installs entry into <userData>/Playrig/ae/current. Takes effect after After Effects restarts.
|
|
104
|
+
U.install = function (entry) {
|
|
105
|
+
var stamp = String(new Date().getTime()), tmp = Folder.temp.fsName + "/playrig-update-" + stamp, base = baseDir();
|
|
106
|
+
var zip = tmp + "/payload.zip", sigB64 = tmp + "/sig.b64", sigBin = tmp + "/sig.bin", keyFile, fresh = base + "/new-" + stamp, out, v, vf, s;
|
|
107
|
+
if (!entry || !entry.url || !entry.sha256 || !entry.sig) { return fail("The update entry is incomplete."); }
|
|
108
|
+
if (!pinned(entry.url)) { return fail("The update URL is not on the Playrig releases page; refusing it."); }
|
|
109
|
+
if (!U.canVerify()) { return fail("Cannot verify the signature here (openssl not found). Run `playrig update ae` in a terminal instead."); }
|
|
110
|
+
keyFile = env.libFile("update-key.pem");
|
|
111
|
+
if (!keyFile) { return fail("update-key.pem is missing from the installed panel. Run `playrig install ae`."); }
|
|
112
|
+
if (!ensure(tmp) || !ensure(base)) { return fail("Cannot create a working folder (is scripting file access allowed?)."); }
|
|
113
|
+
|
|
114
|
+
out = sh(CURL + " -fsL --connect-timeout 10 --max-time 180 -o " + q(zip) + " " + q(entry.url) + (WIN ? " && echo PP_OK" : " && echo PP_OK"));
|
|
115
|
+
if (out.indexOf("PP_OK") < 0 || !new File(zip).exists) { return fail("Download failed.", tmp); }
|
|
116
|
+
if (sha256(zip) !== String(entry.sha256).toLowerCase()) { return fail("Checksum mismatch: the download is not the release that was published. Nothing was installed.", tmp); }
|
|
117
|
+
|
|
118
|
+
if (!writeText(new File(sigB64), entry.sig)) { return fail("Cannot write the signature file.", tmp); }
|
|
119
|
+
out = sh("openssl base64 -d -A -in " + q(sigB64) + " -out " + q(sigBin) + " && echo PP_OK");
|
|
120
|
+
if (out.indexOf("PP_OK") < 0) { return fail("The signature is not valid base64.", tmp); }
|
|
121
|
+
out = sh("openssl dgst -sha256 -verify " + q(keyFile.fsName) + " -signature " + q(sigBin) + " " + q(zip));
|
|
122
|
+
if (!/Verified OK/i.test(out)) { return fail("Signature check failed: the update was not signed by Playrig. Nothing was installed.", tmp); }
|
|
123
|
+
|
|
124
|
+
if (!ensure(fresh)) { return fail("Cannot create " + fresh, tmp); }
|
|
125
|
+
out = sh((WIN ? "tar -xf " + q(zip) + " -C " + q(fresh) : "/usr/bin/unzip -q -o " + q(zip) + " -d " + q(fresh)) + " && echo PP_OK");
|
|
126
|
+
if (out.indexOf("PP_OK") < 0) { sh(WIN ? "rmdir /s /q " + q(fresh) : "rm -rf " + q(fresh)); return fail("Could not unpack the update.", tmp); }
|
|
127
|
+
|
|
128
|
+
vf = new File(fresh + "/version.json");
|
|
129
|
+
s = vf.exists ? readText(vf) : null;
|
|
130
|
+
v = s ? /"version"\s*:\s*"([^"]+)"/.exec(s) : null;
|
|
131
|
+
if (!new File(fresh + "/core.jsx").exists || !v || v[1] !== entry.version) {
|
|
132
|
+
sh(WIN ? "rmdir /s /q " + q(fresh) : "rm -rf " + q(fresh));
|
|
133
|
+
return fail("The unpacked update is not the expected version.", tmp);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (WIN) {
|
|
137
|
+
out = sh("(if exist " + q(base + "/previous") + " rmdir /s /q " + q(base + "/previous") + ") & (if exist " + q(base + "/current") + " move " + q(base + "/current") + " " + q(base + "/previous") + ") & move " + q(fresh) + " " + q(base + "/current") + " && echo PP_OK");
|
|
138
|
+
} else {
|
|
139
|
+
out = sh("rm -rf " + q(base + "/previous") + "; if [ -d " + q(base + "/current") + " ]; then mv " + q(base + "/current") + " " + q(base + "/previous") + "; fi; mv " + q(fresh) + " " + q(base + "/current") + " && echo PP_OK");
|
|
140
|
+
}
|
|
141
|
+
if (out.indexOf("PP_OK") < 0) { return fail("Could not put the update in place.", tmp); }
|
|
142
|
+
sh(WIN ? "rmdir /s /q " + q(tmp) : "rm -rf " + q(tmp));
|
|
143
|
+
env.setSet("updateInstalled", entry.version);
|
|
144
|
+
return { ok: true, version: entry.version, restart: true };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
G.PlayrigUpdater = U;
|
|
148
|
+
})();
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Motion Library
|
|
2
|
+
|
|
3
|
+
Your own project-independent library of motion-graphics building blocks for After Effects, designed to grow to hundreds of entries and to be used by agents without loading everything.
|
|
4
|
+
|
|
5
|
+
**One Markdown file per recipe.** Each file has YAML frontmatter (what an agent needs to choose and call it), a body (how it works, which settings matter, gotchas), and the ExtendScript in a ```jsx block. `INDEX.md` is the router.
|
|
6
|
+
|
|
7
|
+
This folder is a starter: it ships no recipes. Create yours with the `create-ae-recipe` skill (from a description or a reference video) or `playrig ae lib new <category>/<id>`, then `playrig ae lib index`.
|
|
8
|
+
|
|
9
|
+
## Progressive disclosure (how agents use it)
|
|
10
|
+
|
|
11
|
+
| Level | File | Read when | Size |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| 0 | `INDEX.md` | Always first. One line per recipe: id, summary, aliases. | ~230 bytes/recipe |
|
|
14
|
+
| 0b | `categories/<name>.md` | Only when the library is large (see below) | one category |
|
|
15
|
+
| 1 | `recipes/<category>/<id>.md` | Once a recipe is chosen: parameters, tweaks, gotchas, what it combines with | ~3-7 KB |
|
|
16
|
+
| 2 | `<id>--t<seconds>.png` next to the recipe, embedded in its `## Preview` section | Only to see what it looks like | images |
|
|
17
|
+
| 3 | the `jsx` block | Only to modify the recipe (running it never needs reading it) | in the recipe file |
|
|
18
|
+
|
|
19
|
+
The agent never has to read recipe scripts to *use* a recipe: `playrig ae lib run` pulls the script out of the file itself.
|
|
20
|
+
|
|
21
|
+
When the library passes **40 recipes**, `playrig ae lib index` automatically turns `INDEX.md` into a short category list with one index per category, so level 0 stays small however big the library gets.
|
|
22
|
+
|
|
23
|
+
## Use a recipe
|
|
24
|
+
|
|
25
|
+
**Start a video with the `create-ae-video` skill** (`.claude/skills/create-ae-video/`): preflight, discovery, storyboard, then it hands off to the two skills below.
|
|
26
|
+
|
|
27
|
+
For agents and for building whole scenes, the **`use-ae-recipes`** skill (`.claude/skills/use-ae-recipes/`) wraps this: it routes through the index, chains recipes, previews, tunes with you and assembles a master comp. To *add* recipes use **`create-ae-recipe`**.
|
|
28
|
+
|
|
29
|
+
Needs After Effects open with the Playrig for After Effects panel started (see the edition README). Run from anywhere:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
playrig ae lib list <word> # search ids, names, tags, aliases, summaries
|
|
33
|
+
playrig ae lib list --category camera
|
|
34
|
+
playrig ae lib show <recipe-id> # parameters + preview paths (--full prints the notes too)
|
|
35
|
+
playrig ae lib run <recipe-id> --param comp="Scene 1" --param text="Ship faster" --param boldWord=faster
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `--param key=value`, repeatable. Validated by type: numbers (with min/max), `#RRGGBB` colours, true/false, lists with `|`. Unknown parameters are rejected with the valid list.
|
|
39
|
+
- Every recipe has a `comp` parameter (the comp to build in, **created if missing**). To chain recipes into one scene, give them the same `comp`; pass `width`, `height`, `duration`, `fps` on the first one.
|
|
40
|
+
- Each run is one Undo step in After Effects. Preview frames are rendered and their paths printed; `--no-snap` skips them.
|
|
41
|
+
- Planned recipes (no script yet) refuse to run and point to their file, which describes how they are built.
|
|
42
|
+
|
|
43
|
+
### Editing in place (no rebuilding)
|
|
44
|
+
|
|
45
|
+
Every run tags the layers it creates in the target comp (layer comment `aeb:<recipe>:<instance>`). To change a recipe's look later, re-run it with `--replace`: only **that recipe's layers** are removed and the new ones take the **same stacking position**; everything else in the scene is untouched.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
playrig ae lib run <recipe-id> --param comp=S1 --param text="New words" --param size=100 --replace
|
|
49
|
+
playrig ae lib run chips-pop-stagger --param comp=S1 --param seed=3 --instance second --replace # a specific instance
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Running a recipe again **without** `--replace` adds another instance on purpose (`default`, `default-2`, …); name them with `--instance`.
|
|
53
|
+
- Replacing **content** under a rig (`camera-slow-push-in`) leaves the new layers outside the rig: re-run the rig with `--replace`. Layers that lost a parent are reported.
|
|
54
|
+
- Recipes that modify existing layers (`text-cmy-split-scatter`, `rig-tilt-board`) are `idempotent: false`; `--replace` refuses and prints what to do instead. `playrig ae lib show <id>` says which.
|
|
55
|
+
- For new words only: `.claude/skills/use-ae-recipes/scripts/edit-text.jsx` edits the text layer directly and keeps its animators.
|
|
56
|
+
- Authoring: a recipe should only create layers (so removing them restores the comp). If it must change existing layers, set `idempotent: false` and write the `replace_note`. `playrig ae lib check` enforces the pair.
|
|
57
|
+
|
|
58
|
+
### Scenes as chains
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
playrig ae lib run bg-radial-gradient --param comp=S1 --param duration=3.5
|
|
62
|
+
playrig ae lib run <recipe-id> --param comp=S1 --param text="the bar for excellence goes high" --param boldWord=excellence
|
|
63
|
+
playrig ae lib run chips-pop-stagger --param comp=S1 --param start=0.5 --param outAt=2.9
|
|
64
|
+
playrig ae lib run text-cmy-split-scatter --param comp=S1 --param splitAt=2.9 --param cutAt=3.5
|
|
65
|
+
|
|
66
|
+
playrig ae lib run bg-blueprint-grid --param comp=S2 --param duration=2.5
|
|
67
|
+
playrig ae lib run text-letter-stagger-pop --param comp=S2 --param text="proof of work" --param driftPercent=0
|
|
68
|
+
playrig ae lib run camera-slow-push-in --param comp=S2
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Order matters where a recipe acts on content: run `text-cmy-split-scatter`, `camera-slow-push-in` and `rig-tilt-board` **after** the layers they operate on exist. Each recipe's "Combine with" and "Gotchas" sections say so.
|
|
72
|
+
|
|
73
|
+
## Recipe file format
|
|
74
|
+
|
|
75
|
+
```markdown
|
|
76
|
+
---
|
|
77
|
+
id: <recipe-id> # lowercase-kebab; must equal the file name
|
|
78
|
+
name: "Per-word 3D fly-in headline"
|
|
79
|
+
category: type # must equal the folder name
|
|
80
|
+
status: ready # ready | experimental | planned
|
|
81
|
+
summary: "One sentence, <= 150 chars. Read for every routing decision."
|
|
82
|
+
use_when: "When the user wants ..."
|
|
83
|
+
tags: ["kinetic-typography", "3D"]
|
|
84
|
+
aliases: ["word by word reveal", "3D text animator"] # other names / industry terms people will say
|
|
85
|
+
composes_with: ["bg-radial-gradient", "text-cmy-split-scatter"] # must be existing ids
|
|
86
|
+
origin: "Where it came from (project / scene)"
|
|
87
|
+
idempotent: true # optional; false = cannot be replaced in place (needs replace_note)
|
|
88
|
+
replace_note: "..." # only with idempotent: false: how to change it instead
|
|
89
|
+
preview_times: [0.25, 0.5, 1.0] # seconds in the comp; rendered on every run, saved with --save-preview
|
|
90
|
+
params: # one flow-map per parameter; quote strings
|
|
91
|
+
comp: {type: string, default: "Scene", desc: "Target comp (created if missing)"}
|
|
92
|
+
size: {type: number, default: 86, min: 4, desc: "Font size in px"}
|
|
93
|
+
color: {type: color, default: "#111111", desc: "Text colour"}
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
# Title
|
|
97
|
+
<!-- previews --> images embedded here automatically by --save-preview
|
|
98
|
+
## What it looks like plain-words description, as you would brief an artist
|
|
99
|
+
## How it works the After Effects mechanics in a few lines
|
|
100
|
+
## Tweaks that matter table: setting -> what it changes visually, with useful values
|
|
101
|
+
## Gotchas what bit you, ordering rules, limits
|
|
102
|
+
## Combine with related recipes
|
|
103
|
+
## Script ```jsx block: ES3; parameters arrive as P.<name>; lib* helpers from _common.jsx
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Parameter types: `string`, `number`, `boolean`, `color`, `list`. Defaults live **only** in the frontmatter; the CLI merges them with `--param` values and hands the script a complete `P`. The frontmatter is a small YAML subset (top-level keys, one level of nesting, flow lists/maps), parsed without dependencies.
|
|
107
|
+
|
|
108
|
+
## Add a recipe
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
playrig ae lib new type/my-effect # scaffold (already passes the linter)
|
|
112
|
+
# edit recipes/type/my-effect.md, then:
|
|
113
|
+
playrig ae lib check # lint everything
|
|
114
|
+
playrig ae lib run my-effect --param comp=TEST --save-preview
|
|
115
|
+
playrig ae lib index # rebuild INDEX.md (+ categories/, index.json)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Guided way:** use the `create-ae-recipe` skill (`.claude/skills/create-ae-recipe/`): describe the effect and iterate with visual previews, or give it a reference video to break down frame by frame and convert into a recipe. It handles scratch comps, previews, the lint, and cleanup.
|
|
119
|
+
|
|
120
|
+
**Easiest way:** finish a good effect in a real project and tell Claude: *"add the <thing> in comp X to the library"*. It inspects it through the bridge, writes the recipe file, tests it on a scratch comp, looks at the preview, saves it and re-indexes.
|
|
121
|
+
|
|
122
|
+
`playrig ae lib check` enforces: id matches file name and kebab-case; category matches folder; required fields; summary length; valid param types and defaults (inside min/max); every `P.x` used by the script is declared; `composes_with` ids exist; the script parses and uses no ES3-forbidden syntax (`let`, `const`, `=>`, template literals, `JSON`, `forEach/map/filter`); ready recipes have previews; embedded images exist. Run it before committing.
|
|
123
|
+
|
|
124
|
+
### Conventions (each came from a real bug)
|
|
125
|
+
|
|
126
|
+
- **matchNames**, never display names (`"ADBE Text Animator"`, `"ADBE Vector Shape - Rect"`).
|
|
127
|
+
- **ES3**: no `let/const`, arrow functions, `Array.indexOf/map/forEach`, `JSON`. The linter catches most.
|
|
128
|
+
- Name parameters for what they do visually (`staggerFrames`, `popFrom`) and put units in `desc`. Give numbers sensible `min`/`max`.
|
|
129
|
+
- Expressions use the JavaScript expression engine (After Effects default) and must be self-contained.
|
|
130
|
+
- Never depend on a specific project: create what you need, find existing things by a parameter.
|
|
131
|
+
- Order-sensitive recipes must say so in "Gotchas". Document what each setting does at low/high values in "Tweaks that matter": that table is what lets an agent tune a recipe without reading the script.
|
|
132
|
+
- Test on a scratch comp, look at the frames, then save previews.
|
|
133
|
+
|
|
134
|
+
## Scaling and housekeeping
|
|
135
|
+
|
|
136
|
+
- Add a category by creating a folder under `recipes/` and (optionally) a line in `categories.json` for its description.
|
|
137
|
+
- Past 40 recipes the index splits by category automatically (`INDEX_SPLIT_THRESHOLD` in the CLI (`playrig/lib/ae.js`)).
|
|
138
|
+
- `index.json` is the machine-readable catalogue (all metadata + params + path), for tools that prefer JSON.
|
|
139
|
+
- The library is just a folder. Copy it anywhere, or point to it with `--lib <dir>` / `AE_LIBRARY`. The scripts are plain ExtendScript; the Playrig for After Effects is only needed to run them.
|
|
140
|
+
- Preview images live **next to the recipe** as `<id>--t<seconds>.png` and are embedded in its `## Preview` block (between `<!-- previews:start -->` and `<!-- previews:end -->` markers, kept up to date by `--save-preview`). Moving or copying a recipe means moving its `.md` and its `<id>--t*.png` files together.
|
|
141
|
+
|
|
142
|
+
## Things to know
|
|
143
|
+
|
|
144
|
+
- **Fonts** must be installed (After Effects substitutes silently). Defaults use Inter; pass `--param font=…` (PostScript name).
|
|
145
|
+
- **Pixel defaults** (positions, areas) assume a 1920x1080 comp.
|
|
146
|
+
- `text-cmy-split-scatter` only works on **light** backgrounds.
|
|
147
|
+
|
|
148
|
+
## Layout
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
<library>/
|
|
152
|
+
├── README.md this file
|
|
153
|
+
├── INDEX.md level-0 router (generated)
|
|
154
|
+
├── index.json machine-readable catalogue (generated)
|
|
155
|
+
├── categories.json one-line description per category
|
|
156
|
+
├── categories/<c>.md per-category indexes (generated, only when the library is large)
|
|
157
|
+
├── _common.jsx shared helper functions appended to every script
|
|
158
|
+
└── recipes/<category>/
|
|
159
|
+
├── <id>.md
|
|
160
|
+
└── <id>--t<seconds>.png its preview images
|
|
161
|
+
```
|