Screen Transition

Source: src/screen_transition.c · 418 lines · 30 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

ensureSnapshot() · source line 18
static bool ensureSnapshot(screen_transition_t *transition)
  1. Decisiontransition->snapshot != NULL
    True
    1. Exit this functionreturn true;
    False

    Continue after this decision.

  2. Executetransition->snapshot = heap_caps_malloc(SCREEN_TRANSITION_FRAME_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  3. Exit this functionreturn transition->snapshot != NULL;
Read original function source
static bool ensureSnapshot(screen_transition_t *transition)
{
    // Allocate on first use, then retain it for every later transition in this screen.
    if (transition->snapshot != NULL) return true;
    transition->snapshot = heap_caps_malloc(SCREEN_TRANSITION_FRAME_BYTES,
                                            MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    return transition->snapshot != NULL;
}
ensureScrollIncomingSnapshot() · source line 27
static bool ensureScrollIncomingSnapshot(screen_transition_t *transition)
  1. Decisiontransition->scroll_incoming_snapshot != NULL
    True
    1. Exit this functionreturn true;
    False

    Continue after this decision.

  2. Executetransition->scroll_incoming_snapshot = heap_caps_malloc( SCREEN_TRANSITION_FRAME_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  3. Exit this functionreturn transition->scroll_incoming_snapshot != NULL;
Read original function source
static bool ensureScrollIncomingSnapshot(screen_transition_t *transition)
{
    if (transition->scroll_incoming_snapshot != NULL) return true;
    transition->scroll_incoming_snapshot = heap_caps_malloc(
        SCREEN_TRANSITION_FRAME_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    return transition->scroll_incoming_snapshot != NULL;
}
screenTransitionStart() · source line 35
bool screenTransitionStart(screen_transition_t *transition, const uint16_t *frame,
                           uint32_t now_ms, uint32_t freeze_ms, uint32_t zoom_ms)

Calls into this documentation:

  1. Decisiontransition == NULL || frame == NULL || zoom_ms == 0U
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Decision!ensureSnapshot(transition)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Executememcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
  4. Executetransition->started_at_ms = now_ms;
  5. Executetransition->freeze_ms = freeze_ms;
  6. Executetransition->zoom_ms = zoom_ms;
  7. Executetransition->active = true;
  8. Executetransition->complete = false;
  9. Executetransition->zoom_in = false;
  10. Exit this functionreturn true;
Read original function source
bool screenTransitionStart(screen_transition_t *transition, const uint16_t *frame,
                           uint32_t now_ms, uint32_t freeze_ms, uint32_t zoom_ms)
{
    if (transition == NULL || frame == NULL || zoom_ms == 0U) return false;
    if (!ensureSnapshot(transition)) return false;
    // Freeze exactly the completed caller frame before the caller draws another scene.
    memcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    transition->started_at_ms = now_ms;
    transition->freeze_ms = freeze_ms;
    transition->zoom_ms = zoom_ms;
    transition->active = true;
    transition->complete = false;
    transition->zoom_in = false;
    return true;
}
screenTransitionZoomInStart() · source line 51
bool screenTransitionZoomInStart(screen_transition_t *transition, const uint16_t *frame,
                                 uint32_t now_ms, uint32_t zoom_ms)

Calls into this documentation:

  1. Decision!screenTransitionStart(transition, frame, now_ms, 0U, zoom_ms)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executetransition->zoom_in = true;
  3. Exit this functionreturn true;
Read original function source
bool screenTransitionZoomInStart(screen_transition_t *transition, const uint16_t *frame,
                                 uint32_t now_ms, uint32_t zoom_ms)
{
    if (!screenTransitionStart(transition, frame, now_ms, 0U, zoom_ms)) return false;
    transition->zoom_in = true;
    return true;
}
screenTransitionDraw() · source line 59
void screenTransitionDraw(screen_transition_t *transition, uint16_t *frame,
                          uint32_t now_ms)

Calls into this documentation:

  1. Decisiontransition == NULL || frame == NULL || !transition->active || transition->snapshot == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst uint32_t elapsed_ms = now_ms - transition->started_at_ms;
  3. Decision!transition->zoom_in && elapsed_ms < transition->freeze_ms
    True
      1. Executememcpy(frame, transition->snapshot, SCREEN_TRANSITION_FRAME_BYTES);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  4. Executeconst uint32_t zoom_elapsed_ms = transition->zoom_in ? elapsed_ms : elapsed_ms - transition->freeze_ms;
  5. Executefloat progress = (float)zoom_elapsed_ms / (float)transition->zoom_ms;
  6. Decisionprogress > 1.0f
    True
    1. Executeprogress = 1.0f;
    False

    Continue after this decision.

  7. Executeconst float scale = transition->zoom_in ? progress * progress : 1.0f - progress * progress;
  8. Executeint width = (int)lroundf((float)LCD_WIDTH * scale);
  9. Executeint height = (int)lroundf((float)LCD_HEIGHT * scale);
  10. Decisionwidth < 1
    True
    1. Executewidth = 1;
    False

    Continue after this decision.

  11. Decisionheight < 1
    True
    1. Executeheight = 1;
    False

    Continue after this decision.

  12. Executeclear(frame, COLOR_BLACK);
  13. Executedraw_rgb565_scaled(frame, (LCD_WIDTH - width) / 2, (LCD_HEIGHT - height) / 2, transition->snapshot, NULL, LCD_WIDTH, LCD_HEIGHT, width, height);
  14. Decisionprogress >= 1.0f
    True
    1. Executetransition->complete = true;
    False

    Continue after this decision.

Read original function source
void screenTransitionDraw(screen_transition_t *transition, uint16_t *frame,
                          uint32_t now_ms)
{
    if (transition == NULL || frame == NULL || !transition->active ||
        transition->snapshot == NULL) return;
    const uint32_t elapsed_ms = now_ms - transition->started_at_ms;
    if (!transition->zoom_in && elapsed_ms < transition->freeze_ms) {
        // The initial phase is a literal frozen copy, not a re-rendered scene.
        memcpy(frame, transition->snapshot, SCREEN_TRANSITION_FRAME_BYTES);
        return;
    }

    const uint32_t zoom_elapsed_ms = transition->zoom_in
        ? elapsed_ms : elapsed_ms - transition->freeze_ms;
    float progress = (float)zoom_elapsed_ms / (float)transition->zoom_ms;
    if (progress > 1.0f) progress = 1.0f;
    // Square progress eases into the outgoing dot and eases away from the incoming dot.
    const float scale = transition->zoom_in ? progress * progress : 1.0f - progress * progress;
    int width = (int)lroundf((float)LCD_WIDTH * scale);
    int height = (int)lroundf((float)LCD_HEIGHT * scale);
    if (width < 1) width = 1;
    if (height < 1) height = 1;
    clear(frame, COLOR_BLACK);
    draw_rgb565_scaled(frame, (LCD_WIDTH - width) / 2, (LCD_HEIGHT - height) / 2,
                       transition->snapshot, NULL, LCD_WIDTH, LCD_HEIGHT, width, height);
    if (progress >= 1.0f) transition->complete = true;
}
screenTransitionActive() · source line 87
bool screenTransitionActive(const screen_transition_t *transition)
  1. Exit this functionreturn transition != NULL && transition->active;
Read original function source
bool screenTransitionActive(const screen_transition_t *transition)
{
    return transition != NULL && transition->active;
}
screenTransitionComplete() · source line 92
bool screenTransitionComplete(const screen_transition_t *transition)
  1. Exit this functionreturn transition != NULL && transition->complete;
Read original function source
bool screenTransitionComplete(const screen_transition_t *transition)
{
    return transition != NULL && transition->complete;
}
screenTransitionReset() · source line 97
void screenTransitionReset(screen_transition_t *transition)
  1. Decisiontransition == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executetransition->active = false;
  3. Executetransition->complete = false;
  4. Executetransition->zoom_in = false;
Read original function source
void screenTransitionReset(screen_transition_t *transition)
{
    if (transition == NULL) return;
    transition->active = false;
    transition->complete = false;
    transition->zoom_in = false;
}
screenTransitionDeinitialize() · source line 105
void screenTransitionDeinitialize(screen_transition_t *transition)
  1. Decisiontransition == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeheap_caps_free(transition->snapshot);
  3. Executeheap_caps_free(transition->reveal_block_order);
  4. Executeheap_caps_free(transition->scroll_incoming_snapshot);
  5. Execute*transition = (screen_transition_t){0};
Read original function source
void screenTransitionDeinitialize(screen_transition_t *transition)
{
    if (transition == NULL) return;
    // Both allocations are owned by this reusable effect and live only while its caller is open.
    heap_caps_free(transition->snapshot);
    heap_caps_free(transition->reveal_block_order);
    heap_caps_free(transition->scroll_incoming_snapshot);
    *transition = (screen_transition_t){0};
}
screenRevealStart() · source line 115
bool screenRevealStart(screen_transition_t *transition, const uint16_t *frame,
                       uint32_t now_ms, uint32_t reveal_ms, unsigned reveal_percent)

Calls into this documentation:

  1. Decisiontransition == NULL || frame == NULL || reveal_ms == 0U || reveal_percent == 0U
    True
      1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Decision!ensureSnapshot(transition)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Decisiontransition->reveal_block_order == NULL
    True
      1. Executetransition->reveal_block_order = heap_caps_malloc( SCREEN_REVEAL_BLOCK_COUNT * sizeof(transition->reveal_block_order[0]), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
      2. Decisiontransition->reveal_block_order == NULL
        True
        1. Exit this functionreturn false;
        False

        Continue after this decision.

    False

    Continue after this decision.

  4. Executememcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
  5. FOR loopuint16_t block = 0U; block < SCREEN_REVEAL_BLOCK_COUNT; ++block

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

      1. Executetransition->reveal_block_order[block] = block;
  6. FOR loopuint16_t block = SCREEN_REVEAL_BLOCK_COUNT - 1U; block > 0U; --block

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

      1. Executeconst uint16_t swap_with = (uint16_t)(esp_random() % (block + 1U));
      2. Executeconst uint16_t saved = transition->reveal_block_order[block];
      3. Executetransition->reveal_block_order[block] = transition->reveal_block_order[swap_with];
      4. Executetransition->reveal_block_order[swap_with] = saved;
  7. Decisionreveal_percent > 100U
    True
    1. Executereveal_percent = 100U;
    False

    Continue after this decision.

  8. Executetransition->reveal_target_count = (uint16_t)((SCREEN_REVEAL_BLOCK_COUNT * reveal_percent + 99U) / 100U);
  9. Executetransition->reveal_block_count = 0U;
  10. Executetransition->reveal_started_at_ms = now_ms;
  11. Executetransition->reveal_ms = reveal_ms;
  12. Executetransition->reveal_active = true;
  13. Executetransition->reveal_complete = false;
  14. Exit this functionreturn true;
Read original function source
bool screenRevealStart(screen_transition_t *transition, const uint16_t *frame,
                       uint32_t now_ms, uint32_t reveal_ms, unsigned reveal_percent)
{
    if (transition == NULL || frame == NULL || reveal_ms == 0U || reveal_percent == 0U) {
        return false;
    }
    if (!ensureSnapshot(transition)) return false;
    if (transition->reveal_block_order == NULL) {
        transition->reveal_block_order = heap_caps_malloc(
            SCREEN_REVEAL_BLOCK_COUNT * sizeof(transition->reveal_block_order[0]),
            MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
        if (transition->reveal_block_order == NULL) return false;
    }
    // Capture the already rendered destination stage before replacing it with black blocks.
    memcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    // Build a complete non-repeating block order, then shuffle it for this reveal.
    for (uint16_t block = 0U; block < SCREEN_REVEAL_BLOCK_COUNT; ++block) {
        transition->reveal_block_order[block] = block;
    }
    for (uint16_t block = SCREEN_REVEAL_BLOCK_COUNT - 1U; block > 0U; --block) {
        const uint16_t swap_with = (uint16_t)(esp_random() % (block + 1U));
        const uint16_t saved = transition->reveal_block_order[block];
        transition->reveal_block_order[block] = transition->reveal_block_order[swap_with];
        transition->reveal_block_order[swap_with] = saved;
    }
    if (reveal_percent > 100U) reveal_percent = 100U;
    // Round up so the requested percentage is never under-represented by a block.
    transition->reveal_target_count = (uint16_t)((SCREEN_REVEAL_BLOCK_COUNT * reveal_percent +
                                                  99U) / 100U);
    transition->reveal_block_count = 0U;
    transition->reveal_started_at_ms = now_ms;
    transition->reveal_ms = reveal_ms;
    transition->reveal_active = true;
    transition->reveal_complete = false;
    return true;
}
drawRevealBlock() · source line 152
static void drawRevealBlock(uint16_t *frame, const uint16_t *snapshot, uint16_t block)
  1. Executeconst int x = (int)(block % SCREEN_REVEAL_COLUMNS) * SCREEN_REVEAL_BLOCK_PIXELS;
  2. Executeconst int y = (int)(block / SCREEN_REVEAL_COLUMNS) * SCREEN_REVEAL_BLOCK_PIXELS;
  3. Executeint width = SCREEN_REVEAL_BLOCK_PIXELS;
  4. Executeint height = SCREEN_REVEAL_BLOCK_PIXELS;
  5. Decisionx + width > LCD_WIDTH
    True
    1. Executewidth = LCD_WIDTH - x;
    False

    Continue after this decision.

  6. Decisiony + height > LCD_HEIGHT
    True
    1. Executeheight = LCD_HEIGHT - y;
    False

    Continue after this decision.

  7. FOR loopint row = 0; row < height; ++row

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

      1. Executememcpy(&frame[(size_t)(y + row) * LCD_WIDTH + x], &snapshot[(size_t)(y + row) * LCD_WIDTH + x], (size_t)width * sizeof(frame[0]));
Read original function source
static void drawRevealBlock(uint16_t *frame, const uint16_t *snapshot, uint16_t block)
{
    // Copy an unscaled 8 x 8 cell; clipped edge cells handle display sizes not divisible by eight.
    const int x = (int)(block % SCREEN_REVEAL_COLUMNS) * SCREEN_REVEAL_BLOCK_PIXELS;
    const int y = (int)(block / SCREEN_REVEAL_COLUMNS) * SCREEN_REVEAL_BLOCK_PIXELS;
    int width = SCREEN_REVEAL_BLOCK_PIXELS;
    int height = SCREEN_REVEAL_BLOCK_PIXELS;
    if (x + width > LCD_WIDTH) width = LCD_WIDTH - x;
    if (y + height > LCD_HEIGHT) height = LCD_HEIGHT - y;
    for (int row = 0; row < height; ++row) {
        memcpy(&frame[(size_t)(y + row) * LCD_WIDTH + x],
               &snapshot[(size_t)(y + row) * LCD_WIDTH + x],
               (size_t)width * sizeof(frame[0]));
    }
}
screenRevealDraw() · source line 168
void screenRevealDraw(screen_transition_t *transition, uint16_t *frame, uint32_t now_ms)

Calls into this documentation:

  1. Decisiontransition == NULL || frame == NULL || !transition->reveal_active || transition->snapshot == NULL || transition->reveal_block_order == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst uint32_t elapsed_ms = now_ms - transition->reveal_started_at_ms;
  3. Executeuint16_t visible_blocks = transition->reveal_target_count;
  4. Decisionelapsed_ms < transition->reveal_ms
    True
      1. Executevisible_blocks = (uint16_t)(((uint64_t)transition->reveal_target_count * elapsed_ms) / transition->reveal_ms);
    False

    Continue after this decision.

  5. Executeclear(frame, COLOR_BLACK);
  6. FOR loopuint16_t index = 0U; index < visible_blocks; ++index

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

      1. ExecutedrawRevealBlock(frame, transition->snapshot, transition->reveal_block_order[index]);
  7. Executetransition->reveal_block_count = visible_blocks;
  8. Decisionelapsed_ms >= transition->reveal_ms
    True
      1. Executememcpy(frame, transition->snapshot, SCREEN_TRANSITION_FRAME_BYTES);
      2. Executetransition->reveal_complete = true;
    False

    Continue after this decision.

Read original function source
void screenRevealDraw(screen_transition_t *transition, uint16_t *frame, uint32_t now_ms)
{
    if (transition == NULL || frame == NULL || !transition->reveal_active ||
        transition->snapshot == NULL || transition->reveal_block_order == NULL) return;
    const uint32_t elapsed_ms = now_ms - transition->reveal_started_at_ms;
    uint16_t visible_blocks = transition->reveal_target_count;
    if (elapsed_ms < transition->reveal_ms) {
        // Reveal only the early portion of the shuffled order until the configured percentage.
        visible_blocks = (uint16_t)(((uint64_t)transition->reveal_target_count * elapsed_ms) /
                                    transition->reveal_ms);
    }
    clear(frame, COLOR_BLACK);
    for (uint16_t index = 0U; index < visible_blocks; ++index) {
        drawRevealBlock(frame, transition->snapshot, transition->reveal_block_order[index]);
    }
    transition->reveal_block_count = visible_blocks;
    if (elapsed_ms >= transition->reveal_ms) {
        // The partial pattern has done its job: restore the complete frozen destination frame.
        memcpy(frame, transition->snapshot, SCREEN_TRANSITION_FRAME_BYTES);
        transition->reveal_complete = true;
    }
}
screenRevealActive() · source line 191
bool screenRevealActive(const screen_transition_t *transition)
  1. Exit this functionreturn transition != NULL && transition->reveal_active;
Read original function source
bool screenRevealActive(const screen_transition_t *transition)
{
    return transition != NULL && transition->reveal_active;
}
screenRevealComplete() · source line 196
bool screenRevealComplete(const screen_transition_t *transition)
  1. Exit this functionreturn transition != NULL && transition->reveal_complete;
Read original function source
bool screenRevealComplete(const screen_transition_t *transition)
{
    return transition != NULL && transition->reveal_complete;
}
screenRevealReset() · source line 201
void screenRevealReset(screen_transition_t *transition)
  1. Decisiontransition == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executetransition->reveal_active = false;
  3. Executetransition->reveal_complete = false;
  4. Executetransition->reveal_block_count = 0U;
Read original function source
void screenRevealReset(screen_transition_t *transition)
{
    if (transition == NULL) return;
    transition->reveal_active = false;
    transition->reveal_complete = false;
    transition->reveal_block_count = 0U;
}
screenScrollStart() · source line 209
static bool screenScrollStart(screen_transition_t *transition, const uint16_t *frame,
                              uint32_t now_ms, screen_transition_scroll_direction_t direction,
                              uint32_t step_ms, uint16_t pixels_per_step, bool scroll_in)

Calls into this documentation:

  1. Decisiontransition == NULL || frame == NULL || step_ms == 0U || pixels_per_step == 0U
    True
      1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Decision!ensureSnapshot(transition)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Executememcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
  4. Executetransition->scroll_started_at_ms = now_ms;
  5. Executetransition->scroll_step_ms = step_ms;
  6. Executetransition->scroll_pixels_per_step = pixels_per_step;
  7. Executetransition->scroll_direction = direction;
  8. Executetransition->scroll_has_incoming = false;
  9. Executetransition->scroll_gap_pixels = 0U;
  10. Executetransition->scroll_active = true;
  11. Executetransition->scroll_complete = false;
  12. Executetransition->scroll_in = scroll_in;
  13. Exit this functionreturn true;
Read original function source
static bool screenScrollStart(screen_transition_t *transition, const uint16_t *frame,
                              uint32_t now_ms, screen_transition_scroll_direction_t direction,
                              uint32_t step_ms, uint16_t pixels_per_step, bool scroll_in)
{
    if (transition == NULL || frame == NULL || step_ms == 0U || pixels_per_step == 0U) {
        return false;
    }
    if (!ensureSnapshot(transition)) return false;
    // Capture the complete source before translating it so every step uses the same pixels.
    memcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    transition->scroll_started_at_ms = now_ms;
    transition->scroll_step_ms = step_ms;
    transition->scroll_pixels_per_step = pixels_per_step;
    transition->scroll_direction = direction;
    transition->scroll_has_incoming = false;
    transition->scroll_gap_pixels = 0U;
    transition->scroll_active = true;
    transition->scroll_complete = false;
    transition->scroll_in = scroll_in;
    return true;
}
screenScrollOutStart() · source line 231
bool screenScrollOutStart(screen_transition_t *transition, const uint16_t *frame,
                          uint32_t now_ms, screen_transition_scroll_direction_t direction,
                          uint32_t step_ms, uint16_t pixels_per_step)

Calls into this documentation:

  1. Exit this functionreturn screenScrollStart(transition, frame, now_ms, direction, step_ms, pixels_per_step, false);
Read original function source
bool screenScrollOutStart(screen_transition_t *transition, const uint16_t *frame,
                          uint32_t now_ms, screen_transition_scroll_direction_t direction,
                          uint32_t step_ms, uint16_t pixels_per_step)
{
    return screenScrollStart(transition, frame, now_ms, direction, step_ms,
                             pixels_per_step, false);
}
screenScrollInStart() · source line 239
bool screenScrollInStart(screen_transition_t *transition, const uint16_t *frame,
                         uint32_t now_ms, screen_transition_scroll_direction_t direction,
                         uint32_t step_ms, uint16_t pixels_per_step)

Calls into this documentation:

  1. Exit this functionreturn screenScrollStart(transition, frame, now_ms, direction, step_ms, pixels_per_step, true);
Read original function source
bool screenScrollInStart(screen_transition_t *transition, const uint16_t *frame,
                         uint32_t now_ms, screen_transition_scroll_direction_t direction,
                         uint32_t step_ms, uint16_t pixels_per_step)
{
    return screenScrollStart(transition, frame, now_ms, direction, step_ms,
                             pixels_per_step, true);
}
screenScrollSetIncoming() · source line 247
bool screenScrollSetIncoming(screen_transition_t *transition, const uint16_t *frame,
                             screen_transition_scroll_direction_t direction,
                             uint16_t gap_pixels)

Calls into this documentation:

  1. Decisiontransition == NULL || frame == NULL || !transition->scroll_active || transition->scroll_in || !ensureScrollIncomingSnapshot(transition)
    True
      1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executememcpy(transition->scroll_incoming_snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
  3. Executetransition->scroll_incoming_direction = direction;
  4. Executetransition->scroll_gap_pixels = gap_pixels;
  5. Executetransition->scroll_has_incoming = true;
  6. Exit this functionreturn true;
Read original function source
bool screenScrollSetIncoming(screen_transition_t *transition, const uint16_t *frame,
                             screen_transition_scroll_direction_t direction,
                             uint16_t gap_pixels)
{
    if (transition == NULL || frame == NULL || !transition->scroll_active ||
        transition->scroll_in || !ensureScrollIncomingSnapshot(transition)) {
        return false;
    }
    // Preserve the fully rendered destination so it can travel beside the outgoing frame.
    memcpy(transition->scroll_incoming_snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    transition->scroll_incoming_direction = direction;
    transition->scroll_gap_pixels = gap_pixels;
    transition->scroll_has_incoming = true;
    return true;
}
screenScrollAxisPixels() · source line 265
static uint32_t screenScrollAxisPixels(screen_transition_scroll_direction_t direction)
  1. Exit this functionreturn direction == SCREEN_TRANSITION_SCROLL_UP || direction == SCREEN_TRANSITION_SCROLL_DOWN ? LCD_HEIGHT : LCD_WIDTH;
Read original function source
static uint32_t screenScrollAxisPixels(screen_transition_scroll_direction_t direction)
{
    return direction == SCREEN_TRANSITION_SCROLL_UP ||
           direction == SCREEN_TRANSITION_SCROLL_DOWN ? LCD_HEIGHT : LCD_WIDTH;
}
screenScrollDraw() · source line 271
void screenScrollDraw(screen_transition_t *transition, uint16_t *frame, uint32_t now_ms)

Calls into this documentation:

  1. Decisiontransition == NULL || frame == NULL || !transition->scroll_active || transition->snapshot == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst uint32_t elapsed_ms = now_ms - transition->scroll_started_at_ms;
  3. Executeconst uint32_t steps = elapsed_ms / transition->scroll_step_ms;
  4. Executeuint32_t offset = steps * transition->scroll_pixels_per_step;
  5. Executeconst uint32_t axis_pixels = screenScrollAxisPixels(transition->scroll_direction);
  6. Executeconst uint32_t travel_pixels = axis_pixels + (transition->scroll_has_incoming ? transition->scroll_gap_pixels : 0U);
  7. Decisionoffset > travel_pixels
    True
    1. Executeoffset = travel_pixels;
    False

    Continue after this decision.

  8. Executeint x = 0;
  9. Executeint y = 0;
  10. Select a casetransition->scroll_direction

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

      1. Case / labelcase SCREEN_TRANSITION_SCROLL_LEFT:
      2. Executex = transition->scroll_in ? -LCD_WIDTH + (int)offset : -(int)offset;
      3. Exit enclosing loop or switchbreak;
      4. Case / labelcase SCREEN_TRANSITION_SCROLL_RIGHT:
      5. Executex = transition->scroll_in ? LCD_WIDTH - (int)offset : (int)offset;
      6. Exit enclosing loop or switchbreak;
      7. Case / labelcase SCREEN_TRANSITION_SCROLL_UP:
      8. Executey = transition->scroll_in ? -LCD_HEIGHT + (int)offset : -(int)offset;
      9. Exit enclosing loop or switchbreak;
      10. Case / labelcase SCREEN_TRANSITION_SCROLL_DOWN:
      11. Executey = transition->scroll_in ? LCD_HEIGHT - (int)offset : (int)offset;
      12. Exit enclosing loop or switchbreak;
  11. Executeclear(frame, COLOR_BLACK);
  12. Executedraw_rgb565_scaled(frame, x, y, transition->snapshot, NULL, LCD_WIDTH, LCD_HEIGHT, LCD_WIDTH, LCD_HEIGHT);
  13. Decisiontransition->scroll_has_incoming
    True
      1. Executeint incoming_x = 0;
      2. Executeint incoming_y = 0;
      3. Select a casetransition->scroll_incoming_direction

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

          1. Case / labelcase SCREEN_TRANSITION_SCROLL_LEFT:
          2. Executeincoming_x = -LCD_WIDTH - (int)transition->scroll_gap_pixels + (int)offset;
          3. Exit enclosing loop or switchbreak;
          4. Case / labelcase SCREEN_TRANSITION_SCROLL_RIGHT:
          5. Executeincoming_x = LCD_WIDTH + (int)transition->scroll_gap_pixels - (int)offset;
          6. Exit enclosing loop or switchbreak;
          7. Case / labelcase SCREEN_TRANSITION_SCROLL_UP:
          8. Executeincoming_y = -LCD_HEIGHT - (int)transition->scroll_gap_pixels + (int)offset;
          9. Exit enclosing loop or switchbreak;
          10. Case / labelcase SCREEN_TRANSITION_SCROLL_DOWN:
          11. Executeincoming_y = LCD_HEIGHT + (int)transition->scroll_gap_pixels - (int)offset;
          12. Exit enclosing loop or switchbreak;
      4. Executedraw_rgb565_scaled(frame, incoming_x, incoming_y, transition->scroll_incoming_snapshot, NULL, LCD_WIDTH, LCD_HEIGHT, LCD_WIDTH, LCD_HEIGHT);
    False

    Continue after this decision.

  14. Decisionoffset >= travel_pixels
    True
    1. Executetransition->scroll_complete = true;
    False

    Continue after this decision.

Read original function source
void screenScrollDraw(screen_transition_t *transition, uint16_t *frame, uint32_t now_ms)
{
    if (transition == NULL || frame == NULL || !transition->scroll_active ||
        transition->snapshot == NULL) return;
    const uint32_t elapsed_ms = now_ms - transition->scroll_started_at_ms;
    const uint32_t steps = elapsed_ms / transition->scroll_step_ms;
    uint32_t offset = steps * transition->scroll_pixels_per_step;
    const uint32_t axis_pixels = screenScrollAxisPixels(transition->scroll_direction);
    const uint32_t travel_pixels = axis_pixels +
        (transition->scroll_has_incoming ? transition->scroll_gap_pixels : 0U);
    if (offset > travel_pixels) offset = travel_pixels;
    // An outgoing image moves from its origin toward the selected edge. An incoming
    // image starts beyond that edge and moves back to its origin on either axis.
    int x = 0;
    int y = 0;
    switch (transition->scroll_direction) {
        case SCREEN_TRANSITION_SCROLL_LEFT:
            x = transition->scroll_in ? -LCD_WIDTH + (int)offset : -(int)offset;
            break;
        case SCREEN_TRANSITION_SCROLL_RIGHT:
            x = transition->scroll_in ? LCD_WIDTH - (int)offset : (int)offset;
            break;
        case SCREEN_TRANSITION_SCROLL_UP:
            y = transition->scroll_in ? -LCD_HEIGHT + (int)offset : -(int)offset;
            break;
        case SCREEN_TRANSITION_SCROLL_DOWN:
            y = transition->scroll_in ? LCD_HEIGHT - (int)offset : (int)offset;
            break;
    }
    clear(frame, COLOR_BLACK);
    draw_rgb565_scaled(frame, x, y, transition->snapshot, NULL,
                       LCD_WIDTH, LCD_HEIGHT, LCD_WIDTH, LCD_HEIGHT);
    if (transition->scroll_has_incoming) {
        // The second frame moves in at the same rate. A zero gap makes the two
        // frame edges touch; increasing gap_pixels leaves a controlled black band.
        int incoming_x = 0;
        int incoming_y = 0;
        switch (transition->scroll_incoming_direction) {
            case SCREEN_TRANSITION_SCROLL_LEFT:
                incoming_x = -LCD_WIDTH - (int)transition->scroll_gap_pixels + (int)offset;
                break;
            case SCREEN_TRANSITION_SCROLL_RIGHT:
                incoming_x = LCD_WIDTH + (int)transition->scroll_gap_pixels - (int)offset;
                break;
            case SCREEN_TRANSITION_SCROLL_UP:
                incoming_y = -LCD_HEIGHT - (int)transition->scroll_gap_pixels + (int)offset;
                break;
            case SCREEN_TRANSITION_SCROLL_DOWN:
                incoming_y = LCD_HEIGHT + (int)transition->scroll_gap_pixels - (int)offset;
                break;
        }
        draw_rgb565_scaled(frame, incoming_x, incoming_y,
                           transition->scroll_incoming_snapshot, NULL,
                           LCD_WIDTH, LCD_HEIGHT, LCD_WIDTH, LCD_HEIGHT);
    }
    if (offset >= travel_pixels) transition->scroll_complete = true;
}
screenScrollActive() · source line 329
bool screenScrollActive(const screen_transition_t *transition)
  1. Exit this functionreturn transition != NULL && transition->scroll_active;
Read original function source
bool screenScrollActive(const screen_transition_t *transition)
{
    return transition != NULL && transition->scroll_active;
}
screenScrollComplete() · source line 334
bool screenScrollComplete(const screen_transition_t *transition)
  1. Exit this functionreturn transition != NULL && transition->scroll_complete;
Read original function source
bool screenScrollComplete(const screen_transition_t *transition)
{
    return transition != NULL && transition->scroll_complete;
}
screenScrollReset() · source line 339
void screenScrollReset(screen_transition_t *transition)
  1. Decisiontransition == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executetransition->scroll_active = false;
  3. Executetransition->scroll_complete = false;
  4. Executetransition->scroll_in = false;
  5. Executetransition->scroll_has_incoming = false;
  6. Executetransition->scroll_gap_pixels = 0U;
Read original function source
void screenScrollReset(screen_transition_t *transition)
{
    if (transition == NULL) return;
    transition->scroll_active = false;
    transition->scroll_complete = false;
    transition->scroll_in = false;
    transition->scroll_has_incoming = false;
    transition->scroll_gap_pixels = 0U;
}
screenSliceStart() · source line 349
bool screenSliceStart(screen_transition_t *transition, const uint16_t *frame,
                      uint32_t now_ms, uint16_t slice_count, uint32_t slice_ms)

Calls into this documentation:

  1. Decisiontransition == NULL || frame == NULL || slice_count == 0U || slice_ms == 0U
    True
      1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Decision!ensureSnapshot(transition)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Decisionslice_count > (uint16_t)LCD_HEIGHT
    True
    1. Executeslice_count = (uint16_t)LCD_HEIGHT;
    False

    Continue after this decision.

  4. Executememcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
  5. Executetransition->slice_started_at_ms = now_ms;
  6. Executetransition->slice_ms = slice_ms;
  7. Executetransition->slice_count = slice_count;
  8. Executetransition->slice_active = true;
  9. Executetransition->slice_complete = false;
  10. Exit this functionreturn true;
Read original function source
bool screenSliceStart(screen_transition_t *transition, const uint16_t *frame,
                      uint32_t now_ms, uint16_t slice_count, uint32_t slice_ms)
{
    if (transition == NULL || frame == NULL || slice_count == 0U || slice_ms == 0U) {
        return false;
    }
    if (!ensureSnapshot(transition)) return false;
    if (slice_count > (uint16_t)LCD_HEIGHT) slice_count = (uint16_t)LCD_HEIGHT;
    // Capture the fully rendered outgoing scene so every strip keeps its original pixels.
    memcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    transition->slice_started_at_ms = now_ms;
    transition->slice_ms = slice_ms;
    transition->slice_count = slice_count;
    transition->slice_active = true;
    transition->slice_complete = false;
    return true;
}
drawSliceRows() · source line 367
static void drawSliceRows(uint16_t *frame, const uint16_t *snapshot, int first_row,
                          int row_count, int offset_x)
  1. Decisionoffset_x <= -LCD_WIDTH || offset_x >= LCD_WIDTH
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst int source_x = offset_x < 0 ? -offset_x : 0;
  3. Executeconst int destination_x = offset_x > 0 ? offset_x : 0;
  4. Executeconst int copy_width = LCD_WIDTH - (offset_x < 0 ? -offset_x : offset_x);
  5. FOR loopint row = 0; row < row_count; ++row

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

      1. Executeconst size_t index = (size_t)(first_row + row) * LCD_WIDTH;
      2. Executememcpy(&frame[index + destination_x], &snapshot[index + source_x], (size_t)copy_width * sizeof(frame[0]));
Read original function source
static void drawSliceRows(uint16_t *frame, const uint16_t *snapshot, int first_row,
                          int row_count, int offset_x)
{
    if (offset_x <= -LCD_WIDTH || offset_x >= LCD_WIDTH) return;
    const int source_x = offset_x < 0 ? -offset_x : 0;
    const int destination_x = offset_x > 0 ? offset_x : 0;
    const int copy_width = LCD_WIDTH - (offset_x < 0 ? -offset_x : offset_x);
    for (int row = 0; row < row_count; ++row) {
        const size_t index = (size_t)(first_row + row) * LCD_WIDTH;
        memcpy(&frame[index + destination_x], &snapshot[index + source_x],
               (size_t)copy_width * sizeof(frame[0]));
    }
}
screenSliceDraw() · source line 381
void screenSliceDraw(screen_transition_t *transition, uint16_t *frame, uint32_t now_ms)

Calls into this documentation:

  1. Decisiontransition == NULL || frame == NULL || !transition->slice_active || transition->snapshot == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst uint32_t elapsed_ms = now_ms - transition->slice_started_at_ms;
  3. Executefloat progress = (float)elapsed_ms / (float)transition->slice_ms;
  4. Decisionprogress > 1.0f
    True
    1. Executeprogress = 1.0f;
    False

    Continue after this decision.

  5. Executeconst int offset = (int)lroundf((float)LCD_WIDTH * progress);
  6. Executeclear(frame, COLOR_BLACK);
  7. Executeconst int base_height = LCD_HEIGHT / (int)transition->slice_count;
  8. Executeconst int remainder = LCD_HEIGHT % (int)transition->slice_count;
  9. Executeint first_row = 0;
  10. FOR loopuint16_t slice = 0U; slice < transition->slice_count; ++slice

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

      1. Executeconst int row_count = base_height + (slice < (uint16_t)remainder ? 1 : 0);
      2. Executeconst int direction_offset = (slice & 1U) == 0U ? -offset : offset;
      3. ExecutedrawSliceRows(frame, transition->snapshot, first_row, row_count, direction_offset);
      4. Executefirst_row += row_count;
  11. Decisionprogress >= 1.0f
    True
    1. Executetransition->slice_complete = true;
    False

    Continue after this decision.

Read original function source
void screenSliceDraw(screen_transition_t *transition, uint16_t *frame, uint32_t now_ms)
{
    if (transition == NULL || frame == NULL || !transition->slice_active ||
        transition->snapshot == NULL) return;
    const uint32_t elapsed_ms = now_ms - transition->slice_started_at_ms;
    float progress = (float)elapsed_ms / (float)transition->slice_ms;
    if (progress > 1.0f) progress = 1.0f;
    const int offset = (int)lroundf((float)LCD_WIDTH * progress);
    clear(frame, COLOR_BLACK);
    // Spread any remainder rows over the earliest strips so the complete display is covered.
    const int base_height = LCD_HEIGHT / (int)transition->slice_count;
    const int remainder = LCD_HEIGHT % (int)transition->slice_count;
    int first_row = 0;
    for (uint16_t slice = 0U; slice < transition->slice_count; ++slice) {
        const int row_count = base_height + (slice < (uint16_t)remainder ? 1 : 0);
        const int direction_offset = (slice & 1U) == 0U ? -offset : offset;
        drawSliceRows(frame, transition->snapshot, first_row, row_count, direction_offset);
        first_row += row_count;
    }
    if (progress >= 1.0f) transition->slice_complete = true;
}
screenSliceActive() · source line 403
bool screenSliceActive(const screen_transition_t *transition)
  1. Exit this functionreturn transition != NULL && transition->slice_active;
Read original function source
bool screenSliceActive(const screen_transition_t *transition)
{
    return transition != NULL && transition->slice_active;
}
screenSliceComplete() · source line 408
bool screenSliceComplete(const screen_transition_t *transition)
  1. Exit this functionreturn transition != NULL && transition->slice_complete;
Read original function source
bool screenSliceComplete(const screen_transition_t *transition)
{
    return transition != NULL && transition->slice_complete;
}
screenSliceReset() · source line 413
void screenSliceReset(screen_transition_t *transition)
  1. Decisiontransition == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executetransition->slice_active = false;
  3. Executetransition->slice_complete = false;
Read original function source
void screenSliceReset(screen_transition_t *transition)
{
    if (transition == NULL) return;
    transition->slice_active = false;
    transition->slice_complete = false;
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "screen_transition.h"

#include "esp_heap_caps.h"
#include "esp_random.h"
#include "game_graphics.h"

#include <math.h>
#include <string.h>

// Shared PSRAM image capacity. It must remain one complete RGB565 LCD frame.
#define SCREEN_TRANSITION_FRAME_BYTES (LCD_WIDTH * LCD_HEIGHT * sizeof(uint16_t))
// Rebuild tuning: these square cells are selected in a shuffled order.
#define SCREEN_REVEAL_BLOCK_PIXELS 8U
#define SCREEN_REVEAL_COLUMNS ((LCD_WIDTH + SCREEN_REVEAL_BLOCK_PIXELS - 1U) / SCREEN_REVEAL_BLOCK_PIXELS)
#define SCREEN_REVEAL_ROWS ((LCD_HEIGHT + SCREEN_REVEAL_BLOCK_PIXELS - 1U) / SCREEN_REVEAL_BLOCK_PIXELS)
#define SCREEN_REVEAL_BLOCK_COUNT (SCREEN_REVEAL_COLUMNS * SCREEN_REVEAL_ROWS)

static bool ensureSnapshot(screen_transition_t *transition)
{
    // Allocate on first use, then retain it for every later transition in this screen.
    if (transition->snapshot != NULL) return true;
    transition->snapshot = heap_caps_malloc(SCREEN_TRANSITION_FRAME_BYTES,
                                            MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    return transition->snapshot != NULL;
}

static bool ensureScrollIncomingSnapshot(screen_transition_t *transition)
{
    if (transition->scroll_incoming_snapshot != NULL) return true;
    transition->scroll_incoming_snapshot = heap_caps_malloc(
        SCREEN_TRANSITION_FRAME_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    return transition->scroll_incoming_snapshot != NULL;
}

bool screenTransitionStart(screen_transition_t *transition, const uint16_t *frame,
                           uint32_t now_ms, uint32_t freeze_ms, uint32_t zoom_ms)
{
    if (transition == NULL || frame == NULL || zoom_ms == 0U) return false;
    if (!ensureSnapshot(transition)) return false;
    // Freeze exactly the completed caller frame before the caller draws another scene.
    memcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    transition->started_at_ms = now_ms;
    transition->freeze_ms = freeze_ms;
    transition->zoom_ms = zoom_ms;
    transition->active = true;
    transition->complete = false;
    transition->zoom_in = false;
    return true;
}

bool screenTransitionZoomInStart(screen_transition_t *transition, const uint16_t *frame,
                                 uint32_t now_ms, uint32_t zoom_ms)
{
    if (!screenTransitionStart(transition, frame, now_ms, 0U, zoom_ms)) return false;
    transition->zoom_in = true;
    return true;
}

void screenTransitionDraw(screen_transition_t *transition, uint16_t *frame,
                          uint32_t now_ms)
{
    if (transition == NULL || frame == NULL || !transition->active ||
        transition->snapshot == NULL) return;
    const uint32_t elapsed_ms = now_ms - transition->started_at_ms;
    if (!transition->zoom_in && elapsed_ms < transition->freeze_ms) {
        // The initial phase is a literal frozen copy, not a re-rendered scene.
        memcpy(frame, transition->snapshot, SCREEN_TRANSITION_FRAME_BYTES);
        return;
    }

    const uint32_t zoom_elapsed_ms = transition->zoom_in
        ? elapsed_ms : elapsed_ms - transition->freeze_ms;
    float progress = (float)zoom_elapsed_ms / (float)transition->zoom_ms;
    if (progress > 1.0f) progress = 1.0f;
    // Square progress eases into the outgoing dot and eases away from the incoming dot.
    const float scale = transition->zoom_in ? progress * progress : 1.0f - progress * progress;
    int width = (int)lroundf((float)LCD_WIDTH * scale);
    int height = (int)lroundf((float)LCD_HEIGHT * scale);
    if (width < 1) width = 1;
    if (height < 1) height = 1;
    clear(frame, COLOR_BLACK);
    draw_rgb565_scaled(frame, (LCD_WIDTH - width) / 2, (LCD_HEIGHT - height) / 2,
                       transition->snapshot, NULL, LCD_WIDTH, LCD_HEIGHT, width, height);
    if (progress >= 1.0f) transition->complete = true;
}

bool screenTransitionActive(const screen_transition_t *transition)
{
    return transition != NULL && transition->active;
}

bool screenTransitionComplete(const screen_transition_t *transition)
{
    return transition != NULL && transition->complete;
}

void screenTransitionReset(screen_transition_t *transition)
{
    if (transition == NULL) return;
    transition->active = false;
    transition->complete = false;
    transition->zoom_in = false;
}

void screenTransitionDeinitialize(screen_transition_t *transition)
{
    if (transition == NULL) return;
    // Both allocations are owned by this reusable effect and live only while its caller is open.
    heap_caps_free(transition->snapshot);
    heap_caps_free(transition->reveal_block_order);
    heap_caps_free(transition->scroll_incoming_snapshot);
    *transition = (screen_transition_t){0};
}

bool screenRevealStart(screen_transition_t *transition, const uint16_t *frame,
                       uint32_t now_ms, uint32_t reveal_ms, unsigned reveal_percent)
{
    if (transition == NULL || frame == NULL || reveal_ms == 0U || reveal_percent == 0U) {
        return false;
    }
    if (!ensureSnapshot(transition)) return false;
    if (transition->reveal_block_order == NULL) {
        transition->reveal_block_order = heap_caps_malloc(
            SCREEN_REVEAL_BLOCK_COUNT * sizeof(transition->reveal_block_order[0]),
            MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
        if (transition->reveal_block_order == NULL) return false;
    }
    // Capture the already rendered destination stage before replacing it with black blocks.
    memcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    // Build a complete non-repeating block order, then shuffle it for this reveal.
    for (uint16_t block = 0U; block < SCREEN_REVEAL_BLOCK_COUNT; ++block) {
        transition->reveal_block_order[block] = block;
    }
    for (uint16_t block = SCREEN_REVEAL_BLOCK_COUNT - 1U; block > 0U; --block) {
        const uint16_t swap_with = (uint16_t)(esp_random() % (block + 1U));
        const uint16_t saved = transition->reveal_block_order[block];
        transition->reveal_block_order[block] = transition->reveal_block_order[swap_with];
        transition->reveal_block_order[swap_with] = saved;
    }
    if (reveal_percent > 100U) reveal_percent = 100U;
    // Round up so the requested percentage is never under-represented by a block.
    transition->reveal_target_count = (uint16_t)((SCREEN_REVEAL_BLOCK_COUNT * reveal_percent +
                                                  99U) / 100U);
    transition->reveal_block_count = 0U;
    transition->reveal_started_at_ms = now_ms;
    transition->reveal_ms = reveal_ms;
    transition->reveal_active = true;
    transition->reveal_complete = false;
    return true;
}

static void drawRevealBlock(uint16_t *frame, const uint16_t *snapshot, uint16_t block)
{
    // Copy an unscaled 8 x 8 cell; clipped edge cells handle display sizes not divisible by eight.
    const int x = (int)(block % SCREEN_REVEAL_COLUMNS) * SCREEN_REVEAL_BLOCK_PIXELS;
    const int y = (int)(block / SCREEN_REVEAL_COLUMNS) * SCREEN_REVEAL_BLOCK_PIXELS;
    int width = SCREEN_REVEAL_BLOCK_PIXELS;
    int height = SCREEN_REVEAL_BLOCK_PIXELS;
    if (x + width > LCD_WIDTH) width = LCD_WIDTH - x;
    if (y + height > LCD_HEIGHT) height = LCD_HEIGHT - y;
    for (int row = 0; row < height; ++row) {
        memcpy(&frame[(size_t)(y + row) * LCD_WIDTH + x],
               &snapshot[(size_t)(y + row) * LCD_WIDTH + x],
               (size_t)width * sizeof(frame[0]));
    }
}

void screenRevealDraw(screen_transition_t *transition, uint16_t *frame, uint32_t now_ms)
{
    if (transition == NULL || frame == NULL || !transition->reveal_active ||
        transition->snapshot == NULL || transition->reveal_block_order == NULL) return;
    const uint32_t elapsed_ms = now_ms - transition->reveal_started_at_ms;
    uint16_t visible_blocks = transition->reveal_target_count;
    if (elapsed_ms < transition->reveal_ms) {
        // Reveal only the early portion of the shuffled order until the configured percentage.
        visible_blocks = (uint16_t)(((uint64_t)transition->reveal_target_count * elapsed_ms) /
                                    transition->reveal_ms);
    }
    clear(frame, COLOR_BLACK);
    for (uint16_t index = 0U; index < visible_blocks; ++index) {
        drawRevealBlock(frame, transition->snapshot, transition->reveal_block_order[index]);
    }
    transition->reveal_block_count = visible_blocks;
    if (elapsed_ms >= transition->reveal_ms) {
        // The partial pattern has done its job: restore the complete frozen destination frame.
        memcpy(frame, transition->snapshot, SCREEN_TRANSITION_FRAME_BYTES);
        transition->reveal_complete = true;
    }
}

bool screenRevealActive(const screen_transition_t *transition)
{
    return transition != NULL && transition->reveal_active;
}

bool screenRevealComplete(const screen_transition_t *transition)
{
    return transition != NULL && transition->reveal_complete;
}

void screenRevealReset(screen_transition_t *transition)
{
    if (transition == NULL) return;
    transition->reveal_active = false;
    transition->reveal_complete = false;
    transition->reveal_block_count = 0U;
}

static bool screenScrollStart(screen_transition_t *transition, const uint16_t *frame,
                              uint32_t now_ms, screen_transition_scroll_direction_t direction,
                              uint32_t step_ms, uint16_t pixels_per_step, bool scroll_in)
{
    if (transition == NULL || frame == NULL || step_ms == 0U || pixels_per_step == 0U) {
        return false;
    }
    if (!ensureSnapshot(transition)) return false;
    // Capture the complete source before translating it so every step uses the same pixels.
    memcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    transition->scroll_started_at_ms = now_ms;
    transition->scroll_step_ms = step_ms;
    transition->scroll_pixels_per_step = pixels_per_step;
    transition->scroll_direction = direction;
    transition->scroll_has_incoming = false;
    transition->scroll_gap_pixels = 0U;
    transition->scroll_active = true;
    transition->scroll_complete = false;
    transition->scroll_in = scroll_in;
    return true;
}

bool screenScrollOutStart(screen_transition_t *transition, const uint16_t *frame,
                          uint32_t now_ms, screen_transition_scroll_direction_t direction,
                          uint32_t step_ms, uint16_t pixels_per_step)
{
    return screenScrollStart(transition, frame, now_ms, direction, step_ms,
                             pixels_per_step, false);
}

bool screenScrollInStart(screen_transition_t *transition, const uint16_t *frame,
                         uint32_t now_ms, screen_transition_scroll_direction_t direction,
                         uint32_t step_ms, uint16_t pixels_per_step)
{
    return screenScrollStart(transition, frame, now_ms, direction, step_ms,
                             pixels_per_step, true);
}

bool screenScrollSetIncoming(screen_transition_t *transition, const uint16_t *frame,
                             screen_transition_scroll_direction_t direction,
                             uint16_t gap_pixels)
{
    if (transition == NULL || frame == NULL || !transition->scroll_active ||
        transition->scroll_in || !ensureScrollIncomingSnapshot(transition)) {
        return false;
    }
    // Preserve the fully rendered destination so it can travel beside the outgoing frame.
    memcpy(transition->scroll_incoming_snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    transition->scroll_incoming_direction = direction;
    transition->scroll_gap_pixels = gap_pixels;
    transition->scroll_has_incoming = true;
    return true;
}

// Horizontal scrolls cross LCD_WIDTH pixels; vertical scrolls cross LCD_HEIGHT.
// Keeping this in one helper means the caller uses the same step settings for all axes.
static uint32_t screenScrollAxisPixels(screen_transition_scroll_direction_t direction)
{
    return direction == SCREEN_TRANSITION_SCROLL_UP ||
           direction == SCREEN_TRANSITION_SCROLL_DOWN ? LCD_HEIGHT : LCD_WIDTH;
}

void screenScrollDraw(screen_transition_t *transition, uint16_t *frame, uint32_t now_ms)
{
    if (transition == NULL || frame == NULL || !transition->scroll_active ||
        transition->snapshot == NULL) return;
    const uint32_t elapsed_ms = now_ms - transition->scroll_started_at_ms;
    const uint32_t steps = elapsed_ms / transition->scroll_step_ms;
    uint32_t offset = steps * transition->scroll_pixels_per_step;
    const uint32_t axis_pixels = screenScrollAxisPixels(transition->scroll_direction);
    const uint32_t travel_pixels = axis_pixels +
        (transition->scroll_has_incoming ? transition->scroll_gap_pixels : 0U);
    if (offset > travel_pixels) offset = travel_pixels;
    // An outgoing image moves from its origin toward the selected edge. An incoming
    // image starts beyond that edge and moves back to its origin on either axis.
    int x = 0;
    int y = 0;
    switch (transition->scroll_direction) {
        case SCREEN_TRANSITION_SCROLL_LEFT:
            x = transition->scroll_in ? -LCD_WIDTH + (int)offset : -(int)offset;
            break;
        case SCREEN_TRANSITION_SCROLL_RIGHT:
            x = transition->scroll_in ? LCD_WIDTH - (int)offset : (int)offset;
            break;
        case SCREEN_TRANSITION_SCROLL_UP:
            y = transition->scroll_in ? -LCD_HEIGHT + (int)offset : -(int)offset;
            break;
        case SCREEN_TRANSITION_SCROLL_DOWN:
            y = transition->scroll_in ? LCD_HEIGHT - (int)offset : (int)offset;
            break;
    }
    clear(frame, COLOR_BLACK);
    draw_rgb565_scaled(frame, x, y, transition->snapshot, NULL,
                       LCD_WIDTH, LCD_HEIGHT, LCD_WIDTH, LCD_HEIGHT);
    if (transition->scroll_has_incoming) {
        // The second frame moves in at the same rate. A zero gap makes the two
        // frame edges touch; increasing gap_pixels leaves a controlled black band.
        int incoming_x = 0;
        int incoming_y = 0;
        switch (transition->scroll_incoming_direction) {
            case SCREEN_TRANSITION_SCROLL_LEFT:
                incoming_x = -LCD_WIDTH - (int)transition->scroll_gap_pixels + (int)offset;
                break;
            case SCREEN_TRANSITION_SCROLL_RIGHT:
                incoming_x = LCD_WIDTH + (int)transition->scroll_gap_pixels - (int)offset;
                break;
            case SCREEN_TRANSITION_SCROLL_UP:
                incoming_y = -LCD_HEIGHT - (int)transition->scroll_gap_pixels + (int)offset;
                break;
            case SCREEN_TRANSITION_SCROLL_DOWN:
                incoming_y = LCD_HEIGHT + (int)transition->scroll_gap_pixels - (int)offset;
                break;
        }
        draw_rgb565_scaled(frame, incoming_x, incoming_y,
                           transition->scroll_incoming_snapshot, NULL,
                           LCD_WIDTH, LCD_HEIGHT, LCD_WIDTH, LCD_HEIGHT);
    }
    if (offset >= travel_pixels) transition->scroll_complete = true;
}

bool screenScrollActive(const screen_transition_t *transition)
{
    return transition != NULL && transition->scroll_active;
}

bool screenScrollComplete(const screen_transition_t *transition)
{
    return transition != NULL && transition->scroll_complete;
}

void screenScrollReset(screen_transition_t *transition)
{
    if (transition == NULL) return;
    transition->scroll_active = false;
    transition->scroll_complete = false;
    transition->scroll_in = false;
    transition->scroll_has_incoming = false;
    transition->scroll_gap_pixels = 0U;
}

bool screenSliceStart(screen_transition_t *transition, const uint16_t *frame,
                      uint32_t now_ms, uint16_t slice_count, uint32_t slice_ms)
{
    if (transition == NULL || frame == NULL || slice_count == 0U || slice_ms == 0U) {
        return false;
    }
    if (!ensureSnapshot(transition)) return false;
    if (slice_count > (uint16_t)LCD_HEIGHT) slice_count = (uint16_t)LCD_HEIGHT;
    // Capture the fully rendered outgoing scene so every strip keeps its original pixels.
    memcpy(transition->snapshot, frame, SCREEN_TRANSITION_FRAME_BYTES);
    transition->slice_started_at_ms = now_ms;
    transition->slice_ms = slice_ms;
    transition->slice_count = slice_count;
    transition->slice_active = true;
    transition->slice_complete = false;
    return true;
}

static void drawSliceRows(uint16_t *frame, const uint16_t *snapshot, int first_row,
                          int row_count, int offset_x)
{
    if (offset_x <= -LCD_WIDTH || offset_x >= LCD_WIDTH) return;
    const int source_x = offset_x < 0 ? -offset_x : 0;
    const int destination_x = offset_x > 0 ? offset_x : 0;
    const int copy_width = LCD_WIDTH - (offset_x < 0 ? -offset_x : offset_x);
    for (int row = 0; row < row_count; ++row) {
        const size_t index = (size_t)(first_row + row) * LCD_WIDTH;
        memcpy(&frame[index + destination_x], &snapshot[index + source_x],
               (size_t)copy_width * sizeof(frame[0]));
    }
}

void screenSliceDraw(screen_transition_t *transition, uint16_t *frame, uint32_t now_ms)
{
    if (transition == NULL || frame == NULL || !transition->slice_active ||
        transition->snapshot == NULL) return;
    const uint32_t elapsed_ms = now_ms - transition->slice_started_at_ms;
    float progress = (float)elapsed_ms / (float)transition->slice_ms;
    if (progress > 1.0f) progress = 1.0f;
    const int offset = (int)lroundf((float)LCD_WIDTH * progress);
    clear(frame, COLOR_BLACK);
    // Spread any remainder rows over the earliest strips so the complete display is covered.
    const int base_height = LCD_HEIGHT / (int)transition->slice_count;
    const int remainder = LCD_HEIGHT % (int)transition->slice_count;
    int first_row = 0;
    for (uint16_t slice = 0U; slice < transition->slice_count; ++slice) {
        const int row_count = base_height + (slice < (uint16_t)remainder ? 1 : 0);
        const int direction_offset = (slice & 1U) == 0U ? -offset : offset;
        drawSliceRows(frame, transition->snapshot, first_row, row_count, direction_offset);
        first_row += row_count;
    }
    if (progress >= 1.0f) transition->slice_complete = true;
}

bool screenSliceActive(const screen_transition_t *transition)
{
    return transition != NULL && transition->slice_active;
}

bool screenSliceComplete(const screen_transition_t *transition)
{
    return transition != NULL && transition->slice_complete;
}

void screenSliceReset(screen_transition_t *transition)
{
    if (transition == NULL) return;
    transition->slice_active = false;
    transition->slice_complete = false;
}