Self-Portrait: On the Run

Project Concept

My concept for this project is to portray a guy (supposedly) running on the road. Then I started to think about the elements I could add because just a static picture is boring. I figured the two elements I could improve are the background and the figure. I decided to add some animation to the figure and some interactivity with the user’s mouse so that it would chase the user’s mouse along the X-axis. For the background at first, I only wanted to implement a sky featuring a sunrise, that is to use a gradient color for the background. But it still seemed boring so I decided to add a moon and alternate them over time. Naturally, that comes with stars and different sky colors so I implemented that also. I already have the cycle for a day, why not extend it to a month? Therefore I also added phases of the moon. Then the project finally seemed complete, and it is shown below.

Code I am Proud of

I am not very proud of anything in particular actually, because it is all simple code, and not many calculations were needed. If I have to mention something then it would be the implementation of the Sky. It involved calculations based on deltaTime,  relative positions of the sun and the moon, integrated loops with the looping nature of the draw() function to ensure the coherency of sky color changes, and used a wide variety of shapes and curves. The code goes on like this, and the comments should be enough to explain the code.

class Star{
  constructor(){
    // Random position of stars
    this.x = random(400);
    this.y = random(400);
    this.speed = 1;
  }
  
  display(t){
    this.y -= this.speed * deltaTime / 20; // Star movement
    if(this.y <= 0){
      this.y = 400 - this.y % 400;
    }
    // Transparency control
    fill("rgba(255,255,255,"+t+")");
    stroke("rgba(255,255,255,"+t+")");
    let starBody = rect(this.x,this.y,2,2);
  }
}

class CrescentMoon{
  constructor(y){
    this.phase = 0;
    this.moonY = y;
  }
  
  display(){
    this.phase = this.phase % 16;
    fill("white");
    
    // Moon shape
    beginShape();
    vertex(200,this.moonY-75);
    if(this.phase <= 9){
      bezierVertex(200-(100-25*(this.phase)),this.moonY-75,200-(100-25*(this.phase)),this.moonY+75,200,this.moonY+75);
      bezierVertex(200+100,this.moonY+75,200+100,this.moonY-75,200,this.moonY-75);
    }
    else{
      bezierVertex(200-100,this.moonY-75,200-100,this.moonY+75,200,this.moonY+75);
      bezierVertex(200+(100-25*(16-this.phase)),this.moonY+75,200+(100-25*(16-this.phase)),this.moonY-75,200,this.moonY-75);
    }
    endShape();
  }
}

class Sky{
  constructor(){
    this.sunY = 400;
    this.speed = 1;
    this.moonPhase = 0;
    this.timer = 0;
  }
  
  display(){//10sec/round
    // Timer sync
    this.timer = this.timer % 20;
    this.sunY = -this.timer * 50 + 400;
    
    // Moon phase add
    if(this.sunY + 500 <= -125){
      this.moonPhase++;
    }
    
    // Create colors for gradient
    let topColor;
    let botColor;
    
    // Transparency control
    let displayVar;
    
    // Frames 0-300 back to sunrise
    if(this.timer < 3){
      topColor = lerpColor(color("rgb(23,23,197)"),color("rgb(135,206,235)"),this.timer/3);
      botColor = lerpColor(color("rgb(23,23,197)"),color("rgb(255,77,00)"),this.timer/3);
      displayVar = 1-(this.timer/3);
    }    
    // Frames 300-1000
    else if(this.timer >= 3 && this.timer < 10){
      // Frames 300-650 sunrise
      if(this.timer<6.5){
        botColor=lerpColor(color("rgb(255,77,00)"),color("rgb(135,206,235)"),(this.timer-3)/3.5);
      }
      // Frames 650-1000 day
      else{
        botColor = color("rgb(135,206,235)");
      }
      topColor = color("rgb(135,206,235)");
      displayVar = 0; // Transparent
    }
    // Frames 1000-1300 dawn
    else if(this.timer >= 10 && this.timer < 13){
      topColor = lerpColor(color("rgb(135,206,235)"),color("rgb(23,23,197)"),(this.timer-10)/3);
      botColor = lerpColor(color("rgb(135,206,235)"),color("rgb(23,23,197)"),(this.timer-10)/3);
      displayVar = (this.timer-10)/3;
    }
    // Frames 1300-2000 night
    else if(this.timer >= 13 && this.timer < 20){
      topColor = color("rgb(23,23,197)");
      botColor = color("rgb(23,23,197)");
      displayVar = 1; // Solid
    }
    
    // For loop used to create gradient
    for(let y = 0; y < 250; y++){
      let lineColor = lerpColor(topColor,botColor,y/250);
      stroke(lineColor);
      line(0,y,400,y);
    }
    
    // Display stars
    for(let i = 0; i < 120; i++){
        starlist[i].display(displayVar);
    }
    
    // Display sun and moon
    noStroke();
    fill("#FFC107");
    ellipse(200,this.sunY,150,150);
    moon = new CrescentMoon(this.sunY + 500);    
    moon.phase = this.moonPhase;
    moon.display();  
  }
}

Possible Improvements

As I said, I am not particularly proud of the project. It has lots of room for improvement in many aspects. To begin with, the code itself is sloppy and unorganized. It is stitched with different coding styles and practices, and it is unoptimized in aspects such as (but not limited to) time and methodology. This can be improved with more advanced coding techniques and better planning in code structure. The figure’s animation is also unsatisfying. Because p5js doesn’t support rigging, therefore it was extremely difficult to animate the figure. It was impossible to accurately illustrate each of the keyframes of the figure running because the figure is fundamentally just a set of shapes combined together. Each approximate keyframe takes a huge amount of time to finish. It also doesn’t support interpolating the keyframes so the intended fluctuations in the Y-axis to make the character more lively turned out to seem mechanical and shaky. I could try to smoothly interpolate the keyframes using algorithms, but I don’t have enough mathematical knowledge to support me in achieving this. This can be improved by using careful calculations to accurately orient the keyframes and adding a lot more frames to the animation, then using linear interpolation to connect the keyframes together. However, this method will still feel unnatural because linear interpolation will result in mechanical movements with sharp turns. I tried a few third-party libraries but none could really achieve this. For instance, I tried using Toxiclibsjs’ modules to create a skeleton, but it focuses more on physics instead of animation, therefore it still cannot be used for creating a perfect animation, unless I can find mathematical equations that could represent the movements of every joint, which I tried to do but failed dramatically.

One thought on “Self-Portrait: On the Run”

Leave a Reply