Explore Nature’s Elements – Final Project by Hana

For the final project, I decided to create an experience which involves interactivity through buttons, led lights, a fan and a screen. During finals week we have so much to work on that we neglect nature and its healing powers, so I decided to bring that indoors and onto our screens as a way to create an enjoyable and relaxing experience.

This project consists of the screen display part which is coded on P5, and the user interactivity with buttons, a potentiometer, a fan and the neopixel ring lights. Here is a demo to show the experience better:

Interaction

One can switch through the different elements to experience them: rain, fire, or earth&wind. For a more immersive feel beyond the simulations on the screen, I decided to have light shows on a neopixel ring with patterns which imitate what is happening on the screen. The magnitude of the potentiometer affects what is happening in the simulations.

Higher magnitude in rain leads to more raindrops and at higher speeds. In the lightshow this translates to the raindrops rotating faster. The fan is activated during rain, as wind is often associated with rain.

Higher magnitude in fire leads to more fire particles and a bigger spread (essentially more fire). In the lightshow this is shown by how high the fire reaches the top of the ring. The fan is off at this time.

Magnitude in earth&wind affects the direction of the wind on the screen. The light show is unrelated to the work of the potentiometer, as the light show is imitating the piling of the leaves on the screen, which happens at a constant rate regardless of the values of the potentiometer. It is interesting to note that the constant rate of the leaf piling creates an hourglass effect on both the screen and the neopixel ring (the pile gets higher after a constant x amount of time). The fan is turned on to imitate the wind.

Arduino

The arduino works by taking as input the values of the switches and the potentiometers, and outputting the neopixel light show patterns as well as turning the fan on and off when appropriate.

The Fan

The fan required a higher voltage than the 5V that arduino supplies, so in order to get it to work I had to use a different power source and use a transistor in order to control the fan through the arduino. As for code, I simply had to do a digitalWrite of HIGH on the appropriate pin when I wanted it on and digitalWrite of LOW when I wanted it off.

Light Shows

I had to come up with my own patterns for the light shows to imitate best what I had come up with for the screen. Because of the ring shape of the neopixel, I had to get very creative, since the screen is a rectangle.

For the raindrops, I decided to create 4 raindrops which go around in a clockwise direction, and the magnitude of the rain would affect the speed at which they go around. To create a more interesting effect, I created the raindrops as a sort of trail, where the pixel at the front was the brightest, and the 2 pixels behind it each get dimmer, followed by fully dim pixels to distinguish between the rain drops. Here is the code for this pattern:

void raindrops(int del) {
  //map potentiometer value to del
  del = map(del, 40, 1020, 150, 30);
  // a total of 24 pixels
  // separate into 4 sections by tackling 6 pixels at a time
  for(int i=0; i<6; i++) { 
    //first pixels in the section are dark
    pixels.setPixelColor(i, pixels.Color(0, 0, 0));
    pixels.setPixelColor(i+6, pixels.Color(0, 0, 0));
    pixels.setPixelColor(i+12, pixels.Color(0, 0, 0));
    pixels.setPixelColor(i+18, pixels.Color(0, 0, 0));

    //second pixels in the section are dark
    pixels.setPixelColor(i+1, pixels.Color(0, 0, 0));
    pixels.setPixelColor(i+7, pixels.Color(0, 0, 0));
    pixels.setPixelColor(i+13, pixels.Color(0, 0, 0));
    pixels.setPixelColor(i+19, pixels.Color(0, 0, 0));

    //third pixels in the section are dark  
    pixels.setPixelColor(i+2, pixels.Color(0, 0, 0));
    pixels.setPixelColor(i+8, pixels.Color(0, 0, 0));
    pixels.setPixelColor(i+14, pixels.Color(0, 0, 0));
    pixels.setPixelColor(i+20, pixels.Color(0, 0, 0));

    //fourth pixels in the section are dim
    pixels.setPixelColor(i+3, pixels.Color(20, 20, 20));
    pixels.setPixelColor(i+9, pixels.Color(20, 20, 20));
    pixels.setPixelColor(i+15, pixels.Color(20, 20, 20));
    pixels.setPixelColor(i+21, pixels.Color(20, 20, 20));

    //fifth pixels in the section are brighter
    pixels.setPixelColor((i+4)%NUMPIXELS, pixels.Color(100,100,100));
    pixels.setPixelColor((i+10)%NUMPIXELS, pixels.Color(100,100,100));
    pixels.setPixelColor((i+16)%NUMPIXELS, pixels.Color(100,100,100));
    pixels.setPixelColor((i+22)%NUMPIXELS, pixels.Color(100,100,100));

    //sixth pixels (at the front) in the section are the brightest
    pixels.setPixelColor((i+5)%NUMPIXELS, pixels.Color(200, 200, 200));
    pixels.setPixelColor((i+11)%NUMPIXELS, pixels.Color(200, 200, 200));
    pixels.setPixelColor((i+17)%NUMPIXELS, pixels.Color(200, 200, 200));
    pixels.setPixelColor((i+23)%NUMPIXELS, pixels.Color(200, 200, 200));

    pixels.show();   // Send the updated pixel colors to the hardware.

    delay(del); //delay affects how fast the raindrops are moving
  }
}

For the fire, I decided to have the fire start at the bottom of the ring, and get reach higher the higher the value of the potentiometer. To create a realistic fire crackling effect, I had to experiment a lot with colors and dimness, the colors at the bottom had to be more yellow and brighter, and the colors at the top had to be more red and dimmer – and this all had to be relative to the total height of the fire. To achieve this, I had to do a lot of calculations and a lot of experimenting until I got it right. Here is the code which gave me the desired effect:

void fire(int maxNum){ //maxNum should be 4 to 12
  //maxNum refers to the height of the fire. the number corresponds 
  //to how many pixels will be colored on each side.
  maxNum = map(maxNum, 40, 1024, 4, 12);

  //changing delay so that the fire crackles and changes more
  //the larger the fire is
  DELAYVAL = map(maxNum, 4, 12, 50, 25);

  for(int i=0; i<maxNum; i++) { 
    //calculating the dimness in relation to how many pixels there are
    //in total
    double percentage=1-0.1*map(i, 0, maxNum, 0, 10);

    //do not dim the first maxNum/3 pixels
    //the fire at the bottom must be brightest
    if(i>maxNum/3){
      // pixels.setPixelColor(i, pixels.Color(int(random(100,255)*0.1*(maxNum-i)), int(random(40)*0.1*(maxNum-i)), 0));
      // pixels.setPixelColor(NUMPIXELS-i, pixels.Color(int(random(100,255)*0.1*(maxNum-i)), int(random(40)*0.1*(maxNum-i)), 0));
      pixels.setPixelColor(i, pixels.Color(int(random(100,255)*percentage), int(random(40)*percentage), 0));
      pixels.setPixelColor(NUMPIXELS-i, pixels.Color(int(random(100,255)*percentage), int(random(40)*percentage), 0));
    }
    else{
      //full brightness
      pixels.setPixelColor(i, pixels.Color(random(150,255), random(10,60), 0));
      pixels.setPixelColor(NUMPIXELS-i, pixels.Color(random(150,255), random(10,60), 0));
    }

    pixels.show();   // Send the updated pixel colors to the hardware.

    delay(DELAYVAL); // Pause before next pass through loop

  }
  //turn off the pixels above the current height, if the height decreases
  for(int i=maxNum; i<13; i++) {
    pixels.setPixelColor(i, pixels.Color(0,0,0));
    pixels.setPixelColor(NUMPIXELS-i, pixels.Color(0,0,0));
  }
}

For the leaves, I wanted to imitate the pile of leaves which is being accumulated on the screen. So I get the level of pile that has been reached from P5, and light up a corresponding number of pixels. I use alternating shades of pink to imitate the colors of the leaves on the screen, which are also alternating. Here is the code:

void leaves(int leavesNum) {
  for(int i=0; i<leavesNum; i++) {
    //turn on fan
    digitalWrite(fan, HIGH);

    //light up two pixels at a time on each side
    //left side
    pixels.setPixelColor(2*i, pixels.Color(255, 0, 144));
    pixels.setPixelColor(2*i+1, pixels.Color(255, 69, 174));

    //right side
    pixels.setPixelColor(NUMPIXELS-2*i, pixels.Color(255, 0, 144));
    pixels.setPixelColor(NUMPIXELS-2*i+1, pixels.Color(255, 69, 174));

    pixels.show();
    delay(1);
  }
}

Beyond that in arduino, I receive values from the buttons to determine which simulation to experience, and display the appropriate light show, and turn on and off the fan whenever needed. I will discuss the arduino/P5 connection below.

P5

For the simulations, I utilized objects and arrays to create them. If you notice, each of the simulations is made up of small similar particles which are created and disappear in different ways. We have raindrops, fire particles and leaves. The code for each of the classes is quite similar, with small differences to create the different simulations, as the particles look and behave differently for each of the simulations, but they follow a similar structure. Here is the code for one of them:

class RainDrop {
  constructor() {
    //randomly decide horizontal location
    this.x = random(0, width);
    //start at the top of the screen
    this.y = 0;
    //random vertical speed
    this.vy = random(5,8);
    //random vertical diameter
    this.d = random(5,8);
    //color variation, create different shades
    //of light blue
    this.R = random(190,200);
    this.G = random(190, 200);
    this.B = random(200,255);
  }

  //finished when they reach the bottom of the screen
  finished() {
    return this.y > 600;
  }

  //update postition based on vertical speed
  update() {
    //add additional boost based on the value of the 
    //potentiometer to increase vertical speed
    this.y += this.vy + map(potVal, 100, 1020, 0, 3);
  }

  //drawing out the particle
  show() {
    noStroke();
    fill(this.R, this.G, this.B);
    ellipse(this.x, this.y, 5, this.d);
  }
}

And here is how the objects are created, updated and deleted:

function create_rain() {
  //redraw rain background
  image(rain_bg, 0, 0, 600, 400);
  
  //max num of raindrops created at a time
  //determined by the potentiometer
  let maxI = map(potVal, 40, 1020, 2, 10);
  
  if(frameCount%5==0){
    for (let i = 0; i < maxI; i++) { 
      let p = new RainDrop();
      rain_drops.push(p);
    }
  }
  //update raindrops, and delete them when they are finished
  for (let i = rain_drops.length - 1; i >= 0; i--) {
    rain_drops[i].update();
    rain_drops[i].show();
    if (rain_drops[i].finished()) {
      rain_drops.splice(i, 1);
    }
  }
}

Since the code for the other simulations follows a similar structure, I will not attach it so as not to be repetitive.

P5/Arduino Communication

At first a handshake is established to ensure communication exists before proceeding with the program:

//arduino code

while (Serial.available() <= 0) {
    Serial.println("-1"); // send a starting message
    delay(300);            // wait 1/3 second
  }

Then, the arduino constantly sends the values of the potentiometer and the values of which button was pressed last. It reads from P5 the value of the leaf pile, which it uses later for the light show:

int leavesNum = Serial.parseInt();
if (Serial.read() == '\n') {
   Serial.print(potPosition);
   Serial.print(",");
   Serial.println(currentButton);
}

Here is how P5 reads these values and also sends the leaf pile value:

// This function will be called by the web-serial library
// with each new *line* of data. The serial library reads
// the data until the newline and then gives it to us through
// this callback function
function readSerial(data) {
  ////////////////////////////////////
  //READ FROM ARDUINO HERE
  ////////////////////////////////////

  if (data != null) {
    // make sure there is actually a message
    // split the message
    let fromArduino = split(trim(data), ",");
    // if the right length, then proceed
    if (fromArduino.length == 2) {
      // store values 
      //potentiometer value
      potVal = fromArduino[0];
      currentButton = fromArduino[1];
    }

    //////////////////////////////////
    //SEND TO ARDUINO HERE
    //////////////////////////////////
    let sendToArduino = leaf_bg_choice+1 + "\n";
    writeSerial(sendToArduino);
  }
}

Strengths

As a computer science student, I am comfortable with programming. For this project I wanted to challenge myself creatively. From the creativity of the idea itself, to the screen simulations, light shows and even the crafty parts of assembling the interactive box – I pushed myself creatively as much as I could. This was an incredible learning experience for me. I learned that I can be quite resourceful even with hand work such as creating the box out of cardboard and paper, creating a cardboard ring and pinning down cloth to create a ‘lampshade’ for the neopixel. I also had so many connections and circuits, and that lead to many challenges, but it also taught me so much.

As for the user experience, I think it serves its purpose quite well – it is very simple in its usage, that the testers which you will see below required no instruction before or after to learn how to use it. And it is quite a relaxing experience, you just get to play around and enjoy the different elements, and enjoy my creative efforts at visual arts – whether it is the screen simulations or the light shows. The testers particularly enjoyed the light shows, and were very pleasantly surprised by the fan.

Weaknesses

While working on this project I ran into problem after problem. Starting with the problems I anticipated well in advance, such as learning how to use a neopixel, learning how to connect a fan and learning how to solder. These took only a matter of seeking out help from my professor and/or the internet depending on the task – so they were easier challenges to overcome, I just needed to educate myself on how to do them.

The unexpected problems were quite tough. Since I am dealing with arrays of objects in my code, I had to work on optimizing the code so I do not overwork the computer and have it result to low fps. I already had plenty of experience with that in my midterm project, so this was a relatively easy fix too.

My biggest problem was with the potentiometer. Unfortunately, I am still unsure that I solved the issue at the root – but I learned how to avoid the problem. As long as I arrange my circuits evenly, and keep the wires neat and straight, the potentiometer will give me values that are stable enough for my purposes. But the problem is that the values are not entirely stable. For my purposes though, the problem is somewhat solved – at least enough for my project to work fine.

I believe a lot of the problem from above came from using a breadboard, if I used soldering instead I would have a more stable connection and thus more stable values. In a case where this was a more permanent installation, I would most definitely have to switch out of the breadboard – but this works currently.

User testing

I had two people come and test the project with no instructions beforehand. Both of them had no problems figuring out exactly how the experience works and being able to play around with it exactly as intended. They understood exactly what the buttons and the potentiometer did and would check the screen and the neopixel for the results of their work with the potentiometer.

The simplicity of the interactions worked very well, and both testers reported to have had an enjoyable and relaxing experience.

One area of improvement is with the buttons. The buttons themselves are not very sensitive, and the cardboard box is soft, so the user has to push hard to change the simulation. I put support right underneath the buttons to combat this, but it still wasn’t enough to entirely eliminate the inconvenience. However, the user does not need to push too hard, so it is easy to get used to pushing it the right amount – for my own tests I experienced no problems, but people using it for the first time faced a few challenges.

To fix the responsiveness of the buttons, I would switch out the cardboard box for something stiffer and stronger, like acrylic or wood.

Link to code

Leave a Reply