Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

createOutlines() + pathToPoints() #352

Merged
merged 11 commits into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 298 additions & 1 deletion basil.js
Original file line number Diff line number Diff line change
Expand Up @@ -7763,6 +7763,152 @@ pub.vertex = function() {
}
};

// ----------------------------------------
// Shape/Path to Points
// ----------------------------------------

/**
* @summary Get points and bezier coordinates from path(s).
* @description Returns an object containing an array of all points, an array of all beziers (points + their anchor points) and an array of all paths (containing its array of points + beziers) of a given pageItem in InDesign. Together with `createOutlines()` this can be used on text items. Accepts both single paths or a collection/group of paths.
* When using this on a multi path object (e.g. text with separate paths), the `paths` property can be used to loop over every path separately, whereas the properties `points` and `beziers` contain arrays for all paths combined.
* An optional second parameter adds interpolated points between existing points, which is helpful for subdividing existing paths.
*
* @cat Shape
* @method pathToPoints
* @param {Object} obj The pageItem(s) to process point/bezier coordinates of.
* @param {Number} [addPoints] Optional amount of additional interpolated points.
* @return {Object} Returns object with the following arrays `points`, `beziers`, `paths`
*
* @example <caption>Draw all points of a vector path</caption>
* noFill();
* var myCircle = ellipse(width / 2, height / 2, width / 2, width / 2);
* var pts = pathToPoints(myCircle);
*
* for (var i = 0; i < pts.points.length; i++) {
* var pt = pts.points[i];
* ellipse(pt.x, pt.y, 3, 3);
* }
*
* @example <caption>With Interpolation between Points</caption>
* noFill();
* var myCircle = ellipse(width / 2, height / 2, width / 2, width / 2);
* var pts = pathToPoints(myCircle, 5); // add 5 points between each point
*
* for (var i = 0; i < pts.points.length; i++) {
* var pt = pts.points[i];
* ellipse(pt.x, pt.y, 3, 3);
* }
*
* @example <caption>Draw Beziers and handles from Path</caption>
* noFill();
* textSize(400);
* var myText = text('S', 0, 0, width, height);
* var myOutlines = createOutlines(myText);
* var pts = pathToPoints(myOutlines);
*
* beginShape();
* for (var i = 0; i < pts.beziers.length; i++) {
* var bz = pts.beziers[i];
* vertex(bz.anchor.x, bz.anchor.y, bz.left.x, bz.left.y, bz.right.x, bz.right.y);
* line(bz.anchor.x, bz.anchor.y, bz.left.x, bz.left.y); // left handle
* line(bz.anchor.x, bz.anchor.y, bz.right.x, bz.right.y); // right handle
* }
* endShape(CLOSE);
*
* @example <caption>Separated Paths of Beziers</caption>
* noFill();
* textSize(400);
* var myText = text('B', 0, 0, width, height);
* var myOutlines = createOutlines(myText);
* var pts = pathToPoints(myOutlines); // add 3 for more detail
*
* for (var j = 0; j < pts.paths.length; j++) {
* var path = pts.paths[j];
*
* beginShape();
* for (var i = 0; i < path.beziers.length; i++) {
* var bz = path.beziers[i];
* vertex(bz.anchor.x, bz.anchor.y, bz.left.x, bz.left.y, bz.right.x, bz.right.y);
* line(bz.anchor.x, bz.anchor.y, bz.left.x, bz.left.y); // left handle
* line(bz.anchor.x, bz.anchor.y, bz.right.x, bz.right.y); // right handle
* }
* endShape(CLOSE);
* }
*/

pub.pathToPoints = function(obj, addPoints) {
var pz = {paths:[], points:[], beziers:[]},
pzGroup = false,
grabPoints = function(formElm) {
if(formElm.hasOwnProperty('paths')){
for (var j=0; j < formElm.paths.length; j++) {
var paths = formElm.paths[j];
var pzPaths = [];
var pzPoints = [];
var pzBeziers = [];
for (var i=0; i < paths.pathPoints.length; i++) {
var pt = paths.pathPoints[i];
var pzBezier = {
anchor:{x:pt.anchor[0], y:pt.anchor[1]},
left:{x:pt.leftDirection[0], y:pt.leftDirection[1]},
right:{x:pt.rightDirection[0], y:pt.rightDirection[1]}
};
pz.beziers.push(pzBezier);
pzBeziers.push(pzBezier);

// optionally interpolated points
if (addPoints === undefined) {
var pzPoint = {x:pt.anchor[0], y:pt.anchor[1]};
pz.points.push(pzPoint);
pzPoints.push(pzPoint);
} else {
var nextSel = (i + 1) % paths.pathPoints.length;
var nextPt = paths.pathPoints[nextSel];
var amt = 1.0 / (addPoints + 1);

if (formElm.paths[0].pathType === PathType.OPEN_PATH && i === paths.pathPoints.length-1) {
amt = 1; // don't interpolate end to start on open paths
}

for (var t = 0; t < 1; t += amt) {
var ptStep = interpolateBezier(pt, nextPt, t);
var pzPointStep = {x:ptStep.x, y:ptStep.y};
pz.points.push(pzPointStep);
pzPoints.push(pzPointStep);
}
}
}

var pzPath = {points:pzPoints, beziers:pzBeziers};
pz.paths.push(pzPath);
}
}else{
error("pathToPoints(), no paths found. \nUse: polygon[s]");
}
};

if(obj === undefined){
error("pathToPoints(), no paths found. \nUse: polygon[s]");
}

// catch grouped items
if (obj instanceof Group) {
obj = obj.pageItems;
pzGroup = true;
}

// process type as multi-line/character or single
if (isArray(obj) || pzGroup) {
for (var k=0; k < obj.length; k++) {
grabPoints(obj[k]);
}
} else {
grabPoints(obj);
}

return pz;
};

// ----------------------------------------
// Shape Private
// ----------------------------------------
Expand Down Expand Up @@ -7852,6 +7998,28 @@ function doAddPath() {
}
}

/*
* https://stackoverflow.com/questions/4089443/find-the-tangent-of-a-point-on-a-cubic-bezier-curve
* https://webglfundamentals.org/webgl/lessons/webgl-3d-geometry-lathe.html
*/
function interpolateBezier(startPoint, endPoint, t) {
var newCoord = [];
for (var i = 0; i < 2; i++) {
var p1 = startPoint.anchor[i];
var p2 = startPoint.rightDirection[i];
var p3 = endPoint.leftDirection[i];
var p4 = endPoint.anchor[i];

var C1 = (p4 - (3.0 * p3) + (3.0 * p2) - p1);
var C2 = ((3.0 * p3) - (6.0 * p2) + (3.0 * p1));
var C3 = ((3.0 * p2) - (3.0 * p1));
var C4 = (p1);

newCoord[i] = (C1 * t * t * t + C2 * t * t + C3 * t + C4);
}
return {x:newCoord[0], y:newCoord[1]};
}

function notCalledBeginShapeError () {
error("endShape(), you have to call first beginShape(), before calling vertex() and endShape()");
}
Expand Down Expand Up @@ -8654,7 +8822,11 @@ pub.textLeading = function(leading) {
*/
pub.textSize = function(pointSize) {
if (arguments.length === 1) {
currFontSize = pointSize;
if(pointSize > 0) {
currFontSize = pointSize;
}else{
error("textSize() must be greater than 0.");
}
}
return currFontSize;
};
Expand Down Expand Up @@ -8911,6 +9083,131 @@ pub.paragraphStyle = function(textOrName, props) {
return style;
};

// ----------------------------------------
// Typography/Outlines
// ----------------------------------------

/**
* @summary Convert text items to outlines.
* @description Returns an array of polygons after outlining text and optionally processes them with a callback function.
* Use together with `pathToPoints()` for getting point and bezier coordinates from outlines. If used on a text frame,
* all linked text frames will be converted to outlines as well.
*
* @cat Typography
* @method createOutlines
*
* @param {TextFrame|TextPath} item Text or TextPath to be outlined.
* @param {Function} [cb] Optional: Callback function to use with each polygon. Passed arguments: `obj`, `loopCounter`
* @return {Array of Polygons} Returns an array of polygons.
*
* @example <caption>Convert text to outlines</caption>
* textSize(150);
* var myText = text("Hello", 0, 0, width, height);
* var outlines = createOutlines(myText);
*
* @example <caption>Run a callback function on each resulting shape</caption>
* textSize(150);
* var myText = text("Hello \nWorld", 0, 0, width, height);
* var outlines = createOutlines(myText, function(obj){
* var pts = pathToPoints(obj);
* println(pts.length); // check number of points
* });
*
*/


pub.createOutlines = function(item, cb) {
var outlines, changedFill = false;

// check if textFrames
if (item.hasOwnProperty('createOutlines')) {
// gather whole texts (for linked textFrames)
item = item.parentStory.texts[0];

// catch text w/o fill or stroke
for (var i=0; i < item.texts.length; i++) {
if (item.texts[i].fillColor.name === "None" && item.texts[i].strokeColor.name === "None") {
item.texts[i].fillColor = color(0);
changedFill = true;
}
}

outlines = item.createOutlines();

// check if textPath
} else if ( item instanceof GraphicLine ||
item instanceof Oval ||
item instanceof Rectangle ||
item instanceof Polygon) {
// check shape has textPaths
if (item.textPaths.length > 0) {

item = item.textPaths[0].parentStory.texts[0];

// catch text w/o fill or stroke
for (var i=0; i < item.texts.length; i++) {
if (item.texts[i].fillColor.name === "None" && item.texts[i].strokeColor.name === "None") {
item.texts[i].fillColor = color(0);
changedFill = true;
}
}

// *** how to remove textPath holder object??
outlines = item.createOutlines();
}

// error if neither
} else {
error("createOutlines(), incorrect first parameter. Use:\n Story | TextFrame | Paragraph | Line | Word | Character | TextPath");
}

// remove tempFill for those items
if (changedFill) {
for (var i=0; i < outlines.length; i++) {
outlines[i].fillColor = "None";
}

}

// process outlines
if (outlines.hasOwnProperty('length') && outlines.length === 1) {
// multi-line text from group array
if (outlines[0] instanceof Group) {
outlines = ungroup(outlines[0]);
// textframe multi-line
if (cb instanceof Function) {
return forEach(outlines, cb);
} else {
return outlines;
}
}

// single polygon
// single normal textFrame 1 line
if (cb instanceof Function) {
return forEach(outlines, cb);
} else {
return outlines;
}
} else {
// textPath, linked textFrames
// collection of polygons
if (outlines[0] instanceof Group) {
outlinesGroups = [];
for (var i=0; i < outlines.length; i++) {
outlinesGroups.push(ungroup(outlines[i]));
}
outlines = outlinesGroups;
}

if (cb instanceof Function) {
return forEach(outlines, cb);
} else {
return outlines;
}
}
};

// ----------------------------------------
// Typography/Constants
// ----------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ basil.js x.x.x DAY MONTH YEAR
checks if a given variable is an integer
+ basil scripts don't try to start ExtendScript Toolkit in the background anymore
+ Added week() to return week number of the year
+ Added createOutlines() to convert text + textPaths to outlines paths
+ Added pathToPoints() to get points, beziers, and both isolated by paths, from pageItems
+ Added loadCSV() to load an parse CSV files one step
+ Added saveCSV() to stringify data and save it as CSV file in one step
+ Added CSV.parse() and CSV.stringify() to replace CSV.decode() and CSV.encode()
Expand Down Expand Up @@ -115,7 +117,6 @@ The user is allowed to have his files wherever he wants.
* weekday() now returns number day of the week (0 - 6)
* margins() getter fixed to use current page


- b.go() and b.loop() are removed (use function draw() or function loop() instead)
- b.itemX(), b.itemY(), b.itemWidth(), b.itemHeight(), b.itemPosition() and b.itemSize() are removed
and replaced by the new transform() function
Expand Down
Loading