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, а когда сервер падает из-за одной буквы «ж».