|

Одна сессия в Claude Code — и готова студия 2.5D параллакс-анимации: 3 ключевых фрагмента кода для покадрового детерминированного экспорта в MP4

Одна сессия в Claude Code — и готова студия 2.5D параллакс-анимации: 3 ключевых фрагмента кода для покадрового детерминированного экспорта в MP4

От автора: разбираем весь путь целиком — от промптов и формулы компенсации глубины резкости до покадрового экспорта через Playwright + ffmpeg. Показываю, как с помощью Claude Code собрать студию 2.5D-параллакс-анимации, которая экспортирует видео без потери кадров.

Многие, впервые делая анимацию с помощью AI-ассистента для программирования, сталкиваются с одним и тем же: в браузере превью идёт плавно, а при экспорте видео появляются подтормаживания, пропуски кадров и расхождение по длительности. Причина обычно в архитектуре: «экспорт» с самого начала не считали частью дизайна. В этой статье мы строим всё вокруг одной идеи — чистая функция renderAt(t) + покадровый seek со скриншотами. Разберём, как за одну сессию в Claude Code собрать студию 2.5D-параллакс-анимации и стабильно экспортировать её в MP4.

Это не очередная демонстрация в духе «попросите AI написать вам анимацию». Мы подробно разберём вывод формулы компенсации глубины резкости, один очень типичный баг со смещением и принципиальную разницу между воспроизведением в реальном времени и покадровым экспортом.

Главная ценность: дочитав статью, вы получите структуру промптов, которую можно использовать повторно, воспроизводимый конвейер экспорта и чек-лист для проверки, который легко перенести на другие проекты с программной генерацией видео.

claude-code-2-5d-parallax-motion-studio-deterministic-mp4-export-ru-image-0


Claude Code и студия 2.5D-параллакса: главное

Практика, о которой идёт речь, взята из статьи от 2026-11-09. Автор взял Claude Code с Opus 5.5 и за одну сессию собрал студию в одном HTML-файле, плюс скрипт экспорта на Playwright примерно в 25 строк. На ноутбуке автора 6 секунд видео в 720p (180 кадров) экспортировались примерно за 20 секунд. Конфигурацию железа автор не указал, так что это лишь ориентир по порядку величины.

Ключевой момент Описание Ценность
Граф сцены (Scene Graph) У каждого слоя есть глубина z, размер, позиция и HTML-содержимое Ощущение глубины без 3D-моделей
Таймлайн (Timeline) Ключевые кадры камеры и слоёв, на каждый отрезок своя кривая сглаживания Движение камеры можно редактировать и переиспользовать
Детерминированный рендерер renderAt(t) для любого момента t выдаёт один-единственный кадр Результат экспорта не зависит от скорости машины
Масштаб с компенсацией глубины Размер и смещение умножаются на 1 - z/perspective В покое картинка выглядит ровно так, как написано, а в движении параллакс получается сам собой
Покадровый конвейер экспорта Playwright делает seek на i/fps, снимает скриншот и передаёт его в ffmpeg На любой машине результат одинаковый, кадры не пропускаются

Как работает 2.5D-параллакс

Идея 2.5D в том, чтобы разложить плоские 2D-материалы по разной глубине в 3D-пространстве и провести сквозь них виртуальную камеру. Ближние к камере слои движутся быстро, дальние медленно. Глаз считывает эту разницу скоростей как глубину, это и есть параллакс (parallax). Моделирование, освещение и 3D-движок тут не нужны: хватает CSS-свойств perspective и preserve-3d прямо в браузере.

Разница между «студией» и «одноразовой анимацией» в том, есть ли все три компонента: граф сцены, таймлайн и детерминированный рендерер. Многие туториалы пропускают третий, а ведь именно от него зависит, будет ли экспортированный MP4 дёргаться. Поэтому, когда пишете первую строку промпта, сразу зафиксируйте его как жёсткое требование.

claude-code-2-5d-parallax-motion-studio-deterministic-mp4-export-ru-image-1


Настройка окружения и модели для 2.5D-студии в Claude Code

В источнике для первой сборки рекомендуют Opus 5.5. В официальном обзоре моделей Anthropic он позиционируется как модель для «длительной работы агентов в программировании и интеллектуальных задачах»: контекстное окно 1M токенов, максимальный вывод 128K. Официальная цена — 4 / 20 долларов за миллион входных / выходных токенов; актуальные цены смотрите на официальной странице.

Лучше сразу зафиксировать конкретный ID модели при запуске, чтобы в середине проекта переключение алиаса не изменило поведение:

claude --model claude-opus-5-5 --effort high

По умолчанию Opus 5.5 работает с effort medium. При первой сборке агенту нужно с первого раза правильно написать математику проекции, движок ключевых кадров и конвейер экспорта, поэтому effort стоит поднять до high. Потом, когда вы меняете только значения ключевых кадров или цвета, можно вернуться к medium и сэкономить токены.

Этап Рекомендуемый effort Типичные задачи Почему
Первая сборка high Математика проекции, движок ключевых кадров, скрипты экспорта Нужно согласовать сразу несколько подсистем
Итеративная доводка medium Правка ключевых кадров, смена цветовой схемы, добавление слоёв Изменения небольшие, логика уже устоялась
Интерфейс «описать кадр» low (для старта) Возвращает JSON с ключевыми кадрами по фиксированной схеме Задача короткая, структура вывода фиксирована

Насколько хорошо с такой же сборкой справятся другие модели и какой будет разница в стоимости — в источнике данных для сравнения нет. Если захотите сменить модель, прогоните один и тот же промпт на каждой, а затем сравните результаты по пунктам с помощью метода проверки по статичным кадрам, о котором речь пойдёт дальше.


Структура промпта для 2.5D-студии в Claude Code

Один конкретный промпт работает лучше, чем пять расплывчатых. В исходном материале спецификация сборки записана одним сплошным промптом. Ниже его версия, сохраняющая исходный смысл:

Build a 2.5D motion studio as one file, studio.html, with no dependencies.
- Scene: 6 layers (sky, moon, hills, city skyline, a title card "Ship it.",
  foreground foliage), each at its own z depth, using CSS perspective and preserve-3d.
- Camera: keyframes for x, y, z (dolly) and yaw (orbit around the card).
- Per-layer tracks: opacity, scale, y. Easings: linear, easeInOutCubic,
  easeInOutSine, easeOutBack.
- A timeline bar with keyframe markers, a playhead, click to scrub, space to play or pause.
- renderAt(t) must be pure in t. Expose window.__seek(t) that resolves after
  the frame paints, and a ?capture mode that hides the UI.
Then write export.mjs: Playwright opens ?capture, seeks every frame at 30 fps,
screenshots it and pipes PNGs to ffmpeg as H.264.

Этот промпт делится на несколько слоёв: сцена, камера, треки и easing-функции, интерактивный UI и, наконец, контракт экспорта. Самое важное здесь — последние две строки. Они требуют, чтобы renderAt(t) была чистой функцией от t, чтобы __seek возвращала управление только после отрисовки кадра и чтобы был режим ?capture, скрывающий UI. Так экспорт становится архитектурным ограничением, и агент проектирует код с оглядкой на него.

Если хотите перенести подход в свой проект, берите эту структуру как шаблон. Сначала чётко опишите, что рисуем, а в конце двумя-тремя строками жёстко зафиксируйте, как из этого детерминированно забирать кадры.


Разбор ключевого кода 2.5D, сгенерированного Claude Code

Вся студия занимает около 150 строк, и главное в ней — три фрагмента ниже.

Сэмплирование ключевых кадров и easing

Каждая дорожка — это массив ключевых кадров, отсортированный по времени. Функция sample() находит два ключевых кадра по обе стороны от t, считает прогресс с помощью easing-функции предыдущего кадра, а затем интерполирует все числовые поля:

function sample(keys, t) {
  if (t <= keys[0].t) return keys[0];
  for (let i = 0; i < keys.length - 1; i++) {
    const a = keys[i], b = keys[i + 1];
    if (t <= b.t) {
      const p = (easings[a.ease] || easings.linear)((t - a.t) / (b.t - a.t));
      const out = {};
      for (const k in b)
        if (typeof b[k] === 'number' && k !== 't') out[k] = a[k] + (b[k] - a[k]) * p;
      return out;
    }
  }
  return keys[keys.length - 1];
}

Эта функция не читает текущее время, не зависит от состояния предыдущего кадра и не использует случайные числа. Один и тот же t всегда даёт один и тот же набор значений. Отсюда и берётся детерминированность renderAt(t).

Компенсация глубины: основная формула 2.5D-параллакса

Идея такая: камера стоит на месте, а весь мир движется в обратную сторону. Каждый слой не просто сдвигается по оси z, но ещё и умножается на компенсирующий масштаб:

const PERSPECTIVE = 1200; // должно совпадать с perspective у #viewport
world.style.transform =
  `translate3d(${-cam.x}px, ${-cam.y}px, ${cam.z}px) rotateY(${-cam.yaw}deg)`;
const comp = 1 - L.z / PERSPECTIVE;
el.style.transform = `translate(-50%,-50%) ` +
  `translate3d(${x * comp}px, ${y * comp}px, ${L.z}px) scale(${comp * s})`;

Вот как это выводится. Пусть CSS-дистанция перспективы равна p. Точка на глубине z (отрицательное z означает, что она удалена от наблюдателя) проецируется на экран с масштабом p / (p - z). Берём p = 1200 и z = -900 и получаем 1200 / 2100 ≈ 0.571. Слой при этом сжимается примерно до 57% от исходного размера.

Чтобы это уменьшение скомпенсировать, умножаем на обратную величину: (p - z) / p = 1 - z/p = 1 + 900/1200 = 1.75. Именно это значение и хранится в переменной comp. Поскольку 0.571 × 1.75 = 1, при неподвижной камере каждый слой отображается с тем размером и в том положении, которые вы задали.

Откуда же берётся параллакс? Когда камера двигается, весь world сдвигается на одно и то же dx, но смещение каждого слоя на экране всё равно умножается на его собственный коэффициент p/(p - z). У дальних слоёв коэффициент маленький, поэтому они смещаются слабо, у ближних он больше, и они смещаются сильнее. Компенсация поправляет только композицию в статичном состоянии и не затрагивает разницу скоростей при движении. Эффект 2.5D держится именно на этой разнице.

z слоя Масштаб проекции p/(p−z) Коэффициент компенсации comp Относительная скорость при сдвиге камеры
0 (плоскость титульной карточки) 1.000 1.000 Базовая
-300 0.800 1.250 Медленнее
-900 0.571 1.750 Заметно медленнее
-1800 0.400 2.500 Почти неподвижный фон

(Значения в таблице посчитаны при p = 1200; строки для -300 и -1800 приведены как пример расчёта.)

Баг, который стоит запомнить: компенсировали только размер, но не смещение

В первой версии кода слои умножались только на scale(comp), а смещения по x/y на comp не умножались. В итоге перспектива сжала смещение дальних холмов, композиция поехала, а холмы поднялись и закрыли примерно две трети неба. Исправить просто: x и y тоже умножаются на comp, потому что перспектива масштабирует и позицию, и размер одним и тем же коэффициентом.

Интереснее другое: как этот баг нашли. Автор заметил проблему, посмотрев на один отрендеренный кадр, а при чтении кода её не увидел. Поэтому, прежде чем доверять любому движению камеры, попросите агента отрендерить три статичных кадра: начало, середину и конец.

claude-code-2-5d-parallax-motion-studio-deterministic-mp4-export-ru-image-2


Детерминированный покадровый экспорт 2.5D-параллакс-анимации в MP4

В чём принципиальная разница между воспроизведением в реальном времени и покадровым экспортом

Для живого превью используется requestAnimationFrame. Если машина загружена, кадры пропускаются. На этапе предпросмотра это терпимо, а вот при экспорте терять кадры нельзя. Запись экрана фиксирует то, что «в реальном времени как раз оказалось на экране». Стоит системе отстать на кадр, и вы получите повторяющийся кадр или пропуск.

Критерий Воспроизведение в реальном времени (rAF / запись экрана) Покадровый экспорт через seek
Источник времени Системные часы Номер кадра i / fps
Когда машина занята Потеря кадров, дубли Просто работает медленнее, картинка не меняется
Одинаковый результат на разных машинах Нет Да, на каждой машине получается одно и то же
Число кадров Не контролируется Строго равно duration × fps
Где применять Превью в редакторе, перетаскивание по таймлайну Финальный ролик, автоматическая сборка в CI

Минимальный скрипт экспорта

Время в экспортёре определяется номером кадра. Для каждого кадра сначала делаем seek, ждём, пока картинка отрисуется, снимаем скриншот и записываем PNG в стандартный ввод ffmpeg:

import { chromium } from 'playwright';
import { spawn } from 'node:child_process';
import { pathToFileURL } from 'node:url';

const out = process.argv[2] || 'out.mp4';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
await page.goto(pathToFileURL('studio.html').href + '?capture');

const { duration, fps } = await page.evaluate(() => window.__scene);
const ffmpeg = spawn('ffmpeg', ['-y', '-f', 'image2pipe', '-framerate', String(fps),
  '-i', '-', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', out],
  { stdio: ['pipe', 'ignore', 'inherit'] });

const frames = Math.round(duration * fps);
for (let i = 0; i < frames; i++) {
  await page.evaluate(t => window.__seek(t), i / fps);
  ffmpeg.stdin.write(await page.screenshot({ type: 'png' }));
}
ffmpeg.stdin.end();
await new Promise(r => ffmpeg.on('close', r));
await browser.close();

Несколько деталей, на которые стоит обратить внимание. window.__seek(t) должна резолвиться только после двух анимационных кадров — тогда скриншот гарантированно поймает свежий transform. image2pipe позволяет ffmpeg читать последовательность PNG из stdin. yuv420p нужен, чтобы получившийся файл нормально воспроизводился в браузере. Если в Playwright ещё не установлен браузер, один раз выполните npx playwright install chromium.

Эталонная реализация __seek и проверки статичных кадров (демонстрационный код)
// Внутри studio.html: детерминированный seek, возврат после отрисовки двух кадров
window.__seek = (t) => new Promise(resolve => {
  renderAt(t);                       // Чистая функция: зависит только от t
  requestAnimationFrame(() =>
    requestAnimationFrame(resolve)); // Убеждаемся, что стили применены и кадр отрисован
});
// stills.mjs: рендерим три статичных кадра — начало / середина / конец, сначала смотрим картинку, потом доверяем кадрированию
import { chromium } from 'playwright';
import { pathToFileURL } from 'node:url';

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
await page.goto(pathToFileURL('studio.html').href + '?capture');
const { duration } = await page.evaluate(() => window.__scene);

for (const [name, t] of [['start', 0], ['mid', duration / 2], ['end', duration - 1e-3]]) {
  await page.evaluate(t => window.__seek(t), t);
  await page.screenshot({ path: `still-${name}.png` });
}
await browser.close();

Оба фрагмента выше — демонстрационные реализации, написанные по мотивам справочных материалов. Окончательным вариантом считайте тот код, который агент сгенерирует в вашем проекте.

Результаты замеров автора: 180 кадров, 1280×720, 30 fps, длительность 6,0 секунды, итоговый MP4 весит около 420 КБ, всё заняло примерно 20 секунд. Городские башни на картинке смещаются меньше, чем тёмные холмы на переднем плане, а передний холм в конце полностью уходит за пределы кадра — это и есть эффект параллакса.

claude-code-2-5d-parallax-motion-studio-deterministic-mp4-export-ru-image-3


Добавляем в 2.5D-студию функцию «описать кадр»

Дальше можно дать сценаристу возможность вводить одну фразу с описанием кадра, а модель вернёт JSON с ключевыми кадрами. В исходном материале для этого напрямую вызывают Anthropic Messages API:

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 4096,
    "output_config": { "effort": "low" },
    "system": "Return only JSON: {\"camera\": [{\"t\",\"x\",\"y\",\"z\",\"yaw\",\"ease\"}], \"tracks\": {}}. Times in seconds, 0 to 6.",
    "messages": [{ "role": "user", "content": "Slow push-in, slight orbit right, title fades up in the first second." }]
  }'

Здесь есть две ловушки, в которые легко попасть. Первая: effort нужно указывать в output_config.effort. Вторая: Opus 5.5 всегда работает в режиме адаптивного мышления, и если передать thinking: {"type": "disabled"}, API вернёт ошибку 400. К тому же токены мышления тоже входят в max_tokens. Если выставить max_tokens слишком низко, JSON оборвётся посреди массива, а stop_reason будет равен "max_tokens".

Обрезанный ответ или ответ с лишним текстом приведёт к ошибке в sample() при рендеринге, поэтому для этого вызова лучше написать контрактный тест. В исходном материале для этого используют Apidog CLI: в той же сессии Claude Code создают и запускают тест, который проверяет, что код статуса равен 200, stop_reason не равен "max_tokens", возвращённый текст разбирается как JSON, а значение t в массиве camera растёт от 0 до 6. Того же результата можно добиться с помощью другого инструмента для тестирования API или собственного скрипта с проверками. После любого изменения system prompt или effort тест нужно прогонять заново, потому что оба параметра влияют на содержимое ответа.


Часто задаваемые вопросы (FAQ)

Q1: Чем 2.5D-параллакс отличается от настоящей 3D-анимации?

В 2.5D плоские 2D-материалы раскладываются по разной глубине, а потом их снимает движущаяся камера. Ощущение глубины возникает из-за разницы в скорости слоёв. 3D-модели, освещение и 3D-движок не нужны: хватит обычного CSS в браузере. Цена такого подхода в том, что камера не может сильно повернуться и зайти за слои.

Q2: Почему не записывать экран, а делать покадровый seek со скриншотами?

Запись экрана идёт в реальном времени, и любой тормозящий кадр превращается в пропущенный или повторяющийся. Если делать seek на `i / fps` и снимать скриншот, содержимое каждого кадра определяется только его номером. Поэтому результат одинаков на любой машине.

Q3: Откуда берётся коэффициент компенсации 1.75?

При дистанции перспективы 1200 и глубине -900 масштаб проекции равен 1200/2100 ≈ 0.571. Компенсация берётся как обратная величина: `1 — z/p = 1 + 900/1200 = 1.75`. Произведение двух коэффициентов равно 1, поэтому в статике слой возвращается к проектному размеру и положению.

Q4: Почему для первой сборки советуют Opus 5.5 и high effort?

За одну сессию нужно написать математику проекции, движок ключевых кадров и пайплайн экспорта. В исходных материалах рабочую студию получили с одним промптом и одним раундом исправлений. Для мелких правок потом хватит medium или ниже. Как поведут себя другие модели, публичных сравнений пока нет, так что придётся проверять самостоятельно.

Q5: Можно ли пользоваться студией без API-ключа?

Можно. Сама студия и экспорт в MP4 работают локально, ключ для них не нужен. Messages API вызывается только в функции «описать кадр», и для неё понадобится ваш собственный API-ключ Anthropic. Токены, потраченные при сборке через Claude Code, оплачиваются по тарифам того аккаунта, которым вы пользуетесь.

Q6: Какие у этого подхода известные ограничения?

Скорость покадровых PNG-скриншотов зависит от разрешения и сложности страницы. Цифра в 20 секунд из исходных материалов — это единичный результат на одном ноутбуке. CSS 3D может рендерить большие углы yaw и полупрозрачные наложения по-разному в зависимости от версии браузера. ID моделей, заголовки версии API и параметр effort со временем меняются, поэтому перед использованием сверьтесь с официальной документацией.

Q7: Как перенести «детерминированный рендеринг + проверку по статичным кадрам» на другие проекты с программно генерируемым видео?

Пройдитесь по пунктам этого чек-листа:

— Функция рендера принимает только `t`: она не читает системные часы, не зависит от состояния предыдущего кадра и не использует генератор случайных чисел без фиксированного seed
— Нужен seek-интерфейс, который возвращает управление только после того, как кадр полностью отрисован
— Нужен режим capture со скрытым UI и фиксированным размером viewport
— Число экспортируемых кадров должно строго равняться `round(duration × fps)`
— После любой правки камеры сначала рендерим три статичных кадра (начало, середину и конец), убеждаемся, что всё в порядке, и только потом экспортируем весь ролик
— Для JSON с ключевыми кадрами, который сгенерировала модель, пишем контрактные тесты: JSON должен парситься, время должно монотонно расти, а сам ответ не должен быть обрезан


Claude Code и 2.5D-параллакс-анимация: ключевые выводы (Key Takeaways)

  • Экспорт — часть дизайна: в последних строках промпта жёстко прописывайте чистую функцию renderAt(t) и контракт seek
  • Формула компенсации глубины: comp = 1 − z/perspective; на неё умножаются и размер, и смещения по x/y
  • Откуда берётся параллакс: компенсация исправляет только статичную композицию, а в движении слои всё равно двигаются с разной скоростью в зависимости от глубины
  • Сначала смотрим статичные кадры, потом доверяем камере: три картинки (начало, середина, конец) выявляют баги, которые не заметишь при чтении кода
  • Время привязано к номеру кадра: Playwright делает seek и скриншот, а ffmpeg через image2pipe кодирует всё в H.264
  • Настраиваем effort по этапам: для первой сборки ставим high, для тонкой доводки — medium, а работу со структурированным JSON-интерфейсом начинаем с low
  • Вывод модели проверяем контрактными тестами: так обрезка из-за max_tokens не сломает разбор ключевых кадров

Итоги

Чтобы собрать студию 2.5D-параллакс-анимации на Claude Code, нужно держать в голове три вещи:

  1. Архитектура: граф сцены, таймлайн и детерминированный рендерер. Именно третий компонент определяет качество готового ролика.
  2. Математика: коэффициент перспективной компенсации 1 - z/p влияет и на размер, и на смещение слоя. В статике композиция выглядит точно, а при движении параллакс появляется сам собой.
  3. Пайплайн: кадры перематываются через seek по формуле i/fps, скриншоты передаются в ffmpeg для кодирования, и результат не зависит от производительности машины.

Для начала советую повторить структуру промптов из этой статьи и прогнать шестисекундный тестовый ролик. Заодно обзаведитесь привычкой: прежде чем править камеру, посмотрите три статичных кадра. Дальше можно добавлять слои, подключать интерфейс «описание кадра» или перенести ту же идею детерминированности на другие программные решения для видео, например Remotion.


📚 Источники

  1. How to Build a 2.5D Motion Studio With Opus 5.5: первоисточник практики из этой статьи
  2. Ссылка: apidog.com/blog/build-2-5d-motion-studio-opus-5-5
  3. Описание: полные промпты, код студии, скрипт экспорта и пример интерфейса «описание кадра»

  4. Документация Anthropic по моделям и параметру effort: позиционирование моделей, контекст, цены и описание параметра effort

  5. Ссылка: docs.anthropic.com
  6. Описание: сверка ID модели claude-opus-5-5, заголовка с версией API и использования output_config

  7. Документация ffmpeg по форматам: описание входных форматов, в том числе image2pipe

  8. Ссылка: ffmpeg.org/ffmpeg-formats.html
  9. Описание: как читать последовательность PNG из stdin и какие параметры у кодирования H.264

  10. Документация Playwright: автоматизация браузера и API для скриншотов

  11. Ссылка: playwright.dev
  12. Описание: page.evaluate, page.screenshot и команда установки браузеров

Читайте также

Похожие записи