chromsan-Body

For this project, I created a visualization of the space where the gaze of two characters in conversation meet. As the characters look at or away from one another, a shape is created between them that grows over the course of a scene.

I used ml5.js and PoseNet to track the characters in the videos and p5.js to draw the shape between them. The field of view for each character is determined by a line drawn from their ear to a point between their eyes and nose. The line is extended outward at two different angles to create a triangle. Both characters' fields of vision are checked for intersecting points and the shape drawn between them is progressively built from these points, layered over and over.

I decided to use scenes from movies mainly because of the one scene from There Will Be Blood where Daniel and Eli engage in this intense standoff, where Daniel talks down to Eli in both the dialogue and his body language. Eli's gaze falls to the ground while Daniel's stays fixed on Eli. This scene worked particularly well, as there is a good amount of change in their gazes throughout, and the shape that emerges between the two is quite interesting. I searched for more scenes with dialogue shot from the side, and came up with a number of them, mostly from P.T. Anderson and Tarantino, the only two directors that seem to use it regularly. I ended up using clips from Kill Bill Vol.I, There Will Be Blood, Boogie Nights, The Big Lebowski, and Inglourious Basterds.

I initially wanted to create one shape between the characters and have their gaze pull it around the screen, but I didn't like the result. So I created a fill for the area where the gazes intersected, and then I created multiple fills that built up to create a complex shape that ends up looking like a brushstroke. There are definitely limitations to this method though; the camera has to be relatively still, there can't be too many characters, and the scene has to be pretty short. Drawing all the shapes on every draw call in addition to running PoseNet in realtime is quite computationally demanding. Even with a good graphics card the video can start to lag; the frames definitely start to drop by the end of some of the longer scenes. Overall, I'm pretty happy with the result, but there is certainly more optimization to be done.

A few more:

Some debug views: 

The code can be found on GitHub.

let poseNet, poses = [];
let video, videoIsPlaying; 
let left = [], right = [], points = [];
let initialAlpha = 100, FOVheight = 150;
let time;
let show = true, debug = true;
 
let vidName = 'milkshake';
let rgb = '#513005';
 
class FOVedge {
 
  constructor(_x1, _y1, _x2, _y2) {
    this.x1 = _x1;
    this.x2 = _x2;
    this.y1 = _y1;
    this.y2 = _y2;
  }
  draw(r, g, b, a){
    stroke(r, g, b, a);
    line(this.x1, this.y1, this.x2, this.y2);
  }
  intersects(l2) {
    let denom = ((l2.y2 - l2.y1) * (this.x2 - this.x1) - (l2.x2 - l2.x1) * (this.y2 - this.y1));
    let ua = ((l2.x2 - l2.x1) * (this.y1 - l2.y1) - (l2.y2 - l2.y1) * (this.x1 - l2.x1)) / denom;
    let ub = ((this.x2 - this.x1) * (this.y1 - l2.y1) - (this.y2 - this.y1) * (this.x1 - l2.x1)) / denom;
    if (ua < 0 || ua > 1 || ub < 0 || ub > 1) return false;
    let x = this.x1 + ua * (this.x2 - this.x1);
    let y = this.y1 + ua * (this.y2 - this.y1);
    return {x, y};
  }
}
 
class FOV {
  constructor(x1, y1, x2, y2) {
    this.s1 = new FOVedge(x1, y1, x2, y2 - FOVheight);
    this.s2 = new FOVedge(x1, y1, x2, y2 + FOVheight);
    this.s3 = new FOVedge(x2, y2 + FOVheight, x2, y2 - FOVheight);
    this.col = color(191, 191, 191, initialAlpha); 
    this.show = true;
    // right = 1, left = -1
    if (x2 > x1) {
      this.direction = 1;
    } else {
      this.direction = -1
    }
  }
  checkForIntersections(fov) {
    let intersections = [];
    for (let j = 1; j < 3; j++){
      let sattr = 's' + j.toString();
      for (let i = 1; i < 3; i++) {
        let attr = 's' + i.toString();
        let ints = this[sattr].intersects(fov[attr]);
        if (ints != false) {
          intersections.push(ints);
        }
      }
    }
    if (intersections.length == 2){
      intersections.push({x: this.s1.x1, y: this.s1.y1});
    }
    return intersections;
  }
  fade(){
    this.col.levels[3] = this.col.levels[3] - 3;
    if (this.col.levels[3] < 0) {
      this.show = false;
    }
  } 
  draw() {
    this.s1.draw(this.col.levels[0], this.col.levels[1], this.col.levels[2], this.col.levels[3]);
    this.s2.draw(this.col.levels[0], this.col.levels[1], this.col.levels[2], this.col.levels[3]);
    this.s3.draw(this.col.levels[0], this.col.levels[1], this.col.levels[2], this.col.levels[3]);
  }
}
 
function setup() {
  videoIsPlaying = false; 
  createCanvas(1280, 720, P2D);
  //createCanvas(1920, 1080);
  video = createVideo( vidName + '.mp4', vidLoad);
  video.size(width, height);
  poseNet = ml5.poseNet(video, modelReady);
  poseNet.on('pose', function(results) {
    poses = results;
  });
  video.hide();
}
 
function modelReady() {
  select('#status').html('Model Loaded');
}
 
function mousePressed(){
  vidLoad();
}
 
function draw() {
  if (show) {
    image(video, 0, 0, width, height);
  } else {
    background(214, 214, 214);
  }
 
  drawKeypoints();
}
 
function drawKeypoints()  {
 
  for (let i = 0; i < poses.length; i++) {
 
    let pose = poses[i].pose;
    for (let j = 0; j < 5; j++) { let keypoint = pose.keypoints[j]; if ((j == 3 || j == 4) && keypoint.score > 0.7) { // left or right ear
        // calclulate average x between nose and eye
        let earX = 0, earY = 0;
        if (pose.keypoints[2].score > 0.7){
          earX = pose.keypoints[2].position.x;
          earY = pose.keypoints[2].position.y;
        } else {
          earX = pose.keypoints[1].position.x;
          earY = pose.keypoints[1].position.y;
        }
         let x1 = keypoint.position.x
         let y1 = keypoint.position.y
         let x2 = earX;
         let y2 = earY;
         //let x2 = (earX + pose.keypoints[0].position.x) / 2;
         //let y2 = (earY + pose.keypoints[0].position.y) / 2;
         let length = Math.sqrt(Math.pow(x1 - x2, 2) + pow(y1 - y2 , 2));
         let newX = x2 + (x2 - x1) / length * 1200;
         let newY = y2 + (y2 - y1) / length * 1200;
 
         let look = new FOV(x2, y2, newX, newY);
 
         if (look.direction == -1) {
          let lastR = right.pop();
          if (lastR != undefined){
            let ints = look.checkForIntersections(lastR)
            if (ints.length >= 3 && show) {
              points.push(ints);
            }
            right.push(lastR);
          }
          left.push(look)
         } else {
          let lastL = left.pop();
          if (lastL != undefined) {
            let ints = look.checkForIntersections(lastL)
            if (ints.length >= 3 && show) {
              points.push(ints);
            }
            left.push(lastL);
          }
          look.col =  color(56, 56, 56, initialAlpha); 
          right.push(look)
         }
      }
      let col = color(rgb);
      col.levels[3] = 2;
      fill(col.levels[0], col.levels[1], col.levels[2], col.levels[3]);
      noStroke();
      if (!debug){
      for (let i = 0; i < points.length; i++) {
        beginShape();
        for (let j = 0; j < points[i].length; j++) {
          vertex(points[i][j].x, points[i][j].y);
        }
        endShape(CLOSE)
      }
    }
      for (let i = 0; i < left.length; i++) {
        if (debug){left[i].draw();}
        left[i].fade();
        if (!left[i].show) {
          left.splice(i, 1);
        }
      }
      for (let i = 0; i < right.length; i++) {
        if (debug){right[i].draw();}
        right[i].fade();
        if (!right[i].show) {
          right.splice(i, 1);
        }
      }
    }
  }
}
 
function vidLoad() {
  time = video.duration();
  video.stop();
  video.loop();
  videoIsPlaying = true;
  if (!debug) {
  setTimeout(function(){ 
    show = false;
    video.volume(0);
    video.hide();
   }, time * 1000);
}
}
function keyPressed(){
  if (videoIsPlaying) {
    video.pause();
    videoIsPlaying = false;
  } else {
    video.loop();
    videoIsPlaying = true;
  }
}

chromsan-LookingOutwards03

We Make the Weather is an interactive installation  made as a collaboration between Greg Borenstein, Karolina Sobecka, and Sofy Yuditskaya. It was made in the aftermath of Hurricane Sandy and uses breath detection, motion capture, and the Unity game engine. The user controls a figure crossing a virtual bridge with their breath, where each breath makes the bridge longer and further from the figure. The user wears a headset that sends the sound of their breath to Unity, which controls the 3D environment projected on a screen. This is a particularly clever and unique way of interacting with the environment, and it plays perfectly into their concept and its environmental themes. It's also notable for both its visual simplicity and conceptual complexity.

 

chromsan-telematic


Add a whisper of text and try to interpret everyone else's whispers as each changes little by little.

 

In short, this is the game of telephone for random people on the internet with text instead of audio.

I wanted to implement this game (sometimes called "Chinese whispers," evidently) with text instead of voice while preserving the same quality of randomness that it has. I initially thought about using a reCAPTCHA style system to mess up the text, but that would have been too easy to read. Messing with the unicode made it much more difficult to read the messages, and I found a nice library for doing that. What's interesting about the game telephone is that everyone has an important role in playing since they each pass the message along, but all the players are equal in the sense that everyone's job is the same, except for one. In the original game, the person that starts the message is somewhat more important, but I got rid of this dynamic by having each visitor enter their own message, which is then interpreted by the other visitors.  In my version, all the players have the same role.

I had the idea to keep the messages so that the next visitor could add to the chain of whispers, which Glitch didn't seem to easily be able to do. I then decided to make my life much more difficult by shifting over to python and making a backend with Django, which I then put on Heroku, which was probably really unnecessary especially for this one feature. However, now the whispers that are added are persistent, so anyone can add to each of the chains and the initial phrases can keep morphing- they don't disappear when no one is online.

Direct link to project.

Initial ideas about messing up the text

Project's code can be found on GitHub.

 

chromsan-viewing04

Spectacle: things made with the intention to 'wow' the audience through technical means or particularly beautiful aesthetics.

Speculation: things made with the intention to critique, either something else or itself- usually very self aware.

This is a demo for Unreal Engine's upcoming support for Ray Tracing capabilities with new Nvidia RTX graphics cards. It was developed by Epic Games and Industrial Light and Magic. Defined in Warburton's terms, it is pure spectacle. It's meant to showcase a new technical feature while serving as advertising for both Unreal Engine and Disney. Technically, it is really quite impressive; it could have been lifted from the next Star Wars movie. It has a little narrative, and the characters are familiar, but there's nothing critical or self-aware about it. It's meant to accelerate technology and real time rendering, and it is meant to be very visible considering it is essentially an advertisement, and it has around 1.5 million views on YouTube. The demo is commerce, it is really quite distinct from the world of critical art. I would say it errs on the side of surplus, it's beautiful and clean and new, but probably unnecessary. Finally, it is functional in that it has a clear purpose and has excelled at fulfilling that purpose.

 

 

chromsan-clock

Prevent time from passing by knocking away the incoming seconds. The more you prevent time from passing, the faster the seconds will come. If one of the seconds makes it past you, time is reset and can even go into the future.

(use side to side arrow keys to control the paddle)

I liked the idea of making some sort of a game for this project, as time is usually quite a large part of games. I also wanted to make a clock that was not quite accurate, so that you could have some control over the clock's speed. I took inspiration from Pong, so that the player can prevent time from being added to the clock. To give a bit of a challenge, I made time speed up as the player is more successful. This way, if they miss, time is both reset and a couple seconds may be added to the clock. The main issue with using p5.js for this project is that the animation stops drawing when you leave a tab, so the time that is drawn will be different from the real time, and the scoring system gets messed up. I'd like to find a way around this.

(update on 9/25- I've found a kinda hacky way around this to fix the score and time, embedded sketch has been updated)

The green theme denotes AM and red denotes PM.

Original ideas:

Code also on GitHub.

// real time (not based on game)
let realMS, realS, realM, realH; 
// game time (based on game performance)
let gameS, gameM, gameH; 
// previous logged times 
let p_realS, p_realM, p_realH; 
// how often the times appear [0-999]
const timeFreqInit = 999;
let timeFreq = timeFreqInit, timeFreqInc = 20;
// assorted global vars 
const deflectorSpeed = 15, timeSpeed = -10;
const dispTimeY = 80;
 
let deflector, time = 0;
let times = [], score = 1, dispScore = 0, highScore = 0;
let font, fSize = 60, fillCol, backCol;
let millisRolloverTime = 0; 
 
function preload() {
  font = loadFont('digital-7.ttf');
}
 
function setup() {
  createCanvas(600, 750, P2D);
  textFont(font);
  textSize(fSize);
  updateRealTime();
  // to start, game time is the same as real time
  gameS = realS;
  gameM = realM;
  gameH = realH;
  // if it's AM, color green, otherwise red 
  setAmPmCol();
  // paddle 
  deflector = new Deflector(10, 100, width/2, height/4);
}
 
function draw(){
  setAmPmCol();
  fill(fillCol);
  textSize(fSize);
  background(backCol);
  updateRealTime();
  drawGameTime();
  // every time theres a new second...
  if (p_realS != realS){
    millisRolloverTime = millis();
    p_realS = realS;
  }
  realMS = floor(millis() - millisRolloverTime);
  // add a new time based off freq
  if (realMS % timeFreq == 0) {
    time++;
    let expanded = secondsToMin(time);
    let temp = {s: expanded.s, m: expanded.m, h: realH, disp: expanded.s};
    times.push(new Time(temp, random(30, width - 30), height));
  }
 
  for (let i in times) {
    if (times[i].offscreen()){
      // remove offscreen times
      times.splice(i, 1);
    }
    if (times[i].intersects(deflector)){
      // remove times after hitting paddle
      times[i].bounceMove(random(-10,10), random(6, 12));
      if (score > highScore) { highScore = score }
    } 
    if (times[i].escaped() && !times[i].t.used){
 
      // if the time made it past the paddle
      times[i].t.used = true;
 
      if (-score <= 0){ let res = addToTime(gameS, gameM, gameH, times[i].t.s, times[i].t.m); gameS = res.s; gameM = res.m; gameH = res.h; } // measure the diff between real and game time to get score let d1 = new Date(2018, 9, 20, gameH, gameM, gameS); let d2 = new Date(2018, 9, 20, realH, realM, realS); let d = d2.getTime() - d1.getTime() if (realS - gameS > 0) {score = d /1000;}
      else {score = realS - gameS}
      time = 0;
      timeFreq = timeFreqInit;
    }
    if (times[i].goingUp) {
      // move the times up by speed
      times[i].move(timeSpeed);
    }
    times[i].update();
    times[i].draw();
  }
  deflector.update();
  deflector.draw();
  drawScore();
}
 
function keyReleased() { 
  if (keyIsDown(RIGHT_ARROW)) { deflector.move(deflectorSpeed) }
  else if (keyIsDown(LEFT_ARROW)) { deflector.move(-deflectorSpeed) }
  else { deflector.move(0) }
}
 
function keyPressed() {
  if (keyCode == LEFT_ARROW){ deflector.move(-deflectorSpeed)} 
  if (keyCode == RIGHT_ARROW){ deflector.move(deflectorSpeed)} 
}
 
function updateRealTime() {
  realS = second();
  realM = minute();
  realH = hour() % 12;
}
 
function drawGameTime() {
  textAlign(CENTER);
  text(gameH, width/4, dispTimeY);
  text(":", width/8 * 3, dispTimeY)
  text(gameM, width/4 * 2, dispTimeY);
  text(":", width/8 * 5, dispTimeY)
  text(gameS, width/4 * 3, dispTimeY);
}
 
function drawScore() {
  textSize(25);
  textAlign(CENTER);
  let res = secondsToMin(score);
  let disp, pos = "";
  if (score < 0) {pos = "+"} // check if we need to display minutes if (res.m > 0) {disp = pos + (-1 * res.m) + ":" + res.s}
  else {disp = pos + (-1 * res.s)}
  text(disp, width - 80, 80);
  textSize(12);
  //text("s", width - 150, height- 20);
  textSize(25);
  text("Max:  " + highScore, width - 75, height - 20);
  textSize(12);
  text("s", width - 30, height- 20);
}
 
function secondsToMin(sec) {
  let secRl = sec % 60;
  let minRl = Math.floor(sec / 60);
  return {s: secRl, m: minRl};
}
 
function addToTime(pS, pM, pH, s, m) {\
  // add seconds and minutes new seconds and minutes
  let fixedS = pS + s, fixedM = pM + m, fixedH = pH;
  // case check for if any go over 60 
  if (fixedS >= 60) { 
    let res = secondsToMin(fixedS);
    fixedS = res.s;
    fixedM += res.m;
  }
  if (fixedM >= 60){
    // we can use the same function for mins + hrs
    let res = secondsToMin(fixedM);
    fixedM = res.s;
    fixedH += res.m;
  }
  if (fixedH > 12){
    // kinda hacky but really unlikely to happen
    fixedH = hour() % 12;
  }
  return {h: fixedH, m: fixedM, s: fixedS}
}
 
function setAmPmCol() {
  if (hour() > 12) {fillCol = color(205, 0, 0); backCol = color(20, 0, 0)}
  else { fillCol = color(0, 205, 0); backCol = color(0, 20, 0) }
}
 
// the 'paddle' that used to hit the numbers
class Deflector {
  constructor(h_, w_, x_, y_){
    this.height = h_;
    this.width = w_;
    this.x = x_;
    this.y = y_;
    this.deltaX = 0;
  }
  update() {
    this.x += this.deltaX;
    this.x = constrain(this.x, this.width/2, width - this.width/2);
  }
  move(steps) { this.deltaX = steps }
 
  draw() {
    textAlign(CENTER);
    textSize(70);
    text("___", this.x, this.y);
    textSize(fSize); 
  }
}
 
// the numbers that appear
class Time {
  constructor(t_, x_, y_) {
    this.t = t_;
    this.x = x_;
    this.y = y_;
    this.deltaY = 0;
    this.deltaX = 0;
    this.goingUp = true;
    this.col = color(fillCol.levels[0], fillCol.levels[1], fillCol.levels[2]);
    this.used = false;
  }
  update() {
    this.y += this.deltaY;
    this.x += this.deltaX;
  }
  move(steps) { 
     this.deltaY = steps;
  }
  bounceMove(stepX, stepY){
    this.deltaX = stepX;
    this.deltaY = stepY;
  }
  escaped(){
    if (this.y < height/4) { return true }
    else {return false}
  }
 
  intersects(deflector) { 
    // from https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
    if (deflector.x < this.x + 50 && deflector.x + deflector.width > this.x + 40 &&
        deflector.y < this.y + 10 && deflector.y + deflector.height > this.y - 40) {
      if (this.goingUp) {
        // if the time hits the deflector...
        score++; 
        if (score > 0 && timeFreq >= 750){
          timeFreq -= timeFreqInc;
        }
 
      }
      this.goingUp = false;
      this.t.disp = '#'
      return true;
    }
    return false;
  }
 
  offscreen() {
    if (this.y < -40 || this.y > height + 40) return true;
    else return false;
  }
 
  draw() {
    textAlign(CENTER);
    if (this.escaped()){
      let a = this.col.levels[3];
      this.col.setAlpha(a - 15);
      fill(this.col);
      text(this.t.disp, this.x, this.y);
    } else {
      fill(fillCol);
      text(this.t.disp, this.x, this.y);
 
    }
 
  }
}

chromsan-LookingOutwards02

This is a project by Marcin Ignac entitled "City Icon" from 2012. It's a generative city simulation with a variety of randomly created systems. Their interactions create a dynamic and constantly changing view of an imaginary city. I like the balance between the order of a city and the randomness in the direction that its parts take. How the water flows through and energy sources appear is fairly random, but what's impressive is that it is still is identifiable as a city, like you might see from an airplane above when flying at night. The generation transforms when it becomes a series of cells that make up a living organism or a collection of energy sources. I imagine there was quite a bit of pseudo-random number generation involved in making this project, as its parts seem to relate in a nice way.

Original source. 

chromsan-reading03

I think it's interesting that Naimark sees such a distinction between these types of art. Naimark claims that half the art world sees "first word" art as true art and the other half sees"last word" as true art. So, if we take that to be the case, then good art only comes around at the start or end of an artistic era. This seems silly, especially considering our classifications of the artistic periods are approximations, and there are many examples of outstanding artists working uncharacteristically differently for their time period. How can it be that we compare Giotto to Raphael and make a value judgement about their works simply based off of the technologies and knowledge that each artist had in his time?

We can only examine each artist and their work within the context of their lifetimes, and from there decide if it is good or not. Otherwise, we end up making choices solely based off of variables outside of the artists' control. Would it make sense to make the claim that since computers and computer art has advanced to a vastly more complex state, Vera Molnár's works are bad? Or conversely that her works are the best representation of computer art because they were the first? These seem like difficult propositions to back up.

Technological innovation, culture, and the art that follows is a long lineage that builds on the past. Certainly, Beethoven took what Haydn built and added his own take to it; Beethoven moved Haydn's creation beyond its original state. Is it better or worse for that? Neither. It's something different.

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 >= 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 >= 0 && roundedPercent <= 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 > 33 && roundedPercent <= 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 > 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 < 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 <= 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 > 25 && roundedPercent <= 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 > 55 && roundedPercent <= 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 > 60 && roundedPercent <= 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 > 66 && roundedPercent <= 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 > 85 && roundedPercent <= 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<=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;
}

chromsan-Scope

PDF: praxinoscope-output

This design is essentially four triangles whose top point moves towards the center. I made it as a practice for my main GIF, which uses this same effect. I experimented with coloring the triangles, but in the end settled for a black and white combination, which sort of gives the illusion of a 3D space. The triangles move with an easing function so they close faster than they open, which gives a more interesting movement than just a constant open and close.

The relevant code, inserted into the praxinoscope template:

void drawArtFrame (int whichFrame) { 
 
  int height = 40;
  int width = 40;
  translate(-20, -20);
 
  if (whichFrame <= nFrames/2){
    // close quickly 
    float eased = function_DoubleExponentialSigmoid(map(whichFrame, 0, nFrames/2, 0, 1), 0.7);
    fill(50);
    triangle(0, height, width, height, width/2, map(eased, 0, 1, height, height/2 ));
    fill(100);
    triangle(0, 0, 0, height, width/2 * map(eased, 0, 1, 0, 1), height/2);
    fill(150);
    triangle(0, 0, width, 0, width/2, height/2 * map(eased, 0, 1, 0, 1));
    fill(200);
    triangle(width, 0, width, height, map(eased, 0, 1, width, width/2), height/2); 
 
  } else {
    // open slowly
    float eased = function_DoubleExponentialSigmoid(map(whichFrame, nFrames/2, nFrames, 0, 1), 0.05);
    fill(50);
    triangle(0, height, width, height, width/2, map(eased, 0, 1, height/2, height ));
    fill(100);
    triangle(0, 0, 0, height, width/2 * map(eased, 0, 1, 1, 0), height/2);
    fill(150);
    triangle(0, 0, width, 0, width/2, height/2 * map(eased, 0, 1, 1, 0));
    fill(200);
    triangle(width, 0, width, height, map(eased, 0, 1, width/2, width), height/2); 
  }
}
 
// Taken from 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<=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;
}

 

 

chromsan-Reading02

1A. I think a good example of something exhibiting effective complexity is the structure of a tree. Trees grow in a random way, so no two trees are alike. In this sense they posses some randomness. That said, trees can be relied upon to grow in a fairly recognizable way; everyone can recognize the shape and structure of a tree when they see it. They are ordered in this regard. I think trees, among many natural things, perfectly straddle the line between chaos and order.

1B. The Problem of Locality, Code, and Malleability. This is philosophically an interesting problem, there are clear parallels between it and the mind-body problem. It is an important one as well, as it frames how we think of generative art. The dualist might argue that the art exists discretely from the code, that it exists in the place in which it is seen. The materialist might argue that the art is the code itself. I think to some degree both are correct; though the art is a function of the code, the code may have artistic properties and running it produces other artistic properties that cannot be experienced from just the code itself.