/*
DoorArch.ino
Code by: Gray Mack. Some pieces generated by Cursor AI
License: https://www.gnu.org/licenses/gpl-3.0.en.html
Created: 12/1/2025
One Line Description: A door archway of christmas light patterns
Why I made this: For the love of the blinky
Board: Nano v3
Select Board: Arduino Nano
alternative:
Board: Wemos D1 ESP32
Select Board: LOLIN(WEMOS) D1 R2 & mini
alternative:
Board: Seeeduino XIAO samd21
Select Board: Seeeduino XIAO
Other Hardware: 133 pixels, a pot to adjust speed, 2 switches to set color scheme, PIR sensor
Detailed Description:
I have an arduino driving 133 ws2812 pixels in an arch.
Utilize fastled library and millis techniques.
There are "Lights" with position and velocity) that fire
randomly from one end or the other at a random velocity.
The lights are either red or green can have a tail of light that fade out.
For the first 56 pixels there is a negative constant gravity on the
light which changes it's velocity and for the last 52 the gravity
is positive acting. The center 29 pixels do not experience gravity.
When a light hits the opposite end, it will bounce in the
opposite direction until the velocity is below a small threshold
such that the light can't move at which time it is fired up
again randomly. There will be different light styles and color schemes.
Rev History:
*
* 12/1/2025 initial code creation, add several patterns
* 12/5/2025 code complete
*/
// ----[ configuration ]----
#define CONSOLE_BAUD 115200
#define PROGRAM_VERSION "Door Arch LED Controller v1.0"
// Light configuration
#define MAX_LIGHTS 12
#define MAX_TAIL_LENGTH 10
#define MIN_TAIL_LENGTH 0
const unsigned long MIN_FRAME_DELAY_MS = 3;
const unsigned long MAX_FRAME_DELAY_MS = 120;
const bool UseMotionSensor = true;
const long MotionSenseHoldTimeMs = 20000L;
// ----[ included libraries ]----
#include <FastLED.h>
// ----[ pin definitions ]----
#define SWITCH1_PIN 2
#define SWITCH2_PIN 3
#define MOTION_SENSOR_PIN 5
#define DATA_PIN 6
#define SPEED_CONTROL_PIN A0
#define LED_PIN 13
// ----[ constants ]----
// LED configuration
#define NUM_LEDS 141
#define LAST_LED (NUM_LEDS - 1)
#define CENTER_LED (NUM_LEDS / 2)
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
#define BRIGHTNESS 255
// Physics zones
#define ZONE_1_START 0
#define ZONE_1_END 55 // First 52 pixels (0-51)
#define ZONE_2_START 56
#define ZONE_2_END 84 // Center 29 pixels (52-80)
#define ZONE_3_START 85
#define ZONE_3_END 140 // Last 52 pixels (81-132)
//color schemes
#define COLORS_CHRISTMAS 0
#define COLORS_HALLOWEEN 1
#define COLORS_VALENTINES 2
#define COLORS_OTHER 3
// Velocity range for random firing
const float MIN_VELOCITY = 0.1; //0.5;
const float MAX_VELOCITY = 1.0;
// Gravity constants (pixels per frame^2)
const float GRAVITY = -0.0015; // pulls down
const float BOUNCE_FRICTION_LOSS = 0.90;
// Physics thresholds
const float MIN_VELOCITY_THRESHOLD = 0.1; // Velocity below this stops bouncing
const float BOUNCE_DAMPING = 0.85; // Velocity reduction on bounce
const bool Create = true;
const bool Run = false;
const bool Mirror = true;
const bool NoMirror = false;
// ----[ types ]----
enum LightType : uint8_t {
Dead, // ready to be reborn
Twinkle, // shows up for one interval as a bright sparkle
Shooter, // shoot from bottom left or right, moves at a steady velocity
ShooterBounce, // shoots from bottom left or right, has gravity and bounces until at either end and velocity below a threshold
DualDropBounce, // drops from center down both sides with bounce
Pop // shows at a random spot and gets bigger from that point and fades out
};
// Light structure
struct Light {
float position; // Current position (0.0 to NUM_LEDS-1)
float velocity; // Current velocity (pixels per frame)
LightType lightType; // Whether this light is active
uint16_t animationCounter; // used by some light types
CRGB color; // primary or secondary color
uint8_t tailLength; // Tail length (0-5)
uint8_t tail[MAX_TAIL_LENGTH];
};
//----[ prototypes ]----
void setup();
void loop();
void ReadSwitches();
void ReadSpeed();
bool InBounds(int16_t i);
void AnimateLights();
void CreateLight(uint8_t i);
void TwinkleAnim(Light <, bool create);
void ShooterAnim(Light <, bool create);
void ShooterBounceAnim(Light <, bool create);
void DualDropAnim(Light <, bool create);
void PopAnim(Light <, bool create);
void MotionSenseModeAnim(bool create);
void MoveTail(Light <);
bool DrawWithFadingTail(Light <, bool withMirror);
bool DrawWithoutTail(Light <, bool withMirror);
CRGB GetRandomBrightColor();
void LedFadeTest();
//----[ globals ]----
CRGB leds[NUM_LEDS];
Light lights[MAX_LIGHTS];
CRGB primaryColor = CRGB::Red;
CRGB secondaryColor = CRGB::Green;
uint8_t usersColorScheme = 0;
bool MotionSenseModeActive = false;
unsigned long MotionSenseModeStartTimeMillis = 0;
unsigned long lastFrameTimeMillis = 0;
unsigned long frameTimeMs = 16; // start at ~60 FPS (16ms per frame) but adjustable by the speed control pot
unsigned long lastButtonCheckTimeMillis;
const unsigned long BUTTON_CHECK_TIME_MS = 100;
// ----[ code ]----------------------------------
// ----[ setup ]-------
void setup()
{
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);
Serial.begin(CONSOLE_BAUD);
delay(1000);
Serial.println(PROGRAM_VERSION);
digitalWrite(LED_PIN, LOW);
// Initialize FastLED
FastLED.addLeds<LED_TYPE, DATA_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
FastLED.setBrightness(BRIGHTNESS);
FastLED.setMaxPowerInVoltsAndMilliamps(5,1000);
//LedFadeTest();
// Initialize all lights as inactive
for (int i = 0; i < MAX_LIGHTS; i++)
{
lights[i].lightType = Dead;
}
pinMode(SWITCH1_PIN, INPUT_PULLUP);
pinMode(SWITCH2_PIN, INPUT_PULLUP);
pinMode(SPEED_CONTROL_PIN, INPUT);
Serial.print("Total LEDs: "); Serial.println(NUM_LEDS);
Serial.print("Zone 1 (gravity -): "); Serial.println(ZONE_1_END);
Serial.print("Zone 2 (0 gravity): "); Serial.print(ZONE_2_START); Serial.print(", "); Serial.println(ZONE_2_END);
Serial.print("Zone 3 (gravity +): "); Serial.print(ZONE_3_START); Serial.print(", "); Serial.println(ZONE_3_END);
}
// ----[ loop ]-------
void loop()
{
unsigned long currentTimeMillis = millis();
if (currentTimeMillis - lastFrameTimeMillis > frameTimeMs)
{
lastFrameTimeMillis = currentTimeMillis;
FastLED.clear();
AnimateLights();
FastLED.show();
}
if(currentTimeMillis - lastButtonCheckTimeMillis > BUTTON_CHECK_TIME_MS)
{
lastButtonCheckTimeMillis = currentTimeMillis;
ReadSwitches();
ReadSpeed();
}
if(UseMotionSensor && !MotionSenseModeActive && digitalRead(MOTION_SENSOR_PIN) == HIGH)
{
MotionSenseModeActive = true;
MotionSenseModeStartTimeMillis = currentTimeMillis;
MotionSenseModeAnim(Create);
}
if(MotionSenseModeActive && currentTimeMillis - MotionSenseModeStartTimeMillis > MotionSenseHoldTimeMs)
MotionSenseModeActive = false;
}
// ----[ routines ]-------
void ReadSwitches()
{
uint8_t newColorScheme = (digitalRead(SWITCH2_PIN) == LOW ? 2 : 0) +
(digitalRead(SWITCH1_PIN) == LOW ? 1 : 0);
if(newColorScheme == usersColorScheme) return;
usersColorScheme = newColorScheme;
switch(usersColorScheme) {
case COLORS_CHRISTMAS:
Serial.println("Christmas colors");
primaryColor = CRGB::Red;
secondaryColor = CRGB::Green;
break;
case COLORS_HALLOWEEN:
Serial.println("Halloween colors");
primaryColor = CRGB::Orange;
secondaryColor = CRGB::Purple;
break;
case COLORS_VALENTINES:
Serial.println("Valentines colors");
primaryColor = CRGB::Red;
secondaryColor = CRGB::Pink;
break;
case COLORS_OTHER: default:
Serial.println("Other colors");
primaryColor = CRGB::Yellow;
secondaryColor = CRGB::Cyan;
break;
}
// Initialize all lights as inactive
for (int i = 0; i < MAX_LIGHTS; i++)
{
lights[i].lightType = Dead;
}
}
void ReadSpeed()
{
uint16_t speedDelay = analogRead(SPEED_CONTROL_PIN);
frameTimeMs = map(speedDelay, 0, 1023, MIN_FRAME_DELAY_MS, MAX_FRAME_DELAY_MS);
}
bool InBounds(int16_t i) { return i >=0 && i < NUM_LEDS; }
void AnimateLights()
{
if(MotionSenseModeActive)
{
MotionSenseModeAnim(Run);
return;
}
for (int i = 0; i < MAX_LIGHTS; i++)
{
switch(lights[i].lightType)
{
case Dead:
if(usersColorScheme == COLORS_OTHER) {
primaryColor = GetRandomBrightColor();
secondaryColor = GetRandomBrightColor();
}
CreateLight(i);
break;
case Twinkle:
TwinkleAnim(lights[i], Run);
break;
case Shooter:
ShooterAnim(lights[i], Run);
break;
case ShooterBounce:
ShooterBounceAnim(lights[i], Run);
break;
case DualDropBounce:
DualDropAnim(lights[i], Run);
break;
case Pop:
PopAnim(lights[i], Run);
break;
}
}
digitalWrite(LED_PIN, LOW);
}
void CreateLight(uint8_t i)
{
if(random(40) == 0) TwinkleAnim(lights[i], Create);
else
if(random(40) == 0) ShooterAnim(lights[i], Create);
else
if(random(40) == 0) DualDropAnim(lights[i], Create);
else
if(random(40) == 0) PopAnim(lights[i], Create);
else
if(random(40) == 0) ShooterBounceAnim(lights[i], Create);
if(lights[i].lightType != Dead)
digitalWrite(LED_PIN, HIGH);
}
// brief single pixel light
void TwinkleAnim(Light <, bool create)
{
if(create)
{
Serial.println("T");
lt.lightType = Twinkle;
lt.tail[0] = random(NUM_LEDS);
lt.animationCounter = 0;
lt.color = GetRandomBrightColor();
}
leds[lt.tail[0]] = lt.color;
if(lt.animationCounter++ > 1)
lt.lightType = LightType::Dead;
}
// A shoot from one end that moves at constant velocity to other end
void ShooterAnim(Light <, bool create)
{
if(create)
{
Serial.println("S");
lt.lightType = Shooter;
bool startFromZero = random(2) == 0;
lt.position = startFromZero ? 0 : LAST_LED;
// Random velocity (positive if from left, negative if from right)
float vel = random(MIN_VELOCITY * 100, MAX_VELOCITY * 100) / 100.0;
lt.velocity = startFromZero ? vel : -vel;
lt.tailLength = random(MAX_TAIL_LENGTH);
lt.color = random(2) == 0 ? primaryColor : secondaryColor;
}
else
{
lt.position += lt.velocity;
}
int16_t pos = (int16_t)round(lt.position);
if(pos < 0) pos = 255; // out of bounds to soon die
MoveTail(lt);
lt.tail[0] = (uint8_t)pos;
bool visible = DrawWithFadingTail(lt, NoMirror);
if(!visible) lt.lightType = Dead;
}
void ShooterBounceAnim(Light <, bool create)
{
if(create)
{
Serial.println("B");
lt.lightType = ShooterBounce;
bool startFromZero = random(2) == 0;
lt.position = startFromZero ? 0 : LAST_LED;
// Random velocity (positive if from left, negative if from right)
float vel = random(MIN_VELOCITY * 100, MAX_VELOCITY * 100) / 100.0;
lt.velocity = startFromZero ? vel : -vel;
lt.tailLength = random(MAX_TAIL_LENGTH);
lt.color = random(2) == 0 ? primaryColor : secondaryColor;
}
else
{
lt.position += lt.velocity;
}
int16_t pos = (int16_t)round(lt.position);
// Determine which zone the light is in and apply gravity
float gravity;
if (pos <= ZONE_1_END)
gravity = GRAVITY;
else if (pos >= ZONE_3_START)
gravity = -GRAVITY;
else // at the top, no gravity
gravity = 0.0f;
lt.velocity += gravity;
// ensure it's not too slow at the top
if(pos >= ZONE_2_START && pos <= ZONE_2_END && abs(lt.velocity) < 0.02)
{
lt.velocity = (lt.velocity < 0) ? -0.02 : 0.02;
}
// look for a bounce at either end
if(pos < 0)
{
pos = 0;
lt.velocity = abs(lt.velocity) * BOUNCE_FRICTION_LOSS;
if(abs(lt.velocity) < 0.03) lt.lightType = Dead;
}
if(pos >= LAST_LED)
{
pos = LAST_LED;
lt.velocity = - (abs(lt.velocity) * BOUNCE_FRICTION_LOSS);
if(abs(lt.velocity) < 0.03) lt.lightType = Dead;
}
MoveTail(lt);
lt.tail[0] = (uint8_t)pos;
DrawWithFadingTail(lt, NoMirror);
//DrawWithoutTail(lt, NoMirror);
}
void DualDropAnim(Light <, bool create)
{
if(create)
{
Serial.println("D");
lt.lightType = DualDropBounce;
lt.position = NUM_LEDS / 2;
// Random velocity (positive if from left, negative if from right)
float vel = random(MIN_VELOCITY * 100, MAX_VELOCITY * 100) / 100.0 / 9;
lt.velocity = -vel;
lt.tailLength = random(MAX_TAIL_LENGTH);
lt.color = random(2) == 0 ? primaryColor : secondaryColor;
}
else
{
lt.velocity += GRAVITY;
if(lt.velocity > MAX_VELOCITY) lt.velocity = MAX_VELOCITY;
if(lt.velocity < -MAX_VELOCITY) lt.velocity = -MAX_VELOCITY;
lt.position += lt.velocity;
}
MoveTail(lt);
int16_t pos = (int16_t)round(lt.position);
if(pos < 0) pos = 255; // out of bounds to soon die
lt.tail[0] = (uint8_t)pos;
bool visible = DrawWithFadingTail(lt, Mirror);
if(!visible) lt.lightType = Dead;
}
// starts at a point and radiates outward, then fades
void PopAnim(Light <, bool create)
{
if(create)
{
Serial.println("P");
lt.lightType = Pop;
lt.animationCounter = 0;
lt.tailLength = random(MAX_TAIL_LENGTH);
lt.tail[0] = random(NUM_LEDS);
lt.color = random(2) == 0 ? primaryColor : secondaryColor;
}
if(usersColorScheme == COLORS_OTHER) {
lt.color = GetRandomBrightColor();
}
for(uint8_t t = 0; t<lt.tailLength; t++)
{
uint8_t pos1 = lt.tail[0]+t;
uint8_t pos2 = lt.tail[0]-t;
CRGB col = lt.color;
if(lt.animationCounter > lt.tailLength)
{
int fadeFactor = 0;//noFade
if(lt.animationCounter > lt.tailLength) fadeFactor = ((lt.animationCounter - lt.tailLength)+t) * 40;
fadeFactor = min(fadeFactor, 255);
col.fadeToBlackBy((uint8_t)fadeFactor); // 0=nofade 255=black
}
if(InBounds(pos1)) leds[pos1] = col;
if(InBounds(pos2)) leds[pos2] = col;
if(t >= lt.animationCounter) break;
}
lt.animationCounter++;
if(lt.animationCounter > 4*lt.tailLength) lt.lightType = Dead;
}
void MotionSenseModeAnim(bool create)
{
static uint8_t animationCounter = 0;
if(create)
{
Serial.println("M");
animationCounter = 0;
}
if(animationCounter <= CENTER_LED)
{
leds[animationCounter] = CRGB::Blue;
leds[LAST_LED-animationCounter] = CRGB::Blue;
}
for(uint8_t i=ZONE_1_START; (i<=ZONE_1_END) && (i<animationCounter); i+=6)
{
leds[i] = CRGB::Red;
leds[LAST_LED-i] = CRGB::Red;
}
if(animationCounter > CENTER_LED)
{
uint8_t counter2 = min(animationCounter-CENTER_LED-1, 6); // 0 to 6
for(uint8_t i=0; i<counter2; i++)
{
leds[CENTER_LED-i] = CRGB::White;
leds[LAST_LED-(CENTER_LED-i)] = CRGB::White;
}
}
if(animationCounter<255) animationCounter++;
}
// move the tail down but don't change tail[0]
void MoveTail(Light <)
{
for(uint8_t t=MAX_TAIL_LENGTH-1; t>0; t--)
{
lt.tail[t] = lt.tail[t-1];
}
}
bool DrawWithFadingTail(Light <, bool withMirror)
{
bool visible = false;
for(int8_t t=lt.tailLength-1; t>=0; t--)
{
uint8_t pos = lt.tail[t];
if(InBounds(pos))
{
visible = true;
leds[pos] = lt.color;
// Calculate and apply fade factor (1.0 at head, 0.0 at tail end)
float fadeFactor = 1.0f - ((float)t / (float)(lt.tailLength + 1));
leds[pos].fadeToBlackBy((uint8_t)(255 * (1.0 - fadeFactor))); // 0=nofade 255=black
if(withMirror) leds[LAST_LED-pos] = leds[pos];
}
}
return visible;
}
bool DrawWithoutTail(Light <, bool withMirror)
{
uint8_t pos = lt.tail[0];
if(InBounds(pos))
{
leds[pos] = lt.color;
if(withMirror) leds[LAST_LED-pos] = leds[pos];
return true;
}
return false;
}
// Random bright color, avoids white by keeping saturation maxed
CRGB GetRandomBrightColor()
{
const uint8_t brightness = 150; // or 255 if you want max brightness
CHSV hsv(random8(), 255, brightness);
return hsv; // FastLED will convert CHSV -> CRGB automatically
}
void LedFadeTest()
{
for(int i=0; i<26; i++)
{
leds[i] = CRGB::Red;
leds[i].fadeToBlackBy(i*10);
}
leds[26] = CRGB::White;
FastLED.show();
delay(60000L);
}