Demo Stage Sparkle

Source: src/demo_stage_sparkle.c · 191 lines · 8 functions.

Module entry points

These functions are exposed to the rest of the project. Open one to follow its decisions and the helpers it calls.

How to read these diagrams

Each function has its own expandable diagram using the actual expressions and statements from this file. Decisions split into True and False; loop bodies are indented. Returns end the function immediately; break and continue follow their C meanings. Arrows between statements show their order when execution reaches them.

These are structural source diagrams before preprocessing, not runtime traces. Conditional compilation and macros remain source expressions. Ternary choices remain inside their statement. Calls are linked separately; callback execution and task scheduling are not inferred.

Functions in this file

setStageNineCell() · source line 57
static void setStageNineCell(int column, int row, uint8_t glyph)
  1. Decisioncolumn < 0 || column >= DEMO_STAGE_NINE_COLUMNS || row < 0 || row >= DEMO_STAGE_NINE_ROWS
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executestage_nine_cells[(size_t)row * DEMO_STAGE_NINE_COLUMNS + (size_t)column] = glyph;
Read original function source
static void setStageNineCell(int column, int row, uint8_t glyph)
{
    if (column < 0 || column >= DEMO_STAGE_NINE_COLUMNS ||
        row < 0 || row >= DEMO_STAGE_NINE_ROWS) return;
    stage_nine_cells[(size_t)row * DEMO_STAGE_NINE_COLUMNS + (size_t)column] = glyph;
}
prepareStageNineReflection() · source line 64
static void prepareStageNineReflection(uint32_t now_ms)
  1. Executememset(stage_nine_cells, 0, sizeof(stage_nine_cells));
  2. FOR loopuint8_t index = 0U; index < DEMO_STAGE_NINE_COORDINATE_COUNT; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executestage_nine_x_order[index] = index;
      2. Executestage_nine_y_order[index] = index;
  3. FOR loopuint8_t index = DEMO_STAGE_NINE_COORDINATE_COUNT - 1U; index > 0U; --index

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst uint8_t other = (uint8_t)(esp_random() % (index + 1U));
      2. Executeconst uint8_t x = stage_nine_x_order[index];
      3. Executestage_nine_x_order[index] = stage_nine_x_order[other];
      4. Executestage_nine_x_order[other] = x;
      5. Executeconst uint8_t y = stage_nine_y_order[index];
      6. Executestage_nine_y_order[index] = stage_nine_y_order[other];
      7. Executestage_nine_y_order[other] = y;
  4. Executestage_nine_glyph_base = DEMO_STAGE_NINE_GLYPH_FIRST + (uint8_t)(esp_random() % DEMO_STAGE_NINE_GLYPH_COUNT);
  5. Executestage_nine_reflection_index = 0U;
  6. Executestage_nine_phase = SPARKLE_REFLECTION_DRAW;
  7. Executestage_nine_next_update_at = now_ms;
Read original function source
static void prepareStageNineReflection(uint32_t now_ms)
{
    memset(stage_nine_cells, 0, sizeof(stage_nine_cells));
    // Lines 150-190 initialise and independently shuffle A(0..12) and B(0..12).
    // Fisher-Yates retains that "use every coordinate once" property at our
    // ticker-safe 0..8 range.
    for (uint8_t index = 0U; index < DEMO_STAGE_NINE_COORDINATE_COUNT; ++index) {
        stage_nine_x_order[index] = index;
        stage_nine_y_order[index] = index;
    }
    for (uint8_t index = DEMO_STAGE_NINE_COORDINATE_COUNT - 1U; index > 0U; --index) {
        const uint8_t other = (uint8_t)(esp_random() % (index + 1U));
        const uint8_t x = stage_nine_x_order[index];
        stage_nine_x_order[index] = stage_nine_x_order[other];
        stage_nine_x_order[other] = x;
        const uint8_t y = stage_nine_y_order[index];
        stage_nine_y_order[index] = stage_nine_y_order[other];
        stage_nine_y_order[other] = y;
    }
    stage_nine_glyph_base = DEMO_STAGE_NINE_GLYPH_FIRST +
                            (uint8_t)(esp_random() % DEMO_STAGE_NINE_GLYPH_COUNT);
    stage_nine_reflection_index = 0U;
    stage_nine_phase = SPARKLE_REFLECTION_DRAW;
    stage_nine_next_update_at = now_ms;
}
addStageNineReflectionPoint() · source line 90
static void addStageNineReflectionPoint(uint8_t index)

Calls into this documentation:

  1. Executeconst uint8_t x_offset = stage_nine_x_order[index / DEMO_STAGE_NINE_COORDINATE_COUNT];
  2. Executeconst uint8_t y_offset = stage_nine_y_order[index % DEMO_STAGE_NINE_COORDINATE_COUNT];
  3. Executeconst uint8_t glyph = stage_nine_glyph_base + x_offset + y_offset;
  4. Executeconst int x = (int)x_offset;
  5. Executeconst int y = (int)y_offset;
  6. Executeconst int centre_x = DEMO_STAGE_NINE_CENTRE_COLUMN;
  7. Executeconst int centre_y = DEMO_STAGE_NINE_CENTRE_ROW;
  8. ExecutesetStageNineCell(centre_x + x, centre_y + y, glyph);
  9. ExecutesetStageNineCell(centre_x + x, centre_y - y, glyph);
  10. ExecutesetStageNineCell(centre_x - x, centre_y - y, glyph);
  11. ExecutesetStageNineCell(centre_x - x, centre_y + y, glyph);
  12. ExecutesetStageNineCell(centre_x + y, centre_y + x, glyph);
  13. ExecutesetStageNineCell(centre_x + y, centre_y - x, glyph);
  14. ExecutesetStageNineCell(centre_x - y, centre_y - x, glyph);
  15. ExecutesetStageNineCell(centre_x - y, centre_y + x, glyph);
Read original function source
static void addStageNineReflectionPoint(uint8_t index)
{
    const uint8_t x_offset = stage_nine_x_order[index / DEMO_STAGE_NINE_COORDINATE_COUNT];
    const uint8_t y_offset = stage_nine_y_order[index % DEMO_STAGE_NINE_COORDINATE_COUNT];
    // BASIC line 210 uses C=R+U+T. The base comes from 66..102, while the
    // shuffled offsets extend the final PET graphics character as high as 118.
    const uint8_t glyph = stage_nine_glyph_base + x_offset + y_offset;
    const int x = (int)x_offset;
    const int y = (int)y_offset;
    const int centre_x = DEMO_STAGE_NINE_CENTRE_COLUMN;
    const int centre_y = DEMO_STAGE_NINE_CENTRE_ROW;

    // BASIC lines 240-310: eight reflected versions of the one shuffled point.
    setStageNineCell(centre_x + x, centre_y + y, glyph);
    setStageNineCell(centre_x + x, centre_y - y, glyph);
    setStageNineCell(centre_x - x, centre_y - y, glyph);
    setStageNineCell(centre_x - x, centre_y + y, glyph);
    setStageNineCell(centre_x + y, centre_y + x, glyph);
    setStageNineCell(centre_x + y, centre_y - x, glyph);
    setStageNineCell(centre_x - y, centre_y - x, glyph);
    setStageNineCell(centre_x - y, centre_y + x, glyph);
}
prepareStageNineSparkleField() · source line 113
static void prepareStageNineSparkleField(uint32_t now_ms)
  1. Executememset(stage_nine_cells, 0, sizeof(stage_nine_cells));
  2. Executestage_nine_sparkle_run_count = 0U;
  3. Executestage_nine_sparkle_run_target = DEMO_STAGE_NINE_SPARKLE_RUN_MIN + (uint8_t)(esp_random() % DEMO_STAGE_NINE_SPARKLE_RUN_RANGE);
  4. Executestage_nine_phase = SPARKLE_FIELD_DRAW;
  5. Executestage_nine_next_update_at = now_ms;
Read original function source
static void prepareStageNineSparkleField(uint32_t now_ms)
{
    memset(stage_nine_cells, 0, sizeof(stage_nine_cells));
    stage_nine_sparkle_run_count = 0U;
    stage_nine_sparkle_run_target = DEMO_STAGE_NINE_SPARKLE_RUN_MIN +
        (uint8_t)(esp_random() % DEMO_STAGE_NINE_SPARKLE_RUN_RANGE);
    stage_nine_phase = SPARKLE_FIELD_DRAW;
    stage_nine_next_update_at = now_ms;
}
addStageNineSparkleRun() · source line 123
static void addStageNineSparkleRun(void)
  1. Executeconst uint8_t glyph = 65U + (uint8_t)(esp_random() % 63U);
  2. Executeconst size_t start = (esp_random() % 100U) > 80U ? 1U : 0U;
  3. Executeconst size_t stride = 1U + (size_t)(esp_random() % DEMO_STAGE_NINE_SPARKLE_STEP_MAX);
  4. Executeconst size_t cell_count = sizeof(stage_nine_cells) / sizeof(stage_nine_cells[0]);
  5. FOR loopsize_t cell = start; cell < cell_count; cell += stride

    Repeat body while the loop condition permits; then continue below.

      1. Executestage_nine_cells[cell] = glyph;
Read original function source
static void addStageNineSparkleRun(void)
{
    // Port BASIC lines 420-460 safely: begin in the first or second display
    // cell, choose a random memory stride (1..22), and repeat a PET graphics
    // character along that stride without writing beyond this stage's buffer.
    const uint8_t glyph = 65U + (uint8_t)(esp_random() % 63U);
    const size_t start = (esp_random() % 100U) > 80U ? 1U : 0U;
    const size_t stride = 1U + (size_t)(esp_random() % DEMO_STAGE_NINE_SPARKLE_STEP_MAX);
    const size_t cell_count = sizeof(stage_nine_cells) / sizeof(stage_nine_cells[0]);
    for (size_t cell = start; cell < cell_count; cell += stride) {
        stage_nine_cells[cell] = glyph;
    }
}
demoStageSparkleStart() · source line 137
void demoStageSparkleStart(uint32_t now_ms)

Calls into this documentation:

  1. ExecuteprepareStageNineReflection(now_ms);
Read original function source
void demoStageSparkleStart(uint32_t now_ms)
{
    prepareStageNineReflection(now_ms);
}
demoStageSparkleUpdate() · source line 142
void demoStageSparkleUpdate(uint32_t now_ms)

Calls into this documentation:

  1. Decision(int32_t)(now_ms - stage_nine_next_update_at) < 0
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Select a casestage_nine_phase

    Enter matching case; follow source fall-through until a break, return or end.

      1. Case / labelcase SPARKLE_REFLECTION_DRAW:
      2. ExecuteaddStageNineReflectionPoint(stage_nine_reflection_index++);
      3. Executestage_nine_next_update_at += DEMO_STAGE_NINE_REFLECTION_STEP_MS;
      4. Decisionstage_nine_reflection_index >= DEMO_STAGE_NINE_COORDINATE_COUNT * DEMO_STAGE_NINE_COORDINATE_COUNT
        True
          1. Executestage_nine_phase = SPARKLE_REFLECTION_HOLD;
          2. Executestage_nine_hold_until = now_ms + DEMO_STAGE_NINE_REFLECTION_HOLD_MS;
          3. Executestage_nine_next_update_at = stage_nine_hold_until;
        False

        Continue after this decision.

      5. Exit enclosing loop or switchbreak;
      6. Case / labelcase SPARKLE_REFLECTION_HOLD:
      7. ExecuteprepareStageNineSparkleField(now_ms);
      8. Exit enclosing loop or switchbreak;
      9. Case / labelcase SPARKLE_FIELD_DRAW:
      10. ExecuteaddStageNineSparkleRun();
      11. Execute++stage_nine_sparkle_run_count;
      12. Executestage_nine_next_update_at += DEMO_STAGE_NINE_SPARKLE_RUN_MS;
      13. Decisionstage_nine_sparkle_run_count >= stage_nine_sparkle_run_target
        True
          1. Executestage_nine_phase = SPARKLE_FIELD_HOLD;
          2. Executestage_nine_hold_until = now_ms + DEMO_STAGE_NINE_SPARKLE_HOLD_MS;
          3. Executestage_nine_next_update_at = stage_nine_hold_until;
        False

        Continue after this decision.

      14. Exit enclosing loop or switchbreak;
      15. Case / labelcase SPARKLE_FIELD_HOLD:
      16. ExecuteprepareStageNineReflection(now_ms);
      17. Exit enclosing loop or switchbreak;
Read original function source
void demoStageSparkleUpdate(uint32_t now_ms)
{
    if ((int32_t)(now_ms - stage_nine_next_update_at) < 0) return;

    switch (stage_nine_phase) {
        case SPARKLE_REFLECTION_DRAW:
            addStageNineReflectionPoint(stage_nine_reflection_index++);
            stage_nine_next_update_at += DEMO_STAGE_NINE_REFLECTION_STEP_MS;
            if (stage_nine_reflection_index >=
                DEMO_STAGE_NINE_COORDINATE_COUNT * DEMO_STAGE_NINE_COORDINATE_COUNT) {
                stage_nine_phase = SPARKLE_REFLECTION_HOLD;
                stage_nine_hold_until = now_ms + DEMO_STAGE_NINE_REFLECTION_HOLD_MS;
                stage_nine_next_update_at = stage_nine_hold_until;
            }
            break;
        case SPARKLE_REFLECTION_HOLD:
            prepareStageNineSparkleField(now_ms);
            break;
        case SPARKLE_FIELD_DRAW:
            addStageNineSparkleRun();
            ++stage_nine_sparkle_run_count;
            stage_nine_next_update_at += DEMO_STAGE_NINE_SPARKLE_RUN_MS;
            if (stage_nine_sparkle_run_count >= stage_nine_sparkle_run_target) {
                stage_nine_phase = SPARKLE_FIELD_HOLD;
                stage_nine_hold_until = now_ms + DEMO_STAGE_NINE_SPARKLE_HOLD_MS;
                stage_nine_next_update_at = stage_nine_hold_until;
            }
            break;
        case SPARKLE_FIELD_HOLD:
            prepareStageNineReflection(now_ms);
            break;
    }
}
demoStageSparkleDraw() · source line 176
void demoStageSparkleDraw(uint16_t *frame)

Calls into this documentation:

  1. FOR loopint row = 0; row < DEMO_STAGE_NINE_ROWS; ++row

    Repeat body while the loop condition permits; then continue below.

      1. FOR loopint column = 0; column < DEMO_STAGE_NINE_COLUMNS; ++column

        Repeat body while the loop condition permits; then continue below.

          1. Executeconst uint8_t glyph = stage_nine_cells[(size_t)row * DEMO_STAGE_NINE_COLUMNS + (size_t)column];
          2. Decisionglyph == 0U
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          3. ExecutedrawPet2001Char(frame, column * (int)PET2001_FONT_GLYPH_WIDTH, row * (int)PET2001_FONT_GLYPH_HEIGHT, glyph, DEMO_STAGE_NINE_FOREGROUND, COLOR_BLACK, 1);
Read original function source
void demoStageSparkleDraw(uint16_t *frame)
{
    // SPARKLE uses the same ticker-safe 40 x 19 PET cell window as KALEIDO.
    // The lower two rows remain free for drawScrollText() at scale two.
    for (int row = 0; row < DEMO_STAGE_NINE_ROWS; ++row) {
        for (int column = 0; column < DEMO_STAGE_NINE_COLUMNS; ++column) {
            const uint8_t glyph = stage_nine_cells[(size_t)row *
                                                    DEMO_STAGE_NINE_COLUMNS +
                                                    (size_t)column];
            if (glyph == 0U) continue;
            drawPet2001Char(frame, column * (int)PET2001_FONT_GLYPH_WIDTH,
                            row * (int)PET2001_FONT_GLYPH_HEIGHT, glyph,
                            DEMO_STAGE_NINE_FOREGROUND, COLOR_BLACK, 1);
        }
    }
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "demo_stage_sparkle.h"

#include "PET2001_font.h"
#include "esp_attr.h"
#include "esp_random.h"
#include "game_graphics.h"

#include <stddef.h>
#include <string.h>

// Segment 9 -- SPARKLE, an eight-way-symmetrical PET pattern that shares
// KALEIDO's ticker-safe upper 40 x 19 PET-cell area.
// The original uses 13 shuffled coordinates around row 12 on a 25-row PET;
// nine coordinates around row 9 preserve its eight-way symmetry here.
#define DEMO_STAGE_NINE_COLUMNS 40
#define DEMO_STAGE_NINE_ROWS 19
#define DEMO_STAGE_NINE_CENTRE_COLUMN 19
#define DEMO_STAGE_NINE_CENTRE_ROW 9
#define DEMO_STAGE_NINE_COORDINATE_COUNT 9U
// This selects the BASIC line-140 base character (66..102). The shuffled X/Y
// offsets are then added, so keep FIRST + COUNT + 16 at or below PET code 127.
#define DEMO_STAGE_NINE_GLYPH_FIRST 66U
#define DEMO_STAGE_NINE_GLYPH_COUNT 37U
#define DEMO_STAGE_NINE_REFLECTION_STEP_MS 28U
#define DEMO_STAGE_NINE_REFLECTION_HOLD_MS 2400U
// BASIC lines 410-470 make 10 to 30 random-stride sparkle runs. One run is
// added at this interval; completed fields use this short viewing pause.
#define DEMO_STAGE_NINE_SPARKLE_RUN_MIN 10U
#define DEMO_STAGE_NINE_SPARKLE_RUN_RANGE 21U
#define DEMO_STAGE_NINE_SPARKLE_STEP_MAX 22U
#define DEMO_STAGE_NINE_SPARKLE_RUN_MS 95U
#define DEMO_STAGE_NINE_SPARKLE_HOLD_MS 3000U
#define DEMO_STAGE_NINE_FOREGROUND COLOR_WHITE

typedef enum {
    SPARKLE_REFLECTION_DRAW,
    SPARKLE_REFLECTION_HOLD,
    SPARKLE_FIELD_DRAW,
    SPARKLE_FIELD_HOLD,
} sparkle_phase_t;

// SPARKLE owns a separate PSRAM cell map and shuffled coordinate orders. The
// small timing/count variables remain internal state because they are updated
// every frame; only the display-sized working data needs external RAM.
EXT_RAM_BSS_ATTR static uint8_t
    stage_nine_cells[DEMO_STAGE_NINE_COLUMNS * DEMO_STAGE_NINE_ROWS];
EXT_RAM_BSS_ATTR static uint8_t stage_nine_x_order[DEMO_STAGE_NINE_COORDINATE_COUNT];
EXT_RAM_BSS_ATTR static uint8_t stage_nine_y_order[DEMO_STAGE_NINE_COORDINATE_COUNT];
static sparkle_phase_t stage_nine_phase;
static uint8_t stage_nine_reflection_index;
static uint8_t stage_nine_sparkle_run_count;
static uint8_t stage_nine_sparkle_run_target;
static uint8_t stage_nine_glyph_base;
static uint32_t stage_nine_next_update_at;
static uint32_t stage_nine_hold_until;

static void setStageNineCell(int column, int row, uint8_t glyph)
{
    if (column < 0 || column >= DEMO_STAGE_NINE_COLUMNS ||
        row < 0 || row >= DEMO_STAGE_NINE_ROWS) return;
    stage_nine_cells[(size_t)row * DEMO_STAGE_NINE_COLUMNS + (size_t)column] = glyph;
}

static void prepareStageNineReflection(uint32_t now_ms)
{
    memset(stage_nine_cells, 0, sizeof(stage_nine_cells));
    // Lines 150-190 initialise and independently shuffle A(0..12) and B(0..12).
    // Fisher-Yates retains that "use every coordinate once" property at our
    // ticker-safe 0..8 range.
    for (uint8_t index = 0U; index < DEMO_STAGE_NINE_COORDINATE_COUNT; ++index) {
        stage_nine_x_order[index] = index;
        stage_nine_y_order[index] = index;
    }
    for (uint8_t index = DEMO_STAGE_NINE_COORDINATE_COUNT - 1U; index > 0U; --index) {
        const uint8_t other = (uint8_t)(esp_random() % (index + 1U));
        const uint8_t x = stage_nine_x_order[index];
        stage_nine_x_order[index] = stage_nine_x_order[other];
        stage_nine_x_order[other] = x;
        const uint8_t y = stage_nine_y_order[index];
        stage_nine_y_order[index] = stage_nine_y_order[other];
        stage_nine_y_order[other] = y;
    }
    stage_nine_glyph_base = DEMO_STAGE_NINE_GLYPH_FIRST +
                            (uint8_t)(esp_random() % DEMO_STAGE_NINE_GLYPH_COUNT);
    stage_nine_reflection_index = 0U;
    stage_nine_phase = SPARKLE_REFLECTION_DRAW;
    stage_nine_next_update_at = now_ms;
}

static void addStageNineReflectionPoint(uint8_t index)
{
    const uint8_t x_offset = stage_nine_x_order[index / DEMO_STAGE_NINE_COORDINATE_COUNT];
    const uint8_t y_offset = stage_nine_y_order[index % DEMO_STAGE_NINE_COORDINATE_COUNT];
    // BASIC line 210 uses C=R+U+T. The base comes from 66..102, while the
    // shuffled offsets extend the final PET graphics character as high as 118.
    const uint8_t glyph = stage_nine_glyph_base + x_offset + y_offset;
    const int x = (int)x_offset;
    const int y = (int)y_offset;
    const int centre_x = DEMO_STAGE_NINE_CENTRE_COLUMN;
    const int centre_y = DEMO_STAGE_NINE_CENTRE_ROW;

    // BASIC lines 240-310: eight reflected versions of the one shuffled point.
    setStageNineCell(centre_x + x, centre_y + y, glyph);
    setStageNineCell(centre_x + x, centre_y - y, glyph);
    setStageNineCell(centre_x - x, centre_y - y, glyph);
    setStageNineCell(centre_x - x, centre_y + y, glyph);
    setStageNineCell(centre_x + y, centre_y + x, glyph);
    setStageNineCell(centre_x + y, centre_y - x, glyph);
    setStageNineCell(centre_x - y, centre_y - x, glyph);
    setStageNineCell(centre_x - y, centre_y + x, glyph);
}

static void prepareStageNineSparkleField(uint32_t now_ms)
{
    memset(stage_nine_cells, 0, sizeof(stage_nine_cells));
    stage_nine_sparkle_run_count = 0U;
    stage_nine_sparkle_run_target = DEMO_STAGE_NINE_SPARKLE_RUN_MIN +
        (uint8_t)(esp_random() % DEMO_STAGE_NINE_SPARKLE_RUN_RANGE);
    stage_nine_phase = SPARKLE_FIELD_DRAW;
    stage_nine_next_update_at = now_ms;
}

static void addStageNineSparkleRun(void)
{
    // Port BASIC lines 420-460 safely: begin in the first or second display
    // cell, choose a random memory stride (1..22), and repeat a PET graphics
    // character along that stride without writing beyond this stage's buffer.
    const uint8_t glyph = 65U + (uint8_t)(esp_random() % 63U);
    const size_t start = (esp_random() % 100U) > 80U ? 1U : 0U;
    const size_t stride = 1U + (size_t)(esp_random() % DEMO_STAGE_NINE_SPARKLE_STEP_MAX);
    const size_t cell_count = sizeof(stage_nine_cells) / sizeof(stage_nine_cells[0]);
    for (size_t cell = start; cell < cell_count; cell += stride) {
        stage_nine_cells[cell] = glyph;
    }
}

void demoStageSparkleStart(uint32_t now_ms)
{
    prepareStageNineReflection(now_ms);
}

void demoStageSparkleUpdate(uint32_t now_ms)
{
    if ((int32_t)(now_ms - stage_nine_next_update_at) < 0) return;

    switch (stage_nine_phase) {
        case SPARKLE_REFLECTION_DRAW:
            addStageNineReflectionPoint(stage_nine_reflection_index++);
            stage_nine_next_update_at += DEMO_STAGE_NINE_REFLECTION_STEP_MS;
            if (stage_nine_reflection_index >=
                DEMO_STAGE_NINE_COORDINATE_COUNT * DEMO_STAGE_NINE_COORDINATE_COUNT) {
                stage_nine_phase = SPARKLE_REFLECTION_HOLD;
                stage_nine_hold_until = now_ms + DEMO_STAGE_NINE_REFLECTION_HOLD_MS;
                stage_nine_next_update_at = stage_nine_hold_until;
            }
            break;
        case SPARKLE_REFLECTION_HOLD:
            prepareStageNineSparkleField(now_ms);
            break;
        case SPARKLE_FIELD_DRAW:
            addStageNineSparkleRun();
            ++stage_nine_sparkle_run_count;
            stage_nine_next_update_at += DEMO_STAGE_NINE_SPARKLE_RUN_MS;
            if (stage_nine_sparkle_run_count >= stage_nine_sparkle_run_target) {
                stage_nine_phase = SPARKLE_FIELD_HOLD;
                stage_nine_hold_until = now_ms + DEMO_STAGE_NINE_SPARKLE_HOLD_MS;
                stage_nine_next_update_at = stage_nine_hold_until;
            }
            break;
        case SPARKLE_FIELD_HOLD:
            prepareStageNineReflection(now_ms);
            break;
    }
}

void demoStageSparkleDraw(uint16_t *frame)
{
    // SPARKLE uses the same ticker-safe 40 x 19 PET cell window as KALEIDO.
    // The lower two rows remain free for drawScrollText() at scale two.
    for (int row = 0; row < DEMO_STAGE_NINE_ROWS; ++row) {
        for (int column = 0; column < DEMO_STAGE_NINE_COLUMNS; ++column) {
            const uint8_t glyph = stage_nine_cells[(size_t)row *
                                                    DEMO_STAGE_NINE_COLUMNS +
                                                    (size_t)column];
            if (glyph == 0U) continue;
            drawPet2001Char(frame, column * (int)PET2001_FONT_GLYPH_WIDTH,
                            row * (int)PET2001_FONT_GLYPH_HEIGHT, glyph,
                            DEMO_STAGE_NINE_FOREGROUND, COLOR_BLACK, 1);
        }
    }
}