Skip to content

Instantly share code, notes, and snippets.

@brookjordan
Last active April 30, 2025 14:13
Show Gist options
  • Save brookjordan/26454bfebb34f9ed5fb65f9eb51b79cb to your computer and use it in GitHub Desktop.
Save brookjordan/26454bfebb34f9ed5fb65f9eb51b79cb to your computer and use it in GitHub Desktop.
esphome: 8x8 LED matrix — bouncing ball
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