見出し画像

万年筆の⽣々しい濃淡はどう作られた?「イワタつづり」インク溜まり⽣成の⾃動化スクリプトを徹底解説(後半)

こんにちは。イワタ開発部の狩野です。

前回の記事では、新書体「イワタつづり」のカラーフォントに不可欠な「インク溜まりの自動生成プログラムについて、開発のきっかけから交差点十字路・丁字路)」の処理アルゴリズムまでを解説しました。

▼前半の記事はこちら

後半では、本プログラムの開発において技術的に最も困難であった画線の端の検出アルゴリズムから解説を再開します。また、最後にはプログラムの限界をカバーし、製品クオリティへと昇華させたデザイナーの手作業による修正工程についてもご紹介します。

それでは、前回の続きとなる技術解説に入ります。


3. 技術解説(つづき)

3.3. 画線の端の検出方法

次は画線の端を切り抜く処理ですが、この方法を確立するまでには大変に苦労しました。画線の端を正確に検出することは、今でもできていません。最初の見積もりが若干ずれていてもストロークをほぼ垂直に切り抜く方法を発案して、なんとか間に合わせています(例えば、長さの基準となる線端の位置ズレは、ストロークの切り抜き長さを調整して誤差を吸収しています)。

まず最初の工夫は、ベジェ曲線のパスの曲率が大きい=曲率半径が小さい場所をリストアップすることでした。1本のパスは複数の短いベジェ曲線(セグメント)を継ぎ合わせて作られているのですが、各セグメントで曲率最大の場所をリストアップし、その曲率半径が一定値以下で、隣のセグメントの最小値よりも小さいもののみを残すと、以下のような曲率極大点の一覧が得られます。

図12:曲率半径の極小点


文字の外側のパスは反時計回りに描くのですが、左に曲がっている場所をプラスの曲率半径で、右に曲がっている場所をマイナスの曲率半径で表しています。小さなマイナスの曲率半径をもつ曲率半径の極小点は線の角や折り返しの内側にあたり、その法線方向に近い所にも、それと対になる曲率半径極小点があります。
この2つをペアにしたのが、図12に青字で示した2つの数字です。これらを取り除けば線の端を検出できることになります。

3.4. 折れ曲がり部分の切り出し方

ここで先に、図12の青で示したような筆画が折れ曲がる場所(転折部)の切り出し方を簡単に解説します。

図13-1をご覧ください。
アウトラインの凹みのピークである、曲率半径マイナスの曲率極大(=曲率半径極小)点から法線を引き、その左右30度の範囲内に、曲率半径プラスの曲率極大点が存在すれば、そのあたり一帯が筆画の折れ曲がりまたは撥ねの箇所であることが確定します(図13-1では、青い+で示した場所に存在する)。これらの青い●と+のような対になった2つの曲率極大点はまとめて未処理の尖った点リスト(=線端の候補)からは除外され、転折部としての切り抜き処理が行われます

図13-1:正⾯(法線⽅向)から左右30°の範囲に点があれば、2個の点は転折部の⼀部


転折部を切り出す⽅法は単純で、内側の切断位置は直ちに決まります。インク溜まりが大きくなりすぎないよう、転折部の内側はできるだけタイトに、曲率極大点(図13-1、図13-2の青い●)からパスに沿って前後3メッシュ移動した位置で切ります。
しかし、曲率最大点の近辺で激しく方向が変化している内側に対し、外側の曲線はそれと連動せずに、ほぼ直線に近い緩やかな内向きのカーブを描いています。ですから、法線を延ばしても、前後の筆画は垂直には切れません。筆画の流れる方向に垂直な方向を推定して外側の切断位置を調整する必要があります(調整後の結果が図13-2の赤線)。

図13-2:切断線の両端でパスに交差する角が均等になるように切断位置を調整


図13-2の法線は、転折部の外側の線の流れはお構いなしに、内側だけを見てまっすぐ切った結果ですから、外側でパスと交わる交差角は垂直から大きく傾いた、かなり鋭い角になっています。法線と画線のパスがなす角は内側がちょうど直角、外側が直角よりかなり小さい角というアンバランスが生じています。
 
この直角からの不足分が両側で同じになるように切断角度を調整した結果が太い赤線です。これについては下の図14-1、図14-2のように単純化した例で⾒ると分かりやすいでしょう。

画線の太さの変化率を⼀定に単純化した模式図で見てみましょう。この場合、筆画の両サイドのパスは図14-1のように直線になります。この画線をもし、筆の流れ方向の中心線に直交するように切ることができたら、両脇の角度は等しくなります。

図14-1:切断位置のアタリをつけて垂線を立てるが、正しい切断方向からはズレている

図14-2のようにパスを切断することができれば、上に《カマボコ形》(3.6で説明します)をくっつけた時に、その頂点がちょうど画線の中心に来ることになります(実際には中心線は曲がっているので、現在の自動処理の方法だとカマボコの頂点がパスの中心と微妙にずれ、人の目による確認と修正が必要な部分になっています)。

図14-2:太さが変化する筆画に垂直な切断線は、パスとの交差角が直角より小さくなる

3.5. 筆画端部の処理の難しさ

話を筆画の端(始筆・収筆)に戻します。
筆画の端の切り抜き処理が難しい理由として、どこが本当の端なのか見つけるのが難しいこと以外にも、場所により自然に見えるインク溜まりの長さが違うことが挙げられます。

本来、筆の打ち込みがある場所は筆圧がかかるので大きなインク溜まりができ、筆を止める場所ではやや小さく、撥ねの先端のインク溜まりはごく小さくなるはずですので、それに合わせて筆画の切り抜き長さを長く・中程度に・短くする必要があります。
始筆と収筆(終筆)を区別できればそれをヒントに始筆を長め、収筆を短めに設定できるのですが、「匕」の斜め線のような、やや右上がりの横画は左から書く場合(「比」の続け書きをしているサンプルをご覧ください)もあれば、普通に右から入って左下に下ろす場合もあります。
これを自動判別するのは不可能ですので、現在の処理方法では基本的に始筆収筆を同じように扱っています

線の方向に関わりなく利用できる情報は太さです。
始筆は筆圧がかかっている上に筆の動きも遅いので、筆画が途中で右側(縦画の場合)・下側(横画の場合)に大きく膨らんでいます。線幅が一定になる中心部に比べて最大で倍近くの太さになります。

縦画の収筆部に見られるような「止め」(垂露)の形も、始筆ほどではありませんが線の端の丸みの部分に若干の膨らみを持ちます(一方、「千」の字のように収筆で膨らみを作らずにスッと抜く形(懸針)の場合は、払いの先端と同じくらい小さなインク溜まりとなります)。

この途中でいちばん太くなった場所と、最終的に太さが落ち着いた場所の比率、つまり画線の膨らみ具合を見れば線端の種類を正確に判別できるのではないか、そう期待できます。

まっすぐ輪切りにする処理は相当時間がかかるため(切断角度を何回も試行錯誤しなければならない)、「線の端から15~100メッシュのエリアを5メッシュごと」という条件で妥協しています。実はそれ以前に、切り始める場所をどこに置くか計算するのが一苦労なのです。

ストロークは折れ曲がる部分があるので、端点からの直線距離で切断位置を決めるわけにはいきません。「パスの輪郭線上の道のり」が唯一利用可能な長さです(中心線に沿って均等に切れればそれが理想なのですが、中心線を正しく求める処理はまだ実現できていないので、現時点では影も形もありません)。そのため、ストロークの流れに沿った移動距離の代用品として、パスに沿った道のりを切断開始位置を定める変数として用いることにします。

切断開始位置を決めるパスの道のりは、端点の前に遡るのと、後に進むのとどちらが適切でしょうか。この選択は「できるだけパスが真っ直ぐなほう」としました。100メッシュずつ前後に移動し、出発点から遠い方を採用するのです。

図15:切り始める方向(入口)は、パスが比較的真っ直ぐなほうを選ぶ


次に、端点から15 メッシュの地点から開始し、100メッシュ未満の範囲で5メッシュごとにプロットしたサンプル点から画線をまっすぐに切る線分を定めます。まず試しに法線を引いてみて(図16-1左)、角度がズレている分を補正する(同図右)わけです。(端に近い場所では、線の端の丸みに引っかかって法線が変な方向を向くので、集計対象からあえて外しています)

図16-1:(左)一定間隔に置いたサンプル点における画線幅を計るため、まず法線を伸ばす
(右)反対側の測定位置を調整し、各地点における正しい画線幅を求める


図16-1(右)に示した18箇所で引いた線の長さを利用して、画線幅の太さの変化傾向を調べます
ここで一番重視されるのが、「膨らみ率」と呼ばれる値で、通常の線幅に対して膨らみ部分の線幅が何倍になるかの推定値です。始筆部の筆の打ち込みがある場所では1.5のような大きな値になります。一方、左払いの収筆部のように、先端から中央に向かって線幅がだんだん太くなっていて途中にピークが存在しない場合は、コブのような膨らみ(線幅が膨らんでから細くなる)が全く存在しないので、最大幅と最小幅の差がないと見なされ、膨らみ率は1.0となります。

図16-2:膨らみ率の計算方法

理想的には、画線幅が一番太くなっている部分の幅と、画線の中央部で一番細くなっている場所の幅との比率を調べたいのですが、実際に得られる値は、図16-1で示した5メッシュおきの測定箇所の幅のうち、最大値(図16-2の赤い太線の長さ)を最小値(図16-2の青い太線)で割った比率になります。線の端のほうが中間部よりも当然細いので最大値の手前のサンプル値は取り除き、最大値以降の値のみで最大÷最小の比率を求めています
サンプル間隔が粗くて画線が一番太い場所をぴったり捉えられないためにサンプルの最大幅は真の最大線幅より小さく、だんだん細くなる線幅の測定を途中で打ち切るために、サンプルの最小幅は通常線幅よりも大きくなります。それらの比(=膨らみ率の実測値)は、正確に測ったときに得られるであろう膨らみ率の理論値よりだいぶ小さな値になります。

主に膨らみ率の値を用いて、線端を膨らみ小大の3段階に分類しますそれぞれ表1 のa)b)c)に相当)。ただし縦画だけは画線の角度から収筆部を検知して別扱いにしていますので、現実の分類条件は表1の箇条書きに示した通り、膨らみの有無による2段階と、「縦画の収筆か、それ以外か」による場合分けを組み合わせたものになっています。

しっかり筆押さえがある縦画・横画の始筆に相当する「膨らみ率大」の場合は、より詳しく形状解析を行って切断位置を決める(具体的には、太さのピークの少し先にある変曲点を切断点とする)のが望ましいのですが、現在のアルゴリズムはそこまで洗練されていません。画線の長さはまちまちで、うまく解析できるケースばかりではないからです。最終的に、表1に示すような膨らみ率の小・中・大の3分類に応じてそれぞれ異なる比例定数を線幅に掛け、線端を切り抜く長さを求めます。(線端の曲率極大点から、前後のパスのうち曲がりくねっていない方をその長さだけ辿った位置が切断線の片方の端になります。)

表1:線端の分類方法と切断の長さ

a)全く膨らみがない場合
 →(試しに引いた線の)平均線幅の0.8倍の長さで切る
b)膨らみ率が1.5未満で垂直に近い線の終点の場合
 →平均線幅の約0.96倍の長さで切る
c)それ以外の場合
 →線幅の倍または60メッシュの短い方で切る

膨らみ率に応じた線端の分類と切り抜き長さの設定のやり方をいろいろ試行錯誤しているうちに、本来目指していた物と実際の処理がだいぶ食い違ってしまいました。横画の収筆は意図に反してb)ではなくc)に分類されているので、膨らんだ丸い部分だけ切り抜きたいのに、実際の切断長が長くなる傾向にありました。せめてもの対策として表1のc)に示した「最大60メッシュという制限を入れたのですが、今回はその条件が完全に裏目に出ています。

下の図17(左)は自動切り抜きの結果ですが、60メッシュ制限により、太さのピークパスの一番右端にある点のところの手前で切断されてしまっています
図17(右)が手動で切断位置を適切な長さに設定したもので、断面の右側をアウトラインの変曲点に合わせ、断面の左側はそれに釣り合う位置に設定しています。

図17:プログラムが計算した切断領域(左)と、適切な切断領域(右)

今こうやって説明のために精密に図示してみると、線幅の極大点を求める精度もよくないことが分かりますし、切断場所を決定する時にアウトラインの形状情報をもっと活用するべきであることも分かりますが、今回は処理作業に実際に使用されたバージョンについて、欠陥も含めありのままに解説をしていますのでお許しください。

先ほど「画線の長さはまちまち」と申しましたが、図17ほど長い打ち込みは例外的と言っていいほど稀なものです。逆に、短い画線の例をご紹介しましょう。
図18に示すのは「比」の字の右上の撥ねの部分です。方向を調整する前の法線は下図のように並んでいます。

図18:複雑な形状をもつ部分で、画線幅解析のため一定間隔で法線を引いた結果


図18の赤丸で示した線端に一番近い法線は、撥ねの線端部の丸みの影響で、2本目以降の線に対して斜めになっていて、内側に巻き込んでいます。これは最後(線端から90~95メッシュの場所)の2本の法線も同様です。

そして何より問題なのは、曲がりの激しい側(赤丸から反時計回りに進んだ方向)に沿ってパスを辿ったときに法線の列を順にまたぎ越えてきていたのが、図18の左の青丸のあたりから先では撥ねの付け根側から先端側に後戻りしているため、9本目以降の法線点線は線幅を計るのに役立っていません

プログラムは処理している箇所の形状に関する知識は持っておらず、法線がパスに再度交差した位置とパス上の時刻パラメータしか検出できないのですが、時刻パラメータと画線幅が不連続に変動していることにより、正しい処理が途中から行えなくなったことは検知でき、そこから先のデータを捨てています。

短い跳ねの場合、線幅を解析して意味があるのは先端からせいぜい40メッシュまでということになります。このような短い場所を解析することもあるので、太さを測定するサンプル点の上限を、線端から100メッシュ未満に制限しているのです(画数の多い漢字に出てくる小さな点の長さが100メッシュを少し超えるくらいです)。

3.6. カマボコの追加

切断の開始・終了位置が決まったら、切断位置で画線を横切り、その他の部分ではパスと交差せずに線端をすっぽり包むポリゴンを、パスを1本ずつに分けて収めたレイヤー上に重ねて置きます。次に、既に交差点部分にポリゴンを重ねたcrossingsレイヤーと一括して、Glyphsの提供するパス処理関数を使用して型抜き処理を行った結果をink_bleedレイヤーに集約します。

こうして得られた図形の直線部分(切断面)に放物線状のパスを付け加えると、ちょうどいい塩梅のインクにじみの形が出来上がります。直線と曲線1本ずつからできたこの図形を、適当な用語がないのでカマボコkamaboko)と呼んでいます。

図19-1:(左)カマボコ追加前の切り抜きパターン
      (右)16箇所の切断面にカマボコを追加した結果
(赤丸はズレがある場所)


カマボコ部分の形状は単純で、「ハンドルの長さがカマボコの底辺部分と同じ長さでハンドルを延ばす方向が直線部の端の点に最も近いオリジナルのパス上の点における接線方向のベジェ曲線」です。

言葉で言うと複雑ですが、図19-2に示した形を見れば、接続する図形に自然になじむ形にするためこう定義したことがお分かりいただけるかと思います。

19-2:カマボコの長さ
(左)短すぎる例、(中)実際の設定、(右)長すぎる例

ここで、直線の端点に接続している、長さ5メッシュ程度のベジェ曲線のハンドルをそのまま反対方向に延長すると、ハンドル長のx, y座標が小さな整数に丸められることによる相対誤差が大きいため、カマボコがそっぽを向いてパスからはみ出すことが頻出します。ですので、面倒でもオリジナルのベジェ曲線上での最近接点を求めて、そこでのパス進行方向を利用しています。

それでもまだ、図19-1(右)で赤丸で囲った箇所のように、カマボコのいちばん尖った部分がパスの中心にぴったり乗らず、微妙に斜めになっています。主な原因は切り口の角度が真っすぐになるように調整していないこと(バグで誤検出するのを恐れたため)ですが、パスそのものの方向変換によるズレもあり、これを改善するには仮置きしたカマボコのピークを測り、ハンドルの長さを再調整する処理を実装する必要があります。実装を見送ったというより、開発期間中にはこの問題にまで認識が及ばなかったというのが実情です。


4. 実際の動作


実際のプログラムは多種多様な理由で誤動作を起こします。そういう場合は、ポリゴンの形を調整したり、切り抜く前・後にあらかじめ余分な点を除去したりといった細々とした処置が必要です。これについていちいち触れているとあまりに煩雑なので省略します。
 
プログラム内で、データが矛盾してそれ以上処理を進められなくなっている部分が数%の割合でありますが、これ以上減らすのが困難になった時点で、例外処理を使ってエラー発生箇所の処理をスキップし、引き続き他の場所の切り抜き処理を行うようにしています。切り抜き処理など、既存のパスを操作する失敗が即Glyphs全体をクラッシュさせるような処理は自前で実装せず、Glyphsに組み込まれた安定性の高い機能を利用するようにするのが安定動作のキーポイントです(お世辞にも褒められた話ではありませんが)。


5. デザイナーの美意識と執念


プログラムの自動処理では判断できず、人間の判断を乞う部分もさまざまあります
例えば、カマボコ同士が重なり合ってしまうような場合は、中間部はストローク全体にインクが乗っていると見なします。つまり、切り抜くポリゴンを融合させて、隣り合う切り抜き領域全体を覆うように人間が手動で指定する必要があるのです。

図20:重なり合うインク溜まり領域を手編集でまとめた例


図20のような処理は手作業で行っていただくことを想定していましたが、エンジニア観点で私としては自動処理で十分であると見積もっていたものの、デザイナーの美意識からは見逃せない問題もありました。

それは、切り抜きに伴う丸め誤差です。

図21:切断の丸め誤差によって生じたオリジナル文字のシルエットとのズレ
(黄色い部分。Aは理想的切断位置の例)


図21はインク溜まり部分として切り出した図形の拡大です。
薄い色の線がオリジナルの文字のパス、濃い色の線がインク溜まり用に切り出したパス(カマボコ処理前)です。

ベジェ曲線上に直線を重ねたとき、直線の座標自体が小数の端数を持っていますから、計算で得られる交点の座標は必ず小数部分を含みますが、切り抜き処理を行った結果の座標とは異なります。フォントのパスに使える端点の座標は整数値だけなので、切り抜きのタイミングで整数丸めが行われるからです。
その結果、オリジナルの曲線に比べて1ピクセル未満のズレ(黄色い部分)が生じています。小さく表示されるものだから、このくらいの誤差は仕方がない…そう考えて諦めていました。

しかし、これはデザインの観点から許容できないズレだったため、「イワタつづり」開発秘話でも「人の手による執念の修正」としてご紹介した、根気のいる修正作業が行われました。
薄い色のオリジナル文字の線が、オレンジ色の格子の交点とできる限り重なる所(図21 の場合はAの位置)までポイントを移動し、オリジナル文字とインク溜まりのパスのズレをなくすために赤い点線のように整形し…という作業を、時間の制約が許す限り手間ひま掛けて行ったのです。

そのために、ずいぶん長く皆様をお待たせすることになってしまいました。この切断処理のコードをもう少し洗練させる時間的余裕があればよかったと反省しています。


6. おわりに


新書体「つづり」のインク溜まりを生成する自動生成プログラムがどのようにして作られているのか、内部の仕組みについてほぼ数式を使わずに解説いたしました。考え方の根本は、巻尺で曲線の長さを計ったり分度器で線の角度を計ったりという小学校の算数のような作業の積み重ねであることがお分かりいただけたでしょうか。
図によってだいぶ明快になりましたが、人に伝わるように言葉で補足するのはなかなか簡潔にはできず、前後編に分けてお送りすることになりました(実際のプログラムではこれを全部数式という言葉で説明する必要があるので、全部でおよそ4000行の規模になっています)。「当初は完全自動化を目指していたが、時間が限られている中で、プログラムの完成度は80%で打ち切り未完成な部分を数倍の人手でカバーした」というフォント開発の事情についても所々で触れています。

既に「イワタつづり」のカラーフォントで印字した文字の手書きを思わせる生々しさ、その筆跡の美しさについて多くの方からご好評いただいておりますが、このような完成度の高い書体を生み出せたのも、この書体に関わったデザイナーが、繊細な美意識はもちろん、インク滲みの形状の細部に至るまでゆるがせにしない志の高さを全員持っていたからだと思います。
必ずしも思うようには動かないプログラムを使いこなし、「イワタつづりLayer」2書体で延べ406,270個にのぼるインク溜まりを作り、整えてくださった方々に感謝いたします。


7. 補足情報(Qiita記事のご案内)


今回のnote記事の中では、ベジェ曲線の⻑さを測る幾何学的計算⽅法や、所定の⻑さで切り抜くための値の探索(⾮線形⽅程式の求根)など、数学的・プログラム的に詳細な内容についてはあえて触れませんでした。

これらの処理に関する詳細については、Qiitaに新設したイワタ技術サイト」にて補⾜記事を公開しています。プログラミングやアルゴリズムの側⾯にさらにご興味のある⽅は、ぜひそちらもご覧ください。


いいなと思ったら応援しよう!