# Текст и шрифты — `$.font`, `$('<text>')`, `$('<ui.label>')`

Текст в движке рисуется **своим растеризатором глифов**: stb_truetype режет
шрифт в атлас, атлас уезжает в GPU-текстуру, а каждая буква становится обычным
спрайтом в общем батче кадра ([`src/font.c`](https://github.com/Nikide/russiano2d/blob/main/src/font.c)).

```js
$.ready(() => {
    $.font.load('title', 'assets/fonts/NotoSans-Bold.ttf');   // своё семейство

    $('<text>', { id: 'hint', text: 'Прыгай на Space', size: 24 })
        .at(320, 120).color('#ffd166').appendTo($.world);

    $('<ui.label>', { id: 'score', text: 'Очки: 0' })
        .at(60, 30).font('title').appendTo($.ui);
});
```

Почему так, а не «текстом поверх сцены». Раньше строка складывалась в очередь и
рисовалась шрифтом ImGui поверх кадра: у неё не было z-порядка и обрезки, она не
подчинялась свету, туману и пост-обработке, а в сборке без ImGui пропадала
вовсе. Теперь текст — часть сцены, поэтому он получает всё то же, что спрайты:
`layer`/`depth`, режимы смешивания, шейдеры узлов, обрезку камерой, тряску,
зум и попадание на скриншот агента.

---

## 1. Шрифты

Шрифт — файл `.ttf`/`.otf` и имя семейства, под которым он живёт в движке.

| Функция | Что делает |
|---|---|
| `$.font.load(name, path)` | загрузить файл как семейство; без имени берётся имя файла |
| `$.font.families()` | все семейства, известные движку |
| `$.font.uploaded()` | те, что загрузила игра через `$.font.load` |
| `$.font.default()` | семейство по умолчанию (или `null`) |
| `$.font.atlas()` | `{ glyphs, atlas_w, atlas_h, drawn, first_sprite }` |

**Автозагрузка.** При старте движок сам берёт первый `.ttf`/`.otf` из
`assets/fonts` (сначала из груза игры, потом с диска) и делает его семейством
`default`. Поэтому текст работает без единой строки настройки; `$.font.load`
нужен, только если хочется второе начертание или свой файл.

Путь — как у `.sprite()`: от корня запуска, абсолютные принимаются как есть.
Повторный вызов с тем же именем перезагружает шрифт (удобно при hot reload).

```js
$.font.load('title', 'assets/fonts/NotoSans-Bold.ttf');
$.font.define('hud',   { size: 20, color: '#c8d4e8', font: 'title' });
$.font.apply('#score', 'hud');
```

Стиль умеет нести поле `font` — семейство приезжает вместе с размером и цветом
(§4 в [font.md](highlevel/font) описывает сами стили).

## 2. Семейство на узле: `.font(name)`

Семейство наследуется: его берёт ближайший предок с `.font()`, иначе шрифт по
умолчанию.

```js
$('<ui.col>', { id: 'panel' }).font('title').appendTo($.ui);
$('<ui.label>', { text: 'Заголовок' }).appendTo($('#panel'));   // уже title
$('#hint').font('title');      // поставить
$('#hint').font();             // прочитать (своё или унаследованное)
$('#hint').font(null);         // снять — снова шрифт по умолчанию
```

## 3. Где текст живёт

| Узел | Координаты | Кегль | Выравнивание |
|---|---|---|---|
| `<text>` | мировые (камера влияет) | `size` × зум камеры | `attrs.align` |
| `<ui.label>`, `<ui.button>`, `<ui.bar>`, `<ui.dialog>` | окна | `size` | `center` (у метки — `attrs.align`) |

Текстовый узел **сам получает габарит**: при первом рисовании строка меряется
тем же шрифтом, которым будет нарисована, и `w`/`h` узла становятся её
размером. Без этого отсечение по камере считало бы `<text>` невидимым (у него
нет спрайта), а сортировка по Y — стоящим в одной точке. Габарит пересчитывается
при смене текста, кегля или семейства.

```js
const label = $('<text>', { text: 'Босс', size: 32 }).at(400, 100);
console.log(label.attr('w'), label.attr('h'));   // размер строки на экране
```

## 4. Кегль и зум камеры

Глифы растеризуются под **мировой** кегль, а зум камеры применяется как
масштаб спрайта. Поэтому текст не «печётся» заново на каждом значении зума:
`size: 24` даёт один набор глифов в атласе независимо от того, 1× камера или
2×. Плата — при сильном приближении кромки чуть мягче, чем у идеальной
растеризации под каждый кегль.

Кегли квантуются: 6..32 — по пикселю, дальше шагом 4/8/16. Это компромисс между
качеством и размером атласа; при расхождении меньше полупикселя спрайт просто
масштабируется.

## 5. Измерение

```js
$.font.measure('Счёт: 10', 'hud');           // ширина строки размером стиля
$.font.width('HP', 24, 'title');             // ширина явным кеглем и семейством
```

Обе функции внутри зовут нативный `engine.measureText`
([internal/NATIVE.md](internal/NATIVE)); игре доступны только они.

Измерение и рисование идут одним кодом (кернинг, пробелы, выносы), поэтому
результат совпадает: если строка помещается по измерению, она поместится и в
кадре. Символы нужного кегля растеризуются в момент измерения.

## 6. Атлас

Глифы складываются в один RGBA-атлас с полочной упаковкой. Он растёт сам:
сначала 256×256, при нехватке — вдвое по ширине (до 2048) и по высоте (до
4096). Новые глифы уезжают в GPU в начале следующего кадра.

* символ, растеризованный **впервые**, появляется в кадре со следующего кадра:
  первый кадр нового кегля может показать не все буквы;
* если символа в шрифте нет, курсор всё равно двигается (пустое место), строка
  не «слипается»;
* `$.font.atlas()` показывает, сколько глифов уже нарезано и какой атлас занят —
  удобно ловить «шрифт не нашёлся» и рост памяти.

## 7. Ограничения

* **только однострочный текст**: переносов и многострочной вёрстки нет;
  `attrs.lineHeight` из `$.font` пока никем не читается;
* **начертания не синтезируются**: жирный и курсив — отдельные файлы
  (`$.font.load('bold', '…-Bold.ttf')`), наклон/обводка не подделываются;
* **шейпер не подключён**: сложные системы письма (арабский, деванагари) и
  лигатуры не раскладываются — для них нужен HarfBuzz;
* **RTL не поддержан**: направление всегда слева направо;
* атлас растёт до 2048×4096 (около 32 МБ RGBA); при переполнении новые глифы
  рисуются пустыми с записью в журнал;
* текст — спрайты, поэтому он попадает под пост-обработку и режимы смешивания
  ровно как спрайты, включая аддитивные надписи и свечение.

## 8. Проверка

```bash
# интеграционный прогон: автозагрузка, измерение, растеризация, узлы, наследование
python3 tests/agent/highlevel_text_test.py
```

Тест проверяет, что шрифт нашёлся сам, что ширина растёт с длиной строки и с
кеглем, что глифы появляются в атласе, что `<text>`/`<ui.label>`/`<ui.button>`
создаются и что семейство наследуется ребёнком от родителя.
