Boing Ball
Source: src/boing_ball.c · 206 lines · 7 functions.
Screen captures
Views of this part running on the Mini Arcade.

Screen / module overview
- Enter Boing Ball
Build cached sphere geometry once; reset position, velocity, rotation and bounce settings.
- Handle controls
D/B changes rotation speed. A/C changes bounce height.
- Advance physics
Limit elapsed time to 50 ms; integrate movement and gravity; advance texture rotation.
- Check collisions
Reverse horizontal movement at side walls. At the floor, apply the selected upward bounce velocity.
- Draw and repeat
Draw the shadow and checkerboard sphere using cached geometry. Show control status briefly after changes.
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
buildSphereMap() · source line 55
static void buildSphereMap(void)
- Decision
sphere_readyTrue- Exit this function
return;
FalseContinue after this decision.
- FOR loop
int y = 0; y < BALL_DIAMETER; ++yRepeat body while the loop condition permits; then continue below.
- FOR loop
int x = 0; x < BALL_DIAMETER; ++xRepeat body while the loop condition permits; then continue below.
- Execute
const int index = y * BALL_DIAMETER + x; - Execute
const float normal_x = (float)(x - BALL_RADIUS) / (float)BALL_RADIUS; - Execute
const float normal_y = (float)(y - BALL_RADIUS) / (float)BALL_RADIUS; - Execute
const float radius_squared = normal_x * normal_x + normal_y * normal_y; - Decision
radius_squared > 1.0fTrue- Execute
shade_map[index] = 0U; - Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const float depth = sqrtf(1.0f - radius_squared); - Execute
const float longitude = (atan2f(normal_x, depth) + BALL_PI) / (2.0f * BALL_PI); - Execute
const float latitude = (asinf(normal_y) + BALL_PI * 0.5f) / BALL_PI; - Execute
longitude_map[index] = (uint8_t)(longitude * (float)BALL_LONGITUDE_COUNT); - Execute
latitude_map[index] = (uint8_t)(latitude * (float)BALL_LATITUDE_COUNT); - Execute
shade_map[index] = depth > 0.78f ? 3U : depth > 0.42f ? 2U : 1U;
- Execute
sphere_ready = true;
Read original function source
static void buildSphereMap(void)
{
// Geometry is constant for the life of the application, so build it once.
if (sphere_ready) return;
for (int y = 0; y < BALL_DIAMETER; ++y) {
for (int x = 0; x < BALL_DIAMETER; ++x) {
const int index = y * BALL_DIAMETER + x;
// Convert square bitmap coordinates to a -1..1 sphere plane.
const float normal_x = (float)(x - BALL_RADIUS) / (float)BALL_RADIUS;
const float normal_y = (float)(y - BALL_RADIUS) / (float)BALL_RADIUS;
const float radius_squared = normal_x * normal_x + normal_y * normal_y;
if (radius_squared > 1.0f) {
// Pixels outside the unit circle are outside the visible sphere.
shade_map[index] = 0U;
continue;
}
// Depth is the visible front hemisphere at this position.
const float depth = sqrtf(1.0f - radius_squared);
const float longitude = (atan2f(normal_x, depth) + BALL_PI) / (2.0f * BALL_PI);
const float latitude = (asinf(normal_y) + BALL_PI * 0.5f) / BALL_PI;
// Store checkerboard bands and a simple centre-to-rim lighting level.
longitude_map[index] = (uint8_t)(longitude * (float)BALL_LONGITUDE_COUNT);
latitude_map[index] = (uint8_t)(latitude * (float)BALL_LATITUDE_COUNT);
shade_map[index] = depth > 0.78f ? 3U : depth > 0.42f ? 2U : 1U;
}
}
sphere_ready = true;
}drawShadow() · source line 84
static void drawShadow(uint16_t *frame, int centre_x)
Calls into this documentation:
- Execute
const uint16_t shadow = RGB565(0, 0, 80); - Execute
fill_rect(frame, centre_x - 16, (int)BALL_FLOOR_Y, 33, 1, shadow); - Execute
fill_rect(frame, centre_x - 23, (int)BALL_FLOOR_Y + 1, 47, 2, shadow); - Execute
fill_rect(frame, centre_x - 18, (int)BALL_FLOOR_Y + 3, 37, 1, shadow);
Read original function source
static void drawShadow(uint16_t *frame, int centre_x)
{
// A flattened dark-blue oval visually anchors the otherwise floating sphere.
const uint16_t shadow = RGB565(0, 0, 80);
fill_rect(frame, centre_x - 16, (int)BALL_FLOOR_Y, 33, 1, shadow);
fill_rect(frame, centre_x - 23, (int)BALL_FLOOR_Y + 1, 47, 2, shadow);
fill_rect(frame, centre_x - 18, (int)BALL_FLOOR_Y + 3, 37, 1, shadow);
}startBoingBall() · source line 93
void startBoingBall(uint32_t now_ms)
Calls into this documentation:
- Execute
buildSphereMap(); - Execute
ball_x = 82.0f; - Execute
ball_y = 52.0f; - Execute
ball_velocity_x = BALL_HORIZONTAL_VELOCITY; - Execute
ball_velocity_y = 0.0f; - Execute
rotation_phase = 0.0f; - Execute
rotation_speed_index = 2U; - Execute
bounce_height_index = 2U; - Execute
last_update_ms = now_ms; - Execute
controls_changed_at = now_ms; - Execute
running = true;
Read original function source
void startBoingBall(uint32_t now_ms)
{
// Prepare the cached geometry and reset the familiar left-to-right demo.
buildSphereMap();
ball_x = 82.0f;
ball_y = 52.0f;
ball_velocity_x = BALL_HORIZONTAL_VELOCITY;
ball_velocity_y = 0.0f;
rotation_phase = 0.0f;
rotation_speed_index = 2U;
bounce_height_index = 2U;
last_update_ms = now_ms;
controls_changed_at = now_ms;
running = true;
}stopBoingBall() · source line 109
void stopBoingBall(void)
- Execute
running = false;
Read original function source
void stopBoingBall(void)
{
// This module owns no allocated resources; stopping disables update/draw only.
running = false;
}boingBallAction() · source line 115
void boingBallAction(bool rotation_slower, bool rotation_faster, bool bounce_higher,
bool bounce_lower, uint32_t now_ms)- Decision
!runningTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
bool changed = false; - Decision
rotation_slower && rotation_speed_index > 0UTrue- Execute
--rotation_speed_index; - Execute
changed = true;
FalseContinue after this decision.
- Decision
rotation_faster && rotation_speed_index + 1U < ROTATION_SPEED_COUNTTrue- Execute
++rotation_speed_index; - Execute
changed = true;
FalseContinue after this decision.
- Decision
bounce_higher && bounce_height_index + 1U < BOUNCE_HEIGHT_COUNTTrue- Execute
++bounce_height_index; - Execute
changed = true;
FalseContinue after this decision.
- Decision
bounce_lower && bounce_height_index > 0UTrue- Execute
--bounce_height_index; - Execute
changed = true;
FalseContinue after this decision.
- Decision
changedTrue- Execute
controls_changed_at = now_ms;
FalseContinue after this decision.
Read original function source
void boingBallAction(bool rotation_slower, bool rotation_faster, bool bounce_higher,
bool bounce_lower, uint32_t now_ms)
{
if (!running) return;
bool changed = false;
// D/B adjust texture rotation; A/C adjust the selected bounce launch velocity.
if (rotation_slower && rotation_speed_index > 0U) {
--rotation_speed_index;
changed = true;
}
if (rotation_faster && rotation_speed_index + 1U < ROTATION_SPEED_COUNT) {
++rotation_speed_index;
changed = true;
}
if (bounce_higher && bounce_height_index + 1U < BOUNCE_HEIGHT_COUNT) {
++bounce_height_index;
changed = true;
}
if (bounce_lower && bounce_height_index > 0U) {
--bounce_height_index;
changed = true;
}
if (changed) controls_changed_at = now_ms;
}updateBoingBall() · source line 140
void updateBoingBall(uint32_t now_ms)
Calls into this documentation:
- Decision
!runningTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
uint32_t elapsed_ms = now_ms - last_update_ms; - Decision
elapsed_ms > 50UTrue- Execute
elapsed_ms = 50U;
FalseContinue after this decision.
- Execute
last_update_ms = now_ms; - Execute
const float elapsed = (float)elapsed_ms / 1000.0f; - Execute
ball_x += ball_velocity_x * elapsed; - Execute
ball_y += ball_velocity_y * elapsed; - Execute
ball_velocity_y += BALL_GRAVITY * elapsed; - Execute
rotation_phase += rotation_speeds[rotation_speed_index] * elapsed; - Decision
rotation_phase >= (float)BALL_LONGITUDE_COUNTTrue- Execute
rotation_phase -= (float)BALL_LONGITUDE_COUNT;
FalseContinue after this decision.
- Decision
ball_x < (float)BALL_RADIUSTrue- Execute
ball_x = (float)BALL_RADIUS; - Execute
ball_velocity_x = BALL_HORIZONTAL_VELOCITY; - Execute
audioPlay(AUDIO_SFX_BOING_IMPACT);
False- Decision
ball_x > (float)(LCD_WIDTH - 1 - BALL_RADIUS)True- Execute
ball_x = (float)(LCD_WIDTH - 1 - BALL_RADIUS); - Execute
ball_velocity_x = -BALL_HORIZONTAL_VELOCITY; - Execute
audioPlay(AUDIO_SFX_BOING_IMPACT);
FalseContinue after this decision.
- Decision
ball_y > BALL_FLOOR_Y - (float)BALL_RADIUSTrue- Execute
ball_y = BALL_FLOOR_Y - (float)BALL_RADIUS; - Execute
ball_velocity_y = bounce_velocities[bounce_height_index]; - Execute
audioPlay(AUDIO_SFX_BOING_IMPACT);
FalseContinue after this decision.
Read original function source
void updateBoingBall(uint32_t now_ms)
{
if (!running) return;
// Cap long stalls so physics never jumps by a large visible distance.
uint32_t elapsed_ms = now_ms - last_update_ms;
if (elapsed_ms > 50U) elapsed_ms = 50U;
last_update_ms = now_ms;
const float elapsed = (float)elapsed_ms / 1000.0f;
// Integrate horizontal velocity and gravity-driven vertical velocity.
ball_x += ball_velocity_x * elapsed;
ball_y += ball_velocity_y * elapsed;
ball_velocity_y += BALL_GRAVITY * elapsed;
// Advance and wrap the texture once per complete longitude revolution.
rotation_phase += rotation_speeds[rotation_speed_index] * elapsed;
if (rotation_phase >= (float)BALL_LONGITUDE_COUNT) rotation_phase -= (float)BALL_LONGITUDE_COUNT;
if (ball_x < (float)BALL_RADIUS) {
// Keep the entire 81-pixel ball on screen and bounce at both walls.
ball_x = (float)BALL_RADIUS;
ball_velocity_x = BALL_HORIZONTAL_VELOCITY;
audioPlay(AUDIO_SFX_BOING_IMPACT);
} else if (ball_x > (float)(LCD_WIDTH - 1 - BALL_RADIUS)) {
ball_x = (float)(LCD_WIDTH - 1 - BALL_RADIUS);
ball_velocity_x = -BALL_HORIZONTAL_VELOCITY;
audioPlay(AUDIO_SFX_BOING_IMPACT);
}
if (ball_y > BALL_FLOOR_Y - (float)BALL_RADIUS) {
// Land on the floor, then launch with the currently selected bounce height.
ball_y = BALL_FLOOR_Y - (float)BALL_RADIUS;
ball_velocity_y = bounce_velocities[bounce_height_index];
audioPlay(AUDIO_SFX_BOING_IMPACT);
}
}drawBoingBall() · source line 173
void drawBoingBall(uint16_t *frame)
Calls into this documentation:
- Decision
!running || frame == NULLTrue- Exit this function
return;
FalseContinue after this decision.
- Execute
clear(frame, RGB565(0, 32, 175)); - Execute
const int centre_x = (int)lroundf(ball_x); - Execute
const int centre_y = (int)lroundf(ball_y); - Execute
const uint8_t rotation = (uint8_t)rotation_phase; - Execute
drawShadow(frame, centre_x); - FOR loop
int y = 0; y < BALL_DIAMETER; ++yRepeat body while the loop condition permits; then continue below.
- Execute
const int screen_y = centre_y - BALL_RADIUS + y; - Decision
screen_y < 0 || screen_y >= LCD_HEIGHTTrue- Next loop iteration
continue;
FalseContinue after this decision.
- FOR loop
int x = 0; x < BALL_DIAMETER; ++xRepeat body while the loop condition permits; then continue below.
- Execute
const int index = y * BALL_DIAMETER + x; - Execute
const uint8_t shade = shade_map[index]; - Decision
shade == 0UTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const int screen_x = centre_x - BALL_RADIUS + x; - Decision
screen_x < 0 || screen_x >= LCD_WIDTHTrue- Next loop iteration
continue;
FalseContinue after this decision.
- Execute
const uint8_t longitude = (uint8_t)((longitude_map[index] + rotation) % BALL_LONGITUDE_COUNT); - Execute
const bool red = (((longitude / 2U) + latitude_map[index]) & 1U) != 0U; - Execute
frame[screen_y * LCD_WIDTH + screen_x] = red ? red_shades[shade - 1U] : white_shades[shade - 1U];
- Decision
last_update_ms - controls_changed_at < CONTROL_STATUS_MSTrue- Execute
char text[36]; - Execute
snprintf(text, sizeof(text), "D/B ROT %u A/C BOUNCE %u", (unsigned)rotation_speed_index + 1U, (unsigned)bounce_height_index + 1U); - Execute
draw_text_centred(frame, LCD_WIDTH / 2, 158, text, 1, COLOR_WHITE);
FalseContinue after this decision.
Read original function source
void drawBoingBall(uint16_t *frame)
{
if (!running || frame == NULL) return;
// Draw into the shared framebuffer over the Amiga-blue backdrop.
clear(frame, RGB565(0, 32, 175));
const int centre_x = (int)lroundf(ball_x);
const int centre_y = (int)lroundf(ball_y);
const uint8_t rotation = (uint8_t)rotation_phase;
drawShadow(frame, centre_x);
// Render only the cached sphere pixels that lie inside the screen bounds.
for (int y = 0; y < BALL_DIAMETER; ++y) {
const int screen_y = centre_y - BALL_RADIUS + y;
if (screen_y < 0 || screen_y >= LCD_HEIGHT) continue;
for (int x = 0; x < BALL_DIAMETER; ++x) {
const int index = y * BALL_DIAMETER + x;
const uint8_t shade = shade_map[index];
if (shade == 0U) continue;
const int screen_x = centre_x - BALL_RADIUS + x;
if (screen_x < 0 || screen_x >= LCD_WIDTH) continue;
// Offset longitude to rotate the checkerboard without recomputing geometry.
const uint8_t longitude = (uint8_t)((longitude_map[index] + rotation) % BALL_LONGITUDE_COUNT);
const bool red = (((longitude / 2U) + latitude_map[index]) & 1U) != 0U;
frame[screen_y * LCD_WIDTH + screen_x] = red ? red_shades[shade - 1U] : white_shades[shade - 1U];
}
}
if (last_update_ms - controls_changed_at < CONTROL_STATUS_MS) {
// The current tuning is shown briefly after the user changes a control.
char text[36];
snprintf(text, sizeof(text), "D/B ROT %u A/C BOUNCE %u",
(unsigned)rotation_speed_index + 1U, (unsigned)bounce_height_index + 1U);
draw_text_centred(frame, LCD_WIDTH / 2, 158, text, 1, COLOR_WHITE);
}
}Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "boing_ball.h"
#include "audio.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
// The ball is a procedural 81x81 sphere with rotating checkerboard bands.
#define BALL_RADIUS 40
#define BALL_DIAMETER (BALL_RADIUS * 2 + 1)
#define BALL_PIXEL_COUNT (BALL_DIAMETER * BALL_DIAMETER)
#define BALL_LONGITUDE_COUNT 16U // Checkerboard columns around the sphere.
#define BALL_LATITUDE_COUNT 8U // Checkerboard rows from top to bottom.
#define BALL_FLOOR_Y 146.0f // Shadow and bounce floor baseline.
#define BALL_GRAVITY 380.0f // Downward acceleration in pixels/second^2.
#define BALL_BOUNCE_VELOCITY -215.0f // Negative Y launches the ball upward.
#define BALL_HORIZONTAL_VELOCITY 82.0f
#define BALL_PI 3.14159265f
#define ROTATION_SPEED_COUNT 4U // D/B select one of four rotation speeds.
#define BOUNCE_HEIGHT_COUNT 4U // A/C select one of four bounce heights.
#define CONTROL_STATUS_MS 1500U // Brief duration of the control reminder.
// Cached sphere geometry avoids trigonometry during every rendered frame.
// shade_map is zero outside the disc, otherwise a one-to-three lighting level.
static uint8_t longitude_map[BALL_PIXEL_COUNT];
static uint8_t latitude_map[BALL_PIXEL_COUNT];
static uint8_t shade_map[BALL_PIXEL_COUNT];
// Ball position is its centre; velocities use pixels per second.
static float ball_x;
static float ball_y;
static float ball_velocity_x;
static float ball_velocity_y;
// Texture offset measured in longitude bands, rather than degrees.
static float rotation_phase;
static uint32_t last_update_ms;
static uint32_t controls_changed_at;
// Indexes into the four tuning tables below.
static uint8_t rotation_speed_index;
static uint8_t bounce_height_index;
static bool running;
static bool sphere_ready;
// Three-level directional lighting for red and white checkerboard squares.
static const uint16_t red_shades[] = {
RGB565(105, 10, 10), RGB565(190, 22, 20), RGB565(250, 70, 60)
};
static const uint16_t white_shades[] = {
RGB565(105, 115, 135), RGB565(210, 220, 235), RGB565(255, 255, 255)
};
static const float rotation_speeds[ROTATION_SPEED_COUNT] = {4.0f, 8.0f, 12.0f, 16.0f};
static const float bounce_velocities[BOUNCE_HEIGHT_COUNT] = {-160.0f, -190.0f, BALL_BOUNCE_VELOCITY, -250.0f};
static void buildSphereMap(void)
{
// Geometry is constant for the life of the application, so build it once.
if (sphere_ready) return;
for (int y = 0; y < BALL_DIAMETER; ++y) {
for (int x = 0; x < BALL_DIAMETER; ++x) {
const int index = y * BALL_DIAMETER + x;
// Convert square bitmap coordinates to a -1..1 sphere plane.
const float normal_x = (float)(x - BALL_RADIUS) / (float)BALL_RADIUS;
const float normal_y = (float)(y - BALL_RADIUS) / (float)BALL_RADIUS;
const float radius_squared = normal_x * normal_x + normal_y * normal_y;
if (radius_squared > 1.0f) {
// Pixels outside the unit circle are outside the visible sphere.
shade_map[index] = 0U;
continue;
}
// Depth is the visible front hemisphere at this position.
const float depth = sqrtf(1.0f - radius_squared);
const float longitude = (atan2f(normal_x, depth) + BALL_PI) / (2.0f * BALL_PI);
const float latitude = (asinf(normal_y) + BALL_PI * 0.5f) / BALL_PI;
// Store checkerboard bands and a simple centre-to-rim lighting level.
longitude_map[index] = (uint8_t)(longitude * (float)BALL_LONGITUDE_COUNT);
latitude_map[index] = (uint8_t)(latitude * (float)BALL_LATITUDE_COUNT);
shade_map[index] = depth > 0.78f ? 3U : depth > 0.42f ? 2U : 1U;
}
}
sphere_ready = true;
}
static void drawShadow(uint16_t *frame, int centre_x)
{
// A flattened dark-blue oval visually anchors the otherwise floating sphere.
const uint16_t shadow = RGB565(0, 0, 80);
fill_rect(frame, centre_x - 16, (int)BALL_FLOOR_Y, 33, 1, shadow);
fill_rect(frame, centre_x - 23, (int)BALL_FLOOR_Y + 1, 47, 2, shadow);
fill_rect(frame, centre_x - 18, (int)BALL_FLOOR_Y + 3, 37, 1, shadow);
}
void startBoingBall(uint32_t now_ms)
{
// Prepare the cached geometry and reset the familiar left-to-right demo.
buildSphereMap();
ball_x = 82.0f;
ball_y = 52.0f;
ball_velocity_x = BALL_HORIZONTAL_VELOCITY;
ball_velocity_y = 0.0f;
rotation_phase = 0.0f;
rotation_speed_index = 2U;
bounce_height_index = 2U;
last_update_ms = now_ms;
controls_changed_at = now_ms;
running = true;
}
void stopBoingBall(void)
{
// This module owns no allocated resources; stopping disables update/draw only.
running = false;
}
void boingBallAction(bool rotation_slower, bool rotation_faster, bool bounce_higher,
bool bounce_lower, uint32_t now_ms)
{
if (!running) return;
bool changed = false;
// D/B adjust texture rotation; A/C adjust the selected bounce launch velocity.
if (rotation_slower && rotation_speed_index > 0U) {
--rotation_speed_index;
changed = true;
}
if (rotation_faster && rotation_speed_index + 1U < ROTATION_SPEED_COUNT) {
++rotation_speed_index;
changed = true;
}
if (bounce_higher && bounce_height_index + 1U < BOUNCE_HEIGHT_COUNT) {
++bounce_height_index;
changed = true;
}
if (bounce_lower && bounce_height_index > 0U) {
--bounce_height_index;
changed = true;
}
if (changed) controls_changed_at = now_ms;
}
void updateBoingBall(uint32_t now_ms)
{
if (!running) return;
// Cap long stalls so physics never jumps by a large visible distance.
uint32_t elapsed_ms = now_ms - last_update_ms;
if (elapsed_ms > 50U) elapsed_ms = 50U;
last_update_ms = now_ms;
const float elapsed = (float)elapsed_ms / 1000.0f;
// Integrate horizontal velocity and gravity-driven vertical velocity.
ball_x += ball_velocity_x * elapsed;
ball_y += ball_velocity_y * elapsed;
ball_velocity_y += BALL_GRAVITY * elapsed;
// Advance and wrap the texture once per complete longitude revolution.
rotation_phase += rotation_speeds[rotation_speed_index] * elapsed;
if (rotation_phase >= (float)BALL_LONGITUDE_COUNT) rotation_phase -= (float)BALL_LONGITUDE_COUNT;
if (ball_x < (float)BALL_RADIUS) {
// Keep the entire 81-pixel ball on screen and bounce at both walls.
ball_x = (float)BALL_RADIUS;
ball_velocity_x = BALL_HORIZONTAL_VELOCITY;
audioPlay(AUDIO_SFX_BOING_IMPACT);
} else if (ball_x > (float)(LCD_WIDTH - 1 - BALL_RADIUS)) {
ball_x = (float)(LCD_WIDTH - 1 - BALL_RADIUS);
ball_velocity_x = -BALL_HORIZONTAL_VELOCITY;
audioPlay(AUDIO_SFX_BOING_IMPACT);
}
if (ball_y > BALL_FLOOR_Y - (float)BALL_RADIUS) {
// Land on the floor, then launch with the currently selected bounce height.
ball_y = BALL_FLOOR_Y - (float)BALL_RADIUS;
ball_velocity_y = bounce_velocities[bounce_height_index];
audioPlay(AUDIO_SFX_BOING_IMPACT);
}
}
void drawBoingBall(uint16_t *frame)
{
if (!running || frame == NULL) return;
// Draw into the shared framebuffer over the Amiga-blue backdrop.
clear(frame, RGB565(0, 32, 175));
const int centre_x = (int)lroundf(ball_x);
const int centre_y = (int)lroundf(ball_y);
const uint8_t rotation = (uint8_t)rotation_phase;
drawShadow(frame, centre_x);
// Render only the cached sphere pixels that lie inside the screen bounds.
for (int y = 0; y < BALL_DIAMETER; ++y) {
const int screen_y = centre_y - BALL_RADIUS + y;
if (screen_y < 0 || screen_y >= LCD_HEIGHT) continue;
for (int x = 0; x < BALL_DIAMETER; ++x) {
const int index = y * BALL_DIAMETER + x;
const uint8_t shade = shade_map[index];
if (shade == 0U) continue;
const int screen_x = centre_x - BALL_RADIUS + x;
if (screen_x < 0 || screen_x >= LCD_WIDTH) continue;
// Offset longitude to rotate the checkerboard without recomputing geometry.
const uint8_t longitude = (uint8_t)((longitude_map[index] + rotation) % BALL_LONGITUDE_COUNT);
const bool red = (((longitude / 2U) + latitude_map[index]) & 1U) != 0U;
frame[screen_y * LCD_WIDTH + screen_x] = red ? red_shades[shade - 1U] : white_shades[shade - 1U];
}
}
if (last_update_ms - controls_changed_at < CONTROL_STATUS_MS) {
// The current tuning is shown briefly after the user changes a control.
char text[36];
snprintf(text, sizeof(text), "D/B ROT %u A/C BOUNCE %u",
(unsigned)rotation_speed_index + 1U, (unsigned)bounce_height_index + 1U);
drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 158, text, COLOR_WHITE, 1);
}
}