// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // Scroll demo: a tile layer (jlTileLayer*) over a landscape three screens // wide and two tall, with a ball drawn over it every frame -- no // save-under, since the next layer draw covers it. The arrow keys or a // joystick move the view; left alone it drifts. The two tile rows under // the layer are a status bar whose bar grows with the frames drawn a // second, so the demo shows how fast each port scrolls. Escape ends it. // // The tiles are painted with the drawing calls and snapped, since a // jlTileT is in each port's own format; the map is generated. #include #include #define MAP_W 96 #define MAP_H 40 #define VIEW_ROWS 23 // tile rows of the layer #define BAR_TOP (VIEW_ROWS * JL_TILE_PIXELS_PER_SIDE) #define BAR_H (JL_SURFACE_HEIGHT - BAR_TOP) #define BAR_PIXELS_PER_FPS 5 // the bar's growth for each frame a second #define BAR_INSET 4 #define TILE_SKY 0 #define TILE_GRASS 1 #define TILE_DIRT 2 #define TILE_BRICK 3 #define TILE_WATER 4 #define TILE_CLOUD 5 #define TILE_STONE 6 #define TILE_FLOWER 7 #define TILE_COUNT 8 #define COLOR_SKY 1 #define COLOR_DIRT 2 #define COLOR_GRASS 3 #define COLOR_DIRT_DARK 4 #define COLOR_BRICK 5 #define COLOR_GREY 6 #define COLOR_WATER 7 #define COLOR_FOAM 8 #define COLOR_CLOUD 9 #define COLOR_GREY_DARK 10 #define COLOR_PETAL 11 #define COLOR_BALL 12 #define COLOR_SHINE 13 #define COLOR_BAR_BACK 14 #define COLOR_BAR 15 #define GROUND_LOW 34 // the ground's highest and lowest rows #define GROUND_HIGH 22 #define WATER_LINE 31 // ground below this row floods #define STONE_DEPTH 4 // dirt rows above the stone #define LEDGES 30 // brick ledges and clouds, turn about #define LEDGE_MIN 2 // their length in tiles #define LEDGE_MAX 6 #define LEDGE_TOP 4 // their highest row #define LEDGE_GAP 4 // rows kept clear above the highest ground #define BALL_TILES 2 #define BALL_SIDE (BALL_TILES * JL_TILE_PIXELS_PER_SIDE) #define BALL_X ((JL_SURFACE_WIDTH - BALL_SIDE) / 2) #define BALL_Y ((BAR_TOP - BALL_SIDE) / 2) #define SPEED 90 // pixels per second, the same on every port #define DRIFT_X 40 #define DRIFT_Y 12 #define STICK_THRESHOLD 32 #define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel #define MAX_STEP_MILLIS 100 // after a stall, move no further than this #define MILLIS_PER_SECOND 1000 #define MAP_PX_W ((int32_t)MAP_W * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT) #define MAP_PX_H ((int32_t)MAP_H * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT) #define CODEGEN_BYTES (32UL * 1024) static const uint16_t gPalette[16] = { 0x000, 0x6AF, 0x852, 0x3B3, 0x531, 0xB33, 0x999, 0x24C, 0x8CF, 0xFFF, 0x555, 0xFC2, 0xFD0, 0xFFF, 0x113, 0x4E4 }; // The ball, 16x16 at 4 bits a pixel, high nibble left; colour 0 is clear. static const uint8_t gBallRows[BALL_SIDE * BALL_SIDE / 2] = { 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x0C, 0xCC, 0xDC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0, 0x0C, 0xCD, 0xDC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0, 0xCC, 0xDD, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0, 0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCC, 0xCC, 0x00, 0x00, 0x00 }; static jlTileT gTiles[TILE_COUNT]; static uint8_t gMap[MAP_W * MAP_H]; static uint8_t gBallTiles[BALL_TILES * BALL_TILES * JL_TILE_BYTES]; static uint32_t gSeed = 2026u; static void drawBar(jlSurfaceT *screen, uint16_t framesPerSecond); static void makeBall(void); static void makeMap(void); static void makeTiles(jlSurfaceT *screen); static uint16_t nextRandom(uint16_t below); static int32_t stickAxis(int8_t stick, bool minus, bool plus, int32_t drift); static int32_t wrap(int32_t v, int32_t size); // The status bar under the layer: its bar is BAR_PIXELS_PER_FPS a frame a second. static void drawBar(jlSurfaceT *screen, uint16_t framesPerSecond) { uint16_t w; w = (uint16_t)(framesPerSecond * BAR_PIXELS_PER_FPS); if (w > JL_SURFACE_WIDTH - 2 * BAR_INSET) { w = JL_SURFACE_WIDTH - 2 * BAR_INSET; } jlFillRect(screen, 0, BAR_TOP, JL_SURFACE_WIDTH, BAR_H, COLOR_BAR_BACK); if (w > 0) { jlFillRect(screen, BAR_INSET, BAR_TOP + BAR_INSET, w, BAR_H - 2 * BAR_INSET, COLOR_BAR); } } // The ball's rows, repacked into the tile-major layout jlSpriteCreate takes. static void makeBall(void) { uint16_t tx; uint16_t ty; uint16_t row; uint16_t b; for (ty = 0; ty < BALL_TILES; ty++) { for (tx = 0; tx < BALL_TILES; tx++) { for (row = 0; row < JL_TILE_PIXELS_PER_SIDE; row++) { for (b = 0; b < JL_TILE_BYTES_PER_ROW; b++) { gBallTiles[(ty * BALL_TILES + tx) * JL_TILE_BYTES + row * JL_TILE_BYTES_PER_ROW + b] = gBallRows[(ty * JL_TILE_PIXELS_PER_SIDE + row) * (BALL_SIDE / 2) + tx * JL_TILE_BYTES_PER_ROW + b]; } } } } } // Rolling ground with flooded hollows and flowers, and brick ledges and // clouds above it. static void makeMap(void) { uint16_t tx; uint16_t ty; uint16_t ground; uint16_t len; uint16_t i; uint16_t k; uint8_t t; ground = GROUND_HIGH + 4; for (tx = 0; tx < MAP_W; tx++) { if (nextRandom(3) == 0 && ground < GROUND_LOW) { ground++; } else if (nextRandom(3) == 0 && ground > GROUND_HIGH) { ground--; } for (ty = 0; ty < MAP_H; ty++) { if (ty < ground) { t = (ty >= WATER_LINE) ? TILE_WATER : TILE_SKY; } else if (ty == ground) { t = (ty >= WATER_LINE) ? TILE_STONE : TILE_GRASS; } else if (ty < ground + STONE_DEPTH) { t = TILE_DIRT; } else { t = TILE_STONE; } gMap[ty * MAP_W + tx] = t; } if (ground < WATER_LINE && nextRandom(5) == 0) { gMap[(ground - 1) * MAP_W + tx] = TILE_FLOWER; } } for (i = 0; i < LEDGES; i++) { tx = nextRandom(MAP_W - LEDGE_MAX); ty = (uint16_t)(LEDGE_TOP + nextRandom(GROUND_HIGH - LEDGE_TOP - LEDGE_GAP)); len = (uint16_t)(LEDGE_MIN + nextRandom(LEDGE_MAX - LEDGE_MIN)); for (k = 0; k < len; k++) { gMap[ty * MAP_W + tx + k] = ((i & 1u) != 0u) ? TILE_BRICK : TILE_CLOUD; } } } // Paints the eight tiles in block row 0 of the screen and snaps them. static void makeTiles(jlSurfaceT *screen) { jlFillRect(screen, 0, 0, 8, 8, COLOR_SKY); jlFillRect(screen, 8, 0, 8, 8, COLOR_DIRT); jlFillRect(screen, 8, 0, 8, 3, COLOR_GRASS); jlDrawPixel(screen, 10, 3, COLOR_GRASS); jlDrawPixel(screen, 13, 3, COLOR_GRASS); jlFillRect(screen, 16, 0, 8, 8, COLOR_DIRT); jlDrawPixel(screen, 17, 2, COLOR_DIRT_DARK); jlDrawPixel(screen, 21, 5, COLOR_DIRT_DARK); jlFillRect(screen, 24, 0, 8, 8, COLOR_BRICK); jlDrawLine(screen, 24, 3, 31, 3, COLOR_GREY); jlDrawLine(screen, 24, 7, 31, 7, COLOR_GREY); jlDrawLine(screen, 24, 0, 24, 2, COLOR_GREY); jlDrawLine(screen, 28, 4, 28, 6, COLOR_GREY); jlFillRect(screen, 32, 0, 8, 8, COLOR_WATER); jlDrawLine(screen, 32, 1, 34, 1, COLOR_FOAM); jlDrawLine(screen, 36, 5, 38, 5, COLOR_FOAM); jlFillRect(screen, 40, 0, 8, 8, COLOR_SKY); jlFillRect(screen, 41, 3, 6, 3, COLOR_CLOUD); jlFillRect(screen, 43, 2, 3, 1, COLOR_CLOUD); jlFillRect(screen, 48, 0, 8, 8, COLOR_GREY); jlDrawPixel(screen, 50, 2, COLOR_GREY_DARK); jlDrawPixel(screen, 53, 5, COLOR_GREY_DARK); jlDrawPixel(screen, 49, 6, COLOR_GREY_DARK); jlFillRect(screen, 56, 0, 8, 8, COLOR_SKY); jlDrawLine(screen, 59, 3, 59, 7, COLOR_GRASS); jlFillRect(screen, 58, 1, 3, 2, COLOR_PETAL); jlTileSnap(screen, TILE_SKY, 0, &gTiles[TILE_SKY]); jlTileSnap(screen, TILE_GRASS, 0, &gTiles[TILE_GRASS]); jlTileSnap(screen, TILE_DIRT, 0, &gTiles[TILE_DIRT]); jlTileSnap(screen, TILE_BRICK, 0, &gTiles[TILE_BRICK]); jlTileSnap(screen, TILE_WATER, 0, &gTiles[TILE_WATER]); jlTileSnap(screen, TILE_CLOUD, 0, &gTiles[TILE_CLOUD]); jlTileSnap(screen, TILE_STONE, 0, &gTiles[TILE_STONE]); jlTileSnap(screen, TILE_FLOWER, 0, &gTiles[TILE_FLOWER]); } // A number in [0, below), from a fixed sequence so every port builds the // same landscape. static uint16_t nextRandom(uint16_t below) { gSeed = gSeed * 1103515245u + 12345u; return (uint16_t)((gSeed >> 16) % below); } // The view's speed along one axis, pixels per second: the stick or the two // keys, else the drift. static int32_t stickAxis(int8_t stick, bool minus, bool plus, int32_t drift) { if (minus || stick < -STICK_THRESHOLD) { return -SPEED; } if (plus || stick > STICK_THRESHOLD) { return SPEED; } return drift; } // v moved into [0, size). static int32_t wrap(int32_t v, int32_t size) { while (v < 0) { v += size; } while (v >= size) { v -= size; } return v; } int main(void) { jlConfigT config; jlSurfaceT *screen; jlSpriteT *ball; jlTileLayerT *layer; int32_t x; // the view's origin, 1/256 of a pixel int32_t y; int32_t vx; // pixels per second int32_t vy; uint32_t lastMillis; uint32_t barMillis; uint32_t now; uint32_t elapsed; uint16_t frames; config.codegenBytes = CODEGEN_BYTES; config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); return 1; } screen = jlStageGet(); jlPaletteSet(screen, 0, gPalette); jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0); makeTiles(screen); makeMap(); makeBall(); ball = jlSpriteCreate(gBallTiles, BALL_TILES, BALL_TILES); layer = jlTileLayerCreate(gTiles, gMap, MAP_W, MAP_H, 0, VIEW_ROWS); if (ball == NULL || layer == NULL) { fprintf(stderr, "out of memory\n"); jlShutdown(); return 1; } (void)jlSpriteCompile(ball); jlSurfaceClear(screen, 0); drawBar(screen, 0); x = 0; y = (int32_t)(GROUND_HIGH - VIEW_ROWS / 2) * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT; frames = 0; lastMillis = jlMillisElapsed(); barMillis = lastMillis; for (;;) { jlInputPoll(); if (jlKeyPressed(JL_KEY_ESCAPE)) { break; } vx = stickAxis(jlJoystickX(JL_JOYSTICK_1), jlKeyDown(JL_KEY_LEFT), jlKeyDown(JL_KEY_RIGHT), DRIFT_X); vy = stickAxis(jlJoystickY(JL_JOYSTICK_1), jlKeyDown(JL_KEY_UP), jlKeyDown(JL_KEY_DOWN), DRIFT_Y); now = jlMillisElapsed(); elapsed = now - lastMillis; lastMillis = now; if (elapsed > MAX_STEP_MILLIS) { elapsed = MAX_STEP_MILLIS; } x = wrap(x + (vx * (int32_t)elapsed << SUBPIXEL_SHIFT) / MILLIS_PER_SECOND, MAP_PX_W); y = wrap(y + (vy * (int32_t)elapsed << SUBPIXEL_SHIFT) / MILLIS_PER_SECOND, MAP_PX_H); jlTileLayerDraw(screen, layer, (uint16_t)(x >> SUBPIXEL_SHIFT), (uint16_t)(y >> SUBPIXEL_SHIFT)); jlSpriteDraw(screen, ball, BALL_X, BALL_Y); frames++; if (now - barMillis >= MILLIS_PER_SECOND) { drawBar(screen, (uint16_t)((uint32_t)frames * MILLIS_PER_SECOND / (now - barMillis))); frames = 0; barMillis = now; } jlStagePresent(); } jlTileLayerDestroy(layer); jlSpriteDestroy(ball); jlShutdown(); return 0; }