// by <[email protected]>
// License: Creative Commons CC0
//commented by [email protected]
#include <Arduino.h>
#include <FastLED.h>
#include <colorutils.h>
enum XY_matrix_config {
SERPENTINE = 1,
ROWMAJOR = 2,
FLIPMAJOR = 4,
FLIPMINOR = 8
};
#define BRIGHTNESS 255 // maximum brightness is 255 on real leds this should probably be about 100
// the purpose of this application is to share the brightness between the
// four closest lights to the calculated position of each leaper
// this gives the feel of a display with many more pixels
// the technoligy is called anti-alliasing
#define LED_PIN 3
#define COLOR_ORDER GRB
#define CHIPSET WS2812B
#define kMatrixWidth 40
#define kMatrixHeight 12
#define XY_MATRIX (SERPENTINE | ROWMAJOR | FLIPMINOR)
#define MS_GOAL 10 // to try maintain 1000 / 10ms == 100 FPS / set to 1000 during testing
#define NUM_LEAPERS 8 // the number of leapers you are displaying
#define GRAVITY 10
#define SETTLED_THRESHOLD 48
#define WALL_FRICTION 248 // 255 is no friction
#define DRAG 240 // 255 is no wind resistance
#define SERIAL_UI 0 // allow output to the serial console. If 1 allow if 0 stops all output
#define TEST_UI 0 // allow output to the serial console for testing
#define NUM_LEDS ((kMatrixWidth) * (kMatrixHeight))
///////////////////////////////////////////////////////////////////////
// further reading here on FastLED:
// https://fastled.io/docs/index.html#autotoc_md0
// reserve memory space for each physical pixel of the display
CRGB leds[NUM_LEDS + 1]; // 1 extra for XY() to use when out-of-bounds
// further reading here on FastLED:
// https://fastled.io/docs/group___predefined_palettes.html#gae6b55fb01ce77682c14e4f6989109d69
CRGBPalette16 currentPalette = RainbowColors_p;
uint32_t last_millis = 0;
// typedef struct creates a defines a structure with called LEAPER with four member variables
// x is the location of an object along the x axis, y is the location of an object along the y axis
// if x and y are negative or if they have values greater than the size of the display, then the object is not visible
// xd is the speed the object is traveling in the x direction, this can be a positive or negative number
// yd is the speed the object is traveling in the y direction, this can be a positive or negative number
typedef struct {
int16_t x, y, xd, yd;
// uint8_t state;
} Leaper;
// NOTE: x, y, xd, yd are all 16 bit integers meaning you can place an object in any location on a grid of
// 65536 x 65536 pixels. Obviously you will not have a display with that many lights. This program
// works out where on you display of, for example, 48 x 12 lights your object sits.
// Your object will be somewhere between four actual lights. The program will work out how bright to light each of
// those four lights to reflect the location of the object in a grid of 256 x 256 between each of the four lights.
// So if the location of your object is nearer the top left hand light, it will light more of that light and less
// of the other four. The sum of brightness of the four lights will add up to the total brightness of the object you
// want to display. If the object location is slowly moved, the program will redistribute the total brightness
// between four new lights. This may be the same four lights or it might move into an area between adjacent lights
// or an entirely different set of four lights.
// Leaper leapers[NUM_LEAPERS]; creates an array to hold variable x,y,xd & yd for each object
Leaper leapers[NUM_LEAPERS];
// restart_leaper and move_leaper will not work unless declared in this extern "C" section
// I am still not clear why this should be commenting this out causes errors
// more googling required
extern "C" {
void restart_leaper(Leaper * lpr);
void move_leaper(Leaper * lpr);
}
void setup() {
FastLED.addLeds<CHIPSET, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
// the code above tells the FastLED code what your display is made up of
// It also resrves a memory location for each pixel in the display.
FastLED.setCorrection(UncorrectedColor); //optional but turns off any previously set color correction
FastLED.setTemperature(UncorrectedTemperature); //optional but turns of any previously set temperature correction
FastLED.setDither(DISABLE_DITHER); // stops the lights flickering. see this for explanation: https://github.com/FastLED/FastLED/wiki/FastLED-Temporal-Dithering
FastLED.setBrightness(BRIGHTNESS); // set the maximum brightness of the whole display
pinMode(LED_BUILTIN, OUTPUT); // configure the pin used to send data as an output pin
// if SERIAL_UI is set to 1 at the top of the definitions then turn on the serial output with a begin statement
if (SERIAL_UI) {
// Serial.begin(250000); // use this on a physical board. All three lines will need to be uncommented
// while (!Serial) {
//; // wait for serial port to connect. Needed for native USB
//}
}
if (TEST_UI) {
if (!SERIAL) {
// Serial.begin(9600); // use this on a virtual board in the emulator
}
}
Serial.begin(9600); // use this on a virtual board in the emulator
// The for loop defines a starting x and a y location for each object and the direction and speed they will be traveling at.
for (uint8_t lpr = 0; lpr < NUM_LEAPERS; lpr++) {
restart_leaper(&leapers[lpr]); // for each object, run the restart_leaper code.
// the restart_leaper code defines what direction and speed each object will travel in.
// the two lines below define where each object will start from
// read the comments by the restart_leaper function to undestrand more how information is handled.
leapers[lpr].x = random8() * kMatrixWidth;
leapers[lpr].y = random8() * kMatrixHeight;
if (TEST_UI) {
Serial.print("Setup leapers[lpr].x: ");
Serial.print(leapers[lpr].x);
Serial.print(", Setup leapers[lpr].y: ");
Serial.print(leapers[lpr].y);
Serial.print(", Setup lpr: ");
Serial.print(lpr);
Serial.print(", Setup &leapers[lpr]: ");
Serial.println(uint32_t(&leapers[lpr]), HEX);
}
}
}
// the loop will run the code repeatedly for every frame of the animation
// it will clear the screen and draw each object in a new location to create the optical illusion of movement.
// if the program takes 10ms to create each screen it will create 100 screens or frames a second so your brain
// can't see the frames being drawn and you interpret the consecutive drawings as movement
void loop() {
// for each frame, reset every pixel by clearing the screen
FastLED.clear();
// alternatively by uncommenting the line below and using fadeToBlack,
// you can clear the display by a fraction each frame to create light trails
// if you do uncomment this line you must comment out the FastLED.clear() above for th fading to work
// fadeToBlackBy(leds, NUM_LEDS, 32);
for (uint8_t lpr = 0; lpr < NUM_LEAPERS; lpr++) {
// run the code to move the objects to their next location for each frame
if (TEST_UI) {
Serial.print("lpr: ");
Serial.print(lpr);
Serial.print(", &leapers[lpr]: ");
Serial.println(uint32_t(&leapers[lpr]), HEX);
}
// Using information from my project "Understanding pointers in Arduino", a pointer is first defined
// and then pointed at the memory location of a variable
// e.g.:
// a variable is initiated for example:
// int i;
// a value is assigned to i:
// i = 12;
// a pointer is defined:
// int *p;
// The pointer is 'pointed' at the memory location of variable i:
// p = &i;
// The value of variable i is updated by updating the memory location of pointer p:
// *p = 15;
//
// Below however, the memory location of an array of type struct is sent to the move_leaper function.
// At this stage, a pointer has not been declared; this occurs in the function itself further down:
// 'void move_leaper(Leaper * lpr) {...}', which is declared as void and as such should not return
// anything, however it also has an internal pointer defined as Type 'Leaper' which is a structure
// defined with multiple variables. This means that the memory location sent to the function will
// have its values updated directly.
// By sending memory locations and using pointers in this way can hugely speed up processing.
move_leaper(&leapers[lpr]);
// FastLED functionality is beyond the scope of these comments
// further reading here on FastLED:
// https://fastled.io/docs/index.html#autotoc_md0
// https://fastled.io/docs/struct_c_r_g_b.html
// Apply a color to each moving object
CRGB rgb = ColorFromPalette(currentPalette, lpr * (255 / NUM_LEAPERS), 255, LINEARBLEND);
if (kMatrixWidth > 1)
wu_pixel(leapers[lpr].x, leapers[lpr].y, &rgb);
else
wu_pixel1d(leapers[lpr].y, &rgb);
}
// cap the frame rate and indicate idle time via the built-in LED
uint32_t frame_time = millis() - last_millis;
int16_t pause = MS_GOAL - frame_time;
// if SERIAL_UI is set to 1 at the top of the definitions then output any delays caused in writing a frame
if (pause < 0 && SERIAL_UI) {
Serial.print(-pause);
Serial.println("ms late");
}
digitalWrite(LED_BUILTIN, HIGH);
if (pause > 0) delay(pause);
digitalWrite(LED_BUILTIN, LOW);
last_millis = millis();
FastLED.show();
}
void restart_leaper(Leaper * lpr) {
// leap up and to the side with some random component
lpr->xd = random8() + 32;
lpr->yd = random8() + 512;
// for variety, sometimes go 50% faster
// random8 generates a randmo 8 bit number from 0 (00000000) to 255 (11111111)
// This code creates a fast moving ball once in every 21.25 bounces or 4 out of every 85 times.
// or alternatively 4.7% of the time.
if (random8() < 12) {
// '>>' and '<<' represent bit shifting which is a rapid form of division or multiplication.
// '>> 1' divides by 2 but discards any fractions, much like rounding down e.g. 3 >> 1 = 1
// '<< 1' doubles a value but if your starting number is greater than 127 doubling starts
// to fail as you will lose one or more significant numbers
// e.g. 127 << 1 = is 1111 1111 binary and fits in 8 bits which is 127 * 2 = 254
// but 128 << 1 = is 1 0000 0000 binary, the most significant number '1' is lost
// so the result is 0 if using 8 bits
// Please note this method is used as it is very fast.
lpr->xd += lpr->xd << 1; // doubles the x velocity of some leapers
lpr->yd += lpr->yd << 1; // doubles the y velocity of some leapers
}
// leap towards the centre of the screen
if (lpr->x > (kMatrixWidth / 2 * 256)) {
lpr->xd = -lpr->xd;
}
}
void move_leaper(Leaper * lpr) {
// outside this function, lpr was a number from 0 to NUM_LEAPER
// 'lpr' inside the function now represents the memory location of
// Serial.print("lpr: ");
// Serial.print(uint32_t(lpr), HEX);
// Serial.print(", x: ");
// Serial.print(lpr->x);
// Serial.print(", y: ");
// Serial.print(lpr->y);
// Serial.print(", xd: ");
// Serial.print(lpr->xd);
// Serial.print(", yd: ");
// Serial.println(lpr->yd);
// add the xd & xy velocities to the position variables x and y
// this will be the new location of the objects
lpr->x += lpr->xd;
lpr->y += lpr->yd;
// if the object is going out of frame redirect it
// bounce off the floor and ceiling?
if (lpr->y < 0 || lpr->y >= ((kMatrixHeight - 1) << 8)) {
lpr->xd = ((int32_t) lpr->xd * WALL_FRICTION) >> 8;
lpr->yd = ((int32_t) - lpr->yd * WALL_FRICTION) >> 8;
if (lpr->y < 0) lpr->y = -lpr->y;
// settled on the floor?
if (lpr->y <= SETTLED_THRESHOLD && abs(lpr->yd) <= SETTLED_THRESHOLD) {
restart_leaper(lpr);
}
}
// if the object is going off the sids of the frame redirect it back into frame
// bounce off the sides of the screen?
if (lpr->x <= 0 || lpr->x >= (kMatrixWidth - 1) << 8) {
lpr->xd = ((int32_t) - lpr->xd * WALL_FRICTION) >> 8;
lpr->yd = ((int32_t) lpr->yd * WALL_FRICTION) >> 8;
if (lpr->x <= 0) {
lpr->x = -lpr->x;
} else {
lpr->x = ((2 * kMatrixWidth - 1) << 8) - lpr->x;
}
}
// for every object apply some gravity to ydelta to slowly bring the ball back down to rest
// gravity
lpr->yd -= GRAVITY;
// slow the objects down due to the drag caused by the atmosphere
// viscosity, done badly (according to sutaburosu)
// uint32_t speed2 = lpr->xd * lpr->xd + lpr->yd * lpr->yd;
lpr->xd = ((int32_t) lpr->xd * DRAG) >> 8;
lpr->yd = ((int32_t) lpr->yd * DRAG) >> 8;
}
// x and y are 24.8 fixed point
// The idea came from Xiaolin Wu.
void wu_pixel(uint32_t x, uint32_t y, CRGB * col) {
// Serial.print("x: ");
// Serial.print(x);
// Serial.print(", y: ");
// Serial.println(y);
// extract the fractional parts and derive their inverses
uint8_t xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy;
// calculate the intensities for each affected pixel
#define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8))
uint8_t wu[4] = {WU_WEIGHT(ix, iy), WU_WEIGHT(xx, iy),
WU_WEIGHT(ix, yy), WU_WEIGHT(xx, yy)
};
// multiply the intensities by the colour, and saturating-add them to the pixels
for (uint8_t i = 0; i < 4; i++) {
uint16_t xy = XY((x >> 8) + (i & 1), (y >> 8) + ((i >> 1) & 1));
leds[xy].r = qadd8(leds[xy].r, col->r * wu[i] >> 8);
leds[xy].g = qadd8(leds[xy].g, col->g * wu[i] >> 8);
leds[xy].b = qadd8(leds[xy].b, col->b * wu[i] >> 8);
}
}
// note that this will write to NUM_LEDS + 1
void wu_pixel1d(uint32_t y, CRGB * col) {
// extract the fractional parts and derive their inverses
uint8_t yy = y & 0xff, iy = 255 - yy;
y = y >> 8;
uint8_t wu[2] = {iy, yy};
// multiply the intensities by the colour, and saturating-add them to the pixels
for (uint8_t i = 0; i < 2; i++) {
leds[y].r = qadd8(leds[y].r, col->r * wu[i] >> 8);
leds[y].g = qadd8(leds[y].g, col->g * wu[i] >> 8);
leds[y].b = qadd8(leds[y].b, col->b * wu[i] >> 8);
y++;
}
}
uint16_t XY(uint8_t x, uint8_t y) {
uint8_t major, minor, sz_major, sz_minor;
if (x >= kMatrixWidth || y >= kMatrixHeight)
return NUM_LEDS;
if (XY_MATRIX & ROWMAJOR)
major = x, minor = y, sz_major = kMatrixWidth, sz_minor = kMatrixHeight;
else
major = y, minor = x, sz_major = kMatrixHeight, sz_minor = kMatrixWidth;
if ((XY_MATRIX & FLIPMAJOR) ^ (minor & 1 && (XY_MATRIX & SERPENTINE)))
major = sz_major - 1 - major;
if (XY_MATRIX & FLIPMINOR)
minor = sz_minor - 1 - minor;
return (uint16_t) minor * sz_major + major;
}