Erratic Generator

Creative Coding and Design

About

p5.js textToPoints() function

Get the outlines of your fonts with p5.js and opentype.js

Type animation using font outlines. Image by Author.

p5.js has a function called textToPoints() which is somewhat hidden in the reference. This function returns you with an array of points that are on the outline of the text. It does resampling for you (and you can control the sampling resolution) so it is great to use for quick typographic experiments. In this post, I will show you the basics of how to use it and a few use cases and challenges such as dealing with multiple letters and counter shapes (ie. holes in letters — 0 a, p, B, etc.)

Basic Usage

The basic usage is very simple. First, you need to load an external OTF file. It only works with externally loaded font files, and because loadFont() is asynchronous, you will need to place the function call in preload() and it will be loaded before the font object is used in the setup and draw functions.

Next, font.textToPoints() function takes 4 arguments — the text you want to get points from, x and y coordinates, and what size the text will be. It also has an optional fifth argument, where you can define samplingFactor, which you can think of as sampling resolution and simplifyThrehold, which removes collinear points if you need to have a minimum number of points to draw the outlines. I suggest that you play with these values (start with something small) to get a feel for it.

Once you have all the points stored in thepts array, you can use them however you want. You can use x, y coordinates to draw lines, ellipses, etc. Here is the result of my example and the code below. I am using a beautiful open source font called Chunk, but if you don’t have it, you will need to replace it with your own font file.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.1.9/lib/p5.js"></script>
</head>
<body>
<script>
/*
1. basic use of p5.Font.textToPoints()
*/
let font
let fSize // font size
let msg // text to write
let pts = [] // store path data
function preload() {
// preload OTF font file
font = loadFont('../ChunkFive-Regular.otf')
}
function setup() {
createCanvas(800, 500)
fSize = 256
textFont(font)
textSize(fSize)
msg = 'point'
pts = font.textToPoints(msg, 0, 0, fSize, {
sampleFactor: 0.1, // increase for more points
simplifyThreshold: 0.0 // increase to remove collinear points
})
console.log(pts) // { x, y, path angle }
stroke(255)
strokeWeight(2)
noFill();
}
function draw() {
background(0)
// 1. interactive
// const d = dist(mouseX, mouseY, width/2, height/2)
// const angle = atan2(mouseY-height/2, mouseX-width/2)
// 2. animate on its own
const d = 10 + sin(frameCount/50.) * 50
const angle = frameCount/100.
push()
translate(60, height*5/8)
for (let i = 0; i < pts.length; i++) {
const p = pts[i]
push()
translate(p.x, p.y)
rotate(angle)
line(-d, -d, +d, +d)
pop()
}
pop()
}
</script>
</body>
</html>

First code example result. Image by Author.

Outline of Multiple Letters

Let’s say I want to draw the outline of my text. You can simply use beginShape() and endShape() with vertex() function calls inside. Since we already have all the points stored in the pts array, the implementation is quite simple. Just replace the code inside draw() with this:

function draw() {
  background(0)
  push()
  translate(60, height*5/8)
  noFill()
  stroke(255)
  beginShape()
  for (let i = 0; i < pts.length; i++) {
    const p = pts[i]
    vertex(p.x, p.y)
  }
  endShape()
  pop()
}

And you get this:

All the letters are connected. Image by Author
All the letters are connected. Image by Author

It looks pretty cool, but the problem is all the letters are connected. The pts array contains all the coordinates but it does not tell us when each letter starts and ends. One workaround is to use a 2-dimensional array and store the points for each letter.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.1.9/lib/p5.js"></script>
</head>
<body>
<script>
/*
2. individual letters with 2d array
at least, we can separate letters but still have issue with counter shapes.
*/
let font
let fSize // font size
let msg // text to write
let pts = [] // store path data
function preload() {
// preload OTF font file
font = loadFont('../ChunkFive-Regular.otf')
}
function setup() {
createCanvas(800, 500)
fSize = 256
textFont(font)
textSize(fSize)
msg = 'point'
let x = 0
let y = 0
for (let m of msg) {
const arr = font.textToPoints(m, x, y, fSize)
pts.push(arr)
x += textWidth(m)
}
console.log(pts)
stroke(255)
noFill();
}
function draw() {
background(0)
push()
translate(60, height * 5 / 8)
for (let pt of pts) {
beginShape()
for (let p of pt) {
vertex(p.x, p.y)
}
endShape(CLOSE)
}
pop()
}
</script>
</body>
</html>

Using 2-d array. Image by Author.
Using 2-d array. Image by Author.

This is looking a lot better. In the image, the letters n and t look perfect, but we still have problems with the letters p, o and i because either they are made up of multiple shapes or there are counter shapes (holes). Unfortunately, the way the pts array is structured and the way p5js handles these counter shapes (using beginContour() and endContour()) did not allow me to distinguish these shapes. So, I had to turn to something else — another library called opentype.js.

Opentype.js for More Control

In fact, p5js internally uses opentype.js to get the text outlines, but when you directly use opentype.js to extract the text path data, it not only gives you the x and y coordinates but also SVG commands that are associated with the points. We can use this data to find out when a path should start and end. You can look more into the SVG spec on the W3 website. I will summarize what we need to know here:

  • M: moveTo() command; p5 equivalent is beginShape() and vertex()
  • L: lineTo() command; p5 equivalent is vertex()
  • C: bezierCurveTo() command; p5 equivalent is bezierVertex()
  • Q: quadraticCurveTo() command; p5 equivalent is quadraticVertex()
  • Z: closePath() command; p5 equivalent is endShape()

Also, the way opentype.js loads a font is a bit different so we will also need to change that part of the code. Here is the full working code:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.1.9/lib/p5.js"></script>
<script src="https://cdn.jsdelivr.net/npm/opentype.js@latest/dist/opentype.min.js"></script>
</head>
<body>
<script>
/*
3. use opentype.js for more control
reference: https://github.com/opentypejs/opentype.js
*/
let font // opentype.js font object
let fSize // font size
let msg // text to write
let pts = [] // store path data
let path
function setup() {
createCanvas(800, 500)
opentype.load('../ChunkFive-Regular.otf', function (err, f) {
if (err) {
alert('Font could not be loaded: ' + err);
} else {
font = f
console.log('font ready')
fSize = 256
msg = 'point'
let x = 60
let y = 300
path = font.getPath(msg, x, y, fSize)
console.log(path.commands)
}
})
}
function draw() {
if (!font) return
background(0)
noFill()
stroke(255)
for (let cmd of path.commands) {
if (cmd.type === 'M') {
beginShape()
vertex(cmd.x, cmd.y)
} else if (cmd.type === 'L') {
vertex(cmd.x, cmd.y)
} else if (cmd.type === 'C') {
bezierVertex(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y)
} else if (cmd.type === 'Q') {
quadraticVertex(cmd.x1, cmd.y1, cmd.x, cmd.y)
} else if (cmd.type === 'Z') {
endShape(CLOSE)
}
}
}
</script>
</body>
</html>

With opentype.js, we can create counter shapes. Image by Author.
With opentype.js, we can create counter shapes. Image by Author.

Now, we have very clean outlines with all the properly separated counter shapes! Now, with this path data, you can draw anything you want. Maybe, convert the points to p5.Vector and do physical simulations?

One caveat here is that the path data we get from the opentype.js is from the original font data and it does not automatically resample them just like how p5js’ textToPoints() did. The post is getting long so I won’t cover it here but the P.3 section of the Generative Design website has many great examples you should definitely check out. I hope you have fun experimenting with fonts!

If you like my contents, please consider supporting my website. It helps me create more contents like this. Thank you!

Buy Me a Coffee at ko-fi.com
Copyright 2020-2021 • ErraticGenerator.com