/**
* Операции над кадрами (Uint8Array w×h×4 RGBA) — синтез производных кадров
* из опорных: псевдо-walk-циклы, idle-кадры оживителей, зеркалирование.
* Всё nearest, без сглаживания — пиксель-арт не мылится.
* Модуль жанронезависим: знает только формат RGBA-кадра.
*/
/** Прозрачный ли пиксель (alpha 0). */
const isEmpty = (rgba, i) => rgba[i + 3] === 0;
/** Копия пикселя src[i] в dst[j] (или прозрачность, если src вне кадра). */
function blit(src, dst, w, h, sx, sy, dx, dy) {
const j = (dy * w + dx) * 4;
if (sx < 0 || sy < 0 || sx >= w || sy >= h) {
dst[j + 3] = 0;
return;
}
const i = (sy * w + sx) * 4;
dst[j] = src[i];
dst[j + 1] = src[i + 1];
dst[j + 2] = src[i + 2];
dst[j + 3] = src[i + 3];
}
/**
* Сдвиг кадра на (dx, dy) px; пустота заполняется прозрачностью.
* dx > 0 — вправо, dy > 0 — вниз.
*/
export function shiftFrame(src, w, h, dx, dy) {
const out = new Uint8Array(w * h * 4);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
blit(src, out, w, h, x - dx, y - dy, x, y);
}
}
return out;
}
/**
* Idle-кадр «парения»: тело поднимается на dy px (по умолчанию 1),
* снизу открывается прозрачность — для спрайтов с якорем по низу.
*/
export function bobFrame(src, w, h, dy = 1) {
return shiftFrame(src, w, h, 0, -dy);
}
/**
* «Приседание»: весь спрайт уходит вниз на 1 px, нижний ряд срезается —
* спрайт становится ниже при том же якоре. Дешёвый 2-й кадр idle для
* спрайтов с полем прозрачности под ногами.
*/
export function squashFrame(src, w, h) {
return shiftFrame(src, w, h, 0, 1);
}
/** Зеркалирование по горизонтали (кадр «вправо» → «влево»). */
export function mirrorFrame(src, w, h) {
const out = new Uint8Array(w * h * 4);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
blit(src, out, w, h, w - 1 - x, y, x, y);
}
}
return out;
}
/**
* Покачивание: nearest-поворот на angle рад вокруг точки опоры
* (по умолчанию низ-центр — «укоренённое» качание, как sway).
* pivot: { x, y } в px.
*/
export function swingFrame(src, w, h, angle, pivot) {
const px = pivot ? pivot.x : (w - 1) / 2;
const py = pivot ? pivot.y : h - 1;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const out = new Uint8Array(w * h * 4);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
// Обратный поворот dest → src (экранные координаты, y вниз).
const dx = x - px;
const dy = y - py;
blit(src, out, w, h, Math.round(px + dx * cos + dy * sin), Math.round(py - dx * sin + dy * cos), x, y);
}
}
return out;
}
/**
* Цикл из опорного кадра: ops — список дескрипторов, один выходной кадр на
* дескриптор; операции внутри дескриптора применяются по порядку:
* { dx?, dy?, bob?, squash?, swing?, mirror? }
* (dx/dy — сдвиг; bob — парение вверх; squash — приседание;
* swing — угол покачивания в рад; mirror — зеркалирование).
*/
export function deriveCycle(src, w, h, ops) {
return ops.map((op) => applyOps(src, w, h, op));
}
function applyOps(src, w, h, op) {
let frame = src;
if (op.dx || op.dy) frame = shiftFrame(frame, w, h, op.dx ?? 0, op.dy ?? 0);
if (op.bob) frame = bobFrame(frame, w, h, op.bob);
if (op.squash) frame = squashFrame(frame, w, h);
if (op.swing) frame = swingFrame(frame, w, h, op.swing, op.pivot);
if (op.mirror) frame = mirrorFrame(frame, w, h);
return frame;
}