Converting font characters to contours

FontShape-2023-06-18-19.02.21

It is possible to convert font characters to shapes and contours. I believe it’s not yet documented, but here a quick example:

imports
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.font.loadFace
import org.openrndr.extensions.Screenshots
import org.openrndr.shape.LineSegment
fun main() = application {
    program {
        val face = loadFace("data/fonts/default.otf")
        val shape = face.glyphForCharacter('a').shape(750.0)
        val contours = shape.contours
        val normals = contours.map { c ->
            List(100) {
                val t = it / 99.0
                LineSegment(
                    c.position(t) - c.normal(t) * 2.0,
                    c.position(t) - c.normal(t) * 10.0
                )
            }
        }.flatten()
        extend(Screenshots())
        extend {
            drawer.clear(ColorRGBa.WHITE)
            drawer.translate(drawer.bounds.center - shape.bounds.center)

            drawer.fill = ColorRGBa.BLACK
            drawer.stroke = null
            drawer.shape(shape)

            drawer.stroke = ColorRGBa.BLACK
            drawer.strokeWeight = 2.0
            drawer.lineSegments(normals)
        }
    }
}

In this example I get the shape for the character a, get the contours out of it, then sample 100 points on each contour getting positions and normals. Note that it is possible to get those points evenly distributed but I can show that later.

It would make me happy to see images of any kind of experiment using this feature :slight_smile:

2 Likes

Oh wow this is great! I started trying to implement it in my experiment, works great for converting the letter into shape, displaying the shape as well:

EDIT:
I managed to apply it to your example filtering points inside the shape, only it seems that “inside” is just what is inside the eye of the letter:

fun main() = application {
    configure {
        width = 768
        height = 576
        windowResizable = true
    }

    program {

        val face = loadFace("data/fonts/default.otf")

            )
        )

        val letter = face.glyphForCharacter('P').shape(1000.0)

        extend {

            val r = Random(0)
            val area = Rectangle(letter.bounds.center.x - drawer.bounds.width / 2.0, letter.bounds.center.y - drawer.bounds.height / 2.0, width * 1.0, height * 1.0)
            val pts = area.scatter(20.0, random = r)

            val insidePoints = pts.filter{it in letter}

            drawer.translate(drawer.bounds.center - letter.bounds.center)

            drawer.fill = ColorRGBa.WHITE
            drawer.shape(letter)

            drawer.fill = ColorRGBa.BLUE
            drawer.stroke = null
            drawer.circles(pts,20.0)
            drawer.fill = ColorRGBa.RED
            drawer.circles(insidePoints, 20.0)

        }
    }
}

(I am learning so much trying this stuff thanks again, please keep in mind I’m a code noob so maybe my code includes some unforgiveable sin)

Nice to see your experiments :slight_smile:

A workaround for only the inner points being detected is this:

    val letter2 = Shape(letter.contours.map { it.reversed })
    val insidePoints = pts.filter { it in letter2 }

You can print it.winding for the contours to see why. It seems like the outer contour has COUNTER_CLOCKWISE and the inner one CLOCKWISE but it should be the other way around.

Update: a PR to address this.

1 Like

By the way, maybe something useful: you can apply transformations to shapes:

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.font.loadFace
import org.openrndr.math.transforms.transform

fun main() = application {
    program {
        val face = loadFace("data/fonts/default.otf")
        val letter = face.glyphForCharacter('a').shape(1000.0)

        extend {
            drawer.translate(drawer.bounds.center)
            drawer.fill = ColorRGBa.WHITE
            drawer.shape(letter.transform(
                transform {
                    rotate(seconds * 10)
                    translate(-letter.bounds.center)
                }
            ))
        }
    }
}

Playing with character contours:

1 Like