mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-22 18:22:19 +00:00
M13.1: branching history, puppet pin recording, WebAssembly plug-in API, ScriptUI model and hosts, File ▸ Scripts, deterministic render test
- engine: History keeps undone lines as branches; edit.history.list/goto; History panel tree; MCP history tool - puppet.recordPin / puppet.recordOptions; viewer Cmd-drag records; Record Options form - effects::plugin (EffectPlugin trait, register_plugin, API v1); effectcraft-plugin (wasmi, sandboxed, fuel-limited); effect.plugins.load/list - ScriptUI: engine scriptui model + layout + scriptui.* commands; script crate scriptui.js and script-host threads (dialogs block show()) - File ▸ Scripts: install script / ScriptUI panel, sample scripts, window.scriptPanel - host test: bit-identical renders across runs and thread counts Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ebcb8c95ce
commit
89c9b1b6ee
48 files changed
+4440
-134
No files matched your search
@@ -0,0 +1,41 @@
|
||||
// Create Null at Selected Layers — an EffectCraft sample script (original work, MIT OR
|
||||
// Apache-2.0).
|
||||
//
|
||||
// Adds a null at the centroid of the selected layers' positions (at the current time) and
|
||||
// parents the layers that have no parent to it (set PARENT below to false to skip that).
|
||||
// One undo step.
|
||||
|
||||
(function createNullAtCentroid() {
|
||||
var PARENT = true;
|
||||
var comp = app.project.activeItem;
|
||||
if (!(comp instanceof CompItem)) {
|
||||
alert("Create Null: open a composition first.");
|
||||
return;
|
||||
}
|
||||
var layers = comp.selectedLayers;
|
||||
if (layers.length === 0) {
|
||||
alert("Create Null: select one or more layers.");
|
||||
return;
|
||||
}
|
||||
var t = comp.time;
|
||||
var sum = [0, 0, 0];
|
||||
for (var i = 0; i < layers.length; i++) {
|
||||
var p = layers[i].transform.position.valueAtTime(t, false);
|
||||
sum[0] += p[0];
|
||||
sum[1] += p[1];
|
||||
sum[2] += p.length > 2 ? p[2] : 0;
|
||||
}
|
||||
var c = [sum[0] / layers.length, sum[1] / layers.length, sum[2] / layers.length];
|
||||
|
||||
app.beginUndoGroup("Create Null at Selected Layers");
|
||||
var nul = comp.layers.addNull();
|
||||
nul.name = "Centroid";
|
||||
nul.transform.position.setValue(c);
|
||||
if (PARENT) {
|
||||
for (var k = 0; k < layers.length; k++) {
|
||||
if (!layers[k].parent) layers[k].setParentWithJump(nul);
|
||||
}
|
||||
}
|
||||
app.endUndoGroup();
|
||||
writeLn("Null at [" + c[0].toFixed(1) + ", " + c[1].toFixed(1) + "]");
|
||||
})();
|
||||
@@ -0,0 +1,71 @@
|
||||
// Rename Layers — an EffectCraft sample script (original work, MIT OR Apache-2.0).
|
||||
//
|
||||
// Renames the selected layers of the active composition (all layers when none are selected):
|
||||
// find & replace, then an optional prefix, suffix and running number. One undo step.
|
||||
|
||||
(function renameLayers() {
|
||||
var comp = app.project.activeItem;
|
||||
if (!(comp instanceof CompItem)) {
|
||||
alert("Rename Layers: open a composition first.");
|
||||
return;
|
||||
}
|
||||
var layers = comp.selectedLayers;
|
||||
if (layers.length === 0) {
|
||||
layers = [];
|
||||
for (var i = 1; i <= comp.numLayers; i++) layers.push(comp.layer(i));
|
||||
}
|
||||
|
||||
var dlg = new Window("dialog", "Rename Layers");
|
||||
dlg.orientation = "column";
|
||||
dlg.alignChildren = ["fill", "top"];
|
||||
|
||||
var find = dlg.add("panel", undefined, "Find and Replace");
|
||||
find.alignChildren = ["fill", "top"];
|
||||
var row1 = find.add("group");
|
||||
row1.add("statictext", undefined, "Find:");
|
||||
var findText = row1.add("edittext", undefined, "");
|
||||
findText.characters = 20;
|
||||
var row2 = find.add("group");
|
||||
row2.add("statictext", undefined, "Replace:");
|
||||
var replaceText = row2.add("edittext", undefined, "");
|
||||
replaceText.characters = 20;
|
||||
|
||||
var extra = dlg.add("panel", undefined, "Add");
|
||||
extra.alignChildren = ["fill", "top"];
|
||||
var row3 = extra.add("group");
|
||||
row3.add("statictext", undefined, "Prefix:");
|
||||
var prefix = row3.add("edittext", undefined, "");
|
||||
prefix.characters = 12;
|
||||
row3.add("statictext", undefined, "Suffix:");
|
||||
var suffix = row3.add("edittext", undefined, "");
|
||||
suffix.characters = 12;
|
||||
var number = extra.add("checkbox", undefined, "Number the layers");
|
||||
var row4 = extra.add("group");
|
||||
row4.add("statictext", undefined, "Start at:");
|
||||
var start = row4.add("edittext", undefined, "1");
|
||||
start.characters = 5;
|
||||
start.enabled = false;
|
||||
number.onClick = function () {
|
||||
start.enabled = number.value;
|
||||
};
|
||||
|
||||
var buttons = dlg.add("group");
|
||||
buttons.alignment = "right";
|
||||
buttons.add("button", undefined, "Cancel", { name: "cancel" });
|
||||
buttons.add("button", undefined, "OK", { name: "ok" });
|
||||
|
||||
if (dlg.show() !== 1) return;
|
||||
|
||||
app.beginUndoGroup("Rename Layers");
|
||||
var n = parseInt(start.text, 10);
|
||||
if (isNaN(n)) n = 1;
|
||||
for (var k = 0; k < layers.length; k++) {
|
||||
var name = layers[k].name;
|
||||
if (findText.text !== "") name = name.split(findText.text).join(replaceText.text);
|
||||
name = prefix.text + name + suffix.text;
|
||||
if (number.value) name += " " + (n + k);
|
||||
layers[k].name = name;
|
||||
}
|
||||
app.endUndoGroup();
|
||||
writeLn("Renamed " + layers.length + " layer(s).");
|
||||
})();
|
||||
@@ -0,0 +1,52 @@
|
||||
// Render Queue Batch — an EffectCraft sample script (original work, MIT OR Apache-2.0).
|
||||
//
|
||||
// Adds the compositions selected in the Project panel (every composition when none are
|
||||
// selected) to the Render Queue, each with an output file named after the comp in a folder you
|
||||
// choose, and optionally starts rendering.
|
||||
|
||||
(function renderQueueBatch() {
|
||||
var comps = [];
|
||||
var sel = app.project.selection;
|
||||
for (var i = 0; i < sel.length; i++) if (sel[i] instanceof CompItem) comps.push(sel[i]);
|
||||
if (comps.length === 0) {
|
||||
for (var j = 1; j <= app.project.numItems; j++) {
|
||||
var it = app.project.item(j);
|
||||
if (it instanceof CompItem) comps.push(it);
|
||||
}
|
||||
}
|
||||
if (comps.length === 0) {
|
||||
alert("Render Queue Batch: the project has no compositions.");
|
||||
return;
|
||||
}
|
||||
|
||||
var dlg = new Window("dialog", "Render Queue Batch");
|
||||
dlg.alignChildren = ["fill", "top"];
|
||||
dlg.add("statictext", undefined, comps.length + " composition(s) will be queued.");
|
||||
var row = dlg.add("group");
|
||||
row.add("statictext", undefined, "Output folder:");
|
||||
var folder = row.add("edittext", undefined, Folder.desktop ? Folder.desktop.fsName : "");
|
||||
folder.characters = 30;
|
||||
var fmtRow = dlg.add("group");
|
||||
fmtRow.add("statictext", undefined, "Format:");
|
||||
var format = fmtRow.add("dropdownlist", undefined, ["H.264 (.mp4)", "PNG Sequence"]);
|
||||
format.selection = 0;
|
||||
var go = dlg.add("checkbox", undefined, "Start rendering");
|
||||
var buttons = dlg.add("group");
|
||||
buttons.alignment = "right";
|
||||
buttons.add("button", undefined, "Cancel", { name: "cancel" });
|
||||
buttons.add("button", undefined, "Queue", { name: "ok" });
|
||||
if (dlg.show() !== 1) return;
|
||||
|
||||
var png = format.selection && format.selection.index === 1;
|
||||
app.beginUndoGroup("Render Queue Batch");
|
||||
for (var k = 0; k < comps.length; k++) {
|
||||
var rq = app.project.renderQueue.items.add(comps[k]);
|
||||
var name = comps[k].name.replace(/[\\\/:*?"<>|]/g, "_");
|
||||
var om = rq.outputModule(1);
|
||||
if (png) om.format = "png";
|
||||
om.file = new File(folder.text + "/" + name + (png ? "_[#####].png" : ".mp4"));
|
||||
}
|
||||
app.endUndoGroup();
|
||||
writeLn("Queued " + comps.length + " composition(s).");
|
||||
if (go.value) app.project.renderQueue.render();
|
||||
})();
|
||||
@@ -0,0 +1,99 @@
|
||||
// Layer Tools — an EffectCraft sample ScriptUI panel (original work, MIT OR Apache-2.0).
|
||||
//
|
||||
// Installed in the ScriptUI Panels folder it docks like any panel (Window ▸ Layer Tools.jsx);
|
||||
// run from File ▸ Scripts it opens as a floating palette. Buttons act on the selected layers of
|
||||
// the active composition.
|
||||
|
||||
(function layerTools(thisObj) {
|
||||
var ui = thisObj instanceof Panel ? thisObj : new Window("palette", "Layer Tools", undefined, { resizeable: true });
|
||||
ui.orientation = "column";
|
||||
ui.alignChildren = ["fill", "top"];
|
||||
ui.spacing = 6;
|
||||
|
||||
function comp() {
|
||||
var c = app.project.activeItem;
|
||||
if (!(c instanceof CompItem)) {
|
||||
status.text = "Open a composition first.";
|
||||
return null;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
function selected(c) {
|
||||
var l = c.selectedLayers;
|
||||
if (l.length === 0) status.text = "Select some layers.";
|
||||
return l;
|
||||
}
|
||||
|
||||
var labelRow = ui.add("group");
|
||||
labelRow.add("statictext", undefined, "Opacity:");
|
||||
var opacity = labelRow.add("slider", undefined, 100, 0, 100);
|
||||
opacity.preferredSize.width = 120;
|
||||
var opacityValue = labelRow.add("statictext", undefined, "100%");
|
||||
opacityValue.characters = 5;
|
||||
opacity.onChanging = function () {
|
||||
opacityValue.text = Math.round(opacity.value) + "%";
|
||||
};
|
||||
opacity.onChange = function () {
|
||||
var c = comp();
|
||||
if (!c) return;
|
||||
var layers = selected(c);
|
||||
app.beginUndoGroup("Set Opacity");
|
||||
for (var i = 0; i < layers.length; i++) layers[i].transform.opacity.setValue(Math.round(opacity.value));
|
||||
app.endUndoGroup();
|
||||
status.text = "Opacity " + Math.round(opacity.value) + "% on " + layers.length + " layer(s).";
|
||||
};
|
||||
|
||||
var shy = ui.add("checkbox", undefined, "Shy");
|
||||
shy.onClick = function () {
|
||||
var c = comp();
|
||||
if (!c) return;
|
||||
var layers = selected(c);
|
||||
app.beginUndoGroup("Shy Layers");
|
||||
for (var i = 0; i < layers.length; i++) layers[i].shy = shy.value;
|
||||
app.endUndoGroup();
|
||||
};
|
||||
|
||||
var nameRow = ui.add("group");
|
||||
nameRow.alignChildren = ["fill", "center"];
|
||||
var newName = nameRow.add("edittext", undefined, "Layer");
|
||||
newName.characters = 14;
|
||||
var renameBtn = nameRow.add("button", undefined, "Rename");
|
||||
renameBtn.onClick = function () {
|
||||
var c = comp();
|
||||
if (!c) return;
|
||||
var layers = selected(c);
|
||||
app.beginUndoGroup("Rename Layers");
|
||||
for (var i = 0; i < layers.length; i++) layers[i].name = layers.length > 1 ? newName.text + " " + (i + 1) : newName.text;
|
||||
app.endUndoGroup();
|
||||
status.text = "Renamed " + layers.length + " layer(s).";
|
||||
};
|
||||
|
||||
var nullBtn = ui.add("button", undefined, "Null at Centroid");
|
||||
nullBtn.onClick = function () {
|
||||
var c = comp();
|
||||
if (!c) return;
|
||||
var layers = selected(c);
|
||||
if (layers.length === 0) return;
|
||||
var x = 0, y = 0;
|
||||
for (var i = 0; i < layers.length; i++) {
|
||||
var p = layers[i].transform.position.value;
|
||||
x += p[0];
|
||||
y += p[1];
|
||||
}
|
||||
app.beginUndoGroup("Null at Centroid");
|
||||
var n = c.layers.addNull();
|
||||
n.transform.position.setValue([x / layers.length, y / layers.length]);
|
||||
app.endUndoGroup();
|
||||
status.text = "Added " + n.name + ".";
|
||||
};
|
||||
|
||||
var status = ui.add("statictext", undefined, "Select layers, then pick a tool.");
|
||||
status.characters = 30;
|
||||
|
||||
if (ui instanceof Window) {
|
||||
ui.center();
|
||||
ui.show();
|
||||
} else {
|
||||
ui.layout.layout(true);
|
||||
}
|
||||
})(this);
|
||||
@@ -0,0 +1,37 @@
|
||||
// Sort Layers by In Point — an EffectCraft sample script (original work, MIT OR Apache-2.0).
|
||||
//
|
||||
// Reorders the selected layers of the active composition (all layers when none are selected)
|
||||
// so the layer that starts first is on top. Layers with the same in point keep their order.
|
||||
// One undo step.
|
||||
|
||||
(function sortLayersByInPoint() {
|
||||
var comp = app.project.activeItem;
|
||||
if (!(comp instanceof CompItem)) {
|
||||
alert("Sort Layers by In Point: open a composition first.");
|
||||
return;
|
||||
}
|
||||
var layers = comp.selectedLayers;
|
||||
if (layers.length === 0) {
|
||||
layers = [];
|
||||
for (var i = 1; i <= comp.numLayers; i++) layers.push(comp.layer(i));
|
||||
}
|
||||
if (layers.length < 2) return;
|
||||
|
||||
// The slots the layers occupy now (stack indices, top first).
|
||||
var slots = layers.map(function (l) { return l.index; }).sort(function (a, b) { return a - b; });
|
||||
var order = layers
|
||||
.map(function (l, k) { return { layer: l, inPoint: l.inPoint, k: k }; })
|
||||
.sort(function (a, b) { return a.inPoint - b.inPoint || a.k - b.k; });
|
||||
|
||||
app.beginUndoGroup("Sort Layers by In Point");
|
||||
// Place each layer, earliest first, in the next slot from the top.
|
||||
for (var j = 0; j < order.length; j++) {
|
||||
var target = slots[j];
|
||||
var layer = order[j].layer;
|
||||
if (layer.index === target) continue;
|
||||
var anchor = comp.layer(target);
|
||||
layer.moveBefore(anchor);
|
||||
}
|
||||
app.endUndoGroup();
|
||||
writeLn("Sorted " + layers.length + " layer(s) by in point.");
|
||||
})();
|
||||
Reference in new issue
Block a user