Skip to main content
Participant
June 9, 2020
Answered

Adding arrowhead to line drawn with curvature pen

  • June 9, 2020
  • 5 replies
  • 7806 views

I'm sure this is very easy but as a new user I'm struggling to find how to do it.

 

I want to draw a curved line using the curvature pen tool and add an arrowhead to one end. What is the best way?

Correct answer c.pfaffenbichler

This is pure GOLD, that you so much @c.pfaffenbichler !! I was sick for a few weeks but now I'm back to try this and I made a shortcut to it and it simply works, you are genius! Please Adobe give this person credit! brilliant! I will look deeper into this script stuff, never tried this but this is powerful stuff! Code! whoa!


A piece of advice regarding Photoshop Scripting: 

This Script utilizes ExtendScript and this is essentially on legacy-status (without a definitive timeline, but it might at some time be removed from Photoshop altogether). 

So if you want to dig into Scripting you may want to focus on the newer UXP Scripting right away. 

5 replies

Flowgun
Inspiring
July 28, 2025

paint.NET is great for drawing arrows and it's free and extremely lightweight. it even head dynamic gradients way before photoshop. Adobe can learn a thing or two from that app.

c.pfaffenbichler
Community Expert
Community Expert
February 16, 2024

A Script can create an »arrowhead« for a stroked Shape Layer. 

// create arrowhead for shape layer with stroke and one subpathitem;
// 2024, use it at your own risk;
if (app.documents.length > 0) {
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
var myDocument = app.activeDocument;
// check stroke;
try {
var ref = new ActionReference();
ref.putEnumerated( charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") ); 
var layerDesc = executeActionGet(ref);
var theStroke = layerDesc.getObjectValue(stringIDToTypeID("AGMStrokeStyleInfo"));
var theStrokeWidth = theStroke.getUnitDoubleValue(stringIDToTypeID("strokeStyleLineWidth"));
// if stroked shape layer is selected;
var thePath = collectPathInfoFromDesc2023 (myDocument, myDocument.pathItems[myDocument.pathItems.length-1]);
if (thePath.length == 1) {
var thePoint = thePath[0][thePath[0].length - 3];
if (thePoint[0][0] != thePoint[2][0] && thePoint[0][1] != thePoint[2][1]) {var thePoint2 = thePoint[2]}
else {var thePoint2 = thePath[0][thePath[0].length - 4][1]};
var thePoint1 = thePoint[0];
var theAngle = getAngle (thePoint1, thePoint2);
var thePointBefore = createPointAtAngleAndDistance(thePoint1, theAngle-45, theStrokeWidth);
var thePointAfter = createPointAtAngleAndDistance(thePoint1, theAngle+45, theStrokeWidth);
// create arrowhead;
var theArray = [[[thePointBefore, thePointBefore, thePointBefore, false], [thePoint1, thePoint1, thePoint1, false], [thePointAfter, thePointAfter, thePointAfter, false],false,1097098272]];
var theX = rectangleShapeLayerToPolygon(theArray, theStroke);
} else {alert ("please select a stroked shape layer with only one subPathItem")};
} catch (e) {alert ("please select a stroked shape layer with only one subPathItem")};
app.preferences.rulerUnits = originalRulerUnits;
};
////////////////////////////////////
////// create shape layer and then change the path to maintain an open path //////
function rectangleShapeLayerToPolygon (theArray, theStroke) {
var idpixelsUnit = stringIDToTypeID( "pixelsUnit" );
var idcontentLayer = stringIDToTypeID( "contentLayer" );
// =======================================================
    var desc254 = new ActionDescriptor();
    var idnull = stringIDToTypeID( "null" );
        var ref5 = new ActionReference();
        ref5.putClass( idcontentLayer );
    desc254.putReference( idnull, ref5 );
    var idusing = stringIDToTypeID( "using" );
        var desc255 = new ActionDescriptor();
        var idtype = stringIDToTypeID( "type" );
            var desc256 = new ActionDescriptor();
            var idcolor = stringIDToTypeID( "color" );
                var desc257 = new ActionDescriptor();
                var idred = stringIDToTypeID( "red" );
                desc257.putDouble( idred, 0.000000 );
                var idgrain = stringIDToTypeID( "grain" );
                desc257.putDouble( idgrain, 0.000000 );
                var idblue = stringIDToTypeID( "blue" );
                desc257.putDouble( idblue, 0.000000 );
            desc256.putObject( idcolor,  stringIDToTypeID( "RGBColor" ), desc257 );
        desc255.putObject( idtype, stringIDToTypeID( "solidColorLayer" ), desc256 );
        var idshape = stringIDToTypeID( "shape" );
            var desc258 = new ActionDescriptor();
            var idunitValueQuadVersion = stringIDToTypeID( "unitValueQuadVersion" );
            desc258.putInteger( idunitValueQuadVersion, 1 );
            var idtop = stringIDToTypeID( "top" );
            desc258.putUnitDouble( idtop, idpixelsUnit, 1111.492188 );
            var idleft = stringIDToTypeID( "left" );
            desc258.putUnitDouble( idleft, idpixelsUnit, 413.148438 );
            var idbottom = stringIDToTypeID( "bottom" );
            desc258.putUnitDouble( idbottom, idpixelsUnit, 1827.453125 );
            var idright = stringIDToTypeID( "right" );
            desc258.putUnitDouble( idright, idpixelsUnit, 1107.109375 );
            var idtopRight = stringIDToTypeID( "topRight" );
            desc258.putUnitDouble( idtopRight, idpixelsUnit, 0.000000 );
            var idtopLeft = stringIDToTypeID( "topLeft" );
            desc258.putUnitDouble( idtopLeft, idpixelsUnit, 0.000000 );
            var idbottomLeft = stringIDToTypeID( "bottomLeft" );
            desc258.putUnitDouble( idbottomLeft, idpixelsUnit, 0.000000 );
            var idbottomRight = stringIDToTypeID( "bottomRight" );
            desc258.putUnitDouble( idbottomRight, idpixelsUnit, 0.000000 );
        var idrectangle = stringIDToTypeID( "rectangle" );
        desc255.putObject( idshape, idrectangle, desc258 );
        desc255.putObject( stringIDToTypeID( "strokeStyle" ), stringIDToTypeID( "strokeStyle" ), theStroke );
    desc254.putObject( idusing, idcontentLayer, desc255 );
executeAction( stringIDToTypeID( "make" ), desc254, DialogModes.NO );
////////////////////////////////////
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
// thanks to xbytor;
cTID = function(s) { return app.charIDToTypeID(s); };
sTID = function(s) { return app.stringIDToTypeID(s); };
var desc1 = new ActionDescriptor();
var ref1 = new ActionReference();
ref1.putEnumerated( cTID('Path'), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") ); 
desc1.putReference(sTID('null'), ref1);
var list1 = new ActionList();
for (var m = 0; m < theArray.length; m++) {
var thisSubPath = theArray[m];
var desc2 = new ActionDescriptor();
desc2.putEnumerated(sTID('shapeOperation'), sTID('shapeOperation'), thisSubPath[thisSubPath.length - 1]);
var list2 = new ActionList();
var desc3 = new ActionDescriptor();
desc3.putBoolean(cTID('Clsp'), thisSubPath[thisSubPath.length - 2]);
var list3 = new ActionList();
for (var n = 0; n < thisSubPath.length - 2; n++) {
var thisPoint = thisSubPath[n];
var desc4 = new ActionDescriptor();
var desc5 = new ActionDescriptor();
desc5.putUnitDouble(cTID('Hrzn'), cTID('#Rlt'), thisPoint[0][0]);
desc5.putUnitDouble(cTID('Vrtc'), cTID('#Rlt'), thisPoint[0][1]);
desc4.putObject(cTID('Anch'), cTID('Pnt '), desc5);
var desc6 = new ActionDescriptor();
desc6.putUnitDouble(cTID('Hrzn'), cTID('#Rlt'), thisPoint[1][0]);
desc6.putUnitDouble(cTID('Vrtc'), cTID('#Rlt'), thisPoint[1][1]);
desc4.putObject(cTID('Fwd '), cTID('Pnt '), desc6);
var desc7 = new ActionDescriptor();
desc7.putUnitDouble(cTID('Hrzn'), cTID('#Rlt'), thisPoint[2][0]);
desc7.putUnitDouble(cTID('Vrtc'), cTID('#Rlt'), thisPoint[2][1]);
desc4.putObject(cTID('Bwd '), cTID('Pnt '), desc7);
desc4.putBoolean(cTID('Smoo'), thisPoint[3]);
list3.putObject(cTID('Pthp'), desc4);
};
desc3.putList(cTID('Pts '), list3);
list2.putObject(cTID('Sbpl'), desc3);
desc2.putList(cTID('SbpL'), list2);
list1.putObject(cTID('PaCm'), desc2);
};
desc1.putList(cTID('T   '), list1);
executeAction(cTID('setd'), desc1, DialogModes.NO);
app.preferences.rulerUnits = originalRulerUnits;
};
////// collect path info from actiondescriptor, indices start at 1, not 0 //////
function collectPathInfoFromDesc2023 (myDocument, thePath) {
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.POINTS;
// based of functions from xbytor’s stdlib;
var idPath = charIDToTypeID( "Path" );
var ref = new ActionReference();
// check if thePath is an index or a dom-path;
if (thePath.constructor == Number) {
ref.putIndex(idPath, thePath)
}
else {
thePath.select();
var ref = new ActionReference();
ref.putEnumerated( charIDToTypeID( "Path" ), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") );
};
// get stuff;
var desc = app.executeActionGet(ref);
var pname = desc.getString(cTID('PthN'));
// create new array;
var theArray = new Array;
var pathComponents = desc.getObjectValue(cTID("PthC")).getList(sTID('pathComponents'));
// for subpathitems;
for (var m = 0; m < pathComponents.count; m++) {
    var listKey = pathComponents.getObjectValue(m).getList(sTID("subpathListKey"));
    var operation1 = pathComponents.getObjectValue(m).getEnumerationValue(sTID("shapeOperation"));
    switch (operation1) {
        case 1097098272:
        var operation = 1097098272 //cTID('Add ');
        break;
        case 1398961266:
        var operation = 1398961266 //cTID('Sbtr');
        break;
        case 1231975538:
        var operation = 1231975538 //cTID('Intr');
        break;
        default:
//		case 1102:
        var operation = sTID('xor') //ShapeOperation.SHAPEXOR;
        break;
        };
// for subpathitem’s count;
    for (var n = 0; n < listKey.count; n++) {
        theArray.push(new Array);
        var points = listKey.getObjectValue(n).getList(sTID('points'));
        try {var closed = listKey.getObjectValue(n).getBoolean(sTID("closedSubpath"))}
        catch (e) {var closed = false};
// for subpathitem’s segment’s number of points;
        for (var o = 0; o < points.count; o++) {
            var anchorObj = points.getObjectValue(o).getObjectValue(sTID("anchor"));
            var anchor = [anchorObj.getUnitDoubleValue(sTID('horizontal')), anchorObj.getUnitDoubleValue(sTID('vertical'))];
            var thisPoint = [anchor];
            try {
                var left = points.getObjectValue(o).getObjectValue(cTID("Fwd "));
                var leftDirection = [left.getUnitDoubleValue(sTID('horizontal')), left.getUnitDoubleValue(sTID('vertical'))];
                thisPoint.push(leftDirection)
                }
            catch (e) {
                thisPoint.push(anchor)
                };
            try {
                var right = points.getObjectValue(o).getObjectValue(cTID("Bwd "));
                var rightDirection = [right.getUnitDoubleValue(sTID('horizontal')), right.getUnitDoubleValue(sTID('vertical'))];
                thisPoint.push(rightDirection)
                }
            catch (e) {
                thisPoint.push(anchor)
                };
            try {
                var smoothOr = points.getObjectValue(o).getBoolean(cTID("Smoo"));
                thisPoint.push(smoothOr)
                }
            catch (e) {thisPoint.push(false)};
            theArray[theArray.length - 1].push(thisPoint);
            };
        theArray[theArray.length - 1].push(closed);
        theArray[theArray.length - 1].push(operation);
        };
    };
// by xbytor, thanks to him;
function cTID (s) { return cTID[s] || cTID[s] = app.charIDToTypeID(s); };
function sTID (s) { return sTID[s] || sTID[s] = app.stringIDToTypeID(s); };
// reset;
app.preferences.rulerUnits = originalRulerUnits;
return theArray;
};
////// get an angle, 3:00 being 0˚, 6:00 90˚, etc. //////
function getAngle (pointOne, pointTwo) {
// calculate the triangle sides;
var width = pointTwo[0] - pointOne[0];
var height = pointTwo[1] - pointOne[1];
var sideC = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); 
// calculate the angles;
if (width+width > width) {theAngle = Math.asin(height / sideC) * 360 / 2 / Math.PI}
else {theAngle = 180 - (Math.asin(height / sideC) * 360 / 2 / Math.PI)};
if (theAngle < 0) {theAngle = (360 + theAngle)};
//	if (theAngle > 180) {theAngle = (360 - theAngle) * (-1)};
return theAngle
};
////// radians //////
function radiansOf (theAngle) {
return theAngle * Math.PI / 180
};
////// rotate and scale point //////
function createPointAtAngleAndDistance (thePoint, theAngle, theDist) {
var newX = thePoint[0] + Math.cos(radiansOf (theAngle))*theDist;
var newY = thePoint[1] + Math.sin(radiansOf (theAngle))*theDist;
return [newX, newY]
};
Participating Frequently
February 18, 2024

Hi @c.pfaffenbichler whoaaaa this is incredible!! I want to use this script, may I? And how do I go about saving this code as a script/action? I there a guide somewhere? I looked a bit online but didn't find a clear walk-thru for this kind of scenario, can you maybe point me in the right direction? Thank you for looking into this!! So wish Adobe would look at this an just implement it for everybody

c.pfaffenbichler
Community Expert
Community Expert
February 19, 2024

Save the code as a txt-file (Simpletxt), change the extension to .jsx and copy it into Photoshop’s Presets/Scripts-Folder; after restarting Photoshop it should be available under File > Scripts and can be assigned a Shortcut or used in an Action. 

JJMack
Community Expert
Community Expert
June 9, 2020

The easiest way in Photoshop would be to use a legacy custom arrow shape or the line tool set  for adding an arrow head.  Then use the curvature pen tool on the path Photoshop tool laid out.

JJMack
Participant
June 10, 2020

Thank you, all. I'm rather surprised that this fairly basic function needs what are in effect workarounds.

c.pfaffenbichler
Community Expert
Community Expert
June 10, 2020

You might be confusing Photoshop with a vector-oriented application like Illustrator. 

davescm
Community Expert
Community Expert
June 9, 2020

You would need to draw your curve first then use the line tool to add a short arrow on the end

 

Dave

Participating Frequently
January 27, 2024

@davescmyou still think it's normal we use this workaround in 2024? How do we push this topic to the top? It's driving me crazy for YEARS

davescm
Community Expert
Community Expert
January 27, 2024

Since you ask me in particular - if I want vector drawing tools I use Illustrator. I keep Photoshop vectors for paths to make selections/masks.

 

Dave

c.pfaffenbichler
Community Expert
Community Expert
June 9, 2020

I am afraid for this you should turn to Illustrator (or other vector oriented applications).