// Wokwi Demo Sketch - ESP32 Face-Tracking Eye Display
// Potentiometer-controlled version for simulation (no WiFi/OSC)
//
// Hardware:
// - Potentiometer 1 (GPIO 4): Horizontal control (animationVarY, 0-100)
// - Potentiometer 2 (GPIO 5): Vertical control (animationVarX, 0-100)
// - 13 NeoPixel LED strips (661 total LEDs) - SAME AS REAL HARDWARE
// - NO TFT Display (removed for Wokwi compatibility)
//
// Features:
// - Directional color fade (white → yellow based on gaze direction)
// - Progressive brightness (rings fill bottom-to-top based on vertical)
// - Animated noise field flowing around LED rings for organic movement
// - Directional wing noise (intensifies toward gaze direction, subtle when centered)
#include <Adafruit_NeoPixel.h>
// Potentiometer Configuration (ESP32-S3 ADC1 pins - avoid conflicts with LEDs)
#define POT_PIN_HORIZONTAL 4 // animationVarY (0-100, left-right)
#define POT_PIN_VERTICAL 5 // animationVarX (0-100, up-down)
// LED Ring Configuration (Face Tracking Rings) - SAME AS REAL HARDWARE
#define NUM_LEDS_RING_TOP 66 // Top ring
#define NUM_LEDS_RING_MIDDLE 59 // Middle ring (not connected yet)
#define NUM_LEDS_RING_BOTTOM 51 // Bottom ring
#define LED_PIN_RING_TOP 35
#define LED_PIN_RING_MIDDLE 36 // Not connected yet
#define LED_PIN_RING_BOTTOM 37
Adafruit_NeoPixel ringTop = Adafruit_NeoPixel(NUM_LEDS_RING_TOP, LED_PIN_RING_TOP, NEO_GRB + NEO_KHZ800);
// Adafruit_NeoPixel ringMiddle = Adafruit_NeoPixel(NUM_LEDS_RING_MIDDLE, LED_PIN_RING_MIDDLE, NEO_GRB + NEO_KHZ800); // DISABLED - not connected yet
Adafruit_NeoPixel ringBottom = Adafruit_NeoPixel(NUM_LEDS_RING_BOTTOM, LED_PIN_RING_BOTTOM, NEO_GRB + NEO_KHZ800);
// LED Ring Configuration (Static White Ring) - SAME AS REAL HARDWARE
#define NUM_LEDS_WHITE_RING 9
#define LED_PIN_WHITE_RING 1
Adafruit_NeoPixel whiteRing = Adafruit_NeoPixel(NUM_LEDS_WHITE_RING, LED_PIN_WHITE_RING, NEO_GRB + NEO_KHZ800);
// LED Wing Configuration (10 strips total - 5 per side, mirrored) - SAME AS REAL HARDWARE
#define NUM_LEDS_WING1 63
#define NUM_LEDS_WING2 76
#define NUM_LEDS_WING3 95
#define NUM_LEDS_WING4 110
#define NUM_LEDS_WING5 200
// Left wing pins - SAME AS REAL HARDWARE
#define LED_PIN_LEFT_WING1 20
#define LED_PIN_LEFT_WING2 21
#define LED_PIN_LEFT_WING3 47
#define LED_PIN_LEFT_WING4 48
#define LED_PIN_LEFT_WING5 45
// Right wing pins - SAME AS REAL HARDWARE
#define LED_PIN_RIGHT_WING1 39
#define LED_PIN_RIGHT_WING2 40
#define LED_PIN_RIGHT_WING3 41
#define LED_PIN_RIGHT_WING4 42
#define LED_PIN_RIGHT_WING5 2
// Left wing NeoPixel objects
Adafruit_NeoPixel leftWing1 = Adafruit_NeoPixel(NUM_LEDS_WING1, LED_PIN_LEFT_WING1, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel leftWing2 = Adafruit_NeoPixel(NUM_LEDS_WING2, LED_PIN_LEFT_WING2, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel leftWing3 = Adafruit_NeoPixel(NUM_LEDS_WING3, LED_PIN_LEFT_WING3, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel leftWing4 = Adafruit_NeoPixel(NUM_LEDS_WING4, LED_PIN_LEFT_WING4, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel leftWing5 = Adafruit_NeoPixel(NUM_LEDS_WING5, LED_PIN_LEFT_WING5, NEO_GRB + NEO_KHZ800);
// Right wing NeoPixel objects
Adafruit_NeoPixel rightWing1 = Adafruit_NeoPixel(NUM_LEDS_WING1, LED_PIN_RIGHT_WING1, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel rightWing2 = Adafruit_NeoPixel(NUM_LEDS_WING2, LED_PIN_RIGHT_WING2, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel rightWing3 = Adafruit_NeoPixel(NUM_LEDS_WING3, LED_PIN_RIGHT_WING3, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel rightWing4 = Adafruit_NeoPixel(NUM_LEDS_WING4, LED_PIN_RIGHT_WING4, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel rightWing5 = Adafruit_NeoPixel(NUM_LEDS_WING5, LED_PIN_RIGHT_WING5, NEO_GRB + NEO_KHZ800);
// Global animation variables (controlled by potentiometers)
int animationVarY = 0; // Horizontal (0-100, left-right)
int animationVarX = 0; // Vertical (0-100, up-down)
// Noise field parameters - Adjust these for different effects!
float noiseOffset = 0.0; // Time-based offset for animation (auto-incremented)
float noiseSpeed = 0.1; // Speed of noise movement (0.01=slow, 0.1=fast)
float noiseScale = 0.5; // Spatial frequency (0.1=large clouds, 0.5=small ripples)
float noiseIntensity = 0.6; // Strength of noise effect on rings (0.0=off, 1.0=max darkening)
float wingNoiseIntensityMax = 0.6; // Max noise intensity on wings when looking that direction
float wingNoiseIntensityMin = 0.15; // Min noise intensity on wings when looking away
// Simple 1D Perlin-like noise function
float noise1D(float x) {
int xi = (int)x;
float xf = x - xi;
// Simple pseudo-random hash function
auto hash = [](int n) -> float {
n = (n << 13) ^ n;
return (1.0 - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0);
};
// Get random values at integer positions
float v1 = hash(xi);
float v2 = hash(xi + 1);
// Smoothstep interpolation (smoother than linear)
float t = xf * xf * (3.0 - 2.0 * xf);
// Interpolate and return in range [0, 1]
return (v1 * (1.0 - t) + v2 * t) * 0.5 + 0.5;
}
void setup() {
Serial.begin(115200);
Serial.println("Wokwi Demo - Starting...");
// Configure potentiometer pins
pinMode(POT_PIN_HORIZONTAL, INPUT);
pinMode(POT_PIN_VERTICAL, INPUT);
// Initialize NeoPixel - Face Tracking Rings
ringTop.begin();
// ringMiddle.begin(); // DISABLED - not connected yet
ringBottom.begin();
// Initialize NeoPixel - Static White Ring
whiteRing.begin();
// Initialize NeoPixel - Left Wings
leftWing1.begin();
leftWing2.begin();
leftWing3.begin();
leftWing4.begin();
leftWing5.begin();
// Initialize NeoPixel - Right Wings
rightWing1.begin();
rightWing2.begin();
rightWing3.begin();
rightWing4.begin();
rightWing5.begin();
// Set all NeoPixels to off initially
ringTop.clear();
ringBottom.clear();
whiteRing.clear();
leftWing1.clear();
leftWing2.clear();
leftWing3.clear();
leftWing4.clear();
leftWing5.clear();
rightWing1.clear();
rightWing2.clear();
rightWing3.clear();
rightWing4.clear();
rightWing5.clear();
// Show initial state (all off)
ringTop.show();
ringBottom.show();
whiteRing.show();
leftWing1.show();
leftWing2.show();
leftWing3.show();
leftWing4.show();
leftWing5.show();
rightWing1.show();
rightWing2.show();
rightWing3.show();
rightWing4.show();
rightWing5.show();
Serial.println("Setup complete - ready for potentiometer input");
Serial.println("NOTE: Single-core, LED-only version for Wokwi");
}
void loop() {
// Read potentiometers (ESP32 ADC: 0-4095)
int potHorizontalRaw = analogRead(POT_PIN_HORIZONTAL);
int potVerticalRaw = analogRead(POT_PIN_VERTICAL);
// Map potentiometer values to animation range (0-100)
animationVarY = map(potHorizontalRaw, 0, 4095, 0, 100);
animationVarY = constrain(animationVarY, 0, 100);
animationVarX = map(potVerticalRaw, 0, 4095, 0, 100);
animationVarX = constrain(animationVarX, 0, 100);
// === LED UPDATE ===
// LED Ring Control - Face tracking rings
// Looking down (animationVarX = 100): all rings bright
// Looking up (animationVarX = 0): all rings off
// Progressive: largest ring (ringTop) turns off first when looking up
// Total LEDs: 66 (ringTop) + 51 (ringBottom) = 117 LEDs (ringMiddle disabled)
// Max brightness: 117 × 255 = 29,835
int totalRingBrightness = map(animationVarX, 0, 100, 0, 29835);
// Distribute brightness progressively (smallest stays on longest)
// ringBottom (smallest) fills first, then ringTop (largest)
int ringBottomMax = 51 * 255; // 13,005
int ringTopMax = 66 * 255; // 16,830
uint8_t ringTopBrightness = 0;
uint8_t ringBottomBrightness = 0;
if (totalRingBrightness <= ringBottomMax) {
// Only smallest ring lit
ringTopBrightness = 0;
ringBottomBrightness = totalRingBrightness / 51;
} else {
// Smallest ring at max, largest ring filling
ringBottomBrightness = 255;
ringTopBrightness = (totalRingBrightness - ringBottomMax) / 66;
ringTopBrightness = constrain(ringTopBrightness, 0, 255);
}
// Update LED Rings with directional color (circular distribution)
// Rings are circular, so use angular positioning
// animationVarY (0-100) maps to look direction (0-360 degrees)
float targetAngleDeg = map(animationVarY, 0, 100, 0, 360);
// Ring Top (66 LEDs)
for (int i = 0; i < NUM_LEDS_RING_TOP; i++) {
// Calculate LED angle (evenly distributed around ring)
float ledAngle = (i * 360.0) / NUM_LEDS_RING_TOP;
// Calculate angular distance from target (shortest path)
float angleDiff = abs(ledAngle - targetAngleDeg);
if (angleDiff > 180) angleDiff = 360 - angleDiff;
// Map angular distance to directional fade (0° = white, 180° = yellow)
float directionalFade = angleDiff / 180.0; // 0.0 to 1.0
// Add noise field effect
float noiseValue = noise1D(i * noiseScale + noiseOffset);
float noiseMod = 1.0 - (noiseIntensity * (1.0 - noiseValue)); // Reduce brightness based on noise
// Apply color with brightness and noise modulation
uint8_t r = (uint8_t)(ringTopBrightness * noiseMod);
uint8_t g = (uint8_t)(ringTopBrightness * noiseMod);
uint8_t b = (uint8_t)(ringTopBrightness * (1.0 - directionalFade) * noiseMod);
ringTop.setPixelColor(i, ringTop.Color(r, g, b));
}
// Ring Bottom (51 LEDs)
for (int i = 0; i < NUM_LEDS_RING_BOTTOM; i++) {
// Calculate LED angle (evenly distributed around ring)
float ledAngle = (i * 360.0) / NUM_LEDS_RING_BOTTOM;
// Calculate angular distance from target (shortest path)
float angleDiff = abs(ledAngle - targetAngleDeg);
if (angleDiff > 180) angleDiff = 360 - angleDiff;
// Map angular distance to directional fade (0° = white, 180° = yellow)
float directionalFade = angleDiff / 180.0; // 0.0 to 1.0
// Add noise field effect (offset slightly from top ring for variety)
float noiseValue = noise1D(i * noiseScale + noiseOffset + 10.0);
float noiseMod = 1.0 - (noiseIntensity * (1.0 - noiseValue)); // Reduce brightness based on noise
// Apply color with brightness and noise modulation
uint8_t r = (uint8_t)(ringBottomBrightness * noiseMod);
uint8_t g = (uint8_t)(ringBottomBrightness * noiseMod);
uint8_t b = (uint8_t)(ringBottomBrightness * (1.0 - directionalFade) * noiseMod);
ringBottom.setPixelColor(i, ringBottom.Color(r, g, b));
}
// Update Wings - Directional color based on horizontal gaze (animationVarY)
// animationVarY: 0 = looking left, 100 = looking right
// Left wings: white when looking left, yellow when looking right
// Right wings: yellow when looking left, white when looking right
// Calculate fade value (0.0 = left, 1.0 = right)
float horizontalFade = animationVarY / 100.0;
// Calculate directional noise intensity for wings
// Left wings: more noise when looking left (horizontalFade = 0)
float leftWingNoiseIntensity = wingNoiseIntensityMin + (wingNoiseIntensityMax - wingNoiseIntensityMin) * (1.0 - horizontalFade);
// Right wings: more noise when looking right (horizontalFade = 1)
float rightWingNoiseIntensity = wingNoiseIntensityMin + (wingNoiseIntensityMax - wingNoiseIntensityMin) * horizontalFade;
// Left wings color interpolation (white → yellow as we look right)
uint8_t leftWing_r = 255;
uint8_t leftWing_g = 255;
uint8_t leftWing_b = (uint8_t)(255 * (1.0 - horizontalFade)); // Blue fades out
// Update all left wing strips with noise modulation
for (int i = 0; i < NUM_LEDS_WING1; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 20.0); // Slower spatial frequency for wings
float noiseMod = 1.0 - (leftWingNoiseIntensity * (1.0 - noiseValue));
leftWing1.setPixelColor(i, leftWing1.Color(
(uint8_t)(leftWing_r * noiseMod),
(uint8_t)(leftWing_g * noiseMod),
(uint8_t)(leftWing_b * noiseMod)
));
}
for (int i = 0; i < NUM_LEDS_WING2; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 25.0);
float noiseMod = 1.0 - (leftWingNoiseIntensity * (1.0 - noiseValue));
leftWing2.setPixelColor(i, leftWing2.Color(
(uint8_t)(leftWing_r * noiseMod),
(uint8_t)(leftWing_g * noiseMod),
(uint8_t)(leftWing_b * noiseMod)
));
}
for (int i = 0; i < NUM_LEDS_WING3; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 30.0);
float noiseMod = 1.0 - (leftWingNoiseIntensity * (1.0 - noiseValue));
leftWing3.setPixelColor(i, leftWing3.Color(
(uint8_t)(leftWing_r * noiseMod),
(uint8_t)(leftWing_g * noiseMod),
(uint8_t)(leftWing_b * noiseMod)
));
}
for (int i = 0; i < NUM_LEDS_WING4; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 35.0);
float noiseMod = 1.0 - (leftWingNoiseIntensity * (1.0 - noiseValue));
leftWing4.setPixelColor(i, leftWing4.Color(
(uint8_t)(leftWing_r * noiseMod),
(uint8_t)(leftWing_g * noiseMod),
(uint8_t)(leftWing_b * noiseMod)
));
}
for (int i = 0; i < NUM_LEDS_WING5; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 40.0);
float noiseMod = 1.0 - (leftWingNoiseIntensity * (1.0 - noiseValue));
leftWing5.setPixelColor(i, leftWing5.Color(
(uint8_t)(leftWing_r * noiseMod),
(uint8_t)(leftWing_g * noiseMod),
(uint8_t)(leftWing_b * noiseMod)
));
}
// Right wings color interpolation (yellow → white as we look right)
uint8_t rightWing_r = 255;
uint8_t rightWing_g = 255;
uint8_t rightWing_b = (uint8_t)(255 * horizontalFade); // Blue fades in
// Update all right wing strips with noise modulation
for (int i = 0; i < NUM_LEDS_WING1; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 50.0); // Different phase offset
float noiseMod = 1.0 - (rightWingNoiseIntensity * (1.0 - noiseValue));
rightWing1.setPixelColor(i, rightWing1.Color(
(uint8_t)(rightWing_r * noiseMod),
(uint8_t)(rightWing_g * noiseMod),
(uint8_t)(rightWing_b * noiseMod)
));
}
for (int i = 0; i < NUM_LEDS_WING2; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 55.0);
float noiseMod = 1.0 - (rightWingNoiseIntensity * (1.0 - noiseValue));
rightWing2.setPixelColor(i, rightWing2.Color(
(uint8_t)(rightWing_r * noiseMod),
(uint8_t)(rightWing_g * noiseMod),
(uint8_t)(rightWing_b * noiseMod)
));
}
for (int i = 0; i < NUM_LEDS_WING3; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 60.0);
float noiseMod = 1.0 - (rightWingNoiseIntensity * (1.0 - noiseValue));
rightWing3.setPixelColor(i, rightWing3.Color(
(uint8_t)(rightWing_r * noiseMod),
(uint8_t)(rightWing_g * noiseMod),
(uint8_t)(rightWing_b * noiseMod)
));
}
for (int i = 0; i < NUM_LEDS_WING4; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 65.0);
float noiseMod = 1.0 - (rightWingNoiseIntensity * (1.0 - noiseValue));
rightWing4.setPixelColor(i, rightWing4.Color(
(uint8_t)(rightWing_r * noiseMod),
(uint8_t)(rightWing_g * noiseMod),
(uint8_t)(rightWing_b * noiseMod)
));
}
for (int i = 0; i < NUM_LEDS_WING5; i++) {
float noiseValue = noise1D(i * noiseScale * 0.5 + noiseOffset + 70.0);
float noiseMod = 1.0 - (rightWingNoiseIntensity * (1.0 - noiseValue));
rightWing5.setPixelColor(i, rightWing5.Color(
(uint8_t)(rightWing_r * noiseMod),
(uint8_t)(rightWing_g * noiseMod),
(uint8_t)(rightWing_b * noiseMod)
));
}
// Update static white ring (always full brightness white)
for (int i = 0; i < NUM_LEDS_WHITE_RING; i++) {
whiteRing.setPixelColor(i, whiteRing.Color(255, 255, 255)); // Full white
}
// Show all LED strips
ringTop.show();
ringBottom.show();
whiteRing.show();
leftWing1.show();
leftWing2.show();
leftWing3.show();
leftWing4.show();
leftWing5.show();
rightWing1.show();
rightWing2.show();
rightWing3.show();
rightWing4.show();
rightWing5.show();
// Update noise offset for animation
noiseOffset += noiseSpeed;
// Debug output (every 2000ms)
static unsigned long lastDebug = 0;
if (millis() - lastDebug > 2000) {
Serial.println("=== Wokwi LED Demo with Directional Noise ===");
Serial.print("Pot Raw - Horizontal: "); Serial.print(potHorizontalRaw);
Serial.print(" | Vertical: "); Serial.println(potVerticalRaw);
Serial.print("Animation - animY: "); Serial.print(animationVarY);
Serial.print(" | animX: "); Serial.println(animationVarX);
Serial.print("LED Ring - TopBright: "); Serial.print(ringTopBrightness);
Serial.print(" | BottomBright: "); Serial.println(ringBottomBrightness);
Serial.print("LED Wings - HorizontalFade: "); Serial.print(horizontalFade);
Serial.print(" | LeftNoise: "); Serial.print(leftWingNoiseIntensity);
Serial.print(" | RightNoise: "); Serial.println(rightWingNoiseIntensity);
Serial.print("Noise - Offset: "); Serial.print(noiseOffset);
Serial.print(" | RingIntensity: "); Serial.println(noiseIntensity);
Serial.println();
lastDebug = millis();
}
}
Loading
esp32-s3-devkitc-1
esp32-s3-devkitc-1