Circular Script for Adobe Illustrator
Description
The circular.jsx
script is designed for Adobe Illustrator CC+ and was created by Alexander Ladygin. This script allows users to quickly and easily arrange selected objects in a circular pattern. It provides a variety of settings to customize the rotation, placement, and duplication of objects, enabling users to achieve the desired circular arrangement.

Author Information
- Author: Alexander Ladygin
- Email: i@ladygin.pro
- Website: www.ladyginpro.ru
Features
- Circular Arrangement:
- Arrange objects in a circular pattern.
- Specify the start and end angles for placement.
- Rotate each object by a given angle or randomly.
- Set the offset from the center.
- Direction and Position:
- Choose the direction for arrangement (Top, Right, Bottom, Left).
- Specify the position of elements (Absolute or Relative).
- Consider visible or geometric boundaries for placement.
- Advanced Options:
- Enable counterclockwise arrangement.
- Reset rotation for items to preserve their original state.
- Create copies of elements in a circular pattern.
- Customization:
- Set minimum and maximum values for random offset.
- Enable or disable the creation of copies.
- Extensive functionality to flexibly configure rotation and placement parameters.

Usage
- Prerequisites:
- Ensure you have Adobe Illustrator CC+ installed.
- Select the objects you want to arrange in a circular pattern.
- Running the Script:
- Open the
circular.jsx
script in Adobe Illustrator. - The script will check if there are any objects selected.
- If objects are selected, the GUI will appear.
- GUI Components:
- Start Angle: Specify the angle from which the placement of objects will begin.
- End Angle: Specify the angle at which the placement of objects will end.
- Rotate Each Items: Rotate each object by a given angle.
- Rotate Each Items Random: Rotate each object randomly.
- Offset: Set the indent from the center.
- Direction: Choose the direction for arrangement (Top, Right, Bottom, Left).
- Random Offset: Enable random offset from the center and specify minimum and maximum values.
- Position: Choose the position of elements (Absolute or Relative).
- Bounds: Consider visible or geometric boundaries for placement.
- Counter Clockwise: Enable counterclockwise arrangement.
- Reset Rotate from Item: Arrange in a circle but preserve the original state of the object.
- Copies Enabled: Enable copying of the element by the entered quantity.
- Buttons:
- Preview: Enable or disable preview.
- Cancel: Close the GUI without applying changes.
- OK: Apply the circular arrangement with specified parameters.
- Settings:
- The script saves settings to a JSON file in the user’s documents folder.
- Settings are loaded automatically when the script is run.
Additional Information
- If no objects are selected when the script is run, an alert will prompt the user to select objects.
- The script includes helper functions for handling input events, unit conversions, and random value generation.
Notes
- If you enable the “copies enabled” option and select many items with a high number of copies, it may heavily load your device.
Contact
For any questions or support, you can contact the author at i@ladygin.pro or visit www.ladyginpro.ru.
JavaScript
/*
Author: Alexander Ladygin (i@ladygin.pro)
Program version: Adobe Illustrator CC+ (presumably in Adobe Illustrator CS6 - did not test)
Name: circular.jsx;
Copyright (c) 2018
www.ladyginpro.ru
*/
$.errorMessage = function (err) {alert(err + '\n' + err.line);};
Object.prototype.extend = function (userObject, deep) {try {for (var key in userObject) {if (this.hasOwnProperty(key)) {if (deep&& this[key] instanceof Object&& !(this[key] instanceof Array)&& userObject[key] instanceof Object&& !(userObject[key] instanceof Array)) {this[key].extend(userObject[key], deep);}else this[key] = userObject[key];}}return this;}catch (e) {$.errorMessage('$.objectParser() - error: ' + e);}};
$.getBounds = function (arr, bounds) {bounds = bounds || 'geometricBounds';bounds = (bounds && bounds.toLowerCase().indexOf('bounds') === -1) ? bounds += 'Bounds' : bounds;var x = [], y = [], w = [], h = [];for (var i = 0; i < arr.length; i++) {x.push(arr[i][bounds][0]);y.push(arr[i][bounds][1]);w.push(arr[i][bounds][2]);h.push(arr[i][bounds][3]);};return [Math.min.apply(null, x), Math.max.apply(null, y), Math.max.apply(null, w), Math.min.apply(null, h)];};
$.toArr = function (classCollection, callback) {var arr = [], l = classCollection.length;if (l > 0) {for (var i = 0; i < l; i++) {arr.push(classCollection[i]);}}if (callback instanceof Function) return callback(arr);return arr;}
$.getUnits = function (val, def) {return 'px,pt,mm,cm,in,pc'.indexOf(val.slice(-2)) > -1 ? val.slice(-2) : def;};
$.convertUnits = function (obj, b) {if (obj === undefined) {return obj;}if (b === undefined) {b = 'px';}if (typeof obj === 'number') {obj = obj + 'px';}if (typeof obj === 'string') {var unit = $.getUnits(obj),val = parseFloat(obj);if (unit && !isNaN(val)) {obj = val;}else if (!isNaN(val)) {obj = val; unit = 'px';}}if (((unit === 'px') || (unit === 'pt')) && (b === 'mm')) {obj = parseFloat(obj) / 2.83464566929134;}else if (((unit === 'px') || (unit === 'pt')) && (b === 'cm')) {obj = parseFloat(obj) / (2.83464566929134 * 10);}else if (((unit === 'px') || (unit === 'pt')) && (b === 'in')) {obj = parseFloat(obj) / 72;}else if ((unit === 'mm') && ((b === 'px') || (b === 'pt'))) {obj = parseFloat(obj) * 2.83464566929134;}else if ((unit === 'mm') && (b === 'cm')) {obj = parseFloat(obj) * 10;}else if ((unit === 'mm') && (b === 'in')) {obj = parseFloat(obj) / 25.4;}else if ((unit === 'cm') && ((b === 'px') || (b === 'pt'))) {obj = parseFloat(obj) * 2.83464566929134 * 10;}else if ((unit === 'cm') && (b === 'mm')) {obj = parseFloat(obj) / 10;}else if ((unit === 'cm') && (b === 'in')) {obj = parseFloat(obj) * 2.54;}else if ((unit === 'in') && ((b === 'px') || (b === 'pt'))) {obj = parseFloat(obj) * 72;}else if ((unit === 'in') && (b === 'mm')) {obj = parseFloat(obj) * 25.4;}else if ((unit === 'in') && (b === 'cm')) {obj = parseFloat(obj) * 25.4;}return parseFloat(obj);};
var isUndo = false,
scriptName = 'Circular',
copyright = ' \u00A9 www.ladyginpro.ru',
settingFile = {
name: scriptName + '__setting.json',
folder: Folder.myDocuments + '/LA_AI_Scripts/'
},
$items = selection;
function ungroup (group) {
var i = group.pageItems.length;
if (i > 0) while (i--) group.pageItems[i].moveBefore(group);
group.remove();
}
function rvbn (min, max) {
// random value between numbers
return min + Math.floor(Math.random() * (max - min));
}
function circular (userOptions) {
var options = {
copies: 7,
offset: 200,
offsetRandom: false,
offsetMin: '0 px',
offsetMax: '0 px',
angleStart: 0,
angleEnd: 360,
notCopies: true,
resetItem: true,
bounds: 'visible',
direction: 'down',
isRotateItem: false,
rotateItemAngle: 45,
position: 'relative',
counterclockwise: false,
}.extend(userOptions || {});
var CCW = !options.counterclockwise ? -1 : 1,
TFP = options.direction.toUpperCase().replace('UP', 'TOP').replace('DOWN', 'BOTTOM');
options.bounds = (options.bounds.slice(0,1).toLowerCase() === 'v' ? 'visibleBounds' : 'geometricBounds');
options.direction = options.direction.toLowerCase().slice(0,1);
options.copies = (options.copies > 0 ? options.copies : 1);
options.offset = $.convertUnits(options.offset, 'px');
options.offsetMin = $.convertUnits(options.offsetMin, 'px');
options.offsetMax = $.convertUnits(options.offsetMax, 'px');
var sbnds = $.getBounds($items, options.bounds),
cpos = {
x: sbnds[0] + (sbnds[2] - sbnds[0]) / 2,
y: sbnds[1] - (sbnds[1] - sbnds[3]) / 2,
};
if (options.notCopies) {
var items = $items, l = items.length,
aVal = (options.angleEnd - options.angleStart) / (l > 1 ? (options.angleEnd >= 350 ? l : l - 1) : l);
// aVal = (options.angleEnd - options.angleStart) / (l - 1 > 0 ? l - 1 : l);
for (var i = 0; i < l; i++) {
toRotate(items[i], options.angleStart + aVal * i, cpos);
}
}
else {
var items = $items, l = items.length,
aVal = (options.angleEnd - options.angleStart) / options.copies;
for (var i = 0; i < l; i++) {
for (var j = 0; j < options.copies; j++) {
toRotate(items[i], options.angleStart + aVal * j);
}
// items[i].remove();
}
}
function toRotate (item, angle, $pos) {
angle *= CCW;
var $offset = (options.offsetRandom ? rvbn(options.offsetMin, options.offsetMax) : options.offset),
bnds = item[options.bounds],
$w = bnds[2] - bnds[0],
$h = bnds[1] - bnds[3];
if (options.position.toLowerCase().slice(0,1) === 'r') $offset += ($w >= $h ? $w : $h);
// align center
if (options.notCopies) {
item.position = [$pos.x - $w / 2, $pos.y + $h / 2];
bnds = item[options.bounds];
}
var centerPointSize = 0,
$group = item.parent.groupItems.add(),
node = (options.notCopies ? item : item.duplicate($group, ElementPlacement.INSIDE)),
centerPoint = $group.pathItems.ellipse(bnds[1] - $h / 2 + centerPointSize / 2, bnds[0] + $w / 2 - centerPointSize / 2, centerPointSize, centerPointSize);
// move items
$group.moveBefore(item);
if (options.notCopies) node.moveToBeginning($group);
// set $offset
// node.position = [node.position[0], node.position[1] - $offset];
if (options.direction === 'u') node.top -= $offset;
else if (options.direction === 'r') node.left -= $offset;
else if (options.direction === 'd') node.top += $offset;
else if (options.direction === 'l') node.left += $offset;
// rotate item
$group.rotate(angle, true, true, true, true, Transformation[TFP]);
// reset rotate from the item
if (options.resetItem) node.rotate(angle * -1, true, true, true, true, Transformation.CENTER);
// rotate only item
if (options.isRotateItem) node.rotate((__randomRotate.value ? Math.floor(Math.random() * 360) : options.rotateItemAngle * CCW), true, true, true, true, Transformation.CENTER);
centerPoint.remove();
ungroup($group);
return item;
}
}
function inputNumberEvents (ev, _input, min, max, callback){
var step,
_dir = (ev.keyName ? ev.keyName.toLowerCase().slice(0,1) : '#none#'),
_value = parseFloat(_input.text),
units = (',px,pt,mm,cm,in,'.indexOf(_input.text.length > 2 ? (',' + _input.text.replace(/ /g, '').slice(-2) + ',') : ',!,') > -1 ? _input.text.replace(/ /g, '').slice(-2) : '');
min = (min === undefined ? 0 : min);
max = (max === undefined ? Infinity : max);
step = (ev.shiftKey ? 10 : (ev.ctrlKey ? .1 : 1));
if (isNaN(_value)) {
_input.text = min;
}
else {
_value = ( ((_dir === 'u') || (_dir === 'r')) ? _value + step : (((_dir === 'd') || (_dir === 'l')) ? _value - step : false) );
if (_value !== false) {
_value = (_value <= min ? min : (_value >= max ? max : _value))
_input.text = _value;
if (callback instanceof Function) callback(_value, _input, min, max, units);
else if (units) _input.text = parseFloat(_input.text) + ' ' + units;
}
else if (units) _input.text = parseFloat(_input.text) + ' ' + units;
}
}
function normalizeAngle (val, item, min, max) {
if (item === __startAngle) {
if (val > parseFloat(__endAngle.text)) { __endAngleSlider.value = __endAngle.text = ((val + 1) >= max ? max : val + 1); }
}
else if (item === __endAngle) {
if (val < parseFloat(__startAngle.text)) { __startAngleSlider.value = __startAngle.text = ((val - 1) <= min ? min - 1 : val - 1); }
}
}
function normalizeOffset (val, item, min, max, units) {
if (item === __offsetMin) {
if (val > $.convertUnits(__offsetMax.text, 'px')) {
__offsetMax.text = ((val + 1) >= max ? max : val + 1) + (units ? ' ' + units : '');
}
}
else if (item === __offsetMax) {
if (val < $.convertUnits(__offsetMin.text, 'px')) {
__offsetMin.text = ((val - 1) <= min ? min - 1 : val - 1) + (units ? ' ' + units : '');
}
}
if (units) item.text += ' ' + units;
}
var win = new Window('dialog', scriptName + copyright);
win.alignChildren = 'fill';
with (panel = win.add('panel')) {
alignChildren = ['fill', 'bottom'];
with (add('group')) {
orientation = 'row';
add('statictext', [0, 0, 80, 25], 'Start angle:').justify = 'center';
var __startAngleSlider = add('slider', [0, 0, 170, 15], 0, 0, 359),
__startAngle = add('edittext', [0, 0, 50, 25], 0);
__startAngleSlider.onChanging = function (e) { __startAngle.text = Math.round(this.value); normalizeAngle(Math.round(this.value), __startAngle, 1, 360); }
__startAngleSlider.onChange = function (e) { previewStart(); }
__startAngle.addEventListener('keydown', function (e) { inputNumberEvents(e, this, 0, 359); __startAngleSlider.value = Math.round(this.text); });
__startAngle.addEventListener('keyup', function (e) { normalizeAngle(this.value, __startAngle, 1, 360); previewStart(); });
}
with (add('group')) {
orientation = 'row';
var __isEndAngle = add('checkbox', [0, 0, 80, 25], 'End angle:'),
__endAngleSlider = add('slider', [0, 0, 170, 15], 360, 1, 360),
__endAngle = add('edittext', [0, 0, 50, 25], 360);
__isEndAngle.alignment = 'bottom';
__endAngleSlider.onChanging = function (e) { __endAngle.text = Math.round(this.value); normalizeAngle(Math.round(this.value), __endAngle, 1, 360); }
__endAngleSlider.onChange = function (e) { previewStart(); }
__endAngle.addEventListener('keydown', function (e) { inputNumberEvents(e, this, 1, 360); __endAngleSlider.value = Math.round(this.text); });
__endAngle.addEventListener('keyup', function (e) { normalizeAngle(this.value, __endAngle, 1, 360); previewStart(); });
__isEndAngle.onClick = function () { __endAngleSlider.enabled = __endAngle.enabled = this.value; previewStart(); }
__endAngleSlider.enabled = __endAngle.enabled = false;
}
with (add('group')) {
orientation = 'row';
var __isRotateItem = add('checkbox', undefined, 'Rotate each items'),
__randomRotate = add('checkbox', undefined, 'Rotate each items Random');
__isRotateItem.onClick = function () {
__randomRotate.enabled = this.value;
__rotateItemSlider.enabled = __rotateItemAngle.enabled = (this.value && __randomRotate.value ? false : this.value);
previewStart();
}
__randomRotate.onClick = function () { __rotateItemSlider.enabled = __rotateItemAngle.enabled = !this.value; previewStart(); }
}
with (add('group')) {
var __rotateItemSlider = add('slider', [0, 0, 260, 15], 360, 0, 360),
__rotateItemAngle = add('edittext', [0, 0, 50, 25], 360);
__rotateItemSlider.onChanging = function (e) { __rotateItemAngle.text = Math.round(this.value); normalizeAngle(Math.round(this.value), __rotateItemAngle, 0, 360); }
__rotateItemSlider.onChange = function (e) { previewStart(); }
__rotateItemAngle.addEventListener('keydown', function (e) { inputNumberEvents(e, this, 0, 360); __rotateItemSlider.value = Math.round(this.text); });
__rotateItemAngle.addEventListener('keyup', function (e) { previewStart(); });
__rotateItemSlider.enabled = __rotateItemAngle.enabled = false;
}
with (add('group')) {
orientation = 'row';
alignChildren = ['fill', 'fill'];
add('statictext', undefined, 'Offset:');
var __offset = add('edittext', [0, 0, 100, 25], '0 px'),
__direction = add('dropdownlist', undefined, 'Direction: Up,Direction: Right,Direction: Down,Direction: Left'.split(','));
__direction.selection = 0;
__offset.addEventListener('keydown', function (e) { inputNumberEvents(e, this, -Infinity, Infinity); });
__offset.addEventListener('keyup', function (e) { previewStart(); });
__direction.onChange = function () { previewStart(); }
}
with (add('group')) {
orientation = 'row';
alignChildren = ['fill', 'fill'];
// add('statictext', undefined, 'Offset:');
var __randomOffset = add('checkbox', undefined, 'Random offset');
__randomOffset.alignment = 'bottom';
add('statictext', undefined, 'Min:');
var __offsetMin = add('edittext', [0, 0, 60, 25], '0 px');
add('statictext', undefined, 'Max:');
var __offsetMax = add('edittext', [0, 0, 60, 25], '0 px');
__offsetMin.enabled = __offsetMax.enabled = __randomOffset.value;
__randomOffset.onClick = function () {
__offsetMin.enabled = __offsetMax.enabled = this.value;
__offset.enabled = !this.value;
previewStart();
}
__offsetMin.addEventListener('keydown', function (e) { inputNumberEvents(e, __offsetMin, 0, Infinity, normalizeOffset); });
__offsetMin.addEventListener('change', function (e) { normalizeOffset($.convertUnits(this.text, 'px'), __offsetMin, 0, Infinity); });
__offsetMin.addEventListener('keyup', function (e) { previewStart(); });
__offsetMax.addEventListener('keydown', function (e) { inputNumberEvents(e, __offsetMax, 0, Infinity, normalizeOffset); });
__offsetMax.addEventListener('change', function (e) { normalizeOffset($.convertUnits(this.text, 'px'), __offsetMax, 0, Infinity); });
__offsetMax.addEventListener('keyup', function (e) { previewStart(); });
}
__offset.enabled = !__randomOffset.value;
with (add('group')) {
orientation = 'row';
alignChildren = ['fill', 'fill'];
var __pos = add('dropdownlist', [0, 0, 105, 25], 'Position: Absolute,Position: Relative'.split(','));
__pos.selection = 1;
__pos.onChange = function () { previewStart(); }
var __bounds = add('dropdownlist', [0, 0, 105, 25], 'Bounds: Geometric,Bounds: Visible'.split(','));
__bounds.selection = 0;
__bounds.onChange = function () { previewStart(); }
}
with (add('group')) {
orientation = 'row';
alignChildren = 'left';
var __counterClockwise = add('checkbox', undefined, 'Counter Clockwise');
__counterClockwise.onClick = function () { previewStart(); }
var __notRotate = add('checkbox', undefined, 'Reset rotate from item');
__notRotate.onClick = function () { previewStart(); }
}
with (add('group')) {
orientation = 'row';
alignChildren = 'left';
var __copiesEnabled = add('checkbox', undefined, 'Copies enabled');
__copiesEnabled.onClick = function () { __copies.enabled = this.value; previewStart(); }
var __copies = add('edittext', [0, 0, 200, 25], 1);
__copies.enabled = false;
__copies.addEventListener('keydown', function (e) { inputNumberEvents(e, this, 1, Infinity); this.text = parseInt(this.text); });
__copies.addEventListener('keyup', function (e) { previewStart(); });
__copies.onChange = function () { if (isNaN(parseInt(this.text))) this.text = 1; }
}
}
with (win.add('group')) {
orientation = 'row';
alignChildren = ['fill', 'fill'];
var preview = add('checkbox', undefined, 'Preview'),
cancelBtn = add('button', undefined, 'Cancel'),
applyBtn = add('button', undefined, 'OK');
preview.onClick = function() { previewStart(); }
cancelBtn.onClick = function() { win.close(); }
applyBtn.onClick = function() {
if (preview.value && isUndo) {
isUndo = false;
win.close();
}
else {
app.undo();
startAction();
isUndo = false;
win.close();
}
}
}
function getData() {
return {
copies: parseInt(__copies.text),
offset: __offset.text,
angleStart: parseInt(__startAngle.text),
angleEnd: (!__isEndAngle.value ? 360 : parseInt(__endAngle.text)),
notCopies: !__copiesEnabled.value,
resetItem: __notRotate.value,
bounds: __bounds.selection.text.toLowerCase().replace('bounds: ', ''),
direction: __direction.selection.text.toLowerCase().replace('direction: ', ''),
position: __pos.selection.text.toLowerCase().replace('position: ', ''),
counterclockwise: __counterClockwise.value,
isRotateItem: __isRotateItem.value,
rotateItemAngle: __rotateItemSlider.value,
offsetRandom: __randomOffset.value,
offsetMin: __offsetMin.text,
offsetMax: __offsetMax.text,
};
}
function startAction() {
circular(getData());
}
function previewStart() {
if (preview.value) {
if (isUndo) app.undo();
else isUndo = true;
startAction();
app.redraw();
}
else if (isUndo) {
app.undo();
app.redraw();
isUndo = false;
}
}
win.onClose = function () {
if (isUndo) {
app.undo();
app.redraw();
isUndo = false;
}
// selection = $items;
saveSettings();
return true;
}
function saveSettings() {
try{
var $file = new File(settingFile.folder + settingFile.name),
data = [
__startAngleSlider.value,
__startAngle.text,
__isEndAngle.value,
__endAngleSlider.value,
__endAngle.text,
__offset.text,
__direction.selection.index,
__pos.selection.index,
__bounds.selection.index,
__counterClockwise.value,
__notRotate.value,
__copiesEnabled.value,
__copies.text,
__isRotateItem.value,
__rotateItemSlider.value,
__rotateItemAngle.text,
__randomRotate.value,
__randomOffset.value,
__offsetMin.text,
__offsetMax.text
].toString();
$file.open('w');
$file.write(data);
$file.close();
}catch(e){$.errorMessage(e);}
}
function loadSettings() {
var $file = File(settingFile.folder + settingFile.name);
if ($file.exists) {
try {
$file.open('r');
var data = $file.read().split('\n'),
$main = data[0].split(',');
__startAngleSlider.value = parseInt($main[0]);
__startAngle.text = $main[1];
__isEndAngle.value = ($main[2] === 'true');
__endAngleSlider.value = parseInt($main[3]);
__endAngle.text = $main[4];
__offset.text = $main[5];
__direction.selection.index = parseInt($main[6]);
__pos.selection.index = parseInt($main[7]);
__bounds.selection.index = parseInt($main[8])
__counterClockwise.value = ($main[9] === 'true');
__notRotate.value = ($main[10] === 'true');
__copiesEnabled.value = ($main[11] === 'true');
__copies.text = $main[12];
__isRotateItem.value = ($main[13] === 'true');
__rotateItemSlider.value = parseInt($main[14]);
__rotateItemAngle.text = $main[15];
__randomRotate.value = ($main[16] === 'true');
if ($main[17]) __randomOffset.value = ($main[17] === 'true');
if ($main[18]) __offsetMin.text = $main[18];
if ($main[19]) __offsetMax.text = $main[19];
__copies.enabled = __copiesEnabled.value;
__endAngleSlider.enabled = __endAngle.enabled = __isEndAngle.value;
__randomRotate.enabled = __isRotateItem.value;
__rotateItemSlider.enabled = __rotateItemAngle.enabled = (__isRotateItem.value && __randomRotate.value ? false : __isRotateItem.value);
__offset.enabled = !__randomOffset.value;
__offsetMin.enabled = __offsetMax.enabled = __randomOffset.value;
} catch (e) {}
$file.close();
}
}
function checkSettingFolder() {
var $folder = new Folder(settingFile.folder);
if (!$folder.exists) $folder.create();
}
checkSettingFolder();
loadSettings();
selection = null;
app.redraw();
win.center();
win.show();
Download it from original source : https://raw.githubusercontent.com/alexander-ladygin/illustrator-scripts/master/circular.jsx
Circular – Illustrator Script (0 downloads )