Attention: Sunlight Color Wheel

Demo

Project Summary

This object utilizes a joystick controller and a NeoPixel LED Ring to mimic colored sunlight and its effect on people’s mood and emotions. The viewer is able to move the joystick to create different combinations of colored light, and is able to push down on the joystick in order to adjust the brightness.

Materials

Process

The first step is to get the joystick module working in isolation. I followed this tutorial: https://arduinogetstarted.com/tutorials/arduino-joystick. It was important observe the range of values as well as the default values. Later I used this to calibrate the input threshold.

Joystick Module Closeup

Second is to get the NeoPixel ring working in isolation. I followed this tutorial: https://www.instructables.com/How-to-Use-NeoPixel-WS2812-16-bit-Ring-Using-Ardui/.

NeoPixel Ring Closeup

The third step was writing some helper functions to be able to more easily interface with the NeoPixel ring. I referenced the NeoPixel library here: https://github.com/adafruit/Adafruit_NeoPixel

// clear pixel buffer
void clear() {
  pixels.clear(); // Set all pixel colors to 'off'
}

// reveal updated pixel buffer
void show() {
  pixels.show();
}

// write to pixel buffer
void setPixel(int pixel, int r, int g, int b) {
  pixels.setPixelColor(pixel, pixels.Color(r, g, b));
}

// set a color to all pixels
void setAllPixels(int r, int g, int b) {
  for(int i=0; i<NUMPIXELS; i++) { // For each pixel...
    pixels.setPixelColor(i, pixels.Color(r, g, b));
  }
}

Next I had to figure out how to map the joystick x and y values to degrees, and then map the degrees to a pixel index on the NeoPixel Ring. I wrote a few more helpers for this:

// computes an angle from an x and y value
float getAngle(int x, int y) {
  return radToDeg(atan2(y - 500, x - 500)) + 180;
}

// converts radian value to degrees
float radToDeg(float rad) {
  return rad * (180 / PI);
}

// maps a degree value to a pixel
int mapDegToPixel(float deg) {
  float degStep = 360 / 16;
  return floor(deg / degStep);
}

Additionally, we didn’t want the ring to light up by default. We needed to make sure that the inputs were calibrated to an intentional interaction. I wrote a helper function to calculate that threshold.

bool isJoystickMoved() {
  return (joystickXVal > 600 || joystickXVal < 400) || (joystickYVal < 400 || joystickYVal > 600);
}

Finally, we integrate both in the loop function. There were a few complex edge cases that needed be accounted with mapping directions to a start and current index, and handling overlaps.

void loop() { 
  if (isJoystickMoved()) {
    if (startPixel == -1) {
      startPixel = currentPixel;      
    }

    if (currentPixel == startPixel) {
      setPixel(currentPixel, r, g, b);
      if (direction != 0) {     
        generateRandomColor();     
      }
      direction = 0;
    }

    // case 1: currentPixel > startPixel
    if (currentPixel > startPixel) {
      if (direction == -1) {
        for (int i = startPixel; i >= 0; i--) {
          setPixel(i, r, g, b);
        }
        for (int i = NUMPIXELS - 1; i >= currentPixel; i--) {
          setPixel(i, r, g, b);
        }
      } else {
        direction = 1;
        for(int i = startPixel; i <= currentPixel; i++) {
          setPixel(i, r, g, b);
        }  
      }    
    }

    // case 2: currentPixel < startPixel
    if (currentPixel < startPixel) {
      if (direction == 1) {
        for(int i = startPixel; i <= NUMPIXELS - 1; i++) {
          setPixel(i, r, g, b);
        }
        for(int i = 0; i <= currentPixel; i++) {
          setPixel(i, r, g, b);
        }           
      } else {
        direction = -1;
        for (int i = startPixel; i >= currentPixel; i--) {
          setPixel(i, r, g, b);
        }
      }
    }
  } else if (isJoystickPressed()) {
    fadeInAndOut();
    clear();
    setAllPixels(r, g, b);
  } else {
    generateRandomColor();
    direction = 0;
    startPixel = -1;
    clear();
  }
  
  update();
  // debug();
  show();
  delay(10);
}

Full code is below:

// NeoPixel Ring simple sketch (c) 2013 Shae Erisson
// Released under the GPLv3 license to match the rest of the
// Adafruit NeoPixel library

#include <Adafruit_NeoPixel.h>
#include <ezButton.h>
#include <math.h>
#ifdef __AVR__
 #include <avr/power.h> // Required for 16 MHz Adafruit Trinket
#endif

// Which pin on the Arduino is connected to the NeoPixels?
#define NEOPIXEL_PIN 6 // On Trinket or Gemma, suggest changing this to 1

#define SW_PIN   5  // Arduino pin connected to SW  pin
#define VRX_PIN  A0 // Arduino pin connected to VRX pin
#define VRY_PIN  A1 // Arduino pin connected to VRY pin

// How many NeoPixels are attached to the Arduino?
#define NUMPIXELS 16 // Popular NeoPixel ring size
#define MAX_BRIGHTNESS 100

// When setting up the NeoPixel library, we tell it how many pixels,
// and which pin to use to send signals. Note that for older NeoPixel
// strips you might need to change the third parameter -- see the
// strandtest example for more information on possible values.
Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

int brightness = MAX_BRIGHTNESS;
int joystickXVal = 0;
int joystickYVal = 0;
int joystickSWVal = 0;
int startAngle = 0;
int joystickAngle = 0;
int currentPixel = 0;
int startPixel = -1;
int direction = 0;
int r = 0;
int g = 0;
int b = 0;
int fadeAmount = 1;

void setup() {
  Serial.begin(9600);
  pinMode(SW_PIN,INPUT_PULLUP);
  Serial.print("Hello World");

  pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
  pixels.setBrightness(brightness);
  generateRandomColor();
}

void loop() { 
  if (isJoystickMoved()) {
    if (startPixel == -1) {
      startPixel = currentPixel;      
    }

    if (currentPixel == startPixel) {
      setPixel(currentPixel, r, g, b);
      if (direction != 0) {     
        generateRandomColor();     
      }
      direction = 0;
    }

    // case 1: currentPixel > startPixel
    if (currentPixel > startPixel) {
      if (direction == -1) {
        for (int i = startPixel; i >= 0; i--) {
          setPixel(i, r, g, b);
        }
        for (int i = NUMPIXELS - 1; i >= currentPixel; i--) {
          setPixel(i, r, g, b);
        }
      } else {
        direction = 1;
        for(int i = startPixel; i <= currentPixel; i++) {
          setPixel(i, r, g, b);
        }  
      }    
    }

    // case 2: currentPixel < startPixel
    if (currentPixel < startPixel) {
      if (direction == 1) {
        for(int i = startPixel; i <= NUMPIXELS - 1; i++) {
          setPixel(i, r, g, b);
        }
        for(int i = 0; i <= currentPixel; i++) {
          setPixel(i, r, g, b);
        }           
      } else {
        direction = -1;
        for (int i = startPixel; i >= currentPixel; i--) {
          setPixel(i, r, g, b);
        }
      }
    }
  } else if (isJoystickPressed()) {
    fadeInAndOut();
    clear();
    setAllPixels(r, g, b);
  } else {
    generateRandomColor();
    direction = 0;
    startPixel = -1;
    clear();
  }
  
  update();
  // debug();
  show();
  delay(10);
}

// clear pixel buffer
void clear() {
  pixels.clear(); // Set all pixel colors to 'off'
}

// reveal updated pixel buffer
void show() {
  pixels.show();
}

// write to pixel buffer
void setPixel(int pixel, int r, int g, int b) {
  pixels.setPixelColor(pixel, pixels.Color(r, g, b));
}

void setAllPixels(int r, int g, int b) {
  for(int i=0; i<NUMPIXELS; i++) { // For each pixel...
    pixels.setPixelColor(i, pixels.Color(r, g, b));
  }
}

void update() {
  joystickXVal = analogRead(VRX_PIN);
  joystickYVal = analogRead(VRY_PIN);
  joystickSWVal = digitalRead(SW_PIN);
  joystickAngle = getAngle(joystickXVal, joystickYVal);
  currentPixel = mapDegToPixel(joystickAngle);
}

void debug() {
  Serial.print("x = ");
  Serial.print(joystickXVal);
  Serial.print(", y = ");
  Serial.print(joystickYVal);
  Serial.print(", angle = ");
  Serial.print(joystickAngle);
  Serial.print(", button = ");
  Serial.print(isJoystickPressed());
  Serial.print(", startPixel = ");
  Serial.print(startPixel);
  Serial.print(", currentPixel = ");
  Serial.print(currentPixel);
  Serial.print(", direction = ");
  Serial.println(direction);
}

bool isJoystickMoved() {
  return (joystickXVal > 600 || joystickXVal < 400) || (joystickYVal < 400 || joystickYVal > 600);
}

bool isJoystickPressed() {
  return !joystickSWVal;
}

float getAngle(int x, int y) {
  return radToDeg(atan2(y - 500, x - 500)) + 180;
}

float radToDeg(float rad) {
  return rad * (180 / PI);
}

int mapDegToPixel(float deg) {
  float degStep = 360 / 16;
  return floor(deg / degStep);
}

void generateRandomColor() {
  r = random(0, 255);
  g = random(0, 255);
  b = random(0, 255);
}

void fadeInAndOut() {
  if (brightness <= 0 || brightness >= MAX_BRIGHTNESS) {
    fadeAmount = -fadeAmount;
  }
  if (brightness <= 0) {
    generateRandomColor();
  }
  brightness = brightness + fadeAmount;
  pixels.setBrightness(brightness);
}

Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *