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