(function fake3DCube() {
var SCRIPT_NAME = "Fake 3D Cube";
var comp = app.project.activeItem;
if (!(comp && comp instanceof CompItem)) {
alert("Open a composition (or select one in the Project panel) and run the script again.", SCRIPT_NAME);
return;
}
// Unit cube corners. x right, y down, z away from the viewer.
var V = [
[-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], // front ring (z = -1, closest)
[-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1] // back ring
];
// Corners are listed clockwise as seen from outside the cube, so a face
// whose projected polygon has a positive area is facing the viewer.
var FACES = [
{ name: "Front", v: [0, 1, 2, 3], n: [0, 0, -1], color: [0.93, 0.33, 0.33] },
{ name: "Back", v: [5, 4, 7, 6], n: [0, 0, 1], color: [0.98, 0.62, 0.22] },
{ name: "Left", v: [4, 0, 3, 7], n: [-1, 0, 0], color: [0.30, 0.75, 0.45] },
{ name: "Right", v: [1, 5, 6, 2], n: [1, 0, 0], color: [0.25, 0.55, 1.00] },
{ name: "Top", v: [4, 5, 1, 0], n: [0, -1, 0], color: [0.98, 0.85, 0.30] },
{ name: "Bottom", v: [3, 2, 6, 7], n: [0, 1, 0], color: [0.65, 0.40, 0.90] }
];
var size = Math.round(Math.min(comp.width, comp.height) * 0.25);
var camDistance = Math.round(Math.max(comp.width, comp.height) * 0.8);
var CONTROLS = [
["slider", "Width", size],
["slider", "Height", size],
["slider", "Depth", size],
["angle", "Rotation X", 25],
["angle", "Rotation Y", 40],
["angle", "Rotation Z", 0],
["slider", "Camera Distance", camDistance],
["checkbox", "Orthographic", 0],
["color", "Base Color", [0.25, 0.55, 1.00]],
["slider", "Shading", 45],
["angle", "Light Angle", -35],
["angle", "Light Elevation", 50],
["slider", "Cube Opacity", 100],
["slider", "Fill Opacity", 100],
["color", "Stroke Color", [1, 1, 1]],
["slider", "Stroke Width", 0],
["checkbox", "Wireframe", 0],
["slider", "Hidden Edge Opacity", 25],
["checkbox", "Use Face Colors", 0]
];
for (var f = 0; f < FACES.length; f++) {
CONTROLS.push(["color", FACES[f].name + " Color", FACES[f].color]);
}
var CONTROL_MATCH_NAMES = {
slider: "ADBE Slider Control",
angle: "ADBE Angle Control",
checkbox: "ADBE Checkbox Control",
color: "ADBE Color Control"
};
function layerNameTaken(name) {
for (var i = 1; i <= comp.numLayers; i++) {
if (comp.layer(i).name === name) return true;
}
return false;
}
var cubeIndex = 1;
while (layerNameTaken("Cube " + cubeIndex + " - Controller")) cubeIndex++;
var prefix = "Cube " + cubeIndex;
var ctrlName = prefix + " - Controller";
// ---------------------------------------------------------------- expressions
function lines(arr) { return arr.join("\n"); }
// Controller lookup, shared by every expression.
var EXPR_HEAD = [
'// ' + SCRIPT_NAME + ' - driven by "' + ctrlName + '"',
'var C = thisComp.layer("' + ctrlName + '");',
'function fx(n) { return C.effect(n)(1).value; }'
];
// 3D rotation: Y (turntable) first, then X (tilt), then Z (roll).
var EXPR_ROTATE = [
'var ax = degreesToRadians(fx("Rotation X")), ay = degreesToRadians(fx("Rotation Y")), az = degreesToRadians(fx("Rotation Z"));',
'function rot(p) {',
' var x = p[0], y = p[1], z = p[2], c, s, t;',
' c = Math.cos(ay); s = Math.sin(ay); t = x * c + z * s; z = z * c - x * s; x = t;',
' c = Math.cos(ax); s = Math.sin(ax); t = y * c - z * s; z = y * s + z * c; y = t;',
' c = Math.cos(az); s = Math.sin(az); t = x * c - y * s; y = x * s + y * c; x = t;',
' return [x, y, z];',
'}'
];
// Projected corners of one face (Q), relative to the controller's anchor point.
function exprCorners(face) {
var calls = [];
for (var i = 0; i < 4; i++) {
var u = V[face.v[i]];
calls.push("vert(" + u[0] + ", " + u[1] + ", " + u[2] + ")");
}
return EXPR_HEAD.concat(EXPR_ROTATE, [
'var hw = fx("Width") / 2, hh = fx("Height") / 2, hd = fx("Depth") / 2;',
'var dist = Math.max(fx("Camera Distance"), 1), ortho = fx("Orthographic") > 0;',
'function vert(x, y, z) {',
' var p = rot([x * hw, y * hh, z * hd]);',
' var k = ortho ? 1 : dist / Math.max(dist + p[2], 1);',
' return [p[0] * k, p[1] * k];',
'}',
'var Q = [' + calls.join(", ") + '];'
]);
}
function exprPath(face) {
return lines(exprCorners(face).concat([
'var a = C.transform.anchorPoint.value, pts = [];',
'for (var i = 0; i < 4; i++) {',
' var local = a.length > 2 ? [a[0] + Q[i][0], a[1] + Q[i][1], a[2]] : [a[0] + Q[i][0], a[1] + Q[i][1]];',
' var p = fromComp(C.toComp(local));',
' pts.push([p[0], p[1]]);',
'}',
'createPath(pts, [], [], true);'
]));
}
// Backface culling. In wireframe mode every edge is shared by two back
// faces, so each gets 1 - sqrt(1 - h) to land on exactly h once stacked.
function exprLayerOpacity(face) {
return lines(exprCorners(face).concat([
'var area = 0;',
'for (var i = 0; i < 4; i++) { var j = (i + 1) % 4; area += Q[i][0] * Q[j][1] - Q[j][0] * Q[i][1]; }',
'var o = clamp(fx("Cube Opacity"), 0, 100);',
'var hidden = clamp(fx("Hidden Edge Opacity") / 100, 0, 1);',
'area > 0 ? o : (fx("Wireframe") > 0 ? o * (1 - Math.sqrt(1 - hidden)) : 0);'
]));
}
function exprFillColor(face) {
return lines(EXPR_HEAD.concat(EXPR_ROTATE, [
'var n = rot([' + face.n.join(", ") + ']);',
'var la = degreesToRadians(fx("Light Angle")), le = degreesToRadians(fx("Light Elevation"));',
'var L = [Math.sin(la) * Math.cos(le), -Math.sin(le), -Math.cos(la) * Math.cos(le)];',
'var lambert = Math.max(0, n[0] * L[0] + n[1] * L[1] + n[2] * L[2]);',
'var k = 1 - clamp(fx("Shading") / 100, 0, 1) * (1 - lambert);',
'var c = fx("Use Face Colors") > 0 ? fx("' + face.name + ' Color") : fx("Base Color");',
'[c[0] * k, c[1] * k, c[2] * k, 1];'
]));
}
var EXPR_FILL_OPACITY = lines(EXPR_HEAD.concat([
'fx("Wireframe") > 0 ? 0 : fx("Fill Opacity");'
]));
// A 1px stroke in the fill color hides the hairline gaps anti-aliasing
// leaves between adjacent faces. Only used when faces are fully opaque.
var EXPR_SEAM_COLOR = 'content("Face").content("Face Fill")("ADBE Vector Fill Color").value;';
var EXPR_SEAM_OPACITY = lines(EXPR_HEAD.concat([
'fx("Wireframe") > 0 || fx("Fill Opacity") < 100 ? 0 : 100;'
]));
var EXPR_STROKE_COLOR = lines(EXPR_HEAD.concat([
'fx("Stroke Color");'
]));
var EXPR_STROKE_WIDTH = lines(EXPR_HEAD.concat([
'var w = fx("Stroke Width");',
'fx("Wireframe") > 0 && w <= 0 ? 2 : w;'
]));
// ---------------------------------------------------------------- build
function addControl(layer, type, name, value) {
var effect = layer.property("ADBE Effect Parade").addProperty(CONTROL_MATCH_NAMES[type]);
effect.name = name;
effect.property(1).setValue(type === "color" ? [value[0], value[1], value[2], 1] : value);
}
// Re-fetch shape contents by name each time: adding a property invalidates
// ExtendScript references to its siblings.
function faceContents(layer) {
return layer.property("ADBE Root Vectors Group").property("Face").property("ADBE Vectors Group");
}
function buildFace(face) {
var layer = comp.layers.addShape();
layer.name = prefix + " - " + face.name;
var group = layer.property("ADBE Root Vectors Group").addProperty("ADBE Vector Group");
group.name = "Face";
// Stacking inside the group, top to bottom: path, edge stroke, seam stroke, fill.
faceContents(layer).addProperty("ADBE Vector Shape - Group").name = "Face Path";
faceContents(layer).addProperty("ADBE Vector Graphic - Stroke").name = "Edge Stroke";
faceContents(layer).addProperty("ADBE Vector Graphic - Stroke").name = "Seam Fix";
faceContents(layer).addProperty("ADBE Vector Graphic - Fill").name = "Face Fill";
var items = faceContents(layer);
items.property("Face Path").property("ADBE Vector Shape").expression = exprPath(face);
var edge = items.property("Edge Stroke");
edge.property("ADBE Vector Stroke Color").expression = EXPR_STROKE_COLOR;
edge.property("ADBE Vector Stroke Width").expression = EXPR_STROKE_WIDTH;
edge.property("ADBE Vector Stroke Line Cap").setValue(2); // round
edge.property("ADBE Vector Stroke Line Join").setValue(2); // round
var seam = items.property("Seam Fix");
seam.property("ADBE Vector Stroke Width").setValue(1);
seam.property("ADBE Vector Stroke Line Join").setValue(2);
seam.property("ADBE Vector Stroke Color").expression = EXPR_SEAM_COLOR;
seam.property("ADBE Vector Stroke Opacity").expression = EXPR_SEAM_OPACITY;
var fill = items.property("Face Fill");
fill.property("ADBE Vector Fill Color").expression = exprFillColor(face);
fill.property("ADBE Vector Fill Opacity").expression = EXPR_FILL_OPACITY;
layer.property("ADBE Transform Group").property("ADBE Opacity").expression = exprLayerOpacity(face);
return layer;
}
app.beginUndoGroup("Create " + SCRIPT_NAME);
try {
// Controller first so the face expressions never point at a missing layer.
var ctrl = comp.layers.addNull();
ctrl.name = ctrlName;
for (var c = 0; c < CONTROLS.length; c++) {
addControl(ctrl, CONTROLS[c][0], CONTROLS[c][1], CONTROLS[c][2]);
}
// Layer order between faces doesn't matter: only the front faces are
// visible and they never overlap.
for (var i = 0; i < FACES.length; i++) buildFace(FACES[i]);
ctrl.moveToBeginning();
var selected = comp.selectedLayers;
for (var s = 0; s < selected.length; s++) selected[s].selected = false;
ctrl.selected = true;
} catch (err) {
alert("Could not build the cube:\n" + err.toString() + (err.line ? " (line " + err.line + ")" : ""), SCRIPT_NAME);
} finally {
app.endUndoGroup();
}
})();