DEV Community

zeankun
zeankun

Posted on

Making strokefont usage in web much easier with Derakuma

For the past 2 weeks or so now, I've been building Derakuma, a stroke font library, for the web. It's built on top of the Fontobene font format, web-ready, and the foundation for the future CompassCAD NEXT client.

Background and lore (I guess)

Finding a good stroke font technique is always a hit and miss. For example, we have the industry standard Hershey fonts. KiCad's font, Newstroke, natively uses them, but to an average programmer or a noob, they will not understand how a Hershey font works, if they just want to make their first "parser". An example is this, from Newstroke's U+C0 glyph.

    "I[MUWU RK[RFY[ RP>SA", /* U+C0 A_CAP GRAVE */
    "I[MUWU RK[RFY[ RT>QA",
    "I[MUWU RK[RFY[ RNAR>VA",
    "I[MUWU RK[RFY[ RMAN@P?TAV@W?",
    "I[MUWU RK[RFY[ RN?O@NAM@N?NA RV?W@VAU@V?VA",
    "I[MUWU RK[RFY[ RRFPEOCPAR@TAUCTERF",
    "F`JURU RRPYP RH[OF\\F RRFR[\\[",
    "F[WYVZS[Q[NZLXKVJRJOKKLINGQFSFVGWH RR\\T]U_TaRbOb",
    "H[MPTP RW[M[MFWF RP>SA",
    "H[MPTP RW[M[MFWF RT>QA",
    "H[MPTP RW[M[MFWF RNAR>VA",
    "H[MPTP RW[M[MFWF RN?O@NAM@N?NA RV?W@VAU@V?VA",
    "MWR[RF RP>SA",
    "MWR[RF RT>QA",
    "MWR[RF RNAR>VA",
    "MWR[RF RN?O@NAM@N?NA RV?W@VAU@V?VA"
Enter fullscreen mode Exit fullscreen mode

Now, I got a great explanation here, by Claude. Claude says that
each glyph is a plain string. It parses in three layers:

  1. Bounding box — the first two characters. Every character's numeric value is ord(char) - ord('R') (so 'R' = 0, 'N' = -4, 'V' = 4, etc.). The first two chars give left/right extent of the glyph in font units.

  2. Stroke data — everything after that, read in pairs of characters = one (x, y) coordinate, same ord(c)-ord('R') decoding.

  3. Pen lifts — a literal space ' ' appearing where an x-coordinate is expected means "lift the pen, start a new stroke," with the pair that follows the space giving the new starting point. So one glyph string can contain multiple disconnected polylines (strokes), each one a separate moveto + a chain of linetos.
    ~Claude~

There are lots of technical stuffs involved, but most of us won't understand what the hell does R represent, out of the box (without the detailed explanation).

Now again. Back in 2024, I was happily playing around with LibrePCB (an alternative to KiCad). They had Newstroke, but I didn't know how that was done. I looked at the underlying tech, and that's when I first discovered FontoBene. The thing that fascinates me the most, about Fontobene, is how intuitive they define the strokes for each character. Frankly, they are easily digestible by some developers (but not at most).

Fontobene specs define a character by a 9x9 right-up coordinate system. Each pair is separated by a semicolon[spec], which looks highly intuitive, but unprecedented at scale. AI models weren't advanced at the time, yet. I was only constrained to GPT 3.5-turbo at that time, which was not great (getting GPT 4 was a luxury back then).

That was only imagined as a dream back then, getting Fontobene to work on CompassCAD 1.0. That idea was scrapped, so we were left with unscalable, non-variable, TTF/OTF versions of Newstroke, in which most of the converting work was done purely by me, by hand. That was used for a while, until readability issues came in.

Fast forward to 2026, I'm just on the right track of building CompassCAD NEXT. I don't want a gimmicky stroke font for CAD. I want true stroke fonts. Then I came across Fontobene again. Now this time, Claude handles the heavy parser logic lifting (still hard though), while I wrote most of the helper/lookup functions, as well as the glyph-to-command idea. That one was just called FontobeneParser. The parser turned out to be really good, and here I am, a library for all of you! Derakuma!

CompassCAD NEXT screenshot showing Hello and FONTOBENE WORKS

*It worked well!*

Seem to stick around? Love it? Good, now let me show you on how to use Derakuma on your project!

The (actual) implementation stuff

Alright, let's get started! First up, let's install it as a package! It may vary, but it should work on npm, yarn and bun.

npm i derakuma
Enter fullscreen mode Exit fullscreen mode

Now, create index.js (or whatever your entry point is). I'm not going to show you the full code, as we will be walking through what each code does, so you'll gain knowledge, not just copy-pasting code you don't trust from the internet, or you'll end up making code you don't understand and if you modify a teeny-tiny part of the copypasted code, it will turn into a maze of spaghetti!

Now, after installing, depending on whether you use Typescript or JavaScript-as-module, the imports may vary. If you are using Typescript or Javascript-as-a-module, do that with so:

import { DerakumaParser } from 'derakuma';
Enter fullscreen mode Exit fullscreen mode

Or if you are opting for a classic Javascript method:

const { DerakumaParser } = require('derakuma');
Enter fullscreen mode Exit fullscreen mode

Now, DerakumaParser is the main parser object. You'll do your initial starting point for parsing, here. To initialize, it's just:

const myFont = new DerakumaParser('/path/to/your/bene/file.bene');
Enter fullscreen mode Exit fullscreen mode

The path can be a URL, local resource relative to your script's working directory. If you want to use filesystem-based loading, you can. The key is to append the FontLoadMethod enum.

import { DerakumaParser, FontLoadMethod } from 'derakuma';
Enter fullscreen mode Exit fullscreen mode

If you want it to be fetched (URL), set it to FETCH. If not, use FILE. This is Node/Bun only.

const myFont = new DerakumaParser('C:/path/to/your/bene/file.bene', FontLoadMethod.FILE);
Enter fullscreen mode Exit fullscreen mode

For some .bene fonts, you can grab Derakuma's testing font library. If none of that suits you, you can grab a LibreCAD font (.lff) of your liking, then use an LFF to Bene converter.

Great, all done? Now, you can call ready() as an async, to wait while the font is being fetched and be ready for your use.

You should wrap Derakuma in an async function for the ready() or FETCH method to work.

const myFont = new DerakumaParser('C:/path/to/your/bene/file.bene', FontLoadMethod.FILE);
await myFont.ready();
Enter fullscreen mode Exit fullscreen mode

Nice! That should do!

Getting your first glyph

That should be done for the initialization. Let's now grab your glyph! Say, let's grab the glyph commands for A. You can do so by doing this:

myFont.getGlyph('A');
Enter fullscreen mode Exit fullscreen mode

If that succeeded, it should return a series of objects, called pen commands. These pen commands allow you to instruct to your rendering pipeline to render the fonts.

[
    {
        "command": "PD",
        "x": 3.43,
        "y": 9
    },
    {
        "command": "MP",
        "x": 0,
        "y": 0
    },
    {
        "command": "PU",
        "x": 0,
        "y": 0
    },
    {
        "command": "PD",
        "x": 3.43,
        "y": 9
    },
    {
        "command": "MP",
        "x": 6.86,
        "y": 0
    },
    {
        "command": "PU",
        "x": 6.86,
        "y": 0
    },
    {
        "command": "PD",
        "x": 1.29,
        "y": 3
    },
    {
        "command": "MP",
        "x": 5.57,
        "y": 3
    },
    {
        "command": "PU",
        "x": 5.57,
        "y": 3
    }
]
Enter fullscreen mode Exit fullscreen mode

Alright, now how to pass that to our rendering pipeline? Simple. Pass the returned glyph as a variable:

const capitalA = myFont.getGlyph('A');
Enter fullscreen mode Exit fullscreen mode

Now that's done, getGlyph fetches the commands and returns the commands to you. Since the commands are just PenCommand[], we can do so with this loop. Assuming we have ctx as a CanvasRenderingContext2D object, but feel free to adjust as your rendering pipeline!

capitalA.forEach((pen) => {
    if (pen.command === 'PD') ctx.moveTo(pen.x, pen.y);
    else if (pen.command === 'MP') ctx.lineTo(pen.x, pen.y);
    // There's no PU, because PU is just a marker for some applications (gcode, etc)
})
Enter fullscreen mode Exit fullscreen mode

Congrats! You made Derakuma work!

If you want to explore more possibilities, undocumented here, you can either look into the Getting Started guide or the list of full API functions.

Anything to ask? Feel free to ask below!

Top comments (0)