Processingでフォントのアウトラインを読み取って再構築する

この記事ではProcessingでフォントのアウトライン(輪郭)をデータとして取得し、 そのデータを元にいくつかの仕方で再構築する方法を試していく。

フォントのアウトラインを読み取る

フォントのアウトラインを取得するためのProcessing独自の関数といったものは存在しないようだ。 JavaのAWTではFontからGlyphVectorを取得し、getOutlineメソッドでアウトラインを取得することができるのでこれを使用する。

import java.awt.Font;
import java.awt.font.FontRenderContext;
import java.awt.image.BufferedImage;
import java.awt.geom.PathIterator;

PathIterator createOutline(String name, int size, String text, float x, float y) {
  FontRenderContext frc =
    new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
      .createGraphics()
      .getFontRenderContext();

  Font font = new Font(name, Font.PLAIN, size);

  PathIterator iter = font.createGlyphVector(frc, text)
    .getOutline(x, y)
    .getPathIterator(null);

  return iter;
}

このメソッドでは最終的にPathIteratorというオブジェクトが得られる。 これはタートルグラフィックのように点から点をつなぐ情報を表現している。 これを使ってそのまま輪郭を描くには次のようにすればいい。

void drawNormally() {
  PathIterator iter = createOutline("Segoe UI", 50, "wonderful cool something", 10, 40);
  float coords[] = new float[6];
  while (!iter.isDone()) {
    int type = iter.currentSegment(coords);
    switch (type) {
      case PathIterator.SEG_MOVETO: // beginning of new path
        beginShape();
        vertex(coords[0], coords[1]);
        break;
      case PathIterator.SEG_LINETO:
        vertex(coords[0], coords[1]);
        break;
      case PathIterator.SEG_CLOSE: // back to last MOVETO point.
        endShape();
        break;
      case PathIterator.SEG_QUADTO:
        quadraticVertex(coords[0], coords[1], coords[2], coords[3]);
        break;
      case PathIterator.SEG_CUBICTO:
        bezierVertex(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
        break;
      default:
        throw new RuntimeException("should not reach here");
    }
    iter.next();
  }
}

結果は次のようになる。

f:id:tokb:20140802183830p:plain

再構築する

しかしこれだけだと回りくどい方法で画面に文字を描いたにすぎない。 低レベルに降りることによって、間違った方法で文字描画を再構築することが可能になる。

一筆書きにする

先のコードではSEG_MOVETOは線を引かずに位置を移動すること、SEG_CLOSEは図形の描画を一段落させることに対応していた。 この2つの役割を無視すると一筆書きのように線をつなぐようになる。

void draw2() {
  PathIterator iter = createOutline("Segoe UI", 50, "wonderful cool something", 10, 40);
  float coords[] = new float[6];
  beginShape();
  while (!iter.isDone()) {
    int type = iter.currentSegment(coords);
    switch (type) {
      case PathIterator.SEG_MOVETO:
        vertex(coords[0], coords[1]);
        break;
      case PathIterator.SEG_LINETO:
        vertex(coords[0], coords[1]);
        break;
      case PathIterator.SEG_CLOSE:
        break;
      case PathIterator.SEG_QUADTO:
        quadraticVertex(coords[0], coords[1], coords[2], coords[3]);
        break;
      case PathIterator.SEG_CUBICTO:
        bezierVertex(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
        break;
      default:
        throw new RuntimeException("should not reach here");
    }
    iter.next();
  }
  endShape();
}

結果は次のようになる。

f:id:tokb:20140802184025p:plain

曲線と直線を分離する

少し物足りないので今度は直線を表す移動と曲線を表す移動を別々に一筆書きしてみる。

void draw3() {
  stroke(#ff4a5d);
  
  PathIterator iter = createOutline("Segoe UI", 50, "wonderful cool something", 10, 40);
  float coords[] = new float[6];
  beginShape();
  boolean init = true;
  while (!iter.isDone()) {
    int type = iter.currentSegment(coords);
    switch (type) {
      case PathIterator.SEG_MOVETO:
        if (init) vertex(coords[0], coords[1]);
        init = false;
        break;
      case PathIterator.SEG_LINETO:
      case PathIterator.SEG_CLOSE:
        break;
      case PathIterator.SEG_QUADTO:
        quadraticVertex(coords[0], coords[1], coords[2], coords[3]);
        break;
      case PathIterator.SEG_CUBICTO:
        bezierVertex(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
        break;
      default:
        throw new RuntimeException("should not reach here");
    }
    iter.next();
  }
  endShape();
}

void draw4() {
  stroke(#6144b0);
  
  PathIterator iter = createOutline("Segoe UI", 50, "wonderful cool something", 10, 40);
  float coords[] = new float[6];
  beginShape();
  boolean init = true;
  while (!iter.isDone()) {
    int type = iter.currentSegment(coords);
    switch (type) {
      case PathIterator.SEG_MOVETO:
        if (init) vertex(coords[0], coords[1]);
        init = false;
        break;
      case PathIterator.SEG_LINETO:
        vertex(coords[0], coords[1]);
        break;
      case PathIterator.SEG_CLOSE:
      case PathIterator.SEG_QUADTO:
      case PathIterator.SEG_CUBICTO:
        break;
      default:
        throw new RuntimeException("should not reach here");
    }
    iter.next();
  }
  endShape();
}

結果は次のようになる。おもしろい効果が得られるようになってきた。

f:id:tokb:20140802184055p:plain

f:id:tokb:20140802184118p:plain

f:id:tokb:20140802184134p:plain

手書き風にする

一筆書きなのでもう少し手書き風にすることを考える。

Processing用のライブラリとしてHandyというものがあるのでこれを利用する。 使い方はHandyRendererというオブジェクトを作成して、Processingの組み込みのbeginShapeやvertexの代わりにそのオブジェクトの同名メソッドを呼べばよい。

HandyRenderer h;

void setup() {
  size(640, 60);
  smooth();
  h = new HandyRenderer(this);
  noLoop();
}

void drawHandy() {
  noFill();
  stroke(#6144b0);
  
  PathIterator iter = createOutline("Segoe UI", 50, "wonderful cool something", 10, 40);
  float coords[] = new float[6];
  h.beginShape();
  boolean init = true;
  while (!iter.isDone()) {
    int type = iter.currentSegment(coords);
    switch (type) {
      case PathIterator.SEG_MOVETO:
        if (init) h.vertex(coords[0], coords[1]);
        init = false;
        break;
      case PathIterator.SEG_LINETO:
        h.vertex(coords[0], coords[1]);
        break;
      case PathIterator.SEG_CLOSE:
      case PathIterator.SEG_QUADTO:
      case PathIterator.SEG_CUBICTO:
        break;
      default:
        throw new RuntimeException("should not reach here");
    }
    iter.next();
  }
  h.endShape();
}

f:id:tokb:20140802184323p:plain

曲線のパスだけ塗りつぶす

やっぱり手書きはやめて曲線の部分だけ塗りつぶすことにする。

void drawCurveClosed() {
  fill(#ff4a5d);
  stroke(#ff4a5d);
  PathIterator iter = createOutline("Segoe UI", 50, "wonderful cool something", 10, 40);
  float coords[] = new float[6];
  while (!iter.isDone()) {
    int type = iter.currentSegment(coords);
    switch (type) {
      case PathIterator.SEG_MOVETO: // beginning of new path
        beginShape();
        vertex(coords[0], coords[1]);
        break;
      case PathIterator.SEG_LINETO:
        //vertex(coords[0], coords[1]);
        break;
      case PathIterator.SEG_CLOSE: // back to last MOVETO point.
        endShape();
        break;
      case PathIterator.SEG_QUADTO:
        quadraticVertex(coords[0], coords[1], coords[2], coords[3]);
        break;
      case PathIterator.SEG_CUBICTO:
        bezierVertex(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
        break;
      default:
        throw new RuntimeException("should not reach here");
    }
    iter.next();
  }
}

f:id:tokb:20140802184341p:plain

この記事のコードの全体はGistに置いた。