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

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 дёргаться. Поэтому, когда пишете первую строку промпта, сразу зафиксируйте его как жёсткое требование.

Настройка окружения и модели для 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, потому что перспектива масштабирует и позицию, и размер одним и тем же коэффициентом.
Интереснее другое: как этот баг нашли. Автор заметил проблему, посмотрев на один отрендеренный кадр, а при чтении кода её не увидел. Поэтому, прежде чем доверять любому движению камеры, попросите агента отрендерить три статичных кадра: начало, середину и конец.

Детерминированный покадровый экспорт 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 секунд. Городские башни на картинке смещаются меньше, чем тёмные холмы на переднем плане, а передний холм в конце полностью уходит за пределы кадра — это и есть эффект параллакса.

Добавляем в 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 - z/pвлияет и на размер, и на смещение слоя. В статике композиция выглядит точно, а при движении параллакс появляется сам собой. - Пайплайн: кадры перематываются через seek по формуле
i/fps, скриншоты передаются в ffmpeg для кодирования, и результат не зависит от производительности машины.
Для начала советую повторить структуру промптов из этой статьи и прогнать шестисекундный тестовый ролик. Заодно обзаведитесь привычкой: прежде чем править камеру, посмотрите три статичных кадра. Дальше можно добавлять слои, подключать интерфейс «описание кадра» или перенести ту же идею детерминированности на другие программные решения для видео, например Remotion.
📚 Источники
- How to Build a 2.5D Motion Studio With Opus 5.5: первоисточник практики из этой статьи
- Ссылка:
apidog.com/blog/build-2-5d-motion-studio-opus-5-5 -
Описание: полные промпты, код студии, скрипт экспорта и пример интерфейса «описание кадра»
-
Документация Anthropic по моделям и параметру effort: позиционирование моделей, контекст, цены и описание параметра effort
- Ссылка:
docs.anthropic.com -
Описание: сверка ID модели claude-opus-5-5, заголовка с версией API и использования output_config
-
Документация ffmpeg по форматам: описание входных форматов, в том числе image2pipe
- Ссылка:
ffmpeg.org/ffmpeg-formats.html -
Описание: как читать последовательность PNG из stdin и какие параметры у кодирования H.264
-
Документация Playwright: автоматизация браузера и API для скриншотов
- Ссылка:
playwright.dev - Описание: page.evaluate, page.screenshot и команда установки браузеров
