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), вернётся в начальное состояние в конце цикла.

Правила бесшовного цикла

  1. Не использовать произвольное t * 0.9, t * 1.7, t * 2.2, если длительность не подобрана так, чтобы это давало целое число оборотов.
  2. Все частоты сделать целыми числами оборотов за цикл: sin(angle * 1), sin(angle * 2), sin(angle * 3).
  3. Не рендерить последний кадр как дубликат первого. Если в видео N кадров, лучше брать u = frame / N, где frame идёт от 0 до N-1; следующий невидимый кадр N совпал бы с первым.
  4. Орбиты мыслей должны завершать целое число оборотов: каждая мысль может иметь скорость 1, 2, -1, 3 оборота за весь loop.
  5. Моргание тоже должно быть периодическим, например через smooth pulse вокруг фиксированных фаз, которые повторяются каждый цикл.
  6. Фоновые волны должны зависеть от 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.png
  • last.png
  • diff.png
  • loop_score.txt

5. Crossfade как запасной трюк

Если математически замкнуть всё сложно, можно сделать скрытый crossfade: последние 0.5 сек смешивать с первыми 0.5 сек. Но это менее чистый способ. Лучше сначала сделать фазово-замкнутую математику.

Следующий практический шаг

Сделать eye_visualization_v2.py, где все параметры движения завязаны на loop = 2πu, а затем проверить разницу между первым и последним кадром. После этого можно перенести идею в HTML Canvas и встроить в Trip2G-страницу как экспортируемую живую карту глаза.