トップ

Making of Dangerous Bend Sign SVG

Knuth 教授の TeX ブックなどにある「急カーブ注意」マークの SVG 版を作ってみたのですが、いろいろと面白い知見が得られたので備忘録です。

全体像

まず、拡大したものを示します。

元データは METAFONT のソースで、私の手元の FreeBSD 環境の場合 /usr/local/share/texmf-dist/fonts/source/public/misc/manfnt.mf にありました。teTeX-texmf というパッケージによってインストールされたものです( pkg_info -W コマンドで確認)。

コード 127 番の Dangerous Bend Sign の定義部分だけ抜き出したものを以下に示します。

beginchar(127,25u#,hheight#+border#,0); "Dangerous bend sign";
pickup pencircle scaled rulethickness;
top y1=25/27h; lft x4=0;
x1+x1=x1a+x1b=x4b+x2a=x4+x2=x4a+x2b=x3b+x3a=x3+x3=w;
x4a=x4b=x4+u; x3b=x1a=x1-2u;
y4+y4=y4a+y4b=y3b+y1a=y3+y1=y3a+y1b=y2b+y2a=y2+y2=0;
y1a=y1b=y1-2/27h; y4b=y2a=y4+4/27h;
draw z1a..z1..z1b---z2a..z2..z2b---
  z3a..z3..z3b---z4a..z4..z4b---cycle;  % signboard
x10=x11=x12=x13=good.x(.5w-u); x14=x15=x16=x17=w-x10;
y10=y14=28/27h+epsilon; bot y13=-baselinedistance;
z11=(z10..z13) intersectionpoint (z1a{z1a-z4b}..z1{right});
y15=y11; y16=y12=-y11; y17=y20=y21=y13;
draw z11--z10--z14--z15; draw z12--z13; draw z16--z17;  % signpost
x20=w-x21; x21-x20=16u; draw z20--z21;  % ground level
x38=w-x31; x38-x31=8u; x32=x34=x38; x31=x35=x37;
y31=-y38=12/27h; y32=-y37=9/27h; y34=-y35=3/27h;
pickup pencircle scaled heavyline;
draw z32{z32-z31}..z34---z35..z37{z38-z37};  % the dangerous bend
pickup penrazor xscaled heavyline rotated (angle(z32-z31)+90);
draw z31--z32; draw z37--z38;    % upper and lower bars
labels(1,1a,1b,2,2a,2b,3,3a,3b,4,4a,4b,10,11,12,13,14,15,16,17,20,21,
  31,32,33,34,35,36,37,38);
picture dbend; dbend=currentpicture;
endchar;

全体の設定

先頭から説明していきます。

長さの単位の u, hheight, border はファイル内のここより前の場所で指定されていて、u は 20/36 ポイント、hheight は 250/36 ポイント、border は 20/36 ポイントです。

beginchar により、w と h が設定されます。w は 25u なので 125/9 ポイント、h は 250/36 + 20/36 ポイントで 15/2 ポイントです。

看板の輪郭

pickup pencircle scaled rulethickness;

top y1=25/27h; lft x4=0;
x1+x1=x1a+x1b=x4b+x2a=x4+x2=x4a+x2b=x3b+x3a=x3+x3=w;
x4a=x4b=x4+u; x3b=x1a=x1-2u;
y4+y4=y4a+y4b=y3b+y1a=y3+y1=y3a+y1b=y2b+y2a=y2+y2=0;
y1a=y1b=y1-2/27h; y4b=y2a=y4+4/27h;
draw z1a..z1..z1b---z2a..z2..z2b---
  z3a..z3..z3b---z4a..z4..z4b---cycle;  % signboard

この部分で看板の輪郭部分の線を描いています。最後の「draw z1a..」で始まるのが描画コマンドで、「z??」というのがペンが通る点の指定です。ここでは、x と y をそれぞれ別に計算していて、たとえば z1a に対応するのは (x1a, y1a) の点です。

最初の「top y1=25/27h; lft x4=0;」では、座標値を直接指定していますが、残りは連立 1 次方程式になっています。

この連立方程式を全部解くと座標が得られます。この METAFONT ファイル内では分数のポイント単位の値になりますが、18/5 を掛けると切りのよい整数値になりますので、それを採用します。次に示すような位置関係になります。

各点の座標は次の通り。

z1a = (21, 23)
z1 = (25, 25)
z1a = (29, 23)
z2a = (48, 4)
z2 = (50, 0)
z2b = (48, -4)
z3a = (29, -23)
z3 = (25, -25)
z3b = (21, -23)
z4a = (2, -4)
z4 = (0, 0)
z4b = (2, 4)

角の丸めの部分についての処理ですが、METAFONT を厳密にエミュレートすることはあまり考えず、ここでは SVG のパスの処理にある、ベジェ曲線でつなぐことにします。

角の部分の点の位置関係は、拡大すると、次の図のようになっています(小丸を打った場所が、線の通過点として指定された場所)。

菱形で示した点を制御点として指定して、2 次ベジェ曲線の描画を指定すれば、ちょうど通過点として指定された場所を通るので、そのようにパスを指定します。青色で示したような曲線になります。

輪郭部分の線の太さは rulethickness で 0.4 ポイントとファイル内の別の場所で指定されているので 18/5 を掛けて 1.44 になります。

以上をまとめると、次のような SVG のパス要素になります。

<path
d="M21 23
Q25 27 29 23
L48 4
Q52 0 48 -4
L29 -23
Q25 -27 21 -23
L2 -4
Q-2 0 2 4
Z"
fill="yellow" stroke="black" stroke-width="1.44"></path>

棒と地面

看板の上下の棒と、下の水平線は METAFONT ファイルでは次のようになっています。

x10=x11=x12=x13=good.x(.5w-u); x14=x15=x16=x17=w-x10;
y10=y14=28/27h+epsilon; bot y13=-baselinedistance;
z11=(z10..z13) intersectionpoint (z1a{z1a-z4b}..z1{right});
y15=y11; y16=y12=-y11; y17=y20=y21=y13;
draw z11--z10--z14--z15; draw z12--z13; draw z16--z17;  % signpost
x20=w-x21; x21-x20=16u; draw z20--z21;  % ground level

看板との交点まで線を引くよう指定がされていますが、SVG では描画順によって上書きすることで解決することにします。他は同じ要領ですので( good.x というのがよくわからないのだが、とりあえず .5w-u が指定されているものとした。また epsilon はごく小さな値のはずだが、実際の TeX の出力と比べると見た目でわかるくらいに調整されているので、ちょっと調整してある。baselinedistance もファイル内の別の所にある)、特に解説はしません。次のような SVG 要素群になります。

<rect x="23" y="-39.6" width="4" height="68" fill="gray" stroke="black" stroke-width="1.44" stroke-linejoin="round"></rect>
(ここに看板を描画するパス要素が入る)
<line x1="9" y1="-39.6" x2="41" y2="-39.6" stroke="black" stroke-width="1.44" stroke-linecap="round"></line>

ジグザグ模様

ジグザグ模様部分は、次のようになっています。

x38=w-x31; x38-x31=8u; x32=x34=x38; x31=x35=x37;
y31=-y38=12/27h; y32=-y37=9/27h; y34=-y35=3/27h;
pickup pencircle scaled heavyline;
draw z32{z32-z31}..z34---z35..z37{z38-z37};  % the dangerous bend

pickup penrazor xscaled heavyline rotated (angle(z32-z31)+90);
draw z31--z32; draw z37--z38;    % upper and lower bars

同様にして連立方程式を解くと(と言ってもほとんどが方程式ではなく直接指定されているが)、ジグザグ模様の指定点は以下のようになります。

(17, 12)  -- 31
             (33, 9)  -- 32
             (33, 3)  -- 34

(17, -3)  -- 35
(17 -9)  -- 37
             (33, -12)  -- 38

輪郭線の場合と違い、曲線部分は始点と終点のみで、通過点の指定がありません(通過点のためと思われる番号が飛ばされています)。やってみればわかりますが、この前後のパスに滑らかにつながるような 2 次ベジェ曲線は、だいぶ尖った感じで、METAFONT の結果とはかなり異なるものになります。METAFONT の結果に厳密に近づけるには、3 次ベジェ曲線を使うべきと思われますが、ここでは、やはり METAFONT の挙動を追跡することはさぼって、滑らかに接続する円弧(いわゆる「角丸長方形」のような)でつなぐことにします。

SVG の矩形(長方形)の描画には、角を丸くする指定がありますが、パスには、接続部分の幅の部分を丸くする指定があるのみで、描画の芯線そのものを円弧で滑らかにつなぐ、という指定はありません(そのような描画指定が仮にあったとすると、いくつかのめんどうなコーナーケースが発生するものと思います)。

SVG のパスで指定できるのは、A または a 命令による、楕円弧により接続する、というものです。これは SVG 処理系により「接続する円弧」が描かれるもので、「滑らかに」接続されるとは限りません。滑らかに接続したい場合は、ユーザがそのようなパラメータを指定する必要があります。以下、そのためのパラメータの指定について説明します。

31-32 と 35-34 の各直線の位置関係を図示すると、以下のようになっています。

32 の点と 34 の点の x 座標が同じで、直線の傾きは非対称ですから、楕円ではなく真円の円弧で滑らかに接続したい場合、34 の点ではなく、もう少し延長した点(右にある交点から点 32 までの距離と、同じ距離にある点)で接続しなければいけません。具体的には、小さな縦線で示した所です。

交点から点 32 までの距離は簡単に厳密解を得ることができて、(2/3)*√(5*53) となります。34 を修正した点は数値的に解いてしまいました。(33.50511019963996, 3.1894163248649843) という点になります。

もうひとつ、円弧の半径もパラメータとして必要です。これも数値的に求めてしまいます。

それぞれの直線の傾きはわかっていますから、逆正接でなす角を求めると、

Math.atan(3.0/16.0) + Math.atan(6.0/16.0) = 0.5441186202662669 ラジアン

になります。

求めたい半径は直角三角形の sin の所で、cos の部分の長さは既知ですからそれを tan に掛ければ得られます。

(2.0/3.0)*Math.sqrt(265.0) * Math.tan(0.5441186202662669 / 2.0) = 3.027605014567413

残りのパラメータは、円弧を開口部のどちら側に生成するか、というものなので、望む側に円弧ができるよう調整すれば完成です。もうひとつの曲線部も同様にパラメータを求めて、以下のようなパス要素になります。

( METAFONT では上と下の直線部は別に描画しているが、SVG では stroke-linecap="butt" と指定すれば同じ効果が得られるので、いっぺんに全部描画している)

<path
d="M17 12
L33 9
A 3.027605014567413 3.027605014567413 0 0 0 33.50511019963996 3.1894163248649843
L16.494889800360042 -3.1894163248649843
A 3.027605014567413 3.027605014567413 0 0 1 17 -9
L33 -12"
fill="none" stroke="black" stroke-width="5" stroke-linecap="butt"></path>

おわりに

dbend の mf 定義の解読にあたっては、函館高専の北見さんによる「TeX関係」のページ( http://www.hakodate-ct.ac.jp/~kitami/TeX/ )にある、画像化したものが大変参考になりました。この場を借りて御礼申し上げます。

追加

直線の延長と円弧による接続のデモ http://jsdo.it/metanest/bQbQ

楕円による滑らかな接続に関する考察

このような、直接には円弧で滑らかに接続できない区間を、楕円で接続する方法について考えます。

次の図のような場合で考えてみました。

黒で描かれている楕円が、この区間を接続する楕円の候補のひとつです。径が同じ楕円を、この 2 直線に色々な角度で接触するように置くと、下側の接点がもっとも左側に寄る角度があります。その時、両方の接点が望む位置に来るような楕円を探すとこのようになりました。長径を半径 430、短径を半径 171 とし、13 度傾けると、このようになります。

観察してみると、楕円の縦横比がかなり大きいため、結構とがった感じになり、2 次ベジェ曲線にも少し似ています。滑らか感は良いようにも思いますが、ここでは 2 次ベジェ曲線との違いが大きいもの、縦横比が 1 に近いものを探してみることにします。

次の候補です。

2 個の接点を通る直線の傾きの変化の度合いは、縦横比の大きさに比例します。よって、望む傾きが得られる最小の縦横比が存在します。傾きが決定したら、縦横比を保って拡大または縮小することにより、望む点で接する楕円が得られます。そのようにして作られた楕円がこれになります。

長径の半径 187、短径の半径 115.4、傾き -13 度になっています。

(一つ前の例と角度がちょうど対称なので、幾何的に解法があるのではないか、という気がすごくするのですが(そして射影幾何のすごく初歩で解決する問題なのではないかという気がしますが)そこまで学習する気力が足りてません)

プログラムを実装し、実際に計算してみたパラメータに基づく SVG を示します。

同様のデモをこちらも公開しました http://jsdo.it/metanest/uxxM