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

very interesting. -

Is that the same as polymorphism?

I tried to do the same thing [having an ArrayList of different classes] with interface, but that wasn’t a good experience, because I had a lot of same methods inside all classes using the interface. Is inheritance different and better?

Thank you!

Chrisir


interface example

(side question: why do I have still a switch() there, is it inevitable? switch() is evil)

// Demo for Drawing Program with ArrayList

// for mouse is on item / drag
final float hotSpotSize = 22; // radius

// Help text
final String helpText =
  "Use 0,1,2, click mouse, use Backspace and 'c'.\n"
  + "Use right mouse to drag previous items. \nWhen you hold an item (right mouse button) use cursor to change size and rotation, use DEL to remove.";

// shapes / items
ArrayList<Shape> listShapes = new ArrayList();

// the type we add
int currentShapeToGenerate = 0;

// current shape we are adding (from currentShapeToGenerate)
String[] texts = {
  "circle",
  "square",
  "triangle"
};

// mouse drag
boolean hold = false;
int i_hold=0;

// -----------------------------------------------------------------------------------------------
// Core funcs

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

void draw() {
  background(0);

  // display all
  displayShapes();

  // drag
  dragItem();

  // show when mouse is on Hot Spot
  showSmallRedCircleOnHotSpot();

  // text
  showText();
}

// -----------------------------------------------------------------------------------------------
// Input funcs

void mousePressed() {

  if (mouseButton == LEFT) { // ----------------------------------

    Shape newShape = null ;

    switch( currentShapeToGenerate ) {
    case 0:
      newShape = new shapeCircle (mouseX, mouseY);
      break;

    case 1:
      newShape = new shapeSquare (mouseX, mouseY);
      break;

    case 2:
      newShape = new shapeTriangle (mouseX, mouseY);
      break;
    }//switch

    // add
    if (newShape!=null) {
      listShapes.add(newShape);
    } //if
    //
  } else {
    // right mouse button --------------------------------
    // search the item to drag
    int i=0;
    for (Shape currentShape : listShapes) {
      if ( currentShape.underMouse() ) {
        // store the item to drag
        hold=true;
        i_hold=i;
        return;
      }
      i++;
    }
  }
}//func

void mouseReleased() {
  hold = false;
}

void keyPressed() {
  // get keys:
  // these first keys apply always ---
  if (key=='0'||key=='1'||key=='2') {
    currentShapeToGenerate=int(key+"");
  } else if (key==ESC) {
    key=0;//kill Esc
  } else if (key==BACKSPACE) {
    // delete last item
    if (listShapes.size()>0)
      listShapes.remove(listShapes.size()-1);
  } else if (key=='c') {
    listShapes.clear();
  } else if ( hold && keyCode == DELETE ) {  // these following keys only apply when we hold a shape ---
    listShapes.remove(i_hold);
    hold=false;
  } else if ( hold && keyCode == LEFT ) {
    listShapes.get(i_hold).incAngle();
  } else if ( hold && keyCode == RIGHT ) {
    listShapes.get(i_hold).decAngle();
  } else if ( hold && keyCode == UP ) {
    listShapes.get(i_hold).incSize();
  } else if ( hold && keyCode == DOWN ) {
    listShapes.get(i_hold).decSize();
  }
  //
}// func

// --------------------------------------------------------------------------------------------
// Other funcs

void displayShapes() {
  stroke(0);
  int i=0;
  for (Shape currentShape : listShapes) {
    // set color (hold / not hold)
    fill(255); // White = not hold
    if (hold && i==i_hold) {
      fill(255, 0, 0); // Red = hold
    }
    // display
    currentShape.display();
    // currentShape.move();
    i++;
  }//for
}

void dragItem() {
  if (hold) {
    listShapes.get(i_hold).setX(mouseX);
    listShapes.get(i_hold).setY(mouseY);
  }//if
}

void showSmallRedCircleOnHotSpot() {
  for (Shape currentShape : listShapes) {
    if (currentShape.underMouse()) {
      // display the empty red circle
      currentShape.showSmallRectCircle();
      return;
    }//if
  }//for
}

void showText() {
  fill(255);
  text (helpText
    + "\n"
    + "curent > "
    + texts[ currentShapeToGenerate ], 17, 17);
}

// =============================================================================================

// not called directly
interface Shape {
  void display();
  void move();

  float getX();
  float getY();

  void setX(float x_);
  void setY(float y_);

  void incAngle();
  void decAngle();

  void incSize();
  void decSize();

  boolean underMouse();

  void showSmallRectCircle();
} //interface

// =============================================================================================

class shapeCircle implements Shape {
  float x = 50;
  float y = 50;
  float angle;
  float size1=25;

  shapeCircle(float x_, float y_) {
    x=x_;
    y=y_;
  }

  void move() {
    x = x + random(-1, 1);
  }

  void display() {
    fill(255);
    ellipse(x, y, size1, size1);
  }

  Shape copy() {
    return new shapeCircle(x, y);
  }

  // ------------------------------------------------

  float getX() {
    return x;
  }
  float getY() {
    return y;
  }

  void setX(float x_) {
    x=x_;
  }
  void setY(float y_) {
    y=y_;
  }
  void incAngle() {
    angle+=0.01;
  }
  void decAngle() {
    angle-=0.01;
  }

  void incSize() {
    size1+=1;
  }
  void decSize() {
    size1-=1;
  }
  boolean underMouse() {
    return
      dist(mouseX, mouseY, x, y)<hotSpotSize;
  }
  void showSmallRectCircle() {
    // display the empty red circle
    noFill();
    stroke(255, 0, 0);
    circle(x, y, hotSpotSize);
  }
} //class

// =============================================================================================

class shapeSquare implements Shape {
  float x = 50;
  float y = 50;
  float angle=0;
  float size1 = 25;

  shapeSquare(float x_, float y_) {
    x=x_;
    y=y_;
  }

  void move() {
    y = y + random(-1, 1);
  }

  void display() {
    rectMode(CENTER);
    push();
    translate(x, y);
    rotate(angle);
    rect(0, 0, size1, size1);
    pop();
    //reset
    rectMode(CORNER); // default mode
  }

  // ------------------------------------------------

  float getX() {
    return x;
  }
  float getY() {
    return y;
  }
  void setX(float x_) {
    x=x_;
  }
  void setY(float y_) {
    y=y_;
  }
  void incAngle() {
    angle+=0.01;
  }
  void decAngle() {
    angle-=0.01;
  }
  void incSize() {
    size1+=1;
  }
  void decSize() {
    size1-=1;
  }
  boolean underMouse() {
    return
      dist(mouseX, mouseY, x, y)<hotSpotSize;
  }
  void showSmallRectCircle() {
    // display the empty red circle
    noFill();
    stroke(255, 0, 0);
    circle(x, y, hotSpotSize);
  }
} //class

// =============================================================================================

class shapeTriangle implements Shape {
  float x = 50;
  float y = 50;
  float angle;
  float size1=25;

  shapeTriangle(float x_, float y_) {
    x=x_;
    y=y_;
  }

  void move() {
    y = y + random(-1, 1);
  }

  void display() {
    push();
    translate(x, y);
    rotate(angle);
    triangle(0, 0,
      0-size1, 0,
      0, 0+size1);
    pop();
  }

  // ------------------------------------------------

  float getX() {
    return x;
  }
  float getY() {
    return y;
  }
  void setX(float x_) {
    x=x_;
  }
  void setY(float y_) {
    y=y_;
  }
  void incAngle() {
    angle+=0.01;
  }
  void decAngle() {
    angle-=0.01;
  }
  void incSize() {
    size1+=1;
  }
  void decSize() {
    size1-=1;
  }
  boolean underMouse() {
    return
      dist(mouseX, mouseY, x, y)<hotSpotSize;
  }
  void showSmallRectCircle() {
    // display the empty red circle
    noFill();
    stroke(255, 0, 0);
    circle(x, y, hotSpotSize);
  }
} //class
//
2 Likes

Thank you very much @GoToLoop for these clarifications regarding Java and the way Processing use it! Useful to know it…

3 Likes

I assume that you mean this switch statement, if true then yes you still need it. No matter what method you use to create the shape you still need some way of deciding (using conditional statements) which shape type to create. I don’t understand why you think switch is evil :thinking:

Not exactly. In programming there are 2 main types of polymorphism

1. Parametric polymorphism (Overloading)
You maybe more familiar with the terms method overloading or function overloading where you have two or more functions with the same name within the same scope. e.g.

int square(int n){
  return n * n;
}
float square(float n){
  return n * n;
}

Here we have two functions with the same name. If we had a variable called x then which method would be executed with the statement a = square(x);? The answer depends on the data type of the variable x, if an integer (int) then the first one if a floating point number (float) then the second one.
Since Java is a statically typed language then the compiler knows the data type of x so can statically link to the correct method.
You might be interested to know that Javascript does not support parametric polymorphism because it is a dynamically typed language.

2. Subtype polymorphism (Runtime)
This is specific to OO and inheritance and is the type used in my sketch. This code shows only the parts needed to discuss subtype polymorphism.

abstract class Element {
  abstract void render();
}
class Arc extends Element {
  void render() { /* code to draw an ARC */ }
}
class Line extends Element {
  void render() { /* code to draw a LINE */ }
}

Now in the Glyph class we have

abstract class Glyph {
  List<Element> parts = new ArrayList<Element>();

  void render() {
    for (Element part : parts) part.render();
  }
}

The render method in Glyph iterates over the list and calls the render method on each object in the list called parts.

Two important points -

  • In Java all instantiated objects are aware of the class they were created from.
  • Any typed Java collection (ArrayList, Set, Map …) can hold objects of that type or any object that inherits from that type.

So when for(Element part : parts) part.render(); is executed then Java will look at the class that created part and call the render method in that class. In other words the method executed is determined at runtime!

This is perfectly acceptable the choice between an interface or a class is beyond the scope of this discussion but I will say

  • inheritance should only be used if the relationship between classes can be expressed as “class B is a kind of class A
  • interfaces are useful where classes have the same functionality (methods with the same name and parameters) but are not related otherwise
3 Likes

Dear @Chrisir,
Thank you very much to enrich the issue with your code example.
As I dig a bit the subject, there’s a good intro to inheritance and polymorphism in Dan Schiffman’s online book : The Nature of Code https://natureofcode.com/book/chapter-4-particle-systems/
As for the rest as a noob I can’t really help…
Maybe this :

@Chrisir :

// not called directly
interface Shape {
void display();
void move();

float getX();
float getY();

void setX(float x_);
void setY(float y_);

void incAngle();
void decAngle();

void incSize();
void decSize();

boolean underMouse();

void showSmallRectCircle();
} //interface

Maybe it could be an abstract class that you use as a reference “not called directly” and that stores the methods and variables you will reuse in the children classes?!
But maybe I just miss the point of your interface… I Hope at least the inheritance and polymporphism’s link can help!

1 Like

I heard that from Uncle Bob on YouTube when he talks about clean code. A great series of approximately 6 videos.

I understand it’s inevitable here.

Thanks for your great input as always.

(Link: https://youtu.be/7EmboKQH8lM?si=XR9FK1ytNuYyn5aJ&t=4585)

1 Like

I got that wrong LOL, read on.

I rarely have the need to use switch statements except where the number of cases is fixed and for a fixed number of cases this is OK and avoids if-else spaghetti.

On the other hand, if enhancing or increasing the functionality of the sketch requires the programmer to change the number of cases in the switch statement I can see this as undesirable or even EVIL :smile:

In my example I have displayed five letters Q U A R K no problem but what if I provide the string “QUARK” the software would have to take each character in turn and decide which glyph to create. I can already see a huge switch statement getting bigger as the number of glyphs increase OUCH!!! .

There is a simple solution but I am away for a few days so I will work on it when I get back and post it here.

2 Likes

In my last post I suggested that to display an animated string would require the software to examine each character in turn and decide which glyph to create.

This could be done with a switch statement but even if we just used uppercase and lowercase letters it would require 52 case statements. To add further glyphs means editing this huge switch statement - hence a really evil switch statement LOL.

In this new sketch there is a new method to animate a string without a switch statement in sight. :smile:

To achieve this I have created a new class called GlyphFactory which uses Java reflection to store the constructor to use to create a specific glyph.

This sketch has the exactly the same output as my previous one but adding new glyphs can be done easily and with minimum code changes.

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

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

public void setup() {
  size(560, 360);
  // Initialise glyph factory and add glyphs
  initGlyphs();
  // £ animations with different aspect ratio
  animateString("QUARK", 20, 20, 100, 100, 1000, 600);
  animateString("QUARK", 20, 140, 100, 50, 4000, 600);
  animateString("QUARK", 40, 220, 60, 120, 8000, 600);

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


/**
 * Initialize glyph factory and add glyphs.
 * Glyphs can be added
 */
void initGlyphs() {
  gf = new GlyphFactory(this);
  gf.add("Q", Q.class);
  gf.add("U", U.class);
  gf.add("A", A.class);
  gf.add("R", R.class);
  gf.add("K", K.class);
}

/**
 * Animate the string ignoring characters which have no glyph
 * @param s string to animate
 * @param px top-left corner X position for string
 * @param py top-left corner X position for string
 * @param gw glyph width
 * @param gh glyph height
 * @param startTime start time for animation
 * @param deltaTime start time delay between characters
 */
void animateString(String s, float px, float py, float gw, float gh, int startTime, int deltaTime) {
  for (int i = 0; i < s.length(); i++) {
    String id = s.substring(i, i+1);
    if (gf.contains(id)) {
      Glyph glyph = gf.get(id, px + i * gw, py, gw, gh, startTime + i * deltaTime);
      glyphs.add(glyph);
    }
  }
}

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
   */
  public 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 {

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

class U extends Glyph {
  public U(float gx, float gy, float gw, float gh, int startTime) {
    super(gx, gy, gw, gh, startTime);
    float cx = 0.5f * gw, cy = 0.6f * gh, rx = 0.25f * gw, ry = 0.3f * 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.1f * gh, cx - rx, 0.6f * gh, -1, startTime + 500, 250));
    parts.add(new Line(cx + rx, 0.1f * gh, cx + rx, 0.6f * gh, -1, startTime + 500, 250));
  }
}

class A extends Glyph {
  public A(float gx, float gy, float gw, float gh, int startTime) {
    super(gx, gy, gw, gh, startTime);
    float x0 = 0.2f * gw, x1 = 0.31f * gw, x2 = 0.5f * gw, x3 = 0.7f * gw, x4 = 0.8f * gw;
    float y0 = 0.9f * gh, y1 = 0.6f * gh, y2 = 0.1f * 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 {
  public R(float gx, float gy, float gw, float gh, int startTime) {
    super(gx, gy, gw, gh, startTime);
    float x0 = 0.2f * gw, x1 = 0.5f * gw, x2 = 0.55f * gw, x3 = 0.75f * gw;
    float y0 = 0.1f * gh, y1 = 0.3f * gh, y2 = 0.5f * gh, y3 = 0.9f * 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 {
  public K(float gx, float gy, float gw, float gh, int startTime) {
    super(gx, gy, gw, gh, startTime);
    float x0 = 0.2f * gw, x1 = 0.4f * gw, x2 = 0.8f * gw;
    float y0 = 0.1f * gh, y1 = 0.43f * gh, y2 = 0.6f * gh, y3 = 0.9f * 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));
  }
}

/**
 * Factory for storing and retrieving of glyphs
 *
 */
class GlyphFactory {
  PApplet pa;
  HashMap<String, Constructor> map;

  GlyphFactory(PApplet applet) {
    pa = applet;
    map = new HashMap<String, Constructor>();
  }

  void add(String glyphID, Class glyphClass) {
    if (!map.containsKey(glyphID)) {
      Constructor ctor = null;
      Constructor[] ctors = glyphClass.getConstructors();
      if (ctors.length > 0)
        for (int i = 0; i < ctors.length; i++)
          if (ctors[i].getParameterCount() == 6) {
            ctor = ctors[i];
            break;
          }
      map.put(glyphID, ctor);
    }
  }

  boolean contains(String glyphID) {
    return map.containsKey(glyphID);
  }

  Glyph get(String glyphID, float gx, float gy, float gw, float gh, int startTime ) {
    Glyph glyph = null;
    if (map.containsKey(glyphID)) {
      Constructor ctor = map.get(glyphID);
      try {
        glyph = (Glyph) ctor.newInstance(pa, gx, gy, gw, gh, startTime);
      }
      catch (Exception e) {
        System.err.println("Unable to create glyph for '" + glyphID + "'");
      }
    }
    return glyph;
  }
}
1 Like

Impressive.

But I think we still have to have to automate
initGlyphs().

Let’s say we want to implement all letters of
the alphabet and use a for loop (for(char letter='a'; letter<'z';....) { ... ). Then we have
to establish the class name automatically.

How can we do this?

gf.add("R", R.class);

must become

for(char letter=‘a’; letter<=‘z’; letter++){// not sure if this works 
    gf.add(letter+"", classFromLetter(letter));
}

Chrisir

3 Likes

What a great piece of code! :clap: Thank you so much @quark for finding so sophisticated solutions.
Very impressive. I didn’t think I would open such a passionate topic, but I learn a lot thanks to you and @Chrisir. Now I still digest the inheritance and polymporphism concepts, so I’ll certainly ask you many questions regarding Hashmap soon, but before I’ll read the reference and try to manipulate myself to get the idea. I love very much the way you help each other and the other members of this forum, that’s what I love in coding so far even as a baby coder like me :sweat_smile:

3 Likes

Personally I don’t agree because it assumes that there is a sequential relationship (i.e. without selection statements) between the glyph character and the name of its associated class e.g. Q —> Q.class, ZZ.class. Consider creating a glyph for ? we cannot use ?.class because ? is not a valid class name.

With the existing code I could

  • create a class called QuestMark
  • and in initGlyphs include the line add("?", QuestMark.class);

Although the initGlyphs function will become very long we can simply give it a tab all to itself. Also, since each line adds a single glyph it is very easy to see what glyphs are available.

There are always several ways to do the same thing. If we could get the class from its name then we could use
gf.add("?", "QuestMark"); // String type, String type
instead of
gf.add("?", QuestMark.class); // String type, Class type

Fortunately this is easy to do, simply add the following code to the GlyphFactory class.
IMPORTANT - do not replace the existing add method with this one, you will need both.

 void add(String glyphID, String className) {
    if (className.indexOf("$") < 0)
      className = pa.getClass().getName() +"$" + className;
    try {
      add(glyphID, Class.forName(className));
    }
    catch (ClassNotFoundException e) {
      System.err.println("Unable to find glyph class '" + className + "'");
    }
  }

Notice that the class now has 2 add methods but with different parameters types. (In other words uses method overloading see my previous post). The other thing to note is that this method simply gets the class from the class name and then calls the other add method to register the glyph. Neat even if I say so myself :innocent:

With this method we can change the initGlyph method to

void initGlyphs() {
  gf = new GlyphFactory(this);
  gf.add("Q", "Q");
  gf.add("U", "U");
  gf.add("A", "A");
  gf.add("R", "R");
  gf.add("K", "K");
}

This more flexible because we could now create a text file contain the glyph IDs and class names, then at runtime read the file and register the glyphs.

4 Likes

Thanks a ton for the explanation!

:wink:

Thanks a megaton for the explanation and the code Master @quark :pray:

1 Like

Dear @quark ,
I’ve been away for a week. Now it’s time for (many) questions.
The first one : why do you have to use the PApplet in the constructor of the GlyphFactory class?!

In the add method below the “Class glyphClass” stands for the Glyph’s Class?!
and you select its constructors with this method getConstructors() ?
Then you check if the class holds 6 parameters like the Glyph’s Class?!
Then fill the hashMap with map.put(glyphID, ctor) ?!!

I believe I understand the general purpose of this class but some parts are really obscures to me. I am really sorry to bother you with this but I don’t want to use a code I hardly undersatnd! Thanks a million in advance. Best wishes, L

1 Like

I think you can work with this version before I asked
my questions which made the code more complicated

Sorry for that!

1 Like

Preamble before I answer your questions. :grinning:

In the GlyphFactory class there are only three methods we need to concern ourselves with

  1. void add(String glyphID, String className)
  2. void add(String glyphID, Class glyphClass)
  3. Glyph get(String glyphID, float gx, float gy, float gw, float gh, int startTime )

Method (1) does the following

  • finds the correct class object for the given class name
  • pass the glyph ID and the class object to method (2)

Method (2) does the following

  • from the class object find the constructor we will use to instantiate the Glyph object
  • store the [key (glyphID), object (constructor)] pair in the hashmap attribute

Method (3) does the following

  • given the glyphID retrieve the constructor
  • instantiate a glyph object using the retrieved constructor

The first task is to find the correct class object for the given class name but what is the class name. Copy the code below into the Processing IDE and run it (no need to save it first)

void setup(){
  println(Quark.class.getName());
  println(HashMap.class.getName());
  println(String.class.getName());
}

class Quark{}

The output will look like this

sketch_231108a$Quark
java.util.HashMap
java.lang.String

Do you see the difference? Quark is called a nested or inner class because it is defined inside another class called sketch_231108a.

NOTE: sketch_231108a this is the name of the sketch allocated by Processing and will be replaced by the name you use when saving the sketch. When Processing runs your sketch it wraps all your code into a class named after the sketch name and creates an instance of that class.

In this example sketch_231108a is the enclosing or outer class because it encloses the inner class Quark

So the name of an inner class is <outer_class_name>$<inner_class_name> and I need this fully qualified name to get the class object and then the constructor.

So when we create the GlyphFactory object we pass the instance of the sketch outer class (this)
gf = new GlyphFactory(this);

so I can calculate the fully qualified name for any inner class.

This is not the end of it in method (3) this line creates an instance of the required glyph class.
glyph = (Glyph) ctor.newInstance(pa, gx, gy, gw, gh, startTime);

The parameter pa is the outer class instance passed in the constructor and is required to instantiate an inner class object. So that answers the first question. :wink:

Yes. Every Java library class, every user defined class has a static attribute called class see it being used in the code earlier in this post.

Yes. Classes can have more than one constructor so we must find the one I want to use.

In the GlyphFactory I had a choice of object types I could store in the map e.g.

HashMap<String, String> map;  // <glyph ID, glyph class name>
HashMap<String, Class> map; // <glyph ID, glyph class>
HashMap<String, Constructor> map; // <glyph ID, glyph constructor>

The last option was the obvious choice because to instantiate and object I will always need the constructor. The first two options would require me to find the constructor for every glyph I want to instantiate so DODO would be 4 glyphs. It is better to find the right constructor once and use it multiple times.

I understand where you are coming from because if I see the source code I want to understand how it works.

On the other hand we (and I include myself) all do that anyway. We use HashMap, ArrayList and the other Java classes all the time without even looking at the source code or knowing how it works. Thousands of people have used my G4P library without ever looking at the source code.

That is the beauty of OO (object orientation) we are interested in what a class can do, but do not need to know how it does it.

Hope this helps - feel free to ask more questions. :+1:

2 Likes

OK the method (2) from above
void add(String glyphID, Class glyphClass)

can be replaced with

  void add(String glyphID, Class glyphClass) {
    if (!map.containsKey(glyphID)) {
      Constructor ctor = null;
      try {
        ctor = glyphClass.getConstructor(new Class[] {pa.getClass(), float.class, float.class, float.class, float.class, int.class});
        map.put(glyphID, ctor);
      }
      catch(Exception e) {
        System.err.println("Can't find constructor for " + glyphClass.getName() );
      }
    }
  }

It selects the constructor based on its parameter types and is better that looping through all available constructors. To be honest I did try this first and couldn’t get it to work because I was making a mistake in the parameter list but it works now :+1:

1 Like

Dear @Chrisir on the contrary you did great, it does help me to see how @quark push the code further on
and it will certainly help others too :wink:

1 Like

Just mentioning that it’s also a subclass of Processing’s PApplet class:
http://Processing.GitHub.io/processing-javadocs/core/processing/core/PApplet.html

And every class & interface inside a “.pde” file is nested inside that PApplet subclass which has the same name as the 1st main tab.

2 Likes

Dear @quark ,
Thank you so much for your very precious help and for taking some time to detail so precisely your answer in a very didactic way.
Of course you don’t need to understand evrything about the classes you use, but in order to change the code you need to know quite a few things at least!! :joy: I didn’t have the time to work on it until now I hope tomorrow I will be able to do so and maybe new questions will arise.
See you soon and thanks a lot again! :pray:

1 Like