The Equal Distance Points Generator is a powerful Adobe Illustrator script that redraws selected paths with evenly spaced points while intelligently preserving the original path structure. It solves the common problem of uneven point distribution in vector paths, allowing for more consistent editing and effects. Whether working with geometric shapes like rectangles and squares or organic curves, this script maintains straight lines as straight and curves as curves, ensuring the integrity of your original design while optimizing point placement.
Developed by grasycho (2025-04-04), this tool is essential for illustrators, logo designers, and vector artists who need precise control over path points without distorting their artwork.
Usage
- Select one or more paths in Adobe Illustrator
- Run the script (File > Scripts > redrawPathWithCorrectPointCount)
- Configure the options:
- Number of Points: Total number of anchor points to distribute along the path
- Handle Length (0-1): Controls the length of Bézier handles relative to segment length (smaller values create tighter curves)
- Mode: Choose “Preview Only” to see the result in red before applying, or “Replace Original” to directly replace selected paths
Best Practices:
- For precise geometric shapes, use “Preview” mode first to ensure your corners remain sharp
- For closed paths like circles, the points will be distributed evenly around the entire circumference
- For open paths, points are distributed from start to end
- Start with the default handle length of 0.3 and adjust as needed for your specific paths
JavaScript
//@target illustrator
/*
Equal Distance Points Generator
Version: 4.1
Last Updated: 2025-04-04 19:13:15
Author: grasycho
This script redraws selected paths with evenly spaced points
while preserving the original path structure (straight lines stay straight).
*/
// Main function - entry point of script
function main() {
if (!app.documents.length) {
alert("Please open a document first.");
return;
}
// Process paths
processSelectedPaths();
}
// Process selected paths
function processSelectedPaths() {
var doc = app.activeDocument;
var selection = doc.selection;
if (!selection.length) {
alert("Please select at least one path.");
return;
}
// Get options from user
var options = getUserOptions();
if (!options) return; // User cancelled
var targetLayer;
if (options.previewOnly) {
// Create a preview layer
targetLayer = doc.layers.add();
targetLayer.name = "PREVIEW - Equal Points";
} else {
targetLayer = doc.activeLayer;
}
// Process each path
var processedCount = 0;
for (var i = 0; i < selection.length; i++) {
if (selection[i].typename === "PathItem") {
var path = selection[i];
// Create a new path
var newPath = targetLayer.pathItems.add();
newPath.stroked = path.stroked;
newPath.strokeWidth = path.strokeWidth;
newPath.strokeColor = path.strokeColor;
newPath.filled = path.filled;
newPath.fillColor = path.fillColor;
newPath.closed = path.closed;
// Make preview paths red for visibility
if (options.previewOnly) {
var redColor = new RGBColor();
redColor.red = 255;
redColor.green = 0;
redColor.blue = 0;
newPath.strokeColor = redColor;
}
// Extract path structure
var pathStructure = extractPathStructure(path);
// Redraw with equal spacing while preserving structure
if (redrawWithEqualSpacing(path, newPath, pathStructure, options.numPoints, options.handleLength)) {
processedCount++;
// If not preview, remove the original
if (!options.previewOnly) {
path.remove();
}
} else {
// If failed, remove the new path
newPath.remove();
}
}
}
if (processedCount === 0) {
alert("No paths were processed. Please check your selection.");
if (options.previewOnly && targetLayer.pageItems.length === 0) {
targetLayer.remove();
}
} else {
if (options.previewOnly) {
alert("Preview created with " + processedCount + " paths.\nOriginal paths are unchanged.");
} else {
alert("Successfully processed " + processedCount + " paths.");
}
}
}
// Get options from user
function getUserOptions() {
var dialog = new Window("dialog", "Equal Distance Points");
dialog.alignChildren = "fill";
// Options panel
var optionsPanel = dialog.add("panel", undefined, "Options");
optionsPanel.orientation = "column";
optionsPanel.alignChildren = "left";
optionsPanel.margins = 15;
// Number of points
var pointsGroup = optionsPanel.add("group");
pointsGroup.add("statictext", undefined, "Number of Points:");
var pointsInput = pointsGroup.add("edittext", undefined, "100");
pointsInput.characters = 5;
// Handle length
var handleGroup = optionsPanel.add("group");
handleGroup.add("statictext", undefined, "Handle Length (0-1):");
var handleInput = handleGroup.add("edittext", undefined, "0.3");
handleInput.characters = 5;
// Mode selection
var modePanel = dialog.add("panel", undefined, "Mode");
modePanel.orientation = "row";
modePanel.alignChildren = "center";
var previewRadio = modePanel.add("radiobutton", undefined, "Preview Only");
var replaceRadio = modePanel.add("radiobutton", undefined, "Replace Original");
replaceRadio.value = true; // Default
// Info text
var infoText = dialog.add("statictext", undefined, "Preserves original path structure");
infoText.alignment = "center";
// Buttons
var buttonGroup = dialog.add("group");
buttonGroup.alignment = "center";
var okButton = buttonGroup.add("button", undefined, "OK", {name: "ok"});
var cancelButton = buttonGroup.add("button", undefined, "Cancel", {name: "cancel"});
// Show dialog
if (dialog.show() !== 1) return null;
// Return options
return {
numPoints: parseInt(pointsInput.text, 10) || 100,
handleLength: parseFloat(handleInput.text) || 0.3,
previewOnly: previewRadio.value
};
}
// Extract path structure - identify corners, straight lines, and curved segments
function extractPathStructure(path) {
var points = path.pathPoints;
var numPoints = points.length;
var structure = {
corners: [], // Indices of corner points
straightSegments: [], // Pairs of indices representing straight segments
curvedSegments: [], // Pairs of indices representing curved segments
segments: [], // All segments with metadata
pointTypes: [] // Type of each point (CORNER, SMOOTH, etc.)
};
// First, identify all corners and point types
for (var i = 0; i < numPoints; i++) {
var point = points[i];
structure.pointTypes[i] = point.pointType;
if (point.pointType === PointType.CORNER) {
structure.corners.push(i);
}
}
// Function to check if handle is effectively at the anchor
function isHandleAtAnchor(anchor, handle) {
var dx = handle[0] - anchor[0];
var dy = handle[1] - anchor[1];
return Math.sqrt(dx*dx + dy*dy) < 0.1; // Small threshold
}
// Function to check if segment is a straight line
function isSegmentStraightLine(p1, p2) {
// Check if handles are collinear (or very close to it)
var a = p1.anchor;
var b = p1.rightDirection;
var c = p2.leftDirection;
var d = p2.anchor;
// If both handles are at the anchors, it's a straight line
if (isHandleAtAnchor(a, b) && isHandleAtAnchor(d, c)) {
return true;
}
// Otherwise check if points are collinear
var v1x = b[0] - a[0];
var v1y = b[1] - a[1];
var v2x = d[0] - a[0];
var v2y = d[1] - a[1];
// Cross product near zero means collinear
var cross = Math.abs(v1x * v2y - v1y * v2x);
var len1 = Math.sqrt(v1x * v1x + v1y * v1y);
var len2 = Math.sqrt(v2x * v2x + v2y * v2y);
if (len1 > 0 && len2 > 0 && cross / (len1 * len2) < 0.0001) {
// Also check second part
v1x = c[0] - a[0];
v1y = c[1] - a[1];
cross = Math.abs(v1x * v2y - v1y * v2x);
len1 = Math.sqrt(v1x * v1x + v1y * v1y);
return len1 > 0 && cross / (len1 * len2) < 0.0001;
}
return false;
}
// Analyze each segment
for (var i = 0; i < numPoints; i++) {
var nextIdx = (i + 1) % numPoints;
// Skip if we're at the last point of an open path
if (!path.closed && i === numPoints - 1) continue;
var p1 = points[i];
var p2 = points[nextIdx];
var isStraight = isSegmentStraightLine(p1, p2);
var segment = {
startIdx: i,
endIdx: nextIdx,
isStraight: isStraight,
startAnchor: p1.anchor,
endAnchor: p2.anchor,
startRightDir: p1.rightDirection,
endLeftDir: p2.leftDirection,
startType: p1.pointType,
endType: p2.pointType,
// Calculate length
length: isStraight ?
getDistance(p1.anchor, p2.anchor) :
getBezierLength(p1.anchor, p1.rightDirection, p2.leftDirection, p2.anchor)
};
structure.segments.push(segment);
if (isStraight) {
structure.straightSegments.push([i, nextIdx]);
} else {
structure.curvedSegments.push([i, nextIdx]);
}
}
// Calculate total path length
var totalLength = 0;
for (var i = 0; i < structure.segments.length; i++) {
totalLength += structure.segments[i].length;
}
structure.totalLength = totalLength;
return structure;
}
// Redraw path with equal spacing while preserving original structure
function redrawWithEqualSpacing(srcPath, destPath, pathStructure, numPoints, handleLengthFactor) {
// Clear any default points
while (destPath.pathPoints.length > 0) {
destPath.pathPoints[0].remove();
}
var totalLength = pathStructure.totalLength;
var segments = pathStructure.segments;
if (totalLength <= 0 || segments.length === 0) {
return false;
}
// Calculate point spacing
var spacing;
var pointsToCreate;
if (srcPath.closed) {
// For closed paths, distribute points evenly around the entire path
pointsToCreate = numPoints;
spacing = totalLength / numPoints;
} else {
// For open paths, distribute points from start to end
pointsToCreate = numPoints;
spacing = totalLength / (numPoints - 1);
}
// Create points
var currentDistance = 0;
var segmentIndex = 0;
var segmentOffset = 0;
for (var i = 0; i < pointsToCreate; i++) {
var targetDistance;
if (srcPath.closed) {
// For closed paths, distribute evenly around the path
targetDistance = (i * spacing) % totalLength;
} else {
// For open paths, distribute from start to end
targetDistance = Math.min(i * spacing, totalLength);
}
// Find the segment containing this distance
while (segmentIndex < segments.length &&
(currentDistance + segments[segmentIndex].length) < targetDistance) {
currentDistance += segments[segmentIndex].length;
segmentIndex = (segmentIndex + 1) % segments.length;
}
// Current segment
var segment = segments[segmentIndex];
// Relative distance within this segment (0-1)
var t = (targetDistance - currentDistance) / segment.length;
// Create a new point
var newPoint = destPath.pathPoints.add();
if (segment.isStraight) {
// Linear interpolation for straight segments
var x = segment.startAnchor[0] + t * (segment.endAnchor[0] - segment.startAnchor[0]);
var y = segment.startAnchor[1] + t * (segment.endAnchor[1] - segment.startAnchor[1]);
newPoint.anchor = [x, y];
// For straight segments, make the handles also lie on the line
var dx = segment.endAnchor[0] - segment.startAnchor[0];
var dy = segment.endAnchor[1] - segment.startAnchor[1];
var dist = Math.sqrt(dx*dx + dy*dy);
if (dist > 0) {
var handleLen = spacing * handleLengthFactor;
var nx = dx / dist;
var ny = dy / dist;
// Align handles with the straight line
newPoint.leftDirection = [x - nx * handleLen, y - ny * handleLen];
newPoint.rightDirection = [x + nx * handleLen, y + ny * handleLen];
} else {
// Failsafe
newPoint.leftDirection = newPoint.anchor;
newPoint.rightDirection = newPoint.anchor;
}
// For straight segments, make it a SMOOTH point to ensure straight line
newPoint.pointType = PointType.SMOOTH;
// If we're very close to a corner, restore its corner nature
if ((t < 0.05 && segment.startType === PointType.CORNER) ||
(t > 0.95 && segment.endType === PointType.CORNER)) {
newPoint.pointType = PointType.CORNER;
// For corners, make handles coincide with anchor
newPoint.leftDirection = newPoint.anchor;
newPoint.rightDirection = newPoint.anchor;
}
} else {
// Bezier interpolation for curved segments
var point = bezierAtParameter(
segment.startAnchor, segment.startRightDir,
segment.endLeftDir, segment.endAnchor, t
);
var tangent = bezierTangentAtParameter(
segment.startAnchor, segment.startRightDir,
segment.endLeftDir, segment.endAnchor, t
);
newPoint.anchor = [point.x, point.y];
// Calculate handle length based on spacing and factor
var handleLen = spacing * handleLengthFactor;
var tanLen = Math.sqrt(tangent.x*tangent.x + tangent.y*tangent.y);
if (tanLen > 0) {
var nx = tangent.x / tanLen;
var ny = tangent.y / tanLen;
newPoint.leftDirection = [point.x - nx * handleLen, point.y - ny * handleLen];
newPoint.rightDirection = [point.x + nx * handleLen, point.y + ny * handleLen];
} else {
// Failsafe
newPoint.leftDirection = newPoint.anchor;
newPoint.rightDirection = newPoint.anchor;
}
// For curved segments, make it a SMOOTH point for smooth curves
newPoint.pointType = PointType.SMOOTH;
}
}
return true;
}
// Calculate point at parameter t along a Bezier curve
function bezierAtParameter(p0, p1, p2, p3, t) {
var u = 1 - t;
var tt = t * t;
var uu = u * u;
var uuu = uu * u;
var ttt = tt * t;
var x = uuu * p0[0] +
3 * uu * t * p1[0] +
3 * u * tt * p2[0] +
ttt * p3[0];
var y = uuu * p0[1] +
3 * uu * t * p1[1] +
3 * u * tt * p2[1] +
ttt * p3[1];
return {x: x, y: y};
}
// Calculate tangent at parameter t along a Bezier curve
function bezierTangentAtParameter(p0, p1, p2, p3, t) {
var u = 1 - t;
var x = 3 * u * u * (p1[0] - p0[0]) +
6 * u * t * (p2[0] - p1[0]) +
3 * t * t * (p3[0] - p2[0]);
var y = 3 * u * u * (p1[1] - p0[1]) +
6 * u * t * (p2[1] - p1[1]) +
3 * t * t * (p3[1] - p2[1]);
return {x: x, y: y};
}
// Get distance between two points
function getDistance(p1, p2) {
var dx = p2[0] - p1[0];
var dy = p2[1] - p1[1];
return Math.sqrt(dx*dx + dy*dy);
}
// Get approximate length of a Bezier curve
function getBezierLength(p0, p1, p2, p3) {
var steps = 30; // Number of steps for approximation
var length = 0;
var prevX = p0[0];
var prevY = p0[1];
for (var i = 1; i <= steps; i++) {
var t = i / steps;
var point = bezierAtParameter(p0, p1, p2, p3, t);
var dx = point.x - prevX;
var dy = point.y - prevY;
length += Math.sqrt(dx*dx + dy*dy);
prevX = point.x;
prevY = point.y;
}
return length;
}
// Run the script
main();