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

6.8 KiB
Raw Permalink Blame History

Архитектура

Идея

Три независимых слоя, не знающих друг о друге:

┌─────────────┐     ┌──────────────────┐     ┌─────────────┐
│   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 один раз и переиспользовать для всех превью.