Photos of your completed project (video optional):
This Halloween, I built a “Stray Robot” costume as a special tribute to our family. With my husband and I expecting our “second” son at the end of this year, we wanted to make this Halloween all about our “first son,” our orange tabby cat, Apu, putting him in the main spotlight. The costume is inspired by the video game Stray, which was a perfect theme since the main character is an orange tabby cat in a world populated only by robots. Our costume symbolized our deep love for Apu—we would even turn into robots to make him the star. The construction was a learning process; the head was built with an internal support and a waist belt for stability, and I taught myself how to program Arduino for the LED lights. Although it was a success, if I were to make it again, I would definitely design the head to be a bit smaller.
List (with links) of materials and parts used:
BTF-LIGHTING WS2812B RGB 5050SMD Individually Addressable Digital 16×16 256 Pixels 6.3in x 6.3in LED Matrix Flexible FPCB Dream Full Color Works with SP802E Controller Image Video Text Display DC5V
BTF-LIGHTING WS2812B IC RGB 5050SMD Pure Gold Individual Addressable LED Strip 16.4FT 300LED 60Pixel/m Flexible Full Color IP30 DC5V for DIY Chasing Color Project(No Adapter or Controller)
Cardboard
Acrylic board
Spray paints
Glass paints
Hot glue
Circuit diagram
Arduino code
// Simple demonstration on using an input device to trigger changes on your
// NeoPixels. Wire a momentary push button to connect from ground to a
// digital IO pin. When the button is pressed it will change to a new pixel
// animation. Initial state has all pixels off -- press the button once to
// start the first animation. As written, the button does not interrupt an
// animation in-progress, it works only when idle.
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h> // Required for 16 MHz Adafruit Trinket
#endif
// Digital IO pin connected to the button. This will be driven with a
// pull-up resistor so the switch pulls the pin to ground momentarily.
// On a high -> low transition the button press logic will execute.
#define BUTTON_PIN 2
#define PIXEL_PIN 1 // Digital IO pin connected to the NeoPixels.
//#define PIXEL_COUNT 256 // Number of NeoPixels
#define MATRIX_WIDTH 14
#define MATRIX_HEIGHT 16
#define PIXEL_COUNT (MATRIX_WIDTH * MATRIX_HEIGHT)
//#define BRIGHTNESS 10
// Define the brightness values requested
const uint8_t BACKGROUND_BRIGHTNESS = 100; // For the RGB rainbow part
const uint8_t FACE_BRIGHTNESS = 150; // For the RGB components of the white face
// // Get current button state.
// const uint8_t BACKGROUND_BRIGHTNESS = 20; // For the RGB rainbow part
// const uint8_t FACE_BRIGHTNESS = 35; // For the RGB components of the white face
const byte On_HOLDMap[MATRIX_HEIGHT][MATRIX_WIDTH] = {
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,1,1,1,1,0,0,0,0,0,0},
{0,0,0,0,1,1,1,1,0,0,0,0,0,0}, // Eye
{0,0,0,0,1,1,1,1,0,0,1,0,0,0}, // Eye
{0,0,0,0,0,1,1,0,0,0,1,0,0,0}, // Eye
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,1,1,0,0,0,1,0,0,0}, // Mouth
{0,0,0,0,1,1,1,1,0,0,1,0,0,0}, // Mouth
{0,0,0,0,1,1,1,1,0,0,0,0,0,0},
{0,0,0,0,1,1,1,1,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};
const byte Wink_Map[MATRIX_HEIGHT][MATRIX_WIDTH] = {
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0}, // Eye
{0,0,0,0,0,1,1,0,0,0,1,0,0,0}, // Eye
{0,0,0,0,0,1,1,0,0,0,1,0,0,0}, // Eye
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,1,1,0,0,0,1,0,0,0}, // Mouth
{0,0,0,0,1,1,1,1,0,0,1,0,0,0}, // Mouth
{0,0,0,0,1,1,1,1,0,0,0,0,0,0},
{0,0,0,0,1,1,1,1,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};
const byte no_words_Map[MATRIX_HEIGHT][MATRIX_WIDTH] = {
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0}, // Eye
{0,0,0,0,0,1,1,0,0,0,1,0,0,0}, // Eye
{0,0,0,0,0,1,1,0,0,0,1,0,0,0}, // Eye
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,0,0,0,0,0,1,0,0,0},
{0,0,0,0,0,1,1,0,0,0,1,0,0,0}, // Mouth
{0,0,0,0,0,1,1,0,0,0,1,0,0,0}, // Mouth
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,0,1,1,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};
const byte heartMap[MATRIX_HEIGHT][MATRIX_WIDTH] = {
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,1,1,0,0,0,0,0,0,0,0},
{0,0,0,1,1,1,1,0,0,0,0,0,0,0}, // Bigger Heart Top
{0,0,1,1,1,1,1,1,0,0,0,0,0,0},
{0,0,1,1,1,1,1,1,1,0,0,0,0,0},
{0,0,1,1,1,1,1,1,1,1,0,0,0,0},
{0,0,0,1,1,1,1,1,1,1,1,0,0,0},
{0,0,0,0,1,1,1,1,1,1,1,1,0,0},
{0,0,0,1,1,1,1,1,1,1,1,0,0,0},
{0,0,1,1,1,1,1,1,1,1,0,0,0,0},
{0,0,1,1,1,1,1,1,1,0,0,0,0,0},
{0,0,1,1,1,1,1,1,0,0,0,0,0,0}, // Bigger Heart Tip
{0,0,0,1,1,1,1,0,0,0,0,0,0,0},
{0,0,0,0,1,1,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};
// Declare our NeoPixel strip object:
Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);
// Argument 1 = Number of pixels in NeoPixel strip
// Argument 2 = Arduino pin number (most are valid)
// Argument 3 = Pixel type flags, add together as needed:
// NEO_KHZ800 800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
// NEO_KHZ400 400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
// NEO_GRB Pixels are wired for GRB bitstream (most NeoPixel products)
// NEO_RGB Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
// NEO_RGBW Pixels are wired for RGBW bitstream (NeoPixel RGBW products)
boolean oldState = HIGH;
int mode = 0; // Currently-active animation mode, 0-9
int brightness = 25;
void setup() {
strip.setBrightness(brightness);
pinMode(BUTTON_PIN, INPUT_PULLUP);
strip.begin(); // Initialize NeoPixel strip object (REQUIRED)
strip.show(); // Initialize all pixels to 'off'
}
void loop() {
boolean newState = digitalRead(BUTTON_PIN);
// Check if state changed from high to low (button press).
if((newState == LOW) && (oldState == HIGH)) {
// Short delay to debounce button.
delay(20);
// Check if button is still low after debounce.
newState = digitalRead(BUTTON_PIN);
if(newState == LOW) { // Yes, still low
if(++mode > 9) mode = 0; // Advance to next mode, wrap around after #8
switch(mode) { // Start the new animation...
case 0:
colorWipe(strip.Color( 0, 0, 0), 50); // Black/off
break;
case 1:
drawPattern_on_hold_face();
break;
case 2:
drawPattern_wink_face();
//
break;
case 3:
//colorWipe(strip.Color( 0, 0, 255), 50); // Blue
drawPattern_on_hold_face();
break;
case 4:
//theaterChase(strip.Color(127, 127, 127), 50); // White
//drawCirclePattern();
drawPattern_rainbow_heart_face();
break;
case 5:
drawPattern_on_hold_face();
//theaterChase(strip.Color(127, 0, 0), 50); // Red
break;
case 6:
drawPattern_no_words_face();
//theaterChase(strip.Color( 0, 0, 127), 50); // Blue
break;
case 7:
//rainbow(10);
//drawCirclePattern();
drawPattern_heart_face();
break;
case 8:
theaterChaseRainbow(50);
break;
}
}
}
// Set the last-read button state to the old state.
oldState = newState;
}
void drawPattern_on_hold_face() {
// Iterate over every pixel in the matrix.
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
// Get the 1D index for the pixel at (x,y) coordinates.
int pixelIndex = getPixelIndex(x, y);
// Check the bitmap to see if this pixel is part of the smiley.
if (On_HOLDMap[y][x] == 1) {
// This pixel is part of the face.
uint8_t current_brightness = FACE_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if (x % 2 != 0) {
// Add brightness for the stripe, ensuring it doesn't exceed 255.
current_brightness = min(255, current_brightness + 15);
}
// Set an RGB white color by setting R, G, and B to the same value.
strip.setPixelColor(pixelIndex, strip.Color(current_brightness, current_brightness, current_brightness));
} else {
// This pixel is part of the background.
// To reverse the rainbow direction (green->yellow->red...), we start
// at the green hue (21845) and SUBTRACT a value that increases
// across the diagonal. This makes the hue decrease.
uint16_t hue = 21845 - (((uint32_t)((x - y) + (MATRIX_HEIGHT - 1)) * 65535) / (MATRIX_WIDTH + MATRIX_HEIGHT - 2));
uint8_t current_brightness = BACKGROUND_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if (x % 2 != 0) {
current_brightness += 8; // Make stripes on background brighter
}
// Get the 32-bit color value from HSV (Hue, Saturation, Value/Brightness).
uint32_t color_background = strip.ColorHSV(hue, 255, current_brightness);
strip.setPixelColor(pixelIndex, color_background);
}
}
}
// After setting all the pixel colors, push the data to the strip.
strip.show();
}
void drawPattern_no_words_face() {
// Iterate over every pixel in the matrix.
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
// Get the 1D index for the pixel at (x,y) coordinates.
int pixelIndex = getPixelIndex(x, y);
// Check the bitmap to see if this pixel is part of the smiley.
if (no_words_Map[y][x] == 1) {
// This pixel is part of the face.
uint8_t current_brightness = FACE_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if (x % 2 != 0) {
// Add brightness for the stripe, ensuring it doesn't exceed 255.
current_brightness = min(255, current_brightness + 15);
}
// Set an RGB white color by setting R, G, and B to the same value.
strip.setPixelColor(pixelIndex, strip.Color(current_brightness, current_brightness, current_brightness));
} else {
// This pixel is part of the background.
// To reverse the rainbow direction (green->yellow->red...), we start
// at the green hue (21845) and SUBTRACT a value that increases
// across the diagonal. This makes the hue decrease.
uint16_t hue = 21845 - (((uint32_t)((x - y) + (MATRIX_HEIGHT - 1)) * 65535) / (MATRIX_WIDTH + MATRIX_HEIGHT - 2));
uint8_t current_brightness = BACKGROUND_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if (x % 2 != 0) {
current_brightness += 8; // Make stripes on background brighter
}
// Get the 32-bit color value from HSV (Hue, Saturation, Value/Brightness).
uint32_t color_background = strip.ColorHSV(hue, 255, current_brightness);
strip.setPixelColor(pixelIndex, color_background);
}
}
}
// After setting all the pixel colors, push the data to the strip.
strip.show();
}
void drawPattern_wink_face() {
// Iterate over every pixel in the matrix.
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
// Get the 1D index for the pixel at (x,y) coordinates.
int pixelIndex = getPixelIndex(x, y);
// Check the bitmap to see if this pixel is part of the smiley.
if (Wink_Map[y][x] == 1) {
// This pixel is part of the face.
uint8_t current_brightness = FACE_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if (x % 2 != 0) {
// Add brightness for the stripe, ensuring it doesn't exceed 255.
current_brightness = min(255, current_brightness + 15);
}
// Set an RGB white color by setting R, G, and B to the same value.
strip.setPixelColor(pixelIndex, strip.Color(current_brightness, current_brightness, current_brightness));
} else {
// This pixel is part of the background.
// To reverse the rainbow direction (green->yellow->red...), we start
// at the green hue (21845) and SUBTRACT a value that increases
// across the diagonal. This makes the hue decrease.
uint16_t hue = 21845 - (((uint32_t)((x - y) + (MATRIX_HEIGHT - 1)) * 65535) / (MATRIX_WIDTH + MATRIX_HEIGHT - 2));
uint8_t current_brightness = BACKGROUND_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if (x % 2 != 0) {
current_brightness += 8; // Make stripes on background brighter
}
// Get the 32-bit color value from HSV (Hue, Saturation, Value/Brightness).
uint32_t color_background = strip.ColorHSV(hue, 255, current_brightness);
strip.setPixelColor(pixelIndex, color_background);
}
}
}
// After setting all the pixel colors, push the data to the strip.
strip.show();
}
void drawPattern_heart_face() {
// Iterate over every pixel in the matrix.
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
// Get the 1D index for the pixel at (x,y) coordinates.
int pixelIndex = getPixelIndex(x, y);
// Check the bitmap to see if this pixel is part of the heart.
if (heartMap[y][x] == 1) {
// This pixel is part of the heart.
uint8_t current_brightness = FACE_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if (x % 2 != 0) {
// Add brightness for the stripe, ensuring it doesn't exceed 255.
current_brightness = min(255, current_brightness + 15);
}
// Set an RGB red color.
strip.setPixelColor(pixelIndex, strip.Color(current_brightness, 0, 0));
} else {
// This pixel is part of the background.
// Set a static sky blue color.
uint16_t sky_blue_hue = 36000; // A nice cyan/sky blue hue.
uint8_t current_brightness = BACKGROUND_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if(x == 0 && y == 0){
Serial.print("before: ");
Serial.println(current_brightness);
}
// if (x % 2 == 0) {
// current_brightness += 8; // Make stripes on background brighter
// }
if(x == 0 && y == 0){
Serial.print("after: ");
Serial.println(current_brightness);
}
// Get the 32-bit color value from HSV (Hue, Saturation, Value/Brightness).
uint32_t color_background = strip.ColorHSV(sky_blue_hue, 255, 255);
strip.setPixelColor(pixelIndex, color_background);
}
}
}
// After setting all the pixel colors, push the data to the strip.
strip.show();
}
void drawPattern_rainbow_heart_face() {
// Iterate over every pixel in the matrix.
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
// Get the 1D index for the pixel at (x,y) coordinates.
int pixelIndex = getPixelIndex(x, y);
// Check the bitmap to see if this pixel is part of the smiley.
if (heartMap[y][x] == 1) {
// This pixel is part of the face.
uint8_t current_brightness = FACE_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if (x % 2 != 0) {
// Add brightness for the stripe, ensuring it doesn't exceed 255.
current_brightness = min(255, current_brightness + 15);
}
// Set an RGB white color by setting R, G, and B to the same value.
strip.setPixelColor(pixelIndex, strip.Color(current_brightness, current_brightness, current_brightness));
} else {
// This pixel is part of the background.
// To reverse the rainbow direction (green->yellow->red...), we start
// at the green hue (21845) and SUBTRACT a value that increases
// across the diagonal. This makes the hue decrease.
uint16_t hue = 21845 - (((uint32_t)((x - y) + (MATRIX_HEIGHT - 1)) * 65535) / (MATRIX_WIDTH + MATRIX_HEIGHT - 2));
uint8_t current_brightness = BACKGROUND_BRIGHTNESS;
// Add vertical stripe effect by brightening odd-numbered columns.
if (x % 2 != 0) {
current_brightness += 8; // Make stripes on background brighter
}
// Get the 32-bit color value from HSV (Hue, Saturation, Value/Brightness).
uint32_t color_background = strip.ColorHSV(hue, 255, current_brightness);
strip.setPixelColor(pixelIndex, color_background);
}
}
}
// After setting all the pixel colors, push the data to the strip.
strip.show();
}
void drawCirclePattern() {
// Define the colors we will use.
uint32_t color_circle = strip.Color(255, 255, 255); // White
uint32_t color_background = strip.Color(255, 0, 0); // Green
// Define the center of the matrix (using float for accuracy).
float centerX = (MATRIX_WIDTH - 1) / 2.0f;
float centerY = (MATRIX_HEIGHT - 1) / 2.0f;
// Define the radius of the circle.
float radius = 1.0f;
// This value controls the thickness of the circle's line.
float thickness = 1.0f;
// Iterate over every pixel in the matrix.
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
// Calculate the distance of the current pixel (x, y) from the center.
float distance = sqrt(pow(y - centerY, 2) + pow(x - centerX, 2));
// Get the 1D index for the pixel at (x,y) coordinates.
int pixelIndex = getPixelIndex(x, y);
// Check if the distance is within the circle's line.
if (fabs(distance - radius) < thickness) {
// This pixel is part of the circle, set it to white.
strip.setPixelColor(pixelIndex, color_circle);
} else {
// This pixel is part of the background, set it to green.
strip.setPixelColor(pixelIndex, color_background);
}
}
}
// After setting all the pixel colors in memory, push the data to the strip.
strip.show();
}
int getPixelIndex(int x, int y) {
if (y % 2 == 0) {
// Even row: 0, 2, 4,...
return y * MATRIX_WIDTH + x;
} else {
// Odd row: 1, 3, 5,...
return y * MATRIX_WIDTH + (MATRIX_WIDTH - 1 - x);
}
}
// Fill strip pixels one after another with a color. Strip is NOT cleared
// first; anything there will be covered pixel by pixel. Pass in color
// (as a single 'packed' 32-bit value, which you can get by calling
// strip.Color(red, green, blue) as shown in the loop() function above),
// and a delay time (in milliseconds) between pixels.
void colorWipe(uint32_t color, int wait) {
for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
strip.setPixelColor(i, color); // Set pixel's color (in RAM)
strip.show(); // Update strip to match
delay(wait); // Pause for a moment
}
}
// Theater-marquee-style chasing lights. Pass in a color (32-bit value,
// a la strip.Color(r,g,b) as mentioned above), and a delay time (in ms)
// between frames.
void theaterChase(uint32_t color, int wait) {
for(int a=0; a<10; a++) { // Repeat 10 times...
for(int b=0; b<3; b++) { // 'b' counts from 0 to 2...
strip.clear(); // Set all pixels in RAM to 0 (off)
// 'c' counts up from 'b' to end of strip in steps of 3...
for(int c=b; c<strip.numPixels(); c += 3) {
strip.setPixelColor(c, color); // Set pixel 'c' to value 'color'
}
strip.show(); // Update strip with new contents
delay(wait); // Pause for a moment
}
}
}
// Rainbow cycle along whole strip. Pass delay time (in ms) between frames.
void rainbow(int wait) {
// Hue of first pixel runs 3 complete loops through the color wheel.
// Color wheel has a range of 65536 but it's OK if we roll over, so
// just count from 0 to 3*65536. Adding 256 to firstPixelHue each time
// means we'll make 3*65536/256 = 768 passes through this outer loop:
for(long firstPixelHue = 0; firstPixelHue < 3*65536; firstPixelHue += 256) {
for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
// Offset pixel hue by an amount to make one full revolution of the
// color wheel (range of 65536) along the length of the strip
// (strip.numPixels() steps):
int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());
// strip.ColorHSV() can take 1 or 3 arguments: a hue (0 to 65535) or
// optionally add saturation and value (brightness) (each 0 to 255).
// Here we're using just the single-argument hue variant. The result
// is passed through strip.gamma32() to provide 'truer' colors
// before assigning to each pixel:
strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
}
strip.show(); // Update strip with new contents
delay(wait); // Pause for a moment
}
}
// Rainbow-enhanced theater marquee. Pass delay time (in ms) between frames.
void theaterChaseRainbow(int wait) {
int firstPixelHue = 0; // First pixel starts at red (hue 0)
for(int a=0; a<30; a++) { // Repeat 30 times...
for(int b=0; b<3; b++) { // 'b' counts from 0 to 2...
strip.clear(); // Set all pixels in RAM to 0 (off)
// 'c' counts up from 'b' to end of strip in increments of 3...
for(int c=b; c<strip.numPixels(); c += 3) {
// hue of pixel 'c' is offset by an amount to make one full
// revolution of the color wheel (range 65536) along the length
// of the strip (strip.numPixels() steps):
int hue = firstPixelHue + c * 65536L / strip.numPixels();
uint32_t color = strip.gamma32(strip.ColorHSV(hue)); // hue -> RGB
strip.setPixelColor(c, color); // Set pixel 'c' to value 'color'
}
strip.setBrightness(10);
strip.show(); // Update strip with new contents
delay(wait); // Pause for a moment
firstPixelHue += 65536 / 90; // One cycle of color wheel over 90 frames
}
}
}