Last active
April 30, 2025 14:13
-
-
Save brookjordan/26454bfebb34f9ed5fb65f9eb51b79cb to your computer and use it in GitHub Desktop.
esphome: 8x8 LED matrix — bouncing ball
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
esphome: | |
... | |
on_boot: | |
then: | |
- script.execute: initialize_vectors | |
- script.execute: update_ball_physics | |
globals: | |
# Ball constants | |
- id: ball_radius | |
type: float | |
# initial_value: '0.564189583547756' # sqrt(1/pi) | |
initial_value: '0.7' | |
- id: collision_distance | |
type: float | |
initial_value: '1.0' | |
- id: max_speed | |
type: float | |
initial_value: '0.6' | |
- id: restitution | |
type: float | |
initial_value: '0.8' | |
- id: gravity | |
type: float | |
initial_value: '0.008' | |
- id: friction | |
type: float | |
initial_value: '0.995' | |
- id: max_balls | |
type: int | |
initial_value: '6' # Maximum number of balls allowed | |
- id: total_balls_created | |
type: int | |
initial_value: '3' # Start at 3 since that's our initial count | |
- id: ball_colors | |
type: std::vector<int> | |
restore_value: no | |
- id: trail_index | |
type: int | |
initial_value: '0' | |
# Ball properties | |
- id: num_balls | |
type: int | |
initial_value: '3' | |
- id: trail_steps | |
type: int | |
initial_value: '7' | |
- id: canvas_width | |
type: float | |
initial_value: '8.0' | |
- id: canvas_height | |
type: float | |
initial_value: '8.0' | |
- id: trail_initial_brightness | |
type: float | |
initial_value: '0.9' # 0.0 to 1.0, was previously hardcoded at 0.5 | |
- id: trail_fade_rate | |
type: float | |
initial_value: '0.9' # Higher = slower fade, lower = faster fade | |
# Ball positions and velocities | |
- id: pos_x | |
type: std::vector<float> | |
restore_value: no | |
- id: pos_y | |
type: std::vector<float> | |
restore_value: no | |
- id: speed_x | |
type: std::vector<float> | |
restore_value: no | |
- id: speed_y | |
type: std::vector<float> | |
restore_value: no | |
- id: color | |
type: std::vector<int> | |
restore_value: no | |
# Trail arrays for each ball | |
- id: trails_x | |
type: std::vector<std::vector<float>> | |
restore_value: no | |
- id: trails_y | |
type: std::vector<std::vector<float>> | |
restore_value: no | |
light: | |
- platform: neopixelbus | |
type: GRB | |
variant: WS2811 | |
pin: GPIO1 | |
num_leds: 64 | |
name: "NeoPixel Light" | |
id: led_matrix_light | |
button: | |
- platform: template | |
name: "Reset Balls" | |
on_press: | |
then: | |
- lambda: |- | |
const float speedRange = 0.5; | |
const float heightRange = 4.0; | |
for (int i = 0; i < id(num_balls); i++) { | |
id(speed_x)[i] = ((static_cast<float>(esp_random()) / UINT32_MAX) * speedRange * 2.0) - speedRange; | |
id(speed_y)[i] = ((static_cast<float>(esp_random()) / UINT32_MAX) * speedRange * 2.0) - speedRange; | |
id(pos_x)[i] = ((static_cast<float>(esp_random()) / UINT32_MAX) * (id(canvas_width) - id(ball_radius) * 2.0)) + id(ball_radius); | |
id(pos_y)[i] = -((static_cast<float>(esp_random()) / UINT32_MAX) * heightRange); | |
int colorCycle = i % 6; // Changed to cycle through 6 colors | |
id(color)[i] = id(ball_colors)[colorCycle]; | |
} | |
- platform: template | |
name: "Add Ball" | |
on_press: | |
then: | |
- lambda: |- | |
// Add new ball with random position and speed | |
const float speedRange = 0.5; | |
const float heightRange = 4.0; | |
if (id(num_balls) >= id(max_balls)) { | |
// Remove the oldest ball by shifting all arrays left | |
for (int i = 0; i < id(num_balls) - 1; i++) { | |
id(pos_x)[i] = id(pos_x)[i + 1]; | |
id(pos_y)[i] = id(pos_y)[i + 1]; | |
id(speed_x)[i] = id(speed_x)[i + 1]; | |
id(speed_y)[i] = id(speed_y)[i + 1]; | |
id(color)[i] = id(color)[i + 1]; | |
id(trails_x)[i] = id(trails_x)[i + 1]; | |
id(trails_y)[i] = id(trails_y)[i + 1]; | |
} | |
// Reduce sizes to remove the last element | |
id(pos_x).pop_back(); | |
id(pos_y).pop_back(); | |
id(speed_x).pop_back(); | |
id(speed_y).pop_back(); | |
id(color).pop_back(); | |
id(trails_x).pop_back(); | |
id(trails_y).pop_back(); | |
id(num_balls)--; | |
} | |
// Add new ball | |
id(pos_x).push_back(((static_cast<float>(esp_random()) / UINT32_MAX) * (id(canvas_width) - id(ball_radius) * 2.0)) + id(ball_radius)); | |
id(pos_y).push_back(-((static_cast<float>(esp_random()) / UINT32_MAX) * heightRange)); | |
id(speed_x).push_back(((static_cast<float>(esp_random()) / UINT32_MAX) * speedRange * 2.0) - speedRange); | |
id(speed_y).push_back(((static_cast<float>(esp_random()) / UINT32_MAX) * speedRange * 2.0) - speedRange); | |
// Add new color (cycling through 6 colors using total_balls_created) | |
int colorCycle = id(total_balls_created) % 6; | |
id(color).push_back(id(ball_colors)[colorCycle]); | |
// Add new trail vectors | |
std::vector<float> empty_trail(id(trail_steps), 0.0f); | |
id(trails_x).push_back(empty_trail); | |
id(trails_y).push_back(empty_trail); | |
// Increment counters | |
id(num_balls)++; | |
id(total_balls_created)++; | |
script: | |
- id: initialize_vectors | |
then: | |
- lambda: |- | |
// Initialize vectors with default values | |
id(pos_x) = {0.0f, 1.0f, 2.0f}; | |
id(pos_y) = {-2.0f, -2.0f, -2.0f}; | |
id(speed_x) = {0.2f, -0.2f, 0.1f}; | |
id(speed_y) = {0.0f, 0.0f, 0.0f}; | |
// Initialize color palette | |
id(ball_colors) = {0x01BEFE, 0xFFDD00, 0xFF7D00, 0xFF006D, 0xADFF02, 0x8F00FF}; | |
// Set initial ball colors | |
id(color) = {id(ball_colors)[0], id(ball_colors)[1], id(ball_colors)[2]}; | |
id(total_balls_created) = 3; | |
// Initialize trail vectors for each ball | |
std::vector<float> empty_trail(id(trail_steps), 0.0f); | |
id(trails_x).resize(id(num_balls), empty_trail); | |
id(trails_y).resize(id(num_balls), empty_trail); | |
- id: update_ball_physics | |
mode: restart | |
then: | |
- lambda: |- | |
for (int i = 0; i < id(num_balls); i++) { | |
// Clamp X position | |
if (id(pos_x)[i] < id(ball_radius)) { | |
id(pos_x)[i] = id(ball_radius); | |
} else if (id(pos_x)[i] > id(canvas_width) - id(ball_radius)) { | |
id(pos_x)[i] = id(canvas_width) - id(ball_radius); | |
} | |
// Clamp Y position (only at bottom, allow above screen for drops) | |
if (id(pos_y)[i] > id(canvas_height) - id(ball_radius)) { | |
id(pos_y)[i] = id(canvas_height) - id(ball_radius); | |
} | |
} | |
for (int i = 0; i < id(num_balls); i++) { | |
for (int j = i + 1; j < id(num_balls); j++) { | |
float dx = id(pos_x)[j] - id(pos_x)[i]; | |
float dy = id(pos_y)[j] - id(pos_y)[i]; | |
float distanceSquared = dx * dx + dy * dy; | |
if (distanceSquared < id(collision_distance) * id(collision_distance)) { | |
float distance = sqrt(distanceSquared); | |
float nx = dx / distance; | |
float ny = dy / distance; | |
float overlap = id(collision_distance) - distance; | |
if (overlap > 0) { | |
float correction = overlap * 0.5; | |
id(pos_x)[i] -= nx * correction; | |
id(pos_y)[i] -= ny * correction; | |
id(pos_x)[j] += nx * correction; | |
id(pos_y)[j] += ny * correction; | |
} | |
} | |
} | |
} | |
// Update velocities | |
for (int i = 0; i < id(num_balls); i++) { | |
id(speed_y)[i] += id(gravity); | |
id(speed_y)[i] *= id(friction); | |
id(speed_x)[i] *= id(friction); | |
float currentSpeed = sqrt(id(speed_x)[i] * id(speed_x)[i] + id(speed_y)[i] * id(speed_y)[i]); | |
if (currentSpeed > id(max_speed)) { | |
float scale = id(max_speed) / currentSpeed; | |
id(speed_x)[i] *= scale; | |
id(speed_y)[i] *= scale; | |
} | |
} | |
// Update positions | |
for (int i = 0; i < id(num_balls); i++) { | |
id(pos_x)[i] += id(speed_x)[i]; | |
id(pos_y)[i] += id(speed_y)[i]; | |
} | |
// Handle collisions | |
for (int i = 0; i < id(num_balls); i++) { | |
// Boundary collisions | |
if (id(pos_y)[i] >= id(canvas_height) - id(ball_radius)) { | |
id(pos_y)[i] = id(canvas_height) - id(ball_radius); | |
id(speed_y)[i] *= -id(restitution); | |
id(speed_x)[i] *= id(friction); | |
} | |
if (id(pos_x)[i] >= id(canvas_width) - id(ball_radius)) { | |
id(pos_x)[i] = id(canvas_width) - id(ball_radius); | |
id(speed_x)[i] *= -id(restitution); | |
} else if (id(pos_x)[i] <= id(ball_radius)) { | |
id(pos_x)[i] = id(ball_radius); | |
id(speed_x)[i] *= -id(restitution); | |
} | |
// Ball collisions | |
for (int j = i + 1; j < id(num_balls); j++) { | |
float dx = id(pos_x)[j] - id(pos_x)[i]; | |
float dy = id(pos_y)[j] - id(pos_y)[i]; | |
float distanceSquared = dx * dx + dy * dy; | |
if (distanceSquared <= id(collision_distance) * id(collision_distance)) { | |
float distance = sqrt(distanceSquared); | |
float nx = dx / distance; | |
float ny = dy / distance; | |
// Relative velocity | |
float rvx = id(speed_x)[i] - id(speed_x)[j]; | |
float rvy = id(speed_y)[i] - id(speed_y)[j]; | |
float velAlongNormal = rvx * nx + rvy * ny; | |
if (velAlongNormal <= 0) { | |
float impulse = -(1 + id(restitution)) * velAlongNormal; | |
id(speed_x)[i] += nx * impulse * 0.5; | |
id(speed_y)[i] += ny * impulse * 0.5; | |
id(speed_x)[j] -= nx * impulse * 0.5; | |
id(speed_y)[j] -= ny * impulse * 0.5; | |
} | |
} | |
} | |
} | |
// Update trails for all balls | |
id(trail_index) = (id(trail_index) + 1) % id(trail_steps); | |
for (int i = 0; i < id(num_balls); i++) { | |
id(trails_x)[i][id(trail_index)] = id(pos_x)[i]; | |
id(trails_y)[i][id(trail_index)] = id(pos_y)[i]; | |
} | |
- delay: 16ms | |
- script.execute: update_ball_physics | |
display: | |
- platform: addressable_light | |
id: led_matrix_display | |
addressable_light_id: led_matrix_light | |
width: 8 | |
height: 8 | |
rotation: 0° | |
update_interval: 16ms | |
auto_clear_enabled: True | |
pixel_mapper: |- | |
if (x % 2 == 0) { | |
return (x * 8) + y; | |
} | |
return (x * 8) + (7 - y); | |
lambda: |- | |
// Draw all trails first | |
for (int i = 0; i < id(num_balls); i++) { | |
for (int t = 0; t < id(trail_steps); t++) { | |
int trail_pos = (id(trail_index) - t + id(trail_steps)) % id(trail_steps); | |
if (id(trails_y)[i][trail_pos] < id(canvas_height)) { | |
int drawX = static_cast<int>(std::round(id(trails_x)[i][trail_pos] - 0.5f)); | |
int drawY = static_cast<int>(std::round(id(trails_y)[i][trail_pos] - 0.5f)); | |
if (drawX >= 0 && drawX < id(canvas_width) && drawY >= 0 && drawY < id(canvas_height)) { | |
float intensity = id(trail_initial_brightness) * pow(id(trail_fade_rate), t); | |
int baseColor = id(color)[i]; | |
int r = ((baseColor >> 16) & 0xFF) * intensity; | |
int g = ((baseColor >> 8) & 0xFF) * intensity; | |
int b = (baseColor & 0xFF) * intensity; | |
it.draw_pixel_at(drawX, drawY, Color(r, g, b)); | |
} | |
} | |
} | |
} | |
// Structure to hold ball drawing information | |
struct BallInfo { | |
int index; // Original ball index | |
int targetX; // Target grid position | |
int targetY; | |
float originalX; // Original floating point position | |
float originalY; | |
float distance; // Distance from original to target position | |
bool placed; // Whether this ball has been placed | |
}; | |
// Collect all valid balls to draw | |
std::vector<BallInfo> balls; | |
balls.reserve(id(num_balls)); | |
for (int i = 0; i < id(num_balls); i++) { | |
// Only check bottom boundary for balls | |
if (id(pos_y)[i] < id(canvas_height)) { | |
int drawX = static_cast<int>(std::round(id(pos_x)[i] - 0.5f)); | |
int drawY = static_cast<int>(std::round(id(pos_y)[i] - 0.5f)); | |
// Add explicit bounds checking | |
if (drawX >= 0 && drawX < id(canvas_width) && drawY >= 0 && drawY < id(canvas_height)) { | |
BallInfo ball; | |
ball.index = i; | |
ball.targetX = drawX; | |
ball.targetY = drawY; | |
ball.originalX = id(pos_x)[i]; | |
ball.originalY = id(pos_y)[i]; | |
ball.distance = 0; | |
ball.placed = false; | |
balls.push_back(ball); | |
} | |
} | |
} | |
// Function to calculate distance between two points | |
auto distance = [](float x1, float y1, float x2, float y2) -> float { | |
float dx = x2 - x1; | |
float dy = y2 - y1; | |
return dx * dx + dy * dy; | |
}; | |
// Track occupied positions | |
bool occupied[8][8] = {{false}}; | |
// First, place all balls that don't conflict | |
for (auto& ball : balls) { | |
if (!occupied[ball.targetX][ball.targetY]) { | |
occupied[ball.targetX][ball.targetY] = true; | |
ball.placed = true; | |
it.draw_pixel_at(ball.targetX, ball.targetY, Color(id(color)[ball.index])); | |
} | |
} | |
// For remaining balls, handle conflicts by finding nearest positions | |
bool changes; | |
do { | |
changes = false; | |
float bestTotalDistance = 999999.0f; | |
int bestBallIndex = -1; | |
int bestX = -1; | |
int bestY = -1; | |
// Find the unplaced ball that can be placed with minimal displacement | |
for (int i = 0; i < balls.size(); i++) { | |
if (balls[i].placed) continue; | |
// Try all adjacent positions | |
for (int d = 1; d < 8; d++) { | |
for (int dx = -d; dx <= d; dx++) { | |
for (int dy = -d; dy <= d; dy++) { | |
if (abs(dx) == d || abs(dy) == d) { | |
int newX = balls[i].targetX + dx; | |
int newY = balls[i].targetY + dy; | |
if (newX >= 0 && newX < 8 && newY >= 0 && newY < 8 && !occupied[newX][newY]) { | |
float dist = distance(balls[i].originalX, balls[i].originalY, newX, newY); | |
if (dist < bestTotalDistance) { | |
bestTotalDistance = dist; | |
bestBallIndex = i; | |
bestX = newX; | |
bestY = newY; | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
// Place the ball with the smallest displacement | |
if (bestBallIndex >= 0) { | |
occupied[bestX][bestY] = true; | |
balls[bestBallIndex].placed = true; | |
it.draw_pixel_at(bestX, bestY, Color(id(color)[balls[bestBallIndex].index])); | |
changes = true; | |
} | |
} while (changes); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment