フォントサイズに合わせて回転するフォントを作る(1)
TrueType命令で遊ぶシリーズ第2弾。第1弾は次を参照:
TrueType命令を利用して、フォントサイズに応じた角度だけ回転するようなフォントを作った。
完成品
コンピュータの世界広がりすぎ。
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の部分にはそのグリフの最大の制御点番号が入る。全部のグリフへの適用については前回の記事を参照。
以下、どのように実装をおこなったかを説明する。
方針
グリフを回転させることを考える。回転中心はグリフの真ん中あたりとし、その座標をとする。を中心に回転させるには、次のような手順をとる。
- 回転中心が原点に重なるよう平行移動
- (原点を中心に)回転
- 原点に移動していた回転中心が元の座標となるよう平行移動
制御点に対する座標を計算する。
1. 回転中心が原点に重なるよう平行移動
制御点の座標か回転中心の座標を引く。
2. (原点を中心に)回転
これは回転行列を掛けて
みたいな感じではあるが、に対するを計算するのが面倒だったので、固定した角度に対応する定数の値を用いて、
のように回転行列をフォントサイズに応じた回数適用することで、サイズに応じた回転を計算する。かなり効率が悪いのでどうにかしたいところではあるが……。*1
3. 原点に移動していた回転中心が元の座標となるよう平行移動
これは1.の逆である。
回転後の座標が計算できたので、制御点をそこに移動する。これをグリフに含まれる全ての制御点について繰り返せば、グリフの回転が計算できるという訳である。
実装
さて、より実装に近い部分について。
Storage Areaへの座標の割り付け
index | 割り付け |
---|---|
0 | (回転中心x座標) |
1 | (回転中心y座標) |
2 | |
3 |
Storage Areaにはこんな感じで割り付けを行い、一時的な変数として利用したり、あるいは定数を格納して使ったりしている。
回転中心の座標の取得
回転中心の取得は、y座標についてはグリフに依らず事前に値を決め打ちし、'cvt 'テーブルに1番地に座標を(Funitで)書き込んでおく。
x座標についてはphantom pointによって行う。phantom pointとは、グリフの幅や高さを取得(したり、制御)するためにラスタライザが自動で付加する制御点であり、取り扱うグリフにn個の制御点がある場合、がphantom pointとなる。次に図次するように、がx位置左端、が右端、が上端、が下端を示す。
なお、は古いバージョン(Microsoft rasterizer v.1.7より前)では使えないので、GETINFOでバージョンを取得し、MS rasterizerのバージョンが1.7以上であることを確認してから使わないといけないそうである。
Phantom Pointsについて、詳しくは、https://www.microsoft.com/typography/otspec/ttinst.htmのInstructing TrueType Glyphsを参照。
今回はグリフ幅の中心を求めるので、についてを計算するようにする。は原点にくるので特に値を参照しなくても幅の中心が求まる。
回転中心の座標を取得し、Storage Areaに書き込むコードを考える。
回転中心のx座標
x座標についてはphantom point の座標、すなわちグリフの幅を取得し、それを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種類あり、
である。これらは点を移動した場合などに得られる値が変わってくる。
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
グリフの回転
次に回転を計算する部分を考える。
回転行列の計算は地道に計算していくしかないのだが、定数について、小数点以下に12ビット使うようにすることで精度を上げようとした(つまり、6ビット分下駄をはかせている)*3。
定数のセット
'prep'において、Storage Areaの2番地に, 3番地にの近似値を書きこんでいる。程度の近似である。
参考:
その昔、高校の先輩が8ビットマシンで背景が回転するゲームを作るにあたり、sinθ=1/8、cosθ=127/128として、Z80の機械語でいとも簡単に回転行列の計算をしてしまっていたのを見て、この人には追い着けないなあ、と感じたものです。どうやったらこんなの思い付けるんだろう。
— KenKenMkIISR (@KenKenMkIISR) 2016年9月10日
別にこんなことしなくても、定数を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を測定しそれに応じた回転行列のパラメータを計算しておくのがいいのだろうと思う。まあそれは追い追い。
UbuntuにX11転送を有効にしてsshで接続したらgeditなどがSegmentation Faultした
sshでX11 forwardingを有効にしてUbuntuにログインし、ログイン先のnautilusやgeditなどを起動しようとすると、Segmentation Faultと表示されて動作しなかった。
一方で、xtermなどは正常に実行でき、ちゃんとそのウィンドウが表示された。
ログイン先のOSはUbuntu 16.04 64bitで、Nvidiaのグラボを利用している。オンボードグラフィックがないマザーボードを使っている。
解決策
Open GUI apps on a Ubuntu 16.04 machine via SSH from an Ubuntu 14.04 machine - Ask Ubuntu
これを見て解決した。
原因はNvidiaのドライバかなんかにあるらしい。 libGLX_indirect.so.0 がうまく見つけられてないとのこと。
次の様にリンクを張ると動作する様になった。
cd /usr/lib/x86_64-linux-gnu sudo ln -s /usr/lib/nvidia-361/libGLX_indirect.so.0
nvidia-361の部分は環境(ドライバ)に合わせてといった感じになると思う。
GPOSのCursive Attachment Positioningについて
序説
OpenTypeフォントの高度組版機能においてグリフの位置調整を行うのがGPOSである。GPOSは主にペアカーニングやダイアクリティカルマークを適切な位置に表示するために利用される。
GPOSの中には更に、筆記体の接続を実現するためのCursive Attachment Positioningというものがある。筆記体の接続と言う名前ではあるが、文字が水平に並ぶ英語などの筆記体のためのものではなく、ペルシア語やウルドゥー語で用いられるアラビア文字のナスタアリーク体などのために追加されたものだろう。ナスアリーク体では文字の塊ごとに文字が右上から右下に流れ、グリフの上下位置が一定しないために、この機能がないと適切な表示ができない(例などは参考サイト参照)。
特にウルドゥー語などではこれがないと標準的な組版もできない様な機能ではあるが、日本語や欧文では不要である。
それでも、グリフ間の位置調整ができれば便利だという気持ちもあるので、使えないか調べてみる。
Fontforgeによる設定方法
さて、Fontforgeでどのように利用するかを見ながら実際の動作を見ていく。
M+フォントのmplus-1p-regular.ttfを利用することにする。
まず、Fontforgeでmplus-1p-regular.ttfを開く。最初にASCII外のグリフを削除し、GSUB, GPOSについても既存のものを削除しておいた。
GPOS lookupとanchor classの作成
メインウィンドウのメニューから、エレメント→フォント情報を開く。
Lookupsタブ→GPOSタブを開き、Add Lookupをクリックする。
次に開くLookupウィンドウで、
- 種類: 「Cursive Position」を選択。
- 機能: 「curs」を選択。
- 用字系と言語は適切に。デフォルトで動くはず。
- Lookup Name: 適当に設定。デフォルトでも良い。
など設定し、OKをクリック。
すると今設定したlookupが追加されるので、追加したlookupを選択した状態で「Add Subtable」をクリック。
サブテーブルの名前を聞かれるダイアログが開くので適当に名前をつけて(デフォルトでよい)、OKをクリック。
するとAnchor Class Nameを設定するダイアログが開くのでクラスを追加していく。<New Anchor Class>をクリックするとスロットが追加されるので適当に名前を入力して(ここでは“cursive”)、OKを押す。
「フォント情報」ウィンドウをOKを押して閉じる。
これによってアンカークラスが追加されたので、アンカー点が設定できるようになる。
Anchor pointの設定
適当なグリフ(ここではa)をダブルクリックしアウトラインウィンドウを開く。
メニューから、点→アンカーを追加(あるいはアウトライン領域で右クリック→アンカーを追加)を選ぶ。
するとアンカー点の情報が表示されるので、先ほど追加したcursiveクラスを選び、「筆記体の始点」を選択、OKを押す。座標はこのダイアログで設定してもいいし、青い+字点をドラッグして移動することもできる。このダイアログは、青い+字点を右クリック→情報を得る、などから再度表示させることができる。
同様に筆記体の終点を追加する。前とほぼ一緒で、違いは「筆記体の終点」を選択するところだけである。
追加したのが上の画像である。始点を(0, 0)に、終点を(548, 0)に置いてみた。y座標が一緒でx座標の差分がグリフの幅に一致しているため、この状態だとcursを有効にしてもなにも変化しない、はずである。
試しにメトリックウィンドウを開いて確認してみる。
有効無効によって変化がない。同じアンカークラスに属するものについて、次のグリフの始点が前のグリフの終点に重なるように移動する、というのがアンカーポイントによる位置調整なので、当然の動作であるように思う。
さて、今度はcursive終点の位置を少し上側にずらしてみる。
今度は次にくるaはcursive終点の位置に移動するはずなので、やや上に上がって表示されるはずだ。メトリックウィンドウで試してみる。
期待通りである。
同様に別のグリフにも対してもアンカー点を追加していってみる。小文字には始点・終点を設定し、大文字には終点だけ設定してみる。コンマピリオドにも始点を設定してみる。
最終的に、TrueTypeアウトラインのフォントとして出力する。
ソフトウェア側の対応
さて、フォントが対応していてもフォントを扱う側のソフトウェアが対応していないと正しく表示できないのがフォントの世界である。この機能が必須であるウルドゥー語などを除いては機能を無効にしているなどといった可能性もある。
それでは、ソフトウェア側の対応を見ていく。OpenTypeタグ'curs'が有効にできればいいのであるが。なお以下、すべてWindows 10で動かした結果である。
Adobe InDesign CS6
Adobe InDesign CS6はダメっぽい。そもそも'curs'に対応する設定項目が見当たらない。他のAdobeソフトも対応は期待できなさそうな気がする。
Microsoft Word 2013
設定項目が見当たらないのでダメっぽい。
各種ブラウザ
CSS3でfont-feature-settingsプロパティが追加され、それによってOpenType featureが個別に有効を設定できる。期待できそう。
CSSだと、次のような設定をすることになる。
font-feature-settings: "curs";
これを実際にはフォント指定と組み合わせる。
Webフォントで表示させてみたのが次である。
各ブラウザの対応を見ていく。
Firefox (50.1.0)
Google Chrome (55.0.2883.87 m)
Microsoft Edge (25.10586.672.0)
EdgeやIEは
font-feature-settings: curs;
ではだめで、
font-feature-settings: 'curs';
のように(シングルでもダブルでもいいけど)クォーテーションでくくらないとちゃんと動かないみたい。
さて、最近のブラウザ正しく動いてるようである。しかし、滅茶苦茶不安になるな、これ……。
LaTeX
LaTeXは、fontspecパッケージによって、OpenType featuresを個別に設定することができる。
\setmainfont[Path=./,Extension=.ttf,RawFeature=+curs]{Acurstest} \newfontfamily\curstest[Path=./,Extension=.ttf,RawFeature=+curs]{Acurstest}
みたいにフォント設定時にRawFeature=+cursオプションを設定するか、あるいは、オプションを適用したいテキストの直前で
\addfontfeature{RawFeature=+curs}
と書いて指定する。
XeLaTeXとLuaLaTeXのみ検討する。なお以下はどちらもCygwinで導入したTeX Live 2016のものによる出力である。
XeLaTeX
U+2019を削除してしまったのでXeLaTeXではアポストロフィ'が豆腐になっている。
LuaLaTeX
どちらでも正しく動いているようであるが、aaaaa...の行が上と重ならないなど、行間の扱いがブラウザの場合と異なっているのがわかる。また、aaaaa...の行を見るに、XeLaTeXとLuaLaTeXでも行間の扱いが異なっているようである。どうなってるの…。
なお、XeLaTeXでは\begin{document}より前に\addfontfeatures{RawFeature=+curs}を書いても有効になったのだが、LuaLaTeXでは有効にならなかった。微妙に動作が異なるらしい。
まとめ
ソフトウェアの対応が悪い。使用できる環境が限られるので、万能ではない。先に使いたい環境で使えるかを確かめてからやらないといけなさそう。
『ギリシャ文字・キリル文字・ラテン文字』
サークル“ヒュアリニオス”として頒布した『ギリシャ文字・キリル文字・ラテン文字』(初出: コミックマーケット90)を公開します。文字の対応を見ながら、ギリシャ文字からキリル文字が作られた過程をラテン文字を絡めて説明している感じの漫画です。
サポートページ
ダウンロード
PDFファイルのダウンロードはこちらから: greek_latin_cyrillic.pdf (3.91MB)
本文
LaTeX (TikZ)でキーボード配列表を作成
XeLaTeXでTikZを使ってキーボード配列表を作っていた。101キーボード向け。
多言語用のキーボード配列を作るときに便利…なはず。とても泥臭いので誰か改良してください。
ちなみにXeTeXを使ったのはモンゴル文字への対応がLuaLaTeXは良くないためで、試してないがfontspecが使えればLuaLaTeXなどでも普通に使えるかもしれない。
ソース
\documentclass{standalone} \usepackage{fontspec} \usepackage{lmodern} \setmainfont{Times New Roman} \usepackage{readarray} \usepackage{calc} \usepackage{xparse} \usepackage{listings} \usepackage{tikz} \usetikzlibrary{positioning,shapes,fit} \tikzstyle{abstract}=[rectangle, draw=black, rounded corners, fill=white,text centered, text=black, text width=8mm] \tikzstyle{gkey}=[rectangle, draw=black, rounded corners, fill=gray!20,text centered, text=black, text width=8mm] \newcommand{\mykey}[2]{% \begin{tikzpicture} \node (Item) [abstract, rectangle split, rectangle split, rectangle split parts=2]% {#1 \nodepart{second}#2};% \end{tikzpicture}} \tikzset{ pics/vhsplit/.style n args = {4}{ code = { \node[anchor=west] (A) at (-3mm,-0.5mm) {#1}; \node[anchor=west] (B) at (-3mm,-6.5mm) {#2}; \node[anchor=east] (C) at (9mm,-0.5mm) {#3}; \node[anchor=east] (D) at (9mm,-6.5mm) {#4}; \draw[rounded corners=1mm] (-3mm,-9.5mm) rectangle (9mm,2.5mm); } } } \tikzset{ pics/specialkey/.style n args = {2}{ code = { \draw[rounded corners=1mm,fill=gray!20] (-3mm,-9.5mm) rectangle (#2-1mm,2.5mm); \node[inner sep=1mm,anchor=south west,text width=#2,align=center] (A) at (-3mm,-9.5mm) {#1}; } } } \makeatletter \newlength{\my@keylength} \setlength{\my@keylength}{13mm} % {name}{label}{xpos}{ypos}{width} \def\my@specialkey#1#2#3#4#5{% \path pic (#1) at (#3\my@keylength,#4\my@keylength) {specialkey={#2}{#5\my@keylength}}; } % {name}{index}{north-west}{south-west}{north-east}{south-east}{vertical-index}{left-offset} \def\my@keypath#1#2#3#4#5#6#7#8{% \path pic (#1) at ([xshift=#8\my@keylength]#2\my@keylength,#7\my@keylength) {vhsplit={#3}{#4}{#5}{#6}}; \typeout{#1/#2/} } % \keyassign{1st}{2nd}{3rd}{4th line} \def\keyassign#1#2#3#4{% \begin{tikzpicture}% \newcount\my@keycount % % 1st line \my@keycount=0 \@for\lp@elem:=#1\do{% \expandafter\def\expandafter\my@cnum\expandafter{\the\my@keycount} % \expandafter\def\expandafter\my@keyname\expandafter{\expandafter{\expandafter a\my@cnum}} % \expandafter\def\expandafter\my@keyindex\expandafter{\expandafter{\my@cnum}} % \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\my@keypath\expandafter\expandafter\expandafter\my@keyname\expandafter\my@keyindex\lp@elem{0}{0} % \advance\my@keycount by 1 % } % \my@specialkey{BS}{Back\\Space}{13}{0}{1.27} % 2nd line \my@specialkey{Tab}{Tab}{0}{-1}{1.27} \my@keycount=0 \@for\lp@elem:=#2\do{% \expandafter\def\expandafter\my@cnum\expandafter{\the\my@keycount} % \expandafter\def\expandafter\my@keyname\expandafter{\expandafter{\expandafter b\my@cnum}} % \expandafter\def\expandafter\my@keyindex\expandafter{\expandafter{\my@cnum}} % \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\my@keypath\expandafter\expandafter\expandafter\my@keyname\expandafter\my@keyindex\lp@elem{-1}{1.5} % \advance\my@keycount by 1 % } % % 3rd line \my@specialkey{CapsLock}{Caps Lock}{0}{-2}{1.57} \my@keycount=0 \@for\lp@elem:=#3\do{% \expandafter\def\expandafter\my@cnum\expandafter{\the\my@keycount} % \expandafter\def\expandafter\my@keyname\expandafter{\expandafter{\expandafter c\my@cnum}} % \expandafter\def\expandafter\my@keyindex\expandafter{\expandafter{\my@cnum}} % \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\my@keypath\expandafter\expandafter\expandafter\my@keyname\expandafter\my@keyindex\lp@elem{-2}{1.8} % \advance\my@keycount by 1 % } % \my@specialkey{Enter}{Enter}{12.80}{-2}{1.47} % 4th line \my@specialkey{Shift}{Shift}{0}{-3}{2.07} \my@keycount=0 \@for\lp@elem:=#4\do{% \expandafter\def\expandafter\my@cnum\expandafter{\the\my@keycount} % \expandafter\def\expandafter\my@keyname\expandafter{\expandafter{\expandafter d\my@cnum}} % \expandafter\def\expandafter\my@keyindex\expandafter{\expandafter{\my@cnum}} % \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\my@keypath\expandafter\expandafter\expandafter\my@keyname\expandafter\my@keyindex\lp@elem{-3}{2.3} % \advance\my@keycount by 1 % } % \my@specialkey{Shift}{Shift}{12.3}{-3}{1.97} \end{tikzpicture}% } \makeatother \begin{document} \keyassign{{}{}{\char"07E}{\char"060},{}{}!1,{}{}@2,{}{}\#3,{}{}\$4,{}{}\%5,{}{}{\char"05E}6,{}{}\&7,{}{}*8,{}{}(9,{}{})0,{}{}\_-,{}{}+=}% {{}{}{}Q,{}{}{}W,{}{}{}E,{}{}{}R,{}{}{}T,{}{}{}Y,{}{}{}U,{}{}{}I,{}{}{}O,{}{}{}P,{}{}\{[,{}{}\}],{}{}|{\char"05C}}% {{}{}{}A,{}{}{}S,{}{}{}D,{}{}{}F,{}{}{}G,{}{}{}H,{}{}{}J,{}{}{}K,{}{}{}L,{}{}{:}{;},{}{}{\char"022}{\char"027}}% {{}{}{}Z,{}{}{}X,{}{}{}C,{}{}{}V,{}{}{}B,{}{}{}N,{}{}{}M,{}{}{<}{\char"02C},{}{}{>}.,{}{}?/} \end{document}
これをXeLaTeXでコンパイルすると次の様に描画される。
雑な解説
簡単に解説しておくと、\keyassignマクロは4つの引数、すなわち順に(上から)第1行目(キー13個)、第2行目(キー13個)、第3行目(キー11個)、第4行目(キー10個)のキー配列を引数としてとり、キー配列を描画する。
キー配列はキーをカンマ“,”で区切ったものである。
キーは文字4つ(何も文字がないところには{}を置く)からなり、左上、左下、右上、右下の順である。複数文字を一つの場所に入れる場合には{}で囲む。なお(La)TeXの特殊記号となっているものはエスケープしたりしないといけない。
\begin{document}~\end{document}の中の\keyassignマクロの部分を変えると他のキーボード配列を作成することができる。例えばロシア語キーボード配列だと、
\keyassign{{\char"07E}{\`{}}{}Ё,!1{}{},@2{\char"022}{},\#3№{},\$4;{},\%5{}{},{\char"05E}6:{},\&7?{},*8{}{},(9{}{},)0{}{},\_-{}{},+={}{}}% {Q{}{}Й,W{}{}Ц,E{}{}У,R{}{}К,T{}{}Е,Y{}{}Н,U{}{}Г,I{}{}Ш,O{}{}Щ,P{}{}З,\{[{}Х,\}]{}Ъ,|{\char"05C}/{}}% {A{}{}Ф,S{}{}Ы,D{}{}В,F{}{}А,G{}{}П,H{}{}Р,J{}{}О,K{}{}Л,L{}{}Д,{:}{;}{}Ж,{\char"022}{\char"027}{}Э}% {Z{}{}Я,X{}{}Ч,C{}{}С,V{}{}М,B{}{}И,N{}{}Т,M{}{}Ь,{<}{\char"02C}{}Б,{>}.{}Ю,?/.{\char"02C}}
のようにして、次のような画像が得られる。
なぜこんなものを作ったかというと、モンゴル文字キーボードのキー配列表を作りたかったからで、フォントの変更や文字の回転などを組み合わせて次のようになった。なかなかいい感じだと思う。
というか、作ってから時間たってるので、マクロ内部で何やってるのか覚えていない。何やら\expandafterとTikZに苦しんでいたのは覚えているが……。
無理な時に押すボタンをつくった
最近いろいろと無理なので無理な時に押すためのボタンをつくった。Arduino Unoを使って押すとむーりぃーって言うもの。
無理な時に押すボタンを作った。 pic.twitter.com/qAI0iB4MfX
— にせねこ@コミケ三日目東R-06a (@nixeneko) December 27, 2016
へぇボタンじゃねーか。
ハードウェア
Arduino Uno (R3)を使った。
全体図こんな感じ。
ボタン
ボタンは100円ショップで売っている押すとオン・オフが切り替わるプッシュライトのスイッチを改造した。
上のサイトを参考に、スイッチ内部でカチカチと引っかかる部品を取り除き、押している間だけスイッチが切り替わるようにする。また、押している間通電するようにしたかったので元の配線から配線を付け替えている。
さらに、ライト部分も生かしたかったのでそこにもリード線をつけて外に引っ張りだした。
ライトを開けるとこんな感じ。
配線
こんな感じの回路図になると思う。入力ピンに繋がれる部分は、ボタンが押されてないときはGNDに、押されてる間は+5Vになる。
実装時には、あまり良くないけどプルダウン抵抗は省略した(要するにプッシュライトのLED用抵抗がプルダウン抵抗の役目を果たすことになる)が、一応問題なく動いているようである。
ソフトウェア
音声の出力については
を参考にした。
音声を用意する
今回はArduinoのフラッシュメモリ(32kB)に書きこむので、データのサイズをメモリに乗る程度に小さくしないといけない。という訳でメモリぎりぎりのサイズになるようサンプリング周波数を調整した。
- サンプリング周波数13800Hz
- モノラル
- 符号なし8bit
- ヘッダなし
の音声ファイルをAudacityを使って用意した。音量は割れない程度に大きめにしておいた。
ファイル名はmuri13k8.rawとした。
次に、Arduinoで読み込めるように、.hファイルに8ビット符号なし整数の配列として書き出す。
適当なpythonスクリプトconv.pyを書いて変換する。
with open('muri13k8.raw', 'rb') as f: data = f.read() print('const unsigned char muri[] PROGMEM = {') for x in data[:-1]: print('{},'.format(x), end="") print('{}}};'.format(data[-1]))
この出力をmuri.hとして保存し、スケッチのフォルダに突っ込んでおく。
python conv.py > muri.h
muri.hの内容はこんな感じになる。
const unsigned char muri[] PROGMEM = { 126,125,125,125,125,125,125,125,125,125,125,125,(以下略) };
Arduinoコード
Arduinoのプログラムを書く。Arduino IDEのバージョンは1.6.11。
#include <avr/pgmspace.h> #include "muri.h" #define ARRAYSIZE(x) (sizeof(x) / sizeof((x)[0])) #define OUT_PIN 3 #define BUTTON 19 int i = 0; // 音声の再生位置 int lastval = 0; // ボタン状態レジスタ void setup() { pinMode(BUTTON, INPUT); pinMode(OUT_PIN, OUTPUT); // http://qiita.com/kinu/items/6cd5da0415e31834e7da から // これを設定するとすごい速さで動くようになるらしい // Non-inverting fast PWM mode on Pin 3. // COM2B1:0 == 10: Non-inverting mode // WGM22:0 == 011: Fast PWM mode, 256 cycle (16MHz / 256 == 62.5kHz) // CS2:0 == 001: No prescaler (runs at maximum rate, 62.5kHz) TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20); TCCR2B = _BV(CS20); i = ARRAYSIZE(muri); // 再生位置を最後に指定し自動再生されなくする } void loop() { if(i < ARRAYSIZE(muri)){ OCR2B = pgm_read_byte_near(&muri[i]); // 音声データ書き込み i++; } lastval = (lastval << 1) | digitalRead(BUTTON); // チャタリング回避 if(lastval == 0x7FFF) // ボタンが押されたら i=0; // 再生位置を0に delayMicroseconds(72); // 1000000microsec / 13800Hz }
- setup()関数内でマイコンが早い周波数で動くように設定している。
- 単純にloop()関数内で音声の周期(72μs)ごとに音データを書き込んでるだけで、ボタンが押されると音声データの配列のインデックスを0に更新することで最初から再生されるようにし、ボタンを連打できるようにしている。
- チャタリング回避を行っている。具体的には、digitalReadの値がボタンが押されていないときに0、押されているときに1となるのを利用して、0のあと15回1が続いた場合、信号が安定したとみなして音の再生を行う。ボタンの数値のログを保管するのにはシフトレジスタを使い、毎回レジスタを1ビット左シフトしてLSBにdigitalReadの値を入れていく。シフトレジスタ(lastval)の値が0b0111 1111 1111 1111となったときにボタンが押されたときの操作を実行する(再生位置を0に更新する)。これによりチャタリングが抑制されボタンを離した時に音が再生されることがほぼなくなった。
最後に
むーりぃー