asciigen/ARCHITECTURE.md
2026-08-26 11:56:56 +03:00

109 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Архитектура
## Идея
Три независимых слоя, не знающих друг о друге:
```
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ 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
один раз и переиспользовать для всех превью.