Eye visualization v2
Eye visualization v2
Пример генерации v2
Вторая версия уже собрана как loop: все движения завязаны не на линейное время, а на окружность loop = 2π * frame / N. Проверка показала, что переход последнего кадра к первому мягче среднего соседнего перехода: wrap_diff=5.2473, avg_adjacent=5.6927.
Минимальный принцип генерации:
N = FPS * DUR
for frame in range(N):
u = frame / N
loop = 2 * math.pi * u
gaze_x = math.sin(loop * 1) * 12 + math.sin(loop * 3) * 3
gaze_y = math.sin(loop * 2 + 1.1) * 6
wave = (
math.sin(r * 0.065 - loop * 2) +
math.sin(a * 7 + loop * 1) * 0.52 +
math.sin((xx + yy) * 0.035 + loop * 3) * 0.42
)
Файл генератора для этого конкретного видео: /tmp/make_eye_loop_v2.py. Результат в статье: eye_visualization_loop_v2.mp4.
Откуда появилась идея
Идея родилась из ограничения: нужно было быстро отправить не страницу, а готовый MP4. Поэтому я не пошёл через HTML/CSS, браузер, Steel и MediaRecorder, а выбрал прямой путь: процедурная генерация видео как последовательность пиксельных кадров.
Я взял образ с главной страницы проекта — глаз, который смотрит на мысли, орбиты, грибная живая среда — и перевёл его в простую математику: эллипс для глаза, окружности для радужки и зрачка, синусоиды для дыхания фона, орбиты для мыслей. Это похоже на маленький software shader, только написанный на Python и сразу скормленный в ffmpeg.
Ключевая мысль: если объект можно описать формулой, его можно не рисовать руками. Глаз — эллипс. Моргание — изменение высоты эллипса. Взгляд — синусоидальное смещение центра радужки. Мысли — светящиеся точки на орбитах. Грибная анимация — несколько наложенных волн.
Как была собрана первая версия
Скрипт создавал кадры 512×512 в raw RGB. Для каждого кадра он проходил по всем пикселям и считал цвет из нескольких слоёв:
- жидкий фон из радиальных, угловых и диагональных синусоид;
- кольца-мандалы как пики радиальной волны;
- белок глаза как эллиптическая маска;
- радужку и зрачок как круги со спицами;
- светящиеся мысли как маленькие орбитальные glow-точки;
- vignette, чтобы края уходили в темноту.
Потом кадры передавались в ffmpeg через stdin как rawvideo, а ffmpeg кодировал их в H.264 MP4. Это не HTML/CSS, а прямой raster pipeline: Python → raw RGB frames → ffmpeg/libx264 → MP4.
Как сделать видео полностью цикличным
Сейчас переход может быть заметен, потому что время t просто идёт от 0 до конца, а синусы и орбиты не обязаны прийти в ту же фазу на последнем кадре, с которой начался первый. Для бесшовного loop нужно сделать время не линейной дорожкой, а окружностью.
Главный приём: нормализовать прогресс кадра в фазу u от 0 до 1, а все движения выражать через полный оборот:
u = frame_index / total_frames
angle = 2 * pi * u
Тогда любое движение, построенное на sin(angle) и cos(angle), вернётся в начальное состояние в конце цикла.
Правила бесшовного цикла
- Не использовать произвольное
t * 0.9,t * 1.7,t * 2.2, если длительность не подобрана так, чтобы это давало целое число оборотов. - Все частоты сделать целыми числами оборотов за цикл:
sin(angle * 1),sin(angle * 2),sin(angle * 3). - Не рендерить последний кадр как дубликат первого. Если в видео
Nкадров, лучше братьu = frame / N, гдеframeидёт от0доN-1; следующий невидимый кадрNсовпал бы с первым. - Орбиты мыслей должны завершать целое число оборотов: каждая мысль может иметь скорость
1,2,-1,3оборота за весь loop. - Моргание тоже должно быть периодическим, например через smooth pulse вокруг фиксированных фаз, которые повторяются каждый цикл.
- Фоновые волны должны зависеть от
angle, а не от произвольного секундного времени, иначе жидкий фон будет прыгать на стыке.
Вариант v2: loop-time как окружность
Для v2 я бы переписал временную часть так:
N = FPS * DUR
for frame in range(N):
u = frame / N
loop = 2 * math.pi * u
gaze_x = math.sin(loop * 1) * 18
gaze_y = math.sin(loop * 2 + 1.2) * 8
wave = (
math.sin(r * 0.045 - loop * 2) +
math.sin(a * 9 + loop * 1) * 0.55 +
math.sin((xx + yy) * 0.025 + loop * 3) * 0.45
)
Так первый и последний визуальные состояния становятся соседними точками одной окружности, а не концами отрезка.
Ещё мысли для улучшения
1. Два слоя времени
Можно разделить время на:
- loop time — строго цикличный слой для всего, что должно бесшовно замыкаться;
- texture time — тоже цикличный, но с другим числом оборотов, чтобы движение не выглядело механическим.
Например фон делает 2 оборота, радужка — 1, мысли — 3, тонкие кольца — 5. Все числа целые, поэтому loop замыкается, но внутри выглядит богаче.
2. Больше “мыслей”, меньше шариков
Сейчас мысли — светящиеся точки. Следующий шаг: сделать их похожими на маленькие фразы, руны или обрывки сигналов. Для Python-пайплайна это неудобно без PIL, но в HTML Canvas легко: рисовать слова с blur/glow и пускать их по орбитам.
3. HTML Canvas как лучший путь
Для будущей версии лучше перейти на HTML Canvas или WebGL shader:
- проще делать текстовые мысли;
- проще настраивать визуал в браузере;
- можно добавить кнопку
Export MP4; - Steel сможет открыть страницу, нажать кнопку и скачать видео;
- тот же код может жить на Trip2G как интерактивная визуализация.
Это уже будет ближе к демо глаза и к главной странице проекта.
4. Loop preview checker
Нужен маленький тест качества loop: взять первый кадр и последний кадр, посчитать разницу пикселей. Если разница высокая — loop заметно дёрнется. Если низкая — можно публиковать.
Для v2 можно автоматически сохранять:
first.pnglast.pngdiff.pngloop_score.txt
5. Crossfade как запасной трюк
Если математически замкнуть всё сложно, можно сделать скрытый crossfade: последние 0.5 сек смешивать с первыми 0.5 сек. Но это менее чистый способ. Лучше сначала сделать фазово-замкнутую математику.
Следующий практический шаг
Сделать eye_visualization_v2.py, где все параметры движения завязаны на loop = 2πu, а затем проверить разницу между первым и последним кадром. После этого можно перенести идею в HTML Canvas и встроить в Trip2G-страницу как экспортируемую живую карту глаза.