#include <FastLED.h>

#define WIDTH 16
#define HEIGHT 16
#define NUM_LEDS ((WIDTH) * (HEIGHT))

CRGB leds[NUM_LEDS + 1];

uint16_t XY(uint8_t x, uint8_t y) {
  if (x >= WIDTH) return NUM_LEDS;
  if (y >= HEIGHT) return NUM_LEDS;
  if (y & 1)
    return (y + 1) * WIDTH - 1 - x;
  else
    return y * WIDTH + x;
}

void setup() {
  Serial.begin(115200);
  FastLED.addLeds<NEOPIXEL, 3>(leds, NUM_LEDS);
  FastLED.setCorrection(UncorrectedColor);
  FastLED.setTemperature(UncorrectedTemperature);
  FastLED.setDither(DISABLE_DITHER);
}

struct Ball {
  float x_speed, y_speed, radius, transition;
  CRGB colour;
};

Ball balls[] = {
  {8, 9.58, 0, 15, CRGB::Red},
  {7, 7.2, 0, 15, CRGB::Green},
  {6, 6.5, 0, 15, CRGB::Blue},
};

void loop() {
  for (auto b = 0; b < sizeof(balls) / sizeof(*balls); b++) {
    float x = sinf(balls[b].x_speed * millis() / 3000.f) * WIDTH / 2;
    float y = cosf(balls[b].y_speed * millis() / 3000.f) * HEIGHT / 2;
    float radius = balls[b].radius;
    float transition = balls[b].transition;
    float max_sum_squares = radius + transition;
    max_sum_squares *= max_sum_squares;

    for (int screen_y = 0; screen_y < HEIGHT; screen_y++) {
      for (int screen_x = 0; screen_x < WIDTH; screen_x++) {
        float offset_x = x + screen_x - WIDTH / 2;
        float offset_y = y + screen_y - HEIGHT / 2;
        float sum_squares = offset_x * offset_x + offset_y * offset_y;
        if (sum_squares <= max_sum_squares) {
          uint8_t coverage = 255;
          float distance = sqrtf(sum_squares) - radius;
          if (distance >= 0 && distance < transition) {
            float c = distance / transition;
            coverage = 255 - 255.f * c;
          }
          CRGB faded = balls[b].colour;
          faded %= coverage;
          leds[XY(screen_x, screen_y)] += faded;
        }
      }
    }
  }

  FastLED.show();
  FastLED.clear();
  static uint8_t fps_frame = 0;
  if (!++fps_frame)
    Serial.println(FastLED.getFPS());
}