diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index bd8a0cb..2602d5f 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -44,6 +44,7 @@ rt_test 4892 seek 4188 simple 955 solidt 11531 +space 10956 spranim 8752 spriteclip 4119 sprites 6791 diff --git a/examples/space/.gitignore b/examples/space/.gitignore new file mode 100644 index 0000000..948892b --- /dev/null +++ b/examples/space/.gitignore @@ -0,0 +1,2 @@ +space.atl +*.raw diff --git a/examples/space/Makefile b/examples/space/Makefile new file mode 100644 index 0000000..7eb7526 --- /dev/null +++ b/examples/space/Makefile @@ -0,0 +1,13 @@ +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := space +EXTRA_FLAGS ?= --gfx 256 --memory tiny +EXTRA_DATA := space.atl +include $(PROJ_ROOT)/app.mk + +# Атлас: три ленты 16×16×4 (астероид/взрыв/маяк) → одна EMM-страница. +space.atl: gen_sprites.py $(PROJ_ROOT)/toolchain/mkatlas.py + python3 gen_sprites.py + python3 $(PROJ_ROOT)/toolchain/mkatlas.py $@ \ + asteroid.raw:16x16:1x4 explode.raw:16x16:1x4 beacon.raw:16x16:1x4 + +$(EXAMPLE).exe: space.atl diff --git a/examples/space/gen_sprites.py b/examples/space/gen_sprites.py new file mode 100644 index 0000000..e8670e7 --- /dev/null +++ b/examples/space/gen_sprites.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Генератор спрайтов для examples/space (raw-ленты для mkatlas.py). + +asteroid.raw — 16×16×4 (LOOP): серый астероид, кратеры вращаются по +фазам (шаг 22.5°). +explode.raw — 16×16×4 (ONCE): расширяющийся взрыв (жёлтое ядро → +кольца → разлёт искр). +beacon.raw — 16×16×4 (PINGPONG): пульсирующий маяк (крест с +растущим свечением). + +Палитра — дефолтная EGA (индексы 0..15), 0xFF = прозрачно. +""" +import math + +T = 0xFF # прозрачный +W = H = 16 +CX = CY = 7.5 + + +def blank(): + return [[T] * W for _ in range(H)] + + +def disc(px, cx, cy, r, c): + for y in range(H): + for x in range(W): + if (x - cx) ** 2 + (y - cy) ** 2 <= r * r: + px[y][x] = c + + +def ring(px, cx, cy, r, c): + for a in range(0, 360, 4): + x = int(round(cx + r * math.cos(math.radians(a)))) + y = int(round(cy + r * math.sin(math.radians(a)))) + if 0 <= x < W and 0 <= y < H: + px[y][x] = c + + +def dot(px, x, y, c): + if 0 <= x < W and 0 <= y < H: + px[int(y)][int(x)] = c + + +def asteroid(phase): + px = blank() + disc(px, CX, CY, 6.8, 7) # LIGHTGRAY тело + ring(px, CX, CY, 6.8, 8) # DARKGRAY контур + for i in range(3): # кратеры на орбите + a = math.radians(phase * 22.5 + i * 120) + disc(px, CX + 3.6 * math.cos(a), CY + 3.6 * math.sin(a), 1.4, 8) + dot(px, CX - 2, CY - 3, 15) # блик WHITE + return px + + +def explode(phase): + px = blank() + if phase == 0: + disc(px, CX, CY, 2.2, 14) # YELLOW ядро + elif phase == 1: + disc(px, CX, CY, 3.8, 14) + ring(px, CX, CY, 4.6, 12) # LIGHTRED + elif phase == 2: + ring(px, CX, CY, 4.2, 14) + ring(px, CX, CY, 6.0, 12) + ring(px, CX, CY, 7.0, 4) # RED + else: + for a in range(0, 360, 30): # разлёт искр + r = 6.5 + dot(px, CX + r * math.cos(math.radians(a)), + CY + r * math.sin(math.radians(a)), 4) + dot(px, CX + 4 * math.cos(math.radians(a + 15)), + CY + 4 * math.sin(math.radians(a + 15)), 6) # BROWN + return px + + +def beacon(phase): + px = blank() + r = 2 + phase * 1.6 + for d in range(int(r) + 1): # крест-лучи + for xx, yy in ((CX - d, CY), (CX + d, CY), (CX, CY - d), (CX, CY + d)): + dot(px, round(xx), round(yy), 11) # LIGHTCYAN + disc(px, CX, CY, 1.4, 15) # WHITE ядро + if phase >= 2: + ring(px, CX, CY, r + 1, 9) # LIGHTBLUE ореол + return px + + +def save(name, frames): + with open(name, "wb") as f: + for fr in frames: + for row in fr: + f.write(bytes(row)) + print(f"gen_sprites: {name} ({len(frames)} кадров)") + + +save("asteroid.raw", [asteroid(k) for k in range(4)]) +save("explode.raw", [explode(k) for k in range(4)]) +save("beacon.raw", [beacon(k) for k in range(4)]) diff --git a/examples/space/space.c b/examples/space/space.c new file mode 100644 index 0000000..4d60ea1 --- /dev/null +++ b/examples/space/space.c @@ -0,0 +1,195 @@ +/* + * space — демо всего спрайтового стека libbgi (2026-07-13): + * + * .atl-атлас с диска (mkatlas.py) → EMM-страница, подключаемая в W0 + * (atlas_load: ISR-стаб, ESTEX READ в W3) → движок sprite_t.page + * (sprite_update сам мапит страницу на блит-проход) → авто-анимации: + * + * - 5 астероидов: кадровая ANIM_LOOP (вращение, 4 кадра) + + * sprite_moveto к случайным целям с разными скоростями; + * по прибытии (sprite_moving()==0) — ВЗРЫВ в точке + новая цель; + * - 5 взрывов: ANIM_ONCE (4 кадра); по SPR_ANIM_DONE — sprite_hide; + * - маяк: ANIM_PINGPONG (пульс 0..3..0); + * - дабл-буфер: фон (звёзды + планеты) на обеих страницах, кадр = + * sprite_update на скрытой + vsync + флип; FPS-метр. + * + * Цели держатся в пределах экрана → gfx_sprite_clip(0) (noclip-ядра). + * ESC — выход. + */ +#include +#include +#include +#include +#include +#include + +#define NAST 5 /* астероиды: sp[0..4] */ +#define NEX NAST /* взрывы: sp[5..9] */ +#define IBEAC (NAST + NEX) /* маяк: sp[10] */ +#define NSPR (IBEAC + 1) + +#define MAXX (320 - 16) +#define MAXY (256 - 16) + +static atlas_t at; +static sprite_t sp[NSPR]; + +/* Случайная точка в пределах экрана (модуль — не горячий путь). */ +static int rnd_x(void) { return rand() % (MAXX - 32) + 16; } +static int rnd_y(void) { return rand() % (MAXY - 48) + 32; } + +/* Новая цель астероиду i: скорость по номеру (шаг 1..3, интервал 1..2). */ +static void new_target(uint8_t i) +{ + sprite_moveto(&sp[i], rnd_x(), rnd_y(), + (uint8_t)(1 + i % 3), (uint8_t)(1 + (i & 1))); +} + +static void draw_space(void) +{ + int i; + + /* Фон обязан быть ИДЕНТИЧЕН на обеих страницах (иначе флип 25 Гц + * мерцает разными звёздами) — фиксированный seed на отрисовку. */ + srand(1234); + + cleardevice(); + setfillstyle(SOLID_FILL, BLUE); /* планеты */ + fillellipse(60, 200, 26, 26); + setfillstyle(SOLID_FILL, LIGHTBLUE); + fillellipse(60, 200, 18, 18); + setfillstyle(SOLID_FILL, BROWN); + fillellipse(270, 190, 14, 14); + + for (i = 0; i < 90; i++) /* звёзды */ + putpixel(rand() % 320, rand() % 256, + (i & 3) ? DARKGRAY : WHITE); + + setcolor(WHITE); + outtextxy(36, 4, "SPACE: atlas W0 + auto-anim, ESC"); +} + +#define FPS_METER 1 +#if FPS_METER +static void show_fps(uint8_t v) +{ + char buf[4]; + + buf[0] = (char)('0' + (v / 100) % 10); + buf[1] = (char)('0' + (v / 10) % 10); + buf[2] = (char)('0' + v % 10); + buf[3] = 0; + setcolor(WHITE); + setfillstyle(SOLID_FILL, BLACK); + bar(0, 0, 27, 9); + outtextxy(2, 1, buf); +} +#endif + +int main(void) +{ + uint8_t i, page, hidden; + datetime_t dt; +#if FPS_METER + uint8_t frames = 0, last_sec, fps_val = 0, fps_draw = 0; +#endif + + if (atlas_load(&at, "space.atl") != 0 && + atlas_load(&at, "a:\\space.atl") != 0) + return 1; + + getdatetime(&dt); + srand((unsigned)(dt.second * 77u + dt.minute)); + + initgraph(); + gfx_sprite_clip(0); /* цели в пределах */ + + /* Палитра страницы 1 = палитре страницы 0 — иначе флип на page 1 + * показывает сцену ЧЁРНОЙ (у каждой страницы своя палитра). */ + { + static uint8_t egapal[16 * 4]; + gfx_pal_get(0, 0, 16, egapal); + gfx_pal_load(1, 0, 16, egapal); + } + + /* Спрайты: астероиды (LOOP + цель), взрывы (скрыты), маяк (PINGPONG). */ + for (i = 0; i < NAST; i++) { + atlas_sprite_init(&sp[i], &at, 0); + sp[i].x = rnd_x(); + sp[i].y = rnd_y(); + sprite_anim(&sp[i], 0, 3, (uint8_t)(4 + i), ANIM_LOOP); + new_target(i); + sprite_show(&sp[i]); + } + for (i = 0; i < NEX; i++) + atlas_sprite_init(&sp[NAST + i], &at, 1); + atlas_sprite_init(&sp[IBEAC], &at, 2); + sp[IBEAC].x = 292; sp[IBEAC].y = 20; + sprite_anim(&sp[IBEAC], 0, 3, 6, ANIM_PINGPONG); + sprite_show(&sp[IBEAC]); + + /* Фон на обеих страницах (ОЗУ-копия = бэкап для heal). */ + for (page = 0; page < 2; page++) { + gfx_set_draw_page(page); + draw_space(); + sprite_update(sp, NSPR); + } + gfx_set_visible_page(0); + getdatetime(&dt); + srand((unsigned)(dt.second * 77u + dt.minute)); /* цели — случайные */ +#if FPS_METER + getdatetime(&dt); + last_sec = dt.second; +#endif + + for (;;) { + if (kbhit() && getch() == 27) + break; + + /* Прибывшие астероиды: взрыв в точке + новая цель. */ + for (i = 0; i < NAST; i++) { + if (!sprite_moving(&sp[i])) { + sprite_t *ex = &sp[NAST + i]; + + sprite_move(ex, sp[i].x, sp[i].y); + sprite_anim(ex, 0, 3, 4, ANIM_ONCE); + sprite_show(ex); + new_target(i); + } + } + /* Отгоревшие взрывы — спрятать. */ + for (i = 0; i < NEX; i++) { + sprite_t *ex = &sp[NAST + i]; + + if ((sprite_anim_status(ex) & SPR_ANIM_DONE) && + (ex->flags & SPR_VISIBLE)) + sprite_hide(ex); + } + + hidden = gfx_get_visible_page() ^ 1; + gfx_set_draw_page(hidden); +#if FPS_METER + /* Свежую цифру рисовать на ОБЕИХ страницах (fps_draw = 2 кадра + * подряд) — иначе она живёт на одной и мерцает при флипе. */ + frames++; + getdatetime(&dt); + if (dt.second != last_sec) { + last_sec = dt.second; + fps_val = frames; + frames = 0; + fps_draw = 2; + } + if (fps_draw) { + fps_draw--; + show_fps(fps_val); + } +#endif + sprite_update(sp, NSPR); + gfx_wait_vsync(); + gfx_set_visible_page(hidden); + } + + closegraph(); + atlas_free(&at); + return 0; +}