BLIT386BLIT386

Example: Primitives

Every basic shape BLIT386 can draw: pixels, lines, rectangles, and the filled and outlined variants.

Written for blit386 1.8.0. The rest of blit386.dev documents the newest engine, so an API you find there may be missing from a game pinned to 1.8.0.

Live version: https://demos.blit386.dev/primitives. This is the demo-site source, not a standalone file: it imports and calls helpers from ./shared/ (themed panels, on-screen D-pad, post-process fallbacks), which are not part of blit386 and are not on npm. To run it as is, keep those files from https://github.com/blit386/blit386/tree/main/packages/demos/src/shared at the same relative path. To adapt it, remove or replace the helper imports and every call to them.

primitives.js
/*

Primitives: every basic shape BLIT386 can draw, each with a small animation.
@description Every basic shape BLIT386 can draw: pixels, lines, rectangles, and the filled and outlined variants.

Part of the BLIT386 demo series.

Prerequisites:
  - Basics: https://demos.blit386.dev/basics

Live version: https://demos.blit386.dev/primitives

Guides:
  - Palette: https://blit386.dev/docs/guides/palette#draw-using-absolute-palette-indices
  - Overlay: https://blit386.dev/docs/guides/overlay#optional-bands

Reference:
  - Rendering: https://blit386.dev/docs/api/rendering#primitives
  - Core types: https://blit386.dev/docs/api/core-types#color32

---

WHAT YOU WILL SEE
-----------------

Six captioned sections on a dark blue-gray screen, each showing one drawing call:

- Pixels: fifty scattered dots whose colors cycle through the rainbow.
- Lines: a red horizontal, a green vertical, and a blue diagonal line, plus a white line
  spinning around a center point.
- Rect Outlines: red, green, and blue hollow rectangles, plus a yellow square that
  grows and shrinks.
- Rect Fills: the same three rectangles filled in, plus a yellow square sliding
  left and right.
- Clear Rect: a grid of steel-blue squares with a moving window erased out of it.
- Combined: a dark graph box with a gray border and a scrolling cyan sine wave inside.

The engine overlay is open from the start, with the palette grid showing 24 slots per row
and 3 rows at a time.

WHAT YOU WILL LEARN
-------------------

- "Primitives" are the simplest building blocks of drawing: single dots,
  lines, and rectangles.
- BT.drawPixel(pos, slot), BT.drawLine(p0, p1, slot), BT.drawRect(rect, slot),
  BT.drawRectFill(rect, slot), and BT.clearRect(rect, slot). Every call takes a palette
  slot number, never a Color32.
- Rect2i(x, y, width, height) describes a rectangle; Vector2i(x, y) a point.
- Animating in update() with a tick counter, then reading it in render() to spin, pulse,
  and slide shapes. Math.sin() and Math.cos() turn the counter into smooth motion.
- "Palette animation": update() computes fifty rainbow colors with Color32.fromHSL() and
  writes them into palette slots 20-69, so render() only passes slot numbers.
- Section captions with the shared UI kit's ui.caption(x, y, text).

TRY THIS
--------

- The overlay is already open. Watch slots 20-69 in its palette grid at the bottom: they
  cycle through the rainbow in step with the dots in the Pixels section.
- Press the backquote key (`) or click/tap the symbol in the bottom-left corner to hide
  the overlay and see the whole screen.

*/

import { bootstrap, BT, Color32, Rect2i, Vector2i } from 'blit386';

// The shared demo UI kit: applyTheme() installs the series' standard UI colors into the
// palette, and ui.caption() prints the section captions with them. We met the kit in the
// Basics demo: https://demos.blit386.dev/basics
import { applyTheme, ui } from './shared/ui.js';

/** @typedef {import('blit386').Palette} Palette */

// Every color we draw with is registered first in a numbered palette slot. Think of each
// slot as a labeled jar of paint on a shelf. Slot 0 is always transparent (invisible), so
// our colors start at 1. Slot 3 is left unused.
const C_WHITE = 1; // pure white: the spinning line and the overlay bar
const C_BG = 2; // dark blue-gray: background and the clearRect erase color
const C_RED = 4; // red: first rectangle in each row
const C_GREEN = 5; // green: second rectangle in each row
const C_BLUE = 6; // blue: third rectangle in each row
const C_YELLOW = 7; // yellow: the pulsing and sliding squares
const C_CYAN = 8; // cyan (bright blue-green): the sine wave line
const C_GRAY_BORDER = 9; // gray: the graph outline
const C_DARK = 10; // very dark blue: graph background and overlay text
const C_STEEL = 11; // steel blue: the squares in the clearRect grid

// Animated palette slots for the rainbow pixels. Each pixel needs its own slot so they
// can all be different colors. update() stores each pixel's current color in slots
// 20-69 every tick, and render() just passes the slot number to BT.drawPixel() - no
// Color32 needed there.
const C_PIXEL_BASE = 20; // pixel 0 uses slot 20, pixel 1 slot 21, ... pixel 49 slot 69

// How many rainbow pixels there are. init(), update(), and renderPixel() all loop over
// this same count, so it lives in one place instead of three repeated 50s.
const PIXEL_COUNT = 50;

// Shows every primitive drawing call, each in its own captioned section with
// a small animation.
class Demo {
    // How many update ticks have passed since the demo started. Everything that moves
    // reads this number.
    animTicks = 0;

    // The palette holding every color this demo uses.
    /** @type {Palette | null} */
    palette = null;

    // Palette slot map for the shared UI kit theme, filled in init() by applyTheme(): a
    // map like { bg, text, dim, header, ... } of slot numbers.
    theme = null;

    // Optional engine settings. We keep the default 320x240 screen, open the overlay from
    // the start, and show its palette grid with 24 chips per row and 3 visible rows.
    configure() {
        return {
            isOverlayPaletteEnabled: true,
            overlayPaletteColumns: 24,
            overlayPaletteRowsVisible: 3,
            isOverlayVisibleAtStart: true,

            // The overlay bars draw in white with very dark text, and the gaps between
            // bars in the background color.
            overlayStyle: {
                barPaletteIndex: C_WHITE,
                textPaletteIndex: C_DARK,
                gapPaletteIndex: C_BG,
            },
        };
    }

    // Runs once when the demo starts and sets up the palette.
    async init() {
        // We pick all the colors we need BEFORE drawing anything - like an artist
        // squeezing paint onto a palette before picking up the brush.
        this.palette = BT.paletteCreate(256);

        // Static colors (these never change from frame to frame).
        this.palette.set(C_WHITE, new Color32(255, 255, 255)); // pure white
        this.palette.set(C_BG, new Color32(40, 50, 80)); // dark blue-gray background
        this.palette.set(C_RED, new Color32(255, 100, 100)); // red shapes
        this.palette.set(C_GREEN, new Color32(100, 255, 100)); // green shapes
        this.palette.set(C_BLUE, new Color32(100, 100, 255)); // blue shapes
        this.palette.set(C_YELLOW, new Color32(255, 255, 100)); // yellow moving squares
        this.palette.set(C_CYAN, new Color32(100, 255, 255)); // cyan sine wave line
        this.palette.set(C_GRAY_BORDER, new Color32(100, 100, 100)); // graph border gray
        this.palette.set(C_DARK, new Color32(10, 15, 25)); // very dark graph background
        this.palette.set(C_STEEL, new Color32(100, 150, 200)); // steel blue grid squares

        // Pre-fill the rainbow pixel slots with gray so no slot is empty on the very
        // first frame, before update() has run.
        for (let i = 0; i < PIXEL_COUNT; i++) {
            this.palette.set(C_PIXEL_BASE + i, new Color32(128, 128, 128)); // start gray
        }

        // Install the shared UI theme. It writes the series' twelve standard UI
        // colors into high palette slots (240-251), far from our scene slots (1-11)
        // and the animated rainbow slots (20-69). The section captions draw with
        // these colors. This must happen BEFORE BT.paletteSet() below so the colors
        // go live with the palette.
        this.theme = applyTheme(this.palette);

        // Tell the engine "use this palette from now on."
        BT.paletteSet(this.palette);

        return true;
    }

    // Runs at a fixed rate, 60 times per second (the engine default). See the Basics demo
    // for the full explanation: https://demos.blit386.dev/basics
    // We count ticks AND pre-compute the rainbow pixel colors here so
    // render() stays fast.
    update() {
        // Add 1 each time update() runs. animTicks counts update ticks, not screen
        // refreshes, so after one second it is 60.
        this.animTicks++;

        // Pre-compute the rainbow pixel colors. Each pixel gets a different hue, and the
        // whole rainbow shifts forward by animTicks so it cycles over time. We do the
        // color math here and store the result in the palette, so render() can just say
        // "use slot 20", "use slot 21", and so on. This keeps ALL color math out of
        // render() - the "palette animation" technique.
        for (let i = 0; i < PIXEL_COUNT; i++) {
            // hue is a position on the color wheel: 0 is red, 120 green, 240 blue, 360
            // back to red. Multiplying i by 17 steps 17 degrees per pixel, so the
            // sequence wraps around the wheel a few times across the fifty pixels - a
            // denser rainbow than one even sweep. Adding animTicks rotates the whole
            // rainbow each tick, and % 360 (the remainder after dividing by 360) keeps
            // the value inside 0-359, like a clock wrapping past 12.
            const hue = (i * 17 + this.animTicks) % 360;

            // Color32.fromHSL(hue, saturation, lightness) turns the hue into an RGB
            // color. Saturation 100 is fully vivid, lightness 50 is a medium brightness.
            const color = Color32.fromHSL(hue, 100, 50);

            // Store this pixel's current color in its reserved palette slot.
            this.palette.set(C_PIXEL_BASE + i, color);
        }
    }

    // Runs once per screen refresh to draw everything. Each helper method draws one type
    // of primitive in its own section.
    render() {
        // Fill the whole screen with the dark background to start fresh each frame.
        BT.clear(C_BG);

        // Draw each type of primitive in its own area of the screen.
        this.renderPixel();
        this.renderLine();
        this.renderRectOutline();
        this.renderRectFill();
        this.renderClearRect();
        this.renderCombined();
    }

    // Shows BT.drawPixel(): one colored dot. We scatter fifty dots, each in a different
    // rainbow color. The colors shift over time because update() rotates them each tick.
    renderPixel() {
        const anchor = new Vector2i(10, 7);

        // Print the section caption with ui.caption() from the shared UI kit. Every demo
        // in the series uses this same widget, so all captions look identical everywhere.
        ui.caption(anchor.x, anchor.y, 'Pixels');

        // Draw the dots scattered across a small area. Their colors were already
        // computed in update() and stored in palette slots 20-69, so here we only
        // pass slot numbers.
        for (let i = 0; i < PIXEL_COUNT; i++) {
            // A formula spreads the dots out so they do not all overlap. Multiplying by
            // 13 and 7 scatters them without an obvious pattern; % 60 and % 20 keep them
            // inside a 60x20 box.
            const x = anchor.x + ((i * 13) % 60);
            const y = anchor.y + ((i * 7) % 20) + 15;
            const pos = new Vector2i(x, y);

            // C_PIXEL_BASE + i is the palette slot for pixel i (slot 20, 21, ..., 69).
            BT.drawPixel(pos, C_PIXEL_BASE + i);
        }
    }

    // Shows BT.drawLine(): a straight line between two points. Three static lines
    // (horizontal, vertical, diagonal) plus one that spins.
    renderLine() {
        const anchor = new Vector2i(10, 75);

        ui.caption(anchor.x, anchor.y, 'Lines');

        // A horizontal line goes straight left to right. Color: red.
        BT.drawLine(new Vector2i(anchor.x, anchor.y + 15), new Vector2i(anchor.x + 60, anchor.y + 15), C_RED);

        // A vertical line goes straight up and down. Color: green.
        BT.drawLine(new Vector2i(anchor.x + 10, anchor.y + 20), new Vector2i(anchor.x + 10, anchor.y + 40), C_GREEN);

        // A diagonal line goes from top-left to bottom-right. Color: blue.
        BT.drawLine(new Vector2i(anchor.x + 20, anchor.y + 20), new Vector2i(anchor.x + 50, anchor.y + 40), C_BLUE);

        // A spinning line that turns 2 degrees per tick. Math.sin and Math.cos want the
        // angle in radians, not degrees: a full circle is 2 * Math.PI radians, or 360
        // degrees, so multiplying degrees by Math.PI / 180 converts them.
        const angle = (this.animTicks * 2 * Math.PI) / 180;

        // The center of the spinning line and how long it is.
        const centerX = anchor.x + 40;
        const centerY = anchor.y + 30;
        const radius = 15;

        // Math.cos and Math.sin turn the angle into horizontal and vertical distances.
        // Adding them to the center gives the far end of the line.
        const endX = centerX + Math.cos(angle) * radius;
        const endY = centerY + Math.sin(angle) * radius;

        // Draw the white line from the center to the far end. Math.floor rounds the
        // decimal result down to a whole pixel.
        BT.drawLine(new Vector2i(centerX, centerY), new Vector2i(Math.floor(endX), Math.floor(endY)), C_WHITE);
    }

    // Shows BT.drawRect(): just the border of a rectangle, hollow inside. Three static
    // rectangles in different colors plus a square that pulses in size.
    renderRectOutline() {
        const anchor = new Vector2i(90, 30);

        ui.caption(anchor.x, anchor.y, 'Rect Outlines');

        // Three rectangles in different colors. Rect2i takes (x, y, width, height).
        BT.drawRect(new Rect2i(anchor.x, anchor.y + 15, 40, 25), C_RED);
        BT.drawRect(new Rect2i(anchor.x + 50, anchor.y + 15, 30, 30), C_GREEN);
        BT.drawRect(new Rect2i(anchor.x + 90, anchor.y + 15, 25, 35), C_BLUE);

        // A yellow square that grows and shrinks. Math.sin swings smoothly between -1 and
        // +1, so 10 + 5 * sin swings between 5 and 15. Math.floor rounds to whole pixels.
        const pulse = Math.floor(10 + Math.sin(this.animTicks * 0.1) * 5);

        // Use pulse as both width and height, doubled so the pulsing is easier to see:
        // the square swings between 10x10 and 30x30.
        BT.drawRect(new Rect2i(anchor.x + 130, anchor.y + 15, pulse * 2, pulse * 2), C_YELLOW);
    }

    // Shows BT.drawRectFill(): a rectangle filled with solid color. Same layout as the
    // outlines above, but filled in.
    renderRectFill() {
        const anchor = new Vector2i(90, 90);

        ui.caption(anchor.x, anchor.y, 'Rect Fills');

        // Three filled rectangles in different colors.
        BT.drawRectFill(new Rect2i(anchor.x, anchor.y + 15, 40, 25), C_RED);
        BT.drawRectFill(new Rect2i(anchor.x + 50, anchor.y + 15, 30, 30), C_GREEN);
        BT.drawRectFill(new Rect2i(anchor.x + 90, anchor.y + 15, 25, 35), C_BLUE);

        // A yellow square that slides back and forth. Math.sin swings between -1 and 1;
        // multiplying by 20 slides the square 20 pixels left and right of anchor.x + 130.
        const slideX = anchor.x + 130 + Math.floor(Math.sin(this.animTicks * 0.05) * 20);

        BT.drawRectFill(new Rect2i(slideX, anchor.y + 15, 20, 20), C_YELLOW);
    }

    // Shows BT.clearRect(): erases a rectangle back to one color. We draw a grid of
    // steel-blue squares, then erase a moving rectangular chunk, which shows the
    // background color underneath. Like drawRect(), it takes the rectangle first and the
    // slot second - it just paints over instead of drawing an outline or fill.
    renderClearRect() {
        const anchor = new Vector2i(10, 135);

        ui.caption(anchor.x, anchor.y, 'Clear Rect');

        // Draw a grid of steel-blue squares: the outer loop goes across (i = 0 to 9), the
        // inner loop goes down (j = 0 to 4).
        for (let i = 0; i < 10; i++) {
            for (let j = 0; j < 5; j++) {
                // Each square is 8x8 pixels with a 2-pixel gap (one square
                // every 10 pixels).
                BT.drawRectFill(new Rect2i(anchor.x + i * 10, anchor.y + 15 + j * 10, 8, 8), C_STEEL);
            }
        }

        // A moving X position for the erased area. It swings between anchor.x + 5 and
        // anchor.x + 35, centered on anchor.x + 20.
        const clearX = anchor.x + 20 + Math.floor(Math.sin(this.animTicks * 0.03) * 15);

        // Erase a 40x30 rectangle back to the background color, like a window sliding
        // across the grid.
        BT.clearRect(new Rect2i(clearX, anchor.y + 25, 40, 30), C_BG);
    }

    // Shows several primitives working together as a sine wave graph: a filled rectangle
    // for the background, an outline for the border, and short lines tracing the wave.
    renderCombined() {
        const anchor = new Vector2i(130, 165);

        ui.caption(anchor.x, anchor.y, 'Combined');

        // The graph's position and size on screen, offset from the section anchor.
        const graphX = anchor.x;
        const graphY = anchor.y + 15;
        const graphW = 180;
        const graphH = 50;

        // Fill the graph area with a very dark color so the wave stands out.
        BT.drawRectFill(new Rect2i(graphX, graphY, graphW, graphH), C_DARK);

        // Draw a gray border around the graph.
        BT.drawRect(new Rect2i(graphX, graphY, graphW, graphH), C_GRAY_BORDER);

        // Draw the scrolling wave by connecting short line segments: walk across
        // the graph one pixel at a time and work out the wave height at this column
        // and the next.
        for (let x = 0; x < graphW - 1; x++) {
            // Math.sin produces the wave. Adding animTicks makes it scroll; multiplying
            // by 0.1 sets how tightly it wiggles; multiplying by graphH / 3 sets how
            // tall it is.
            const y1 = Math.floor(graphH / 2 + Math.sin((x + this.animTicks) * 0.1) * (graphH / 3));
            const y2 = Math.floor(graphH / 2 + Math.sin((x + 1 + this.animTicks) * 0.1) * (graphH / 3));

            // Connect this column's point to the next one with a cyan line.
            BT.drawLine(new Vector2i(graphX + x, graphY + y1), new Vector2i(graphX + x + 1, graphY + y2), C_CYAN);
        }
    }
}

// Hand the Demo class to the BLIT386 engine to start running it.
bootstrap(Demo);