Summary
Один вечер, один промпт и полная цепочка «а давай ещё вот так». Я хотел красивую анимацию, в которой надпись рассыпается на частицы и снова собирается. В итоге получился браузерный инструмент с экспортом в MP4 и GIF и четыре бага, каждый из которых научил чему-то новому.
Начало: одна просьба
Я попросил написать шейдерную функцию для Three.js: двадцать тысяч и больше частиц, текст рассыпается в облако и собирается обратно. Бесконечный цикл.
Идея была простая: красивая анимированная аватарка для Telegram. Но красота начинается там, где кончается «первая версия».
Баг 1. Переменная, которой ещё нет
Первый запуск: в консоли ReferenceError: Cannot access 'tempColor' before initialization.
Расшифровка: я обратился к переменной раньше, чем она была создана. Порядок инициализации в JavaScript - вещь строгая. Исправляется перестановкой двух строчек. Но каждый раз, когда видишь эту ошибку, вспоминаешь, почему линтеры такие занудные.
Баг 2. Telegram хочет MP4, а браузер умеет WebM
Следующая мысль: надо сохранять результат. В браузере это делается через MediaRecorder, и он по умолчанию записывает WebM.
Но Telegram для анимированных аватарок принимает только MP4. Преобразовывать в браузере - больно. Значит, нужен ffmpeg, а он в браузере не живёт.
Решение - поднять маленький локальный Node.js-сервер с маршрутом /api/export-telegram-mp4. Браузер отправляет ему запись, сервер гоняет её через ffmpeg и отдаёт готовый MP4.
А потом выяснилось, что ffmpeg, установленный через winget, не попал в PATH текущей сессии. Пришлось перезапустить терминал. Классика.
Баг 3. Кириллица в заголовке HTTP
Захотелось добавить экспорт в GIF через тот же конвейер. Заодно я решил передавать клиенту предупреждения в заголовке ответа, например «качество снижено».
Сервер упал с ошибкой: Invalid character in header content ["X-Export-Warning"].
Причина: значения HTTP-заголовков в Node должны быть в ASCII. Русская буква в предупреждении - и вся цепочка падает. Это ограничение протокола, а не баг программы, и обходится просто: писать предупреждения латиницей или передавать их в теле ответа.
Баг 4. Настройка, которая ничего не меняла
Самый коварный случай. В интерфейсе есть ползунки: частота кадров, битрейт, размер. Я менял их, смотрел на результат и не видел разницы.
Оказалось, настройки рисовались на панели, но не доходили до команды ffmpeg. Интерфейс врал: он показывал ручку, которая ни к чему не подключена.
Теперь у меня есть правило для любых настроек: сначала проверь, что значение доходит до места применения, а потом уже красиво рисуй. Самое простое - вывести итоговую команду в лог.
Интерфейс тоже менялся
Первая версия панели была длинной прокручиваемой лентой настроек, где превью терялось. Я присылал ИИ скриншоты с пометками, что мешает, и он переделывал. В итоге панель стала двухколоночной: настройки слева, живое превью справа. Для подбора практик интерфейса агент подключал документацию через Context7.
Под конец я попросил сгенерировать для проекта файлы AGENTS.md и README.md, чтобы в следующий раз агент (или я сам) не вспоминал, как всё устроено.
Из чего состоит итог
- Three.js - сами частицы и шейдеры;
- MediaRecorder - запись анимации в браузере;
- Локальный Node.js-сервер - мост к ffmpeg;
- ffmpeg - конвертация в MP4 и GIF;
- localStorage - сохранение пресетов.
Что я понял
Любой «маленький» креативный проект очень быстро превращается в проект про форматы и ограничения. Я собирался поиграть с шейдерами, а провёл вечер, разбираясь с кодеками, заголовками и путями.
И это нормально. Именно на таких вечерах и учишься больше всего: не на уроке про HTTP, а когда сервер падает из-за одной буквы «ж».