# Fonts and Languages SimVX ships its own fonts, so a game draws the same typeface on every machine, whether or not the player has any fonts installed. Choosing a different one, and adding the scripts your game needs, is a few keys in `simvx.toml`: ```toml [rendering] # The face every string is drawn in. Omit it to keep the bundled Noto Sans. font = "assets/fonts/Inter-Regular.ttf" # One face per script the bundled font does not cover. Ship these beside your # game and they work on a machine with no fonts installed at all. fallback_fonts = [ "assets/fonts/NotoSansJP-Regular.ttf", # Japanese "assets/fonts/NotoSansSC-Regular.ttf", # Simplified Chinese ] # Web export only: narrows the baked glyph atlas to the languages you ship. locales = ["en", "ja", "zh-Hans"] ``` That is the whole of an English, Japanese and Chinese game, on the desktop and in the web export alike: naming the faces is what lets the exporter bake them into the page, which is why a game that ships other scripts should name them even when the developer's own desktop already draws them. One thing to know before you copy it: `fallback_fonts` replaces the *whole* chain, including the face the engine ships for the box-drawing and mathematical characters, so put that face back if your game draws any of them (see {ref}`fallback-chain`). The rest of this page is what each key does, what you see when a character has no glyph anywhere, and how the web export differs. Three faces are shipped, named by the job they do rather than by typeface: | Face | Font | Used for | | --- | --- | --- | | `"ui"` | Noto Sans | Everything drawn that is not code: labels, buttons, menus, dialogs | | `"mono"` | Noto Sans Mono | Fixed-pitch text, where columns must line up | | `"fallback"` | DejaVu Sans Mono | Not drawn in directly: the last face the fallback chain asks | The Noto faces are under the [SIL Open Font License](https://openfontlicense.org/) and DejaVu is under the Bitstream Vera licence. Ask for one by name: ```python from simvx.core.text import bundled_font_path ui_font = bundled_font_path("ui") code_font = bundled_font_path("mono") ``` ## Using a different font One key in `simvx.toml`, and every string in the game is drawn in it: ```toml [rendering] font = "assets/fonts/Inter-Regular.ttf" ``` The path is relative to the directory holding `simvx.toml`, the same as every other asset path in a project, and an absolute path works too. A `.ttf` or a `.ttc` will do; a `.ttc` collection is loaded at its first face, so point the key at a single-face file when the collection's first face is not the one you want. If the file is not there the engine says so in the log and carries on with the bundled face, so a mistyped path costs you a typeface rather than all your text. To use whatever the player already has installed instead of the bundled face: ```toml [rendering] prefer_system_fonts = true ``` This is a *preference*, never an exclusion: if the search turns up nothing, the bundled font is still used, so no configuration can leave a game with no text. You can also switch the face from code, which is what you want if the player picks the font in an options menu: ```python from simvx.graphics.draw2d import Draw2D Draw2D.set_font("assets/fonts/Inter-Regular.ttf") ``` Call it whenever you like, including after the scene is built: widget auto-sizing is re-pointed at the new face in the same call, so every label, button and menu re-measures itself against the font it is now drawn in. Passing no arguments restores whatever the project settings resolve to. :::{note} Every widget measures the text it holds against the font's real glyph advances, so a button is as wide as the string inside it. Do not size a widget by multiplying a character count by a nominal advance: under a proportional face like Noto Sans, `i` and `W` differ by nearly four to one. Use `simvx.core.text.measure_text_width()` if you need a width yourself. ::: :::{warning} `[rendering] mono_font` is accepted by the project schema but is **not yet read**. It cannot be, yet: `Draw2D` holds one glyph atlas at a time and `draw_text` takes no face argument, so everything on screen is drawn in one face. See {ref}`fixed-pitch-text` for what that means for a terminal or a code view. ::: ## Supporting other languages Noto Sans holds 2965 characters: Latin down to Vietnamese and the IPA extensions, Greek, Cyrillic, the combining marks those need, and the punctuation, currency, arrows and letterlike symbols around them. No other script, and that is by design. Noto is a family of per-script faces plus a fallback chain, not one file that holds all of Unicode. Everything outside it -- CJK, Arabic, Hebrew, Devanagari, Thai, the box-drawing block -- comes from the fallback chain. :::{note} Glyphs are laid out one per character, left to right, with kerning. There is no shaping and no bidi reordering, so a script that needs its letters joined or its run reversed (Arabic, Hebrew, Devanagari, Thai) draws its characters correctly but in isolated forms and in logical order. Latin, Greek, Cyrillic, Japanese, Chinese and Korean need neither and are unaffected. ::: (fallback-chain)= ### The fallback chain When the face a game draws in has no glyph for a character, SimVX looks through a chain of fallback fonts and the first that has the glyph supplies it. The glyph is packed into the same atlas, so a borrowed character costs no extra draw call. Naming the chain yourself is the reliable option, and what you name is the **whole** chain, on the desktop and in the web bake alike: it replaces auto-detection outright, including the bundled floor described below. List every script you support. ```toml [rendering] fallback_fonts = [ "assets/fonts/NotoSansJP-Regular.ttf", # Japanese "assets/fonts/NotoSansSC-Regular.ttf", # Simplified Chinese "assets/fonts/NotoSansArabic-Regular.ttf", # Arabic "assets/fonts/NotoSansHebrew-Regular.ttf", # Hebrew ] ``` The same list can be set from code, for a game that chooses its faces at runtime. This is also where to put the bundled floor back if you want the box-drawing and mathematical characters it carries, since the TOML list has no way to name a file inside the installed package: ```python from simvx.core.text import bundled_font_path from simvx.graphics.text_renderer import get_shared_text_renderer get_shared_text_renderer().set_font_fallbacks([ "assets/fonts/NotoSansJP-Regular.ttf", "assets/fonts/NotoSansSC-Regular.ttf", bundled_font_path("fallback"), ]) ``` If you name no chain at all, one is assembled for you: 1. The bundled `"fallback"` face, DejaVu Sans Mono. It is the floor, and it is there on every machine because the engine ships it: Arabic, Armenian, Georgian, Lao, all of the box-drawing, block-element, geometric-shape and APL blocks, and about seventy per cent of the mathematical operators. It comes first so that the characters it covers are drawn from it on every machine; several CJK fonts carry the box-drawing block too, and asking them first would draw a box-drawing character in whichever face the player happened to have. 2. **CJK fonts installed on the machine**, which is why Japanese and Chinese usually render on a developer's desktop with no configuration at all. 3. **Nerd fonts installed on the machine**, for the private-use icon glyphs (Powerline separators, git and file-type icons) that no standard typeface carries. Only the first of those three is guaranteed. A game that leans on the other two draws correctly on the developer's machine and shows boxes on a bare container, a stripped desktop, or a player's machine that simply has no CJK font. **Ship the fonts you need.** Nothing in the chain goes looking for Hebrew, Devanagari or Thai on any machine, so those three are yours to ship whatever the developer desktop happens to have. ### What happens without it Nothing ever vanishes silently. A character that no font in the chain can draw is given the room it would have taken and a visible box is drawn in it, and the engine logs the character once: ```text No glyph available for U+05D0 HEBREW LETTER ALEF; drawing a missing-glyph box in its place. ``` The log line names the codepoint and its Unicode name, which is enough to identify which font you are missing. Once per character, not once per frame, so a screen full of unsupported text does not flood the log. The box on screen carries the same information. At body size it is a plain hollow box. At a size large enough to read, the codepoint is **written out inside the boxes**, one box per byte, each holding that byte's two hex digits. `05D0` draws as two boxes reading `05` and `D0`, so you can take the codepoint off the screen and look it up without going near a log file. (fixed-pitch-text)= ## Fixed-pitch text Text that sits on a character grid, a terminal or a tabular readout, tells the layout so by passing the grid's pitch: ```python renderer.draw_text(char, rect=(x, y, cell_w, cell_h), scale=scale, alignment="centre", vertical_alignment="centre", fit_to_width=True, cell_width=cell_w) ``` A character nothing can draw then occupies exactly one cell, scaled to fit, instead of taking the room a box asks for in proportional text. Every column after it stays where the grid puts it. Take the pitch from the **widest** character the face draws, not from a digit: ```python from simvx.core.text import measure_text_width printable = "".join(chr(code) for code in range(0x21, 0x7F)) cell_w = max(measure_text_width(char, font_size / 16.0) for char in printable) ``` A digit is only the right pitch for a face that draws every character on one body, and the face your text is drawn in is not necessarily one: the engine's `"ui"` face is proportional, and a project can point the renderer at anything. This is what `TerminalEmulator.cell_size` does, and why a terminal is legible whatever it is drawn in. :::{warning} `Draw2D` holds one glyph atlas at a time and `draw_text` takes no face argument, so **every widget draws in one face**, including the terminal and code views. Under a proportional face a character grid is correct but sparse: narrow characters sit alone in a cell as wide as `W`. Under the bundled `"ui"` face that is a real difference, not a rounding one. At the default 14px the terminal's cell is 13.1px wide against the 8.5px a fixed-pitch face gives it, so the same eighty columns take half as much width again, and a panel that sizes its grid to its own width fits about a third fewer columns. A terminal running a full-screen program hands that column count to the process, so the program lays itself out for the narrower grid. In a code view the same thing shows up as trailing comments at one source column no longer lining up with each other. If your game needs a tight monospace grid, draw the whole game in a fixed-pitch face: ```python from simvx.core.text import bundled_font_path from simvx.graphics.draw2d import Draw2D Draw2D.set_font(bundled_font_path("fallback")) # DejaVu Sans Mono ``` Use the `"fallback"` face, not `"mono"`, if the grid has to be exact. Noto Sans Mono gives every character the same advance in its outlines, but hinting rounds those advances to whole pixels and does not round every one of them the same way: at 48px its ASCII advances come out 28, 29 and 30 rather than a uniform 28.8, and the two-pixel spread is there at every size from 12px up, so a column count multiplied by a pitch drifts from where its glyphs land. DejaVu Sans Mono rasterises to a single advance at every size. A layout that places each character at its own cell, which is what the terminal does, is unaffected either way, because nothing accumulates. ::: ## The web export Web export works differently, and the difference is worth knowing before it surprises you. A browser cannot rasterise MSDF glyphs the way the desktop renderer does, so the exporter **bakes a glyph atlas ahead of time**. It scans your game's own source strings and the translation catalogues it finds in your asset directories (`.json`, `.toml` and `.csv`), and bakes exactly the characters it found. ### Your own typeface on the web Name the face and the exporter bakes it: ```bash simvx export web game.py --font fonts/MyFont.ttf ``` or once, in your project's `simvx.toml`: ```toml [export.web] font = "fonts/MyFont.ttf" ``` `--font` and `export_web(font=...)` read the path against your **game file's** directory; `[export.web] font` reads it against the directory holding `simvx.toml`, the way that file writes every other path. When the two are the same directory, so is the path. A path that names no file refuses the export: the page has no second face to fall back on, so a face asked for and quietly not baked is a page shipped in the wrong typeface with nothing to say why. Name the `.ttf` or `.otf`, not a `.woff2`: the bake runs here on the host, through FreeType, which usually cannot read the compressed web format. `export_web(font=...)` and `--font` take precedence over the project key, and both take precedence over the `[rendering] font` your game runs in on the desktop, so a game can run in one face and export in another. The font file itself does **not** travel with the page. The atlas is the delivery mechanism: the engine rasterises no face in the browser, so a face sitting in the bundle cannot become the typeface your text is drawn in, and base64 would charge a third over its own size to carry it there. A 2.6 MB face used to add 3.5 MB to a page whose text pipeline could not use a byte of it. So a face left in a `fonts/` directory beside the game, or inside a directory you passed to `--asset`, is dropped. The export reports every file it dropped and names the face it baked in their place; if nothing named a face at all, so the page is set in the fallback rather than the one you shipped, it says so as a warning. Naming a font file **by itself** as an asset (`--asset fonts/MyFace.woff2`) ships it anyway. That is not a way to change the game's typeface, which is settled by the bake; it is for a page that loads the face through the browser's own font machinery and draws the unbaked characters in it, which is what `app.set_dynamic_glyph_font(...)` further down is for. One face per page: the atlas holds a single typeface, so this is the face the whole game is set in. A character the game's own face has no glyph for is baked from the part of the fallback chain that is the same on every machine: the faces you named in `[rendering] fallback_fonts`, or, if you named none, the bundled `"fallback"` face. Both keys are read from **your game's** `simvx.toml`, not from wherever the export command was run, so the page bakes the faces the game runs in whichever directory you build from. Borrowed glyphs go into the same atlas at the same pixel size, so the page draws them exactly as the desktop does. This is why the box-drawing and block-element blocks and most of the mathematical operators survive the export with no configuration at all. The rest of the chain, the CJK and nerd fonts found by searching the machine, is deliberately **not** baked: it would put whatever the exporting developer had installed into the page and leave the next developer's build without it. So a game that draws Japanese on a developer's desktop with no configuration at all bakes no Japanese, and naming the face in `fallback_fonts` is what fixes that. It is the one case where the desktop is more forgiving than the export, so test the exported page rather than trusting the desktop run. Anything it could not predict, text typed by the player, text fetched at runtime, is **rasterised in the browser at runtime** from the fonts the player's machine has. It draws, but in whichever face the browser picks rather than in your game's typeface. The exporter tells you what it could not bake: ```text 11 character(s) have no glyph in the baked font (ときこんにちは世界です): the page rasterises them from the browser's own fonts at runtime, so they draw in whichever face the player's browser picks rather than the game's typeface. ``` By default the page asks the browser for `"Noto Sans", sans-serif`, so a machine that has Noto Sans lays unbaked text out at the same advances the atlas was baked at. If the page can load your own face, name it so that unbaked characters are drawn in your typeface too: ```python app.set_dynamic_glyph_font('"MyGameFont", sans-serif') ``` Call it before the first frame. The family has to be one the page can load, and registering it is yours to do: bundle the face by naming its file on its own (`--asset fonts/MyFace.woff2`, the one way a font file does travel with a page), then register it with the browser's own CSS Font Loading API (`js.FontFace` plus `document.fonts.add`) before the first frame. The engine does not do this for you: a bundled face is the browser's to read, not the engine's, and it only ever draws the characters the atlas had no glyph for. The rest of your text is still set in the baked face, so bake the same one (`--font`) if you want the two to match. ### Narrowing a large catalogue A game with a twenty-language translation catalogue bakes every glyph of all twenty by default, which is a large atlas for a game that ships three. Name the languages you actually ship: ```toml [rendering] locales = ["en", "ja", "zh-Hans"] ``` The exporter then reads only those columns of your catalogues. `en` is always included as a fallback, so a key missing a translation still bakes. Codes are matched as the catalogues write them: `en`, `fr_CA`, `zh-Hans` all work. You can pass the same list to the export API directly: ```python from simvx.web.export import export_web export_web("game.py", "game.html", locales=["en", "ja", "zh-Hans"]) ``` ## See also - {doc}`ui` -- the widgets this text is drawn in - {doc}`../web/export` -- the rest of the web export pipeline - {doc}`../package_resources` -- how bundled assets are resolved