A Designer-Friendly Procedural Layout Tool for Motion Graphics
Motion designers often spend a surprising amount of time arranging layers into circular patterns, arcs, radial layouts, or evenly spaced formations. Whether it’s creating logo reveals, infographic elements, rotating UI graphics, or kinetic typography, manually positioning layers quickly becomes tedious.
The Advanced Circular Rig v4.4 script was created to solve this exact problem. It provides a powerful procedural system inside Adobe After Effects that allows designers to instantly distribute layers around a circle, arc, or randomized radial layout — while maintaining full artistic control through an intuitive controller.
This article explores how the rig works, what problems it solves, and how designers can use it to speed up their workflow dramatically.

Why Motion Designers Need a Circular Rig
Radial layouts appear everywhere in motion graphics:
• Logo bursts
• UI animations
• Radial menus
• Infographics
• Particle-style motion
• Circular typography
• Orbiting elements
In After Effects, creating these layouts manually requires:
- Calculating angles
- Distributing layers evenly
- Adjusting rotation per layer
- Aligning anchor points
- Maintaining symmetry
Every time the design changes — for example adjusting the radius or number of elements — the entire layout must be rebuilt.
The Advanced Circular Rig turns this static process into a fully procedural system.
Instead of manually arranging layers, you simply:
- Select your layers
- Run the script
- Adjust a few sliders
Your design updates instantly.
What Makes This Rig Powerful
The script introduces a Controller Layer that acts as the central control hub for the entire layout.
All layers become dynamically driven by expressions referencing the controller. When you modify a slider or option, every layer updates automatically.
This approach offers several advantages:
• Non-destructive workflow
• Instant layout updates
• Animation-ready parameters
• Easy experimentation
Instead of rebuilding layouts repeatedly, you simply tweak parameters.
Core Features of the Advanced Circular Rig
1. Multiple Layer Selection Modes
The rig can process layers using three different methods.
Layer Prefix Mode
Automatically selects layers based on a naming prefix.
Example:
Shape_1
Shape_2
Shape_3
Entering the prefix “Shape” allows the script to gather all matching layers.
This is especially useful in large compositions.
Selected Layers Mode
Uses only the layers currently selected in the timeline.
Perfect for quick setups.
Duplicate Selected Mode
Creates multiple duplicates of selected layers before rigging them.
This allows rapid generation of radial arrays without manual duplication.
Example use cases:
• radial icons
• repeating design elements
• burst animations
Procedural Circular Distribution
The rig calculates positions using trigonometry.
Each layer is placed using cosine and sine calculations:
x = cos(angle) * radius
y = sin(angle) * radius
This mathematical approach ensures:
• perfect spacing
• smooth arcs
• predictable results
Because the layout is expression-driven, changing the radius or arc angle instantly recalculates positions.
Spread Radius
The Spread Radius slider controls how far layers sit from the center.
Small radius → tight cluster
Large radius → wide circular layout
Designers can animate this value to create:
• radial expansions
• logo bursts
• UI reveals
Arc Angle Control
Instead of always using a full circle, you can restrict the layout to an arc.
Examples:
360° → full circle
180° → semicircle
90° → quarter arc
This is extremely useful for:
• gauges
• radial charts
• circular UI elements
Rotation Offset
The Rotation Offset rotates the entire distribution around the center.
Instead of manually rotating layers, a single slider moves the whole formation.
This parameter is perfect for animation and timing adjustments.
Layer Order Control
The rig supports three ordering modes.
Top to Bottom
Uses timeline order.
Bottom to Top
Reverses distribution direction.
Random
Places layers randomly within the arc.
Random mode uses a seeded random generator so results remain consistent unless the seed changes.
Look At Center Rotation
Designers often want elements to face the center of the circle.
The Look at Centre option automatically rotates layers so they point inward.
This is useful for:
• arrows
• icons
• radial menus
• pointer graphics
An additional Invert Direction checkbox allows layers to face outward instead.
Per-Layer Rotation Variation
To add visual variation, the script supports per-layer rotation offsets.
Controls include:
• Unit Rot Min
• Unit Rot Max
• Unit Rot Fade %
This creates gradual rotation variation across the layout.
Example uses:
• organic layouts
• stylized radial compositions
• asymmetrical design motion
Procedural Scale Distribution
The rig also supports scale gradients across layers.
Using the parameters:
Scale Min
Scale Max
Scale Fade %
Designers can create layouts where elements grow or shrink progressively around the circle.
Example uses:
• depth illusion
• infographic hierarchy
• motion emphasis
Global Opacity Control
The rig includes a global opacity slider.
Instead of adjusting each layer individually, designers can control transparency for all rigged layers at once.
This is useful for:
• fade-ins
• motion timing
• quick visual adjustments
Automatic Anchor Centering
Incorrect anchor points often cause rotation problems.
The script automatically centers each layer’s anchor point using its bounding box.
This ensures accurate circular positioning and smooth rotation behavior.
Parenting to Controller
When enabled, the rig automatically parents processed layers to the controller.
Benefits include:
• easier animation
• moving the entire formation at once
• simple rotation of the whole rig
The controller effectively becomes the pivot for the entire layout.
Visual Guide Circle
Designers can optionally generate a guide circle representing the spread radius.
This guide updates automatically when the radius changes.
Benefits:
• visual layout reference
• faster composition alignment
• clearer design previews
Live Parameter Updates
One of the most important improvements in version 4.4 is live updating sliders.
While dragging a slider:
• values update instantly
• expressions recalculate live
• no rebuild required
Undo groups are handled carefully to prevent performance issues.
Performance Optimizations
Large layer setups can easily slow down After Effects.
This script includes several performance-focused improvements:
• safe expression refresh
• optimized effect detection
• minimized undo stack creation
• stable layer referencing
These optimizations allow the rig to handle complex compositions more smoothly.
Designer-Friendly Workflow
The script is designed specifically with motion designers in mind.
Key usability features include:
• simple UI layout
• descriptive labels
• non-destructive operation
• flexible layer selection modes
You don’t need programming knowledge to use it.
The workflow feels similar to procedural tools found in 3D software.
Ideal Use Cases
This rig is extremely versatile.
Designers can use it for:
• logo animations
• radial menus
• circular infographics
• icon bursts
• UI micro-interactions
• particle-style motion
• orbiting design elements
Because everything is procedural, you can quickly iterate different visual ideas.
Why Procedural Design Matters
Procedural tools dramatically speed up motion design workflows.
Instead of spending time performing repetitive tasks, designers can focus on creativity.
With a procedural rig:
• layouts update automatically
• design variations are instant
• animation becomes easier
The Advanced Circular Rig demonstrates how scripting can transform After Effects into a much more powerful motion design environment.
Conclusion
The Advanced Circular Rig v4.4 provides motion designers with a powerful and flexible system for creating radial layouts quickly and efficiently.
With features such as:
• procedural positioning
• arc distribution
• random placement
• scale gradients
• automatic anchor alignment
• controller-driven animation
this tool removes many of the manual steps involved in circular compositions.
Whether you’re building UI animations, logo reveals, or complex motion graphics systems, this rig can dramatically accelerate your workflow while keeping your designs flexible and animation-ready.
(function(thisObj) {
// =============================================================================
// ADVANCED CIRCULAR RIG v4.4 (Live Updates & Parenting)
// =============================================================================
// =========================================================================
// EXPRESSION TEMPLATES (tokens: ###TOTAL### ###INDEX###)
// =========================================================================
var POS_EXPR =[
"try {",
" var ctrl = thisComp.layer('Controller');",
" var radius = ctrl.effect('Spread Radius')('Slider');",
" var arcAngle = ctrl.effect('Arc Angle')('Slider');",
" var rotOffset = ctrl.effect('Rotation Offset')('Slider');",
" var orderMenu = Math.round(ctrl.effect('Order')('Slider'));",
" var seedVal = ctrl.effect('Random Seed')('Slider');",
" var total = ###TOTAL###; var myIdx = ###INDEX###;",
" var offsetRad = degreesToRadians(rotOffset) - Math.PI / 2;",
" var angle, r;",
" if (orderMenu === 3) {",
" seedRandom(myIdx + seedVal, true);",
" angle = random(0, degreesToRadians(arcAngle)) + offsetRad;",
" r = random(radius * 0.1, radius);",
" } else {",
" var isFull = arcAngle >= 360;",
" var div = isFull ? total : (total > 1 ? total - 1 : 1);",
" var step = degreesToRadians(arcAngle) / div;",
" var cs = (orderMenu === 2) ? (total - 1 - myIdx) : myIdx;",
" angle = cs * step + offsetRad; r = radius;",
" }",
" value +[Math.cos(angle) * r, Math.sin(angle) * r];",
"} catch(e) { value; }"
].join("\n");
var ROT_EXPR =[
"try {",
" var ctrl = thisComp.layer('Controller');",
" var lookAt = ctrl.effect('Look at Center')('Checkbox');",
" var dir = ctrl.effect('Invert Direction')('Checkbox');",
" var arcAngle = ctrl.effect('Arc Angle')('Slider');",
" var rotOffset = ctrl.effect('Rotation Offset')('Slider');",
" var orderMenu = Math.round(ctrl.effect('Order')('Slider'));",
" var seedVal = ctrl.effect('Random Seed')('Slider');",
" var unitMin = ctrl.effect('Unit Rot Min')('Slider');",
" var unitMax = ctrl.effect('Unit Rot Max')('Slider');",
" var unitFade = ctrl.effect('Unit Rot Fade %')('Slider') / 100;",
" var total = ###TOTAL###; var myIdx = ###INDEX###;",
" var t = total > 1 ? myIdx / (total - 1) : 0;",
" var ft = linear(t, 0, 1, unitFade, 1 - unitFade);",
" var uRot = unitMin + (unitMax - unitMin) * ft;",
" var offsetRad = degreesToRadians(rotOffset) - Math.PI / 2;",
" var angle;",
" if (orderMenu === 3) {",
" seedRandom(myIdx + seedVal, true);",
" angle = random(0, degreesToRadians(arcAngle)) + offsetRad;",
" } else {",
" var isFull = arcAngle >= 360;",
" var div = isFull ? total : (total > 1 ? total - 1 : 1);",
" var step = degreesToRadians(arcAngle) / div;",
" var cs = (orderMenu === 2) ? (total - 1 - myIdx) : myIdx;",
" angle = cs * step + offsetRad;",
" }",
" var baseRot = value;",
" if (lookAt == 1) {",
" var deg = radiansToDegrees(angle) + 90;",
" baseRot = (dir == 1) ? deg + 180 : deg;",
" }",
" baseRot + uRot;",
"} catch(e) { value; }"
].join("\n");
var SCALE_EXPR =[
"try {",
" var ctrl = thisComp.layer('Controller');",
" var sMin = ctrl.effect('Scale Min')('Slider');",
" var sMax = ctrl.effect('Scale Max')('Slider');",
" var sFade = ctrl.effect('Scale Fade %')('Slider') / 100;",
" var total = ###TOTAL###; var myIdx = ###INDEX###;",
" var t = total > 1 ? myIdx / (total - 1) : 0;",
" var s = sMin + (sMax - sMin) * linear(t, 0, 1, sFade, 1 - sFade);",
" value * (s / 100);",
"} catch(e) { value; }"
].join("\n");
var OPA_EXPR =[
"try {",
" var op = thisComp.layer('Controller').effect('Opacity')('Slider');",
" value * (op / 100);",
"} catch(e) { value; }"
].join("\n");
// =========================================================================
// HELPERS
// =========================================================================
function safeRefresh() {
try { app.refresh(); } catch(e) {}
}
function getComp() {
var c = app.project.activeItem;
return (c && c instanceof CompItem) ? c : null;
}
function getController(comp) {
try { return comp.layer("Controller"); } catch(e) { return null; }
}
function getTransformProp(layer, kind) {
var tg = layer.property("ADBE Transform Group");
if (!tg) return null;
var map = {
anchorPoint :["ADBE Anchor Point"],
position :["ADBE Position", "ADBE Position_0"],
rotation :["ADBE Rotate Z", "ADBE Rotation"],
scale : ["ADBE Scale"],
opacity : ["ADBE Opacity"]
};
var tries = map[kind] ||[];
for (var i = 0; i < tries.length; i++) {
try { var p = tg.property(tries[i]); if (p) return p; } catch(e) {}
}
var disp = {anchorPoint:"Anchor Point", position:"Position",
rotation:"Rotation", scale:"Scale", opacity:"Opacity"}[kind];
for (var n = 1; n <= tg.numProperties; n++) {
try { var s = tg.property(n); if (s && s.name === disp) return s; } catch(e) {}
}
return null;
}
function centerAnchorPoint(layer) {
try {
var tg = layer.property("ADBE Transform Group");
var ap = tg.property("ADBE Anchor Point");
var pos = tg.property("ADBE Position");
if (!ap || !pos) return;
var r = layer.sourceRectAtTime(0, false);
var ncx = r.left + r.width / 2;
var ncy = r.top + r.height / 2;
var oldApExpr = ap.expression;
var oldPosExpr = pos.expression;
var curPos = pos.value;
var curAp = ap.value;
if (oldApExpr) ap.expression = "";
if (oldPosExpr) pos.expression = "";
ap.setValue([ncx, ncy]);
pos.setValue([curPos[0] + (ncx - curAp[0]), curPos[1] + (ncy - curAp[1])]);
if (oldApExpr) ap.expression = oldApExpr;
if (oldPosExpr) pos.expression = oldPosExpr;
} catch(e) {}
}
function getEffectProp1(comp, fxName) {
var ctrl = getController(comp);
if (!ctrl) return null;
var fx = ctrl.property("ADBE Effect Parade");
for (var e = 1; e <= fx.numProperties; e++) {
if (fx.property(e).name === fxName) {
try { return fx.property(e).property(1); } catch(ee) {}
}
}
return null;
}
function syncToAE(effectName, val, isDragging) {
var comp = getComp();
if (!comp) return;
var prop = getEffectProp1(comp, effectName);
if (!prop) return;
// Prevent undo-stack bloat & script-freeze by only opening Undo Groups on mouse up (onChange)
if (isDragging) {
try { prop.setValue(val); } catch(e) {}
} else {
app.beginUndoGroup("Rig: " + effectName);
try { prop.setValue(val); } catch(e) {}
app.endUndoGroup();
}
}
function ensureEffect(fx, matchName, displayName, defVal) {
for (var e = 1; e <= fx.numProperties; e++) {
if (fx.property(e).name === displayName) return;
}
try {
var p = fx.addProperty(matchName);
p.name = displayName;
p.property(1).setValue(defVal);
} catch(err) {}
}
function removeAllExpressions(layer) {
var ks =["position","rotation","scale","opacity"];
for (var i = 0; i < ks.length; i++) {
try { var p = getTransformProp(layer, ks[i]); if (p) p.expression = ""; } catch(e) {}
}
}
function rigLayer(lyr, pExpr, rExpr, sExpr, oExpr, cx, cy, doCentre, doParent, ctrlLyr) {
var pPos = getTransformProp(lyr, "position");
var pRot = getTransformProp(lyr, "rotation");
var pScl = getTransformProp(lyr, "scale");
var pOpa = getTransformProp(lyr, "opacity");
if (pPos) {
try {
if (typeof pPos.dimensionsSeparated !== 'undefined' && pPos.dimensionsSeparated) {
pPos.dimensionsSeparated = false;
}
if (doCentre && pPos.expression === "") {
// Temporarily remove parent to set world position accurately
var oldParent = lyr.parent;
lyr.parent = null;
pPos.setValue(pPos.value.length === 3 ? [cx, cy, 0] : [cx, cy]);
lyr.parent = oldParent;
}
} catch(e) {}
}
if (doParent && ctrlLyr) {
lyr.parent = ctrlLyr;
}
if (pPos) { try { pPos.expression = pExpr; } catch(e) {} }
if (pRot) { try { pRot.expression = rExpr; } catch(e) {} }
if (pScl) { try { pScl.expression = sExpr; } catch(e) {} }
if (pOpa) { try { pOpa.expression = oExpr; } catch(e) {} }
}
// =========================================================================
// UI
// =========================================================================
function buildUI(thisObj) {
var win = (thisObj instanceof Panel)
? thisObj
: new Window("palette", "Advanced Circular Rig v4.4", undefined, {resizeable:true});
win.orientation = "column";
win.alignChildren =["fill","top"];
win.spacing = 7;
win.margins = 12;
var statusBar = win.add("statictext", undefined, "Ready.");
statusBar.alignment =["fill","top"];
statusBar.justify = "center";
function setStatus(msg) { statusBar.text = msg; }
// =====================================================================
// PANEL 1 — SETUP
// =====================================================================
var pSetup = win.add("panel", undefined, "1. Setup");
pSetup.orientation = "column";
pSetup.alignChildren =["fill","top"];
pSetup.spacing = 5;
pSetup.margins =[10,14,10,10];
var modeGrp = pSetup.add("group");
modeGrp.orientation = "column";
modeGrp.alignChildren =["left","top"];
modeGrp.spacing = 4;
var r1 = modeGrp.add("group");
r1.orientation = "row";
r1.alignChildren =["left","center"];
var rbPrefix = r1.add("radiobutton", undefined, "Layer Prefix:");
var prefixInput = r1.add("edittext", undefined, "Shape");
prefixInput.preferredSize.width = 90;
var r2 = modeGrp.add("group");
r2.orientation = "row";
r2.alignChildren =["left","center"];
var rbSelected = r2.add("radiobutton", undefined, "Use Selected Layers");
var r3 = modeGrp.add("group");
r3.orientation = "row";
r3.alignChildren = ["left","center"];
var rbDuplicate = r3.add("radiobutton", undefined, "Duplicate Selected \u00D7");
var dupCountInput = r3.add("edittext", undefined, "8");
dupCountInput.preferredSize.width = 34;
var currentMode = "prefix";
function setMode(which) {
currentMode = which;
rbPrefix.value = (which === "prefix");
rbSelected.value = (which === "selected");
rbDuplicate.value = (which === "duplicate");
prefixInput.enabled = (which === "prefix");
dupCountInput.enabled = (which === "duplicate");
}
setMode("prefix");
rbPrefix.onClick = function() { setMode("prefix"); };
rbSelected.onClick = function() { setMode("selected"); };
rbDuplicate.onClick = function() { setMode("duplicate"); };
var optsGrp = pSetup.add("group");
optsGrp.orientation = "column";
optsGrp.alignChildren = ["left", "top"];
optsGrp.spacing = 3;
var cbCenterCtrl = optsGrp.add("checkbox", undefined, "Place Controller at Comp Centre");
var cbCenterLayers = optsGrp.add("checkbox", undefined, "Move Target Layers to Comp Centre");
var cbCenterAnchor = optsGrp.add("checkbox", undefined, "Centre Anchor Point on Each Layer");
var cbParentLayer = optsGrp.add("checkbox", undefined, "Parent Processed Layers to Controller");
var cbGuide = optsGrp.add("checkbox", undefined, "Add visual guide circle");
cbCenterCtrl.value = true;
cbCenterLayers.value = true;
cbCenterAnchor.value = true;
cbParentLayer.value = true;
cbGuide.value = false;
var rColor = pSetup.add("group");
rColor.orientation = "row";
rColor.alignChildren = ["left", "center"];
rColor.add("statictext", undefined, "Set Rig Color:");
var dropLabel = rColor.add("dropdownlist", undefined,["(Don't Change)", "1: Red", "2: Yellow", "3: Aqua", "4: Pink", "5: Lavender", "6: Peach", "7: Sea Foam", "8: Blue", "9: Green", "10: Purple", "11: Orange", "12: Brown", "13: Fuchsia", "14: Cyan", "15: Sandstone", "16: Dark Grn"]);
dropLabel.selection = 0;
dropLabel.preferredSize.width = 110;
var actGrp = pSetup.add("group");
actGrp.orientation = "row";
actGrp.margins =[0,5,0,0];
var btnBuild = actGrp.add("button", undefined, "\u25B6 Build / Update Rig");
var btnClear = actGrp.add("button", undefined, "\u2715 Remove Rig");
// =====================================================================
// PANEL 2 — LIVE SETTINGS
// =====================================================================
var pLive = win.add("panel", undefined, "2. Live Settings");
pLive.orientation = "column";
pLive.alignChildren = ["fill","top"];
pLive.spacing = 4;
pLive.margins =[10,14,10,10];
var LABEL_W = 100;
function addRow(parent, label, minV, maxV, defVal, dp) {
dp = (dp === undefined) ? 0 : dp;
var g = parent.add("group");
g.orientation = "row";
g.alignChildren =["left","center"];
g.margins =[0,1,0,1];
var lbl = g.add("statictext", undefined, label + ":");
lbl.preferredSize.width = LABEL_W;
var sl = g.add("slider", undefined, defVal, minV, maxV);
sl.preferredSize =[110, 18];
sl.minimumSize =[60, 16];
var et = g.add("edittext", undefined, defVal.toFixed(dp));
et.preferredSize.width = 44;
et.justify = "right";
// onChanging triggers rapidly. We pass isDragging = true
sl.onChanging = function() {
var v = parseFloat(sl.value.toFixed(dp));
et.text = v.toFixed(dp);
syncToAE(label, v, true);
};
// onChange triggers on release. We pass isDragging = false
sl.onChange = function() {
var v = parseFloat(sl.value.toFixed(dp));
et.text = v.toFixed(dp);
syncToAE(label, v, false);
};
et.onChange = function() {
var v = parseFloat(et.text);
if (isNaN(v)) v = defVal;
v = Math.max(minV, Math.min(maxV, v));
sl.value = v;
et.text = v.toFixed(dp);
syncToAE(label, v, false);
};
return {
slider: sl, text: et,
getValue: function() { return parseFloat(sl.value.toFixed(dp)); }
};
}
function secLabel(parent, txt) {
var g = parent.add("group");
g.margins =[0,5,0,0];
var st = g.add("statictext", undefined, txt.toUpperCase());
st.graphics.font = ScriptUI.newFont("dialog", "BOLD", 10);
}
function addDropRow(parent, label, items) {
var g = parent.add("group");
g.orientation = "row";
g.alignChildren =["left","center"];
g.margins = [0,1,0,1];
var lbl = g.add("statictext", undefined, label + ":");
lbl.preferredSize.width = LABEL_W;
var dd = g.add("dropdownlist", undefined, items);
dd.selection = 0;
dd.preferredSize.width = 160;
return dd;
}
// ── POSITION ─────────────────────────────────────────────────────────
secLabel(pLive, "Position");
var uiRadius = addRow(pLive, "Spread Radius", 0, 2000, 500);
var uiArc = addRow(pLive, "Arc Angle", 0, 360, 360);
var uiOffset = addRow(pLive, "Rotation Offset", -360, 360, 0);
var uiOrder = addDropRow(pLive, "Order",["Top to Bottom", "Bottom to Top", "Random"]);
uiOrder.onChange = function() { syncToAE("Order", uiOrder.selection.index + 1, false); };
// ── ROTATION ─────────────────────────────────────────────────────────
secLabel(pLive, "Rotation");
var lkGrp = pLive.add("group");
lkGrp.orientation = "row";
lkGrp.alignChildren = ["left","center"];
lkGrp.margins =[0,1,0,1];
var lkSpc = lkGrp.add("statictext", undefined, "");
lkSpc.preferredSize.width = LABEL_W;
var cbLook = lkGrp.add("checkbox", undefined, "Look at Centre");
var cbDir = lkGrp.add("checkbox", undefined, "Invert Direction");
cbDir.enabled = false;
cbLook.onClick = function() {
syncToAE("Look at Center", cbLook.value ? 1 : 0, false);
cbDir.enabled = cbLook.value;
};
cbDir.onClick = function() {
syncToAE("Invert Direction", cbDir.value ? 1 : 0, false);
};
var uiUnitMin = addRow(pLive, "Unit Rot Min", -720, 720, 0);
var uiUnitMax = addRow(pLive, "Unit Rot Max", -720, 720, 0);
var uiUnitFade = addRow(pLive, "Unit Rot Fade %", 0, 100, 0);
// ── SCALE ─────────────────────────────────────────────────────────────
secLabel(pLive, "Scale");
var uiScaleMin = addRow(pLive, "Scale Min", 0, 500, 100);
var uiScaleMax = addRow(pLive, "Scale Max", 0, 500, 100);
var uiScaleFade = addRow(pLive, "Scale Fade %", 0, 100, 0);
// ── GLOBAL ────────────────────────────────────────────────────────────
secLabel(pLive, "Global");
var uiOpacity = addRow(pLive, "Opacity", 0, 100, 100);
var uiSeed = addRow(pLive, "Random Seed", 0, 999, 1);
// ── Info + bottom buttons ─────────────────────────────────────────────
var infoTxt = win.add("statictext", undefined, "Rigged layers: \u2014");
infoTxt.alignment =["fill","top"];
infoTxt.justify = "center";
var btnRow = win.add("group");
btnRow.alignment = "right";
var btnClose = btnRow.add("button", undefined, "Close");
// =====================================================================
// BUILD LOGIC
// =====================================================================
function executeBuild(andClose) {
var comp = getComp();
if (!comp) { alert("Please activate a composition first."); return false; }
var mode = currentMode;
var prefix = prefixInput.text;
if (mode === "prefix" && prefix === "") {
alert("Layer Prefix cannot be empty."); return false;
}
var dupCount = parseInt(dupCountInput.text, 10);
if (mode === "duplicate" && (isNaN(dupCount) || dupCount < 1)) {
alert("Duplicate count must be a positive integer."); return false;
}
var snapshotIndices =[];
if (mode === "selected" || mode === "duplicate") {
var rawSel = comp.selectedLayers;
for (var si = 0; si < rawSel.length; si++) {
if (rawSel[si].name !== "Controller") snapshotIndices.push(rawSel[si].index);
}
if (snapshotIndices.length === 0) {
alert(mode === "duplicate"
? "Select source layer(s) in the timeline before building."
: "No layers selected (excluding Controller).");
return false;
}
}
app.beginUndoGroup("Advanced Circular Rig: Build");
var errMsg = null;
var targets =[];
var total = 0;
var chosenLabel = dropLabel.selection.index;
try {
var cx = comp.width / 2;
var cy = comp.height / 2;
if (mode === "duplicate") {
for (var s = 0; s < snapshotIndices.length; s++) {
var srcLayer = comp.layer(snapshotIndices[s]);
if (srcLayer.locked) srcLayer.locked = false;
var baseName = srcLayer.name;
var group = [srcLayer];
for (var di = 0; di < dupCount; di++) {
var dup = srcLayer.duplicate();
group.push(dup);
}
group.sort(function(a, b) { return a.index - b.index; });
for (var ni = 0; ni < group.length; ni++) {
group[ni].name = baseName + "_" + (ni + 1);
targets.push(group[ni]);
}
}
mode = "duplicate_done";
}
if (!errMsg && mode !== "duplicate_done") {
if (mode === "selected") {
for (var si = 0; si < snapshotIndices.length; si++) {
targets.push(comp.layer(snapshotIndices[si]));
}
} else if (mode === "prefix") {
for (var li = 1; li <= comp.numLayers; li++) {
var lyr = comp.layer(li);
if (lyr.name !== "Controller" && lyr.name.indexOf(prefix) === 0) {
targets.push(lyr);
}
}
}
}
if (targets.length > 0) {
targets.sort(function(a, b) { return a.index - b.index; });
}
total = targets.length;
if (!errMsg && total === 0) {
errMsg = (mode === "selected")
? "No layers selected (excluding Controller)."
: "No layers found with prefix: \"" + prefix + "\"";
}
if (!errMsg && total > 0) {
// ── Controller ────────────────────────────────────────────
var ctrlLayer = getController(comp);
if (!ctrlLayer) {
ctrlLayer = comp.layers.addNull();
ctrlLayer.name = "Controller";
}
if (chosenLabel > 0) {
ctrlLayer.label = chosenLabel;
} else if (ctrlLayer.label === 0) {
ctrlLayer.label = 2; // Default if nothing chosen
}
if (cbCenterCtrl.value && ctrlLayer.property("ADBE Transform Group").property("ADBE Position").expression === "") {
ctrlLayer.property("ADBE Transform Group")
.property("ADBE Position").setValue([cx, cy]);
}
ctrlLayer.shy = false;
ctrlLayer.locked = false;
var fx = ctrlLayer.property("ADBE Effect Parade");
ensureEffect(fx, "ADBE Slider Control", "Spread Radius", uiRadius.getValue());
ensureEffect(fx, "ADBE Slider Control", "Arc Angle", uiArc.getValue());
ensureEffect(fx, "ADBE Slider Control", "Rotation Offset", uiOffset.getValue());
ensureEffect(fx, "ADBE Slider Control", "Order", uiOrder.selection.index + 1);
ensureEffect(fx, "ADBE Checkbox Control", "Look at Center", cbLook.value ? 1 : 0);
ensureEffect(fx, "ADBE Checkbox Control", "Invert Direction",cbDir.value ? 1 : 0);
ensureEffect(fx, "ADBE Slider Control", "Unit Rot Min", uiUnitMin.getValue());
ensureEffect(fx, "ADBE Slider Control", "Unit Rot Max", uiUnitMax.getValue());
ensureEffect(fx, "ADBE Slider Control", "Unit Rot Fade %", uiUnitFade.getValue());
ensureEffect(fx, "ADBE Slider Control", "Scale Min", uiScaleMin.getValue());
ensureEffect(fx, "ADBE Slider Control", "Scale Max", uiScaleMax.getValue());
ensureEffect(fx, "ADBE Slider Control", "Scale Fade %", uiScaleFade.getValue());
ensureEffect(fx, "ADBE Slider Control", "Opacity", uiOpacity.getValue());
ensureEffect(fx, "ADBE Slider Control", "Random Seed", uiSeed.getValue());
// ── Per-layer ─────────────────────────────────────────────
for (var k = 0; k < total; k++) {
var lk = targets[k];
if (chosenLabel > 0) lk.label = chosenLabel;
if (cbCenterAnchor.value) centerAnchorPoint(lk);
var pE = POS_EXPR .replace(/###TOTAL###/g, total).replace(/###INDEX###/g, k);
var rE = ROT_EXPR .replace(/###TOTAL###/g, total).replace(/###INDEX###/g, k);
var sE = SCALE_EXPR.replace(/###TOTAL###/g, total).replace(/###INDEX###/g, k);
rigLayer(lk, pE, rE, sE, OPA_EXPR, cx, cy, cbCenterLayers.value, cbParentLayer.value, ctrlLayer);
}
// ── Guide circle ──────────────────────────────────────────
if (cbGuide.value) {
try { comp.layer("RIG_Guide_Circle").remove(); } catch(e) {}
var gl = comp.layers.addShape();
gl.name = "RIG_Guide_Circle"; gl.label = (chosenLabel > 0) ? chosenLabel : 12; gl.shy = true;
if (cbParentLayer.value) {
gl.parent = ctrlLayer;
gl.property("ADBE Transform Group").property("ADBE Position").setValue([0, 0]);
} else {
gl.property("ADBE Transform Group").property("ADBE Position").setValue([cx, cy]);
}
var ct = gl.property("ADBE Root Vectors Group");
var gg = ct.addProperty("ADBE Vector Group");
var vc = gg.property("ADBE Vectors Group");
var el = vc.addProperty("ADBE Vector Shape - Ellipse");
var r0 = uiRadius.getValue();
el.property("ADBE Vector Ellipse Size").setValue([r0*2, r0*2]);
var stk = vc.addProperty("ADBE Vector Graphic - Stroke");
stk.property("ADBE Vector Stroke Color").setValue([1, 0.55, 0, 1]);
stk.property("ADBE Vector Stroke Width").setValue(1);
try {
el.property("ADBE Vector Ellipse Size").expression =[
"try{var r=thisComp.layer('Controller').effect('Spread Radius')('Slider');[r*2,r*2];}catch(e){value;}"
].join("");
} catch(e) {}
}
}
} catch(err) {
errMsg = "Build failed:\n" + err.toString();
}
app.endUndoGroup();
if (errMsg) {
alert(errMsg);
safeRefresh();
return false;
}
var src = (currentMode === "selected") ? "selection" : (currentMode === "duplicate") ? "duplicated copies" : "prefix \"" + prefix + "\"";
infoTxt.text = "Rigged " + total + " layers (" + src + ")";
setStatus("\u2714 Rig built on " + total + " layers.");
safeRefresh();
if (andClose && win instanceof Window) win.close();
return true;
}
function executeClear() {
var comp = getComp();
if (!comp) { alert("Select a composition first."); return; }
var clearTargets =[];
if (currentMode === "prefix") {
var px = prefixInput.text;
if (px === "") { alert("Layer Prefix cannot be empty."); return; }
for (var i = 1; i <= comp.numLayers; i++) {
var lyr = comp.layer(i);
if (lyr.name !== "Controller" && lyr.name.indexOf(px) === 0)
clearTargets.push(lyr);
}
} else {
var rawSel = comp.selectedLayers;
for (var si = 0; si < rawSel.length; si++) {
if (rawSel[si].name !== "Controller") clearTargets.push(rawSel[si]);
}
}
if (clearTargets.length === 0) {
alert((currentMode === "prefix") ? "No target layers found with prefix." : "No valid layers selected to clear.");
return;
}
app.beginUndoGroup("Remove Circular Rig Expressions");
for (var j = 0; j < clearTargets.length; j++) removeAllExpressions(clearTargets[j]);
app.endUndoGroup();
infoTxt.text = "Rigged layers: \u2014";
setStatus("Expressions removed from " + clearTargets.length + " layers.");
safeRefresh();
}
btnBuild.onClick = function() { executeBuild(false); };
btnClear.onClick = executeClear;
btnClose.onClick = function() { if (win instanceof Window) win.close(); };
win.layout.layout(true);
win.layout.resize();
return win;
}
// =========================================================================
// LAUNCH
// =========================================================================
var myWin = buildUI(thisObj);
if (myWin instanceof Window) { myWin.center(); myWin.show(); }
})(this);