Drawing a font "in code" - AxiDraw + Processing

I am working with the AxiDraw penplotter and want to interactively draw text using Processing as the interface.

For fonts I am running into trouble, since the way I communicative with AxiDraw through Processing only lets me specify coordinates for it to move to, which makes writing text a bit tricky…

Any ideas for workarounds?
I am looking into getting the outlines of the fonts like they do in the Generative Design Book http://www.generative-gestaltung.de/1/P_3_2_1_01 and the Geomerative library https://github.com/rikrd/geomerative, but it gives me twice the number of key points needed, even for “thin” fonts that look like they are drawn with a single line. This make the AxiDraw draw the font twice, and I cannot find any real system in the way the points are organised.

Another idea might be to design a font from coordinates myself, but that would be my last resort… It would be very time consuming and also not give me the aesthetics I am looking for.

To sum up, I guess my question is something like this:

How do I go from a font to a set of specific coordinates that I can connect in order to draw the font in code?

1 Like

Here is my (failed) attempt using geomerative and a screenshot showing the double points.

import geomerative.*;
RFont font;
String myText = "help";

void setup() {
  size(640, 480);  
  RG.init(this);
  font = new RFont("Eng_VandLine.ttf", 250, RFont.LEFT);
  RCommand.setSegmentLength(20);
  RCommand.setSegmentator(RCommand.UNIFORMLENGTH);
}


void draw() {
  background(255);
  RGroup grp;
  grp = font.toGroup(myText);
  grp = grp.toPolygonGroup();
  RPoint[] pnts = grp.getPoints();

  translate(20, 320);
  drawUsingLines(pnts);
}

void drawUsingLines(RPoint[] pnts) {
  fill(255, 0, 0);
  beginShape();
  for (int i = 0; i < pnts.length-1; i++ ) {    
    vertex(pnts[i].x, pnts[i].y);   
    text(i, pnts[i].x, pnts[i].y);
  }
  noFill();
  endShape();
}

1 Like

Hi @AndreasRef,

I can’t run your code at the moment but it seems to me that no matter how thin the font you load is, Geomerative will always try to find the contour points around it.

It means that even if your font is 1 pixel wide you will still end-up with points corresponding to the edges of it (outer/inner, left/right, up/down). In your case edges are probably colliding hence all the points appearing at the same location (roughly).

Maybe you could try to cull the duplicate points, i.e removing points that are below a chosen threshold distance from another point.

int N = 1000;

void setup() {
  size(640, 480);  
  background(255);
  strokeWeight(3);
  
  ArrayList<PVector> points = new ArrayList<PVector>();
  
  for (int i = 0; i < N; i++) {
    points.add(new PVector(random(width), random(height)));
  }
  

  ArrayList<PVector> culledPoints = cull(points, 10); 
  
  for (PVector pt : culledPoints) {
    point(pt.x, pt.y);
  }
   
}

public ArrayList<PVector> cull(ArrayList<PVector>arr, float t) {

  // arr = array of points to cull 
  // t = threshold distance
  
  ArrayList<PVector> plist = new ArrayList<PVector>();
  plist.add(arr.get(0));
  
  for (PVector pt1 : arr) {
    boolean add = true;
    for (PVector pt2 : plist) {
      if (pt1.dist(pt2) < t) {
        add = false;
        break;
      }
    }
    if (add == true) {
        plist.add(pt1);
    }
  }
    
    return plist;
    
  }

Also is there a reason why you are grouping your text and getting the corresponding points in draw() rather than in setup() ?

2 Likes

You want the outline (so a big letter?) or one line (as in your image?)

See also Straight Skeleton - or how to draw a center line in a polygon or shape?

1 Like

Oh, that is a really neat idea actually!
I’m gonna try that tomorrow. Thanks so much for the suggestion and the code.

And no, there is no reason for doing this in draw().

One line :slight_smile:

The suggestion from @solub got my a lot further :slight_smile:

However, I am still facing some trouble: With letters like “i” and “j” it is important that no line is drawn between the dot over the letter (the “tittle”: https://en.wikipedia.org/wiki/Tittle). On the other hand, some letters will have (joints/points of connections/stitches) where the points are close, but should still be drawn and not culled by duplicate points - and example is the lower right part of the letter “h”.

What would be the logic for correctly setting the values (in my system these are currently fontSize, cullDist, segmentLength & minPointDistWhenDrawing), so my system will work for different fonts of different sizes? Or am I doing it an a fundamentally wrong way?

Below is my code. You cannot run it unless you download the font I am using or replace it with another .ttf font. You can also download the whole project as a zip here.

import geomerative.*;
RFont font;
String myText = "ifhj";

int fontSize = 200;
int cullDist = 4;
int segmentLength = 10;  
int minPointDistWhenDrawing = 15;

void setup() {
  size(840, 480);  
  background(255);
  RG.init(this);
  font = new RFont("Eng_VandLine.ttf", fontSize, RFont.LEFT);
  RCommand.setSegmentLength(segmentLength);
  RCommand.setSegmentator(RCommand.UNIFORMLENGTH);
  noFill();

  textSize(8);
  RGroup grp;
  grp = font.toGroup(myText);
  grp = grp.toPolygonGroup();
  RPoint[] pnts = grp.getPoints();

  ArrayList<PVector> plist = convertRPointsToArrayList(pnts);
  ArrayList<PVector> culledPoints = cull(plist, cullDist);

  translate(50, 220);
  drawUsingLinesMinSegment(culledPoints);
}


void drawUsingLinesMinSegment(ArrayList<PVector>arr) {
  beginShape();
  for (int i = 0; i < arr.size()-1; i++ ) {
    if (dist(arr.get(i).x, arr.get(i).y, arr.get(i+1).x, arr.get(i+1).y) < minPointDistWhenDrawing) { //Is this the right way to do it?
      vertex(arr.get(i).x, arr.get(i).y);
      push();
      fill(255, 0, 0);
      text(i, arr.get(i).x, arr.get(i).y);
      pop();
    } else {
      endShape();

      push();
      fill(255, 0, 0);
      text(i, arr.get(i).x, arr.get(i).y);
      pop();
      beginShape();
    }
  }
  noFill();
  endShape();
}

public ArrayList<PVector> cull(ArrayList<PVector>arr, float t) {
  // arr = array of points to cull 
  // t = threshold distance

  ArrayList<PVector> plist = new ArrayList<PVector>();
  plist.add(arr.get(0));

  for (PVector pt1 : arr) {
    boolean add = true;
    for (PVector pt2 : plist) {
      if (pt1.dist(pt2) < t) {
        add = false;
        break;
      }
    }
    if (add == true) {
      plist.add(pt1);
    }
  }
  return plist;
}

public ArrayList<PVector> convertRPointsToArrayList(RPoint[] pnts) {
  ArrayList<PVector> plist = new ArrayList<PVector>();
  for (int i = 0; i < pnts.length-1; i++ ) {
    plist.add(new PVector(pnts[i].x, pnts[i].y));
  }
  return plist;
}

1 Like

I wouldn’t say that but in my opinion it is not the cleanest way to achieve that thin look you are looking for.

I personnaly would opt for the Straight Skeleton approach suggested above but it is a long and tricky process that can be daunting, especially when dealing with fonts. Actually the author of the Geomerative library himself had planned to implement this algorithm (most likely to retrieve the medial axis of a letter) but never went ahead, probably for these very reasons.

Nevertheless, the best approach would probably consist in:

  • computing the Straight Skeleton of a polygonized letter
  • pruning the branches to retrieve the medial axis
  • smoothening the axis or approximating it with piecewise cubic Bezier curves

Your workaround, however, has the advantage of being quite simple but requires to address all the edge cases manually. That is, making a connection between 2 consecutive points:

  • unless their distance is equal to the distance from a dot (the “tittle”) to its corresponding glyph (letter “i” and “j”)

  • if their distance is below a maximum threshold

    • unless they must be connected despite their distance exceeding that threshold.
      (letter “d”)

These parameters will vary depending on the font size and the culling threshold.

Example: with a font size of 340 and culling threshold of 6:

  • the distances between the dot (tittle) and the glyph for letter “i” and “j” are 43 and 63
  • the distance between the 2 distant points on the back of the letter “d” is 70
  • the maximum connection distance between 2 consecutive points is 58
add_library('geomerative')

fsize = 340

def setup():
    size(1900, 480, P2D)
    background('#FFFFFF')
    fill(255, 30, 30)
    strokeWeight(2)
    smooth(8)
    
    RG.init(this)
    font = RFont("Eng_VandLine.ttf", 300, RFont.LEFT)
    txt = "abcdefghij"

    grp = font.toGroup(txt)
    grp.translate(40, 340)
    pts = cull(grp.getPoints(), 6)
    

    for i, (p1, p2) in enumerate(zip(pts, pts[1:])):
        
        #rounded distance bewteen 2 consecutive points
        d = round(p1.dist(p2)) 
        
        #display index
        text(i, p1.x - 30, p1.y)
               
        # do not draw line if d = distance between the dot (tittle) and the glyph for letter "i" and "j" 
        if d == 43 or d == 63: 
            continue
        
        # draw line if d = distance between the 2 distant points on the back of the letter "d" 
        elif d == 70: 
            line(p1.x, p1.y, p2.x, p2.y)
            continue
            
        # draw line if d < maximum connection distance between 2 consecutive points
        elif d < 58: 
            line(p1.x, p1.y, p2.x, p2.y)




def cull(arr, t):
        
    cpts = [arr[0]]
    for pt in arr:
        for cpt in cpts:
            if pt.dist(cpt) < t: 
                break
        else:
            cpts.append(pt)
            
    return cpts



2 Likes

For anybody interested in this topic, I just discovered a really nice cross-platform library (with a example for Processing):
https://skeleton-tracing.netlify.app/

2 Likes

I just accidentally rediscovered the skeleton-tracing library that you previously linked the demo for above, along with its Processing example:

It seems to work well with still images, webcam, fonts et cetera.

Despite the name, it isn’t specialized for detecting human poses – it will create a fantastical “skeleton” from a human silhouette. But it is extremely versatile.

2 Likes