rigatoni-AnimatedLoop

This project felt like jumping into the deep end as far as rendering goes. I was inspired by Dan Shiffman's 4D Tesseract challenge, although I wanted to make the gif my own. Until now I hadn't really given any thought to there being dimensions beyond the ones we can comfortably perceive and how they work, and thinking about higher dimensions led me down a rabbit hole of awesome math and logic content by channels like Numberphile and 3B1B. Dan Shiffman's challenge in particular was in Java and was a lot more conducive to 3d with its P3D renderer, but I think I found a suitable workaround by drawing quads instead of individual points using WEBGL. I was also treating the easing function (doubleExpSig) as an after thought, but once I actually used it to control what I see as distance along the 4th dimension I was surprised by what a huge role it played in the aesthetic of the loop. I can't imagine pulling off a smooth and natural motion without it. That being said however, the gif doesn't convincingly feel "4D" and I was to revisit it once I have more time.

I didn't end up sketching very much for this assignment, but here's a few things I did that helped me keep track of what I was doing

I realized pretty early into the project that hardcoding in each of the 16 4d vertices was time-consuming and I often drew the quads in the wrong order. I decided to make use of modulus and int division to set the points in the right order.

This is me marking the points needed to make up each face. This is maybe a quarter of what I ended up needing and I think I could have spent more time on this planning phase.

// This is a template for creating a looping animation in p5.js (JavaScript). 
// When you press the 'F' key, this program will export a series of images into
// your default Downloads folder. These can then be made into an animated gif. 
// This code is known to work with p5.js version 0.6.0
// Prof. Golan Levin, 28 January 2018
 
// INSTRUCTIONS FOR EXPORTING FRAMES (from which to make a GIF): 
// 1. Run a local server, using instructions from here:
//    https://github.com/processing/p5.js/wiki/Local-server
// 2. Set the bEnableExport variable to true.
// 3. Set the myNickname variable to your name.
// 4. Run the program from Chrome, press 'f'. 
//    Look in your 'Downloads' folder for the generated frames.
// 5. Note: Retina screens may export frames at twice the resolution.
 
 
//===================================================
// User-modifiable global variables. 
var myNickname = "rigatoni";
var nFramesInLoop = 60;
var bEnableExport = true;
 
// Other global variables you don't need to touch.
var nElapsedFrames;
var bRecording;
var theCanvas;
 
//===================================================
function setup() {
  theCanvas = createCanvas(720, 720, WEBGL);
  bRecording = false;
  nElapsedFrames = 0;
}
 
//===================================================
function keyTyped() {
  if (bEnableExport) {
    if ((key === 'f') || (key === 'F')) {
      bRecording = true;
      nElapsedFrames = 0;
    }
  }
}
 
//===================================================
function draw() {
 
  // Compute a percentage (0...1) representing where we are in the loop.
  var percentCompleteFraction = 0;
  if (bRecording) {
    percentCompleteFraction = float(nElapsedFrames) / float(nFramesInLoop);
  } else {
    percentCompleteFraction = float(frameCount % nFramesInLoop) / float(nFramesInLoop);
  }
 
  // Render the design, based on that percentage. 
  // This function renderMyDesign() is the one for you to change. 
  renderMyDesign (percentCompleteFraction);
 
  // If we're recording the output, save the frame to a file. 
  // Note that the output images may be 2x large if you have a Retina mac. 
  // You can compile these frames into an animated GIF using a tool like: 
  if (bRecording && bEnableExport) {
    var frameOutputFilename = myNickname + "_frame_" + nf(nElapsedFrames, 4) + ".png";
    print("Saving output image: " + frameOutputFilename);
    saveCanvas(theCanvas, frameOutputFilename, 'png');
    nElapsedFrames++;
 
    if (nElapsedFrames >= nFramesInLoop) {
      bRecording = false;
    }
  }
}
 
//===================================================
function renderMyDesign (percent) {
  background(180);
  var cube = new Hypercube(500, percent)
  rotateY(percent*PI)
  cube.Draw()
}
 
function Hypercube(size, margin) {
	this.points = []
  margin -= 1
  margin = doubleExponentialSigmoid(margin)
	for(var i=0; i<16; i++) {
    var j = i
		var w = floor(j/8)*margin
    j=j%8 
    var stereo = 1/(2-w)
    var z = floor(j/4)*stereo-(0.5*stereo)
    j=j%4
    var y = floor(j/2)*stereo-(0.5*stereo)
    j=j%2
    var x = floor(j/1)*stereo-(0.5*stereo)
  	this.points[i] = new P4(x*size, y*size, z*size, 0)
  }
  this.Draw = function() {
    fill(225,15)
		var front = new Face(this.points[4], this.points[5], 
                         this.points[6], this.points[7])
		var back = new Face(this.points[0], this.points[1], 
                         this.points[2], this.points[3])
		var left = new Face(this.points[0], this.points[2], 
                         this.points[4], this.points[6]) 
    var right = new Face(this.points[1], this.points[3], 
                     this.points[5], this.points[7]) 
		var top = new Face(this.points[2], this.points[3], 
                         this.points[6], this.points[7]) 
		var bottom = new Face(this.points[0], this.points[1], 
                         this.points[4], this.points[5]) 
		var sFront = new Face(this.points[12], this.points[13], 
                         this.points[14], this.points[15])
		var sBack = new Face(this.points[8], this.points[9], 
                         this.points[10], this.points[11])
		var sLeft = new Face(this.points[8], this.points[10], 
                         this.points[12], this.points[14]) 
    var sRight = new Face(this.points[9], this.points[11], 
                     this.points[13], this.points[15]) 
		var sTop = new Face(this.points[10], this.points[11], 
                         this.points[14], this.points[15]) 
		var sBottom = new Face(this.points[8], this.points[9], 
                         this.points[12], this.points[13]) 
 
    var pfront = new Face(this.points[4], this.points[5], 
                         this.points[12], this.points[13])
		var pback = new Face(this.points[0], this.points[1], 
                         this.points[8], this.points[9])
		var pleft = new Face(this.points[0], this.points[2], 
                         this.points[8], this.points[10]) 
    var pright = new Face(this.points[1], this.points[3], 
                     this.points[9], this.points[11]) 
		var ptop = new Face(this.points[2], this.points[3], 
                         this.points[10], this.points[11]) 
		var pbottom = new Face(this.points[0], this.points[4], 
                         this.points[8], this.points[12]) 
		var psFront = new Face(this.points[1], this.points[5], 
                         this.points[9], this.points[13])
		var psBack = new Face(this.points[5], this.points[7], 
                         this.points[13], this.points[15])
		var psLeft = new Face(this.points[3], this.points[7], 
                         this.points[11], this.points[15]) 
    var psRight = new Face(this.points[2], this.points[6], 
                     this.points[10], this.points[14]) 
		var psTop = new Face(this.points[6], this.points[7], 
                         this.points[14], this.points[15]) 
		var psBottom = new Face(this.points[4], this.points[6], 
                         this.points[12], this.points[14]) 
 
  	front.Draw()
    back.Draw()
    left.Draw()
    right.Draw()
    sFront.Draw()
    sBack.Draw()
    sLeft.Draw()
    sRight.Draw()
  	pfront.Draw()
    pback.Draw()
    pleft.Draw()
    pright.Draw()
    psFront.Draw()
    psBack.Draw()
    psLeft.Draw()
    psRight.Draw() 
  }
}
 
function Face(p1, p2, p3, p4) {
  var distance = 200
  this.p1 = p1
  this.p2 = p2
  this.p3 = p3
  this.p4 = p4
 
  this.Draw = function() {
    beginShape()
    	vertex(this.p1.x,this.p1.y,this.p1.z)
    	vertex(this.p2.x,this.p2.y,this.p2.z)
    	vertex(this.p4.x,this.p4.y,this.p4.z)
    	vertex(this.p3.x,this.p3.y,this.p3.z)
    endShape(CLOSE)
  }
 
  this.Print = function() {
  	 this.p1.Print()
     this.p2.Print()
     this.p3.Print()
     this.p4.Print()
  }
}
 
function P4(x,y,z,w) {
	this.x = x
  this.y = y
  this.z = z
  this.w = w
  this.Print = function() {
		print(this.x, this.y, this.z, this.w) 
  }
  this.ScalarMult = function(multiplier) {
		this.x*=multiplier
    this.y*=multiplier
    this.z*=multiplier
    this.w*=multiplier
  }
}
// Symmetric double-element sigmoid function ('_a' is the slope)
// See https://github.com/IDMNYU/p5.js-func/blob/master/lib/p5.func.js
// From: https://idmnyu.github.io/p5.js-func/
//===================================================
function doubleExponentialSigmoid (_x, _a){
  if(!_a) _a = 0.75; // default
 
  var min_param_a = 0.0 + Number.EPSILON;
  var max_param_a = 1.0 - Number.EPSILON;
  _a = constrain(_a, min_param_a, max_param_a);
  _a = 1-_a;
 
  var _y = 0;
  if (_x<=0.5){
    _y = (pow(2.0*_x, 1.0/_a))/2.0;
  }
  else {
    _y = 1.0 - (pow(2.0*(1.0-_x), 1.0/_a))/2.0;
  }
  return(_y);
}

chaine-AnimatedLoop

When I was first brainstorming ideas for my looping gif, I wanted to involve something "gobstopper-esque" just because of its simple but striking colors. I also somehow wanted to convey bursts of "flavor" and change into my gif, so I already knew from the start that I wanted to use the mapping function a lot on the colors. Looking back, I thought my sketches were very basic and almost too simple. I ended up making it more complicated both to make my gif more interesting and also to test myself. I started off with the given double exponential sigmoid function to drag my gobstopper down from the top to the center of the canvas, but I decided that starting straight from the center looked better because the circle wasn't chopped up in the beginning. Then I used the bounce in, bounce out, and bounce in and out functions to make my gobstoppers pop, and I thought that this function suited how they tasted. And finally, I used the exponential for the circle directly in the center to overtake the canvas as the frames went on. I think I did alright with the color palettes and color changes, but I would have liked to incorporate more functions and more elements to this gif. I also think the way the white circle grows larger is awkward and would like to make it maybe so that it "bounces" like its other circular counterparts and finds a more natural way of clearing the canvas.

// This is a template for creating a looping animation in p5.js (JavaScript). 
// When you press the 'F' key, this program will export a series of images into
// your default Downloads folder. These can then be made into an animated gif. 
// This code is known to work with p5.js version 0.6.0
// Prof. Golan Levin, 28 January 2018
 
// INSTRUCTIONS FOR EXPORTING FRAMES (from which to make a GIF): 
// 1. Run a local server, using instructions from here:
//    https://github.com/processing/p5.js/wiki/Local-server
// 2. Set the bEnableExport variable to true.
// 3. Set the myNickname variable to your name.
// 4. Run the program from Chrome, press 'f'. 
//    Look in your 'Downloads' folder for the generated frames.
// 5. Note: Retina screens may export frames at twice the resolution.
 
 
//===================================================
// User-modifiable global variables. 
var myNickname = "chaine";
var nFramesInLoop = 240;
var bEnableExport = true;
 
// Other global variables you don't need to touch.
var nElapsedFrames;
var bRecording;
var theCanvas;
 
//===================================================
function setup() {
  theCanvas = createCanvas(640, 640);
  bRecording = false;
  nElapsedFrames = 0;
}
 
//===================================================
function keyTyped() {
  if (bEnableExport) {
    if ((key === 'f') || (key === 'F')) {
      bRecording = true;
      nElapsedFrames = 0;
    }
  }
}
 
//===================================================
function draw() {
 
  // Compute a percentage (0...1) representing where we are in the loop.
  var percentCompleteFraction = 0;
  if (bRecording) {
    percentCompleteFraction = float(nElapsedFrames) / float(nFramesInLoop);
  } else {
    percentCompleteFraction = float(frameCount % nFramesInLoop) / float(nFramesInLoop);
  }
 
  // Render the design, based on that percentage. 
  // This function renderMyDesign() is the one for you to change. 
  renderMyDesign (percentCompleteFraction);
 
  // If we're recording the output, save the frame to a file. 
  // Note that the output images may be 2x large if you have a Retina mac. 
  // You can compile these frames into an animated GIF using a tool like: 
  if (bRecording &amp;&amp; bEnableExport) {
    var frameOutputFilename = myNickname + "_frame_" + nf(nElapsedFrames, 4) + ".png";
    print("Saving output image: " + frameOutputFilename);
    saveCanvas(theCanvas, frameOutputFilename, 'png');
    nElapsedFrames++;
 
    if (nElapsedFrames &gt;= nFramesInLoop) {
      bRecording = false;
    }
  }
}
 
//===================================================
function renderMyDesign (percent) {
  //
  // THIS IS WHERE YOUR ART GOES. 
  // This is an example of a function that renders a temporally looping design. 
  // It takes a "percent", between 0 and 1, indicating where we are in the loop. 
  // Use, modify, or delete whatever you prefer from this example. 
  // This example uses several different graphical techniques. 
  // Remember to SKETCH FIRST!
 
  //----------------------
  // here, I set the background and some other graphical properties
  background(255);
  smooth();
  stroke(0, 0, 0);
  strokeWeight(2);
 
  //----------------------
  // Here, I assign some handy variables. 
  var cx = 320;
  var cy = 320;
  noStroke();
  var arcSize = 20;
  var topY = 0 - arcSize - 2;
  var botY = height + 2;
 
    push();
 
    //I ended up not using this because it looked better starting centered
    //var eased = doubleExponentialSigmoid ((percent), 0.7); 
    //eased = (eased)%1.0; 
    //var yPosition2 = map(eased, 0, 1, topY, botY/2); 
 
    var eased2 = exponential (percent*1.2);
    var circleGrow = map(eased2, 0, 2, 0, 600);
    //ellipse (width/2, yPosition2, 20, 20);
 
    var eased3 = bounceInOut (percent * 0.5);
    var circle2Grow = map(eased3, 0, 1, 0, 2000);
 
    var eased4 = bounceInOut (percent * 0.5);
    var circle3Grow = map(eased4, 0, 1, 0, 1500);
 
    var eased5 = bounceInOut (percent * 0.5);
    var circle4Grow = map(eased5, 0, 1, 0, 1000);
 
    var easedLast = exponential (percent);
    var easedGrow = map(easedLast, 0, 1, 0, 1000);
 
    //black arc
    var test = map(percent, 0, 1, 0, 2*PI);
    //neon green arc
    var test1 = map(percent, 0, 1, 0, (3 * PI)/2);
    //light green arc
    var test2 = map(percent, 0, 1, 0, PI);
    //pink arc
    var test3 = map(percent, 0, 1, 0, PI/2);
 
 
    var arcCol1 = map(percent, 0.2, 1, 255, 204);
    var arcCol11 = map(percent, 0.2, 1, 204, 0);
    var arcCol111 = map(percent, 0.2, 1, 229, 102);
 
    //arc 1 light pink 255 204 229 -&gt; 204 0 102
    fill (arcCol1,arcCol11,arcCol111);
    arc(width/2, height/2, 1100*percent,1100*percent,0,test);
 
    var arcCol2 = map(percent, 0.2, 1, 204, 76);
    var arcCol22 = map(percent, 0.2, 1, 255, 153);
    var arcCol222 = map(percent, 0.2, 1, 229, 0);
 
    //arc 2 light green 204 255 229 -&gt; 76 153 0
    fill (arcCol2, arcCol22, arcCol222);
    arc(width/2, height/2, 1100*percent,1100*percent,0,test1);
 
 
    var arcCol3 = map(percent, 0.2, 1, 200, 0);
    var arcCol33 = map(percent, 0.2, 1, 255, 102);
    var arcCol333 = map(percent, 0.2, 1, 200, 102);
 
    //arc 3 200 255 200 -&gt; 0 102 102
    fill (arcCol3, arcCol33, arcCol333);
    arc(width/2, height/2, 1100*percent,1100*percent,0,test2);
 
    var arcCol4 = map(percent, 0.2, 1, 204, 76);
    var arcCol44 = map(percent, 0.2, 1, 229, 0);
    var arcCol444 = map(percent, 0.2, 1, 255, 153);
 
    //arc 4 light blue 204 229 255 -&gt; 76 0 153
    fill (arcCol4, arcCol44, arcCol444);
    arc(width/2, height/2, 1100*percent,1100*percent,0,test3);
 
 
    var circleCol1 = map(eased3, 0, 0.5, 102, 0);
    var circleCol11 = map(eased3, 0, 0.5, 178, 0);
    var circleCol111 = map(eased3, 0, 0.5, 255, 0);
 
    //center circle 1 pink 255 102 178
    fill (circleCol111,circleCol1,circleCol11); 
    ellipse (width/2, height/2, circle2Grow, circle2Grow); 
 
    var circleCol2 = map(eased4, 0, 0.75, 255, 0);
    var circleCol22 = map(eased4, 0, 0.75, 102, 0);
    var circleCol222 = map(eased4, 0, 0.75, 255, 0);
 
    //center circle 2 lighter pink 255 102 255
    fill (circleCol2, circleCol22, circleCol222); 
    ellipse (width/2, height/2, circle3Grow, circle3Grow); 
 
    var circleCol3 = map(eased5, 0, 1, 255, 0);
    var circleCol33 = map(eased5, 0, 1, 204, 0);
    var circleCol333 = map(eased5, 0, 1, 255, 0);
 
    //center circle 2 lightest pink 255 204 255
    fill (circleCol3, circleCol33, circleCol333); 
    ellipse (width/2, height/2, circle4Grow, circle4Grow); 
 
 
    var circleCol0 = map(easedLast, 0.1, 0.15, 200, 255);
    var circleCol00 = map(easedLast, 0.1, 0.15, 200, 255);
    var circleCol000 = map(easedLast, 0.1, 0.15, 200, 255);
 
    //center circle 2 white
    fill (circleCol0, circleCol00, circleCol000); 
    ellipse (width/2, height/2, easedGrow, easedGrow); 
 
 
    pop();
 
}
 
 
// Symmetric double-element sigmoid function ('_a' is the slope)
// See https://github.com/IDMNYU/p5.js-func/blob/master/lib/p5.func.js
// From: https://idmnyu.github.io/p5.js-func/
//===================================================
function doubleExponentialSigmoid (_x, _a){
  if(!_a) _a = 0.75; // default
 
  var min_param_a = 0.0 + Number.EPSILON;
  var max_param_a = 1.0 - Number.EPSILON;
  _a = constrain(_a, min_param_a, max_param_a);
  _a = 1-_a;
 
  var _y = 0;
  if (_x&lt;=0.5){
    _y = (pow(2.0*_x, 1.0/_a))/2.0;
  }
  else {
    _y = 1.0 - (pow(2.0*(1.0-_x), 1.0/_a))/2.0;
  }
  return(_y);
}
 
function exponential (_x){
  return((_x == 0.0) ? _x : pow(2, 10 * (_x - 1)));
}
 
function bounceIn (_x){
  return(1 - this.bounceOut(1 - _x));
}
 
function bounceOut (_x){
    if(_x &lt; 4/11.0)
    {
      return((121 * _x * _x)/16.0);
    }
    else if(_x &lt; 8/11.0)
    {
      return((363/40.0 * _x * _x) - (99/10.0 * _x) + 17/5.0);
    }
    else if(_x &lt; 9/10.0)
    {
      return((4356/361.0 * _x * _x) - (35442/1805.0 * _x) + 16061/1805.0);
    }
    else
    {
      return((54/5.0 * _x * _x) - (513/25.0 * _x) + 268/25.0);
    }
}
 
function bounceInOut (_x){
      if(_x &lt; 0.5)
    {
      return(0.5 * this.bounceIn(_x*2));
    }
    else
    {
      return(0.5 * this.bounceOut(_x * 2 - 1) + 0.5);
    }
}
 
function exponentialEmphasis (_x, _a){
    if(!_a) _a = 0.25; // default
 
    var min_param_a = 0.0 + Number.EPSILON;
    var max_param_a = 1.0 - Number.EPSILON;
    _a = constrain(_a, min_param_a, max_param_a);
 
    if (_a &lt; 0.5) {
      // emphasis
      _a = 2*(_a);
      var _y = pow(_x, _a);
      return(_y);
    }
    else {
      // de-emphasis
      _a = 2*(_a-0.5);
      var _y = pow(_x, 1.0/(1-_a));
      return(_y);
    }
}

ocannoli-AnimatedLoop

Sketches:

GIF:

Description:

In conceptualizing the piece, I had a lot of grand ideas about recreating something in 8-Bit from the original Legend of Zelda game or making a pong-like loop with new colors. Mainly, I was experimenting with circles and opacity to ultimately just mess around. There were many experiments with small pong sized rotating circles and creating weird shapes but eventually my sketches ended up morphing into to creating a minimalist start button for a game. I used PennerEaseOutExpo easing function for the background white circle to create a slight scrolling effect and add more movement. Admittedly, I did not spend as much time as I wished I could on the project; therefore, I'm not very proud of the results, but it was a good introduction to start thinking about experimenting with graphics and the many possibilities regarding such. I feel like I succeeded in some aesthetic aspects and creating a good basis but not the strongest for a final product.

Code:

// Global variables. 
String  myNickname = "nickname"; 
int     nFramesInLoop = 120;
int     nElapsedFrames;
boolean bRecording; 
 
//===================================================
void setup() {
  size (640, 640); 
  bRecording = false;
  nElapsedFrames = 0;
}
//===================================================
void keyPressed() {
  if ((key == 'f') || (key == 'F')) {
    bRecording = true;
    nElapsedFrames = 0;
  }
}
 
//===================================================
void draw() {
 
  // Compute a percentage (0...1) representing where we are in the loop.
  float percentCompleteFraction = 0; 
  if (bRecording) {
    percentCompleteFraction = (float) nElapsedFrames / (float)nFramesInLoop;
  } else {
    percentCompleteFraction = (float) (frameCount % nFramesInLoop) / (float)nFramesInLoop;
  }
 
  // Render the design, based on that percentage. 
  renderMyDesign (percentCompleteFraction);
 
  // If we're recording the output, save the frame to a file. 
  if (bRecording) {
    saveFrame("frames/" + myNickname + "_frame_" + nf(nElapsedFrames, 4) + ".png");
    nElapsedFrames++; 
    if (nElapsedFrames &gt;= nFramesInLoop) {
      bRecording = false;
    }
  }
}
 
//===================================================
void renderMyDesign (float percent) {
  background (5, 44, 64);
  smooth(); 
  stroke (0, 0, 0); 
  strokeWeight (2); 
  // Here, I assign some handy variables. 
  float cx = 100;
  float cy = 100;
    //----------------------
   //Here's a sigmoidally-moving pink square!
   //This uses the "Double-Exponential Sigmoid" easing function 
   //from https://github.com/golanlevin/Pattern_Master
  float eased = function_PennerEaseOutExpo (percent); 
  float yPosition2 = map(eased, 0, 1, 0, 640); 
  noStroke();
  fill (255, 255, 255);
  yPosition2=yPosition2-600;
  for(int j=0; j&lt;=9;j++){
    int rX=-40;
    for(int i=0; i&lt;10; i++){
    rX=rX+90;
    if(i!=2 &amp;&amp; i!=3 &amp;&amp; i!=4){
    ellipse (rX, yPosition2, 15, 15);
    }
    }
    yPosition2+=120;
  }
  //----------------------
  // Here's a pulsating ellipse
  float ellipsePulse = sin ( 1.0 * percent * TWO_PI); 
  float ellipseW = map(ellipsePulse, -1, 1, 200, 400); 
  float ellipseH = map(ellipsePulse, -1, 1, 200, 400); 
  float ellipseColor = map(ellipsePulse, -1, 1, 239, 200); 
  float ellipsePulse2 = sin ( 2.0 * percent * TWO_PI); 
  float ellipseW2 = map(ellipsePulse2, -1, 1, 200, 350); 
  float ellipseH2 = map(ellipsePulse2, -1, 1, 200, 350);
  float ellipsePulse3 = sin ( 1.7* percent * TWO_PI); 
  float ellipseW3= map(ellipsePulse3, -1, 1, 10, 250); 
  float ellipseH3 = map(ellipsePulse3, -1, 1, 10, 250);
  noStroke();
  fill (252, ellipseColor, ellipseColor, 70); 
  ellipse (325, 300, ellipseW, ellipseH); 
  ellipse (325, 300, ellipseW2-30, ellipseH2-30);  
  ellipse (325, 300, ellipseW2-30, ellipseH2-30);
}
//===================================================
// Taken from https://github.com/golanlevin/Pattern_Master
float function_PennerEaseOutExpo(float t) {
  //functionName = "Penner's EaseOut Exponential";
  return (t==1) ? 1 : (-pow(2, -10 * t) + 1);
}

 

sapeck-AnimatedLoop


1920x1920 H.264 (MP4) video version (2.8 MB)

I began this piece by mapping my pixel art from my sketchbook into a spreadsheet. This gave me a grid of 0's and 1's to create my stick figure bitmaps. I gave each row the same Generalized Blackman Window easing function but with slightly different parameters based based on Perlin noise. The differing parameters adds the "lagging" effect to the animation. The Blackman Window function starts with a slowly-ramping curve that then spikes and returns slows down. I feel that the animation is a little boring. A continuous scroll of five or so figures would be more interesting. The background gradient reminds me more of Nyan Cat than I want it to. A single-tone gradient for each row would be less eye-straining and just as appealing.

/* Sapeck    9/12/2017
"sapeck-AnimatedLoop"
Based on a template for creating a looping animation in Processing/Java by Prof. Golan Levin, January 2018
60-212                        Carnegie Mellon University
Copyright (C) 2018-present  Sapeck, Prof. Golan Levin
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*/
 
//===================================================
// Global variables. 
String  myNickname = "sapeck"; 
int     nFramesInLoop = 500;
int     nElapsedFrames;
boolean bRecording; 
 
//===================================================
void setup() {
  size (640, 640);
  noiseSeed(283092);
  colorMode(HSB, 100);
  bRecording = false;
  nElapsedFrames = 0;
}
//===================================================
void keyPressed() {
  if ((key == 'f') || (key == 'F')) {
    bRecording = true;
    nElapsedFrames = 0;
  }
}
 
//===================================================
void draw() {
 
  // Compute a percentage (0...1) representing where we are in the loop.
  float percentCompleteFraction = 0; 
  if (bRecording) {
    percentCompleteFraction = (float) nElapsedFrames / (float)nFramesInLoop;
  } else {
    percentCompleteFraction = (float) (frameCount % nFramesInLoop) / (float)nFramesInLoop;
  }
 
  // Render the design, based on that percentage. 
  renderMyDesign (percentCompleteFraction);
 
  // If we're recording the output, save the frame to a file. 
  if (bRecording) {
    saveFrame("frames"+width+"/" + myNickname + "_frame_" + nf(nElapsedFrames, 4) + ".png");
    nElapsedFrames++; 
    if (nElapsedFrames >= nFramesInLoop) {
      bRecording = false;
    }
  }
}
 
int DIMENSION = 20;
int colorFilled = color(0,0,100,255);
int LOOPS = 3;
PVector easeCurveBase = new PVector(0.1, 0);
int NOISESEED = 283092;
 
void renderMyDesign (float percent) {
  background(0);
  smooth();
 
  for (int y=0;y<DIMENSION;y++) {
    pushMatrix();
    boolean right = (y % 2 == 0);
    right = false;
    int moveX = 0;
    int startRow = 0;
    PVector thisEaseCurve = new PVector(easeCurveBase.x+0.3*noise(y*10)/1, easeCurveBase.y+noise(y*10)/1);
    if (y > (DIMENSION-1)/2) thisEaseCurve = new PVector(easeCurveBase.x-0.3*noise(y*10)/1, easeCurveBase.y-noise(y*10)/1);
    float thisPercent = percent;
    thisPercent = function_GeneralizedBlackmanWindow(percent,thisEaseCurve.x);
    if (right) {
      startRow = -1*(LOOPS-1)*width;
      moveX = int(thisPercent*(LOOPS-1)*width);
    } else {
      moveX = int(-1*thisPercent*(LOOPS-1)*width);
    }
    translate(moveX ,0);
    for (int loop=0;loop<LOOPS;loop++) {
      for (int x=0;x<DIMENSION;x++) {
        int thisBox = 0;
        if (loop == LOOPS-1 && right) thisBox = MAN1[y][x];
        else if (loop == LOOPS-1 && !right) thisBox = MAN2[y][x];
        else if (loop == 0 && right) thisBox = MAN2[y][x];
        else if (loop == 0 && !right) thisBox = MAN1[y][x];
        PVector thisLoc = new PVector(x*(width/DIMENSION)+(loop*width)+startRow, y*(height/DIMENSION));
        if (thisBox == 1) {
          fill(colorFilled);
          rect(thisLoc.x, thisLoc.y, width/DIMENSION, height/DIMENSION);
        } else {
          int colorX = x+y;
          if (colorX > DIMENSION) colorX -= DIMENSION;
          fill(color(map(colorX,0,DIMENSION,0,100),
                     100,
                     100));
          rect(thisLoc.x, thisLoc.y, width/DIMENSION, height/DIMENSION);
        }
      }
    }
    popMatrix();
  }
}
 
int[][] MAN1 = {
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0},
  {0,0,0,1,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,0,0,0,0},
  {0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};
int[][] MAN2 = {
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};
 
 
//===================================================
// Taken from https://github.com/golanlevin/Pattern_Master
float function_GeneralizedBlackmanWindow (float x, float a) {
  // http://en.wikipedia.org/wiki/Window_function
  float a0 = (1.0 - a)/2.0;
  float a1 = 0.5;
  float a2 = a / 2.0;
 
  float pix = PI*x;
  float y = a0 - a1*cos(2*pix) + a2*cos(4*pix);
  return y;
}

weirdie-AnimatedLoop

My initial inspiration for the piece was simply thinking about how objects could visually track movement, and because I'm me I wanted to do that in kind of a weird way. The first step was creating the eyes that would be able to track movement, initially using the mouse for testing. After that, I tried a bunch of functions to find a path for the fly that I liked, eventually picking a 3-petaled polar graph "flower" because of the way it looped around 3 of the eyes. Getting the fly to face the direction it was traveling was a little tricky, as that involved calculating the direction of the next frame of the fly and rotating it towards that. I chose the Double Exponential Ogee function because I wanted the fly to slow down slightly going around corners, and speed up when it was traveling in more of a straight line, and the Ogee function had that pattern - fast, slow, fast - and so I used that while phase-shifting it a bit. Overall, I'm pretty pleased with the result, as I came pretty close to that concept that I had in mind and I learned a lot about mapping and direction changes in the process. I initially thought about having some of the eyes blink randomly or blink in response to the fly coming too close. However, with the short length of the GIF I chose not too, but it could be something to try in the future. I also think the addition of motion blur to the fly would have made the appearance more smooth.

Sketches:

Code:

function renderMyDesign(percent) {
 
  // here, I set the background
	background(255, 147, 140);
  smooth();
 
  // coordinates of the fly
  var flyx = 0;
  var flyy = 0;
 
  var p = map(percent, 0, 1, 0, 3.14);
 
  if(percent &gt;=0 &amp;&amp; percent &lt;= 0.3333) { var frac = map(percent, 0, 0.3333, 0, 1); var speed = function_DoubleExponentialOgee(frac, 0.15); p = map(speed, 0, 1, 0, PI/3); } else if(percent &gt; 0.333 &amp;&amp; percent &lt;= 0.666)
  {
    var frac = map(percent, 0.3333, 0.666, 0, 1);
  	var speed = function_DoubleExponentialOgee(frac, 0.15);
  	p = map(speed, 0, 1, PI/3, 2*PI/3);
  }
  else
  {
    var frac = map(percent, 0.666, 1, 0, 1);
  	var speed = function_DoubleExponentialOgee(frac, 0.15);
  	p = map(speed, 0, 1, 2*PI/3, PI);
  }
 
  var r = cos(3*(p+PI/2));
  var nr = cos(3*(p+0.0157+PI/2));
 
  var jousx = map(r*cos((p+PI/3+PI/2)), -1, 1, 30, 630);
  var jousy = map(r*sin((p+PI/3+PI/2)), -1, 1, 30, 630);
  var nextx = map(nr*cos((p+0.0157 + PI/3+PI/2)), -1, 1, 30, 630);
  var nexty = map(nr*sin((p+0.0157 + PI/3+PI/2)), -1, 1, 30, 630);
 
  var jx = map(jousx, 30, 630, -1, 1);
  var jy = map(jousy, 30, 630, -1, 1);
  var nx = map(nextx, 30, 630, -1, 1);
  var ny = map(nexty, 30, 630, -1, 1);
 
  var direction = atan2((ny - jy), (nx - jx)) + PI/2.2;
 
  flyx = jousx;
  flyy = jousy;
 
  //eyeballs!!!
  noStroke();
 
  for (var r = 0; r &lt; 5; r++) {
    for (var c = 0; c &lt; 4; c++) {
      var midx = 0;
      var midy = 0;
      if (r % 2 == 0) {
        midx = 106.66 + c * 213.33;
        midy = 160 * r;
      } else {
        midx = 213.333 * c;
        midy = 160 * r
      }
 
      //direction of the fly
      var fx = map(flyx, 0, width, -1, 1);
      var fy = map(flyy, 0, height, -1, 1);
      var mx = map(midx, 0, width, -1, 1);
      var my = map(midy, 0, height, -1, 1);
      var dir = atan2((fy - my), (fx - mx));
 
      var amp = 30;
      if (dist(midx, midy, flyx, flyy) &lt;= 30)
        amp = dist(midx, midy, flyx, flyy);
 
      //white of eye
      fill(244, 244, 233);
      ellipse(midx, midy, 130, 130);
 
      push();
      translate(midx + amp * cos(dir), midy + amp * sin(dir));
      rotate(dir);
 
      //distortion value
      var d = constrain(dist(midx, midy, flyx, flyy), 0, 30);
      var squish = map(d, 0, 30, 1, 0.9);
 
      //cornea
      stroke(20, 114, 80);
      strokeWeight(3);
      fill(61, 249, 187);
      ellipse(0, 0, 70 * squish, 70);
      noStroke();
      //pupil
      fill(19, 20, 45);
      ellipse(0, 0, 30 * squish, 30);
      pop();
    }
  }
 
  push();
  fill(19, 20, 45);
 
  translate(flyx, flyy);
  rotate(direction);
  stroke(19, 20, 45);
  strokeWeight(1.5);
 
  ellipse(0, 0, 20, 30);
  ellipse(0, -15, 16, 10);
 
  line(0, 0, -17, -17);
  line(0, 0, 17, -17);
  line(-17, -17, -20, -15);
  line(17, -17, 20, -15);
 
  line(0, -10, 17, 3);
  line(0, -10, -17, 3);
  line(17, 3, 20, 6);
  line(-17, 3, -20, 6);
 
  line(0, 0, 17, 17);
  line(0, 0, -17, 17);
 
  fill(255, 89, 0);
  noStroke();
  ellipse(6, -17, 6, 8);
  ellipse(-6, -17, 6, 8);
 
  fill(160, 255, 223, 150);
  beginShape();
  vertex(0, -10);
  vertex(-15, 0);
  vertex(-15, 25);
  vertex(-4, 15);
  vertex(0, -10);
  endShape();
 
  beginShape();
  vertex(0, -10);
  vertex(15, 0);
  vertex(15, 25);
  vertex(4, 15);
  vertex(0, -10);
  endShape();
  pop();
 
} 
// Double Exponential Ogee function ('_a' is the slope)
//(it goes fast, slowwww, fast)
// See https://github.com/IDMNYU/p5.js-func/blob/master/lib/p5.func.js
// From: https://idmnyu.github.io/p5.js-func/
//===================================================
  function function_DoubleExponentialOgee (x, a){
  functionName = "Double-Exponential Ogee";
 
  var min_param_a = 0.0 + Number.EPSILON;
  var max_param_a = 1.0 - Number.EPSILON;
  a = constrain(a, min_param_a, max_param_a); 
 
  var y = 0;
  if (x&lt;=0.5){
    y = (pow(2.0*x, 1.0-a))/2.0;
  } 
  else {
    y = 1.0 - (pow(2.0*(1.0-x), 1.0-a))/2.0;
  }
  return y;
}

airsun-AnimatedLoop

 

Overall, I am happy that this work turned out to be something similar to what I have imagined when doing the sketches. The choice of color and the overall style goes well with the theme. The idea here is about having an elevator going up and down as the eyeball of the eye. However, I do wish to spend more time on this project to increase the complexity of the gif. Currently, I feel there is something missing in the background. For more improvements, I can add more features to identify the elevator in a clearer way. For example, adding the button that controls the elevator going "up" and "down".

Brainstorm and sketch before starting the project (majority of the sketch was done in ps)

//Name: AirSun
//Sijings
//60-212
//code modified from Code Template for Looping GIFS
 
//===================================================
var myNickname = "airsun";
var nFramesInLoop = 120;
var bEnableExport = true;
var nElapsedFrames;
var bRecording;
var theCanvas;
var frames = [];
 
//===================================================
function setup() {
  theCanvas = createCanvas(640, 640);
  bRecording = false;
  nElapsedFrames = 0;
}
 
//===================================================
function keyTyped() {
  if (bEnableExport) {
    if ((key === 'f') || (key === 'F')) {
      bRecording = true;
      nElapsedFrames = 0;
    }
  }
}
 
//===================================================
function preload(){
  var filenames = [];
  filenames[0] = "https://i.imgur.com/4SUU0dP.png";
  filenames[1] = "https://i.imgur.com/ZkRe8eA.png";
  filenames[2] = "https://i.imgur.com/htCZu9X.png";
  filenames[3] = "https://i.imgur.com/oymFCPW.png";
  filenames[4] = "https://i.imgur.com/ahNh0P1.png";
  for (i=0; i&lt;filenames.length; i++){ frames.push(loadImage(filenames[i])); } } //=================================================== function draw() { var percentCompleteFraction = 0; if (bRecording) { percentCompleteFraction = float(nElapsedFrames) / float(nFramesInLoop); } else { percentCompleteFraction = float(frameCount % nFramesInLoop) / float(nFramesInLoop); } renderMyDesign (percentCompleteFraction); if (bRecording &amp;&amp; bEnableExport) { var frameOutputFilename = myNickname + "_frame_" + nf(nElapsedFrames, 4) + ".png"; print("Saving output image: " + frameOutputFilename); saveCanvas(theCanvas, frameOutputFilename, 'png'); nElapsedFrames++; if (nElapsedFrames &gt;= nFramesInLoop) {
      bRecording = false;
    }
  }
}
 
 
//===================================================
function renderMyDesign (percent) {
  background(216,195,131);
  smooth();
  stroke(0, 0, 0);
  strokeWeight(2);
 
  //----------------------
  // Here, I assign some handy variables. 
  var cx = 100;
  var cy = 100;
 
  //----------------------
  // Here, I use trigonometry to render a rotating element.
  var radius = 80;
  var rotatingArmAngle = percent * TWO_PI;
  var px = cx + radius * cos(rotatingArmAngle);
  var py = cy + radius * sin(rotatingArmAngle);
 
 
 
  //----------------------
  // Here's a linearly-moving square
  var squareSize = 20;
  var topY = height/3 - squareSize - 2;
  var botY = height-height/3 + 2;
  var eyeL = width/10;
  var eyeR = width-width/10;
  var eyeW = 200;
 
  strokeWeight(2);
  fill(248,242,231);
  beginShape();
  noStroke();
  //upper part of the eye
  curveVertex(eyeR, topY+(botY-topY)/2);
  curveVertex(eyeR, topY+(botY-topY)/2);
  curveVertex(eyeL+(eyeR-eyeL)*2/3, topY);
  curveVertex(eyeL+(eyeR-eyeL)/3, topY);
  curveVertex(eyeL, topY+(botY-topY)/2);
  curveVertex(eyeL, topY+(botY-topY)/2);
  //lower part of the eye
  endShape();
 
  beginShape();
  curveVertex(eyeR, topY+(botY-topY)/2);
  curveVertex(eyeR, topY+(botY-topY)/2);
  curveVertex(eyeL+(eyeR-eyeL)*2/3, botY);
  curveVertex(eyeL+(eyeR-eyeL)/3, botY);
  curveVertex(eyeL, topY+(botY-topY)/2);
  curveVertex(eyeL, topY+(botY-topY)/2);
  endShape();
 
 
 
  //other facial features
  noFill();
  stroke(90,84,68);
  strokeWeight(4);
  arc(eyeL+5, topY+(botY-topY)/2, 50, 50, 7/4*PI, 1/4*PI);
 
  beginShape();
  curveVertex(100, height);
  curveVertex(170, height);
  curveVertex(60, height/2+200);
 
  curveVertex(21, height/2);
  curveVertex(65, 91);
  curveVertex(65, 91);
  endShape();
  fill(90,84,68);
  ellipse(70, height, 80, 40);
 
 
 
  //eyeballs' drawing
  var eased = DoublePolynomialSigmoid (percent, 1); 
  if (percent &lt; 0.5) {
  //eased = (eased + 0.5)%2; // shifted by a half-loop, for fun
    var yPosition2 = map(eased, 0, 1, topY-150, botY-100);
  //print(yPosition2, botY-200)
  }else{
    var yPosition2 = map(eased, 0, 1, botY-100, topY-150);
  }
 
  fill (165, 73, 59); 
  //ellipse (eyeL+(eyeR-eyeL)/2, yPosition2, eyeW, eyeW); 
  var currentY=eyeL+(eyeR-eyeL)/5-70;
  var currenyX=yPosition2-150;
  var numofF=0;
  if (frameCount % 5 == 0){
    numofF +=1;
  }
  var framesC = numofF % 5;
 
  image(frames[framesC], currentY, currenyX, 430, 700);
 
 
 
  push();
  fill(216,195,131);
  noStroke();
  rect(width/3, height-180, 400, 200);
  pop();
 
  noFill();
  stroke(216,195,131);
  strokeWeight(200);
  beginShape();
  //upper part of the eye
  curveVertex(eyeR, topY+(botY-topY)/2-125);
  curveVertex(eyeR, topY+(botY-topY)/2-140);
  curveVertex(eyeL+(eyeR-eyeL)*2/3, topY-105);
  curveVertex(eyeL+(eyeR-eyeL)/3, topY-105);
  curveVertex(eyeL, topY+(botY-topY)/2-140);
  curveVertex(eyeL, topY+(botY-topY)/2-125);
  //lower part of the eye
  endShape();
  strokeWeight(70);
  beginShape();
  curveVertex(eyeR, topY+(botY-topY)/2+50);
  curveVertex(eyeR, topY+(botY-topY)/2+50);
  curveVertex(eyeL+(eyeR-eyeL)*2/3, botY+40);
  curveVertex(eyeL+(eyeR-eyeL)/3, botY+40);
  curveVertex(eyeL, topY+(botY-topY)/2+50);
  curveVertex(eyeL, topY+(botY-topY)/2+50);
  endShape();
 
  //eyeLash
  var eyeLashAngel = 45;
  var centerx = width / 2;
  var centery = botY-90;
  var radius = 240;
  stroke(90,84,68);
  strokeWeight(4);
  for (i = 0; i &lt; 10; i++){
    var x = cos(radians(eyeLashAngel)) * radius;
    var y = sin(radians(eyeLashAngel)) * radius;
    var increasement;
    if (i &lt; 5) {
      increasement = i * 15;
    }else{
      increasement = (10 - (i+1)) * 15;}
    line(centerx + x/1.2 , centery - y/1.2, centerx + 1.2*x , centery - y - increasement);
    eyeLashAngel = eyeLashAngel+10;
  }
  centery = topY + 90;
  eyeLashAngel = 225;
  for (i = 0; i &lt; 10; i++){
    var x = cos(radians(eyeLashAngel)) * radius;
    var y = sin(radians(eyeLashAngel)) * radius;
    var increasement;
    if (i &lt; 5) {
      increasement = i * 15;
    }else{
      increasement = (10 - (i+1)) * 15;}
    line(centerx + x/1.2 , centery - y/1.2, centerx + 1.2*x, centery - y + increasement);
    eyeLashAngel = eyeLashAngel+10;
  }
}
 
//following code got from https://github.com/golanlevin/Pattern_Master
//------------------------------------------------------------------
function DoublePolynomialSigmoid (_x, _a){
  var n = _a;
  var _y = 0;
  if (n%2 == 0){ 
    // even polynomial
    if (_x&lt;=0.5){
      _y = pow(2.0*_x, n)/2.0;
    } 
    else {
      _y = 1.0 - pow(2*(_x-1.0), n)/2.0;
    }
  } 
 
  else { 
    // odd polynomial
    if (_x&lt;=0.5){
      _y = pow(2.0*_x, n)/2.0;
    } 
    else {
      _y = 1.0 + pow(2.0*(_x-1.0), n)/2.0;
    }
 
  }
 
  return (_y);
}

shuann-AnimatedLoop

Overall I am pretty satisfied with how this turned out, and it looks very close to what I had on my sketch. For the background switch I decided to use the EaseInOutElastic function after trying out a hand full of different easing functions because this one looks almost exactly like what I imagined the transition to be when I made my sketch. I believe the bounciness really suites the style of the visuals that I created here and defiantly adds to the overall quality of the piece. The hardest part about this assignment was defiantly the fact that I had to think and calculate everything based on the percentage frame rate. It was not so easy to wrap my head around that and when I ran into a glitch I have to really sit down and think about where the problem is. Did I lose count on the frame somewhere or was it something else. For example this one:

I also wish that I can spend more time to make the movement of the boat more natural. Maybe with some tilt at appropriate time.s

My Sketch:

The Code:

// INSTRUCTIONS FOR EXPORTING FRAMES (from which to make a GIF): 
// 1. Run a local server, using instructions from here:
//    https://github.com/processing/p5.js/wiki/Local-server
// 2. Set the bEnableExport variable to true.
// 3. Set the myNickname variable to your name.
// 4. Run the program from Chrome, press 'f'. 
//    Look in your 'Downloads' folder for the generated frames.
// 5. Note: Retina screens may export frames at twice the resolution.
 
 
//===================================================
// User-modifiable global variables. 
var myNickname = "shuann";
var nFramesInLoop = 120;
var bEnableExport = true;
var myScale = 0.006;
var myLoopingNoiseArray0 = [];
var myLoopingNoiseArray1 = [];
var myLoopingNoiseArray2 = [];
var myLoopingNoiseArray3 = [];
var myLoopingNoiseArray4 = [];
var myLoopingNoiseArray5 = [];
var myLoopingNoiseArray6 = [];
var myLoopingNoiseArray7 = [];
var myLoopingNoiseArray8 = [];
var myLoopingNoiseArray9 = [];
 
// Other global variables you don't need to touch.
var nElapsedFrames;
var bRecording;
var theCanvas;
 
//===================================================
function setup() {
  theCanvas = createCanvas(640, 640);
  bRecording = false;
  nElapsedFrames = 0;
 
  var radius = 60;
  noiseSeed(40);
  //waves at the bottom
  //sorry the style is horrible
  for (var i = 0; i < nFramesInLoop; i++){
    var rotatingArmAngle = map(i, 0 ,120, 0, TWO_PI);
    var px = width/2 + radius * cos(rotatingArmAngle);
    var py = height/2 + radius * sin(rotatingArmAngle);
    var noiseAtLoc0 = height - 50 * noise(myScale * px, myScale * py);
    var noiseAtLoc1 = height - 80 * noise(myScale * px, myScale * py);
    var noiseAtLoc2 = height - 150 * noise(myScale * px, myScale * py);
    var noiseAtLoc3 = height - 200 * noise(myScale * px, myScale * py);
    var noiseAtLoc4 = height - 300 * noise(myScale * px, myScale * py);
    myLoopingNoiseArray0[i] = round(noiseAtLoc0);
    myLoopingNoiseArray1[i] = round(noiseAtLoc1);
    myLoopingNoiseArray2[i] = round(noiseAtLoc2);
    myLoopingNoiseArray3[i] = round(noiseAtLoc3);
    myLoopingNoiseArray4[i] = round(noiseAtLoc4);
 
    var px1 = width/2 + radius * 0.7 * cos(rotatingArmAngle);
    var py1 = height/2 + radius * 0.7 * sin(rotatingArmAngle);
    var noiseAtLoc5 = height - 60 * noise(myScale * px1, myScale * py1);
    var noiseAtLoc6 = height - 130 * noise(myScale * px1, myScale * py1);
    var noiseAtLoc7 = height - 230 * noise(myScale * px1, myScale * py1);
    var noiseAtLoc8 = height - 330 * noise(myScale * px1, myScale * py1);
    var noiseAtLoc9 = height - 450 * noise(myScale * px1, myScale * py1);
    myLoopingNoiseArray5[i] = round(noiseAtLoc5);
    myLoopingNoiseArray6[i] = round(noiseAtLoc6);
    myLoopingNoiseArray7[i] = round(noiseAtLoc7);
    myLoopingNoiseArray8[i] = round(noiseAtLoc8);
    myLoopingNoiseArray9[i] = round(noiseAtLoc9);
  }
 
}
 
 
//===================================================
function keyTyped() {
  if (bEnableExport) {
    if ((key === 'f') || (key === 'F')) {
      bRecording = true;
      nElapsedFrames = 0;
    }
  }
}
 
//===================================================
function draw() {
 
  // Compute a percentage (0...1) representing where we are in the loop.
  var percentCompleteFraction = 0;
  if (bRecording) {
    percentCompleteFraction = float(nElapsedFrames) / float(nFramesInLoop);
  } else {
    percentCompleteFraction = float(frameCount % nFramesInLoop) / float(nFramesInLoop);
  }
 
  // Render the design, based on that percentage. 
  // This function renderMyDesign() is the one for you to change. 
  renderMyDesign (percentCompleteFraction);
 
  // If we're recording the output, save the frame to a file. 
  // Note that the output images may be 2x large if you have a Retina mac. 
  // You can compile these frames into an animated GIF using a tool like: 
  if (bRecording && bEnableExport) {
    var frameOutputFilename = myNickname + "_frame_" + nf(nElapsedFrames, 4) + ".png";
    print("Saving output image: " + frameOutputFilename);
    saveCanvas(theCanvas, frameOutputFilename, 'png');
    nElapsedFrames++;
 
    if (nElapsedFrames >= nFramesInLoop) {
      bRecording = false;
    }
  }
}
 
//===================================================
function renderMyDesign (percent) {
  //
  // THIS IS WHERE YOUR ART GOES. 
  // This is an example of a function that renders a temporally looping design. 
  // It takes a "percent", between 0 and 1, indicating where we are in the loop. 
  // Use, modify, or delete whatever you prefer from this example. 
  // This example uses several different graphical techniques. 
  // Remember to SKETCH FIRST!
 
  //----------------------
  smooth();
  var cx = 100;
  var cy = 100;
 
  push();
  translate(width/2, height/2);
 
  if (percent < 0.5){
    var ang = function_PennerEaseInOutElastic(percent*2)*180;
    rotate(radians(ang));
    drawbackdrop(percent);
  } else {
    var ang = function_PennerEaseInOutElastic(map(percent, 0.5, 1, 0, 1))*180;
    rotate(radians(ang) + TWO_PI/2);
    drawbackdrop(percent);
  }
 
  drawbackdrop(percent);
  pop();
 
 
  noStroke();
  push();
  translate(0, -100);
  drawboat(percent,-0.1);
  drawWave(percent, 88, 176, 230, myLoopingNoiseArray9, -0.1);
  drawWave(percent, 80, 160, 210, myLoopingNoiseArray8, 0);
  drawWave(percent, 22, 132, 200, myLoopingNoiseArray7, 0.6);
  drawWave(percent, 49, 80, 152, myLoopingNoiseArray6, 0.8);
  drawWave(percent, 54, 100, 160, myLoopingNoiseArray5, 0.8);
  pop();
  drawWave(percent, 45, 183, 212, myLoopingNoiseArray4, -0.2);
  drawWave(percent, 45, 167, 212, myLoopingNoiseArray3, -0.2);
  drawWave(percent, 65, 130, 190, myLoopingNoiseArray2, -0.2);
  drawWave(percent, 54, 100, 160, myLoopingNoiseArray1, 0.5);
  drawWave(percent, 49, 80, 152, myLoopingNoiseArray0, 0.5);
}
 
function drawbackdrop(p){
  fill(235,219,190);
  rect(-width, -height, width*3, height);
  fill(18,24,56);
  rect(-width, 0, width*3, height);
 
  //draw sun
  push();
  strokeWeight(3);
  stroke(235,172, 45);
  fill(235,172, 45);
  ellipse(200, -200, 50, 50);
  translate(200, -200);
  rotate(0.2 * p * TWO_PI);
  push();
  for (var a = 0; a < 10; a++){
    rotate(TWO_PI/10);
    line (50, 0, 70, 0);
  }
  pop();
  pop();
 
  //draw moon
  push();
  noStroke();
  fill(255);
  translate(-200, 200);
  ellipse(0, 0, 80, 80);
  fill(18,24,56);
  ellipse(-20, -10, 50, 50);
  pop();
 
  //draw star
  push();
  translate(-100, 110);
  noStroke();
  fill(255);
  star(0, 0, 5, 10, 5); 
  star(50, 110, 5, 10, 5); 
  star(80, 90, 3, 6, 5); 
  star(100, 105, 3, 6, 5); 
  pop();
 
}
 
function drawboat(p, tilt){
  currStep = round(p * nFramesInLoop);
  var nx = map(40, 0, nFramesInLoop, 0, width);
  var ny = myLoopingNoiseArray9[(currStep+40)%nFramesInLoop]
  // var newy = doubleExponentialSigmoid(map(y, 315, 370, 0, 1), 0.7);//does not look good
  // var y = map(newy, 0, 1, 315, 370);
  fill(255);
  beginShape();
  vertex(nx - 50, ny - 10);
  vertex(nx + 50, ny - 10);
  vertex(nx + 30, ny + 20);
  vertex(nx - 30, ny + 20);
  endShape();
  stroke(255);
  line(nx, ny, nx, ny - 50);
  triangle(nx, ny - 50, nx + 20, ny - 40, nx, ny - 30);
}
 
function drawWave(p, r, g, b, wave, tilt){
  currStep = round(p * nFramesInLoop);
  fill(r, g, b);
  noStroke();
  beginShape();
  vertex(0,height+50);
  for (var i = 0; i < wave.length; i++) {
    var nx = map(i, 0, nFramesInLoop - 1, 0, width);
    if (i + currStep >= nFramesInLoop) {
      var ny = wave[(i + currStep)-nFramesInLoop] - i*tilt;
    } else {
      var ny = wave[i + currStep] - i*tilt;
    }
    vertex(nx, ny);
  }
  vertex(width,height+50);
  endShape();
}
 
 
// symmetric double-element sigmoid function (a is slope)
// See https://github.com/IDMNYU/p5.js-func/blob/master/lib/p5.func.js
// From: https://idmnyu.github.io/p5.js-func/
//===================================================
function doubleExponentialSigmoid (_x, _a){
  if(!_a) _a = 0.75; // default
 
  var min_param_a = 0.0 + Number.EPSILON;
  var max_param_a = 1.0 - Number.EPSILON;
  _a = constrain(_a, min_param_a, max_param_a);
  _a = 1-_a;
 
  var _y = 0;
  if (_x<=0.5){
    _y = (pow(2.0*_x, 1.0/_a))/2.0;
  }
  else {
    _y = 1.0 - (pow(2.0*(1.0-_x), 1.0/_a))/2.0;
  }
  return(_y);
}
 
//From: https://github.com/golanlevin/Pattern_Master/blob/master/pattern_master/F00.pde
//------------------------------------------------------------------
function function_PennerEaseInOutElastic (t) {
 
  if (t===0) return 0; 
  if ((t/=0.5)==2) return 1;
  var p=(.3 * 1.5);
  var a=1;
  var s=p/4;
 
  if (t < 1) {
    var postFix = pow(2, 10*(t-=1)); // postIncrement is evil
    return -0.5 * (postFix* sin( (t-s)*(2*PI)/p ));
  } 
 
  var postFix = pow(2, -10*(t-=1)); // postIncrement is evil
  return postFix * sin( (t-s)*(2*PI)/p )*.5 + 1;
}
 
//From: https://p5js.org/examples/form-star.html
//------------------------------------------------------------------
function star(x, y, radius1, radius2, npoints) {
  var angle = TWO_PI / npoints;
  var halfAngle = angle/2.0;
  beginShape();
  for (var a = 0; a < TWO_PI; a += angle) {
    var sx = x + cos(a) * radius2;
    var sy = y + sin(a) * radius2;
    vertex(sx, sy);
    sx = x + cos(a+halfAngle) * radius1;
    sy = y + sin(a+halfAngle) * radius1;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}

.
 

chromsan-AnimatedLoop

For this project, I wanted to make something using just geometry made in processing and try to give it a hypnotic quality. I also wanted to play with 2D and 3D shapes and try and find ways to create illusions with their combinations. I experimented a bit and decided on making a set of triangles which merge into a square, which then becomes a pyramid viewed above, which then becomes a triangular hole. After creating this, I wasn't really getting the hypnotic quality I was going for so I added a ball for your eye to follow around the screen, also playing with the physics of the ball and how it interacts with the shapes, in addition to some random movement to give the illusion of air resistance. When coding the loop, the most helpful function, by far, was the map function. I used this to control how every movement is played and the amount of time that each takes as a percentage of the total time. In addition, I added the double exponential sigmoid function to liven up the movements. I liked the way that it gives the transitions a snappy feel. I'm fairly happy with the result, but I think the loop could be reworked towards the end to create a cleverer transition to the beginning, which is much more interesting.

Code is formatted incorrectly by wordpress, the original file is on GitHub

// looping template by
// Prof. Golan Levin, January 2018
 
// Global variables. 
String  myNickname = "nickname"; 
int     nFramesInLoop = 260;
int     nElapsedFrames;
boolean bRecording; 
float myScale = 0.01;
float radius = 100.0;
 
void setup() {
  size (640, 640, P3D); 
  bRecording = false;
  nElapsedFrames = 0;
  frameRate(40);
}
 
void keyPressed() {
  if ((key == 'f') || (key == 'F')) {
    bRecording = true;
    nElapsedFrames = 0;
  }
}
 
void draw() {
 
  // Compute a percentage (0...1) representing where we are in the loop.
  float percentCompleteFraction = 0; 
  if (bRecording) {
    percentCompleteFraction = (float) nElapsedFrames / (float)nFramesInLoop;
  } else {
    percentCompleteFraction = (float) (frameCount % nFramesInLoop) / (float)nFramesInLoop;
  }
 
  // Render the design, based on that percentage. 
  renderMyDesign (percentCompleteFraction);
 
  // If we're recording the output, save the frame to a file. 
  if (bRecording) {
    saveFrame("frames/" + myNickname + "_frame_" + nf(nElapsedFrames, 4) + ".png");
    nElapsedFrames++; 
    if (nElapsedFrames &gt;= nFramesInLoop) {
      bRecording = false;
    }
  }
}
 
void renderMyDesign (float percent) {
 
  smooth(); 
  // create a rounded percent for easy positioning within the loop
  int roundedPercent = round(percent * 100);
 
  pushMatrix();
 
  if (roundedPercent &gt;= 0 &amp;&amp; roundedPercent &lt;= 33){ background(201, 197, 209); noStroke(); // triangles moving in from edges to center float eased = function_DoubleExponentialSigmoid(map(percent, 0, 0.34, 0, 1), 0.8); fill(112, 88, 124); triangle(0, height, width, height, width/2, map(eased, 0, 1, height, height/2 )); fill( 57, 43, 88); triangle(0, 0, 0, height, width/2 * map(eased, 0, 1, 0, 1), height/2); fill(45, 3, 32); triangle(0, 0, width, 0, width/2, height/2 * map(eased, 0, 1, 0, 1)); fill(108, 150, 157); triangle(width, 0, width, height, map(eased, 0, 1, width, width/2), height/2); // rectangle in center shrinking float rectWidth = map(eased, 0, 1, width, 0); float rectHeight = map(eased, 0,1, height, 0); fill(219, 216, 224); rect((width- rectWidth)/2, (height - rectHeight)/2, rectWidth, rectHeight); } else if (roundedPercent &gt; 33 &amp;&amp; roundedPercent &lt;= 66) { pushMatrix(); translate(width/2, height/2, 0); ortho(-width/2, width/2, -height/2, height/2); float eased = function_DoubleExponentialSigmoid(map(percent, 0.33, 0.67, 0, 1), 0.9); // Move from above to side, rotate 45 deg as well rotateX(PI/map(eased, 0, 1, 1200, 2)); rotateZ(PI/map(eased, 0, 1, 1200, 2)); background(201, 197, 209); // 3D Triangle fill(45, 3, 32); beginShape(); scale(map(percent, 0.33, 0.66, 3.6, 1.8), map(percent, 0.33, 0.66, 3.6, 1.8), map(percent, 0.33, 0.66, 3.6, 1.8)); vertex(-100, -100, -100); vertex( 100, -100, -100); vertex( 0, 0, 100); endShape(); fill(108, 150, 157); beginShape(); vertex( 100, -100, -100); vertex( 100, 100, -100); vertex( 0, 0, 100); endShape(); fill(112, 88, 124); beginShape(); vertex( 100, 100, -100); vertex(-100, 100, -100); vertex( 0, 0, 100); endShape(); fill( 57, 43, 88); beginShape(); vertex(-100, 100, -100); vertex(-100, -100, -100); vertex( 0, 0, 100); endShape(); popMatrix(); } else if (roundedPercent &gt; 66) {
 
    float eased = function_DoubleExponentialSigmoid(map(percent, 0.66, 1, 0, 1), 0.9);
    // transition from pale blue to dark purple
    color start = color(108, 150, 157);
    color end = color(102, 71, 92);
    color lerpedCol = lerpColor(start, end, eased);
 
    if (roundedPercent &lt; 83){
 
     background(201, 197, 209);
     fill(lerpedCol);
     translate(0,0);
     // replace 3D triangle with 2D triangle (nasty specific values)
     triangle(map(eased, 0, 1, 140, -140), map(eased, 0, 1, 500, 700), 
              map(eased, 0, 1, 500, 700), map(eased, 0, 1, 500, 700), 
              map(eased, 0, 1, 320, 320), map(eased, 0, 1, 140, -140));
    }
    else {
      // square move in from center to complete loop
      float eased2 = function_DoubleExponentialSigmoid(map(percent, 0.82, 1, 0, 1), 0.5);
      background(lerpedCol);
      float rectWidth = map(eased2, 0, 1, 0, width);
      float rectHeight = map(eased2, 0, 1, 0, height);
      fill(219, 216, 224);
      rect((width- rectWidth)/2, (height - rectHeight)/2, rectWidth, rectHeight);
    }
  }
  popMatrix();
  pushMatrix();
  renderBall(percent, roundedPercent);
  popMatrix();
}
 
// weird bouncing ball shenanigans 
void renderBall(float percent, int roundedPercent) {
 
  // make nose map that gives ball a little random sway 
   int currStep = frameCount % nFramesInLoop;
   float t = map(currStep, 0, nFramesInLoop, 0, TWO_PI); 
   float px = width/2.0 + radius * cos(t); 
   float py = width/2.0 + radius * sin(t);
   float noiseAtLoc = height - 100.0 * noise(myScale*px, myScale*py);
   float noiseAdjusted = map(noiseAtLoc, 570, 620, -15, 15);
   float xDrift = noiseAdjusted;
   float yDrift = noiseAdjusted;
 
 
    if (roundedPercent &lt;= 25){ // ball moves towards bottom, larger float x = lerp(width/2, width/2 + 100, map(percent, 0, 0.25, 0, 1)); float y = lerp(height/2, height - 200, map(percent, 0, 0.25, 0, 1)); float scale = lerp(5, 75, map(percent, 0, 0.25, 0, 1)); translate(x + xDrift, y + yDrift, 400); fill(map(scale, 85, 5, 255,50)); sphere(scale); } else if (roundedPercent &gt; 25 &amp;&amp; roundedPercent &lt;= 55) { // ball moves towards center, smaller float x = lerp(width/2 + 100, width/2, map(percent, 0.25, 0.55, 0, 1)); float y = lerp(height - 200, height/2, map(percent, 0.25, 0.55, 0, 1)); float scale = lerp(75, 2, map(percent, 0.25, 0.55, 0, 1)); translate(x + xDrift, y + yDrift, 400); fill(map(scale, 85, 5, 255, 50)); sphere(scale); } else if (roundedPercent &gt; 55 &amp;&amp; roundedPercent &lt;= 60) { // ball moves to side float x = lerp(width/2, 15, map(percent, 0.55, 0.6, 0, 1)); float y = lerp(height/2, height/2, map(percent, 0.55, 0.6, 0, 1)); float scale = lerp(5, 25, map(percent, 0.55, 0.6, 0, 1)); translate(x + xDrift, y + yDrift, 400); fill(map(scale, 75, 5, 255, 50)); sphere(scale); } else if (roundedPercent &gt; 60 &amp;&amp; roundedPercent &lt;= 66) { // ball goes right float x = lerp(15, width/4, map(percent, 0.6, 0.66, 0, 1)); float y = lerp(height/2, height/3 * 2, map(percent, 0.6, 0.66, 0, 1)); //float scale = lerp(5, 25, map(percent, 0.6, 0.6, 0.66, 1)); translate(x + xDrift, y + yDrift, 400); fill(map(25, 75, 5, 255, 50)); sphere(25); } else if (roundedPercent &gt; 66 &amp;&amp; roundedPercent &lt;= 85) { // ball gets smaller, towards center float x = lerp(width/4, width/2, map(percent, 0.6, 0.85, 0, 1)); float y = lerp(height/3 * 2, height/2, map(percent, 0.6, 0.85, 0, 1)); float easedx = function_DoubleExponentialSigmoid(map(x, width/4, width/2, 0, 1), 0.8); float easedy = function_DoubleExponentialSigmoid(map(y, height/3 * 2, height/2, 0, 1), 0.8); float scale = lerp(25, 15, map(percent, 0.66, 0.85, 0, 1)); translate(map(easedx, 0, 1, width/4, width/2) + xDrift, map(easedy, 0, 1, height/3 * 2, height/2) + yDrift, 400); fill(map(scale, 75, 5, 255, 50)); sphere(scale); } else if (roundedPercent &gt; 85 &amp;&amp; roundedPercent &lt;= 100) {
    // ball gets larger 
    float scale = lerp(15, 5, map(percent, 0.85, 1, 0, 1));
    translate(width/2 + xDrift, height/2 + yDrift, 400);
    fill(map(scale, 75, 5, 255, 50));
    sphere(scale);
  }
}
 
// https://github.com/golanlevin/Pattern_Master
float function_DoubleExponentialSigmoid (float x, float a) {
  // functionName = "Double-Exponential Sigmoid";
 
  float min_param_a = 0.0 + EPSILON;
  float max_param_a = 1.0 - EPSILON;
  a = constrain(a, min_param_a, max_param_a); 
  a = 1-a;
 
  float y = 0;
  if (x&lt;=0.5) {
    y = (pow(2.0*x, 1.0/a))/2.0;
  } else {
    y = 1.0 - (pow(2.0*(1.0-x), 1.0/a))/2.0;
  }
  return y;
}

nixel-AnimatedLoop

I had a lot of fun with this project!

My process:

I started with some messy brainstorms from when we first got the prompt:

I thought that loops of characters bouncing down stairs, or being transported by a never ending escalator would be fun. I also considered doing skyscapes that looped between sunrise and sunset and a swinging character. In the end, I went with rain because it was the simplest in terms of shape and color design.

I thought that I would do some sort of walk cycle and plotted some leg rotations.

I went online and found a shape-heavy walk cycle gif and spliced some frames out to break it down.

However, when I began to code and actually look into the easing functions, I realized that it would be simpler to pair them with a bouncing animation instead of a walking one. It was sad to leave all the walking animation work I did, but I think I made a better choice in terms of time and simplicity. I think I will go back one day and make all of the ideas I had initially.

After a lot of experimentation (and suffering) I chose to use primarily the Flat Top Window easing function since it was bouncy, playful, and worked well for a looping animation since it was symmetrical.

Here is a thumbnail I made half way through coding in order to hash out the locations of some body parts.

I struggled with the rain since I thought it added to the narrative of the gif, but didn't necessarily loop. I also struggled with the umbrella since it's an interesting visual element but doesn't make much sense since the character isn't holding on to it. I did at one point code an umbrella handle and hands, but it made the image too complex so I got rid of them.

Critiquing myself, I think that I did well in keeping the design simple and appealing. I could have been more ambitious with connecting the umbrella to the character and been more efficient with my code (I organized it in groups of body parts but ended up repeating a lot of simple functions like noStroke() or fill() when I could have just organized the code more efficiently).

 

 
// This is a template for creating a looping animation in Processing/Java. 
// When you press the 'F' key, this program will export a series of images
// into a "frames" directory located in its sketch folder. 
// These can then be combined into an animated gif. 
// Known to work with Processing 3.3.6
// Prof. Golan Levin, January 2018
 
 
//===================================================
// Global variables. 
String  myNickname = "nixel"; 
int     nFramesInLoop = 60;
int     nElapsedFrames;
boolean bRecording; 
Drop[] drops = new Drop[50];
 
//===================================================
void setup() {
  size (640, 640); 
  bRecording = false;
  nElapsedFrames = 0;  
  for (int i = 0; i < drops.length; i++) {
   drops[i] = new Drop(); 
  }
}
//===================================================
void keyPressed() {
  if ((key == 'f') || (key == 'F')) {
    bRecording = true;
    nElapsedFrames = 0;
  }
}
//===================================================
void draw() {
  // Compute a percentage (0...1) representing where we are in the loop.
  float percentCompleteFraction = 0; 
  if (bRecording) {
    percentCompleteFraction = (float) nElapsedFrames / (float)nFramesInLoop;
  } else {
    percentCompleteFraction = (float) (frameCount % nFramesInLoop) / (float)nFramesInLoop;
  }
 
  // Render the design, based on that percentage. 
  renderMyDesign (percentCompleteFraction);
 
  // If we're recording the output, save the frame to a file. 
  if (bRecording) {
    saveFrame("frames/" + myNickname + "_frame_" + nf(nElapsedFrames, 4) + ".png");
    nElapsedFrames++; 
    if (nElapsedFrames >= nFramesInLoop) {
      bRecording = false;
    }
  }
}
//===================================================
void renderMyDesign (float percent) {
  int bgColor = 255;
  background (bgColor);
  stroke (0, 0, 0); 
  strokeWeight (2); 
  float pennerEase = FlatTopWindow(percent);
  noStroke();
 
  //umbrella
  float umbrellashift = (map(pennerEase, 0, 1, -50, -20)); 
  fill(255, 0, 0);
  ellipse(width/2, 240 + umbrellashift, 360, 360);
 
  //cutout umbrella
  fill(bgColor);
  int r = 250;
  for (int i = 0; i < 360; i += 30){
  float dy = sin(radians(i)) * r;
  float dx = cos(radians(i)) * r;
  ellipse(width/2 + dx, 240 + umbrellashift + dy, 180, 180);
  }
 
  rectMode(CENTER);
 
  //ground
  fill(10, 95, 223);
  rect(width/2, 600, width, 200);
 
  //puddle
  noFill();
  stroke(255);
  float jumpWeight = map(percent, 0, 1, 15, 0);
  float puddleEase = map(PennerEaseOutExpo(percent-0.7), 0, 1, 100, 1000);
  float puddleEaseY = map(PennerEaseOutExpo(percent), 0, 1, 0, 50); 
  strokeWeight(jumpWeight);
 
  if (percent >= 0.7){
    ellipse(355, 530, puddleEase, puddleEaseY);
    ellipse(200, 560, puddleEase, puddleEaseY);
    ellipse(300, 600, puddleEase, puddleEaseY);
    ellipse(50, 650, puddleEase, puddleEaseY);
    ellipse(550, 610, puddleEase, puddleEaseY);
  }
 
  //hair
  noStroke();
  float hairshift = (map(pennerEase, 0, 1, 60, -40)); 
  fill(0);
  arc(width/2, 240 + hairshift, 180, 300, PI, TWO_PI);
 
 
  //legs 
  float rX = map(pennerEase, 0, 1, 350, 340);
  float Y = map(pennerEase, 0, 1, 560, 510);
  float squishX = map(pennerEase, 0, 1, 10, 0);
  float lX = map(pennerEase, 0, 1, width - 360, width - 350);
  fill(247,227,40);
 
  //right leg
  triangle(rX, Y, rX - 5 - squishX, Y - 150, rX + 10 + squishX, Y - 150);
 
  //left leg
  triangle(lX, Y, lX - 5 - squishX, Y - 150, lX + 10 + squishX, Y - 150);
 
  //body
	float bsquish = map(pennerEase, 0, 1, 10, -10);
  float bodyshift = (map(pennerEase, 0, 1, 50, -20)); 
  fill(247, 227, 40);
  triangle(width/2, 170 + bodyshift, width/2 - 100 - bsquish, 380 + bodyshift, width/2 + 100 + bsquish, 380 + bodyshift);
 
  //mirrored legs 
  float mrX = map(pennerEase, 0, 1, 350, 340);
  float mrY = map(pennerEase, 0, 1, 510, 560);
  float msquishX = map(pennerEase, 0, 1, 10, 0);
  float mlX = map(pennerEase, 0, 1, width - 360, width - 350);
  fill(247,227,40);
 
  //right leg
  triangle(mrX, mrY + 50, mrX - 5 - msquishX, mrY + 175, mrX + 10 + msquishX, mrY + 175);
 
  //left leg
  triangle(mlX, mrY + 50, mlX - 5 - msquishX, mrY + 175, mlX + 10 + msquishX, mrY + 175);
  //head
  float headshift = (map(pennerEase, 0, 1, 55, -30)); 
  fill(255);
  ellipse(width/2, 170 + headshift, 100, 100);
 
  //mouth
  float mouthshift = (map(pennerEase, 0, 1, 30, -70)); 
  float mouthY = (map(pennerEase, 0, 1, -5, 10)); 
  fill(255, 0, 0);
  ellipse(width/2, 225 + mouthshift, 20, 15 + mouthY);
 
  //bangs  
  fill(0);
  arc(width/2, 150 + hairshift, 100, 100, PI, TWO_PI);
 
  //eyes
  float eyeY = (map(pennerEase, 0, 1, 1, 5)); 
  float eyeX = (map(pennerEase, 0, 1, 1, 5)); 
  ellipse(width/2 - 30, 200 + mouthshift, 10 + eyeX, 15 + eyeY);
  ellipse(width/2 + 30, 200 + mouthshift, 10 + eyeX, 15 + eyeY);
 
  //arms
  fill(247,227,40);
 
  float Ya = map(pennerEase, 0, 1, 10, 80);
  //right arm
  triangle(345, 230 + bodyshift, 345, 260 + bodyshift, 450, 250 + Ya);
 
  //left arm
  triangle(width - 345, 230 + bodyshift, width - 345, 260 + bodyshift, width - 450, 250 + Ya);
 
  //rain
  for (int i = 0; i < drops.length; i++){
  drops[i].fall();
  drops[i].show();
  }
 
}
 
//===================================================
//rain code refenced from Dan Shiffman's Purple Rain Coding Challenge
class Drop {
 float x = random(width);
 float y = random(0, height);
 float yspeed = 11;
 float len = 15;
 
 void fall(){
   y += yspeed;
 
  if (y > height) {
   y = 0;
   yspeed = 11;
  }
 
   }
 
 void show(){
  stroke(10, 95, 223);
  strokeWeight(2);
  line(x, y, x, y+len);
 }
}
 
//===================================================
float FlatTopWindow (float x) {
  //easing functions from Pattern Master https://github.com/golanlevin/Pattern_Master
  // http://en.wikipedia.org/wiki/Window_function 
 
  final float a0 = 1.000;
  final float a1 = 1.930;
  final float a2 = 1.290;
  final float a3 = 0.388;
  final float a4 = 0.032;
 
  float pix = PI*x;
  float y = a0 - a1*cos(2*pix) + a2*cos(4*pix) - a3*cos(6*pix) + a4*cos(8*pix);
  y /= (a0 + a1 + a2 + a3 + a4); 
 
  return y;
}
 
float PennerEaseOutExpo(float t) {
  return (t==1) ? 1 : (-pow(2, -10 * t) + 1);
}

Sepho – Animated Loop

I was really fascinated with the method of using circular motion over Perlin noise to get random yet looping 1-D graph. For my GIF I decided to expand on this by using two 1-D graphs as x and y positions for points to get a random motion that looped. I then made many points that moved in random loops and created shapes from them, resulting in crazy, abstract, looping shapes. Overall this project was a fun exploration of how math and graphs can translate to motion and visa versa.

Visualization of point movement: