Привет, Хабр! Возникла необходимость сделать анимации на TFT дисплей ST7789 под управлением ESP32. Учитывая общую нагрузку на МК и требования к «живости кадра», выбор пал на UI-анимацию в противовес GIF-спрайтам и иже с ними.
Что-то хабр матерится на ссылку, вот если не подгрузится: https://vk.ru/clip-243078440_456239017
Логика была такой: нужен самый дешевый по ресурсам (они нужны для работы остальных модулей проекта) но без потери качества способ анимировать лицо, которое в последствии могло бы например следить зрачками за движущимися объектами в кадре. Конечно, это однозначно UI анимация. И тут выясняется, что в отличие от веба (SVG для CSS и Canvas), выбор для C++ весьма скуден. Помимо прочего, я совсем не кодер и честно говоря, не особо себе представлял, как можно «рисовать кодом». Но при таком энтузиазме нет нерешаемых задач.
Суть моего решения была в том, чтобы написать всё на Canvas (как анимации на сайтах), а затем через ИИ портировать на C++. HTML я могу буквально сразу отрендерить в браузере и посмотреть, что там наклякалось. Этот путь показался гораздо проще прямой записи на C++.
Уверен что существуют для этого какие-то визуальные редакторы, но как писал выше — это не мой профиль. Возможно у Вас получился бы лучше этот шаг, но я выбрал путь промптинга как единственно возможный:
1. Первые наброски (DeepSeek в чате)

Первый запрос звучал примерно так:
«Сделай эмулятор дисплея 240×280 в HTML обёртке, нарисуй на нём 2 глаза используя Canvas примитивы, используй цвет “циан”, фон чёрный, примени фильтр заливки черными горизонтальными линиями через строку в стиле Atari аркадных автоматов на площадь всего дисплея поверх всех слоев, добавь ползунки регулировки масштаба/удалённости/высоты глаз и json вывод значений»

Как видно, я прямо на скриншоте в редакторе смартфона прикинул корректировку формы, и ИИ сразу подкорректировал, создав усечённый эллипс и новые ползунки (угол наклона усечения, размер зрачков и т.д.). Немного покрутив которые, мы получили:
Точно также были написаны сценарии с доп. регулировками и утверждением эталонных значений. Где нужно, я присылал быстрые зарисовки спрайтов — мультимодальные модели отлично с ними работают. Вот пример скримера-пасхалки на галагу:

Да, если использовать примитивы разных цветов, как линии в этом примере, то LLM быстрее въезжает, как именно реализовать объект и сколько фигур для этого использовать. Где-то на этом этапе я понял важную вещь: ИИ отлично понимает не только картинки, но и ASCII-арт.
2. ASCII PixelArt как методология
Вы наверняка видели в сети как некие тролли, во избежание бана публикуют всякое непотребство не изображениями, а как бы картинкой из символов? Оказывается это реальная и очень старая технология — ASCII PixelArt. Мне кажется в эпоху LLM Она может пережить второе дыхание т.к. это не просто «картинка из символов», а полноценный формат описания, где каждый символ — это бит. Например использование o для контура и x для фигуры:
ooooo ooooo ooxxxxxoo ooxxxxxoo ooxxxxxxxxxxxxxxxxxxxoo oxxxxxxxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxxxo oxxxxxxxxxxxxxo oxxxxxxxxxxxo oxxxxxxxxxo oxxxxxxxo oxxxxxo oxxxo oxo o
Добавляя новые символы можно обозначить цвета или «подключаемые» пиксели для спрайта движения.
Плюс этого подхода в контуре «текст — человеко/ИИ-читаемость — изображение»:
-
Компактно: легко вставить в
PROGMEMкак массив байт (Continuous Bit Packing, 1bpp). -
Наглядно: можно рисовать прямо в чате с ИИ, сразу видно форму и симметрию.
-
Гибко: легко масштабировать и менять по строкам.
3. Мастерские: каждая эмоция — отдельный стенд

Вот это была основная работа. Писать одну большую HTML сразу — путь в никуда: параметров (ползунков) десятки, они конфликтуют, а правя одно, ломаешь другое, да и файл перестаёт умещаться в 1 инференс LLM.
Поэтому каждая эмоция с сложным сценарием сидела в своём отдельном HTML-стенде. Вот, например, «Идея» — eye_idea.html:
Что здесь есть:
Только две эмоции: neutral и 💡 Idea, ползунки редактируют только IDEA. Можно сначала отредактировать форму обьекта в эталон, а потом переписать слайдеры под сценарии.
Полный набор параметров IDEA: размеры, смещения, растяжения, время появления, свечение, амплитуда «дыхания», количество лучей, длина, толщина, скорость бега лучей, скважность.
Кнопки «Моргнуть», «Сканирование», «Жалюзи», «Глитч» — чтобы сразу видеть, как лампочка и глаза реагируют на наложение модификаторов.
«Скопировать JSON» — выплёвывает эталонные значения каждого ползунка для фиксации и последующего порта.
Цикл: покрутил ползунки → увидел, как лампа падает сверху с ease-in, «дышит», лучи бегут по верхним 2/3 контура, глаза сжимаются в точку → сохранил в пресет → JSON пошёл в след промпт: «фиксируй как эталон».
4. Сшивка в единый массив
Когда все эмоции устаканились, наступил момент склейки. Появился единый eye_full_optimized.html (будет в файлах статьи). Были убраны все слайдеры регулировок. Этот файл и стал эталоном для порта. И именно из него потом буквально вставлялись объекты в emotions.cpp.
5. Оптимизация под ESP32
Браузер стерпит плавающую точку, sin/cos на лету и quadraticCurveTo. ESP32 — нет. Чек-лист:
-
Кривые Безье → дуги окружности. В HTML использовал
quadraticCurveToдля некоторых анимаций. На C++ вычислять Безье каждый кадр — дорого. Значит реализуем через ближайший аналог:circle_through_3ptsстроит окружность через начало, конец и середину кривой, математика запекается один раз, рендер идёт через обычные дуги. -
Свечения → LUT-спрайты. Радиальные градиенты на МК невозможны. Сгенерировал альфа-каналы свечений (лампочка, месяц, аура) один раз, сохранил в PSRAM. В рантайме — просто
raster_blit_alpha_tinted(). -
Запекание констант. С отладочными ползунками выкинуты и их переменные, в
emotions.cppтолько жёсткиеfloat-значения. -
Отказ от LVGL и ручной порт шрифта. LVGL тянет зависимости и рисует с антиалиасингом (как я понял какое-то сглаживание), который на сканлайнах вреден и жрёт CPU. Поэтому через ИИ я выдрал глифы Bender Black попиксельно и портировал в бинарный формат 1bpp (Continuous Bit Packing). Рендер буквы — цикл по байтам и
blend_pixel(). -
Дешёвый глитч. Для сбоя матрицы не стал делать сложную логику. Беру случайный бэнд строк и делаю
memmoveв стороны. Быстро, грязно, киберпанк.
6. Финальный контроль (агент MiniMax)
Написать код — полдела. Главный вопрос: как доказать, что на железе картинка не «поплыла» после всех оптимизаций?
К этому моменту C++ был разбросан по десятку файлов, а простая чат-модель уже не справлялась: контекст переполнялся. Тут пригодился агентный ИИ ( в моем случае MiniMax): в отличие от чат-бота, он сам ходит по проекту, читает файлы, собирает их воедино и отлаживает.
Он собрал дифф-стенд: side-by-side сравнение кадра реальной прошивки и эталонного Chromium рендера из изначального Canvas, Попиксельно сравнил что отрисовывает C++ код с исходным HTML, поправил расхождения до незначительных.
Архитектура анимации: слои
Нельзя жёстко привязывать всё к одному состоянию. Код разделён на слои:
-
База (
emotions.cpp) — 16 эмоций с базовыми параметрами глаз и зрачков. -
Idle-слой (
idle.cpp) — 7 микросценариев в режиме простоя, накладываются поверх базы как дельты. -
Сценарии (
scenarios.cpp) — сложные оверлеи -
Имитация жизни : Дыхание (изменение размеров\пропорций) глаз, саккады зрачков и т.д.
Финал:
Пока железо приехало код уже был готов, осталось разрешить несколько аппаратных вопросов дисплея ( ряды битых пикселей, инверсия цветов, BGR-RGB, сегментированные градиенты, двойной буфер) но и тут Дипсик справился.
Связка «Canvas → LLM (чат) → отдельные стенды по сценариям → сшивка в единый файл → агент (дифф-стенд) → C++» даёт быстрые итерации и прозрачную оптимизацию. Минусы — придется терпеть затупы ИИ т.к. они туго воспринимают стилизацию (всё время пытался сделать пропорциональные и четко разделенные глаза\зрачки\веки, убрать сканлайны как баг и т.д.)
Исходники (эталон HTML, дифф-стенд) в папке: https://drive.google.com/drive/folders/1YLwIt4DilwY0FQsWIU31kAiH7Wt_UI65?usp=drive_link . Открывать в браузере т.к. гугл докс не отображает. Жду вопросы в комментиариях)

ссылка на оригинал статьи https://habr.com/ru/articles/1105154/