Create a personal font

The illustrations I work on often require text. But I don't want to use ready-made fonts. When working with illustrations in a browser, using a “web-safe” font can lead to unpredictable results, and in general, using external fonts can greatly increase the file size of the illustration. Also, I don't want the visual element of the project to be created by someone else.

Instead of using ready-made fonts, I created my own using p5.js and JavaScript.

How do fonts work anyway?

I started by learning what the different points of the font are called.

Huge respect to whoever decided that one of the y-heights of a font should be designated as

Huge respect to whoever decided that one of the y-heights of a font should be designated as “x-height”

I decided to build my letters around a center point, mid_x and mid_y. In retrospect, I realize that it would have been better to work from the bottom left point, and I will do that someday to improve the kerning.

In the Letter class I defined the height xheight cap etc., depending on the font size and midpoint. For example, the full height is from base to cap equals the font size. The distance from y_mid to y_x is 1/3 of the full height.

I also identified a few small distances by which the point could be adjusted relative to the height.

this.adj_1 = this.h_full * 0.05;
this.adj_15 = this.h_full * 0.075;
this.adj_2 = this.h_full * 0.1;
this.adj_25 = this.h_full * 0.125;
this.adj_3 = this.h_full * 0.15;
this.adj_35 = this.h_full * 0.175;
this.adj_4 = this.h_full * 0.2;

Defining the letters

Each letter is defined by a set of outlines made up of several dots. Creating these outlines was a process of placing them in the right places. I used the font as a guide, and also wrote the letters on paper to see how they “should” look.

The result is uneven scribbles.

create_a(){   
  this.paths = [
    [ // stem
      {x: this.x_left+this.adj_2, y: this.y_x + this.adj_4},
      {x: this.x_left+this.adj_3, y: this.y_x + this.adj_1},
      {x: this.x_mid+this.adj_2, y: this.y_x},
      {x: this.x_right, y: this.y_x+this.adj_2},
      {x: this.x_right, y: this.y_base-this.adj_4},
      {x: this.x_right+this.adj_1, y: this.y_base},
    ],
    [ // round
      {x: this.x_right-this.adj_1, y: this.y_mid-this.adj_15},
      {x: this.x_mid-this.adj_1, y: this.y_mid-this.adj_1},
      {x: this.x_left, y: this.y_mid+this.adj_35},          
      {x: this.x_mid, y: this.y_base},
      {x: this.x_mid+this.adj_2, y: this.y_base-this.adj_1},
      {x: this.x_right+this.adj_2, y: this.y_base - this.adj_4},
    ]
  ];
}

Working with curves

The next step is to smooth the trajectories using the Chaikin curve algorithm. Let's see how it works.

Chaikin's algorithm works recursively, and in each round we create a new path by doing the following:

  • Copy the first point

  • For the remaining points before the last point:

    • Add a point offset by 25% to the previous point

    • Let's add a point offset 25% of the way to the next point.

  • Copy the last point.

After one pass we got this. The new path is marked in red.

We repeat the procedure with what we got. Here are the results after 2 and 3 runs.

And the final result.

Not bad. Let's try the same thing with letters. After the first run:

After the third:

Looks good:

3-4 iterations of the algorithm are enough to get a nice curve for small character sizes. If the font is large (with dots located further from each other), then you just need to perform more iterations.

Minimize it

Defining the outlines relative to the mid, cap, etc. points helped me understand how to draw the letter. For example, it was easier for me to think “the base of the b starts at the cap and goes to the base” rather than “the base of the b starts 14.1 pixels above the mid and ends 7.4 pixels below it.”

However, the resulting code was not very neat (this can be seen above in the function create_a). So I wrote a function that looked at each letter and generated a new code, translating everything into simple numeric values.

// Get string of new code
    let string = "";
    for(let l of this.letters){
      string += "create_" + l.letter + "(){\n";
      string += "  this.ip = [\n";
      for(let path of l.ip){
        string += "  [";
        for(let p of path){
          string += "{x: " + nf(p.x, 0, 1) + ", y: " + nf(p.y, 0, 1)  + "}";
          if (path.indexOf(p) != path.length-1) string += ", ";
        }
        string += "]";
        if (l.ip.indexOf(path) != l.ip.length-1) string += ",\n";
        else string += "\n";
      }
      string += "  ]\n";
      string += "}\n";
    }
    console.log(string)

Here is the new code for the letter A. Much more concise and simpler.

create_a(){
  this.ip = [
    [{x: -2.8, y: -3.4}, {x: -1.7, y: -6.8}, {x: 2.3, y: -8.0}, {x: 5.4, y: -5.7}, {x: 5.4, y: 2.9}, {x: 6.6, y: 7.4}],
    [{x: 4.3, y: -1.7}, {x: -0.9, y: -1.1}, {x: -5.1, y: 3.9}, {x: -2.1, y: 7.4}, {x: 2.3, y: 6.3}, {x: 5.4, y: 2.9}]
  ]
}

These numbers are based on 20 font size and scale for different font sizes.

We form a natural thickness

This is what my entire alphabet looks like. It looks pretty natural, but still, something is wrong.

The thickness of the lines that draw the letters is easy to adjust. But handwritten letters are not all the same. Different pressure = different line thickness.

To make it look more natural, I transformed the letters into 2D shapes using an algorithm I call “shapify”.

Here is the familiar zigzag shape after I turned it into a curve.

To achieve the effect of natural thickening/thinning of the letter lines, that is, to create a shaped contour, we will go along it and at each point:

  • Let's find the angle from this point to the next one (for the last point, we find the angle to the previous point and turn it 180°).

  • Using Perlin noise, we select the width of the contour at this point.

Here I have drawn a line at each point of the outline to demonstrate these angles and widths. Notice that each of the lines is slightly different in length, and their angles follow the curve of the outline.

From here you can see how to create a contour with variable line width. Notice how a small loop of dots is created at 180° to create a nice round line on the curve of the letter.

Note: my 'shapify' algorithm is far from perfect. When the stroke width is large, awkward internal loops appear at steep angles.

The last thing I did was move all the dots around a bit using Perlin noise. This adds another layer of naturalness to the letters, making them more varied. Here's what the letters look like:

We bring additional beauty

Now you can play around with the settings to try to create some interesting effects. For example, instead of changing the line width using noise, you can do it based on the placement of the letters.

You can also increase the noise itself so that the letters become more uneven.

How much does it weigh?

My font is 9.7 KB. It contains:

  • Preset paths for all letters A-Z in lower and upper case, as well as 7 punctuation marks.

  • The distance between letters (this can still be adjusted).

  • Function to change the size of letter lines depending on the font size.

  • Function for creating a smooth line of letters by calling Chaikin's algorithm.

  • Functions for creating and drawing NaturalLine objects that define line point jaggedness, shape changes, etc.

Is it possible to reduce the file size? Yes, of course. I haven't thought about this yet. When I first started working on this project, I was worried that I was doing useless work. But now I really like it.

Do you think I stopped there? Not really. A couple of months later I wanted to make a cursive version of my font. At that point it looked like this:

  • Code has been written to determine key points in the lines of each letter (~10 points per letter).

  • A method for smoothing these paths was invented using the Chaikin curve algorithm.

  • Variable line thickness is set.

  • The outlines of the shapes are drawn using p5js.

The work of determining the curve paths and determining the thickness of these letters was done mostly by hand. You had to write their positions into the code, and then move the dots back and forth until the letters looked just right. When it came to creating the italic version of my font, I simplified this process.

We design letters

In the p5js editor I created a tool to define and display key points in the contours of letter lines. There you can place key points of the trajectory in a few clicks – the resulting trajectory will be shown in the form of the Chaikin curve. You can drag the points to get the desired letter shape. I created 2-3 options for each letter.

The path of the letter was approximately 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 reference, so I wrote out some examples of lowercase and uppercase letters and loaded the image into my lettering tool to trace.

The numbers on the paper are the XY coordinates needed to get that area into the letter creation window. After creating all the paths, bending them, and turning them into variable width shapes, this is what I ended up with:

Italicization, cursification, curva… oh, everything

Sometimes connecting letters is easy, you just need to move from one chain of key points to another until Chaikin's algorithm bends them all at once. But some pairs of letters don't go well together.

Let's look at a couple of letters na. We will highlight the last point of the letter in red nwhich is located at the bottom, and in green is the first dot of the letter awhich is located at the top. This leads to the fact that the connecting path (we write together, right?) passes diagonally through the letter awhich makes it look a bit like a letter e.

Meanwhile, in a couple ti letter t ends just above the base line and then starts on it iforming an unnatural protrusion.

To fix this, we can add an extra dot to the beginning a and delete the last two dots in t.

But doing this for all scenarios is too much. Especially if a is at the beginning of a word, the extra dot will be out of place, and if before a there is a letter like wthen it will create a line intersecting a in another way. If t goes well with kthen it becomes deformed.

The dots at the beginning and end of letters should change depending on which letters they are next to.

At first I tried to identify specific “problem” pairs and write rules for them, but in the end I just added one number to the beginning and end of each line of letters, indicating that the letter:

  • Cannot join with another letter (0)

  • Attaches to another letter around the base(1) line

  • Attaches to another letter just above the base line (2)

  • Joins another letter at x height (3)

Here are some examples:

Now each letter path looks something like this. Note the individual numbers 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],

After testing all the letter pairs, I got this:

Here you can also see the variations created by having multiple paths for each letter, as well as by editing letters based on what letter they are next to. Ideally, I would have at least 5 or 6 path variations for each letter, but it is necessary to strike a balance so as not to bloat the font file size.

Word creation

When a word is created:

  • For each letter, a base path is selected from 2-3 options for this symbol.

  • information about the ends of the letter's paths is transmitted to neighboring letters.

  • The basic paths of letters are adjusted depending on their neighbors. For example, if the height of the end of the previous letter is 2, remove 1 point from the beginning of this path, or if the height of the beginning of the next letter is 1, add an additional point at a certain place.

The customization functions can sometimes seem complicated. Here's an example 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})
  }
}

And for the letter n everything is quite simple:

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

Then the main paths for all the letters are joined together. This ignores 1, 2, and 3 in the letter paths, but when there is a 0, a gap is created and a new word line begins.

After giving the letters varying line thicknesses and a bit of jaggedness using Perlin noise, the italics looked like this:

For comparison: two sheets. One of them is filled in by hand, the other is typed.

The italic version weighs much more than the simple character set, 26 KB. Partly because I didn't do any optimization. But I'm happy with it for now. I use this font to create inscriptions on my works. You might come up with something else.

Thank you for your attention!

Examples of works

P.S. Bonus for those who read the article to the end. On Monday, September 9, we will launch a new IT quest. If you don’t know what it is, here is a link to the analysis of the previous one. Follow the news, this time the riddles will be more difficult and varied!

Your Cloud4Y.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *