Skip to contents

A letter is a polygon with holes: an outer ring for the shape and an inner ring for each counter (the enclosed space in A, B, O, 8, g). That is exactly what a constrained triangulation is built for, so laridae can turn any text in any installed font into a mesh for design work: shading, sizing fields, overlaps, extrusion, or as a domain for a field on a mesh.

The outlines come from the font itself. lari_text_outline() shapes the string with ‘textshaping’ (kerning, ligatures and font fallback included) and flattens each glyph’s curves with systemfonts::glyph_outline(). Both packages are only suggested by laridae.

library(laridae)
o <- lari_text_outline("ABO")
head(o)
#>           x         y glyph ring char
#> 1 0.2080156 0.5048438     1    1    A
#> 2 0.4760781 0.5048438     1    1    A
#> 3 0.3417969 0.8676719     1    1    A
#> 4 0.3979531 0.9648438     1    2    A
#> 5 0.6757812 0.2358438     1    2    A
#> 6 0.5732500 0.2358438     1    2    A
## one ring per contour: A has a counter, B has two
tapply(o$ring, o$char, max)
#> A B O 
#> 2 3 2

Coordinates are in ems (set em to scale them), so max_area and tolerance mean the same thing for every font.

A small helper draws all triangles in one polygon() call, filled by a per-triangle colour.

plot_tri <- function(r, col = "grey85", border = "white", lwd = 0.4) {
  V <- r$vertices
  T <- r$triangles
  idx <- rbind(T$v0, T$v1, T$v2, NA)
  op <- par(mar = rep(0.2, 4))
  on.exit(par(op))
  plot(range(V$x), range(V$y), type = "n", asp = 1, axes = FALSE, xlab = "", ylab = "")
  polygon(V$x[idx], V$y[idx], col = col, border = border, lwd = lwd)
}

Letters

lari_text() meshes each glyph and binds the tables, with a glyph and char column so each letter can be treated on its own.

r <- lari_text("laridae", max_area = 0.0015, min_angle = 25)
nrow(r$triangles)
#> [1] 1729
table(r$triangles$char)
#> 
#>   a   d   e   i   l   r 
#> 778 339 339  68  64 141
plot_tri(r, col = hcl.colors(7, "Dark 3")[r$triangles$glyph])

The counters are holes because only odd constraint depth is kept (erase = "holes"): the orientation of the rings does not matter, so a font that winds its contours either way gives the same letters. With erase = "outer" the counters are meshed too and depth tells them apart.

r <- lari_text("ABBA", erase = "outer", max_area = 0.002)
plot_tri(r, col = c("grey30", "goldenrod")[r$triangles$depth])

Sizing fields

Any sizing field of lari_refine() works: here the largest allowed triangle area grows steeply from left to right across the word, and the triangles are shaded by their area.

r <- lari_text("gradient", weight = "bold", tolerance = 0.01,
               size = function(x, y) 2e-5 + 1e-3 * x^3, min_angle = 25)
V <- r$vertices
T <- r$triangles
area <- abs((V$x[T$v1] - V$x[T$v0]) * (V$y[T$v2] - V$y[T$v0]) -
            (V$x[T$v2] - V$x[T$v0]) * (V$y[T$v1] - V$y[T$v0])) / 2
pal <- hcl.colors(64, "Mako")
plot_tri(r, col = pal[cut(log(area), 64)], border = "white", lwd = 0.3)

Overlaps

With merge = TRUE all glyphs go into one mesh, so where they overlap the constraints cross and the depth stacks. Two copies of a word, slightly offset, show the overlap as depth 2: kept with erase = "outer", or cut away (an exclusive-or of the two) with the default erase = "holes".

o <- lari_text_outline("mesh", weight = "bold")
o2 <- transform(o, x = x + 0.06, y = y - 0.05, glyph = glyph + max(o$glyph))
both <- rbind(o, o2)
r <- lari_glyphs(both, merge = TRUE, erase = "outer", max_area = 0.002)
plot_tri(r, col = c("grey70", "firebrick", "grey20", "firebrick")[r$triangles$depth])

r <- lari_glyphs(both, merge = TRUE, max_area = 0.002)
plot_tri(r, col = "grey20", border = "grey60")

Other outline sources

lari_glyphs() takes any data frame of rings with x, y and glyph and ring ids, so outlines from other tools mesh as they are. The output of string2path::string2path() (columns glyph_id, path_id) and of systemfonts::glyph_outline() (glyph, contour) are recognised, and a ring may repeat its first vertex or not. (string2path is not currently on CRAN; it installs from https://yutannihilation.r-universe.dev, and the chunk below only runs when it is installed.)

font <- systemfonts::match_fonts("serif")$path
d <- string2path::string2path("Rings", font)
r <- lari_glyphs(d, max_area = 0.002, min_angle = 30)
plot_tri(r, col = "grey95", border = "grey30")