sidenote

The video is also available on Vimeo.

This animation is strongly connected to LaTeX. How come? – you could ask. I’ll come to the point but first I have to confess that this nice piece of video/animation/whatever is “only” a byproduct of a search for a LaTeX related answer.

I’m a LaTeX enthusiast, and I’m trying to be active on TeX.SX, and found a question about how to draw individual glyphs with randomized paths which I could not stop thinking about. I even wanted to learn Metapost, but when I saw what was laying before me I was intimidated. So I thought I should try something easier first, namely Processing, which was very useful for some small projects in the past. I found the Geomerative library written by Ricard Marxer, and when I was looking into the examples which came with the library I found an interesting one, which used the vertices of font outlines. Then came the idea that it would be nice to try to polygonize individual glyphs with different accuracy, draw Bézier curves along/through the vertices, and stack these images on top each other. Just to see how it looks. In the end it turned out to be pretty awesome, I think.

Font outline animation

I’m sharing the source with some notes what and how it does what it does, so others may come up with nice things too. But if you don’t want the “magic” to be debunked, this is where you should stop reading this post.

Note that the following source code comes with no warranty at all, and is under the CC BY-NC-SA 2.5 license.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/*
Font outline animation
Author: István Szántai (szantaii)
License: CC BY-NC-SA 2.5
*/

import geomerative.*;
import processing.pdf.*;

int frameCount;
float backgroundColor;
float strokeColor;
float strokeOpacity;
float fadeOutOpacity;
float fadeOutOpacityStep;
RShape shape;
RPoint[] points;
int initialPolygonizerLength;
int currentPolygonizerLength;


void setup()
{
  frameCount = 0;
  backgroundColor = 38.25;
  strokeColor = 229.5;
  strokeOpacity = 25.5;
  fadeOutOpacityStep = 10.625;
  fadeOutOpacity = fadeOutOpacityStep;
 
  size(1280, 720, PDF, "frames.pdf");
 
  RG.init(this);

  initialPolygonizerLength = 179;
  currentPolygonizerLength = initialPolygonizerLength;
 
  print("Drawing...");
 
  smooth();
}

void draw()
{
  PGraphicsPDF pdf = (PGraphicsPDF) g;
 
  if (frameCount < 45)
  {
    background(backgroundColor);
    pdf.nextPage();
  }
  else if (frameCount < 399)
  {
    background(backgroundColor);
    noFill();
    strokeCap(ROUND);
    strokeJoin(ROUND);
    stroke(strokeColor, strokeOpacity);
 
    drawBezierVertices("A", width / 2 - 180, 4.95 * height / 6);
    drawBezierVertices("a", width / 2 + 230, 4.95 * height / 6);
   
    if (currentPolygonizerLength > 0)
    {
      --currentPolygonizerLength;
    }
   
    if (frameCount >= 375)
    {
        noStroke();
        fill(backgroundColor, fadeOutOpacity);
        rect(0, 0, 1280, 720);
        fadeOutOpacity += fadeOutOpacityStep;
    }
   
    pdf.nextPage();
  }
  else if (frameCount < 429)
  {
    background(backgroundColor);
    if (frameCount != 428)
    {
      pdf.nextPage();
    }
  }
  else
  {
    println(" done.");
    exit();
  }
 
  frameCount++;
}

void drawBezierVertices(String text, float horizontalPos, float verticalPos)
{
  shape = RG.getText(text, "Palatino-Roman.ttf", 650, CENTER);
  pushMatrix();
  translate(horizontalPos, verticalPos);
 
  for (int i = initialPolygonizerLength; i >= currentPolygonizerLength; i--)
  {
    RG.setPolygonizer(RG.UNIFORMLENGTH);
    RG.setPolygonizerLength(i);
    points = shape.getPoints();
   
    if(points != null && points.length > 3)
    {
      beginShape();
      for(int j = 0; j < points.length - 3; j++)
      {
        if (j == 0)
        {
          vertex(points[j].x, points[j].y);
        }
        else
        {
          bezierVertex(points[j].x, points[j].y,
            points[j + 1].x, points[j + 1].y,
            points[j + 2].x, points[j + 2].y);
        }
      }
      endShape();
    }
  }
  popMatrix();
}

The most important stuff is in the definition of the drawBezierVertices function. This is how it works. It gets a piece of text, which will be “written” with a defined font (note that the chosen font should be in the project’s data folder) making a shape, but it won’t be drawn to the screen. Instead the shape will be polygonized and through the calculated vertices a bezier curve will be drawn. This is iterated a couple times (180 times in this specific example) while vertices are getting closer to each other, so at the end a the true outline of the font would be drawn (more or less).

By reading the source you can also see that every frame of the animation is rendered to pages of a PDF document. Here is why. When rendering to PDF Processing will create a vectorized output. So in the end you can get the frames from the PDF in any resolution you want without losing quality. All is left to rasterize every page of the rendered PDF with the desired resolution into individual raster images (PNG files if you ask me), and make a video from the created image sequence.

Some practical advice. I’ve used Gimp to rasterize the pages of the PDF output (with FullHD, 1920×1080 resolution), and saved them as separate files using the Export Layers plugin. After that I made the image sequence into a video from the command-line using FFmpeg.

ffmpeg -r 30 -i %03d.png -c:v libx264 -preset veryslow -qp 0 -g 1 -bf 2 font_animation.mp4
Megosztás, like stb.