Close
0%
0%

Bringing Text to Life on an ESP32

Text on a small display should come alive. It started with clock digits made of Bézier curves; this is where the idea went.

Similar projects worth following
A clock tells you the time, and you get it at a glance: you look, you know. Good design makes it easier to spot; animation makes it feel alive. Do you need the animation? No. It's a finishing touch, the kind an artist adds: everything works without it, and with it the thing is a pleasure to look at. This project is about pulling off that kind of touch on a microcontroller. It started in 2021 with a clock whose digits were Bézier curves, went on through a font editor, a dot-matrix clock, the Game of Life, and morphing between glyphs, and five years later came back to where it began. I do it simply because I like bringing information to life, and because I like programming as a kind of art. Microcontrollers come with their own limits, and getting around them is the challenge. A warning: it's a long story, and I tell it one step at a time.

"NICE TOUCH, DUDE"

After the Bézier curve clock that I built in 2021 https://habr.com/ru/articles/818873/ (in Russian), I got the idea of doing an entire handwritten font the same way. The existing formats, TTF and OpenType, don't work for this, and not because they're hard to parse. They store the outline of a letter, the boundary of a fill, while a handwritten font needs the path of the pen: the order of the strokes, their direction, where the pen leaves the paper, and how a letter joins its neighbor. None of that is in an outline, and you can't recover it from one. So I needed a format of my own: simple, but more general than the one in that clock, where any digit fit into three curves and, in theory, into two if you were willing to give up on looks. I had to start with a font editor, hw-fonts https://github.com/jef-sure/hw-fonts which I threw together in Vue with no build system at all: open index.html on your own machine and you can load a font you saved earlier or download the result of your editing. More on it in the log about the font.

The next touch was morphing fonts described by the coordinates of their dots: the dot-matrix clock https://habr.com/ru/articles/817927/ (in Russian).

Then came an intermediate step: live morphing in the Game of Life https://habr.com/ru/articles/1081830/ (in Russian).

That step led to the next one, morphing between the glyphs of a font https://github.com/jef-sure/cyd-dotview-morphing — although the font was really just a source there, and what it actually does is morph between any two matrices. The work moved from one idea to the next and left a handful of possible strategies along the way. Out of that grew a separate framework for morphing between matrices in dgx https://github.com/jef-sure/esp32-component-dgx — more on it in the log about dot morphing.

And all of it for a "nice touch." Simply because I like bringing information to life, and because I like programming as a kind of art. Microcontrollers come with their own limits, and that turns into the challenge: how do you get around them?

A warning: this is a long story. Five years have passed since the idea first came up, the experiments and the search for ideas took a long time, and if I cut the story short it stops holding together.

The story is told in the project logs, one step at a time. The page shows the newest log first; this is the order to read them in:

1. Where it started: a clock of Bézier digits
2. Three problems with curves on a microcontroller
3. A font that stores the path of the pen
4. Joining two letters with one curve

More to come.

  • Joining two letters with one curve

    Anton Petrusevich • 9 hours ago • 0 comments

    A letter's connections are drawn by hand in the editor, one for the way in and one for the way out, and are made of ordinary strokes of the font. How to get one smooth curve out of the end connection of one letter and the begin connection of the next is something I never did solve in 2022: two cubic curves have eight control points, one has four, and everything I tried gave either a kink or a loop. That is where the work stopped.

    In the end the solution is this. The last stroke of the end connection and the first stroke of the next letter's begin connection are merged into one. Their two ends, the ones that meet in the gap between the letters, drop out of the eight points, and the two control points next to those ends merge into one, halfway between them. Five points remain: the ends of the connections, the direction points next to them, and that midpoint. That is a fourth-degree curve, and the font draws only cubics, so the nearest cubic is used instead. Its ends are the ends of the connections, and its two middle points lie on the lines along which the connections were drawn: the pen leaves one letter and arrives at the other in the given directions. How far along those lines the middle points sit is found by the smallest deviation from the five-point curve, measured at seven of its points. A connection that has nothing to join is not written: a word neither begins nor ends with a dangling tail.

    Here is how it looks on two pairs from font0ss. Gray is the letters and the other strokes of their connections; dashed, the two strokes being merged, with their control points; dotted, the five-point curve; black, the cubic the joint is drawn with.

    For "i" and "m" the ends of the connections meet at one point, and the joint stays close to the drawn connections:

    The joint of the letters "i" and "m": the ends of the connections meet, the joint stays close to them

    For "k" and "e" they don't meet: the tail of "k" is drawn upward, toward a3, while the start of the connection of "e," b0, lies lower. If the pen followed the tail to its end and then went into "e," you would get a loop. The joint throws out both ends and goes straight across, and the five-point reference and the cubic nearly coincide, which is the whole point of the fit:

    The joint of the letters "k" and "e": the ends of the connections don't meet, the joint goes straight across

    The letter "ж" with its connections. Black is the strokes themselves on the grid; the blue dots and thin lines are the control points of the curves. The begin connection comes up to the letter from the left, and the end connection leaves it up and to the right:

    The editor: the letter "ж" with its begin and end connections

  • A font that stores the path of the pen

    Anton Petrusevich • 9 hours ago • 0 comments

    I drew the font0ss font in the editor, and it ships with dgx. It has the Latin alphabet with the German letters, the Russian alphabet, digits, and punctuation: 155 symbols, 692 elements of four kinds.

    curve, 4 points — a cubic Bézier curve: start, two control points, end
    curve3p, 3 points — a quadratic Bézier curve: start, one control point, end
    line, 2 points — a straight segment
    dot, 1 point — a dot

    Cubic curves are the overwhelming majority, 622 of the 692. The other kinds are used where a cubic is overkill: the bar of the "7" is a straight segment, and the dots over "ё" are dots.

    The editor

    Outline fonts don't store the path of the pen, hence a format of my own and an editor of my own.

    The hw-fonts editor https://github.com/jef-sure/hw-fonts was written in 2022 with Vue and Vuex; I moved it to Pinia recently, when I came back to it. The libraries are loaded as ready-made scripts from a CDN, and the font is stored in a JSON file. In the early days, when I needed to make similar letters, I simply copied the matching pieces of JSON into a new codePoint.

    The editor: all the symbols of the font and the dimensions of the selected letter

    The grid is 256x256 and the coordinates are integers from 0 to 255: a point fits into two bytes, one per axis, just as in the 2021 clock. Back then that was for the sake of memory; now it's more of a habit, but a convenient one for a microcontroller: on that side the points are multiplied by a scale and a polyline is drawn, and fractions have no business there. Inside the grid there is an active area, 100x202 cells in font0ss: the width of a character slot and the height of a line. Around it are margins, which is where the control points of the curves go, along with the tails of letters that reach into their neighbors.

    A symbol in the editor has four sets of elements, and each has its own part to play in writing. The main strokes are the drawing of the letter itself, in the order the hand makes them. The postponed strokes are what the hand adds once the word is finished: the dots over "ё," the crossbar of "t," the bar of "7." The begin connection and the end connection are the strokes by which the pen arrives at the letter from the previous one and leaves for the next; they are not part of the letter's drawing, they are transitions. Lowercase letters have connections on both sides, capitals only on the way out, and "V," "W," the digits, and the punctuation marks have none.

    Besides the symbols, the file holds helper lines shared by the whole font: horizontals at the height of the capitals and of the lowercase letters, two slanted lines for the slant, and two verticals for the edges of a letter. These are the guides I used to keep the heights and the slant the same across all the letters. They are not part of any symbol and never reach the microcontroller.

    Everything that can be computed ahead of time, the editor computes on save: the dimensions of a symbol, the length of each stroke, and the number of segments to draw it with. All the microcontroller has to do is read. The length and the number of segments are needed for more than drawing: together they make up the effort of a stroke, which later lets the pen write at different speeds on a straight line and on a curl; that is the log about the pen.

    The path of the pen

    A symbol is strokes in the order the hand makes them, and the order here is not a formality. The font is designed so that text can be written the way a hand writes it: letter by letter, stroke by stroke. That is why the dot over an "i" can't be part of the letter: the hand puts it there once the word is finished. In joined-up writing the end of one letter and the start of the next are joined by a single curve; digits are written separately and have no connections.

    I tried to get by with as few curves per symbol as possible, even at the cost of an exact shape: the main drawing, not counting connections and postponed strokes, takes three elements on average...

    Read more »

  • Three problems with curves on a microcontroller

    Anton Petrusevich • 9 hours ago • 0 comments

    A cubic Bézier curve is defined by four points: the two ends and two more that the curve is pulled toward. It can bend twice, in opposite directions, like the letter S. The handwritten font also has quadratic curves, defined by three points: the two ends and one that the curve is pulled toward. A curve like that is an arc of a parabola and bends only one way. Where that is enough, it's simpler to draw and simpler in the editor. The details are on Wikipedia: https://en.wikipedia.org/wiki/B%C3%A9zier_curve

    An example of a Bézier curve

    Image: Chris828, Wikimedia Commons, public domain.

    A letter made of such curves is a few strokes, each one a curve of its own, and on paper there is nothing to it. On a microcontroller there are three difficulties, and this article is really about them.

    The first is segments. A microcontroller knows how to draw straight lines, so a curve is some number of straight segments. Too few segments and you see the corners; too many and it takes too long. How many you need depends on the curvature and on the size on the screen, and every curve has its own answer.

    The second is thickness. The line has to be thick, and a thick segment is not a line but a shape. Where two segments meet, a wedge is left open on the outside of the turn, and something has to close it. In 2021 a circle at every joint closed it, and the circles ate up to half of the drawing time, even on a virtual screen.

    The third is animation. Every frame of a transformation is a new picture, and you can't draw it straight on the display: you would see the old one being erased and the new one appearing. So the frame is drawn in memory, on a virtual screen, and transferred to the display over SPI. The hours-and-minutes band, 320x100 at two bytes a pixel, is 64 KB, and on a 40 MHz bus it takes 17 ms to send. Of the ESP32's 520 KB of SRAM an application gets about 300, and Wi-Fi and TLS live in there too; a 320x240 screen at two bytes a pixel would take half of it. Assuming the heap even had a contiguous block that size, one fit for sending directly by DMA.

    How I dealt with them is covered in later logs, on drawing and on morphing, but that is still a long way off: first the font itself, because without it there is nothing to draw.

  • Where it started: a clock of Bézier digits

    Anton Petrusevich • 9 hours ago • 0 comments

    Where it started: a clock of Bézier digits

    I picked up the idea on a channel about ESP32 programming: someone showed a clock made of six displays, one digit on each, and named the source, the Bézier Clock project in JavaScript by Jack Frigaard. There is an article about the original project on Habr, A Bézier curve clock https://habr.com/ru/articles/236455/ (in Russian). I never looked at how it was done there: I decided right away to do it my own way, and first I checked what the transformation looks like on a display. The code of that experiment https://github.com/jef-sure/digits_morping_dgx still exists, and the video of it is in the log about morphing.

    In my clock every digit was three cubic Bézier curves. To begin with, I decided that every symbol had to fit into a 100x160 box, with the control points allowed to fly outside that grid as long as they stayed within 0–255, so that each coordinate fit into a uint8_t: that was my attempt to squeeze the description of a scalable font into a few bytes. I couldn't find an editor for fonts like that, so I just pictured the control points in my head and checked by experiment how well they worked. It came out not bad at all, and I had all the digits picked fairly quickly. I wasn't after perfect lines, and that was one reason picking them from imagination alone went so fast.

    That clock left some questions behind. The Wi-Fi password and the service keys were set in sdkconfig. That was the accepted standard in those years; it's how articles, magazines, and tutorials did it: you describe the parameters you need in Kconfig, set the SSID and the password in menuconfig, and use them in the program as constants from sdkconfig.h. The scheme is fine for a personal project, but if you want to give the thing to someone as a gift, you have to either teach that person to compile your firmware or ask them for their Wi-Fi name and password. Not a great scheme, to put it plainly.

    The second question was finding the city automatically. Same problem: the weather needs a specific place, and setting it through Kconfig is the same problem again. In the end I went with a service that finds your location by IP address. At home it usually guessed right and sometimes named the next town over. But on someone else's network it could show a city a few hundred kilometers away.

    The third problem follows from the second: the time zone. Luckily the same service returns it, so that one is at least partly solved. But relying on a third-party service for your time zone... What if nobody knows the key for it, or the key has stopped working?

    The bitmap font was not a problem: I wrote a converter, font2c, that rasterizes a font to monochrome through FreeType and writes the Arial12.h and Arial12.c I need to add to the project. The rasterizer turned out to be the problem: not every font comes out well in monochrome. So I had to hunt for ones that did. A city name can run to any number of characters, so I also had to come up with a way to estimate the size of the name in a given font and pick the best fit. I hadn't invented scaling for bitmap fonts back then...

    That is where the idea of a handwritten font came from: the same approach as the digits, with curves that scale to any size. Drawing letters in my head the way I had drawn digits no longer worked, and that is how the editor hw-fonts https://github.com/jef-sure/hw-fonts came to be. I drew a Latin font in it, and there the whole thing stalled for several years.

    What stopped me was the joints between letters. Handwriting is written joined up, and the move from one letter to the next has to be a smooth curve that adapts to its neighbor. If you just butt the tail of one letter against the start of the next, which is what I had counted on at first, you get a kink or an extra loop at the joint, and you end up with more points than a curve needs. I didn't find a solution then. I shelved it "until better times."

    In 2026 the font got its Russian alphabet,...

    Read more »

View all 4 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates