Coding my Handwriting
A familiar theme for me is dismissing an idea as being too much work and then later finding myself doing it anyway. That’s what happened here.
A little while ago I created a block script in JavaScript, thinking that cursive would be too complex. But here I am, two months later, ready to talk about the cursive handwriting I’ve created. There is perhaps a lesson in that but let’s not dwell on it.
Block script
This previous article is about my block printed version of the alphabet. As a summary, I created it by:
Writing code to define key points in each letter’s paths (~10 points per letter).
Smoothing those paths using Chaikin’s curve algorithm.
Turning the path into a shape for variable thickness along the length.
Draw the shape paths using p5js.
It looked like this:
By the way, an article about my system for generating these sentences is coming soon, sign up to my newsletter to hear about it.
Defining the original paths for those letters was a very manual process of writing their positions into the code and then nudging the points back and forth until the letters looked right. When it came to coding cursive, I streamlined the process.
Designing Letters
In the p5js editor, for easy access, I created a tool to define and output the key points in the paths.
It displays a sample letter (for scale and context) next to an area in which to design the new letter with these steps:
Click to place key points for the path - the resultant Chaikin-curved path is shown.
Tap ‘p’ to switch to editing mode.
Select points and drag them into position.
Tap ‘enter’ to output the path to the console.
I created 2-3 options for each letter.
The path that results looks like this:
[{x:0.7,y:22.5},{x:8.2,y:18.1},{x:8.9,y:11.2},{x:3.7,y:11.4},{x:1.7,y:18.9},{x:8.4,y:22.4},{x:17.7,y:22.0}]
I wanted to use my own handwriting as a guide, so I wrote out a range of examples of lower and uppercase letters and loaded the image directly into my letter building tool for tracing.
The w/a/s/d keys are used to place the image in the right spot and r/e zooms the image in and out. The blurry ‘e’ you see in the gif above is the sample image.
The numbers noted on the paper are the x y coordinates to get that area to be in the letter creation window.
After creating all the paths, curving them and turning them into shapes with variable width, (check the previous article for more detail), here’s how the characters look individually.
Cursive-ifying, Cursifying (?)
Sometimes joining letters is easy, you just go straight from one path of key points to the next before Chaikin curving them all in one go. But some letter pairs do not work nicely together.
Consider the letter pair na. In red we can see the last point of the letter n, which is low, and in green, the first point of the letter a, which is high. This causes the joining path to go diagonally through the a, making it look a bit like an e.
Meanwhile in the pair ti, the t ends just above the baseline and the i starts on it, causing an unnatural ridge.
To fix these issues we can add an extra point to the start of the a, and delete the last two points on the t.
But we can’t just change the letters like that for all scenarios.
For example, if the a is at the start of a word, the additional point will be out of place and if the a preceded by a letter like w, it creates a line that crosses through the a in a different way. If the t is paired with a k, it becomes deformed.
The points at the beginning and ends of letter paths need to vary depending on which other letters they are next to.
At first I tried calling out particular “problem” pairs and writing rules for them specifically but, in the end, I added a single number to the beginning and end of each path which states if it:
Cannot join another letter (0)
Joins another letter around the base line (1)
Joins another letter just above the base line (2)
Joins another letter around the x-height (3)
Here are some examples:
Each letter path now looks something like this, note the single digits at the beginning and end:
[0,{x:12.2,y:13.2},{x:13.5,y:11.0},{x:6.2,y:8.4},{x:1.1,y:13.0},{x:1.8,y:19.0},{x:7.0,y:23.4},{x:15.2,y:23.6},{x:18.4,y:22.1},1],
I tested all of the letter pairs, like so:
Here you can also see some of the variation, created by having multiple paths for each letter and also by editing the letters depending on what letter they are next to. Ideally I would have at least 5 or 6 options of paths for each letter but there is a balance to be drawn against file size.
Creating words
When a word is created:
a basic path is chosen for each letter from the 2-3 different options for that character.
the information about the ends of the paths are passed to the adjacent letters
(the letter paths all have to be chosen first as, in some cases, different path options for the same letter have different end points)The basic paths are adjusted in response to their neighbours.
E.g. if the previous letter’s end height is 2, remove 1 point from the start of this path, or if the next letter’s start height is 1, add an additional point in a certain location.
The adjustment functions can get a bit complicated, for example here is the one for the letter q:
// ip = path
// pc = previous char's end info
// nc = next char's start info
// n = index of path that was chosen for this letter
adjust: (ip, pc, nc, n) => {
// randomly adds in a break at the end for 70% of this letter
if (rand() < 0.7 ) ip.splice(-1, 1, 0);
// if [2] was chosen for this path from the 4 options,
if (n < 2) {
// Swap out first two points for a different point if the previous char ends at 3
if (pc == 3) ip.splice(1, 2, {x:10,y:12});
// Otherwise, as long as it's not a 0, add a point at the beginning
else if (pc > 0) ip.splice(1, 0, {x:10,y:20});
}
// If there's no break (0) between this character and the next
if (nc > 0 && ip[ip.length-1] != 0){
// Swap out the last two points for a different one
ip.splice(-3, 2, {x:16,y:34})
}
}
But often they are fairly short, here is the one for the letter n:
adjust: (ip, pc, nc) => {
// If the next letter starts at a 3, randomly either create a break or move the last point
if (nc == 3) rand() < 0.3 ? ip.splice(-1, 1, 0) : ip.splice(-2, 1, {x:17,y:23.8})
}
Next the basic paths for all the letters are joined together. While doing this, it ignores 1, 2 and 3’s in the letter paths but whenever there is a 0 it creates a break by starting a new path.
After curving those paths, turning them into varied width shapes, and adding some jittering around using Perlin noise, here’s what the cursive writing looks like.
An article about generating these sentences will be coming soon, you can sign up to my newsletter to get a heads up when it’s out.
For fun, here’s a side by side comparison of the coded handwriting run through my plotter, next to my actual handwriting.
WHAT DOES IT WEIGH?
The letter class for the block print was 9.7kb. The letter class for the cursive handwriting (after being run through a minifier) is currently 26.1kb.
This one is larger because there are multiple paths for each letter as well as the function for adjust the points to meet the letter’s neighbours, but I have made some other savings. I’m sure further savings could be made - I am not a code golf wizard but I have a few ideas.
For example, currently the letters are designed around a default font size of 20 and then resized, meaning lots of the points are defined as e.g. x: 14.5, but if I switch this to a default size of 200, the point could be defined as 145, removing one character (the decimal place). I need to make this change carefully, so it’s on the To Do list for later.
How I’m Using it
The main purpose for this handwriting is for the titles, labels and scribbled notes on these diagrams I’ve been working on. But I’m also have a lot of fun playing around with the text itself.
One of the best things about having encoded paths instead of using a font is that I can mess around with those paths. Changing the position of letters and changing the thickness across an individual letter and so on.
Next up I’m going to be incorporating the handwriting into those diagrams, but I’m definitely intending to come back and create something focused on the text itself as well, as I’m finding it super beautiful and there’s a lot of possibility there!
😍 Enjoyed this article? I’d love it if you could give a boost on Twitter, thanks!
✨ And don’t forget to sign up for my weekly newsletter, filled with updates.