6
Fonts
MelekTaus edited this page 2026-10-13 15:19:01 +02:00

A text object is drawn in the editor's own typeface until it is given one of its own. That typeface is a font asset, and the text points at it through the ordinary property font — the same design as a sprite and its texture: because it is an ordinary property, instance overrides, Apply to container, Reset to asset, undo and Properties work on it without a line written for them. One placed title can wear a different face than its siblings.

Getting a typeface in. Dropping a .ttf, .otf, .woff or .woff2 file onto the canvas uploads it and makes the asset in one go; several at once are imported one after another, and a file that fails does not stop the rest. The asset is named after the file ("DejaVuSerif.ttf" becomes "DejaVuSerif"), which is worth more than the stored name — see below. A dropped file is recognised by its extension, not by its MIME type: browsers report a dropped font as font/ttf, application/x-font-ttf or as nothing at all.

+ → Font asks the other question and opens the picker: every typeface the server already holds, one row each. There is no thumbnail to show for a font, so each row is the preview — the sample line is drawn in that very face. Picking one and pressing Use font makes the asset; Upload… is the way in for a face the server does not have yet. A row captioned with an asset name is one this project already wears, and picking it uses that asset rather than making a second one; any other row is captioned with the name the file was uploaded under, and picking it names the asset after that file — DejaVuSerif.ttf becomes DejaVuSerif.

Where it goes. The bytes are uploaded to POST ?r=fonts and stored as data/fonts/{folder}/{sha256}.{ext} — in the uploader's folder and named after their own content, exactly like an image, and with the same four consequences (one file per typeface however often it is uploaded, cacheable forever, a short string in the project document, an undoable delete). Delete in the picker removes a file of your own nobody uses, as for images. Unlike an image, a font is never re-encoded on the way: what the server stores is the file that was picked. The format is read from the file's own signature, so nothing a request claims can decide what is written. Bitmap collections (.ttc) and SVG fonts are refused — a collection holds several faces and @font-face cannot say which, and an SVG font can carry script.

Wearing it. A text object's inspector has a Font row between Text and Font size: a dropdown of the project's font assets, with None for the editor's own typeface. That is also what a reference falls back to when the asset behind it is gone, so a deleted font leaves readable text rather than nothing. The row works in the scene view, in the game preview and while playing — all three go through one place (WorldViewport.syncText, which hands the text to TextPicture: glyphs rasterised into a picture of the text's own, drawn like a sprite so a shader can be worn — see Shaders).

Letter spacing. Below Font size, in the same units: what is added between two characters, so a text of ten characters grows by nine times the value. Negative draws them closer, which is how a design's tracking in em carries over: -0.03em at a font size of 216 is -6.5. The spacing counts into the text's footprint, so selecting, picking and "Add collider" follow it.

Height. A text has no height row: it is as tall as the line box its face declares (the browser's fontBoundingBoxAscent + fontBoundingBoxDescent, which is where the face says a line begins and ends), each further line one Pixi line lower. Pixi on its own makes a line only as tall as |ÉqÅM reaches and cuts off whatever reaches further: the tail of Manrope's "g" is 0.015em deeper than its "q" and was cut flat. Where Pixi's box is the taller one, it is kept. So the height does not change with what is typed, and a glyph only loses ink if it reaches beyond the face's own metrics. By Manrope's font file, a line is 1.366em instead of 1.25em.

Filter. A font asset has one row, Filter, the same as a texture's: Linear (the default) draws the glyphs smooth and renders them again, sharper, as the view zooms in. Nearest is for pixel fonts: the glyphs end up at one texel per world unit and are magnified without smoothing, so at a scale or zoom of 4 every font pixel is a block of four screen pixels. To get there they are rasterised eight times finer and brought down again, each 8x8 block becoming one texel that is either fully inked (half the block or more) or empty. No texel is left half covered, on any platform — Chrome on Windows smooths canvas text sideways whatever the filter, and without this step every glyph column there had a grey fringe. The texture stays as small as at 1:1. A pixel font looks cleanest at the size it was drawn for (8 or 16 for most); at any other size its pixels come out hard but unevenly wide, and a font that is no pixel font loses its thinnest strokes. Every text wearing the font follows its filter; the text itself has no filter row.

It arrives late. A face is fetched over the network, so a text is drawn in the fallback for the first frames and then reflows into the real one. That is the same "not yet, redraw when it lands" contract the image library has, and the reason the width of a text can change shortly after a project opens.

The family name is derived, not read. A face is registered with the browser as gm-font-<first 16 characters of the hash>, never under the name inside the font: two uploads of "Regular" cannot then fight over one family, and the same bytes always resolve to the same face. The name inside the file is not read at all — the asset's name comes from the uploaded file name and is yours to change.

The frame is what is drawn. The selection box, the click area in a running game and the camera's framing take the size of the text's picture (setTextMeasure in bounds.ts, set by textPicture.ts). Where nothing can measure — the unit tests in node — they fall back to an estimate from the character count (CHAR_WIDTH, LINE_HEIGHT).

Known limits. Only text objects wear fonts; a sprite has no use for one. There is no bold/italic pair: a font asset is one file, so a bold face is a second asset.