にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

フォントサイズに合わせて回転するフォントを作る(1)

TrueType命令で遊ぶシリーズ第2弾。第1弾は次を参照:


TrueType命令を利用して、フォントサイズに応じた角度だけ回転するようなフォントを作った。

完成品

f:id:nixeneko:20170118014251p:plain
コンピュータの世界広がりすぎ。

ダウンロード


アウトラインとして、M+ Fontsのmplus-1p-regular.ttfを利用した。

TrueType Instructionのコード

最終的に実装したコードは次のようになった。なおFontforgeで読み込める記述となっている(というより、Fontforgeによる逆アセンブル結果と言った方がいいか)。

'fpgm': 関数定義
PUSHB_1
 0
FDEF
PUSHB_1
 2
ADD
SVTCA[x-axis]
GC[orig]
PUSHB_1
 128
DIV
PUSHB_1
 0
SWAP
WS
PUSHB_1
 1
RCVT
PUSHB_1
 1
SWAP
WS
ENDF
PUSHB_1
 1
FDEF
DUP
ROLL
DUP
ROLL
PUSHB_1
 3
RS
MUL
SWAP
PUSHB_1
 2
RS
MUL
SUB
PUSHW_1
 4096
DIV
ROLL
ROLL
PUSHB_1
 3
RS
MUL
SWAP
PUSHB_1
 2
RS
MUL
ADD
PUSHW_1
 4096
DIV
SWAP
ENDF
PUSHB_1
 2
FDEF
DUP
DUP
SVTCA[x-axis]
GC[cur]
SWAP
SVTCA[y-axis]
GC[cur]
PUSHB_1
 1
RS
SUB
SWAP
PUSHB_1
 0
RS
SUB
MPS
PUSHB_1
 1
LOOPCALL
PUSHB_1
 0
RS
ADD
SWAP
PUSHB_1
 1
RS
ADD
SVTCA[y-axis]
PUSHB_1
 3
CINDEX
SWAP
SCFS
SVTCA[x-axis]
SCFS
POP
ENDF
PUSHB_1
 3
FDEF
DUP
PUSHB_1
 0
CALL
DUP
DUP
PUSHB_1
 0
GTEQ
IF
PUSHB_1
 2
CINDEX
PUSHB_1
 2
CINDEX
PUSHB_1
 2
CALL
PUSHB_1
 1
SUB
PUSHB_1
 20
NEG
JMPR
EIF
POP
POP
ENDF
'prep': 定数のStorage Areaへの格納
PUSHB_2
 2
 64
PUSHW_1
 4096
MUL
PUSHW_1
 512
DIV
WS
PUSHW_3
 3
 8128
 4096
MUL
PUSHW_1
 8192
DIV
WS
'cvt ': Control Value Table
index val
0 0
1 379

cvtテーブルは事前にFunit(フォントエディタで図形を定義している時の座標)で数値を記述しておき、RCVT[ ]で読みだすときにはフォントサイズ(PPEM)に応じたピクセル数となって読みだされる。書きこむ際には書きこむ単位によってWCVTF[ ] (Funit), WCVTP[ ] (Pixel unit)の2命令が使える。
0番は0にしておいたが、実際には使わなかった。
1番は回転中心のy座標である。

グリフ固有のTrueType Instruction('glyf'内)
PUSHB_2
 11
 3
CALL

11の部分にはそのグリフの最大の制御点番号が入る。全部のグリフへの適用については前回の記事を参照。


以下、どのように実装をおこなったかを説明する。

方針

グリフを回転させることを考える。回転中心はグリフの真ん中あたりとし、その座標を \left( x_c, y_c \right)とする。 \left( x_c, y_c \right)を中心に回転させるには、次のような手順をとる。

  1. 回転中心が原点に重なるよう平行移動
  2. (原点を中心に)回転
  3. 原点に移動していた回転中心が元の座標となるよう平行移動

制御点 \boldsymbol{p}_i = \left( x_i, y_i \right)に対する座標を計算する。

1. 回転中心が原点に重なるよう平行移動

制御点の座標か回転中心の座標を引く。
 \left(\begin{matrix}x_i' \\ y_i'\end{matrix}\right) = \left(\begin{matrix}x_i \\ y_i \end{matrix}\right) - \left(\begin{matrix}x_c \\ y_c\end{matrix}\right) = \left(\begin{matrix}x_i - x_c \\ y_i - y_c\end{matrix}\right)

2. (原点を中心に)回転

これは回転行列を掛けて
 \left(\begin{matrix}x_i'' \\ y_i''\end{matrix}\right) =  \left(\begin{matrix}\cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{matrix}\right) \left(\begin{matrix}x_i' \\ y_i'\end{matrix}\right) = \left(\begin{matrix}x_i'\cos\theta - y_i' \sin\theta \\ x_i' \sin\theta + y_i' \cos\theta \end{matrix}\right)
みたいな感じではあるが、 \thetaに対する \sin\theta, \cos\thetaを計算するのが面倒だったので、固定した角度 \theta_0に対応する定数 \sin\theta_0, \cos\theta_0の値を用いて、
 \left(\begin{matrix}x_i'' \\ y_i''\end{matrix}\right) =  R R R R \cdots R \left(\begin{matrix}x_i' \\ y_i'\end{matrix}\right), \ \ \ R = \left(\begin{matrix}\cos\theta_0 & -\sin\theta_0 \\ \sin\theta_0 & \cos\theta_0 \end{matrix}\right)
のように回転行列をフォントサイズに応じた回数適用することで、サイズに応じた回転を計算する。かなり効率が悪いのでどうにかしたいところではあるが……。*1

3. 原点に移動していた回転中心が元の座標となるよう平行移動

これは1.の逆である。
 \left(\begin{matrix}x_i''' \\ y_i'''\end{matrix}\right) = \left(\begin{matrix}x_i'' \\ y_i''\end{matrix}\right) + \left(\begin{matrix}x_c \\ y_c\end{matrix}\right) = \left(\begin{matrix}x_i'' + x_c \\ y_i'' + y_c\end{matrix}\right)

回転後の座標 \left( x_i''', y_i''' \right)が計算できたので、制御点 \boldsymbol{p}_iをそこに移動する。これをグリフに含まれる全ての制御点について繰り返せば、グリフの回転が計算できるという訳である。

実装

さて、より実装に近い部分について。

Storage Areaへの座標の割り付け

index 割り付け
0  x_c (回転中心x座標)
1  y_c (回転中心y座標)
2  64\sin\theta_0
3  64\cos\theta_0

Storage Areaにはこんな感じで割り付けを行い、一時的な変数として利用したり、あるいは定数を格納して使ったりしている。

回転中心の座標の取得

回転中心の取得は、y座標についてはグリフに依らず事前に値を決め打ちし、'cvt 'テーブルに1番地に座標を(Funitで)書き込んでおく。

x座標についてはphantom pointによって行う。phantom pointとは、グリフの幅や高さを取得(したり、制御)するためにラスタライザが自動で付加する制御点であり、取り扱うグリフにn個の制御点 \boldsymbol{p}_0, \cdots, \boldsymbol{p}_{n-1}がある場合、 \boldsymbol{p}_n, \boldsymbol{p}_{n+1}, \boldsymbol{p}_{n+2}, \boldsymbol{p}_{n+3}がphantom pointとなる。次に図次するように、 \boldsymbol{p}_nがx位置左端、 \boldsymbol{p}_{n+1}が右端、 \boldsymbol{p}_{n+2}が上端、 \boldsymbol{p}_{n+3}が下端を示す。

f:id:nixeneko:20170118205015p:plain

なお、 \boldsymbol{p}_{n+2}, \boldsymbol{p}_{n+3}は古いバージョン(Microsoft rasterizer v.1.7より前)では使えないので、GETINFOでバージョンを取得し、MS rasterizerのバージョンが1.7以上であることを確認してから使わないといけないそうである。
Phantom Pointsについて、詳しくは、https://www.microsoft.com/typography/otspec/ttinst.htmのInstructing TrueType Glyphsを参照。

今回はグリフ幅の中心を求めるので、 \boldsymbol{p}_{n+1}=(x_{n+1}, y_{n+1})について \frac{x_{n+1}}{2}を計算するようにする。 \boldsymbol{p}_nは原点にくるので特に値を参照しなくても幅の中心が求まる。


回転中心の座標を取得し、Storage Areaに書き込むコードを考える。

回転中心のx座標

x座標についてはphantom point  \boldsymbol{p}_{n+1}の座標、すなわちグリフの幅を取得し、それを2で割ることによって中心を得る。

グリフAに対しては次のようになる。最初にpushしている11がグリフの最大の制御点番号n-1である。

PUSHB_1
 11
PUSHB_1
 2
ADD
SVTCA[x-axis]
GC[orig]
PUSHB_1
 128
DIV
PUSHB_1
 0
SWAP
WS
GC

座標の取得は命令GCを使う。リファレンスによると、制御点番号p(整数)をpopし、その番号に対応する座標cをpushする。pushされる座標は描画するフォントサイズ(PPEM)におけるピクセル単位で表され、F26Dot6つまり小数点以下6bitの固定小数点数で表現される。

Pops p: point number (uint32)
Pushes c: coordinate location (F26Dot6)

GCには2種類あり、

  • GC[0]: 現在の点pの座標
  • GC[1]: オリジナルのアウトラインにおける点pの座標

である。これらは点を移動した場合などに得られる値が変わってくる。

x座標, y座標それぞれについて座標を取得する場合はそれぞれSVTCA[1], SVTCA[0]を事前に実行しておく。

なお、ここでF26Dot6は小数点以下6bitの固定小数点数である。これは実質、小数点数に64をかけた整数であり、F26Dot6を整数として評価した場合に64で割ると実際の数値が得られる。
例えば1.0は整数として評価すると64であり、2.0は128となる。

DIV

割り算命令DIVは、リファレンスによると

Pops n2: divisor (F26Dot6)
n1: dividend (F26Dot6)
Pushes (n1 * 64)/n2: quotient (F26Dot6)

となっていて、F26Dot6に対して計算が定義されている。
スタックが

… | n1 | n2 | 

となっているとき、n1, n2を整数として考えると(n1 * 64)/n2が計算される。

… | n1/n2(F26Dot6) | 

n1, n2はF26Dot6で与えなければいけないため、2で割るためにはn2の部分が128になる。


従って、上のコードはGC[1]で取得したx座標を2.0(128)で割っている。

WS

最終的にWS命令でStorage Areaの0番に計算した値を保存する。

Pops v: storage area value (uint32)
l: storage area location (uint32)
before … | l | v |
after  … |

Storage Areaのインデックスiに値vを書きこむ。

SWAP

なお、命令が要求するようにスタックの順番を合わせるために、SWAP命令を使っている。SWAP命令はトップ2項目の順番を入れ替える。

before: … | e1 | e2 |
after:  … | e2 | e1 |

スタックの入れ替えには他にもROLL(上から3番目の項目を先頭に)やMINDEX(指定したnについて、上からn番目の項目を先頭に)が使える。

回転中心のy座標

次に回転中心のy座標の取得をする。

PUSHB_1
 1
RCVT
PUSHB_1
 1
SWAP
WS

事前に'cvt 'テーブルの1番地に書いておいた値をRCVT命令で読みだしている。

Pops location: CVT entry number (uint32)
Pushes value: CVT value (F26Dot6)

CVTに事前に値を設定しておく場合はFunitで書くが、RCVTで読みだした値は現在のPPEMに応じたピクセル数単位に変換されて読みだされる。
上のコードでは、読みだした値をWSでStorage Areaの1番地に保存している。

関数化: 関数0

これを関数化した。
回転中心の座標の取得を行い、回転中心のx座標をStorage Areaの0番地に、y座標を1番地に書きこむ関数が関数0である。引数としてグリフの最大の制御点番号(n-1)をとる。参考までにスタックの状態とコメントを書いた*2

PUSHB_1
 0
FDEF          /* ..| n-1 |             *//* ← initial stack */
PUSHB_1       /* ..| n-1 | 2 |         */ 
 2
ADD           /* ..| n+1 |             *//* n+1: phantom point n+1 に対応 */
SVTCA[x-axis]                            /* 座標の測定をx軸方向にする */
GC[orig]      /* ..| x_(n+1) |         *//* phantom point n+1の(X)座標取得 */
PUSHB_1       /* ..| x_(n+1) | 2.0 |   *//* Uint 128 == F24Dot6 2.0 */
 128
DIV           /* ..| x_(n+1) / 2 |     */
PUSHB_1       /* ..| x_(n+1) / 2 | 0 | */
 0
SWAP          /* ..| 0 | x_(n+1) / 2 | */
WS            /* ..|                   *//* StorageArea[0] = x_(n+1) / 2 */
PUSHB_1       /* ..| 1 |               */
 1
RCVT          /* ..| CVT[1] |          *//* read control value table */
PUSHB_1       /* ..| CVT[1] | 1 |      */     /* CVT[1]は回転中心y_cに対応 */
 1
SWAP          /* ..| 1 | CVT[1] |      */
WS            /* ..|                   *//* StorageArea[1] = CVT[1] */
ENDF

グリフの回転

次に回転を計算する部分を考える。
回転行列の計算は地道に計算していくしかないのだが、定数 \cos\theta_0, \sin\theta_0について、小数点以下に12ビット使うようにすることで精度を上げようとした(つまり、6ビット分下駄をはかせている)*3

定数のセット

'prep'において、Storage Areaの2番地に \sin\theta_0 \approx \frac{1}{8}, 3番地に \cos\theta_0 \approx \frac{127}{128}の近似値を書きこんでいる。 \theta_0 \approx 0.125 \approx 7.2\mbox{°}程度の近似である。
参考:


別にこんなことしなくても、定数をPUSHWしてWSすればいいって話でもある。

PUSHB_2  /* |2|1.0|              */
 2
 64
PUSHW_1  /* |2|1.0|64.0|         */
 4096
MUL      /* |2|1.0*64.0|         */ /* 64(6ビット分)下駄をはかせる */
PUSHW_1  /* |2|1.0*64.0|8.0|     */
 512
DIV      /* |2|0.125*64.0|       */
WS       /* |                    */ /* StorageArea[2] = 0.125*64.0 */
PUSHW_3  /* |3|127.0|64.0|       */
 3
 8128
 4096
MUL      /* |3|127.0*64.0|       */ /* 64(6ビット分)下駄をはかせる */
PUSHW_1  /* |3|127.0*64.0|128.0| */
 8192
DIV      /* |3|(127/128)*64.0|   */
WS       /* |                    */ /* StorageArea[3] = (127/128)*64.0 */

'prep'はフォントサイズ(PPEM)が変更される度に呼び出されるプログラムである。

制御点の座標を取得

最初に移動させたい制御点の番号kがスタクトップに乗っているとする。

DUP           /* ..|k|k| */
DUP           /* ..|k|k|k| */
SVTCA[x-axis]                   /* X座標に設定 */
GC[cur]       /* ..|k|k|x_k| */
SWAP          /* ..|k|x_k|k| */
SVTCA[y-axis]                   /* Y座標に設定 */
GC[cur]       /* ..|k|x_k|y_k| */

STVCAで座標軸を変更しながら、GC[0]で制御点kの現在の座標の値を取得している。

平行移動

Storage Areaから回転中心のx, y座標を読みだして、座標から引いている。

PUSHB_1       /* ..|k|x_k|y_k|1|           */
 1
RS            /* ..|k|x_k|y_k|y_c|         */ /* y_c = SA[1]: 回転中心Y座標 */
SUB           /* ..|k|x_k|y_k - y_c|       */
SWAP          /* ..|k|y_k - y_c|x_k|       */
PUSHB_1       /* ..|k|y_k - y_c|x_k|0|     */
 0
RS            /* ..|k|y_k - y_c|x_k|x_c|   */ /* x_c = SA[0]: 回転中心X座標 */
SUB           /* ..|k|y_k - y_c|x_k - x_c| */
回転

回転を行う関数が関数1である。

/* Function 1: Calculates rotated coordinates with pre-defined angle T */
/* initial stack ..| y | x |             x' = x*cosT - y*sinT          */
/* final stack   ..| y'| x'|      while  y' = x*sinT + y*cosT          */
PUSHB_1
 1
FDEF     /* ..|y|x|                                 */
DUP      /* ..|y|x|x|                               */
ROLL     /* ..|x|x|y|                               */
DUP      /* ..|x|x|y|y|                             */
ROLL     /* ..|x|y|y|x|                             */
PUSHB_1  /* ..|x|y|y|x|3|                           */
 3
RS       /* ..|x|y|y|x|64*cosT|                     */ /* SA[3] = 64*cosT */
MUL      /* ..|x|y|y|64*x*cosT|                     */
SWAP     /* ..|x|y|64*x*cosT|y|                     */
PUSHB_1  /* ..|x|y|64*x*cosT|y|2|                   */
 2
RS       /* ..|x|y|64*x*cosT|y|64*sinT|             */ /* SA[2] = 64*sinT */
MUL      /* ..|x|y|64*x*cosT|64*y*sinT|             */
SUB      /* ..|x|y|64*(x*cosT - y*sinT)|            */
PUSHW_1  /* ..|x|y|64*(x*cosT - y*sinT)|64.0|       */
 4096
DIV      /* ..|x|y|x*cosT - y*sinT|                 */
ROLL     /* ..|y|x*cosT - y*sinT|x|                 */
ROLL     /* ..|x*cosT - y*sinT|x|y|                 */
PUSHB_1  /* ..|x*cosT - y*sinT|x|y|3|               */
 3
RS       /* ..|x*cosT - y*sinT|x|y|64*cosT|         */ /* SA[3] = 64*cosT */
MUL      /* ..|x*cosT - y*sinT|x|64*y*cosT|         */
SWAP     /* ..|x*cosT - y*sinT|64*y*cosT|x|         */
PUSHB_1  /* ..|x*cosT - y*sinT|64*y*cosT|x|2|       */
 2
RS       /* ..|x*cosT - y*sinT|64*y*cosT|x|64*sinT| */ /* SA[2] = 64*sinT */
MUL      /* ..|x*cosT - y*sinT|64*y*cosT|64*x*sinT| */
ADD      /* ..|x*cosT - y*sinT|64*(x*sinT + y*cosT)| */
PUSHW_1  /* ..|x*cosT - y*sinT|64*(x*sinT + y*cosT)|64.0| */
 4096
DIV      /* ..|x*cosT - y*sinT|x*sinT + y*cosT|     */
SWAP     /* ..|x*sinT + y*cosT|x*cosT - y*sinT|     */ 
ENDF                        /* ↑関数呼び出し時とx, yの順番を合わせている */

これは、スタックにy座標、x座標が積まれた状態で呼び出す。だいたい7.2°の回転を計算し、計算結果も同様にスタックに積まれる。関数が終了するときのスタック状態が関数開始時と同型になっているので、複数回関数を実行すればそれだけ回転が計算されるというわけである。

回転の繰り返し

回転回数の取得はポイント数取得命令MPSによって得られた整数値をそのまま使っている。頭悪い。
LOOPCALL命令を利用して、(MPSで得られた値)回だけ関数1を実行している。

MPS      /* ..|k|y_k-y_c|x_k-x_c|PS|   */ /* measure point size. */
                                          /* PS…ポイントサイズ, Uint */
PUSHB_1  /* ..|k|y_k-y_c|x_k-x_c|PS|1| */
 1
LOOPCALL /* ..|k|y_k-y_c|x_k-x_c|      */ /* call function 1 PS times */
             /* y_k' = y_k-y_c, x_k' = x_k-x_c are changed to y_k'', x_k'' */
平行移動
PUSHB_1       /* ..|k|y_k''|x_k''|0|       */ 
 0
RS            /* ..|k|y_k''|x_k''|x_c|     */ /* x_c = SA[0]: 回転中心X座標 */
ADD           /* ..|k|y_k''|x_k''+x_c|     */
SWAP          /* ..|k|x_k''+x_c|y_k''|     */
PUSHB_1       /* ..|k|x_k''+x_c|y_k''|1|   */
 1
RS            /* ..|k|x_k''+x_c|y_k''|y_c| */ /* y_c = SA[1]: 回転中心Y座標 */
ADD           /* ..|k|x_k''+x_c|y_k''+y_c| */

Storage Areaから回転中心のx, y座標を読みだして、計算した座標に足し算している。

制御点の移動

STVCAで座標軸を切り替えながら、SCFSを利用して、指定した制御点kを計算した座標に移動させている。

SVTCA[y-axis]                              /* Y座標に設定 */
PUSHB_1  /* ..|k|x_k''+x_c|y_k''+y_c|3| */ 
 3
CINDEX   /* ..|k|x_k''+x_c|y_k''+y_c|k| */ /*stack先頭から3番目をトップにコピー*/
SWAP     /* ..|k|x_k''+x_c|k|y_k''+y_c| */
SCFS     /* ..|k|x_k''+x_c|             */ /* 制御点kのY座標を y_k''+y_c に */
SVTCA[x-axis]                              /* X座標に設定 */
SCFS     /* ..|                         */ /* 制御点kのX座標を x_k''+x_c に */
CINDEX

ここで、CINDEXは、整数kをpopし、スタックトップからk番目の要素をコピーしてスタックに積む命令である。

Pops k: stack element number (int32)
Pushes ek: kth stack element (StkElt)

k=1のとき、DUPと同じ動作となる。

関数化: 関数2

これらの、一つの制御点kを移動する命令群を関数2としてまとめた。引数としてグリフ最大の制御点番号n-1, 編集したい制御点番号kを与えて呼び出すと制御点kを回転した位置に移動する。n-1は考えたら使ってないのでもっとシンプルにできるはず。

/* Function 2: moves a control point k to the rotated coordinates         */
/* initial stack ..| n-1 | k |    n-1: 最大の制御点番号, k: 編集する制御点番号 */
/* final stack   ..|                                                      */
PUSHB_1
 2
FDEF     /* ..|n-1|k| */
DUP      /* ..|n-1|k|k| */
DUP      /* ..|n-1|k|k|k| */
SVTCA[x-axis]                                  /* X座標に設定 */
GC[cur]  /* ..|n-1|k|k|x_k| */
SWAP     /* ..|n-1|k|x_k|k| */
SVTCA[y-axis]                                  /* Y座標に設定 */
GC[cur]  /* ..|n-1|k|x_k|y_k| */
PUSHB_1  /* ..|n-1|k|x_k|y_k|1| */
 1
RS       /* ..|n-1|k|x_k|y_k|y_c|           */ /* y_c = SA[1]: 回転中心Y座標 */
SUB      /* ..|n-1|k|x_k|y_k-y_c|           */
SWAP     /* ..|n-1|k|y_k-y_c|x_k|           */
PUSHB_1  /* ..|n-1|k|y_k-y_c|x_k|0|         */
 0
RS       /* ..|n-1|k|y_k-y_c|x_k|x_c|       */ /* x_c = SA[0]: 回転中心X座標 */
SUB      /* ..|n-1|k|y_k-y_c|x_k-x_c|       */
MPS      /* ..|n-1|k|y_k-y_c|x_k-x_c|PS|    */ /* measure point size.*/
PUSHB_1  /* ..|n-1|k|y_k-y_c|x_k-x_c|PS|1|  */
 1
LOOPCALL /* ..|n-1|k|y_k-y_c|x_k-x_c|       */ /* call function 1 PS times */
             /* y_k' = y_k-y_c, x_k' = x_k-x_c are changed to y_k'', x_k'' */
PUSHB_1  /* ..|n-1|k|y_k''|x_k''|0|         */ 
 0
RS       /* ..|n-1|k|y_k''|x_k''|x_c|       */ /* x_c = SA[0]: 回転中心X座標 */
ADD      /* ..|n-1|k|y_k''|x_k''+x_c|       */
SWAP     /* ..|n-1|k|x_k''+x_c|y_k''|       */
PUSHB_1  /* ..|n-1|k|x_k''+x_c|y_k''|1|     */
 1
RS       /* ..|n-1|k|x_k''+x_c|y_k''|y_c|   */ /* y_c = SA[1]: 回転中心Y座標 */
ADD      /* ..|n-1|k|x_k''+x_c|y_k''+y_c|   */
SVTCA[y-axis]                                 /* Y座標に設定 */
PUSHB_1  /* ..|n-1|k|x_k''+x_c|y_k''+y_c|3| */ 
 3
CINDEX   /* ..|n-1|k|x_k''+x_c|y_k''+y_c|k| * stack先頭から3番目をトップにコピー*/
SWAP     /* ..|n-1|k|x_k''+x_c|k|y_k''+y_c| */
SCFS     /* ..|n-1|k|x_k''+x_c|            *//* 制御点kのY座標を y_k''+y_c に */
SVTCA[x-axis]                                 /* X座標に設定 */
SCFS     /* ..|n-1|                        *//* 制御点kのX座標を x_k''+x_c に */
POP      /* ..|                    */ /*そういえばこのn-1は関数内で全く使ってない*/
ENDF

全ての制御点に対し適用

イデア前回の記事で使ったのと同じ。関数に渡す値を用意する部分がやや複雑になっている。
最初にスタックにn-1が積まれてることとする。

                                 /* i = n-1 とおく */
DUP       /* ..|n-1|i|         */ 
                                 /* jump_to_here */
DUP       /* ..|n-1|i|i|       */
PUSHB_1   /* ..|n-1|i|i|0|     */
 0
GTEQ      /* ..|n-1|i|i<0|     */
IF        /* ..|n-1|i|         *//* if i<0 then */
  PUSHB_1 /* ..|n-1|i|2|       */
   2
  CINDEX  /* ..|n-1|i|n-1|     */
  PUSHB_1 /* ..|n-1|i|n-1|2|   */
   2
  CINDEX  /* ..|n-1|i|n-1|i|   */
  PUSHB_1 /* ..|n-1|i|n-1|i|2| */
   2
  CALL    /* ..|n-1|i|n-1|i|   *//* call function 2 */ 
  PUSHB_1 /* ..|n-1|i|1|       */ 
   1
  SUB     /* ..|n-1|i-1|       */
  PUSHB_1 /* ..|n-1|i-1|20|    */
   20
  NEG     /* ..|n-1|i-1|-20|   */
  JMPR    /* ..|n-1|i-1|       *//* goto jump_to_here with i=i-1*/
EIF                              /* end if */

疑似コードで書くとこんな感じ。

    int i = n-1;
jump_to_here:
    if (i>=0){
      func2(n-1, i);
      i--;
      goto jump_to_here;
    }
関数化: 関数3

最終的にグリフから実行する処理を関数3としてまとめた。回転中心座標の取得と、全ての制御点に対し回転を実行することをやっている。引数としてグリフの最大の制御点番号n-1を与えて実行する。

PUSHB_1
 3
FDEF      /* ..|n-1|           */
DUP       /* ..|n-1|n-1|       */
PUSHB_1   /* ..|n-1|n-1|0|     */
 0
CALL      /* ..|n-1|           */ /* call function 0 */
DUP       /* ..|n-1|i|         */ /* i = n-1 とおく */ 
                                  /* jump_to_here */
DUP       /* ..|n-1|i|i|       */
PUSHB_1   /* ..|n-1|i|i|0|     */
 0
GTEQ      /* ..|n-1|i|i<0|     */
IF        /* ..|n-1|i|         */ /* if i<0 then */
  PUSHB_1 /* ..|n-1|i|2|       */
   2
  CINDEX  /* ..|n-1|i|n-1|     */
  PUSHB_1 /* ..|n-1|i|n-1|2|   */
   2
  CINDEX  /* ..|n-1|i|n-1|i|   */
  PUSHB_1 /* ..|n-1|i|n-1|i|2| */
   2
  CALL    /* ..|n-1|i|n-1|i|   */ /* call function 2 */ 
  PUSHB_1 /* ..|n-1|i|1|       */ 
   1
  SUB     /* ..|n-1|i-1|       */
  PUSHB_1 /* ..|n-1|i-1|20|    */
   20
  NEG     /* ..|n-1|i-1|-20|   */
  JMPR    /* ..|n-1|i-1|       */ /* goto jump_to_here */
EIF                               /* end if */
POP       /* ..|n-1|           */
POP       /* ..|               */
ENDF

よく考えてみれば関数2でn-1の値を使ってないので、ループの時点でn-1の値を保存しなくてよくて、もっとシンプルにできたはず。

各グリフのプログラム

PUSHB_2
 11
 3
CALL

11の部分にはそのグリフの最大の制御点番号が入る。
関数3を、グリフの最大の制御点番号を引数にして呼び出している。

感想

計算量がバカにならないので、割と最新のパソコンでもフォントビューワで表示するとフォントサイズごとに上からポンポンポンと順番に出てくるのがわかる。回転部分をもっと賢くさせたい。
PPEM変わるたびに呼び出されるのが'prep'なので、そこでPPEMを測定しそれに応じた回転行列のパラメータを計算しておくのがいいのだろうと思う。まあそれは追い追い。

*1:今回はグリフの制御点に対して毎回計算しているが、合成された回転行列RRRR…Rの部分はここではフォントサイズ(つまりRの個数)のみに依存するため、prepテーブルで計算しておくと随分マシになるだろうと思う。

*2:厳しいことに、Fontforgeのデバッガではコメントが使えない。空行も危ない。

*3:どこまで意味があるんだろう……。