6.8 KiB
6.8 KiB
Архитектура
Идея
Три независимых слоя, не знающих друг о друге:
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ CLI/main │ --> │ asciigen (core) │ <-- │ tui │
└─────────────┘ └──────────────────┘ └─────────────┘
знает о обоих не знает не знает
ни о CLI, ничего, кроме
ни о TUI интерфейса Renderer
asciigen(корневой пакет) — вся логика рендера. Ничего не знает про терминал, ввод/вывод, флаги. Чистые функции + два типа-рендерера.tui— работает только с интерфейсомasciigen.Renderer. Не знает, картинка перед ним или текст — поэтому один и тот же экран выбора обслуживает оба сценария без дублирования кода.cmd/asciigen— единственное место, которое "склеивает" всё вместе: парсит аргументы, решает какой Renderer создать, передаёт его в TUI.
Такое разделение даёт две вещи, ради которых это всё затевалось:
- Ядро можно импортировать в другой Go-проект (auspex, rimefrost) как библиотеку, вообще не трогая TUI/CLI.
- 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 один раз и переиспользовать для всех превью.