Copy link to clipboard
Copied
Ok... So just like any petulant, stubborn child.... I don't like being told what I can't do... And I've decided i'm no longer going to simply accept that we can't set our own custom corner radii when creating a roundedRectangle... Only being able to change "horizontalRadius" and "verticalRadius" isn't enough, because unless you use the same value for each, the corners will not really be round and the shape won't resemble a rounded rectangle, but rather a bloated and/or stretched rounded rectangle.
To that end... Here's a function i just threw together that lets you create a rectangle at whatever dimensions you want, and lets you pass in an array of "corner radii" listed clockwise from top left to bottom left. So, if you need a rectangle at [0,0] and 200pt wide and 400pt tall with rounded corners of the radii [20,10,25,50] for whatever reason, you could create one with this function call:
customRoundedRectangle(app.activeDocument, 0,0,200,400,[20,10,25,50]);
the first argument is the "parent" object. The container in which you want the rectangle to be created. This could be the document, a layer, a group, a compound path.. (though i think this will need some refinement to properly handle making something inside a clipping mask.. otherwise the pathfinder logic won't work properly, i suspect.. I'll get to it). The next 4 arguments are the standard rectangle arguments (top, left, width, height). Then the last argument is the array of corner radii, again sorted clockwise from top left.
Here's the code:
//Custom Rounded Rectangle Generator
//The built in roundedRectangle() method only allows for changing
//"verticalRadius" and "horizontalRadius".. But if you use different
//values for these, the corners are not truly round... they'll be asymptotic
//and the values given will apply to both left and right sides or top and bottom
//so you can't change the corners individually
.
//This customRoundedRectangle() function allows for each corner to have a separate
//corner radius that is truly rounded and maintains the integrity of the rectangle's existing side
//author: William Dowling
//email: illustrator.dev.pro@gmail.com
//github: https://www.github.com/wdjsdev
//this project on githuh: https://github.com/wdjsdev/public_illustrator_scripts/blob/master/custom_rounded_rectangle.js
//linkedin: https://www.linkedin.com/in/william-dowling-4537449a/
//Adobe Discussion Forum Post that initiated this: https://community.adobe.com/t5/illustrator-discussions/rounded-rectangle/td-p/13076613
//*******//
//Did you find this useful? Would you like to buy me a cup (or a pot) of coffee to say thanks?
//paypal.me/illustratordev
//<3
//Do you have some work to do, but you have more money than time/skill?
//Send me an email or a dm! I'll see what we can do to help each other..
//*******//
//arguments:
//parent: the parent container object inside which to create the rectangle
//y: y (top) coordinate of the rectangle (Number, in points)
//x: x (left) coordinate of the rectangle (Number, in points)
//w: width of the rectangle (Number, in points)
//h: height of the rectangle (Number, in points)
//r: array of radii clockwise from top left corner (array)
function customRoundedRectangle(parent, y, x, w, h, r) {
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
//function calls//
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
if (!app.documents.length) {
alert("Please open a document and try again.");
return;
}
function makeRect()
{
var doc = app.activeDocument;
var swatches = doc.swatches;
var blackSwatch;
try {
blackSwatch = swatches["Black"];
} catch (error) {
blackSwatch = swatches.add();
blackSwatch.name = "Black";
blackSwatch.color = new CMYKColor();
blackSwatch.color.black = 100;
}
//draw the base rectangle
var rect = parent.pathItems.rectangle(y, x, w, h);
rect.filled = true;
rect.fillColor = blackSwatch.color;
rect.stroked = false;
var rectData = { w: rect.width, h: rect.height, l: rect.left, t: rect.top, r: rect.left + rect.width, b: rect.top - rect.height };
var cp = parent.compoundPathItems.add();
//create an ellipse for each radius
r.forEach(function (radius, i) {
var rad = radius * 2;
var ellipse = cp.pathItems.ellipse(y, x, rad, rad);
ellipse.filled = true;
ellipse.fillColor = blackSwatch.color;
ellipse.stroked = false;
switch (i) {
case 1:
ellipse.left += rectData.w - rad;
break;
case 2:
ellipse.left += rectData.w - rad;
ellipse.top -= rectData.h - rad;
break;
case 3:
ellipse.top -= rectData.h - rad;
}
});
//load pathfinder actions into actions panel
createAction("pathfinder", getActionString());
doc.selection = null;
cp.selected = rect.selected = true;
app.doScript("divide", "pathfinder");
//look for the "corner"s of the rectangle...
//the part that's outside our desired radius
afc(doc.selection[0], "pageItems").forEach(function (item) {
var area = Math.abs(item.area);
var calcArea = item.width * item.height;
if (area < (calcArea * .6)) {
item.remove();
}
});
//all that's left should be the base rectangle and
//the ellipses for the corner radii. unite them all
//together into a single pathItem.
app.doScript("unite", "pathfinder");
//remove pathfinder actions from actions panel
removeAction("pathfinder");
return app.selection[0];
//end of script
}
///////////////////////////////////////////////////////////////////////////
//dependencies:
///////////////////////////////////////////////////////////////////////////
//array from container
//get all elements of the type "crit" inside the container
//and return as a standard javascript array
function afc(container, crit) {
var result = [];
var items;
if (!crit || crit === "any") {
items = container.pageItems;
} else {
items = container[crit];
}
for (var x = 0; x < items.length; x++) {
result.push(items[x])
}
return result;
}
//Array.forEach() prototype for easily looping arrays
//this helps eliminate scope issues because variables
//remain contained inside the function block. almost
//kind of imitating "let" in modern javascript.
//Plus, you don't need to worry about pre-declaring
//variables outside the loop to prevent mrap/parm errors.
//args:
//callback: the function to call for each element
//startPos: the index to start at (defaults to 0)
//inc: the increment to move by (defaults to 1)
Array.prototype.forEach = function (callback, startPos, inc) {
if (!inc) inc = 1;
if (!startPos) startPos = 0;
for (var i = startPos; i < this.length; i += inc)
callback(this[i], i, this);
};
//create and load a new action
function createAction(name, actionString) {
var documentsPath = "~/Documents/Adobe_Script_Helpers/";
var dest = Folder(documentsPath);
if (!dest.exists) {
dest.create();
}
var actionFile = new File(decodeURI(dest + "/" + name + ".aia"));
actionFile.open("w");
actionFile.write(actionString);
actionFile.close();
//load the action
app.loadAction(actionFile);
}
//remove all instances of an action with a given name
function removeAction(actionName) {
var localValid = true;
while (localValid) {
try {
app.unloadAction(actionName, "");
}
catch (e) {
localValid = false;
}
}
}
function getActionString() {
//pathfinder action string. this will create an
//action set containing all of the pathfinder options.
//this is much more reliable than app.executeMenuCommand()
//which uses "live pathfinder" effects which don't always work
//and need to be expanded.
var pathfinderActionString = [
"/version 3",
"/name [ 10",
" 7061746866696e646572",
"]",
"/isOpen 1",
"/actionCount 10",
"/action-1 {",
" /name [ 5",
" 756e697465",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 0",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 3",
" 416464",
" ]",
" /value 0",
" }",
" }",
"}",
"/action-2 {",
" /name [ 11",
" 6d696e75735f66726f6e74",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 0",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 8",
" 5375627472616374",
" ]",
" /value 3",
" }",
" }",
"}",
"/action-3 {",
" /name [ 9",
" 696e74657273656374",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 0",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 9",
" 496e74657273656374",
" ]",
" /value 1",
" }",
" }",
"}",
"/action-4 {",
" /name [ 7",
" 6578636c756465",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 1",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 7",
" 4578636c756465",
" ]",
" /value 2",
" }",
" }",
"}",
"/action-5 {",
" /name [ 6",
" 646976696465",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 1",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 6",
" 446976696465",
" ]",
" /value 5",
" }",
" }",
"}",
"/action-6 {",
" /name [ 4",
" 7472696d",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 1",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 4",
" 5472696d",
" ]",
" /value 7",
" }",
" }",
"}",
"/action-7 {",
" /name [ 5",
" 6d65726765",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 1",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 5",
" 4d65726765",
" ]",
" /value 8",
" }",
" }",
"}",
"/action-8 {",
" /name [ 4",
" 63726f70",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 1",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 4",
" 43726f70",
" ]",
" /value 9",
" }",
" }",
"}",
"/action-9 {",
" /name [ 7",
" 6f75746c696e65",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 1",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 7",
" 4f75746c696e65",
" ]",
" /value 6",
" }",
" }",
"}",
"/action-10 {",
" /name [ 10",
" 6d696e75735f6261636b",
" ]",
" /keyIndex 0",
" /colorIndex 0",
" /isOpen 1",
" /eventCount 1",
" /event-1 {",
" /useRulersIn1stQuadrant 0",
" /internalName (ai_plugin_pathfinder)",
" /localizedName [ 10",
" 5061746866696e646572",
" ]",
" /isOpen 0",
" /isOn 1",
" /hasDialog 0",
" /parameterCount 1",
" /parameter-1 {",
" /key 1851878757",
" /showInPalette 4294967295",
" /type (enumerated)",
" /name [ 10",
" 4d696e7573204261636b",
" ]",
" /value 4",
" }",
" }",
"}"
].join("\n");
return pathfinderActionString;
}
//this is the function call that actually
//does the work. I wrapped the logic in a function
//to avoid variable scope issues and accessing
//Array.forEach before it was defined.
//But i still want the function logic to be at the
//top of the file so it's easier to read and
//the dependencies at the bottom and out of the way.
makeRect();
}
//sample function call:
// customRoundedRectangle(app.activeDocument, 0, 0, 200, 400, [20, 10, 25, 50]);
Copy link to clipboard
Copied
Hi William, thanks for sharing!
I've got an error though, r.forEach is not a function on line 67. Moving the function definition above the function call takes care of the error. I don't remember having to do that before. Can you restart illustrator and give it another try?
Copy link to clipboard
Copied
hmmm. strange..? I thought javascript treated functions as priority and "hoists" them to the top when the interpreter runs regardless of where it is in the code..
I've never had that problem before... (definitely had it with variables.. but not functions..) perhaps prototypes work differently than regular functions? I'll reorganize it and update the post and the github page.
Thanks for letting me know. š
Copy link to clipboard
Copied
Ok. I wrapped up the main logic in a function and then placed that function call at the bottom to ensure everything is declared and defined before the logic runs.
I wanted to keep the main logic at the top for readability, and then just kind of bury the dependencies below. In my own workflow, all those dependencies exist in a master "utilities container" that i include in every script, so these kinds of scope issues are never a problem because all this logic simply gets added to the beginning of every script. But unfortunately that's not a super portable solution, so I combined everything so this function would be self sufficient. And now i learned something new. haha. or relearned something i learned before and forgot becasue it didn't affect me.
I appreciate you, Carlos.
Copy link to clipboard
Copied
yeah that's what I thought, that functions could be anywhere but I guess not. I think functions inside functions don't follow that rule?
now we know I think š
Copy link to clipboard
Copied
Hmm. ok. Some quick google fu and a bit of reading has properly edified me. functions and variables are hoisted at runtime... expressions and properties are not.
I guess technically Array.prototype.forEach = function(){//do thing}; is a PROPERTY of the array object, even though it looks/acts like a function.
If i had instead just created a standalone function like this: function forEach(array,myFunc){for(items in array){myFunc(cur item from array)}} then it would be properly hoisted and preclude this type of error. But personally i prefer the prototype and i have no issue using wrapper functions to avoid these situations. Plus, that's an extra argument i gotta type out. who's got time for that?! š (the answer to this question? github copilot... wow.)
I have several other improvements to this on the way as well. default values available so that every argument is actually optional. argument sanitization to ensure no runtime errors when the main logic runs. a temporary sandbox layer to use for creating the shapes and doing all the pathfinder stuff just to ensure we don't accidentally mess with any existing artwork. plus a tiny testing framework for experimenting with any possible arguments.
stay tuned
Copy link to clipboard
Copied
I think the problem was that forEach is a not global function, but a property of the prototype object. And like any property, it will not work before it is defined. That is why polyfills are best put at the top.
var object1 = {};
object1.function1 = function () {alert("");};
object1.function1(); // will work
var object2 = {};
object2.function1(); // will not work
object2.function1 = function () {alert("");};
Copy link to clipboard
Copied
exactly right. prototype functions are properties and not functions as i assumed. But i think your example is a better demonstration of the fact that expressions aren't hoisted. writing functions like you've done here makes them technically expressions instead of properties or functions.
I intentionally wanted to bury the dependencies so that when you open the code, the meaningful code is right there at the top. I know i find it frustrating when I open up some code i didn't write and i have to search around to figure out where the actual logic occurs.
Using a simple wrapper function for the main logic and putting the function call at the end, after all the dependencies, did the trick nicely and i still get my way in terms of having the main logic front and center.
Copy link to clipboard
Copied
They are assignment expressions or expression statements. But I know what you mean. The only things that gets hoisted is a declaration statement. I.e.
var a;
function b() {};
Copy link to clipboard
Copied
Hi @DilliamWowling, just saw this. Thanks for sharing! Great script.
- Mark
Copy link to clipboard
Copied