Copy link to clipboard
Copied
I want my handwriting style fonts to look more realistic. Imagine writing the same words over and over like a gradeschool handwriting assignment. For example:
This is a test.
This is a test.
This is a test.
Even with handwritten style fonts, every letter, and therefore every line, will look identical. Is there a simple way to make each line/letter differ just a bit? It should look like the same person wrote it (same base font), but not be identical. Subtle variations would be the key.
I don't deal with huge volumes of text, so even applying changes to each individual line or letter would be acceptable in this case I suppose.
Thanks in advance! This one has been stumping me
Copy link to clipboard
Copied
Something you could experiment with.
Lay down your type layer and copy it.
Select the copy and go Type > Create Work Path
Ctrl click the original type layer to make it a selection and add a layer mask.
Now find a brush preset with a rough outline. I tried searching with Distressed which found the preset below from Kyle Megapack.
Make the brush the appropriate size, which was the smaller line below.
Now select the Layer Mask > right click the workpath, and with your foreground colour black, chose Stroke Path and select Brush.
This gives you a distressed edges look. You can try changing angle jitter and scatter in brush settings to change the outcome.
Alternatively, instead of using Type > Create work path, use Convert to Shape, and use the Direct Select tool to change the shape a letter at a time. I have exagerated for effect in the shot below
Copy link to clipboard
Copied
If you use Adobe Illustrator, you can try converting the text to paths (Type > Convert to Outlines) and then using the Effect > Distorrt & Transform > Roughen filter to vary all the outlines at once. You can also try combining this with other Illustrator path filters.
Roughen is a path filter, which Photoshop does not have, so it can act on the vector outline paths and their points. In Photoshop, you have to try either of the approaches Trevor showed: Either convert the text to pixels and figure out a way to push them around with a brush or filter, or convert the characters to shapes and manually drag individual path points.
Copy link to clipboard
Copied
Which font/s?
Some fonts offer multiple glyphs for some letters.
Copy link to clipboard
Copied
@c.pfaffenbichler has the more straightforward answer. Some typefaces have alternates, meaning that you can have more than one e, a, you'll find them all in the glypth panel. This specific type of font is called a contextual cursive font, meaning that you can change individual letters within a set. You even may have some fonts that change letters at random (it's called "rand" in some applications, pretty sure Photoshop doesn't support it)
Does it help? Probably no 😉
But if you want to go the ready made route, then you'll know what to look for.
Copy link to clipboard
Copied
I stand corrected, apparently it does
https://typedrawers.com/discussion/3448/random-glyph-variation-to-mimic-handwritten-font
Scroll down to the end and someone suggests a font that just does that: Caveat by Google fonts
(Disclaimer: I haven't tested it)
Copy link to clipboard
Copied
This might be an interesting scripting project. Some text could be converted to a shape. You could then use a script to record all the anchor points and handles. While you can't edit the shape via script, you can redraw it, and then use a random number on each anchor and handle to slightly change it.
Copy link to clipboard
Copied
That would be an interesting project … but I suspect such randomization would not do full justice to handwriting as it would work on the »contour« of the letters, not the »middle line/stroke«.
Copy link to clipboard
Copied
That would be an interesting project … but I suspect such randomization would not do full justice to handwriting as it would work on the »contour« of the letters, not the »middle line/stroke«.
By @c.pfaffenbichler
Well, that's what I was thinking might be better: just having the curves slightly different, not handwriting that looks like you have the shakes. A slightly different flow.
Copy link to clipboard
Copied
I decided to try this, but I ran into a snag. It seems when you make a shape out of a text layer, Photoshop combines the the paths in each letter, so the cutout in the letter "b" is considered one path. But using a script can't detect the inside, subtracted path, just the points on the outside of the letter.
Copy link to clipboard
Copied
Are you using DOM-code to collect the subPathItems?
AM-code should work out here.
Copy link to clipboard
Copied
Yes, I was. I will have to figure out AM code, still not to savy with it.
Copy link to clipboard
Copied
This code is a bit of a hodgepodge but it should serve as an illustration.
It collects the info from the first path in an Array and creates a new Path with two times the elements (scaled and rotated).
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.POINTS;
var myDocument = activeDocument;
var theWidth = myDocument.width;
var theHeight = myDocument.height;
var thisPath = collectPathInfoFromDesc2012 (myDocument, myDocument.pathItems[0]);
alert (thisPath.join("\n"));
//createPath2022(anArray, Math.random());
createPathWithOffsetRotatedMultiples(thisPath, Math.random(), [[0, 0, Math.random()*360, 60+80*Math.random(), false, false], [0,0, Math.random()*360, 60+80*Math.random(), false, false]]);
app.preferences.rulerUnits = originalRulerUnits;
////// collect path info from actiondescriptor, smooth added //////
function collectPathInfoFromDesc2012 (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();
for (var l = 0; l < myDocument.pathItems.length; l++) {
var thisPath = myDocument.pathItems[l];
if (thisPath == thePath && thisPath.kind == PathKind.WORKPATH) {
ref.putProperty(idPath, cTID("WrPt"));
};
if (thisPath == thePath && thisPath.kind != PathKind.WORKPATH && thisPath.kind != PathKind.VECTORMASK) {
ref.putIndex(idPath, l + 1);
};
if (thisPath == thePath && thisPath.kind == PathKind.VECTORMASK) {
var idvectorMask = stringIDToTypeID( "vectorMask" );
ref.putEnumerated( idPath, idPath, idvectorMask );
};
};
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;
};
////// create a path from collectPathInfoFromDesc2012-array //////
function createPath2022(theArray, thePathsName) {
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.putProperty(cTID('Path'), cTID('WrPt'));
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);
// name work path;
var desc30 = new ActionDescriptor();
var ref6 = new ActionReference();
var idPath = charIDToTypeID( "Path" );
ref6.putClass( idPath );
desc30.putReference( charIDToTypeID( "null" ), ref6 );
var ref7 = new ActionReference();
ref7.putProperty( idPath, charIDToTypeID( "WrPt" ) );
desc30.putReference( charIDToTypeID( "From" ), ref7 );
desc30.putString( charIDToTypeID( "Nm " ), thePathsName );
executeAction( charIDToTypeID( "Mk " ), desc30, DialogModes.NO );
/// get index;
var ref = new ActionReference();
ref.putProperty(stringIDToTypeID("property"), stringIDToTypeID("itemIndex"));
ref.putEnumerated( charIDToTypeID("Path"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") );
var pathDesc = executeActionGet(ref);
app.preferences.rulerUnits = originalRulerUnits;
return pathDesc.getInteger(stringIDToTypeID("itemIndex"))
};
////// create a path from collectPathInfoFromDesc2012-array //////
////// create a path with multiple scaled and rotated elements //////
function createPathWithOffsetRotatedMultiples(theArray, thePathsName, theOffsets) {
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.putProperty(cTID('Path'), cTID('WrPt'));
desc1.putReference(sTID('null'), ref1);
var list1 = new ActionList();
for (var x = 0; x < theOffsets.length; x++) {
var thisOffset = theOffsets[x];
if (thisOffset[4] == true) {var theHor = Math.round(Math.random())*2-1};
else {var theHor = 1};
if (thisOffset[5] == true) {var theVer = Math.round(Math.random())*2-1}
else {var theVer = 1};
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 = [scaleRotatePoint (thisSubPath[n][0], thisOffset[2], thisOffset[3], theHor, theVer), scaleRotatePoint (thisSubPath[n][1], thisOffset[2], thisOffset[3], theHor, theVer), scaleRotatePoint (thisSubPath[n][2], thisOffset[2], thisOffset[3], theHor, theVer), thisSubPath[n][3]];
var desc4 = new ActionDescriptor();
var desc5 = new ActionDescriptor();
desc5.putUnitDouble(cTID('Hrzn'), cTID('#Rlt'), thisPoint[0][0]+thisOffset[0]);
desc5.putUnitDouble(cTID('Vrtc'), cTID('#Rlt'), thisPoint[0][1]+thisOffset[1]);
desc4.putObject(cTID('Anch'), cTID('Pnt '), desc5);
var desc6 = new ActionDescriptor();
desc6.putUnitDouble(cTID('Hrzn'), cTID('#Rlt'), thisPoint[1][0]+thisOffset[0]);
desc6.putUnitDouble(cTID('Vrtc'), cTID('#Rlt'), thisPoint[1][1]+thisOffset[1]);
desc4.putObject(cTID('Fwd '), cTID('Pnt '), desc6);
var desc7 = new ActionDescriptor();
desc7.putUnitDouble(cTID('Hrzn'), cTID('#Rlt'), thisPoint[2][0]+thisOffset[0]);
desc7.putUnitDouble(cTID('Vrtc'), cTID('#Rlt'), thisPoint[2][1]+thisOffset[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);
// name work path;
var desc30 = new ActionDescriptor();
var ref6 = new ActionReference();
var idPath = charIDToTypeID( "Path" );
ref6.putClass( idPath );
desc30.putReference( charIDToTypeID( "null" ), ref6 );
var ref7 = new ActionReference();
ref7.putProperty( idPath, charIDToTypeID( "WrPt" ) );
desc30.putReference( charIDToTypeID( "From" ), ref7 );
desc30.putString( charIDToTypeID( "Nm " ), thePathsName );
executeAction( charIDToTypeID( "Mk " ), desc30, DialogModes.NO );
// get index;
var ref = new ActionReference();
ref.putProperty(stringIDToTypeID("property"), stringIDToTypeID("itemIndex"));
ref.putEnumerated( charIDToTypeID("Path"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") );
var pathDesc = executeActionGet(ref);
/*var myPathItem = activeDocument.pathItems[pathDesc.getInteger(stringIDToTypeID("itemIndex")) - 1];
return myPathItem;*/
app.preferences.rulerUnits = originalRulerUnits;
return pathDesc.getInteger(stringIDToTypeID("itemIndex"))
};
////// rotate and scale point //////
function scaleRotatePoint (thePoint, theAngle, theScale, flipHor, flipVer) {
var theDist = getDistance ([0,0], thePoint);
var newAngle = getAngle ([0,0], thePoint)+theAngle;
var newX = Math.cos(radiansOf (newAngle))*theDist*theScale/100*flipHor;
var newY = Math.sin(radiansOf (newAngle))*theDist*theScale/100*flipVer;
return [newX, newY]
};
////// get a distance between two points //////
function getDistance (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));
return sideC
};
////// 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
};
Copy link to clipboard
Copied
Thanks for the code. I'll have to play around with it.
Copy link to clipboard
Copied
I did play with the code a bit. There is still an issue. While it does get all the points, once a shape is merged, the subtracted path from the area inside a letter shows as an operation of add rather than subtract. So reconstructing the shape, you don't know what paths need to be subtracted and which need to be added. I suppose a work around, migh be to use a combination of DOM and AM, but thinking about that is making my old head spin.
Copy link to clipboard
Copied
Depending on the actual intention even the Filter Displace (with a 2 Channel Cloud Displacement Map) might prove useful.