2025

Product Design

Arduino

3D

LED Rainstick

LED Rainstick

LED Rainstick

Describe the scope of the project. Why this issue is important, why it matters to so many people, and the solution / impact of the solution.

Challenge

Design an LED equivalent of a rainstick.

What I Did

Built the LED Rain Stick and circuit, wrote all code, programmed via Arduino.

Design

There are two main methods that I used to capture the visual essence of a real rainstick.

To give the lights a "falling" sensation, I coded a colorwipe with a delay—the lights only begin to extinguish after the entire stick is lit. I also wanted to to give the lights a touch of organic unpredictability—like the beads in a real rainstick—by coding a soft sparkle.

It was essential that the trigger for the sequence be a tilting motion. For this I added a tilt ball switch.

// NeoPixel button cycler with tilt ball switch

// Button on pin 4 cycles through animations

// Tilt switch on pin 0 triggers flip animation

#include <Adafruit_NeoPixel.h>

#ifdef __AVR__

#include <avr/power.h>

#endif

#define BUTTON_PIN 4 // Button for cycling animations

#define TILT_PIN 0 // Tilt ball switch - MUST be different from button and pixel pins

#define PIXEL_PIN 2 // Digital IO pin connected to the NeoPixels

#define PIXEL_COUNT 8 // Number of NeoPixels

Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);

boolean oldButtonState = HIGH;

boolean oldTiltState = HIGH;

int mode = 0; // Currently-active animation mode

boolean wipeDirection = true; // true = forward, false = reverse

void setup() {

pinMode(BUTTON_PIN, INPUT_PULLUP);

pinMode(TILT_PIN, INPUT_PULLUP); // Tilt sensor with pullup

strip.begin();

strip.show();

Serial.begin(9600);

}

void loop() {

// Check button for mode cycling

boolean newButtonState = digitalRead(BUTTON_PIN);


if((newButtonState == LOW) && (oldButtonState == HIGH)) {

delay(20); // Debounce

newButtonState = digitalRead(BUTTON_PIN);

if(newButtonState == LOW) {

if(++mode > 1) mode = 0; // Only 2 modes currently

switch(mode) {

case 0:

colorWipe(strip.Color(255, 243, 218), 50);

break;

case 1:

Sparkle(0, 0, 218, 50);

break;

}

}

}

oldButtonState = newButtonState;


// Check tilt switch separately

boolean newTiltState = digitalRead(TILT_PIN);


if(newTiltState != oldTiltState) {

delay(20); // Debounce

newTiltState = digitalRead(TILT_PIN);

if(newTiltState != oldTiltState) {

// Tilt detected - trigger flip animation

Serial.println("TILT DETECTED!");

tiltFlipAnimation();

}

}

oldTiltState = newTiltState;


delay(10);

}

// Special animation when tube is flipped

void tiltFlipAnimation() {

// 5 LEDs per second = 1000ms / 5 = 200ms per LED

int delayTime = 200;


// Color wipe on - chaser effect with warm white

colorWipeDirectional(strip.Color(255, 243, 218), delayTime, wipeDirection);


// Rain sparkle effect while lit

rainSparkle(strip.Color(255, 243, 218), 1000); // Sparkle for 1 second


// Color wipe off - chaser effect turning off (same direction)

colorWipeDirectional(strip.Color(0, 0, 0), delayTime, wipeDirection);

// Reverse direction for next tilt

wipeDirection = !wipeDirection;

}

void colorWipe(uint32_t color, int wait) {

for(int i=0; i<strip.numPixels(); i++) {

strip.setPixelColor(i, color);

strip.show();

delay(wait);

}

}

void colorWipeDirectional(uint32_t color, int wait, boolean forward) {

if(forward) {

// Wipe from start to end

for(int i=0; i<strip.numPixels(); i++) {

strip.setPixelColor(i, color);

strip.show();

delay(wait);

}

} else {

// Wipe from end to start

for(int i=strip.numPixels()-1; i>=0; i--) {

strip.setPixelColor(i, color);

strip.show();

delay(wait);

}

}

}

void Sparkle(byte red, byte green, byte blue, int SpeedDelay) {

int Pixel = random(PIXEL_COUNT);

strip.setPixelColor(Pixel, strip.Color(red, green, blue));

strip.show();

delay(SpeedDelay);

strip.setPixelColor(Pixel, strip.Color(0, 0, 0));

strip.show();

}

void theaterChase(uint32_t color, int wait) {

for(int a=0; a<10; a++) {

for(int b=0; b<3; b++) {

strip.clear();

for(int c=b; c<strip.numPixels(); c += 3) {

strip.setPixelColor(c, color);

}

strip.show();

delay(wait);

}

}

}

void rainbow(int wait) {

for(long firstPixelHue = 0; firstPixelHue < 3*65536; firstPixelHue += 256) {

for(int i=0; i<strip.numPixels(); i++) {

int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());

strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));

}

strip.show();

delay(wait);

}

}

void theaterChaseRainbow(int wait) {

int firstPixelHue = 0;

for(int a=0; a<30; a++) {

for(int b=0; b<3; b++) {

strip.clear();

for(int c=b; c<strip.numPixels(); c += 3) {

int hue = firstPixelHue + c * 65536L / strip.numPixels();

uint32_t color = strip.gamma32(strip.ColorHSV(hue));

strip.setPixelColor(c, color);

}

strip.show();

delay(wait);

firstPixelHue += 65536 / 90;

}

}

}

void rainSparkle(uint32_t baseColor, int duration) {

unsigned long startTime = millis();


// First, set all LEDs to base color

for(int i=0; i<strip.numPixels(); i++) {

strip.setPixelColor(i, baseColor);

}

strip.show();


// Sparkle random LEDs white

while(millis() - startTime < duration) {

int pixel = random(strip.numPixels());


// Flash white

strip.setPixelColor(pixel, strip.Color(255, 255, 255));

strip.show();

delay(30);


// Return to base color

strip.setPixelColor(pixel, baseColor);

strip.show();

delay(50);

}

}

Exit Thoughts

The tilt ball switch is quite sensitive, and can be triggered accidentally. Doing this again I would try a digital accelerometer instead.

This was a fun project, and I discovered a joy for soldering in the process. I am working on a second iteration that doesn't use Arduino, to be a plug-in sconce or free-leaning lamp.

TinkerCAD

©2026 Josh Kotel

©2026 Josh Kotel