/**********************************************************************************************
Copyright (c) 2017 Tomas Šinkūnas. All rights reserved.
Morpth any shape into a circle.
- This script is provided "as is," without warranty of any kind, expressed
- or implied. In no event shall the author be held liable for any damages
- arising in any way from the use of this script.
+ - Center of the circle is calculated from shapes bounding box.
+ - Adds more radius selection options: "Bounding Box Height" and "Bounding Box Width"
+ Released as open-source under the MIT license:
+ Copyright (c) 2017 Tomas Šinkūnas www.renderTom.com
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ software and associated documentation files (the "Software"), to deal in the Software
+ without restriction, including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+ to whom the Software is furnished to do so, subject to the following conditions:
+ The above copyright notice and this permission notice shall be included in all copies or
+ substantial portions of the Software.
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
**********************************************************************************************/
var etCustomRadius = grpCustomRadius.add("edittext", undefined, "200");
etCustomRadius.alignment = ["fill", "top"];
- var ddRadius = win.add("dropdownlist", undefined, ["Average from center", "Maximum from center", "Minimum from center"])
+ var ddRadius = win.add("dropdownlist", undefined, ["Average from Center", "Bounding Box Height", "Bounding Box Width", "Maximum from Center", "Minimum from Center"]);
var button = win.add("button", undefined, "Do It!");
main(ddRadius.selection.text);
win.onShow = function() {
checkCustomRadius.onClick();
win.onResizing = win.onResize = function() {
var selectedLayers = getSelectedLayers(composition);
var allShapeProperties = getAllShapeProperties(selectedLayers);
if (allShapeProperties.length === 0)
- throw new Error ("Please select Path property")
+ throw new Error("Please select Path property.");
+ var pathValue, circleShape,
+ centerCoordinates = [],
app.beginUndoGroup("Shape to Circle");
- for (var j = 0, jl = allShapeProperties.length; j < jl; j ++) {
- var pathValue = allShapeProperties[j].value;
- var vertices = pathValue.vertices;
- var centerCoordinates = getAverageInArray(vertices);
+ for (var j = 0, jl = allShapeProperties.length; j < jl; j++) {
+ pathValue = allShapeProperties[j].value;
+ boundingBox = getBoundingBox(pathValue);
+ centerCoordinates = averageInArray(boundingBox);
if (isNaN(radiusFromUI)) {
- distanceArray = calculateDistancesFromCenter(centerCoordinates, vertices);
- if (radiusFromUI.match("Average")) radius = getAverageInArray(distanceArray);
+ distanceArray = getDistanceFromCenter(centerCoordinates, pathValue.vertices);
+ if (radiusFromUI.match("Average")) radius = averageInArray(distanceArray);
+ else if (radiusFromUI.match("Width")) radius = (boundingBox[1][0] - boundingBox[0][0]) / 2;
+ else if (radiusFromUI.match("Height")) radius = (boundingBox[1][1] - boundingBox[0][1]) / 2;
else if (radiusFromUI.match("Maximum")) radius = Math.max.apply(null, distanceArray);
else if (radiusFromUI.match("Minimum")) radius = Math.min.apply(null, distanceArray);
- var circlePoints = pointsToCircle(centerCoordinates, radius, vertices);
+ circlePoints = pointsToCircle(centerCoordinates, radius, pathValue);
- var circleShape = new Shape();
- circleShape.vertices = circlePoints.vertices;
- circleShape.inTangents = circlePoints.inTangents;
- circleShape.outTangents = circlePoints.outTangents;
- circleShape.closed = pathValue.closed;
+ circleShape = new Shape();
+ circleShape.vertices = circlePoints.vertices;
+ circleShape.inTangents = circlePoints.inTangents;
+ circleShape.outTangents = circlePoints.outTangents;
+ circleShape.closed = pathValue.closed;
allShapeProperties[j].numKeys === 0
? allShapeProperties[j].setValue(circleShape)
: allShapeProperties[j].setValueAtTime(composition.time, circleShape);
+// -----------------------------------------------
function getActiveComposition() {
var composition = app.project.activeItem;
if (!composition || !(composition instanceof CompItem))
throw new Error("Please select composition first.");
- function getSelectedLayers(composition) {
- var selectedLayers = composition.selectedLayers;
- if (selectedLayers.length === 0)
- throw new Error("Please select layer first.");
+ function getAllShapeProperties(selectedLayers) {
+ var allShapeProperties = [];
+ var layerShapeProperties = [];
+ for (var i = 0, il = selectedLayers.length; i < il; i++) {
+ layerShapeProperties = getLayerShapeProperties(selectedLayers[i]);
+ if (layerShapeProperties.length > 0) {
+ allShapeProperties = allShapeProperties.concat(layerShapeProperties);
+ return allShapeProperties;
function getLayerShapeProperties(layer) {
var shapeProperties = [];
var selectedProperties = layer.selectedProperties;
- for (var i = 0, il = selectedProperties.length; i < il; i ++) {
+ for (var i = 0, il = selectedProperties.length; i < il; i++) {
prop = selectedProperties[i];
if (prop.matchName === "ADBE Vector Shape - Group") {
if (prop.property("ADBE Vector Shape").selected === true) {
- function getAllShapeProperties(selectedLayers) {
- var allShapeProperties = [];
- var layerShapeProperties = [];
- for (var i = 0, il = selectedLayers.length; i < il; i ++) {
- layerShapeProperties = getLayerShapeProperties(selectedLayers[i]);
- if (layerShapeProperties.length > 0) {
- allShapeProperties = allShapeProperties.concat(layerShapeProperties);
+ function getSelectedLayers(composition) {
+ var selectedLayers = composition.selectedLayers;
+ if (selectedLayers.length === 0)
+ throw new Error("Please select layer first.");
+// -----------------------------------------------
+ function averageInArray(verticesArray) {
+ return sumArray(verticesArray) / verticesArray.length;
+ function clockwiseDirection(vertices) {
+ // http://stackoverflow.com/questions/14505565/detect-if-a-set-of-points-in-an-array-that-are-the-vertices-of-a-complex-polygon
+ var polygonArea = getPoligonArea(vertices);
+ return polygonArea > 0;
+ function distanceBetweenPoints(point1, point2) {
+ var deltaX = point1[0] - point2[0];
+ var deltaY = point1[1] - point2[1];
+ var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
+ function getAngle(point1, point2) {
+ var distanceX = point2[0] - point1[0];
+ var distanceY = point2[1] - point1[1];
+ var theta = Math.atan2(distanceY, distanceX);
+ function getBoundingBox(pathValue) {
+ var bezierPoints = toBezierPoints(pathValue),
+ segmentBoundingBox = {};
+ for (var i = 0, il = bezierPoints.length; i < il; i++) {
+ segmentBoundingBox = getBoundsOfCurve(bezierPoints[i][0], bezierPoints[i][1], bezierPoints[i][2], bezierPoints[i][3]);
+ xValues.push(segmentBoundingBox.left);
+ xValues.push(segmentBoundingBox.right);
+ yValues.push(segmentBoundingBox.top);
+ yValues.push(segmentBoundingBox.bottom);
+ [Math.min.apply(null, xValues), Math.min.apply(null, yValues)],
+ [Math.max.apply(null, xValues), Math.max.apply(null, yValues)]
+ function getBoundsOfCurve(p1, p2, p3, p4) {
+ // http://stackoverflow.com/questions/2587751/an-algorithm-to-find-bounding-box-of-closed-bezier-curves
+ for (var i = 0; i < 2; ++i) {
+ b = 6 * p1[0] - 12 * p2[0] + 6 * p3[0];
+ a = -3 * p1[0] + 9 * p2[0] - 9 * p3[0] + 3 * p4[0];
+ c = 3 * p2[0] - 3 * p1[0];
+ b = 6 * p1[1] - 12 * p2[1] + 6 * p3[1];
+ a = -3 * p1[1] + 9 * p2[1] - 9 * p3[1] + 3 * p4[1];
+ c = 3 * p2[1] - 3 * p1[1];
+ if (Math.abs(a) < 1e-12) { // Numerical robustness
+ if (Math.abs(b) < 1e-12) continue; // Numerical robustness
+ if (0 < t && t < 1) tvalues.push(t);
+ // Solve Quadratic Equation
+ roots = quadraticEquation(a, b, c);
+ if (0 < roots.root1 && roots.root1 < 1) tvalues.push(roots.root1);
+ if (0 < roots.root2 && roots.root2 < 1) tvalues.push(roots.root2);
- return allShapeProperties;
+ var u, j = tvalues.length;
+ xvalues[j] = (u * u * u * p1[0]) + (3 * u * u * t * p2[0]) + (3 * u * t * t * p3[0]) + (t * t * t * p4[0]);
+ yvalues[j] = (u * u * u * p1[1]) + (3 * u * u * t * p2[1]) + (3 * u * t * t * p3[1]) + (t * t * t * p4[1]);
+ xvalues.push(p1[0], p4[0]);
+ yvalues.push(p1[1], p4[1]);
+ left: Math.min.apply(null, xvalues),
+ top: Math.min.apply(null, yvalues),
+ right: Math.max.apply(null, xvalues),
+ bottom: Math.max.apply(null, yvalues),
+ function getDistanceFromCenter(center, vertices) {
+ var distanceFromCenter;
+ var distanceArray = [];
+ for (var i = 0, il = vertices.length; i < il; i++) {
+ distanceFromCenter = distanceBetweenPoints(center, vertices[i]);
+ distanceArray.push(distanceFromCenter);
+ function getHandleLength(numPoints, radius) {
+ return radius * getMagicNumber() * 4 / numPoints;
+ function getMagicNumber() {
+ // http://stackoverflow.com/questions/1734745/how-to-create-circle-with-bézier-curves
+ // https://people-mozilla.org/~jmuizelaar/Riskus354.pdf
+ return (4 / 3) * Math.tan(Math.PI / (2 * 4));
- function pointsToCircle(center, radius, pointsArray) {
- var angle, anchor, handle;
+ function getPointCoordinates(center, radius, angle) {
+ center[0] + radius * Math.cos(angle),
+ center[1] + radius * Math.sin(angle)
- var numPoints = pointsArray.length;
+ function getPoligonArea(vertices) {
+ for (var i = 0, il = vertices.length; i < il; i++) {
+ j = (i + 1) % vertices.length;
+ area += vertices[i][0] * vertices[j][1];
+ area -= vertices[j][0] * vertices[i][1];
+ function pointsToCircle(center, radius, pathValue) {
+ var angle, anchor, handle;
+ var vertices = pathValue.vertices;
+ var numPoints = vertices.length;
var slice = 2 * Math.PI / numPoints;
var handleLength = getHandleLength(numPoints, radius);
- var angleOffset = getAngle(center, pointsArray[0]);
+ var angleOffset = getAngle(center, vertices[0]);
- if (clockwiseDirection(pointsArray)) {
+ if (clockwiseDirection(vertices)) {
for (var i = 0; i < numPoints; i++) {
angle = slice * i + angleOffset;
newCoordinates.inTangents.push(handle - anchor);
newCoordinates.outTangents.push(anchor - handle);
- function getPointCoordinates(center, radius, angle) {
- center[0] + radius * Math.cos(angle),
- center[1] + radius * Math.sin(angle)
+ // Add additional point to close the circle
+ if (!pathValue.closed) {
+ newCoordinates.vertices.push(newCoordinates.vertices[0]);
+ newCoordinates.inTangents.push(newCoordinates.inTangents[0]);
+ newCoordinates.outTangents.push(newCoordinates.outTangents[0]);
- function getHandleLength(numPoints, radius) {
- return radius * getMagicNumber() * 4 / numPoints;
- function getMagicNumber() {
- // http://stackoverflow.com/questions/1734745/how-to-create-circle-with-bézier-curves
- // https://people-mozilla.org/~jmuizelaar/Riskus354.pdf
- return (4 / 3) * Math.tan(Math.PI / (2 * 4));
+ function quadraticEquation(a, b, c) {
+ var D = b * b - 4 * a * c; // D is the discriminant of the quadratic equation
+ if (D < 0) return null; // If the discriminant is negative, then there are no real roots
+ var sqrtD = Math.sqrt(D); // If the discriminant is positive, then there are two distinct roots
- function getAverageInArray(verticesArray) {
- return sumArray(verticesArray) / verticesArray.length;
+ root1: (-b + sqrtD) / (2 * a),
+ root2: (-b - sqrtD) / (2 * a)
function sumArray(array) {
for (var i = 1, il = array.length; i < il; i++)
- function clockwiseDirection(vertices) {
- // http://stackoverflow.com/questions/14505565/detect-if-a-set-of-points-in-an-array-that-are-the-vertices-of-a-complex-polygon
- var polygonArea = getPoligonArea(vertices);
- return polygonArea > 0;
+ function toBezierPoints(pathValue) {
- function getPoligonArea(vertices) {
- for (var i = 0; i < vertices.length; i++) {
- j = (i + 1) % vertices.length;
- area += vertices[i][0] * vertices[j][1];
- area -= vertices[j][0] * vertices[i][1];
+ for (var i = 0, il = pathValue.vertices.length - 1; i < il; i++) {
+ p1 = pathValue.vertices[i];
+ p2 = pathValue.vertices[i] + pathValue.outTangents[i];
+ p3 = pathValue.inTangents[i + 1] + pathValue.vertices[i + 1]
+ p4 = pathValue.vertices[i + 1];
+ valuesArray.push([p1, p2, p3, p4]);
- function getAngle(point1, point2) {
- var distanceX = point2[0] - point1[0];
- var distanceY = point2[1] - point1[1];
- var theta = Math.atan2(distanceY, distanceX);
+ if (pathValue.closed) {
+ p1 = pathValue.vertices[pathValue.vertices.length - 1];
+ p2 = pathValue.vertices[pathValue.vertices.length - 1] + pathValue.outTangents[pathValue.outTangents.length - 1];
+ p3 = pathValue.inTangents[0] + pathValue.vertices[0]
+ p4 = pathValue.vertices[0];
- function calculateDistancesFromCenter(center, vertices) {
- var distanceFromCenter;
- var distanceArray = [];
- for (var i = 0, il = vertices.length; i < il; i++) {
- distanceFromCenter = distanceBetweenPoints(center, vertices[i]);
- distanceArray.push(distanceFromCenter);
+ valuesArray.push([p1, p2, p3, p4]);
- function distanceBetweenPoints(point1, point2) {
- var deltaX = point1[0] - point2[0];
- var deltaY = point1[1] - point2[1];
- var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);