蝸牛

最近用 MetaFun [1] 製做了一個小模塊 snail.mp[2] ,用於繪製矢量圖格式的簡單流程圖。git

此事純屬無意之舉。本來是要用 awk 寫一個可以自動編排文檔中的參考文獻和註釋的工具。在醞釀情緒的過程當中,打算用 MetaFun 畫一幅簡單的示意圖。在繪圖過程當中,因不斷嫌棄所用代碼的繁瑣,最終有了十餘行簡短的繪圖代碼以及可以讓這些代碼工做的一個小模塊。github

我將這個模塊命名爲 Snail(蝸牛)。之因此如此命名,一方面是由於以語言描述的方式繪製流程圖,效率過低了;另外一方面,繪圖過程也的確像蝸牛的爬動。segmentfault

簡單的例子

爲求和運算 1 + 2 + 3 + ... + 100 繪製流程圖,以此創建對 Snail 的喜歡、討厭或者不覺得然的初步印象。工具

首先應該用鉛筆在紙上繪製草圖。不過,我沒找到鉛筆,好不容易找到了一支中性筆客串一番。ui

基於以上草圖,用 Snail 繪製流程圖,結果爲spa

所用的繪圖代碼爲3d

\usemodule[zhfonts]
\defineframed
  [SnailBox]
  [frame=off, width=6cm, autowidth=force,
    align={middle, lohi, broad}, offset=overlay]

\startMPpage
input snail;
Node a, b, c, d, e;
a := io("\SnailBox{$i\leftarrow 1$\\$s\leftarrow 0$}");
b := proc("$s\leftarrow s + i$");
c := other("$i > 100$", diamond(b));
d := proc("$i\leftarrow i + 1$");
e := io("\SnailBox{$s$}");
as_planet(b, a, "bottom"); as_planet(c, b, "bottom");
as_planet(d, c, "right"); as_star(e, c, "bottom");
draw_each a, b, c, d, e;

enrich_each a, b, d, e;
flow_each a => b, b => c, walk(d.N, (_n_ _v_(d.N, b.E)), b.E);
tagged_flow("是", "right", .4) c => e;
tagged_flow("否", "top", .4) c => d;
\stopMPpage

繪圖環境

使用 Snail 模塊繪製流程圖,須要將繪圖代碼嵌入 ConTeXt 文檔:code

% 導言區:對 ConTeXt 排版功能予以全局設定
%
%
\startMPpage
input snail; 
% 繪圖區:繪圖語句;
%
%
\stopMPpage

若基於 zhfonts 模塊 [3] 實現中文支持,只需在導言區添加 \usemodule[zhfonts],即blog

\usemodule[zhfonts]
\startMPpage
input snail;
% 繪圖區:繪圖語句;
%
%
\stopMPpage

若將上一節給出的繪圖代碼保存爲 foo.tex 文件,使用 context 命令即可將其編譯爲圖形文件 foo.pdf,token

$ context foo.tex

context 命令隱含了許多細節。在 ConTeXt MkIV 環境裏,這個命令會將 foo.tex 文檔交由 TeX 引擎 LuaTeX 處理,最後生成 PDF 格式文件 foo.pdf。foo.tex 所包含的 MetaPost 代碼由嵌入在 LuaTeX 中的 MPLIB 轉化爲 PDF 格式的圖形文件,而後再由 LuaTeX 將圖形文件嵌入 foo.pdf。這一過程,使用 Snail 可將其描繪爲

結點

一幅流程圖由結點、結點間的連線以及連線上的標註等元素構成。在 Snail 看來,結點只有兩類,一類是 I/O(輸入/輸出)結點,另外一類是過程(Procedure)結點。在 Snail 的默認繪圖設定中,I/O 結點是無邊框的文本,而過程結點是有邊框的文本。

Snail 模塊的 io 宏用於構造 I/O 結點,只需將 I/O 結點的內容以字串的形式做爲參數傳給 io 宏,例如

Node a;
a := io("I/O 結點的內容");

I/O 結點的文本顏色默認爲黑色,若讓它呈彩色,例如深綠色,只需

Node a;
a := io("I/O 結點的內容") withcolor darkgreen;

proc 宏用於構造過程結點。它會根據過程結點的文本自適應肯定一個矩形邊框,文本到邊框的距離(留白)默認是 12 bp,約爲 4.233 mm。例如,

Node d; d := proc("\CONTEXT");

proc 也能想 io 宏那樣經過 withcoloor 語句修改結點文本的顏色。

人只是人,人際關係卻多變。數據只是數據,過程亦多變。對於流程圖而言,過程的多變對應的不過是過程結點的形狀和顏色的變化而已。以矩形爲邊框的過程結點可用於表示通常的過程。其餘形式的過程,其結點可經過 other 宏構造,例如

Node b, c, d; string d.txt;
b := other("\CONTEXT", fullsquare xysized (3cm, 1.5cm));
c := other("\METAPOST", ellipse(like b)) withcolor darkblue;
d.txt := "蝸牛爬得快嗎?"; d := proc(d.txt);
d := other(d.txt, diamond(like d));

b 的邊框是長 3 cm、寬 1.5 cm 的矩形。c 的邊框是橢圓。在默認狀況下,c 邊框的長軸與短軸的尺寸分別是 b 的邊框長度和寬度的 1.25 倍。like 是 Snail 的宏,其做用是根據基於給定圖形的最小包圍盒肯定一個矩形,於是 eillpse(like b) 的含義是基於像 b 的包圍盒那樣的矩形構造橢圓。同理,diamond(like d) 的含義是基於像 d 的包圍盒那樣的矩形構造菱形,只不過在上述代碼中,先構造了普通的過程結點 d,而後基於它的邊框構造菱形,再將新構造的結點賦予 d 這個變量,從而實現了 d 由普經過程結點向菱形過程結點的「進化」。注意,上述代碼也展示了 other 宏能夠像 ioproc 那樣以 withcolor 語句設置結點文本的顏色。

不管是 I/O 結點仍是過程結點,其類型皆爲 Node,該類型是 Snail 爲 MetaPost 的 picture 類型而取的「別名」。所以,I/O 結點與過程結點可直接用 MetaPost 的 draw 命令繪製出來,例如:

draw a; draw b; draw c; draw d;

Snail 的 draw_each 可將一組結點繪製出來,利用這個宏可避免重複輸入 draw 命令,

draw_each a, b, c, d;

上述兩條繪圖語句等價。不過,Snail 所構造的結點,皆以座標原點爲中心,所以上述兩條語句繪製的結果是一組堆疊起來圖形:

所以,結點的繪製必須在流程圖中的具體位置肯定以後方可進行。

恆星與行星

可直接使用 MetaPost 的平移變換命令 shifted 對結點進行定位。例如,對於上一節定義的四個結點,採用如下語句進行繪製:

draw_each a, b shifted (5cm, 0), c shifted (0, 2.5cm), d shifted (5cm, 2.5cm);

結果爲:

採用平移變換命令對結點進行定位,可將任一結點放在圖中的任一位置,這樣作雖然自由,可是隨着結點的增多,這個工做便會變得很是乏味。在繪製流程圖的過程當中,一個結點的位置一般是以它相對於另外一個結點的位置而肯定,並且兩者的間距一般應當是定值。

Snail 是個心懷宇宙的 MetaFun 模塊,它絕對不會知足於牛頓式的絕對空間。若是真的存在絕對的空間,那麼誰能告訴我太陽中心的三維座標呢?假若以上帝的視角去安排流程圖中各個結點的絕對位置或者各個結點的絕對間距,只要用心,也是可以繪製出很是美觀的流程圖,然而這樣的流程圖沒有生命,對結點的形狀與位置略做一些修改,圖的結構便會被破壞。上帝斷然不會去創造無生命的物體,不然它就太愚蠢了,不值得咱們敬仰。

在 Snail 看來,結點的相對位置分爲兩類,恆星定位和行星定位。這兩種定位決定了流程圖結點分佈的舒密。

恆星定位是以結點中心之間的水平或豎直距離做爲約束,基於一個結點的位置肯定另外一個結點的位置,這種定位可經過 Snail 宏 as_star 實現。例如,將結點 b 放在 a 的右側,讓兩者中心的水平距離爲默認的行星距離:

as_star(b, a, "right");

相似地,能夠用 lefttop 以及 bottom,將 b 放在 a 的左側、頂部以及底部。Snail 默認的行星距離是 5 cm,這個值存儲於 Snail 的一個全局變量 _star.s,這意味着可經過修改這個變量控制流程圖中以恆星定位的結點間距。還有一部分相似於 _star.s 這樣控制流程圖總體樣式的全局變量,在本文的最後會專門予以介紹。

行星定位相似於恆星定位,惟一的區別前者在對一個結點進行定位時是以結點邊框的間距——行星間距做爲約束。所謂結點邊框的間距,即對於任意結點 a 和 b,當它們的中心連線爲同一條水平或豎直的線段時,a 和 b 的邊框與該線段交點的距離。Snail 的宏 as_planet 用於實現結點的行星定位,其用法與 as_star 同,例如

as_planet(b, a, "bottom");
as_planet(c, b, "bottom");
as_planet(d, c, "right");

組合

結點的恆星和行星定位仍是太過於嚴格,以至一些特殊的結點定位需求難以知足,例如

結點 d 的寬度與 a、b 和 c 相同,高度則是從 c 的底端到 a 的頂端。爲了知足諸如此類的定位需求,Snail 提供了 +++ 運算符,用它能夠將任意兩個結點綁定起來,從而得到一個新的結點,並且新的結點所佔據的區域剛好包含這兩個結點。重複使用 +++ 即可以實現多個結點的綁定。

對於形如上圖所示的四個結點,可採用如下代碼予以定位:

Node a, b, c, abc, d;
a := other("a", fullsquare xysized (2cm, 1cm));
b := other("b", like a);
c := other("c", like a);
as_planet(b, a, "bottom"); as_planet(c, b, "bottom");
abc := a +++ b +++ c;
d := other("d", like abc);
as_planet(d, abc, "right");

draw_each a, b, c, d;

水平 / 豎直對齊

若將一個結點的中心與另外一個結點的中心在水平或豎直方向上對齊,可相應採用 Snail 的halignvalign 宏。例如,若將結點 a 的中心與結點 b 的中心在水平方向上對齊,即 b 的位置固定,調整 a 的位置,使得兩者的中心在同一水平線上,只需

halign(a, b);

同理,

valign(a, b);

可將以調整 a 的位置,使得它的中心與 b 的中心在豎直方向對齊。

若以一個結點爲基準,讓一組結點的中心在水平或豎直方向上對齊,能夠利用 MetaPost 的循環語句。例如,以 a 爲基準,將 bcd 等結點的中心與 a 的中心在水平方向上對齊:

forsuffixes i = b, c, d: halign(i, a); endfor;

鏈接

當各個結點的擺放位置肯定以後,考慮的即是它們之間的鏈接。基於恆星定位或行星定位的兩個結點,若它們相鄰,可直接鏈接。對於這種鏈接,Snail 提供了 => 運算符。=> 左側的結點稱爲出射結點,右側的結點稱爲入射結點。=> 會根據出射結點與入射結點的位置肯定一條連線,該連線出射結點的邊框上某條邊的中點出發,沿水平或豎直方向抵達入射結點的邊框。例如

Node a, b;
a := proc("Node a");
b := proc("Node b");
as_planet(b, a, "right");

draw_each a, b;
flow a => b;

Snail 的 flow 宏是 MetaFun 的 drawarrowpath 宏的替代,用於繪製有向路徑。

再看一個例子:

Node a, b, c, abc, d;
a := other("a", fullsquare xysized (2cm, 1cm));
b := other("b", like a);
c := other("c", like a);
as_planet(b, a, "bottom");
as_planet(c, b, "bottom");
abc := a +++ b +++ c;
d := other("d", like abc);
as_planet(d, abc, "right");

draw_each a, b, c, d;
flow_each a => d, b => d, c => d;

flow_eachdraw_each 相似,只不過它繪製的是一組有向路徑。

當兩個結點既不水平排列也不在豎直排列時,兩者的鏈接是折線。可以像 => 那樣自動肯定鏈接路徑是一件很美好的事。然而,Snail 決定不要這種美好。

對於彎曲的路徑,Snail 會沿着咱們當心謹慎地構造的路徑,從出射結點爬到入射結點。該基於出射錨點、前進的方向、前行的距離以及入射錨點而肯定。構造該路徑的過程就是模擬蝸牛的爬行或人的行走。例如,從一個肯定的地點,向東走 100 米,向北走 100 米,向西走 500 米,就這樣轉來轉去,直至抵達目標地點爲止。Snail 的 walk 宏可用於構造這種路徑。

walk 的第一個參數是路徑的起點,第二個參數是由行進方向和距離構造的路徑,第三個參數是終點。起點和終點可由 Snail 的 anchor 宏在源結點和目標結點的邊框上肯定。

若結點的邊框爲矩形,anchor 宏具有在該邊框上肯定任意一點的能力。例如,對於結點 a,其左、右、上、下邊框的中點,可由如下代碼肯定:

anchor(a, "left", 0);
anchor(a, "right", 0);
anchor(a, "top", 0);
anchor(a, "bottom", 0);

anchor 前兩個參數的做用已經很明顯了,它的第三個參數是矩形邊框上的參數座標。對於矩形的每條邊框,參數座標的取值範圍爲 [-0.5, 0.5],中點的參數座標爲 0。

如今 walk 宏的第二個參數描述的是路徑的起點與終點之間的部分,可是隻能由行進方向和距離構成。例如

path p;
p := (0, 0) >>> right * 1cm >>> up * 3cm >>> left * 8cm >>> down * 5cm >>> right * 4cm;

表示從原點開始,向右走 1 cm,再向上走 3 cm,再向左走 8 cm,再向下走 6 cm,再向右走 6 cm。

>>> 是 Snail 實現的運算符,用於銜接各段行進方向及距離的「積」。因爲模擬的是行走,以左右上下做爲行進的方向不夠天然,於是 Snail 定義了一組能夠沿地理方向行進的宏:

  • _e__n__w__s_:向東、北、西、南行進;
  • _E__N__W__S_:向東、北、西、南行進,可是事先會行進 0.5 倍的行星間距;
  • _EE__NN__WW__SS_:向東、北、西、南行進,可是事先會行進 1 倍的行星間距。

基於這些宏,上述路徑 p 可表示爲:

p := (0, 0) >>> (_e_ 1cm) >>> (_n_ 3cm) >>> (_w_ 8cm) >>> (_s_ 5cm) >>> (_e_ 4cm);

當蝸牛很任性地繞着圈子爬行的時候,就能夠走出一條漩渦路徑:

numeric s; path p;
s := 0.25cm; p := (0, 0); 
for i = 1 upto 7:
  for j = "_n_", "_w_", "_s_", "_e_":
    s := s + 0.25cm;
    p := p >>> (scantokens(j) s);
  endfor;
endfor;

如今,能夠爲結點構造折線形式的鏈接了。例如,對於一個結點,以其左邊框的中點爲起點,以其下邊框的中點爲終點,讓路徑自結點上方繞行,

Node a; pair a.out, a.in; path a.self, a.self.go; numeric a.w, a.h;
a := proc("打醬油");
a.out := anchor(a, "right", 0);
a.in  := anchor(a, "bottom", 0);
a.w := _bw_ a; a.h := _bh_ a;
a.self.go := (_E_ 0) >>> (_N_ .5a.h) >>> (_WW_ a.w) >>> (_SS_ a.h) >>> (_E_ .5a.w);
a.self := walk(a.out, a.self.go, a.in);
draw a; flow a.self;

Snail 的宏 _bw__bh_ 只是 MetaFun 宏 bbwidthbbheight 的替代,分別用於獲取結點的寬度與高度。宏 flow 是 MetaFun 宏 drawarrowpath 的替代,用於繪製帶箭頭的路徑。

繪製這樣一條簡單的折線路徑,須要這麼多的代碼,這就是 Snail 繪製流程圖的效率瓶頸。所幸之處在於,對於簡單的流程圖而言,折線路徑並不會太多。偶爾這樣模擬一下蝸牛式的爬行,在諸多以恆星和行星方式定位的結點分佈空間中以折線的方式行走,很像乘坐太空飛船做星際旅行。

假若可以自動爲結點構造一些常規錨點,例如每一個結點邊框上的中點,構造彎曲路徑的代碼即可以獲得一些簡化。Snail 的 enrich 宏可基於給定的結點構造 8 個位於邊框上的錨點,它們皆爲 pair 類型,以給定結點的變量名的後綴形式表示,分別位於這個結點的東(E)、東南(SE)、南(S)、西南(SW)、西(W)、西北(NW)、北(N)、東北(NE)位置。例如,

enrich(a);

結果能夠獲得一組 pair 類型的後綴形式的變量,即 a.Ea.SEa.Sa.SW 等。此外,enrich 還能夠得到結點邊框的寬度和高度,例如 a.widtha.height。利用 enrich 宏,即可以對上述的折線路徑的構造過程予以簡化:

Node a; path a.self;
a := proc("打醬油");
enrich(a);
a.self := walk(a.E, ((_E_ 0) >>> (_N_ .5a.height)
                     >>> (_WW_ a.width) >>> (_SS_ a.height) 
                     >>> (_E_ .5a.width)), a.S);
draw a;
flow a.self;

路徑的標註

因爲 MetaPost 支持以取值範圍爲 [0, 1] 的參數方式在一條路徑上定位,所以利用這一特性,即可以對結點之間的鏈接進行標註。Snail 的 tagged_flow 宏實現了這一功能。對於上一節所構造的路徑 a.self,若在參數爲 0.65 的位置左側增長文本標註,只需用 tagged_flow 取代 flow 宏,

tagged_flow("路過", "left", .65) a.self;

若須要對路徑的標註文本進行旋轉變換,使之與所標註位置的路徑更爲貼合,可利用 ConTeXt 的排版予以實現,MetaFun 的價值由此也得以體現。例如,

tagged_flow("\rotate[90]{路過}", "left", .65) a.self;

全局參數

Snail 預約義了一些全局變量,用於控制流程圖的總體樣式——文本顏色、邊框顏色、邊框背景以及留白等參數。

流程圖各個元素的顏色默認爲:

  • _io_color_ := black:I/O 結點的文本顏色,黑色;
  • _proc_color_ := darkred:過程結點的文本顏色,爲暗紅色;
  • _flow_color_ := .9darkgray:結點鏈接線的顏色,更暗一點的深灰色;
  • _frame_color_ := .7white:過程結點的邊框顏色,淺灰色;
  • _bg_color_ := .9white:過程結點的背景顏色,更淺的淺灰色。

結點邊框和結點連線的默認寬度爲:

_pensize_ := 2.5;

結點連線的寬度和顏色默認設定爲:

drawpathoptions(withpen pencircle scaled _pensize_ withcolor _flow_color_);

I/O 結點和過程結點文本四周留白尺寸默認爲

_pad_ := 4;  _proc_pad_ := 4_pad_;

恆星和行星定位時所用的水平和豎直間距默認爲

_star.s := 4cm; _star.sx := _star.s; _star.sy := .5_star.sx;
_planet.s := .2_star.s; _planet.sx := _planet.s; _planet.sy := _planet.sx;

_margin_ 用於 _E__EE__S__SS_ 等地理方向行進宏的預先行進的距離,默認值爲 .5_planet.s

_expansion_ 用於基於矩形構造與以外接的菱形和橢圓等圖形時,後者的長軸與短軸在矩形的寬度與高度的基礎上放大的倍數,默認值爲 1.25。


引用的文獻:

[1] MetaFun 列傳

[2] Snail 模塊:https://github.com/liyanrui/s...

[3] zhfonts:ConTeXt MkIV 的中文支持模塊

相關文章
相關標籤/搜索