Коротко
Покадровая анимация — это атлас с кадрами, формула прямоугольника кадра по индексу, общая опорная точка и своя частота клипа. Если считать номер кадра по времени, а не по числу обновлений экрана, анимация одинаково идёт на мониторах 60 и 144 Гц. События кадров («шаг», «удар») нужно запускать для всех пройденных кадров, иначе просадка FPS их проглотит.
Содержание
- Спрайтовый лист: как найти прямоугольник кадра
- Опорная точка: почему персонаж дёргается и уезжает при развороте
- Частота клипа против частоты экрана
- События на кадрах: шаги, удары, звуки
- Таблица: параметры клипов персонажа
- Код на JavaScript: атлас, разворот и проигрыватель клипа
- Практический сценарий: готовим лист бега к игре
- Когда вам это не подойдёт
Спрайтовый лист: как найти прямоугольник кадра
Прямоугольник кадра в атласе вычисляется из индекса, числа столбцов, размера кадра, поля по краю листа и зазора между кадрами. Столбец — это остаток от деления индекса на число столбцов, строка — целая часть деления. Формула:
столбец = индекс mod столбцов
строка = floor(индекс / столбцов)
x = поле + столбец × (ширина_кадра + зазор)
y = поле + строка × (высота_кадра + зазор)
На холсте браузера этот прямоугольник передают в 9-аргументную форму drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight): первые четыре числа после изображения выбирают область источника, последние четыре — место и размер на экране. Поле и зазор в 1–2 пикселя нужны, чтобы при масштабировании соседний кадр не «подмешивался» по краю; о чётких краях пиксель-арта при масштабе — отдельная тема.
Опорная точка: почему персонаж дёргается и уезжает при развороте
Опорная точка (pivot) — это точка кадра, которая совпадает с позицией объекта в мире; у всех кадров клипа она должна быть одна и та же. Если художник нарисовал кадры бега со смещением на 2–3 пикселя, а движок рисует кадр от левого верхнего угла, персонаж «пляшет» на месте. Если опорная точка не по центру фигуры, при отражении по горизонтали он прыгает в сторону.
Решение — хранить pivot в координатах кадра и при развороте отражать и его:
pivot_x при развороте = ширина_кадра − pivot_x
угол рисования = позиция − pivot
Для героя платформера pivot ставят у стоп: на нижней строке кадра, посередине между ногами. Хитбокс считается от той же точки, поэтому разворот не задвигает героя в стену.
Частота клипа против частоты экрана
Клип анимации идёт со своей частотой, а экран обновляется со своей, и связывать их нельзя. Если менять кадр «каждые 5 обновлений экрана», на мониторе 60 Гц бег идёт 12 к/с, а на 144 Гц — 28,8 к/с, то есть в 2,4 раза быстрее. Номер кадра нужно считать по накопленному времени:
номер = floor(время_клипа × частота_клипа)
кадр = номер mod длина_клипа (для зацикленного)
кадр = min(номер, длина_клипа − 1) (для однократного)
При накоплении времени шагами вида 1/144 с сумма может оказаться чуть меньше целого из-за погрешности дробей, поэтому в коде ниже к произведению добавлена поправка 1e-9.
События на кадрах: шаги, удары, звуки
Событие кадра — это метка «на кадре N произошло X», и запускать её нужно для каждого пройденного кадра, а не только для текущего. При 12 к/с кадр длится 83 мс; если обновление игры однажды заняло 250 мс, за него пройдут три кадра клипа, и кадр со звуком шага может оказаться в середине. Проигрыватель должен помнить номер последнего обработанного кадра и перебрать все номера до текущего.
Удары связывают с событием кадра «активный кадр»: урон включается на нём, а не в момент нажатия атаки. Так замах остаётся видимым, а попадание совпадает с картинкой.
Таблица: параметры клипов персонажа
Значения ниже — рабочие стартовые параметры для персонажа 48×48 пикселей, которые затем подбирают на глаз.
| Клип | Кадров | Частота, к/с | Длительность цикла | Повтор | События кадров |
|---|---|---|---|---|---|
| Бездействие | 4 | 6 | 0,67 с | да | — |
| Бег | 8 | 12 | 0,67 с | да | шаг на 2 и 6 |
| Прыжок вверх | 3 | 12 | 0,25 с | нет | пыль на 0 |
| Атака | 6 | 18 | 0,33 с | нет | урон на 3, конец на 5 |
| Получение урона | 2 | 10 | 0,2 с | нет | — |
Длительность цикла считается как число кадров, делённое на частоту: 8 кадров при 12 к/с дают 0,67 с.
Код на JavaScript: атлас, разворот и проигрыватель клипа
Код считает прямоугольники кадров, точку рисования с учётом разворота и проигрывает клип бега на экранах 60 и 144 Гц. Он проверен в Node.js 24: числа в комментариях совпадают с выводом.
// Прямоугольник кадра в атласе по индексу: поля (margin) и зазоры (spacing) учитываются явно
function frameRect(index, sheet) {
const col = index % sheet.columns;
const row = Math.floor(index / sheet.columns);
return {
x: sheet.margin + col * (sheet.frameW + sheet.spacing),
y: sheet.margin + row * (sheet.frameH + sheet.spacing),
w: sheet.frameW,
h: sheet.frameH,
};
}
// Куда рисовать кадр, чтобы опорная точка (pivot) совпала с позицией объекта.
// flip = true — отражение по горизонтали: pivot тоже отражается внутри кадра.
function drawCorner(pos, sheet, flip) {
const px = flip ? sheet.frameW - sheet.pivotX : sheet.pivotX;
return { x: pos.x - px, y: pos.y - sheet.pivotY };
}
// Проигрыватель клипа: своя частота кадров, не зависит от частоты экрана.
// Возвращает события кадров, которые были пройдены за этот шаг (даже если кадр пропущен).
function createPlayer(clip) {
let time = 0;
let last = -1;
return {
update(dt) {
time += dt;
// 1e-9 гасит ошибку сложения дробей: 144 × (1/144) даёт чуть меньше 1
const total = Math.floor(time * clip.fps + 1e-9);
const frame = clip.loop ? total % clip.frames.length : Math.min(total, clip.frames.length - 1);
const fired = [];
for (let t = last + 1; t <= total; t++) {
const f = clip.loop ? t % clip.frames.length : Math.min(t, clip.frames.length - 1);
if (!clip.loop && t >= clip.frames.length) break;
if (clip.events[f]) fired.push(clip.events[f] + "@" + f);
}
last = total;
return { frame: clip.frames[frame], fired };
},
};
}
const sheet = { columns: 8, frameW: 48, frameH: 48, margin: 1, spacing: 2, pivotX: 20, pivotY: 46 };
console.log(frameRect(0, sheet)); // { x: 1, y: 1, w: 48, h: 48 }
console.log(frameRect(11, sheet)); // { x: 151, y: 51, w: 48, h: 48 }
console.log(drawCorner({ x: 300, y: 200 }, sheet, false)); // { x: 280, y: 154 }
console.log(drawCorner({ x: 300, y: 200 }, sheet, true)); // { x: 272, y: 154 }
// Бег: 8 кадров, 12 к/с, шаги на кадрах 2 и 6. Экран 60 Гц и 144 Гц
const run = { frames: [8, 9, 10, 11, 12, 13, 14, 15], fps: 12, loop: true, events: { 2: "step", 6: "step" } };
for (const hz of [60, 144]) {
const p = createPlayer(run);
let steps = 0;
let frame = 0;
for (let i = 0; i < hz; i++) {
const r = p.update(1 / hz);
steps += r.fired.length;
frame = r.frame;
}
console.log(hz + " Гц: за 1 с шагов", steps, "кадр атласа", frame);
}
// 60 Гц: за 1 с шагов 3 кадр атласа 12
// 144 Гц: за 1 с шагов 3 кадр атласа 12
// Просадка: один кадр длиной 0,25 с не съедает событие шага
const p2 = createPlayer(run);
p2.update(0.01);
console.log(p2.update(0.25).fired); // [ 'step@2' ]
За 1 секунду на обоих экранах прозвучали 3 шага и показан один и тот же кадр атласа. После обновления длиной 0,25 с событие шага на кадре 2 не потерялось, хотя проигрыватель сразу перескочил на кадр 3.
Практический сценарий: готовим лист бега к игре
Подготовка листа занимает около 15 минут и избавляет от дрожания и сдвигов на этапе игры:
- Единый размер кадра. Экспортируйте все 8 кадров бега в одинаковой ячейке, например 48×48 пикселей, даже если фигура занимает меньше.
- Поле и зазор. Задайте поле 1 пиксель по краю листа и зазор 2 пикселя между кадрами, запишите эти числа рядом с листом.
- Опорная точка. Отметьте pivot у стоп на одном кадре и проверьте, что на всех остальных стопы стоят на той же строке пикселей.
- Частота. Начните с 12 к/с, затем сравните бег со скоростью перемещения героя: если ноги «скользят», поднимайте частоту или снижайте скорость.
- События. Поставьте метку «шаг» на кадры касания ногой земли, обычно на 2 и 6 из 8.
- Проверка. Запустите игру с принудительно заниженной частотой обновления и убедитесь, что звуков шагов за секунду столько же, сколько при нормальной.
Когда вам это не подойдёт
Покадровая анимация на атласе — не единственный способ оживить 2D-персонажа:
- Скелетная анимация: если персонаж собран из частей с костями и плавными переходами между движениями, кадры в атласе заменяются ключами поворота костей, а формулы этой статьи нужны только для отдельных эффектов.
- Сотни уникальных кадров на персонажа: когда клипов десятки, а кадров сотни, один лист перестаёт помещаться в разумный размер текстуры; придётся делить его по клипам и подгружать частями.
- Чисто процедурная графика: если объекты рисуются фигурами и шейдерами, атлас и опорные точки кадров не нужны.
Мы разрабатываем J2DRU — браузерный 2D игровой движок; он в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).
Источники (проверено: ноябрь 2026): MDN — CanvasRenderingContext2D.drawImage().
Читайте также: