DIY SERVO LIGHT PAINTING MACHINE
Two servo motors sweep a glowing LED through the dark while a camera captures the light trail — your robot draws glowing art in thin air! 💡🎨
🔬 HOW LIGHT PAINTING WORKS
Light painting is a photography technique where you keep the camera shutter open in a dark room while a light source moves. Every position the light visits gets recorded permanently in the photo — even though you can't see the full picture until the shutter closes!
By attaching the LED to two servo motors forming an X–Y gantry, the Arduino can move the LED to any position in a 2D grid — drawing shapes, letters, or patterns with perfect accuracy, every single time.
Servo 1 sweeps left–right (X axis). Servo 2 tilts up–down (Y axis). Together they position the LED anywhere in a 2D space.
The RGB LED is the brush — turn it ON at specific positions and OFF elsewhere to control exactly which parts of the shape get drawn.
Camera shutter stays open 8–15 seconds while the LED moves through the entire pattern. When the shutter closes, the shape appears fully formed!
Shapes are stored as lists of (X, Y, LED_on) coordinates — like a connect-the-dots drawing but in servo angles.
🛒 PARTS SHOPPING LIST
Everything available on Amazon India or Robu.in. Total build cost ~₹1,400:
CIRCUIT DIAGRAM
Simple and clean — just two servos and one RGB LED connected to one Arduino Uno:
📷 HOW TO TAKE A LIGHT PAINTING PHOTO
The machine + the camera work as a team. Here's the exact sequence every time you shoot:
🔧 STEP-BY-STEP BUILD GUIDE
Build the frame first, then wire, then code. Take your time with the physical setup — accurate servo mounting makes the difference between blurry and sharp light trails!
The machine uses a standard 2-axis pan-tilt servo bracket — you can buy a pre-made kit (much easier!) or build your own:
- Pan-Tilt Kit Route (recommended): Buy a "Pan Tilt Servo Bracket" kit — includes two servo mounts and a camera plate for ~₹180 on Amazon
- DIY Route: Mount Servo X horizontally on a base. Attach a vertical arm to its horn. Mount Servo Y at the top of that arm horizontally. Attach a short LED arm to Servo Y's horn
- LED mount: Hot-glue a small piece of straw or tube at the very end of the tilt arm, pointing forward. Insert the RGB LED into this tube, legs trailing behind
- Mounting base: Screw or glue the whole assembly to a flat wooden or acrylic base plate (~15cm × 15cm)
The light painting stage is where the magic happens — all stray light must be eliminated:
- Take a cardboard box about 40cm × 40cm × 40cm and paint the inside completely flat black
- Cut a viewing window in the front — large enough for your camera lens to look inside
- Place the servo machine in the centre of the box floor
- Cut a small cable hole at the back for the Arduino wires
- Place the camera on a tripod, lens pointing through the viewing window
// Search and install in Arduino IDE: Sketch → Include Library → Manage Libraries "LiquidCrystal_I2C" // LCD display (optional) "Servo" // Built-in — no install needed // analogWrite() for LED PWM is built-in too
This is the complete code. It stores shapes as arrays of (X angle, Y angle, R, G, B) coordinates and sweeps the servo through each point:
light_painter.ino#include <Servo.h> #include <Wire.h> #include <LiquidCrystal_I2C.h> // ── Pin Definitions ──────────────────────────── #define SERVO_X_PIN 9 // Pan (left–right) #define SERVO_Y_PIN 10 // Tilt (up–down) #define LED_R 3 // PWM #define LED_G 5 // PWM #define LED_B 6 // PWM #define BTN_PIN 2 // Start button Servo servoX, servoY; LiquidCrystal_I2C lcd(0x27, 16, 2); // ── LED control ───────────────────────────────── void setLED(int r, int g, int b) { analogWrite(LED_R, r); analogWrite(LED_G, g); analogWrite(LED_B, b); } void ledOff() { setLED(0,0,0); } // ── Move to angle and paint LED colour ────────── // Glide smoothly from current position to target void moveTo(int tx, int ty, int r, int g, int b, int stepMs=12) { int cx = servoX.read(); int cy = servoY.read(); int steps = max(abs(tx-cx), abs(ty-cy)); if (steps == 0) return; for (int s = 1; s <= steps; s++) { servoX.write(map(s, 0, steps, cx, tx)); servoY.write(map(s, 0, steps, cy, ty)); setLED(r, g, b); delay(stepMs); } } // Travel without LED (move to start of next stroke) void travelTo(int tx, int ty) { ledOff(); moveTo(tx, ty, 0,0,0, 6); // faster travel } /* ══════════════════════════════════════════ SHAPE PATTERNS Each row: { angleX, angleY, R, G, B } LED off during travel, on during drawing. Servo X range: 40–140° Servo Y range: 50–120° Centre is X=90, Y=85 ══════════════════════════════════════════ */ // ── Pattern: STAR ────────────────────────────── struct Point { int x, y, r, g, b; }; Point starPts[] = { {90, 55, 255, 200, 0}, // top point {108,78, 255, 200, 0}, // upper-right inner {128,68, 255, 200, 0}, // right point {114,90, 255, 200, 0}, // lower-right inner {122,112,255, 200, 0}, // lower-right point {90, 100,255, 200, 0}, // lower-middle inner {58, 112,255, 200, 0}, // lower-left point {66, 90, 255, 200, 0}, // lower-left inner {52, 68, 255, 200, 0}, // left point {72, 78, 255, 200, 0}, // upper-left inner {90, 55, 255, 200, 0}, // back to top (close) }; // ── Pattern: HEART ───────────────────────────── Point heartPts[] = { {90, 72, 0, 0, 255}, // top centre {73, 60, 0, 0, 255}, // top-left curve {56, 66, 0, 0, 255}, // left bump {52, 82, 0, 0, 255}, // left side {70, 100, 0, 0, 255}, // lower-left {90, 115, 0, 0, 255}, // bottom point {110,100, 0, 0, 255}, // lower-right {128,82, 0, 0, 255}, // right side {124,66, 0, 0, 255}, // right bump {107,60, 0, 0, 255}, // top-right curve {90, 72, 0, 0, 255}, // back to top }; // ── Draw a pattern from array of points ───────── void drawPattern(Point* pts, int count, String name) { Serial.print("Drawing: "); Serial.println(name); lcd.clear(); lcd.print("Drawing: " + name); travelTo(pts[0].x, pts[0].y); // go to start (LED off) delay(200); for (int i = 0; i < count; i++) { lcd.setCursor(0, 1); lcd.print("Pt " + String(i+1) + " of " + String(count)); moveTo(pts[i].x, pts[i].y, pts[i].r, pts[i].g, pts[i].b); } ledOff(); Serial.println("Done!"); } // ── Setup ─────────────────────────────────────── void setup() { Serial.begin(9600); servoX.attach(SERVO_X_PIN); servoY.attach(SERVO_Y_PIN); servoX.write(90); // centre servoY.write(85); // neutral tilt pinMode(LED_R, OUTPUT); pinMode(LED_G, OUTPUT); pinMode(LED_B, OUTPUT); pinMode(BTN_PIN, INPUT_PULLUP); ledOff(); lcd.init(); lcd.backlight(); lcd.print("Light Painter"); lcd.setCursor(0,1); lcd.print("Press to start!"); Serial.println("Light Painter ready. Press button to start!"); } // ── Main Loop ─────────────────────────────────── int patternIndex = 0; void loop() { if (digitalRead(BTN_PIN) == LOW) { delay(50); // debounce if (digitalRead(BTN_PIN) == LOW) { // 5-second countdown for camera setup! for (int i = 5; i > 0; i--) { lcd.clear(); lcd.print("Starting in: "); lcd.setCursor(0,1); lcd.print(String(i) + " sec..."); Serial.println("Starting in " + String(i)); delay(1000); } // Draw alternating patterns if (patternIndex % 2 == 0) { drawPattern(starPts, sizeof(starPts)/ sizeof(Point), "STAR"); } else { drawPattern(heartPts, sizeof(heartPts)/ sizeof(Point), "HEART"); } patternIndex++; // Return to centre travelTo(90, 85); lcd.clear(); lcd.print("Done! Check photo"); lcd.setCursor(0,1); lcd.print("Press for next!"); delay(1000); // wait for button release } } }
Point array with the X/Y servo angles and RGB colours for each point of your shape. Tip: map your shape on paper first with a 40×70 grid (X: 40–140, Y: 50–120), then read off the coordinates for each corner or curve point!
{x, y, 0, 255, 0} (green) and others to {x, y, 255, 0, 128} (magenta) — each segment of the shape becomes a different colour in the photo!
Everything is ready — here's how to capture your first light painting image:
- Put your camera on a tripod, aimed at the machine inside the dark box
- Set to Manual mode: Shutter 12 sec, f/8, ISO 400 (adjust if too bright/dark)
- Turn off ALL room lights — completely dark is critical
- Press the camera shutter — it starts counting 12 seconds
- Immediately press the machine's start button — it counts down 5 seconds then draws
- Don't move! When the shutter closes, check the photo
🛠️ TROUBLESHOOTING GUIDE
Light painting has two systems to debug — the machine and the camera:
| 🔴 Problem | 🟡 Likely Cause | ✅ Fix |
|---|---|---|
| Photo is completely black | Shutter too fast / ISO too low / LED not on | Check LED wires and analogWrite values. Try ISO 800, shutter 15 sec |
| Shape looks wobbly / distorted | Servo moves too fast — motion blur | Increase stepMs in moveTo() from 12 to 20ms — slower = sharper trails |
| Shape is too small in frame | Servo angle range too narrow | Increase the angle spread in your Point array (extend X from 40–140 wider) |
| Button doesn't trigger start | INPUT_PULLUP logic inverted | Button must connect pin to GND. Code checks for LOW = pressed — correct |
| Servo skips positions | Power insufficient for two servos | Use external 5V power supply for servos, share GND with Arduino |
| Lines appear between shapes | LED not turning off during travelTo() | Confirm ledOff() is called in travelTo() before moving to the next start point |
| Photo shows a straight line, not a shape | Only one servo moving — other disconnected | Test each servo separately. Check Pin 9 → ServoX and Pin 10 → ServoY wiring |
🚀 LEVEL UP YOUR LIGHT PAINTER
Once your machine draws stars and hearts, these upgrades unlock professional light art:
Map out letter coordinates — the machine writes your name in glowing light!
Add a microphone — LED colour and speed change with the rhythm of music playing!
Add HC-05 module — draw freehand shapes from a phone app over Bluetooth!
Replace the single LED with a NeoPixel stick — draw with multiple simultaneous colours!
Write a Python script that converts any SVG path into servo angle coordinates automatically!
Program the machine to draw a different frame each shot — create an animated light painting series!
❓ FREQUENTLY ASKED QUESTIONS
During a 12-second exposure, any ambient light in the room will add up and appear in the photo — washing out the LED trail or creating a bright background. In complete darkness, the only thing the camera records is the LED's position at every moment it was on, giving you a pure glowing line on pure black.
Yes! Most modern smartphones have a "Pro" or "Manual" mode that lets you set exposure time to 10–15 seconds. iPhone users can use the Night mode (it automatically does long exposures). Android users look for "Pro" mode in the camera app. Place the phone on a stable surface or mini tripod — any movement during the exposure will blur the result.
Draw your shape on graph paper. Mark key points (corners, curve peaks) and estimate their position on a grid where X goes 40–140 (left to right) and Y goes 50–120 (bottom to top). Create a new Point[] array in the code with those values. Start simple — a triangle, square, or letter E — and work up to complex shapes.
Servo angular response isn't perfectly linear, and the tilt servo creates arc movement rather than straight lines. For very precise shapes, add intermediate points along each line segment to break curves into small straight steps. Professional light painting robots solve this with inverse kinematics — a great advanced challenge!
Outstanding choice — it spans physics (optics, light, camera exposure), engineering (servo control, XY positioning, circuit design), mathematics (coordinate systems, angle mapping), and art (composition, colour, visual design). The photos themselves are beautiful exhibition material. Try varying exposure time and documenting how it affects brightness — that's a clean, measurable experiment.
Absolutely — that's one of the most exciting features! Each point in the array can have its own R, G, B values. So the first half of a star can glow cyan, and the second half amber. The camera captures all the colours simultaneously in a single photo because the LED was each colour at each position during the exposure.

Comments
Post a Comment