• Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
    Dedicated community for Japanese speakers
  • 한국 커뮤니티
    Dedicated community for Korean speakers
Exit
1

How to Make Subtle Changes to Font to Appear Legitimately Handwritten

New Here ,
Apr 04, 2023 Apr 04, 2023

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

TOPICS
Actions and scripting , Windows

Views

5.1K

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe
Community Expert ,
Apr 04, 2023 Apr 04, 2023

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.

 

image.png

 

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.

image.png

 

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

image.png

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 05, 2023 Apr 05, 2023

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.

 

Illustrator Roughen filter on type.gif

 

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.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 05, 2023 Apr 05, 2023

Copy link to clipboard

Copied

Which font/s? 

Some fonts offer multiple glyphs for some letters. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 05, 2023 Apr 05, 2023

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.


Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 05, 2023 Apr 05, 2023

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)

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 05, 2023 Apr 05, 2023

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.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 05, 2023 Apr 05, 2023

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«. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 05, 2023 Apr 05, 2023

Copy link to clipboard

Copied

quote

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.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 07, 2023 Apr 07, 2023

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.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 07, 2023 Apr 07, 2023

Copy link to clipboard

Copied

Are you using DOM-code to collect the subPathItems?

AM-code should work out here. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 07, 2023 Apr 07, 2023

Copy link to clipboard

Copied

Yes, I was. I will have to figure out AM code, still not to savy with it.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 07, 2023 Apr 07, 2023

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). 

Screenshot 2023-04-07 at 14.04.22.pngScreenshot 2023-04-07 at 14.07.08.png

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
	};

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 07, 2023 Apr 07, 2023

Copy link to clipboard

Copied

Thanks for the code. I'll have to play around with it. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 07, 2023 Apr 07, 2023

Copy link to clipboard

Copied

LATEST

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.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 07, 2023 Apr 07, 2023

Copy link to clipboard

Copied

Depending on the actual intention even the Filter Displace (with a 2 Channel Cloud Displacement Map) might prove useful. 

Screenshot 2023-04-07 at 16.27.26.pngScreenshot 2023-04-07 at 16.27.32.png

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines