paukparl-clock

Brief:
I thought of a digital clock with each digit restlessly trying to stand still, while continuously having to offset external forces to stay balanced. I think such state is called dynamic stability. I was inspired by Jacob Tonski's Balance From Within. I used processing with geomerative and fisica libraries.

Rules:
1. Every time a digit needs to update, it disappears and falls again from a fixed position. Same rule when a digit goes out of bounds. This can cause some unexpected chain reactions in the following few seconds.
2. Each digit has an inherent "tremble" generated by noise. The digit would try to offset the torque by shifting its body left or right. But once it falls to its side, it will be hard to get back up again. Sadly, it will stay there until it needs update and a new number takes its place.


when the clock first turns on


some tension from 8


I like how 0 falls on the 5.

Thoughts:
I think my current code cannot perfectly offset the forces. It could be more interesting if the numbers stayed on their feet longer. Also, I first imagined the numbers kind of bouncing left and right micro-scale (like a person does on one foot) and bumping into one another, but couldn't implement that in time. But I can still somewhat see how the numbers are reluctant to fall on its side, and I like that.

import fisica.*;
import geomerative.*;
 
FWorld world;
RFont font;
FBody[] bodies;
 
int FONT_SIZE = 200;
 
int lastShiftTime; 
int newShiftInterval;
float randomShift;
boolean bRecording = false;
int lastMinute;
 
boolean[] needToCheckUpdate = {true, true, true, true};
 
FCharController[] controllers;
FChar[] colon;
 
void setup() {
  size(640,200);
  smooth(8);
 
  frameRate(60);
 
  Fisica.init(this);
  Fisica.setScale(4);
  RG.init(this);
 
  RG.setPolygonizer(RG.ADAPTATIVE);
 
  world = new FWorld();
 
  randomShift = random(-50, 50);
  world.setGravity( 0, 400 );
  newShiftInterval= int(random(2000,8000));
  world.setEdges(0, 0, width, height+5, color(255, 0, 0)); 
  world.setEdgesFriction(1); 
  world.remove(world.top);
  world.remove(world.left);
  world.remove(world.right);
 
  font = RG.loadFont("cheltenham-cond-normal-300.ttf");
 
  colon = new FChar[2];
  colon[0] = new FChar('.', width/2-15, 142);
  colon[1] = new FChar('.', width/2-15, 75);
  world.add(colon[0]);
  world.add(colon[1]);
 
  controllers = new FCharController[4];
  for (int i=0; i<controllers.length; i++) {
    FCharController controller = new FCharController(i);
    controller.dropNewCharObj();
    controllers[i] = controller;
  }  
  lastMinute = minute();
}
 
void draw() {
  checkMinutePassed();
  for (int i=0; i<controllers.length; i++) controllers[i].update();
  background(0);
  world.draw(this);
  world.step();  
}
 
 
void checkMinutePassed() {
  if (minute() == lastMinute) return;
  for (int i=0; i<needToCheckUpdate.length; i++) needToCheckUpdate[i] = true; lastMinute = minute(); } class FCharController { FChar charObj; int index; int lastNumber; boolean disappeared; int dropX; float shiver; float xoff; FCharController(int index) { //charObj = new FChar('9', 100, 150); this.index = index; disappeared = false; lastNumber = 0; // doesn't matter what I assign here dropX = index*150+50; if (index==1) dropX -= 20; else if (index==2) dropX += 20; } void dropNewCharObj() { needToCheckUpdate[index] = false; int newNumber = parseTime(index); println((char)(newNumber+48)); charObj = new FChar((char)(newNumber+48), dropX, -50); world.add(charObj); lastNumber = newNumber; } void dropNewCharObj(int test) { needToCheckUpdate[index] = false; int newNumber = test; println((char)(newNumber+48)); charObj = new FChar((char)(newNumber+48), dropX, -50); //charObj.setRotation(0.115*PI); world.add(charObj); } void update() { if(charObj.isTouchingBody(world.bottom)) { shiver = noise(xoff+=0.025); shiver*=10; shiver-=5; charObj.addTorque(shiver*10000); charObj.addForce(shiver*-10000, 0, 40, 0); } if (charObj.getY()>230) {
      charObj.removeFromWorld();
      dropNewCharObj();
      xoff = random(100);
    }
    if (needToCheckUpdate[index] == true) {
      if (lastNumber == parseTime(index)) return;
      int newNumber = parseTime(index);
      charObj.removeFromWorld();
      dropNewCharObj();
      lastNumber = newNumber;
      xoff = random(100);
    }
  }
 
  int parseTime(int index) {
    switch (index){
      case 0: return hour()/10;
      case 1: return hour()%10;
      case 2: return minute()/10;
      case 3: return minute()%10;
    }
    return 9;
  }
 
}
 
 
class FChar extends FPoly {
  RShape m_shape;
  RShape m_poly;
  boolean m_bodyCreated;
  float initialAngle=0;
 
  FChar(char chr, float dropX, float dropY){
    super();
 
    polygonize(chr);
    setInitialAngle(chr);
 
    this.setFill(0, 0, 0);
    this.setStroke(255);
    if (chr=='.') {
      this.setDamping(1);
      this.setRestitution(0.2);
      this.setBullet(true);
      this.setStaticBody(true);
      this.setPosition(dropX, dropY);
    }
    else {
      this.setDamping(1);
      this.setRestitution(0.2);
      this.setBullet(true);
      this.setPosition(dropX, dropY);
      this.setAllowSleeping(true);
      this.setRotation(initialAngle);
    }
 
 
    m_bodyCreated = true;
  }
 
 
  void polygonize(char chr) {
    String txt = "";
    txt += chr;
 
    RG.textFont(font, FONT_SIZE);
    m_shape = RG.getText(txt);
    m_poly = RG.polygonize(m_shape);
 
    if (m_poly.countChildren() < 1) return;
    m_poly = m_poly.children[0];    
 
    // Find the longest contour of our letter    `  
    float maxLength = 0.0;
    int maxIndex = -1;
    for (int i = 0; i < m_poly.countPaths(); i++) { float currentLength = m_poly.paths[i].getCurveLength(); if (currentLength > maxLength) {
        maxLength = currentLength;
        maxIndex = i;
      }
    }
 
    if (maxIndex == -1) return;
 
    RPoint[] points = m_poly.paths[maxIndex].getPoints();
 
    for (int i=0; i<points.length; i++) {
      this.vertex(points[i].x, points[i].y);
    }
  }
 
 
  void setInitialAngle(char chr) {
    float randomFloat = random(1);
    switch(chr) {
      case '0':
        initialAngle = -0.01*PI;
        break;
      case '1':
        if (randomFloat<0.5) initialAngle = 0.115*PI;
        else initialAngle = -0.1*PI;
        break;
      case '2':
        if (randomFloat<0.5) initialAngle = 0.14*PI;
        else initialAngle = -0.17*PI;
        break;
      case '3':
        initialAngle = -0.15*PI;
        break;
      case '4':
        initialAngle = 0.135*PI;
        break;
      case '5':
        initialAngle = -0.1*PI;
        break;
      case '6':
        //initialAngle = 0.14*PI;
        initialAngle = 0.04*PI;
        break;
      case '7':
        initialAngle = -0.04*PI;
        break;
      case '8':
        initialAngle = -0.03*PI;
        break;
      case '9':
        initialAngle = -0.095*PI;
        break;
 
 
    }
  }
 
 
  boolean bodyCreated(){
    return m_bodyCreated;
  }
 
  void draw(PGraphics applet){
    preDraw(applet);
    m_shape.draw(applet);
    postDraw(applet);
  }
}