Light & Interactivity Week 2 – Roly-poly Candle

This week’s task is to create the “effect” of a candle. I worked with Rui Wang at ITP and we created a roly-poly candle that can simulate how the candle light would react to its shifting center of gravity when you give it a gentle push.

Ideation

Considering what type of candle to build, we checked several examples online, and found Hayeon Hwang’s brush light. She used a neopixel jewel vertically to create the light of a candle, and used brush hairs to diffuse – this had led to an amazing simulation in terms of the color, texture and shape of the candle light. But since it can be viewed from only one angle, Rui and I, after discussion, agreed that we still want to make a candle that can be seen 360. Eventually, we landed on the idea to make one that no longer stands still on a table — it can wobble, and its light will change with respect to how light moves under a wobbling environment.

Prototype

In considering how to create the effect of a candle, rather than just the candle itself, we broke down the characteristics of the candle light into the following aspects:

  • a constant, steady light base comprised of three colors: a little bit of blue in the bottom core, a red ring surrounding the blue at the bottom, and a gradual transition to bright yellow at the top-outer half
  • occasional flickering
  • occasional hue shifting between the red part and the yellow part
  • occasional deforming of the light shape due to small changes in air turbulence

So we decided to:

  • combine a neopixel ring and a jewel to create this varying color base
  • use timed and random functions to recreate different layers of flickering, hue shifting and deformation

And tested with diffuser:

We also used an accerlerometer to tune the intensity of different pixels so that the overall candle light can respond to the tilting of the candle.

We made a base similar to a rolyl-poly toy as the fixture of the candle:

Final Work

Code

#include <ColorConverter.h>
#include <Adafruit_Circuit_Playground.h>

#include <Interval.h>
#include <Adafruit_NeoPixel.h>

Interval flickerInterval1;
Interval flickerInterval2;
Interval flickerRestoreInterval;
Interval randomDimInterval;
ColorConverter converter;

//switch
float overallIntensityScale = 0;
int ignitionCount = 0;

int dir = 0; //0 = netrual, 1 = tilted north, 5 = tilted south, 9 directions in total, clock-wise
int strongBaseLED = 5;
int weakBaseLED = 0;
int strongUpperLED = 3;
int weakUpperLED = 6;

//base ring
const int neoPixelPin1 = 9;
const int pixelCount1 = 10;
float baseH = 18; // from RGB 255, 83, 12
float baseS = 100;
float baseI = 53;
float baseChange = 0.1;
float upperChange = 0.1;


//upper ring
const int neoPixelPin2 = 10;
const int pixelCount2 = 7;
float upperH = 18;
float upperS = 100;
float upperI = 53;
float coreH = 240;
float coreS = 100;
float coreI = 50;

float baseIntensityScale = 0.7;
float upperIntensityScale = 1;
float coreIntensityScale = 0.3;

float defaultDirIntensity = 10;
float dirIntensity = 100;
float dirChange = 2;

boolean isRandomDim = false;
float randomIntensityOffset = 0;
long randomDimTime;

//float flickerIntensity = 255;
int flickerDim = 0;
boolean flickerRestored = true;
long flickerTime;

// set up upper ring:
Adafruit_CPlay_NeoPixel strip2 = Adafruit_CPlay_NeoPixel(pixelCount2, neoPixelPin2, NEO_GRBW + NEO_KHZ800);

int prevX, prevY, prevZ;

void setup() {

  CircuitPlayground.begin();

  strip2.begin();    // initialize pixel strip
  strip2.clear();    // turn all LEDs off
  Serial.begin(9600);

  //flicer randomly
  flickerInterval1.setInterval(flickerDim1, 7000);
  flickerInterval2.setInterval(flickerDim2, 9890);
  randomDimInterval.setInterval(randomDim, 6000);

}

void loop() {


  //accel--------------------------------------------

  int x = CircuitPlayground.motionX();
  int y = CircuitPlayground.motionY();
  int z = CircuitPlayground.motionZ();

  Serial.print("x: ");
  Serial.print(x);
  Serial.print(" y: ");
  Serial.print(y);
  Serial.print(" z: ");
  Serial.println(z);

  int accel = abs(x - prevX) + abs(y - prevY) + abs(z - prevZ);
  Serial.print("total: ");
  Serial.println(accel);

  prevX = x;
  prevY = y;
  prevZ = z;

  //ignition--------------------------
  if (accel > 0) {
    ignitionCount++;
    Serial.print("ignition count: ");
    Serial.println(ignitionCount);
  }
  if (ignitionCount == 10) {
    overallIntensityScale = 0.5;
  } else if (ignitionCount == 12) {
    overallIntensityScale = 0;
  } else if (ignitionCount == 20) {
    overallIntensityScale = 0.8;
  } else if (ignitionCount == 22) {
    overallIntensityScale = 0;
  } else if (ignitionCount >= 30) {
    overallIntensityScale = 1;
  }

  //direction control--------------------

  if (x <= -2 && y == 0) {
    dir = 1;
  } else if (x <= -2 && y >= 2) {
    dir = 2;
  } else if (x == 2 && y >= 2) {
    dir = 3;
  } else if (x >= 2 && y >= 2) {
    dir = 4;
  } else if (x >= 2 && y == 0) {
    dir = 5;
  } else if (x >= 2 && y <= -2) {
    dir = 6;
  } else if (x == 0 && y <= -2) {
    dir = 7;
  } else if (x <= -2 && y <= -2) {
    dir = 8;
  } else {
    dir = 0;
  }
  //--------------------------------------------
  //flickerDim
  if (!flickerRestored) {
    Serial.println("needs restore!");
    //    flickerRestoreInterval.setTimeout(flickerRestore, 49);
    if (millis() > flickerTime + 49) {
      flickerRestore();
    }
  }

  //randomDim
  if (isRandomDim) {
    Serial.println("needs restore random!");
    randomIntensityOffset = random(0, 3);
    if (millis() > randomDimTime + 2500) {
      randomDimRestore();
    }
  }


  switch (dir) {
    case 0:
      //do noting to change the strong, weak LED
      break;
    case 1:
      strongBaseLED = 5;
      weakBaseLED = 0;
      strongUpperLED = 3;
      weakUpperLED = 6;
      break;
    case 2:
      strongBaseLED = 3;
      weakBaseLED = 8;
      strongUpperLED = 4;
      weakUpperLED = 1;
      break;
    case 3:
      strongBaseLED = 2;
      weakBaseLED = 7;
      strongUpperLED = 4;
      weakUpperLED = 1;
      break;
    case 4:
      strongBaseLED = 0;
      weakBaseLED = 5;
      strongUpperLED = 5;
      weakUpperLED = 2;
      break;
    case 5:
      strongBaseLED = 0;
      weakBaseLED = 5;
      strongUpperLED = 6;
      weakUpperLED = 3;
      break;
    case 6:
      strongBaseLED = 9;
      weakBaseLED = 4;
      strongUpperLED = 6;
      weakUpperLED = 3;
      break;
    case 7:
      strongBaseLED = 7;
      weakBaseLED = 2;
      strongUpperLED = 1;
      weakUpperLED = 4;
      break;
    case 8:
      strongBaseLED = 6;
      weakBaseLED = 1;
      strongUpperLED = 2;
      weakUpperLED = 5;
      break;

  }

  //change intensity based on direction
  if (dir == 0) {
    if (dirIntensity >= 25) {
      dirIntensity = dirIntensity - dirChange;
    }
  } else {
    if (dirIntensity < 50) {
      dirIntensity = dirIntensity + dirChange;
    }
  }


  delay(2);

  //determine the final RGB value

  RGBColor baseColorStrong = converter.HSItoRGB(baseH, baseS, (dirIntensity - flickerDim - randomIntensityOffset) * baseIntensityScale * overallIntensityScale);
  RGBColor baseColorWeak = converter.HSItoRGB(baseH, baseS, (50 - dirIntensity - flickerDim - randomIntensityOffset) * baseIntensityScale * overallIntensityScale);
  RGBColor baseColorNeutral = converter.HSItoRGB(baseH, baseS, (defaultDirIntensity - flickerDim - randomIntensityOffset) * baseIntensityScale * overallIntensityScale);
  RGBColor upperColorStrong = converter.HSItoRGB(upperH, upperS, (dirIntensity - flickerDim - randomIntensityOffset) * upperIntensityScale * overallIntensityScale);
  RGBColor upperColorWeak = converter.HSItoRGB(upperH, upperS, 50 - (dirIntensity - flickerDim - randomIntensityOffset) * upperIntensityScale * overallIntensityScale);
  RGBColor upperColorNeutral = converter.HSItoRGB(upperH, upperS, (defaultDirIntensity - flickerDim - randomIntensityOffset) * upperIntensityScale * overallIntensityScale);
  RGBColor coreColor = converter.HSItoRGB(coreH, coreS, (defaultDirIntensity - flickerDim - randomIntensityOffset) * coreIntensityScale * overallIntensityScale);


  //add a constant shifting of HUE for two rings
  baseH = baseH + baseChange;
  if (baseH < 14 || baseH > 22) {
    baseChange = -baseChange;
  }

  upperH = upperH + upperChange;
  if (upperH < 10 || upperH > 22) {
    upperChange = -upperChange;
  }



  for (int pixel = 0; pixel < pixelCount1; pixel++) {
    CircuitPlayground.setBrightness(255);

    //base ring
    if (pixel == strongBaseLED) {
      //      CircuitPlayground.strip.setPixelColor(pixel, baseColor.red * orangeScale * flickerIntensity / 255 * dirIntensity / 255, baseColor.green * orangeScale * flickerIntensity / 255 * dirIntensity / 255, baseColor.blue * orangeScale * flickerIntensity / 255 * dirIntensity / 255 ); // set the color for this pixel
      CircuitPlayground.strip.setPixelColor(pixel, baseColorStrong.red, baseColorStrong.green, baseColorStrong.blue); // set the color for this pixel
    } else if (pixel == weakBaseLED) {
      //      CircuitPlayground.strip.setPixelColor(pixel, baseColor.red * orangeScale * flickerIntensity / 255 * (1 - dirIntensity / 255), baseColor.green * flickerIntensity / 255 * orangeScale * (1 - dirIntensity / 255), baseColor.blue * flickerIntensity / 255 * orangeScale * (1 - dirIntensity / 255)); // set the color for this pixel
      CircuitPlayground.strip.setPixelColor(pixel, baseColorWeak.red, baseColorWeak.green, baseColorWeak.blue); // set the color for this pixel
    } else {
      //      CircuitPlayground.strip.setPixelColor(pixel, baseColor.red * orangeScale * flickerIntensity / 255 * defaultDirIntensity / 255, baseColor.green * orangeScale * flickerIntensity / 255 * defaultDirIntensity / 255, baseColor.blue * orangeScale * flickerIntensity / 255 * defaultDirIntensity / 255); // set the color for this pixel
      CircuitPlayground.strip.setPixelColor(pixel, baseColorNeutral.red, baseColorNeutral.green, baseColorNeutral.blue); // set the color for this pixel
    }

    //upper ring
    if (pixel == 0) {
      //      strip2.setPixelColor(pixel, 0 * blueScale  * flickerIntensity / 255, 0 * blueScale * flickerIntensity / 255, 255 * blueScale * flickerIntensity / 255); // set inner most blue
      strip2.setPixelColor(pixel, coreColor.red, coreColor.green, coreColor.blue); // set the color for this pixel
    } else if (pixel == strongUpperLED) {
      //      strip2.setPixelColor(pixel, red * redScale * flickerIntensity / 255 * dirIntensity / 255, green * redScale * flickerIntensity / 255 * dirIntensity / 255, blue * redScale * flickerIntensity / 255 * dirIntensity / 255 ); // set the color for this pixel
      strip2.setPixelColor(pixel, upperColorStrong.red, upperColorStrong.green, upperColorStrong.blue); // set the color for this pixel

    } else if (pixel == weakUpperLED) {
      //      strip2.setPixelColor(pixel, red * redScale * flickerIntensity / 255 * (1 - dirIntensity / 255), green * redScale * flickerIntensity / 255  * (1 - dirIntensity / 255), blue * redScale * flickerIntensity / 255 * (1 - dirIntensity / 255)); // set the color for this pixel
      strip2.setPixelColor(pixel, upperColorWeak.red, upperColorWeak.green, upperColorWeak.blue); // set the color for this pixel
    }
    else {
      //      strip2.setPixelColor(pixel, red * redScale * flickerIntensity / 255 * defaultDirIntensity / 255, green * redScale * flickerIntensity / 255 * defaultDirIntensity / 255, blue * redScale * flickerIntensity / 255 * defaultDirIntensity / 255 ); // set the color for this pixel
      strip2.setPixelColor(pixel, upperColorNeutral.red, upperColorNeutral.green, upperColorNeutral.blue); // set the color for this pixel
    }
    //delay(500);

  }
  CircuitPlayground.strip.show();    // refresh the strip
  strip2.show();    // refresh the strip

  flickerInterval1.check();
  flickerInterval2.check();
  flickerRestoreInterval.check();
  randomDimInterval.check();

}

void flickerDim1() {
  flickerDim = 50;
  flickerRestored = false;
  flickerTime = millis();
}

void flickerDim2() {
  flickerDim = 50;
  flickerRestored = false;
  flickerTime = millis();
}

void flickerRestore() {
  Serial.print("restored!");
  flickerDim = 0;
  flickerRestored = true;
}

void randomDim() {
  isRandomDim = true;
  randomDimTime = millis();
}

void randomDimRestore() {
  randomIntensityOffset = 0;
  isRandomDim = false;
}

Leave a Reply

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