p5.js textToPoints() function
Get the outlines of your fonts with p5.js and opentype.js
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> |
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:
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> |
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 isbeginShape()
andvertex()
- L:
lineTo()
command; p5 equivalent isvertex()
- C:
bezierCurveTo()
command; p5 equivalent isbezierVertex()
- Q:
quadraticCurveTo()
command; p5 equivalent isquadraticVertex()
- Z:
closePath()
command; p5 equivalent isendShape()
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> |
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!