How to animate properly arcs according to time with millis()? (draw letters animation)

Hello,
Here is my attempt to animate arcs with millis(). It is not smooth, it jumps… Not what it should be!
Thanks a lot in advance.
Best,
L

float startTime;

void setup() {
  size(400, 400);
  startTime=millis();
}
void draw() {
  background(0);

  float x = width/2;
  float y = height/2;
  float d = width * 0.8;

  float t =0;
  float t1=0;
  float angle =0;
  t=map(millis(), 0, 1000, 0, 1);
  t1=map(millis(), 0, 2000, 0, 1);
  fill(255, 100);
  stroke(0);
  // strokeWeight(5);
  if (millis()<startTime+1000) {
    arc(x, y, d, d, 0, lerp(0, radians(90), t));
  }
  if (millis()<startTime+2000 ) {
    arc(x, y, d-80, d-80, 0, lerp(0, radians(180), t1));
  }
  if (millis()>=startTime+1000) {
    arc(x, y, d, d, 0, radians(90));
  }
  if (millis()>=startTime+2000) {    
    arc(x, y, d-80, d-80, 0, radians (330));
  }
}

Please format your code using the </> button
More on code formatting: Guidelines—Asking Questions
Homework policy: https://discourse.processing.org/t/faq-guidelines/5#homework
Asking Questions: Guidelines—Asking Questions

1 Like

Maybe use frameCount instead of millis ()

1 Like

Dear @Chrisir,
I will try with frameCount. Thanks for the suggestion :wink:

1 Like

Hey Chrisir,
Here is a try using p5js (to test if different of P5) using frameCount, strangely the animation of the next segment is starting before and it doesn’t look smooth at all. How come?! Thanks for your help as usual :-))

function setup() {
  createCanvas(400, 400);
  startTime = frameCount;
}

function draw() {
  var t = map(frameCount, 0, 50, 0, 1);
  background(220);
  noFill();
  stroke (0);
  strokeWeight(10);
  strokeCap(SQUARE);
  push();
  if (frameCount < 50) {
    var lineLength = 0;
    line(25, 130, 25, lineLength=map(frameCount, 0,50, 130,60));
  } else {
    line(25, 130, 25, 60);
  }
  pop();
  push();
  if (frameCount > 50 && frameCount<=100) {
    arc(50, 50, 50, 50, radians(180), lerp(radians(180), radians(270), t));
  } if(frameCount>100) {
    arc(50, 50, 50, 50, radians(180), radians(360));
  }
  pop();
  push();
 
  var t1 = map(frameCount, 0, 25, 0, 1);
  if (frameCount > 100 && frameCount<=125) {
    arc(50, 60, 50, 50, radians(360), lerp(radians(360), radians(395), t));
  } if(frameCount>125) {
    arc(50, 60, 50, 50, radians(360), radians(450));
  }
  pop();
}
1 Like

I always use the elapsed time between frames to control animation because it means it is independent of the frame rate.

Do you want a P5 (Java) solution or a p5.js (Javascript) solution?

Hey @quark thanks for your answer.
I’d prefer a P5 solution since I want to animate some letters that will react to the environnement(sound, people movement) in an installation. I just tried a quick test with p5js to see if there is any difference and find a solution I could apply in Java.
You say you work with elapsed time so with millis()?! Yes frameRate can be lower or higer, it fluctuates… Thanks a lot

Although it is possible to animate a specific drawing for each arc using predetermined coordinates (as in your original code) it means you are having to repeat code for each arc.

The preferred solution would be to write one piece of code that can animate any arc by specifying

  • the start time
  • duration (the length of time to perform the animation)
  • location
  • start angle
  • end angle

In Java the best way to do this would be to use OO (object orientation).Have you ever used classes and objects in P5?

Dear @quark,
Thank you very much for your very clear answer.
Yes I need to create a class and then an iteration of the object for each letter composed of arcs with different specificities… I will try my best!
Tks a lot!

Most of my animation work involved objects moving with a given speed and direction so using elapsed time between frames makes sense.

Having looked at your original problem it is probably best to use the elapsed time since the program started as this will help coordinate the animations.

Using OO to create a generic solution is still the way to go. :smile:

BTW I could create a simple example sketch but you might want to have a go yourself first. Let me know.

Yes I try now, not sure I’ll manage to handle it but at least I try… Thank you so much!!

1 Like

Dear @quark,

Here is my attempt, there is certainly some room for improvement but at least it does the job and compile :wink: For instance what if I need to draw a straight vertical line?!

float startTime = 0;
float y;
float compt = 0;
drawArc drawarc;
drawArc drawarc1;

void setup() {
  size(400, 400);
  startTime=millis();
  PVector l = new PVector(150,150);
  PVector m = new PVector(0,0);
  drawarc = new  drawArc(startTime, startTime+1000,  l, 80, radians (90), radians (220));
  drawarc1 = new  drawArc(0, 1000000, m , 80, radians (90), radians (180));
}

void draw() {
  background(0);
  stroke(255);
  translate(width/2, height/2);
  if(millis()<1000){
  drawarc.animate();
   drawarc.display();
  }if(millis()>=1000){
  drawarc1.stat();
  drawarc1.display();
  }
}

class drawArc {
 
  float startT = 0;
  float dur = 0;
  PVector loc;
  float startAngle;
  float endAngle;
  float x = 0;
  float y = 0;
  float arcSize;
  float angle = 0;
  
 drawArc( float _startT, float _dur, PVector _loc, float _arcSize, float _startAngle, float _endAngle) {
    startT = _startT;
    dur = _dur;
    loc = _loc;
    startAngle = _startAngle;
    endAngle = _endAngle;
    arcSize = _arcSize;
   angle = endAngle-startAngle;
  }
  
  void animate(){
     PVector loc = new PVector(x,y);
    arc(loc.x, loc.y, arcSize, arcSize, startAngle, map(millis(), startT, startT+dur, startAngle, endAngle));
  }
  
   void stat(){ 
    arc(loc.x, loc.y, arcSize, arcSize, startAngle, endAngle);
  }
  
  void display(){
    stroke(255);
    strokeWeight(10);
    strokeCap(SQUARE);
    noFill();
  }
}
1 Like

Congratulations on a really good first attempt but I do have some suggestions for you.

Call the class Arc
In Java it is good practice that all class names are capitalized so if you wanted to call the class drawArc then use DrawArc.
In OO classes usually represent concrete entities so we use nouns for their name. drawArc is an activity or verb and would be something we do with an arc.

The class methods
In OO the class methods (functions) describe what activities we can do with objects of this class i.e. Arc(s). Being activities they usually have a verb in their name. So you have a method (aka function) called display but in fact does not actually display anything and two other methods animate and stat which both display the arc depending on whether the arc has finished animation.
In this case I would have two methods, update to determine how much of the arc to draw and render to draw the arc based on the update method. It is good programming practice to separate the drawing code into their own method.

Make the class do the work
In your draw method you have

if(millis()<1000){
  drawarc.animate();
  drawarc.display();
 }
if(millis()>=1000){
  drawarc1.stat();
  drawarc1.display();
}

It would be far better to get the class decide on what is drawn based on the current time.

Limitations of the arc() method
If we ignore animation for a moment, the arc method accepts two angles representing the start and end angles representing the limits of the arc to be drawn. The end angle must be greater than the start angle or nothing gets drawn, this makes animating in an anti clockwise direction or over the zero degree position difficult.

Animating straight lines
Once you have created a working class for Arcs you can use the same pattern for drawing straight lines.

Working example code
Below is a working example for Arcs that covers all the points I made above. Like all code it can be improved but hopefully you find it both helpful and educational.

import java.util.*;

int TIMESTAMP;
List<Arc> arcs = new ArrayList<Arc>();

public void setup() {
  size(400, 400);
  // Clockwise animation
  arcs.add(new Arc(200, 200, 100, radians(135), radians(260), 1, 1000, 2000));
  // Clockwise animation crossing zero degree datum
  arcs.add(new Arc(200, 200, 120, radians(315), radians(55), 1, 2800, 1500));
  // Anti-clockwise animation
  arcs.add(new Arc(200, 200, 170, radians(280), radians(175), -1, 4500, 2000));
  // Anti-clockwise animation crossing zero degree datum
  arcs.add(new Arc(200, 200, 190, radians(45), radians(285), -1, 6200, 1200));
  TIMESTAMP = millis();  // This will compensate for long setup times
}

public void draw() {
  background(0);
  for (Arc a : arcs) a.update(millis() - TIMESTAMP);
  stroke(255);
  strokeWeight(10);
  for (Arc a : arcs) a.render();
}

class Arc {

  private float px, py;
  private float rad;
  private float angS, angE;
  private int dir;
  private int startTime, duration;
  private float t = 0; // parametric variable
  private float TAU = 2 * PI;

  /**
   * @param px arc centre
   * @param py arc centre
   * @param rad radius of the arc
   * @param angS the angle the animation starts from (radians)
   * @param angE the angle the animation ends at (radians)
   * @param dir animation direction (-1 = anti-clockwise ; 1 = clockwise)
   * @param startTime sketch time to start animation (milliseconds)
   * @param duration time to perform animation (milliseconds)
   */
  public Arc(float px, float py, float rad, float angS, float angE, int dir, int startTime, int duration) {
    this.px = px;
    this.py = py;
    this.rad = rad;
    this.angS = normAngle(angS);
    this.angE = normAngle(angE);
    this.dir = dir < 0 ? -1 : 1;
    this.startTime = startTime;
    this.duration = duration;
  }

  /**
   * Converts an angle to its equivalent value in the range 0 - 2Pi
   */
  private float normAngle(float a) {
    while (a < 0) a += TAU;
    while (a > TAU) a -= TAU;
    return a;
  }

  /**
   * Calculate the amount of arc to render
   * @param ct current sketch time
   */
  public void update(int ct) {
    if (t < 1) {
      if (ct > startTime)
        t = (ct - startTime)/(float)duration;
      t = constrain(t, 0, 1);
    }
  }

  /**
   * Render the arc using the current stroke and strkeWeight settings
   */
  public void render() {
    if (t > 0) {
      push();
      noFill();
      translate(px, py);
      float as = angS, ae = angE, a_t = 0;
      if (dir > 0) {
        if (as > ae) ae += TAU;
        a_t = as + (ae - as) * t;
        arc(0, 0, 2*rad, 2*rad, as, a_t);
      } else {
        if (ae > as) as += TAU;
        a_t = as + (ae - as) * t;
        arc(0, 0, 2 * rad, 2 * rad, a_t, as);
      }
      pop();
    }
  }
}
2 Likes

Dear @quark thanks a million for your great sketch!! I have many questions but not enough time to ask you them now, but tomorrow, I’ll be back. Thank your very much for your compliment regarding my attempt too.

1 Like

Dear @quark,
Your code is both very helpful and educational and I thank you very much for spending your precious time on it for me. :pray: :pray: :pray:
I will try to be as concise as possible:
Call the class Arc
Yes sorry I knew this good practice though…

The class methods
I guess it’s the main mistake in my class methods. The display method didn’t draw anything…
And as you put it " the class didn’t decide on what is drawn based on the current time", that’s also why I had to double the arc in draw that it won’t disappear once drawn (do I get it right?!).

Thanks a lot for the smart dir variable into the class that allows to draw in clockwise and anti-clockwise directions! But I don’t get exactly its declaration : ```
this.dir = dir < 0 ? -1 : 1;

And for the Java library why importing  java.util.*?! And for the private and public Java typical terms, I'll read about it...
Sorry for these 'stupid' questions but it's a lot of new informations to grasp and would be worse not to ask them!!
I am working on the line class and another class to group both line and arc classes to form a letter...
Thank you very much again dera @ quark, you and @chrisir are my coding guardian angels!!

It is good practice to capitalize the names of classes and also to start method / function names with lower class letters because it helps other programmers to follow the logic in your code without having to distinguish between classes and functions. It also helps the code creator to debug and / or modify the code.

A class method defines something we can do with objects of this class so generally method names include or start with verbs. The method name should also be descriptive of what it does. So if I saw a class with a method called display then I would expect it to draw something :grinning: :
Also a class method should perform a single task so in this class

  • updating the class attributes / fields based on the current time, and
  • displaying the current state of the arc

should be two separate methods. The advantage of doing this is that you can modify the drawing code without risk of introducing errors into the update code and vice versa. Creating methods with a single responsibility is a practice well worth following as it will make your life easier, especially when coming to larger projects.

Correct. In your draw method the code to update and display the arc is messy and introduces unnecessary complexity. I think you will agree that the code in my draw function is much easier to understand.

?: is a special operator and in this example is the same as

if(dir < 0)
  this.dir = -1;
else
  this.dir = 1;

I have added this import so I can use the Java List and ArrayList classes. Processing imports many Java classes for you but not these two.

These terms are used by Java to control access to variables, fields and methods. In Processing they can ALL be removed since Processing will make them all public access for you. If you ever move to a full Java IDE such as Eclipse then you have to do all your own imports and add your own public, private … access modifiers. Until then I suggest you just ignore them. :smile:

I think I have answered all your questions so far but free to ask more :smile:

2 Likes

Dear @quark,

Thank you so much for your last message that teach me once again many new things I ignored!
I reworked my code, I added a Class to draw lines and then added both Arc and Line Classes inside a Class that draw a single letter (formed by the arcs and lines). I am not sure my Line Class is clean, neither the rest of the code, but at least I managed to do what I intended : writing a short text composed by basic graphic elements. Now Id’ like to write a sentence and to make people interact with it in order to be able to read it… Please can you help me once again to point out my mistakes in order to be able to continue?! Thank you very much in advance for your precious advice :-))
Best wishes,
L

// Importing libraries, listing variables
import java.util.*;
int TIMESTAMP;
boolean drawGrid;
int gridResolutionX, gridResolutionY;
int tileSize = 25;
int gridX, gridY;
List<D> d = new ArrayList<D>();
List<O> o = new ArrayList<O>();
List<Others> others = new ArrayList<Others>();

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void setup() {
  size(1000, 1000);

  gridResolutionX = round(width/tileSize)+2;
  gridResolutionY = round(height/tileSize)+2;
// Add new letters to the ArrayList
  d.add(new D());
  d.add(new D());
  o.add(new O());
  o.add(new O());
  others.add(new Others((float)-10*tileSize, (float)-tileSize*8));

  TIMESTAMP=millis();
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void draw() {
  background(0);
// draw the reference grid
  drawGrid();
// draw the objects of each Class
  translate(tileSize*5, tileSize*7);
  for (D d1 : d) d1.render();
  translate(tileSize*4, 0);
  for (O o1 : o) o1.render();
  translate(tileSize*4, 0);
  for (D d2 : d) d2.render();
  translate(tileSize*4, tileSize*4);
  rotate(3.15);
  for (O o2 : o) o2.render();
  translate(tileSize*5, tileSize*5);
  for (Others ot : others) ot.render();

  //fill(255);
  //rect(250,250,50-tileSize/2,50-tileSize/2);
}

// draw reference grid
void drawGrid() {
  rectMode(CENTER);
  for (int gridX = 0; gridX < gridResolutionX; gridX++) {
    for (int gridY = 0; gridY < gridResolutionY; gridY++) {
      float posX = (gridX*tileSize)-tileSize/2;
      float posY = (gridY*tileSize)-tileSize/2;
      noFill();
      stroke(255, 120);
      strokeWeight(0.25);
      rect(posX, posY, tileSize, tileSize);
    }
  }
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

class DrawArc {

  float startT = 0;
  float dur = 0;
  float startAngle;
  float endAngle;
  float x, y;
  float arcSize;
  float angle = 0;
  float TAU = 2*PI;
  float t = 0;
  int dir;

  DrawArc( float x, float y, float startT, float dur, float arcSize, int dir, float startAngle, float endAngle) {
    this.startT = startT;
    this.dur = dur;
    this.x = x;
    this.y = y;
    this.startAngle = normAngle(startAngle);
    this.endAngle = normAngle(endAngle);
    this.dir = dir < 0 ?-1 : 1;
    this.arcSize = arcSize;
    this.angle = endAngle-startAngle;
  }

  // Convert an angle to its equivalent value in the range 0 - 2PI
  float normAngle(float a) {
    while (a<0) a += TAU;
    while (a>TAU) a -= TAU;
    return a;
  }

  // calculate the amount of arc to render
  void update(int curTime) {
    if (t<1) {
      if (curTime > startT)
        t = (curTime - startT)/dur;
      t = constrain(t, 0, 1);
    }
  }

  // Rendering the arc according to t value and specifying the direction
  void render() {
    rectMode(CENTER);
    if (t>0) {
      push();
      noFill();
      translate(x, y);
      rotate(TWO_PI*t);
      float angleT;
      if (dir > 0) {
        if (startAngle>endAngle)  endAngle += TAU;
        angleT = startAngle+(endAngle - startAngle)*t;
        arc(0, 0, arcSize, arcSize, startAngle, angleT);
      } else {
        if ( endAngle > startAngle) startAngle += TAU;
        angleT = startAngle+(endAngle - startAngle)*t;
        arc(0, 0, arcSize, arcSize, angleT, startAngle);
      }
      pop();
    }
  }
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

class Line {

  float startT = 0;
  float dur = 0;
  float startLineX, endLineX;
  float startLineY, endLineY;
  float t = 0;
  float lineLengthX, lineLengthY;
  int dir;

  Line( float startLineX, float endLineX, float startLineY, float endLineY, int dir, float startT, float dur) {
    this.startT = startT;
    this.dur = dur;
    this.startLineX = startLineX;
    this.endLineX = endLineX;
    this.startLineY =  startLineY;
    this.endLineY = endLineY;
    this.dir = dir < 0 ?-1 : 1;
  }

  void update(int curTime) {
    if (t<1) {
      if (curTime > startT)
        t = (curTime - startT)/dur;
      t = constrain(t, 0, 1);
    }
  }

  void render() {

    if (t>0) {
      push();
      translate(startLineX, startLineY);
      rotate(TWO_PI*t);
      noFill();
      float lineH;
      float lineV;
      if (dir>0) {
        if (startLineX > endLineX) endLineX += lineLengthX;
        if (startLineY > endLineY) endLineY += lineLengthY;
        lineH = startLineX+(endLineX - startLineX)*t;
        lineV = startLineY+(endLineY - startLineY)*t;
        line(startLineX, startLineY, lineH, lineV);
      } else {
        if (endLineX > startLineX) startLineX += lineLengthX;
        if (endLineY > startLineY) startLineY += lineLengthY;
        lineH = startLineX+(endLineX - startLineX)*t;
        lineV = startLineY+(endLineY - startLineY)*t;
        line(lineH, lineV, startLineX, startLineY);
      }
      pop();
    }
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Class to draw the d letter
class D {
  List<DrawArc> arcs = new ArrayList<DrawArc>();
  List<Line> lines = new ArrayList<Line>();
  D() {

    rectMode(CENTER);
    //       DrawArc( float x, float y, float startT, float dur, float arcSize, int dir, float startAngle, float endAngle) {
    arcs.add(new  DrawArc(0, tileSize*2, 1000, 1000, tileSize*3, 1, radians (0), radians (180)));
    arcs.add(new  DrawArc(0, tileSize*2, 1000, 1000, tileSize*4, 1, radians (0), radians (180)));
    arcs.add(new  DrawArc(0, tileSize*2, 2500, 1000, tileSize*3, 1, radians (180), radians (270)));
    arcs.add(new  DrawArc(0, tileSize*2, 2500, 1000, tileSize*4, 1, radians (180), radians (270)));
    arcs.add(new  DrawArc(0, tileSize*-4, 4500, 1000, tileSize*3, -1, radians (360), radians (270)));
    arcs.add(new  DrawArc(0, tileSize*-4, 4500, 1000, tileSize*4, -1, radians (360), radians (270)));

    //        Line( float startLineX, float endLineX, float startLineY, float endLineY, int dir, float startT, float dur) {
    lines.add(new Line(tileSize*0.75, tileSize*0.75, 0, -4*tileSize, 1, 3500, 1000));
    lines.add(new Line(tileSize, tileSize, 0, -4*tileSize, 1, 3500, 1000));
  }

  void render() {

    for (int i = 0; i<arcs.size(); i++) {
      arcs.get(i);
      stroke(255);
      strokeWeight(0.5*i+1);
      strokeCap(SQUARE);
      arcs.get(i).update(millis()- TIMESTAMP);
      arcs.get(i).render();
    }

    for (int j = 0; j<lines.size(); j++) {
      lines.get(j);
      stroke(255);
      strokeWeight(2*j+1);
      lines.get(j).update(millis()- TIMESTAMP);
      lines.get(j).render();
    }
  }
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// class to draw the o letter
class O {
  List<DrawArc> arcs = new ArrayList<DrawArc>();

  O() {
    rectMode(CENTER);
    //       DrawArc( float x, float y, float startT, float dur, float arcSize, int dir, float startAngle, float endAngle) {
    arcs.add(new  DrawArc(0, tileSize*2, 1000, 1000, tileSize*3, 1, radians (0), radians (90)));
    arcs.add(new  DrawArc(0, tileSize*2, 1000, 1000, tileSize*4, 1, radians (0), radians (90)));
    arcs.add(new  DrawArc(0, tileSize*2, 2500, 1000, tileSize*3, 1, radians (180), radians (360)));
    arcs.add(new  DrawArc(0, tileSize*2, 2500, 1000, tileSize*4, 1, radians (180), radians (360)));
  }

  void render() {
    for (int i = 0; i<arcs.size(); i++) {
      arcs.get(i);
      stroke(255);
      strokeWeight(1*i+1);
      strokeCap(SQUARE);
      arcs.get(i).update(millis()- TIMESTAMP);
      arcs.get(i).render();
    }
  }
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

class Others {
  List<DrawArc> arcs = new ArrayList<DrawArc>();
  List<Line> lines = new ArrayList<Line>();

  float x, y;
  Others(float x, float y) {
    this.x = x = tileSize;
    this.y = y = tileSize;
    rectMode(CENTER);
    //       DrawArc( float x, float y, float startT, float dur, float arcSize, int dir, float startAngle, float endAngle) {
    arcs.add(new  DrawArc(x, y+tileSize*2, 1000, 1000, tileSize*3, 1, radians (0), radians (90)));
    arcs.add(new  DrawArc(x, y+tileSize*2, 1000, 1000, tileSize*4, 1, radians (0), radians (90)));
    arcs.add(new  DrawArc(x-tileSize*2, y-tileSize*12, 2500, 1000, tileSize*3, 1, radians (90), radians (270)));
    arcs.add(new  DrawArc(x-tileSize*2, y-tileSize*12, 2500, 1000, tileSize*4, 1, radians (90), radians (270)));
    //        Line( float startLineX, float endLineX, float startLineY, float endLineY, int dir, float startT, float dur) {
      translate(x,y);
    lines.add(new Line(-0.25*tileSize, -0.25*tileSize, -4.5*tileSize, -2.5*tileSize, 1, 3500, 1000));
    lines.add(new Line(-0.5*tileSize,-0.5*tileSize, -4.5*tileSize, -2.5*tileSize, 1, 3500, 1000));
  }


  void render() {
    for (int i = 0; i<arcs.size(); i++) {
      arcs.get(i);
      stroke(255);
      strokeWeight(1);
      strokeCap(ROUND);
      arcs.get(i).update(millis()- TIMESTAMP);
      arcs.get(i).render();
    }
    for (int j = 0; j<lines.size(); j++) {
      lines.get(j);
      stroke(255);
      strokeWeight(1);
      lines.get(j).update(millis()- TIMESTAMP);
      lines.get(j).render();
    }
  }
}

Some suggestions

  • change the name of the DrawArc class to Arc (as previously recommended). Personally I would simply replace your DrawArc class with my Arc class and modify the rest of the code to suit, see next comment.
  • the DrawArc constructor has 9 parameters, a lot to remember so arrange them by function. (See my Arc constructor, they are grouped by function, the first three define position and arc radius, the next 3 define the start and end angles and the direction to take between them, the last define animation start time and duration.
  • make sure your variable names are unambiguous. For instance you have arcSize which could mean the arc radius or the angular size of the arc. To differentiate between these quatities arcRad and arcRange would be better.
  • in the render methods start and end with push() and pop() methods so that you isolate all drawing commands from the rest of the code.
      void render(){
        push();
        // all drawing methods
        pop();
      }
  • In the letter classes you have incorporated the time update logic into the render method. I would recommend making these separate methods in the same way as the Arc and Line classes.
  • when creating the ‘letter’ object you could pass the time to start drawing it. This could be added to the start time for the lines and arcs.

Have to leave it there for the moment :smile:

1 Like

I have been busy but I am back with some more suggestions to improve your code.

We will start here

List<D> d = new ArrayList<D>();
List<O> o = new ArrayList<O>();

This is fine if you want to display ‘dodo’ but what about words using different characters, you would need a list for each and every character that you might want to display. This is not practical especially if we want both uppercase and lowercase letters and maybe special characters e.g. ? & % #.

Classes and objects are the core of object orientation (OO) but probably the most powerful feature of OO is inheritance. Unfortunately, it is probably the most misunderstood / misused feature of OO.

I am going to show how to use inheritance to solve the problem of multiple lists.

Let’s consider the two classes O and D what do they do? Well, they both draw a glyph. Now Wikipedia says a glyph is any kind of purposeful mark so will cover all letters, numbers and special characters. It means we can say that ‘D’ is a kind of glyph and ‘O’ is also a kind of glyph. So we will create 3 classes like this

abstract class Glyph { /* class attributes and methods */ }
class O extends Glyph { /* class attributes and methods */ }
class D extends Glyph { /* class attributes and methods */ }

Glyph is called the parent class and O and D child classes – hence the term inheritance. A child class incorporates all the attribute and methods from the parent class and adds attributes and methods specific to itself. So, when creating the classes any attributes and methods common to all characters are stored in the Glyph class and those specific to a character are stored in the appropriate child class.

So back to the lists, instead of one list for each character type we can have one list to store all character types.

List<Glyph> glyphs = new ArrayList<Glyph>();

And we can store characters with

glyphs.add(new O(…));
glyphs.add(new D(…));
glyphs.add(new D(…));
...

To draw any character, it is simply a matter of creating a new class that extends Glyph. Please note that the keyword abstract does not signify a parent class it has a different meaning altogether which I can explain in another reply if you wish.

A similar situation occurs inside the D class

class D {
    List<Arc> arcs = new ArrayList<Arc>();
    List<Line> lines = new ArrayList<Line>();

Arcs and lines are simple drawing elements used in combination to create a glyph but what if we wanted a Bezier curve element? Same problem same solution but this time I have called the parent class Element and the classes Arc and Line inherit from it.

I should point out that this description simply scratches the surface of OO.

This description would be of little use without some working code to demonstrate it so I have created a sketch which utilises inheritance. The video shows the sketch output and below it the full source code.

Sketch code

import java.util.ArrayList;
import java.util.List;

int TIMESTAMP;
List<Glyph> glyphs = new ArrayList<Glyph>();

void setup() {
  size(560, 360);
  int st = 1000, dt = 600, gw = 100, gh = 100, py = 20;
  glyphs.add(new Q(20, py, gw, gh, st));
  glyphs.add(new U(20 + gw, py, gw, gh, st + dt));
  glyphs.add(new A(20 + gw * 2, py, gw, gh, st + dt * 2));
  glyphs.add(new R(20 + gw * 3, py, gw, gh, st + dt * 3));
  glyphs.add(new K(20 + gw * 4, py, gw, gh, st + dt * 4));

  st = 4000;
  gh = 50;
  py = 140;
  glyphs.add(new Q(20, py, gw, gh, st));
  glyphs.add(new U(20 + gw, py, gw, gh, st + dt));
  glyphs.add(new A(20 + gw * 2, py, gw, gh, st + dt * 2));
  glyphs.add(new R(20 + gw * 3, py, gw, gh, st + dt * 3));
  glyphs.add(new K(20 + gw * 4, py, gw, gh, st + dt * 4));

  st = 8000;
  gw = 60;
  gh = 120;
  py = 220;
  glyphs.add(new Q(40, py, gw, gh, st));
  glyphs.add(new U(40 + gw, py, gw, gh, st+dt));
  glyphs.add(new A(40 + gw * 2, py, gw, gh, st + dt * 2));
  glyphs.add(new R(40 + gw * 3, py, gw, gh, st + dt * 3));
  glyphs.add(new K(40 + gw * 4, py, gw, gh, st + dt * 4));

  TIMESTAMP = millis();  // This will compensate for long setup times
}

void draw() {
  background(0);
  for (Glyph g : glyphs) g.update(millis() - TIMESTAMP);
  stroke(255);
  strokeWeight(10);
  for (Glyph g : glyphs) g.render();
}

/**
 * Base class for graphic elements used to build up a letter or character.
 * At the moment this only covers Arc(s) and Line(s)
 *
 */
abstract class Element {
  int startTime, duration;
  float t = 0; // parametric variable
  int foreCol = 0xFFFFFFFF; // line colour
  float thickness = 10;  // line weight

  abstract void render();

  /**
   * @param startTime sketch time to start animation (milliseconds)
   * @param duration time to perform animation (milliseconds)
   */
  Element(int startTime, int duration) {
    this.startTime = startTime;
    this.duration = duration;
  }

  /**
   * Calculate the how much of the element to render
   * @param ct current sketch time
   */
  void update(int ct) {
    if (t < 1) {
      if (ct > startTime) t = (ct - startTime) / (float) duration;
      t = constrain(t, 0, 1);
    }
  }
}

class Line extends Element {

  float x0, y0, x1, y1;
  int dir;

  /**
   * @param x0 the X position for one end of the line
   * @param y0 the Y position for one end of the line
   * @param x1 the X position for the other end of the line
   * @param y1 the Y position for the other end of the line
   * @param dir animation direction (1 = [x0,y0] >> [x1,y1] ; -1 = reverse)
   * @param startTime sketch time to start animation (milliseconds)
   * @param duration time to perform animation (milliseconds)
   */
  Line(float x0, float y0, float x1, float y1, int dir, int startTime, int duration) {
    super(startTime, duration);
    this.x0 = x0;
    this.y0 = y0;
    this.x1 = x1;
    this.y1 = y1;
    this.dir = dir < 0 ? -1 : 1;
  }

  /**
   * Render the arc segment
   */
  void render() {
    if (t > 0) {
      push();
      stroke(foreCol);
      strokeWeight(thickness);
      //float rangeX = x1 - x0, rangeY = y1 - y0;
      float dtx = (x1 - x0) * t;
      float dty = (y1 - y0) * t;
      if (dir > 0) {
        line(x0, y0, x0 + dtx, y0 + dty);
      } else {
        line(x1, y1, x1 - dtx, y1 - dty);
      }
      pop();
    }
  }
}

class Arc extends Element {

  float px, py, radX, radY;
  float angS, angE;
  int dir;
  float TAU = 2 * PI;

  /**
   * @param px arc centre
   * @param py arc centre
   * @param angS the angle the animation starts from (radians)
   * @param angE the angle the animation ends at (radians)
   * @param dir animation direction (-1 = anti-clockwise ; 1 = clockwise)
   * @param startTime sketch time to start animation (milliseconds)
   * @param duration time to perform animation (milliseconds)
   */
  Arc(float px, float py, float radX, float radY, float angS, float angE, int dir, int startTime, int duration) {
    super(startTime, duration);
    this.px = px;
    this.py = py;
    this.radX = radX;
    this.radY = radY;
    this.angS = normAngle(angS);
    this.angE = normAngle(angE);
    this.dir = dir < 0 ? -1 : 1;
  }

  /**
   * Converts an angle to its equivalent value in the range 0 - 2Pi
   */
  float normAngle(float a) {
    while (a < 0) a += TAU;
    while (a > TAU) a -= TAU;
    return a;
  }

  /**
   * Render the arc segment
   */
  void render() {
    if (t > 0) {
      push();
      stroke(foreCol);
      strokeWeight(thickness);
      noFill();
      translate(px, py);
      float as = angS, ae = angE, a_t = 0;
      if (dir > 0) {
        if (as > ae) ae += TAU;
        a_t = as + (ae - as) * t;
        arc(0, 0, 2*radX, 2*radY, as, a_t);
      } else {
        if (ae > as) as += TAU;
        a_t = as + (ae - as) * t;
        arc(0, 0, 2 * radX, 2 * radY, a_t, as);
      }
      pop();
    }
  }
}

abstract class Glyph {

  float gx, gy, gw, gh;
  float startTime;
  List<Element> parts = new ArrayList<Element>();

  /**
   * @param gx top-left corner X position for glyph
   * @param gy top-left corner Y position for glyph
   * @param gw glyph width to use
   * @param gh glyph height to use
   * @param startTime time to start drawing glyph
   */
  Glyph(float gx, float gy, float gw, float gh, int startTime) {
    this.gx = gx;
    this.gy = gy;
    this.gw = gw;
    this.gh = gh;
    this.startTime = startTime;
  }

  void update(int ct) {
    if (ct > startTime)
      for (Element part : parts) part.update(ct);
  }

  void render() {
    push();
    strokeCap(ROUND);
    translate(gx, gy);
    for (Element part : parts) part.render();
    pop();
  }
}

class Q extends Glyph {
  Q(float gx, float gy, float gw, float gh, int startTime) {
    super(gx, gy, gw, gh, startTime);
    float cx = 0.5 * gw, cy = 0.5 * gh, rx = 0.35 * gw, ry = 0.4 * gh;
    parts.add(new Arc(cx, cy, rx, ry, PApplet.radians(35), PApplet.radians(45), -1, startTime, 800));
    parts.add(new Line(cx + 0.15 * gw, cy + 0.2 * gh, cx + 0.35 * gw, cy + 0.4 * gh, 1, startTime + 800, 100));
  }
}

class U extends Glyph {
  U(float gx, float gy, float gw, float gh, int startTime) {
    super(gx, gy, gw, gh, startTime);
    float cx = 0.5 * gw, cy = 0.6 * gh, rx = 0.25 * gw, ry = 0.3 * gh;
    parts.add(new Arc(cx, cy, rx, ry, PApplet.radians(90), PApplet.radians(180), 1, startTime, 500));
    parts.add(new Arc(cx, cy, rx, ry, PApplet.radians(90), PApplet.radians(0), -1, startTime, 500));
    parts.add(new Line(cx - rx, 0.1 * gh, cx - rx, 0.6 * gh, -1, startTime + 500, 250));
    parts.add(new Line(cx + rx, 0.1 * gh, cx + rx, 0.6 * gh, -1, startTime + 500, 250));
  }
}

class A extends Glyph {
  A(float gx, float gy, float gw, float gh, int startTime) {
    super(gx, gy, gw, gh, startTime);
    float x0 = 0.2 * gw, x1 = 0.31 * gw, x2 = 0.5 * gw, x3 = 0.7 * gw, x4 = 0.8 * gw;
    float y0 = 0.9 * gh, y1 = 0.6 * gh, y2 = 0.1 * gh;
    parts.add(new Line(x0, y0, x2, y2, 1, startTime, 300));
    parts.add(new Line(x2, y2, x3, y1, 1, startTime+ 300, 200));
    parts.add(new Line(x4, y0, x3, y1, 1, startTime+ 450, 200));
    parts.add(new Line(x1, y1, x3, y1, -1, startTime+ 650, 200));
  }
}

class R extends Glyph {
  R(float gx, float gy, float gw, float gh, int startTime) {
    super(gx, gy, gw, gh, startTime);
    float x0 = 0.2 * gw, x1 = 0.5 * gw, x2 = 0.55 * gw, x3 = 0.75 * gw;
    float y0 = 0.1 * gh, y1 = 0.3 * gh, y2 = 0.5 * gh, y3 = 0.9 * gh;
    parts.add(new Line(x0, y0, x2, y0, 1, startTime, 300));
    parts.add(new Line(x0, y2, x2, y2, -1, startTime+ 800, 200));
    parts.add(new Arc(x2, y1, 0.2f*gw, 0.2f*gh, PApplet.radians(270), PApplet.radians(90), 1, startTime + 300, 500));
    parts.add(new Line(x0, y3, x0, y0, 1, startTime+ 700, 200));
    parts.add(new Line(x3, y3, x1, y2, 1, startTime+ 700, 100));
  }
}

class K extends Glyph {
  K(float gx, float gy, float gw, float gh, int startTime) {
    super(gx, gy, gw, gh, startTime);
    float x0 = 0.2 * gw, x1 = 0.4 * gw, x2 = 0.8 * gw;
    float y0 = 0.1 * gh, y1 = 0.43 * gh, y2 = 0.6 * gh, y3 = 0.9 * gh;
    parts.add(new Line(x0, y3, x0, y0, 1, startTime, 500));
    parts.add(new Line(x0, y2, x2, y0, -1, startTime + 300, 400));
    parts.add(new Line(x1, y1, x2, y3, -1, startTime + 700, 200));
  }
}
3 Likes

Wahoooow @quark you are amazing!!
Thank you sooooooooooo much!
I corrected my last sketch renaming everything as you suggested and getting rid of unuseful parts of code and then I made a test with after effect of very same idea and realised then that I was reusing again and again the same forms and animations and then I got back to P5 and I found a video by D. Schiffman about inheritance and then I got your message!
Thank you a million times for taking some precious time coding and explaining me inheritance feature in OO programming. It is very precious to me. I will need some time to dig into it and check the references for each new concept related to inheritance but it will definitely help me making a working and clean code. If you were not that far I would invite you for a beer to thank you for your great work. I am sure it will also help plenty other ‘young at code’ like me. :pray: :pray: :pray: I’ll post something as soon as I can…

Some extra clarifications for completeness’ sake: :nerd_face:

Actually “Java Mode” has always been auto-including ArrayList class:

This is the complete importing list for Java’s standard library:

import java.util.HashMap; 
import java.util.ArrayList; 
import java.io.File; 
import java.io.BufferedReader; 
import java.io.PrintWriter; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

Also it’s best to be explicit about each class/interface we import rather than just use *:
import java.util.*;import java.util.List;

For “.pde” files, Processing’s pre-processor will auto-add keyword public for all methods (but not fields, classes or interfaces though) which we haven’t explicitly added an access keyword for it; except for methods inside a non-top-level anonymous instantiation.

However, even if we explicitly use keywords private or protected, we still can’t prevent them to access members from each other, b/c all classes & interfaces inside a “.pde” file are nested inside a PApplet subclass.

3 Likes