109 lines
6.8 KiB
Markdown
109 lines
6.8 KiB
Markdown
# Архитектура
|
||
|
||
## Идея
|
||
|
||
Три независимых слоя, не знающих друг о друге:
|
||
|
||
```
|
||
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
|
||
│ CLI/main │ --> │ asciigen (core) │ <-- │ tui │
|
||
└─────────────┘ └──────────────────┘ └─────────────┘
|
||
знает о обоих не знает не знает
|
||
ни о CLI, ничего, кроме
|
||
ни о TUI интерфейса Renderer
|
||
```
|
||
|
||
- **`asciigen` (корневой пакет)** — вся логика рендера. Ничего не знает
|
||
про терминал, ввод/вывод, флаги. Чистые функции + два типа-рендерера.
|
||
- **`tui`** — работает только с интерфейсом `asciigen.Renderer`. Не знает,
|
||
картинка перед ним или текст — поэтому один и тот же экран выбора
|
||
обслуживает оба сценария без дублирования кода.
|
||
- **`cmd/asciigen`** — единственное место, которое "склеивает" всё вместе:
|
||
парсит аргументы, решает какой Renderer создать, передаёт его в TUI.
|
||
|
||
Такое разделение даёт две вещи, ради которых это всё затевалось:
|
||
1. Ядро можно импортировать в другой Go-проект (auspex, rimefrost) как
|
||
библиотеку, вообще не трогая TUI/CLI.
|
||
2. TUI не придётся переписывать, если добавится третий вид источника
|
||
(например, ASCII-арт из ANSI-артов или QR-кодов) — он просто реализует
|
||
`Renderer`.
|
||
|
||
## Поток данных: картинка → ASCII
|
||
|
||
```
|
||
файл (png/jpg)
|
||
│ image.Decode() [cmd/asciigen/main.go]
|
||
▼
|
||
image.Image (декодированная картинка)
|
||
│ NewImageRenderer(img)
|
||
▼
|
||
ImageRenderer.Previews()
|
||
│ для каждого пресета из defaultImagePresets():
|
||
│ 1. resize() — уменьшить до previewWidth=60,
|
||
│ высота домножается на 0.5 (компенсация
|
||
│ формы символа терминала, он ~2:1)
|
||
│ 2. render():
|
||
│ for each pixel:
|
||
│ a. RGBA() → r8,g8,b8 (0..255)
|
||
│ b. luma = 0.299R + 0.587G + 0.114B (яркость по глазу)
|
||
│ c. gamma-коррекция: corrected = luma^gamma
|
||
│ d. invert (если задан)
|
||
│ e. индекс в палитре = corrected * (len(palette)-1)
|
||
│ f. если opts.Color — обернуть символ в ANSI \x1b[38;2;R;G;Bm
|
||
▼
|
||
[]Variant{ID, Label, Preview} — 5 готовых превью
|
||
│ TUI показывает список, пользователь листает ↑/↓
|
||
▼
|
||
Enter → ImageRenderer.Full(id)
|
||
│ те же presets[id], но Width = fullWidthFor(img) (до 200 симв.)
|
||
▼
|
||
строка ASCII в полном размере → печатается в stdout
|
||
```
|
||
|
||
## Поток данных: текст → баннер
|
||
|
||
```
|
||
строка текста
|
||
│ NewTextRenderer(text)
|
||
▼
|
||
TextRenderer.Previews()
|
||
│ для каждого шрифта из defaultFonts():
|
||
│ go-figure.NewFigure(text, font, true).String()
|
||
▼
|
||
[]Variant{ID: имя шрифта, Preview: баннер}
|
||
│ TUI, тот же экран выбора, что и для картинок
|
||
▼
|
||
Enter → TextRenderer.Full(font) — фактически тот же рендер,
|
||
FIGlet не теряет детализацию на "превью", в отличие от картинки
|
||
▼
|
||
готовый баннер → stdout
|
||
```
|
||
|
||
## Почему так, а не иначе
|
||
|
||
- **Один и тот же TUI для картинок и текста.** Оба рендерера отдают
|
||
`[]Variant` одинаковой формы — TUI работает с абстракцией, не зная
|
||
деталей. Не пришлось бы писать два экрана выбора.
|
||
- **Превью и полный рендер — один и тот же код (`render()`),
|
||
разный `Width`.** Не два алгоритма, а один параметризованный —
|
||
меньше мест для расхождения багов между "как выглядит" и "что получишь".
|
||
- **Gamma и Invert — это математика над яркостью, а не отдельные ветки
|
||
кода для каждого пресета.** Пресеты в `options.go` — это просто наборы
|
||
значений полей `ImageOptions`, а не пять разных функций рендера.
|
||
- **`resize()` компенсирует форму символа (`* 0.5` по высоте).** Без этого
|
||
картинка выглядит вытянутой по вертикали — символ терминала выше, чем
|
||
широкий (~2:1), и это единственное место, где это учитывается,
|
||
а не размазано по остальному коду.
|
||
|
||
## Известные ограничения (сознательно не решались в скелете)
|
||
|
||
- `fullWidthFor()` — простой кап в 200 символов по ширине оригинала,
|
||
без учёта реального размера терминала пользователя (можно прокинуть
|
||
через `golang.org/x/term.GetSize`, если понадобится).
|
||
- ANSI-цвет включает "сброс" (`\x1b[0m`) после каждого символа — рабочий,
|
||
но не самый компактный вариант; для больших картинок можно оптимизировать,
|
||
сбрасывая только при смене цвета.
|
||
- Нет кэширования уменьшенной копии картинки между пресетами — каждый
|
||
пресет по новой делает `resize()` от оригинала. Для маленьких превью
|
||
(60 символов) это не заметно, но при желании легко вынести resize
|
||
один раз и переиспользовать для всех превью.
|