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(); } }
結果は次のようになる。
再構築する
しかしこれだけだと回りくどい方法で画面に文字を描いたにすぎない。 低レベルに降りることによって、間違った方法で文字描画を再構築することが可能になる。
一筆書きにする
先のコードでは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(); }
結果は次のようになる。
曲線と直線を分離する
少し物足りないので今度は直線を表す移動と曲線を表す移動を別々に一筆書きしてみる。
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(); }
結果は次のようになる。おもしろい効果が得られるようになってきた。
手書き風にする
一筆書きなのでもう少し手書き風にすることを考える。
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(); }
曲線のパスだけ塗りつぶす
やっぱり手書きはやめて曲線の部分だけ塗りつぶすことにする。
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(); } }
この記事のコードの全体はGistに置いた。