From eb9ca0179d06227a0fce1cf5c1bcd3a5ff44def1 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 13 Jul 2026 16:14:31 +0300 Subject: [PATCH] =?UTF-8?q?libbgi:=20=D0=B0=D0=B2=D1=82=D0=BE-=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B9=D1=82=D0=BE=D0=B2=20=E2=80=94=20=D0=BA=D0=B0=D0=B4=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=8F=20+=20tween=20(tests/spranim=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=20PASS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализация §9г по требованиям пользователя: - sprite_anim(first,last,speed,mode): ANIM_LOOP / ANIM_PINGPONG / ANIM_ONCE (one-shot замирает на last), | ANIM_HORIZ для горизонтальных лент. Смена кадра в тике = ±an_step к оси ленты — без умножений (осевое смещение first считается в setup циклом). - sprite_moveto(tx,ty,max_step,interval): Брезенхэм порциями ≤max_step вдоль большей оси (меньшая — err-аккумулятором, нелинейные шаги Y сами собой), деления нет. - Одновременность кадровой и tween — независимые поля/биты. - Статус: sprite_anim_status() — битовое поле SPR_ANIM_ON/DONE + SPR_MOVE_ON/DONE; sprite_anim_frame() — текущий индекс кадра; sprite_moving(). - Стопы: sprite_anim_stop(frame | -1 = текущий); sprite_move_stop(0 = замереть / 1 = прыжок в цель + DONE). - Тикер _sprite_tick — проход 0 sprite_update через funcptr _spr_tick_fn (DCE: без sprite_anim/moveto код не линкуется, цена — один if на кадр; +40 Б программе с движком, sprite_t +18 Б). tests/spranim (MAME dev, все PASS): LOOP/PINGPONG (разворот на границе без удвоения краёв)/ONCE+DONE; пример (0,0)→(100,50), шаг 5, интервал 4 → ровно 80 кадров, Y идёт 3/2/3/2; стопы обоих видов. Co-Authored-By: Claude Fable 5 --- docs/size_baseline.tsv | 5 +- docs/sprite-api-design.md | 16 +++- libbgi/_bgi.h | 9 ++ libbgi/common/_spr_tick_fn.c | 10 +++ libbgi/common/_sprite_tick.c | 94 ++++++++++++++++++++ libbgi/common/atlas_sprite_init.c | 2 + libbgi/common/sprite_anim.c | 54 ++++++++++++ libbgi/common/sprite_anim_stop.c | 27 ++++++ libbgi/common/sprite_init.c | 2 + libbgi/common/sprite_move_stop.c | 19 ++++ libbgi/common/sprite_moveto.c | 46 ++++++++++ libbgi/common/sprite_update.c | 6 ++ libbgi/include/sprite.h | 74 ++++++++++++++++ tests/spranim/Makefile | 4 + tests/spranim/spranim.c | 142 ++++++++++++++++++++++++++++++ 15 files changed, 506 insertions(+), 4 deletions(-) create mode 100644 libbgi/common/_spr_tick_fn.c create mode 100644 libbgi/common/_sprite_tick.c create mode 100644 libbgi/common/sprite_anim.c create mode 100644 libbgi/common/sprite_anim_stop.c create mode 100644 libbgi/common/sprite_move_stop.c create mode 100644 libbgi/common/sprite_moveto.c create mode 100644 tests/spranim/Makefile create mode 100644 tests/spranim/spranim.c diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 12f5046..bd8a0cb 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -2,9 +2,9 @@ accfill 3772 argv 3431 assrtest 3847 -atlas 9154 +atlas 9193 attrprob 4087 -balls 6208 +balls 6249 banked 1056 bankedbg 1067 banklocl 4683 @@ -44,6 +44,7 @@ rt_test 4892 seek 4188 simple 955 solidt 11531 +spranim 8752 spriteclip 4119 sprites 6791 stattest 7549 diff --git a/docs/sprite-api-design.md b/docs/sprite-api-design.md index 87c9ec4..a5721fd 100644 --- a/docs/sprite-api-design.md +++ b/docs/sprite-api-design.md @@ -700,10 +700,22 @@ Probe-тест tests/w0page (MAME dev, 2026-07-13, ВСЕ PASS): W1, но только tiny (в small/big/huge в W1 код) — отвергнута в пользу универсального W0. -## 9г. Авто-анимация спрайтов (ПРЕДЛОЖЕНИЕ 2026-07-13, обсуждается) +## 9г. Авто-анимация спрайтов (РЕАЛИЗОВАНО 2026-07-13; tests/spranim PASS) Идея пользователя: sprite_update (вызывается раз на кадр) сам тикает -анимации — приложение только описывает их декларативно. +анимации — приложение только описывает их декларативно. Реализация: +sprite_anim(first,last,speed,mode: LOOP/PINGPONG/ONCE|HORIZ) / +sprite_anim_stop(frame|-1) / sprite_moveto(tx,ty,max_step,interval) / +sprite_move_stop(at_target) / статус sprite_anim_status() (биты +SPR_ANIM_ON/DONE, SPR_MOVE_ON/DONE) / sprite_anim_frame() / +sprite_moving(). Кадровая и tween независимы (одновременно — да); +тикер _sprite_tick подключается указателем _spr_tick_fn (DCE), +sprite_update зовёт его проходом 0. Кадровая — ±an_step к оси без +умножений; tween — Брезенхэм порциями ≤max_step по большей оси. +Проверено tests/spranim (T1 LOOP / T2 PINGPONG / T3 ONCE+DONE / +T4 пример (0,0)→(100,50)/5/4 = ровно 80 кадров, Y 3/2/3/2 / +T5 стопы) — все PASS. Цена: ~+40 Б программе с движком, sprite_t ++18 Б. Ниже — исходный дизайн-разбор. ### Кадровая анимация (смена изображений) diff --git a/libbgi/_bgi.h b/libbgi/_bgi.h index 430a550..3f7eb05 100644 --- a/libbgi/_bgi.h +++ b/libbgi/_bgi.h @@ -155,6 +155,15 @@ void _gfx_w0_isr(void); #define _ATL_DIR_OFF 0x68u /* каталог: count × 8 Б (макс 19) */ #define _ATL_DATA_OFF 0x100u /* ленты; = резерв ISR-стаба */ +/* ---- Авто-анимация спрайтов (§9г) --------------------------------- * + * Тикер подключается указателем (DCE: программа без sprite_anim/ + * sprite_moveto не тянет его код): дефолт NULL (_BSS), ставят + * sprite_anim()/sprite_moveto(). sprite_update зовёт в начале кадра. + * Типы из здесь недоступны (подключается после) — void*. */ +typedef void (*_spr_tick_fp)(void *arr, uint8_t count); +extern _spr_tick_fp _spr_tick_fn; /* common/_spr_tick_fn.c */ +void _sprite_tick(void *arr, uint8_t count); /* common/_sprite_tick.c */ + /* ---- Fill-burst'ы акселератора 256 (register-ABI, asm-only) ------ * * Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без * глобального скретча в _DATA: diff --git a/libbgi/common/_spr_tick_fn.c b/libbgi/common/_spr_tick_fn.c new file mode 100644 index 0000000..cea0702 --- /dev/null +++ b/libbgi/common/_spr_tick_fn.c @@ -0,0 +1,10 @@ +/* + * _spr_tick_fn — указатель на тикер авто-анимаций (data-модуль). + * Дефолт NULL (_BSS, crt0 зануляет): sprite_update платит один if на + * кадр; sprite_anim()/sprite_moveto() ставят _sprite_tick — только + * тогда код тикера попадает в программу (DCE-паттерн funcptr, как + * clip/noclip). + */ +#include "../_bgi.h" + +_spr_tick_fp _spr_tick_fn; diff --git a/libbgi/common/_sprite_tick.c b/libbgi/common/_sprite_tick.c new file mode 100644 index 0000000..c1bae9f --- /dev/null +++ b/libbgi/common/_sprite_tick.c @@ -0,0 +1,94 @@ +/* + * _sprite_tick — тикер авто-анимаций (§9г): вызывается из sprite_update + * (раз на КАДР, до heal/блит-проходов — dirty должен взводиться раньше). + * Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ (идут одновременно); + * тикаются и у скрытых спрайтов («время идёт», ветка дороже). + * + * Кадровая: смена кадра = ±an_step к оси ленты (sy; при _SPR_ANIM_HORZ + * — sx) — БЕЗ умножения; wrap цикла — прыжок оси в an_lo (посчитан + * sprite_anim'ом один раз). Маятник разворачивается НА границе (…2,3, + * 2,1,0,1…, без удвоения крайних). ANIM_ONCE замирает на last: + * SPR_ANIM_ON снимается, SPR_ANIM_DONE взводится. + * + * Перемещение: раз в mv_speed кадров — порция ≤mv_step итераций + * классического Брезенхэма (err-аккумулятор: большая ось шагает каждую + * итерацию, меньшая подтягивается — нелинейные 2/3/2/3 сами собой, + * деления нет). Цель достигнута — SPR_MOVE_ON→off, SPR_MOVE_DONE. + */ +#include "../_bgi.h" +#include + +void _sprite_tick(void *arr, uint8_t count) +{ + sprite_t *s = (sprite_t *)arr; + uint8_t i; + + for (i = 0; i < count; i++, s++) { + /* ---- кадровая анимация ---- */ + if ((s->an_flags & SPR_ANIM_ON) && --s->an_timer == 0) { + int *axis = (s->an_flags & _SPR_ANIM_HORZ) ? &s->sx : &s->sy; + uint8_t moved = 1; + + s->an_timer = s->an_speed; + if (!(s->an_flags & _SPR_ANIM_BACK)) { + if (s->an_frame < s->an_last) { + s->an_frame++; + *axis += s->an_step; + } else if (s->an_flags & _SPR_ANIM_ONCE) { + s->an_flags &= (uint8_t)~SPR_ANIM_ON; + s->an_flags |= SPR_ANIM_DONE; + moved = 0; + } else if (s->an_flags & _SPR_ANIM_PP) { + s->an_flags |= _SPR_ANIM_BACK; /* разворот на границе */ + if (s->an_frame > s->an_first) { + s->an_frame--; + *axis -= s->an_step; + } else + moved = 0; /* лента из 1 кадра */ + } else { /* ANIM_LOOP: wrap */ + s->an_frame = s->an_first; + *axis = s->an_lo; + } + } else { /* маятник, назад */ + if (s->an_frame > s->an_first) { + s->an_frame--; + *axis -= s->an_step; + } else { + s->an_flags &= (uint8_t)~_SPR_ANIM_BACK; + if (s->an_frame < s->an_last) { + s->an_frame++; + *axis += s->an_step; + } else + moved = 0; + } + } + if (moved) + s->flags |= _SPR_DIRTY; + } + + /* ---- tween-перемещение ---- */ + if ((s->an_flags & SPR_MOVE_ON) && --s->mv_timer == 0) { + uint8_t n = s->mv_step; + int x = s->x, y = s->y; + + s->mv_timer = s->mv_speed; + while (n--) { + int e2; + if (x == s->mv_tx && y == s->mv_ty) + break; + e2 = s->mv_err * 2; + if (e2 >= s->mv_dy) { s->mv_err += s->mv_dy; x += s->mv_sgnx; } + if (e2 <= s->mv_dx) { s->mv_err += s->mv_dx; y += s->mv_sgny; } + } + if (x != s->x || y != s->y) { + s->x = x; + s->y = y; + s->flags |= _SPR_DIRTY; + } + if (x == s->mv_tx && y == s->mv_ty) { + s->an_flags &= (uint8_t)~SPR_MOVE_ON; + s->an_flags |= SPR_MOVE_DONE; + } + } + } +} diff --git a/libbgi/common/atlas_sprite_init.c b/libbgi/common/atlas_sprite_init.c index 9d750ba..4d5b470 100644 --- a/libbgi/common/atlas_sprite_init.c +++ b/libbgi/common/atlas_sprite_init.c @@ -25,6 +25,8 @@ void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx) s->sx = 0; s->sy = 0; s->flags = 0; s->page = a->page; + s->an_flags = 0; /* авто-анимации выключены */ + s->an_frame = 0; s->drawn[0].on = 0; s->drawn[1].on = 0; } diff --git a/libbgi/common/sprite_anim.c b/libbgi/common/sprite_anim.c new file mode 100644 index 0000000..41fed06 --- /dev/null +++ b/libbgi/common/sprite_anim.c @@ -0,0 +1,54 @@ +/* + * sprite_anim — запустить кадровую авто-анимацию: кадры first..last + * вдоль оси ленты (вертикальная; ANIM_HORIZ — вдоль X), смена раз в + * speed кадров, mode = ANIM_LOOP / ANIM_PINGPONG / ANIM_ONCE. + * + * Всё дорогое считается ЗДЕСЬ один раз (не в тике): шаг оси = размер + * кадра, осевое смещение first — циклом сложений (без __mulint). + * Ставит текущий кадр на first, сбрасывает SPR_ANIM_DONE, подключает + * тикер (_spr_tick_fn) — до первого вызова sprite_anim/sprite_moveto + * код тикера в программу не линкуется. Требование: индексы ≤ 127, + * first ≤ last. Move-статусы (SPR_MOVE_*) не трогаются. + */ +#include "../_bgi.h" +#include + +void sprite_anim(sprite_t *s, uint8_t first, uint8_t last, + uint8_t speed, uint8_t mode) +{ + uint8_t f = s->an_flags & (SPR_MOVE_ON | SPR_MOVE_DONE); + uint8_t step = (mode & ANIM_HORIZ) ? s->w : s->h; + int lo = 0; + uint8_t i; + + for (i = 0; i < first; i++) + lo += step; /* first*step без умножения */ + + if (!speed) + speed = 1; + + s->an_step = step; + s->an_lo = lo; + s->an_first = first; + s->an_last = last; + s->an_frame = first; + s->an_speed = speed; + s->an_timer = speed; + + f |= SPR_ANIM_ON; + if (mode & ANIM_HORIZ) + f |= _SPR_ANIM_HORZ; + if ((mode & 0x03u) == ANIM_PINGPONG) + f |= _SPR_ANIM_PP; + if ((mode & 0x03u) == ANIM_ONCE) + f |= _SPR_ANIM_ONCE; + s->an_flags = f; + + /* Ось — на кадр first (обычный путь sprite_frame: dirty при смене). */ + if (mode & ANIM_HORIZ) + sprite_frame(s, lo, s->sy); + else + sprite_frame(s, s->sx, lo); + + _spr_tick_fn = _sprite_tick; +} diff --git a/libbgi/common/sprite_anim_stop.c b/libbgi/common/sprite_anim_stop.c new file mode 100644 index 0000000..97ccb4c --- /dev/null +++ b/libbgi/common/sprite_anim_stop.c @@ -0,0 +1,27 @@ +/* + * sprite_anim_stop — остановить кадровую анимацию. frame ≥ 0 — какой + * кадр останется на экране (индекс вдоль оси ленты, ≤127); -1 — + * замереть на текущем. Ось кадра считается циклом сложений (стоп — + * редкий вызов, __mulint не тянем). Валидна после sprite_anim + * (an_step/ось настроены им); SPR_ANIM_DONE не трогает. + */ +#include "../_bgi.h" +#include + +void sprite_anim_stop(sprite_t *s, int8_t frame) +{ + s->an_flags &= (uint8_t)~(SPR_ANIM_ON | _SPR_ANIM_BACK); + + if (frame >= 0 && (uint8_t)frame != s->an_frame) { + int off = 0; + uint8_t i; + + for (i = 0; i < (uint8_t)frame; i++) + off += s->an_step; + s->an_frame = (uint8_t)frame; + if (s->an_flags & _SPR_ANIM_HORZ) + sprite_frame(s, off, s->sy); + else + sprite_frame(s, s->sx, off); + } +} diff --git a/libbgi/common/sprite_init.c b/libbgi/common/sprite_init.c index 620a183..5b1dc86 100644 --- a/libbgi/common/sprite_init.c +++ b/libbgi/common/sprite_init.c @@ -23,6 +23,8 @@ void sprite_init(sprite_t *s, const void *img) s->h = p[2]; s->flags = 0; s->page = 0; /* img в обычной памяти (не W0-атлас) */ + s->an_flags = 0; /* авто-анимации выключены */ + s->an_frame = 0; s->drawn[0].on = 0; s->drawn[1].on = 0; } diff --git a/libbgi/common/sprite_move_stop.c b/libbgi/common/sprite_move_stop.c new file mode 100644 index 0000000..2cad986 --- /dev/null +++ b/libbgi/common/sprite_move_stop.c @@ -0,0 +1,19 @@ +/* + * sprite_move_stop — остановить tween-перемещение: at_target = 0 — + * замереть в текущих координатах (SPR_MOVE_DONE НЕ взводится — спрайт + * цели не достиг); 1 — прыгнуть в цель и взвести SPR_MOVE_DONE. + * Ничего не делает, если перемещение не шло. + */ +#include "../_bgi.h" +#include + +void sprite_move_stop(sprite_t *s, uint8_t at_target) +{ + if (!(s->an_flags & SPR_MOVE_ON)) + return; + s->an_flags &= (uint8_t)~SPR_MOVE_ON; + if (at_target) { + sprite_move(s, s->mv_tx, s->mv_ty); + s->an_flags |= SPR_MOVE_DONE; + } +} diff --git a/libbgi/common/sprite_moveto.c b/libbgi/common/sprite_moveto.c new file mode 100644 index 0000000..3ab2478 --- /dev/null +++ b/libbgi/common/sprite_moveto.c @@ -0,0 +1,46 @@ +/* + * sprite_moveto — запустить tween-перемещение к (tx,ty): раз в interval + * кадров тикер делает до max_step итераций Брезенхэма (большая ось — + * итерация = пиксель; меньшая подтягивается err-аккумулятором). + * Пример: (0,0)→(100,50), max_step 5, interval 4 → 20 шагов × 4 кадра + * = 80 кадров (1.6 с), Y идёт 2/3/2/3. + * + * Дельты/знаки/аккумулятор считаются здесь один раз. Если цель == + * текущая позиция — сразу SPR_MOVE_DONE (тикер не включается). + * Кадровые статусы (SPR_ANIM_*) не трогаются. + */ +#include "../_bgi.h" +#include + +void sprite_moveto(sprite_t *s, int tx, int ty, + uint8_t max_step, uint8_t interval) +{ + int dx = tx - s->x; + int dy = ty - s->y; + + s->mv_sgnx = 1; + if (dx < 0) { dx = -dx; s->mv_sgnx = -1; } + s->mv_sgny = 1; + if (dy < 0) { dy = -dy; s->mv_sgny = -1; } + + s->an_flags &= (uint8_t)~SPR_MOVE_DONE; + if (dx == 0 && dy == 0) { + s->an_flags = (s->an_flags & (uint8_t)~SPR_MOVE_ON) | SPR_MOVE_DONE; + return; + } + + s->mv_tx = tx; + s->mv_ty = ty; + s->mv_dx = dx; + s->mv_dy = -dy; + s->mv_err = dx - dy; + + if (!max_step) max_step = 1; + if (!interval) interval = 1; + s->mv_step = max_step; + s->mv_speed = interval; + s->mv_timer = interval; + + s->an_flags |= SPR_MOVE_ON; + _spr_tick_fn = _sprite_tick; +} diff --git a/libbgi/common/sprite_update.c b/libbgi/common/sprite_update.c index 2a8fd98..18073d8 100644 --- a/libbgi/common/sprite_update.c +++ b/libbgi/common/sprite_update.c @@ -35,6 +35,12 @@ void sprite_update(sprite_t *arr, uint8_t count) uint8_t i; sprite_t *s; + /* Проход 0: тик авто-анимаций (§9г) — ДО heal/блита (двигает x/y и + * ось кадра, взводя dirty). Указатель ставят sprite_anim/ + * sprite_moveto; без них — один if на кадр, код тикера не линкуется. */ + if (_spr_tick_fn) + (*_spr_tick_fn)(arr, count); + /* Проход 1: heal ВСЕХ старых позиций, требующих обновления. */ gfx_set_bank(GFX_BANK_NORMAL); _bgi_begin(); diff --git a/libbgi/include/sprite.h b/libbgi/include/sprite.h index 2f391e0..a99318f 100644 --- a/libbgi/include/sprite.h +++ b/libbgi/include/sprite.h @@ -46,6 +46,26 @@ #define _SPR_DIRTY1 0x08u #define _SPR_DIRTY (_SPR_DIRTY0 | _SPR_DIRTY1) +/* Статус-биты sprite_t.an_flags (чтение — sprite_anim_status()): + * *_ON — анимация активна (тикается в sprite_update); *_DONE — дошла + * до конца сама (one-shot до last / перемещение до цели) или была + * добита sprite_move_stop(at_target=1). DONE сбрасывается новым + * запуском соответствующей анимации. */ +#define SPR_ANIM_ON 0x01u /* кадровая анимация идёт */ +#define SPR_ANIM_DONE 0x02u /* one-shot дошёл до last */ +#define SPR_MOVE_ON 0x04u /* tween-перемещение идёт */ +#define SPR_MOVE_DONE 0x08u /* перемещение достигло цели */ +#define _SPR_ANIM_BACK 0x10u /* внутр.: маятник идёт назад */ +#define _SPR_ANIM_HORZ 0x20u /* внутр.: лента горизонтальная (ось sx) */ +#define _SPR_ANIM_PP 0x40u /* внутр.: режим маятника */ +#define _SPR_ANIM_ONCE 0x80u /* внутр.: режим one-shot */ + +/* Режимы sprite_anim(). */ +#define ANIM_LOOP 0u /* 0,1,..,last,first,.. (цикл) */ +#define ANIM_PINGPONG 1u /* 0,1,..,last,last-1,..,first,1,.. */ +#define ANIM_ONCE 2u /* first..last, замереть на last + DONE */ +#define ANIM_HORIZ 0x80u /* ИЛИ-флаг: кадры вдоль X (сетка nx>1) */ + typedef struct { const void *img; /* getimage-формат / атлас-лента (вне W3) */ int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */ @@ -55,6 +75,19 @@ typedef struct { uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница * * W0-атласа (img — адрес 0x0100-0x3FFF, движок * * сам мапит страницу в W0 на время блита) */ + /* --- авто-анимация (владеет тикер; настраивать ТОЛЬКО через + * sprite_anim../sprite_moveto.., читать — inline-аксессорами) --- */ + uint8_t an_flags; /* статус: SPR_ANIM_.. | SPR_MOVE_.. */ + uint8_t an_frame; /* ТЕКУЩИЙ индекс кадра (0-based по ленте) */ + uint8_t an_first, an_last; /* границы анимации (индексы) */ + uint8_t an_speed, an_timer; /* кадров между сменами / счётчик */ + uint8_t an_step; /* шаг оси = fh (верт. лента) или fw (гориз.) */ + int an_lo; /* осевое смещение кадра first (для wrap) */ + uint8_t mv_speed, mv_timer; /* кадров между шагами / счётчик */ + uint8_t mv_step; /* макс. пикселей вдоль большей оси за шаг */ + int mv_tx, mv_ty; /* цель */ + int mv_dx, mv_dy, mv_err;/* Брезенхэм: dx, -dy, аккумулятор */ + int8_t mv_sgnx, mv_sgny; /* знаки шага осей */ /* --- внутреннее (владеет движок) --- */ struct { int x, y; /* где нарисован на странице p */ @@ -107,6 +140,47 @@ inline void sprite_hide(sprite_t *s) { * активный спрайт прошёл над статиком — приложение помечает статик). */ inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; } +/* ==== Авто-анимация (§9г дизайна) ==================================== + * Тикается САМА в начале каждого sprite_update (раз на кадр), код + * тикера подключается через funcptr — программа без анимаций его не + * линкует. Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ и могут + * идти одновременно. Пока кадровая активна — не трогать sprite_frame + * вручную (тикер ведёт ось сам); границы кадров должны лежать вдоль + * ОДНОЙ оси ленты (вертикальная по умолчанию, ANIM_HORIZ — вдоль X). */ + +/* Запустить кадровую анимацию: кадры first..last (индексы вдоль оси + * ленты), смена раз в speed кадров (1 = каждый кадр), mode = ANIM_LOOP/ + * ANIM_PINGPONG/ANIM_ONCE (| ANIM_HORIZ для горизонтальной ленты). + * Старт с first; сбрасывает SPR_ANIM_DONE. */ +void sprite_anim(sprite_t *s, uint8_t first, uint8_t last, + uint8_t speed, uint8_t mode); + +/* Остановить кадровую анимацию: frame = индекс кадра, который останется + * на экране, или -1 — замереть на текущем. */ +void sprite_anim_stop(sprite_t *s, int8_t frame); + +/* Запустить перемещение к (tx,ty): раз в interval кадров — шаг до + * max_step пикселей вдоль БОЛЬШЕЙ оси (меньшая тянется Брезенхэмом, + * без деления). Сбрасывает SPR_MOVE_DONE. Пример: (0,0)→(100,50), + * max_step 5, interval 4 → 20 шагов × 4 кадра = 80 кадров (1.6 с). */ +void sprite_moveto(sprite_t *s, int tx, int ty, + uint8_t max_step, uint8_t interval); + +/* Остановить перемещение: at_target = 0 — замереть где есть; + * 1 — прыгнуть в цель (и взвести SPR_MOVE_DONE). */ +void sprite_move_stop(sprite_t *s, uint8_t at_target); + +/* Статус анимаций: битовое поле SPR_ANIM_ON/DONE | SPR_MOVE_ON/DONE. */ +inline uint8_t sprite_anim_status(const sprite_t *s) { return s->an_flags; } + +/* Текущий индекс кадра ленты (ведёт тикер; валиден и после stop). */ +inline uint8_t sprite_anim_frame(const sprite_t *s) { return s->an_frame; } + +/* Перемещение ещё идёт? */ +inline uint8_t sprite_moving(const sprite_t *s) { + return s->an_flags & SPR_MOVE_ON; +} + /* ==== Атласы в EMM-страницах (W0) ==================================== * Файл .atl (docs/sprite-api-design.md §3.1, вариант II): 0x100-байтовый * заголовок (магия 'SPA1', count; каталог с 0x68) + getimage-ленты; diff --git a/tests/spranim/Makefile b/tests/spranim/Makefile new file mode 100644 index 0000000..66dfc65 --- /dev/null +++ b/tests/spranim/Makefile @@ -0,0 +1,4 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := spranim +EXTRA_FLAGS ?= --gfx 256 --memory tiny +include $(PROJ_ROOT)/app.mk diff --git a/tests/spranim/spranim.c b/tests/spranim/spranim.c new file mode 100644 index 0000000..9b2efbe --- /dev/null +++ b/tests/spranim/spranim.c @@ -0,0 +1,142 @@ +/* + * spranim — тест авто-анимаций спрайтового движка (§9г): + * sprite_anim (LOOP/PINGPONG/ONCE) + sprite_moveto + статусы + стопы. + * + * Логика проверяется ПРОГРАММНО по полям sprite_t: спрайты невидимы, + * тикер работает независимо от видимости, блитов нет — sprite_update + * гоняется в текстовом режиме (скобка W3 безвредна). Ожидания: + * + * T1 LOOP 0..3 speed=2: смена раз в 2 тика: после 8 тиков frame + * снова 0 (0→1→2→3→0), sy вернулся в 0. + * T2 PINGPONG 0..3 speed=1: 1,2,3,2,1,0,1 — на тике 7 frame==1, + * разворот на границе без удвоения краёв. + * T3 ONCE 0..2 speed=1: после 2 тиков frame==2 и ANIM_ON ещё стоит, + * после 3-го — ON снят, DONE взведён, frame==2 (замер на last). + * T4 MOVETO (0,0)→(100,50) step=5 interval=4 (пример из §9г): + * тик 4: (5,3); тик 8: (10,5); тик 79: НЕ DONE; тик 80: (100,50), + * MOVE_ON снят, MOVE_DONE взведён. Параллельно у спрайта идёт + * LOOP-анимация (одновременность) — frame успел смениться. + * T5 sprite_anim_stop(1): ON снят, frame==1, ось = 16. + * sprite_move_stop(at_target=1) на полпути: прыжок в цель + DONE. + * + * Кадры: RAM-лента 16×16×4 (getimage), кадр k залит k — содержимое + * не важно (блитов нет), важен размер h=16 → an_step. + */ +#include +#include +#include + +static uint8_t strip[4 + 16 * 16 * 4]; +static sprite_t sp[4]; + +static void build_strip(void) +{ + memset(strip, 0, sizeof strip); + strip[0] = 16; strip[2] = 64; /* w=16, h=64 (4 кадра) */ +} + +static void tick(sprite_t *arr, uint8_t n, uint16_t times) +{ + while (times--) + sprite_update(arr, n); +} + +int main(void) +{ + uint8_t ok; + + puts("SPRANIM: frame/tween auto-animation"); + build_strip(); + + /* ---- T1: LOOP ---------------------------------------------------- */ + sprite_init(&sp[0], strip); + sp[0].w = 16; sp[0].h = 16; + sprite_anim(&sp[0], 0, 3, 2, ANIM_LOOP); + tick(sp, 1, 2); + ok = (sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16); + tick(sp, 1, 4); /* всего 6: 0→1→2→3 */ + ok = ok && (sprite_anim_frame(&sp[0]) == 3 && sp[0].sy == 48); + tick(sp, 1, 2); /* всего 8: wrap → 0 */ + ok = ok && (sprite_anim_frame(&sp[0]) == 0 && sp[0].sy == 0); + printf("T1 LOOP: %s\n", ok ? "PASS" : "FAIL"); + + /* ---- T2: PINGPONG ------------------------------------------------ */ + sprite_init(&sp[0], strip); + sp[0].w = 16; sp[0].h = 16; + sprite_anim(&sp[0], 0, 3, 1, ANIM_PINGPONG); + tick(sp, 1, 3); /* 1,2,3 */ + ok = (sprite_anim_frame(&sp[0]) == 3); + tick(sp, 1, 1); /* разворот: 2 */ + ok = ok && (sprite_anim_frame(&sp[0]) == 2 && sp[0].sy == 32); + tick(sp, 1, 2); /* 1,0 */ + ok = ok && (sprite_anim_frame(&sp[0]) == 0); + tick(sp, 1, 1); /* разворот: 1 */ + ok = ok && (sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16); + printf("T2 PINGPONG: %s\n", ok ? "PASS" : "FAIL"); + + /* ---- T3: ONCE ---------------------------------------------------- */ + sprite_init(&sp[0], strip); + sp[0].w = 16; sp[0].h = 16; + sprite_anim(&sp[0], 0, 2, 1, ANIM_ONCE); + tick(sp, 1, 2); /* 1,2 — дошли до last */ + ok = (sprite_anim_frame(&sp[0]) == 2 && + (sprite_anim_status(&sp[0]) & SPR_ANIM_ON) && + !(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE)); + tick(sp, 1, 1); /* фиксация: ON→off,DONE */ + ok = ok && (sprite_anim_frame(&sp[0]) == 2 && sp[0].sy == 32 && + !(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) && + (sprite_anim_status(&sp[0]) & SPR_ANIM_DONE)); + tick(sp, 1, 5); /* стоит на месте */ + ok = ok && (sprite_anim_frame(&sp[0]) == 2); + printf("T3 ONCE: %s\n", ok ? "PASS" : "FAIL"); + + /* ---- T4: MOVETO + одновременная кадровая ------------------------- */ + sprite_init(&sp[0], strip); + sp[0].w = 16; sp[0].h = 16; /* x=y=0 после init */ + sprite_anim(&sp[0], 0, 3, 3, ANIM_LOOP); + sprite_moveto(&sp[0], 100, 50, 5, 4); + ok = (sprite_moving(&sp[0]) != 0); + tick(sp, 1, 4); + ok = ok && (sp[0].x == 5 && sp[0].y == 3); /* Y: 3/2/3/2 */ + tick(sp, 1, 4); + ok = ok && (sp[0].x == 10 && sp[0].y == 5); + tick(sp, 1, 71); /* всего 79 — ещё едет */ + ok = ok && sprite_moving(&sp[0]) && + !(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE); + tick(sp, 1, 1); /* 80: приехали */ + ok = ok && (sp[0].x == 100 && sp[0].y == 50 && + !sprite_moving(&sp[0]) && + (sprite_anim_status(&sp[0]) & SPR_MOVE_DONE)); + ok = ok && (sprite_anim_frame(&sp[0]) != 0 || + sp[0].an_timer != 3); /* кадровая тоже тикала */ + printf("T4 MOVETO: %s (x=%d y=%d)\n", + ok ? "PASS" : "FAIL", sp[0].x, sp[0].y); + + /* ---- T5: стопы ---------------------------------------------------- */ + sprite_init(&sp[0], strip); + sp[0].w = 16; sp[0].h = 16; + sprite_anim(&sp[0], 0, 3, 1, ANIM_LOOP); + tick(sp, 1, 2); /* frame 2 */ + sprite_anim_stop(&sp[0], 1); /* остановить на кадре 1 */ + ok = (!(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) && + sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16); + tick(sp, 1, 4); /* не двигается */ + ok = ok && (sprite_anim_frame(&sp[0]) == 1); + + sprite_moveto(&sp[0], 60, 60, 3, 1); + tick(sp, 1, 5); /* на полпути */ + sprite_move_stop(&sp[0], 1); /* добить в цель */ + ok = ok && (sp[0].x == 60 && sp[0].y == 60 && + !sprite_moving(&sp[0]) && + (sprite_anim_status(&sp[0]) & SPR_MOVE_DONE)); + + sprite_moveto(&sp[0], 0, 0, 3, 1); + tick(sp, 1, 5); + sprite_move_stop(&sp[0], 0); /* замереть где есть */ + ok = ok && (sp[0].x != 0 && !sprite_moving(&sp[0]) && + !(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE)); + printf("T5 STOPS: %s\n", ok ? "PASS" : "FAIL"); + + puts("done"); + return 0; +}