Strict Standards: Only variables should be passed by reference in /home/abeall/public_html/fireworks/download.php on line 28

Warning: Cannot modify header information - headers already sent by (output started at /home/abeall/public_html/fireworks/download.php:28) in /home/abeall/public_html/fireworks/download.php on line 44
// Fireworks JavaScript Command // Offsets selected points or paths // Install by copying to Fireworks/Configuration/Commands/ // Run in Fireworks via the Commands menu // Aaron Beall 2007-2011 - http://abeall.com // Version 1.5 /* BUGS - on 'aaron walking' test vector, the last couple points' handles seem to not calculate Y correctly and are put at y=0 - [FIXED-v1.2] breaks when handles too close to nodes but not on nodes - [FIXED-v1.5 added miter limit] because of approximations and bezier rounding, when handles are too close to nodes it extrapolates them to extreme values TODO - create 'outline stroke' version which offsets in both directions, with an optional taper - implement 'add points to maintain contour' option to add points on tight curves in order to preserve shape when offsetting - [DONE-v1.3] directoin is not always intuitive, use winding fill rule algorithm to make positive values consistently move "outwards" and negative values move "inwards" - [DONE-v1.4] use offset intersection to maintain corners instead of average rangent - - implement corner options: miter, bevel, round - implement tapering to define different offset amounts at different points along contour */ var dom = fw.getDocumentDOM(); var sel = [].concat(fw.selection); function OffsetPoints(){ // require active document if (!dom) return false; // validate selection var paths = []; for(var s in sel){ if(sel[s] == '[object Path]') paths.push(sel[s]); } if(!paths.length) return alert('This commands requires selected paths, or selected points on a path.'); // user input var input; do{ input = prompt("Enter an offset amount:", fw.OffsetPoints_input || 5); }while(!validate(input)); function validate(input){ if(input == null) return true; if(!isNaN(Number(input))) return true; alert("Invalid input! Enter numbers only."); return false; } if(input == null) return false; var AMOUNT = fw.OffsetPoints_input = Number(input); var isInteger = AMOUNT == Math.round(AMOUNT); // loop through all selected nodes, on all select contours, on all selected paths var newNodes, isClosed, clockwise, nodes, nod, newNode, newPred, newSucc, prevNode, nextNode, hasPred, hasSucc, pre, suc, angle, anglePred, angleSucc, vector, pt, handle, bez, slope1, slope2, intersect; var ANGLE_PERCENT = 0.000001, MIN_HANDLE_DISTANCE = 0.01, MITER_LIMIT = Math.max(dom.width, dom.height) * 2; for(var p = 0; p < paths.length; p++){ for(var c = 0 ; c < paths[p].contours.length; c++){ nodes = paths[p].contours[c].nodes; isClosed = paths[p].contours[c].isClosed; // check to see if contour is clockwise or counter-clockwise clockwise = isClockwise(nodes) var offset = clockwise ? AMOUNT : -AMOUNT; // begin offsetting points on a contour newNodes = [] var nlen = nodes.length; for(var n = 0; n < nlen; n++){ nod = nodes[n]; newNodes.push({x:nod.x, y:nod.y, predX:nod.predX, predY:nod.predY, succX:nod.succX, succY:nod.succY}); newNode = newNodes[newNodes.length - 1]; // skip unselected nodes if(!nod.isSelectedPoint && fw.activeTool == 'Subselection') continue; // find preceding and succeding points along curve, looping if isClosed, otherwise extrapolating if(n == 0){ suc = getBezier(ANGLE_PERCENT, nodes[n + 1], {x:nodes[n + 1].predX, y:nodes[n + 1].predY}, {x:nod.succX, y:nod.succY}, nod); if(isClosed){ pre = getBezier(ANGLE_PERCENT, nodes[nodes.length - 1], {x:nodes[nodes.length - 1].succX, y:nodes[nodes.length - 1].succY}, {x:nod.predX, y:nod.predY}, nod); }else{ pre = {x:nod.x, y:nod.y}; pre.x -= suc.x - nod.x; pre.y -= suc.y - nod.y; } }else if(n == nlen - 1){ pre = getBezier(ANGLE_PERCENT, nodes[n - 1], {x:nodes[n - 1].succX, y:nodes[n - 1].succY}, {x:nod.predX, y:nod.predY}, nod); if(isClosed){ suc = getBezier(ANGLE_PERCENT, nodes[0], {x:nodes[0].predX, y:nodes[0].predY}, {x:nod.succX, y:nod.succY}, nod); }else{ suc = {x:nod.x, y:nod.y}; suc.x -= pre.x - nod.x; suc.y -= pre.y - nod.y; } }else{ pre = getBezier(ANGLE_PERCENT, nodes[n - 1], {x:nodes[n - 1].succX, y:nodes[n - 1].succY}, {x:nod.predX, y:nod.predY}, nod); suc = getBezier(ANGLE_PERCENT, nodes[n + 1], {x:nodes[n + 1].predX, y:nodes[n + 1].predY}, {x:nod.succX, y:nod.succY}, nod); } // find average normal from tangents leading into and out of the point pre.x = nod.x - pre.x; pre.y = nod.y - pre.y; suc.x = suc.x - nod.x; suc.y = suc.y - nod.y; var avg = {x:(pre.x + suc.x) * .5, y:(pre.y + suc.y) * .5}; avg = {x:avg.y, y:-avg.x}; angle = Math.atan2(avg.y, avg.x)//getAngleRadians({x:0, y:0}, avg); // adjust corner with mitering vector = null; if(!nod.isCurvePoint){ scaleVector(pre, offset); scaleVector(suc, offset); newPred = {x:nod.x + pre.y, y:nod.y - pre.x}; newSucc = {x:nod.x + suc.y, y:nod.y - suc.x}; intersect = lineIntersection(newPred, {x:newPred.x + pre.x, y:newPred.y + pre.y}, newSucc, {x:newSucc.x + suc.x, y:newSucc.y + suc.y}); if(intersect && getDistance(nod, intersect) < MITER_LIMIT){ vector = { x:intersect.x - newNode.x, y:intersect.y - newNode.y } } } if(!vector){ // adjust curve point along path normal vector = getVectorRadians(offset, angle); } // pixel rounding if(isInteger){ vector.x = Math.round(vector.x); vector.y = Math.round(vector.y); } // new node position newNode.x += vector.x; newNode.y += vector.y; newNode.predX += vector.x; newNode.predY += vector.y; newNode.succX += vector.x; newNode.succY += vector.y; // find perpendicular bezier point for pred/succ handles, if necessary // expand/contract pred/succ handles based on intersection of previously found perpendicular point and current handle position // predecessor handle adjustment hasPred = nod.predX != nod.x || nod.predY != nod.y; hasPred = hasPred && !(n == 0 && !isClosed); hasPred = Math.abs(nod.predX - nod.x) > MIN_HANDLE_DISTANCE || Math.abs(nod.predY - nod.y) > MIN_HANDLE_DISTANCE; // math breaks when handles too close to nodes if(hasPred){ // find curve normal that intersects handle handle = {x:nod.predX, y:nod.predY}; prevNode = n > 0 ? nodes[n - 1] : nodes[nodes.length - 1]; bez = { p1:prevNode, cp1:{x:prevNode.succX, y:prevNode.succY}, cp2:{x:nod.predX, y:nod.predY}, p2:nod } pt = getBezierPerpendicular({x:nod.predX, y:nod.predY}, bez); //dom.addNewLine({x:nod.predX,y:nod.predY}, {x:pt.x,y:pt.y}); // interpolate handle based on intersection of original normal and adjusted point location intercept = lineIntersection(pt, handle, newNode, {x:newNode.predX, y:newNode.predY}); /*if(pt.x == nod.predX){ // special case for vertical line slope2 = pointPointSlope(newNode, {x:newNode.predX, y:newNode.predY}); intercept = {x:pt.x, y:slope2.m * pt.x + slope2.b}; }else if(newNode.x == newNode.predX){ // special case for vertical line slope1 = pointPointSlope(pt, {x:nod.predX, y:nod.predY}); intercept = {x:newNode.x, y:slope1.m * newNode.x + slope1.b}; }else{ // two non vertical lines slope1 = pointPointSlope(pt, {x:nod.predX, y:nod.predY}); slope2 = pointPointSlope(newNode, {x:newNode.predX, y:newNode.predY}); intercept = intersection(slope1.m, slope1.b, slope2.m, slope2.b); }*/ if(getDistance(handle, intercept) < MITER_LIMIT){ newNode.predX = intercept.x; newNode.predY = intercept.y; } } // successor handle adjustment hasSucc = nod.succX != nod.x || nod.succY != nod.y; hasSucc = hasSucc && !(n == nodes.length-1 && !isClosed); hasSucc = Math.abs(nod.succX - nod.x) > MIN_HANDLE_DISTANCE || Math.abs(nod.succY - nod.y) > MIN_HANDLE_DISTANCE; // math breaks when handles too close to nodes if(hasSucc){ // find curve normal that intersects handle handle = {x:nod.succX, y:nod.succY}; nextNode = n < nodes.length-1 ? nodes[n + 1] : nodes[0]; bez = { p1:nextNode, cp1:{x:nextNode.predX, y:nextNode.predY}, cp2:{x:nod.succX, y:nod.succY}, p2:nod } pt = getBezierPerpendicular({x:nod.succX, y:nod.succY}, bez); //dom.addNewLine({x:nod.succX,y:nod.succY}, {x:pt.x,y:pt.y}); // interpolate handle based on intersection of original normal and adjusted point location intercept = lineIntersection(pt, handle, newNode, {x:newNode.succX, y:newNode.succY}); /*if(pt.x == nod.succX){ // special case for vertical line slope2 = pointPointSlope(newNode, {x:newNode.succX, y:newNode.succY}); intercept = {x:pt.x, y:slope2.m * pt.x + slope2.b}; }else if(newNode.x == newNode.succX){ // special case for vertical line slope1 = pointPointSlope(pt, {x:nod.succX, y:nod.succY}); intercept = {x:newNode.x, y:slope1.m * newNode.x + slope1.b}; }else{ // two non vertical lines slope1 = pointPointSlope(pt, {x:nod.succX, y:nod.succY}); slope2 = pointPointSlope(newNode, {x:newNode.succX, y:newNode.succY}); intercept = intersection(slope1.m, slope1.b, slope2.m, slope2.b); }*/ if(getDistance(handle, intercept) < MITER_LIMIT){ newNode.succX = intercept.x; newNode.succY = intercept.y; } } } // after all the calculations are done, apply the adjustemented nodes var n = nodes.length; while(n--){ nod = nodes[n]; newNode = newNodes[n]; if(!nod.isSelectedPoint && fw.activeTool == 'Subselection') continue; nod.x = newNode.x; nod.y = newNode.y; nod.predX = newNode.predX; nod.predY = newNode.predY; nod.succX = newNode.succX; nod.succY = newNode.succY; } } } } //try{ OffsetPoints(); //}catch(e){ alert([e, e.lineNumber, e.fileName].join("\n")) }; /*testBezierPerpendicular(); testIntersection(); testBezierPerpendicular(); function testBezierPerpendicular(){ for(var s = 0; s < sel.length; s++){ for(var c = 0; c < sel[s].contours.length; c++){ var nodes = sel[s].contours[c].nodes; for(var n = 0; n < nodes.length; n++){ n = Number(n); if(n==nodes.length-1) continue; var nod = nodes[n]; var pt = {x:nod.succX,y:nod.succY}; var bez = { p1:nod, cp1:{x:nod.succX,y:nod.succY}, cp2:{x:nodes[n+1].predX,y:nodes[n+1].predY}, p2:nodes[n+1] } var p = getBezierPerpendicular(pt,bez); dom.addNewLine({x:p.x,y:p.y},pt); } } } } function testIntersection(){ var v1p1 = { x:sel[0].contours[0].nodes[0].x, y:sel[0].contours[0].nodes[0].y } var v1p2 = { x:sel[0].contours[0].nodes[1].x, y:sel[0].contours[0].nodes[1].y } var v2p1 = { x:sel[1].contours[0].nodes[0].x, y:sel[1].contours[0].nodes[0].y } var v2p2 = { x:sel[1].contours[0].nodes[1].x, y:sel[1].contours[0].nodes[1].y } var i1 = pointPointSlope(v1p1,v1p2); var i2 = pointPointSlope(v2p1,v2p2); //var i1 = pointAngleSlope(v1p1,getAngleDegrees(v1p1,v1p2)); //var i2 = pointAngleSlope(v2p1,getAngleDegrees(v2p1,v2p2)); var intercept = intersection(i1.m,i1.b,i2.m,i2.b); sel[2].left = intercept.x - sel[2].width/2; sel[2].top = intercept.y - sel[2].height/2; }*/ // find a point along a bezier which is perpendicular to a target point // it turns out this can be found simply by finding the closest point on the bezier to the target point function getBezierPerpendicular(pt, bez, steps, iterations){ if(!steps) var steps = 10; if(!iterations) var iterations = 5; var i, prevAngle, prevPrevPoint, currAngle, angle, prevPoint, point, currPercent; //return getBezier(.9,bez.p1,bez.cp1,bez.cp2,bez.p2); return doWalkBezier(0,1); function doWalkBezier(minPercent, maxPercent/*, startPoint*/){ //alert('walk bezier: '+minPercent+'-'+maxPercent); //if(startPoint) //prevPoint = startPoint; var step = 1 / steps, range = maxPercent - minPercent; for(var i = 0; i <= steps; i++){ currPercent = minPercent + range * (step * i); point = getBezier(currPercent, bez.p1, bez.cp1, bez.cp2, bez.p2); point.percent = currPercent; /*if(prevPoint){ point.angle = getAngleDegrees(prevPoint,point); angle = getAngleDegrees(point,pt); currAngle = point.angle-angle;//90-(Math.abs(point.angle)-Math.abs(angle)); if(currAngle>180) currAngle -= 180; if(currAngle<-180) currAngle += 180; //if(Math.abs(currAngle)>180) //currAngle += 180; currAngle = 90-Math.abs(currAngle); dom.addNewLine({x:point.x,y:point.y},pt); alert(currPercent+" : "+Math.round(angle)+" | "+Math.round(point.angle)+" =====> "+Math.round(currAngle)); if(currAngle==0) return point; if(((currAngle>0 && prevAngle<0) || (currAngle<0 && prevAngle>0)) && Math.abs(prevAngle)<45) return --iterations ? doWalkBezier(prevPoint.percent,point.percent,prevPrevPoint) : prevPoint; prevAngle = currAngle; }*/ var xdif = point.x - pt.x, ydif = point.y - pt.y point.dist = xdif * xdif + ydif * ydif; //getDistanceSquare(point, pt); if(prevPoint){ //dom.addNewLine({x:point.x,y:point.y},pt); //alert(currPercent+": "+Math.round(point.dist)); if(point.dist > prevPoint.dist){ if(prevPrevPoint) prevPoint = prevPrevPoint; else prevPrevPoint = prevPoint; return --iterations ? doWalkBezier(prevPrevPoint.percent, point.percent) : prevPoint; } } prevPrevPoint = prevPoint; prevPoint = point; } return prevPoint; } } // find a point along a bezier segment(as defined by p1, cp1, cp2, p2) at a specified percent(0-100) function getBezier(percent, p1, cp1, cp2, p2) { function b1(t) { return t*t*t } function b2(t) { return 3*t*t*(1-t) } function b3(t) { return 3*t*(1-t)*(1-t) } function b4(t) { return (1-t)*(1-t)*(1-t) } var pos = {x:0,y:0}; pos.x = p1.x*b1(percent) + cp1.x*b2(percent) + cp2.x*b3(percent) + p2.x*b4(percent); pos.y = p1.y*b1(percent) + cp1.y*b2(percent) + cp2.y*b3(percent) + p2.y*b4(percent); return pos; } // get the angle in degrees between two points function getAngleDegrees(p1, p2){ return Math.atan2((p2.y - p1.y), (p2.x - p1.x)) * (180 / Math.PI); } function getAngleRadians(p1, p2){ return Math.atan2((p2.y - p1.y), (p2.x - p1.x)); } // find vector from length and angle function getVectorDegrees(length, degrees){ var radians = degrees * (Math.PI / 180) return { x:length * Math.cos(radians), y:length * Math.sin(radians) } } function getVectorRadians(length, radians){ return { x:length * Math.cos(radians), y:length * Math.sin(radians) } } // scale a vector to a new size function scaleVector(vector, length){ var len = Math.sqrt(vector.x * vector.x + vector.y * vector.y); vector.x /= len; vector.y /= len; vector.x *= length; vector.y *= length; } // find the squared distance between two points // this provide a number good for comparison, but faster function getDistanceSquare(p1, p2){ return ((p1.x - p2.x) * (p1.x - p2.x)) + ((p1.y - p2.y) * (p1.y - p2.y)); } // find distance between two points function getDistance(p1, p2){ return Math.sqrt(((p1.x - p2.x) * (p1.x - p2.x)) + ((p1.y - p2.y) * (p1.y - p2.y))); } // this function returns the slope and y-intercept of the line going thru x,y at angle degrees (as flash measures) function pointAngleSlope(point, angle) { var m = Math.tan(Math.PI * angle / 180); var b = point.x - point.y * m; return {m:m, b:b}; } // this function returns the slope and y-intercept of the line going thru x1,y1 and x2,y2 function pointPointSlope(p1, p2) { var m = (p1.y - p2.y) / (p1.x - p2.x); var b = p1.y - p1.x * m; return {m:m, b:b}; } // this function returns the x,y point at the intersection of lines with slope and y-intercept m1,b1 and m2,b2. function intersection(m1, b1, m2, b2) { if (m1 != m2) { var x = (b2 - b1) / (m1 - m2); var y = m1 * x + b1; return {x:x, y:y}; } else { return null; } } // use non-zero winding fill rule to determine if nodes are clockwise or counter-clickwise function isClockwise(nodes){ // find point inside path var avg = {x:0, y:0}; for(var i = 0; i < nodes.length; i++){ avg.x += nodes[i].x; avg.y += nodes[i].y; } avg.x /= nodes.length; avg.y /= nodes.length; // count winding var count = 0; for(var i = 0; i < nodes.length; i++){ var p1 = nodes[i]; var p2 = i < nodes.length - 1 ? nodes[i + 1] : nodes[0]; if(p1.x >= avg.x && p1.y < avg.y && p2.x >= avg.x && p2.y >= avg.y) count++; else if(p1.x >= avg.x && p1.y > avg.y && p2.x >= avg.x && p2.y <= avg.y) count--; } if(count == 0){ for(var i = 0; i < nodes.length; i++){ var p1 = nodes[i]; var p2 = i < nodes.length - 1 ? nodes[i + 1] : nodes[0]; if(p1.y >= avg.y && p1.x < avg.x && p2.y >= avg.y && p2.x >= avg.x) count--; else if(p1.y >= avg.y && p1.x > avg.x && p2.y >= avg.y && p2.x <= avg.x) count++; } } return count > 0; } /*function isClockwise2(nodes){ // start at first node var origin = {x:nodes[0].x, y:nodes[0].y}; // count how many times path crosses over ray from origin along x axis var count = 0; for(var i = 0; i < nodes.length; i++){ var p1 = nodes[i]; var p2 = i < nodes.length - 1 ? nodes[i + 1] : nodes[0]; if(p1.x > origin.x && p1.y <= origin.y && p2.x > origin.x && p2.y > origin.y) // crosses top to bottom count++; else if(p1.x > origin.x && p1.y >= origin.y && p2.x > origin.x && p2.y < origin.y) // crosses bottom to top count--; //else if(p1.y == p2.y) // tangent to ray, consider left/right direction instead //p1.x > p2.x ? count-- : count++; } // resolve if no intersection were found by considering initial node direction if(count == 0 && nodes.length > 1){ p1 = nodes[0]; p2 = nodes[1]; if(p1.y == p2.y) count = p1.x > p2.x ? -1 : 1; else count = p1.y < p2.y ? -1 : 1; } // -1 is counter-clockwise, +1 is clockwise return count > 0; }*/ function lineIntersection(line1Pt1, line1Pt2, line2Pt1, line2Pt2, as_seg) { var A = line1Pt1; var B = line1Pt2; var E = line2Pt1; var F = line2Pt2; var ip; var a1; var a2; var b1; var b2; var c1; var c2; a1= B.y-A.y; b1= A.x-B.x; c1= B.x*A.y - A.x*B.y; a2= F.y-E.y; b2= E.x-F.x; c2= F.x*E.y - E.x*F.y; var denom=a1*b2 - a2*b1; if (denom == 0) { return null; } ip={}; ip.x=(b1*c2 - b2*c1)/denom; ip.y=(a2*c1 - a1*c2)/denom; if(as_seg){ if(Math.pow(ip.x - B.x, 2) + Math.pow(ip.y - B.y, 2) > Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2)) return null; if(Math.pow(ip.x - A.x, 2) + Math.pow(ip.y - A.y, 2) > Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2)) return null; if(Math.pow(ip.x - F.x, 2) + Math.pow(ip.y - F.y, 2) > Math.pow(E.x - F.x, 2) + Math.pow(E.y - F.y, 2)) return null; if(Math.pow(ip.x - E.x, 2) + Math.pow(ip.y - E.y, 2) > Math.pow(E.x - F.x, 2) + Math.pow(E.y - F.y, 2)) return null; } return ip; }