備考某等級考試的時候,在教材中碰到了幾個一直不太理解的、關於硬盤的概念:磁道、柱面號、扇區。然而教材沒有配圖,沒法直觀地瞭解這些概念的物理形態。維基百科的硬盤詞條頁中卻是有一副不錯的示意圖,我截圖搬運了過來node
原圖是一張SVG圖片,本質上是一堆指令——也就是所謂的語繪啦。我是一個語繪愛好者,也想試試看可否用代碼畫一幅差很少的圖出來。git
在舊文《程序員特有的畫圖方式——語繪工具小入門》中,我演示過幾款寫代碼畫圖的工具,但它們都不適合用來繪製幾何圖形,因此此次它們沒有用武之地。程序員
原本我想試試用MetaPost來畫的,但鑑於「入門」了太屢次,此次仍是換點新花樣吧。這一次,我用LaTeX+TikZ來畫。github
著名的壓泡麪神器、麻將桌腳墊《TAOCP》的做者發明了TeX,知名的Raft競品Paxos算法的做者在此基礎上創造了LaTeX,它們都是程序員簡歷論文排版的好幫手。而TikZ則是如虎添翼地在LaTeX中實現了簡單易懂的繪圖功能的一個紅包宏包(macro package,TeX的術語)。簡而言之,TikZ自定義了一套「語言」,能夠在用LaTeX編寫的文檔中畫出各類圖形。算法
百聞不如一見,我演示一下如何用TikZ畫一條線段、一個圓,以及一段圓弧。先將下列的代碼保存到一個文件three_in_one.tex
中編程
\documentclass{standalone} \usepackage{tikz} \usetikzlibrary{shapes.geometric, arrows} \begin{document} \begin{tikzpicture}[scale=2] %% 畫一條從原點指向(1, 1)的線段 \draw (0, 0) -- (1, 1); %% 畫一個以(1, 1)爲圓心,半徑爲2的圓。 \draw (1, 1) circle (2); %% 畫一段以原點爲圓心,半徑爲1,張開角度爲30度的圓弧。 \draw (1, 0) arc (0:30:1); \end{tikzpicture} \end{document}
再使用xelatex
將其編譯成PDF文件(xelatex
能夠經過安裝TeXLive 2020得到)函數
xelatex three_in_one.tex
此時便獲得了three_in_one.pdf
文件。爲了能夠在文章中顯示,我用ImageMagick將其轉換爲PNG文件工具
convert three_in_one.pdf /tmp/three_in_one.png
最終的圖片以下字體
簡單,就像畫一匹馬同樣簡單。spa
如今該來試試用TikZ復刻維基百科上的硬盤示意圖了。
在原圖中最引人注目的,當屬那十幾個同心圓了。簡單起見,我只畫六個圓。這六個圓的半徑相差1pt
(pt
是TikZ默認的長度單位),從3pt
一直遞增到8pt
,它們的圓心都在座標原點(0, 0)
上。
%% 爲了節省篇幅,只給出TikZ部分的代碼。 \begin{tikzpicture} \draw (0, 0) circle (3); \draw (0, 0) circle (4); \draw (0, 0) circle (5); \draw (0, 0) circle (6); \draw (0, 0) circle (7); \draw (0, 0) circle (8); \end{tikzpicture}
原圖中有12根線段,將每個圓等分紅了全等的12份。從前一節的內容可知,要用\draw
命令繪製線段,須要的是線段兩端的座標,那麼這批座標要怎麼計算呢?儘管能夠用三角函數計算出這些點的笛卡爾座標,但在TikZ中能夠用更方便的極座標來指定這些點。
以原圖中從X軸開始逆時針旋轉遇到的第一條線段爲例,它在半徑爲3pt
的圓上的點的座標爲(30:3)
(30是極座標中的角度,3是半徑長度),而在半徑爲8pt
的圓上的點的座標爲(30:8)
,所以能夠用\draw (30:3) -- (30:8)
來畫出這根線段。
經過調整其中的角度能夠畫出剩餘的其它線段。
\begin{tikzpicture} \draw (0, 0) circle (3); \draw (0, 0) circle (4); \draw (0, 0) circle (5); \draw (0, 0) circle (6); \draw (0, 0) circle (7); \draw (0, 0) circle (8); \draw (0:3) -- (0:8); \draw (30:3) -- (30:8); \draw (60:3) -- (60:8); \draw (90:3) -- (90:8); \draw (120:3) -- (120:8); \draw (150:3) -- (150:8); \draw (180:3) -- (180:8); \draw (210:3) -- (210:8); \draw (240:3) -- (240:8); \draw (270:3) -- (270:8); \draw (300:3) -- (300:8); \draw (330:3) -- (330:8); \end{tikzpicture}
原圖大體的骨架已經畫完了,如今來嘗試給它上色。在TikZ中,能夠用\fill
命令給一段封閉的曲線上色。好比用\fill[red] (0, 0) -- (1, 0) -- (1, 1) -- (0, 1) -- cycle
能夠將左下角在原點、邊長爲1pt
的正方形塗成紅色。
先給原圖中的區域B上色。區域B是一個扇形,它由兩根長度爲8pt
的半徑和一段夾角爲30度的圓弧構成。要描述這段封閉曲線,能夠藉助入門一節中介紹的arc
命令。
\begin{tikzpicture} %% 給區域B上色。 \fill[blue] (0, 0) -- (30:8) arc (30:60:8) -- cycle; \draw (0, 0) circle (3); \draw (0, 0) circle (4); \draw (0, 0) circle (5); \draw (0, 0) circle (6); \draw (0, 0) circle (7); \draw (0, 0) circle (8); \draw (0:3) -- (0:8); \draw (30:3) -- (30:8); \draw (60:3) -- (60:8); \draw (90:3) -- (90:8); \draw (120:3) -- (120:8); \draw (150:3) -- (150:8); \draw (180:3) -- (180:8); \draw (210:3) -- (210:8); \draw (240:3) -- (240:8); \draw (270:3) -- (270:8); \draw (300:3) -- (300:8); \draw (330:3) -- (330:8); \end{tikzpicture}
\fill
命令那一行最後的cycle
的意思,是讓曲線回到起點組成一個封閉的形狀。另外,\fill
命令須要寫在\draw
命令以前,是爲了不藍色顏料將區域內的圓弧給蓋住了。
對於區域C和區域D,方法是同樣的,只是描述封閉曲線的座標不一樣罷了。
\begin{tikzpicture} %% 給區域B上色。 \fill[blue] (0, 0) -- (30:8) arc (30:60:8) -- cycle; %% 給區域C上色。 \fill[purple] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4); %% 給區域D上色。 \fill[green] (240:6) -- (240:7) arc (240:330:7) -- (330:6) -- (330:6) arc (330:240:6); \draw (0, 0) circle (3); \draw (0, 0) circle (4); \draw (0, 0) circle (5); \draw (0, 0) circle (6); \draw (0, 0) circle (7); \draw (0, 0) circle (8); \draw (0:3) -- (0:8); \draw (30:3) -- (30:8); \draw (60:3) -- (60:8); \draw (90:3) -- (90:8); \draw (120:3) -- (120:8); \draw (150:3) -- (150:8); \draw (180:3) -- (180:8); \draw (210:3) -- (210:8); \draw (240:3) -- (240:8); \draw (270:3) -- (270:8); \draw (300:3) -- (300:8); \draw (330:3) -- (330:8); \end{tikzpicture}
聰明的讀者也許已經發現了,區域A的環形沒辦法用這種方式來描述。不過不要緊,只要將其視爲上下半兩部分,再分別上色便可。
\begin{tikzpicture} %% 環的上半部分 \fill[red] (4, 0) -- (5, 0) arc (0:180:5) -- (-4, 0) -- (-4, 0) arc (180:0:4); %% 環的下半部分 \fill[red] (4, 0) -- (5, 0) arc (360:180:5) -- (-4, 0) -- (-4, 0) arc (180:360:4); %% 給區域B上色。 \fill[blue] (0, 0) -- (30:8) arc (30:60:8) -- cycle; %% 給區域C上色。 \fill[purple] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4); %% 給區域D上色。 \fill[green] (240:6) -- (240:7) arc (240:330:7) -- (330:6) -- (330:6) arc (330:240:6); \draw (0, 0) circle (3); \draw (0, 0) circle (4); \draw (0, 0) circle (5); \draw (0, 0) circle (6); \draw (0, 0) circle (7); \draw (0, 0) circle (8); \draw (0:3) -- (0:8); \draw (30:3) -- (30:8); \draw (60:3) -- (60:8); \draw (90:3) -- (90:8); \draw (120:3) -- (120:8); \draw (150:3) -- (150:8); \draw (180:3) -- (180:8); \draw (210:3) -- (210:8); \draw (240:3) -- (240:8); \draw (270:3) -- (270:8); \draw (300:3) -- (300:8); \draw (330:3) -- (330:8); \end{tikzpicture}
用macOS的「數碼測色計」看了一下原圖中各個區域的顏色的RGB值,區域A大概是(236, 133, 130)
、區域B大概是(122, 127, 237)
、區域C大概是(131, 132, 139)
、區域D大概是(0, 151, 27)
。接下來我讓TikZ以這四種指定的顏色填充圖中的四個區域,先用LaTeX的\definecolor
命令定義四個新的顏色的名字。
%% 下列四行代碼置於document環境以前 \definecolor{areaA}{RGB}{236,133,130} \definecolor{areaB}{RGB}{122,127,237} \definecolor{areaC}{RGB}{131,32,139} \definecolor{areaD}{RGB}{0,151,27}
再替換掉\fill
命令中的顏色名便可
\begin{tikzpicture} %% 環的上半部分 \fill[areaA] (4, 0) -- (5, 0) arc (0:180:5) -- (-4, 0) -- (-4, 0) arc (180:0:4); %% 環的下半部分 \fill[areaA] (4, 0) -- (5, 0) arc (360:180:5) -- (-4, 0) -- (-4, 0) arc (180:360:4); %% 給區域B上色。 \fill[areaB] (0, 0) -- (30:8) arc (30:60:8) -- cycle; %% 給區域C上色。 \fill[areaC] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4); %% 給區域D上色。 \fill[areaD] (240:6) -- (240:7) arc (240:330:7) -- (330:6) -- (330:6) arc (330:240:6); \draw (0, 0) circle (3); \draw (0, 0) circle (4); \draw (0, 0) circle (5); \draw (0, 0) circle (6); \draw (0, 0) circle (7); \draw (0, 0) circle (8); \draw (0:3) -- (0:8); \draw (30:3) -- (30:8); \draw (60:3) -- (60:8); \draw (90:3) -- (90:8); \draw (120:3) -- (120:8); \draw (150:3) -- (150:8); \draw (180:3) -- (180:8); \draw (210:3) -- (210:8); \draw (240:3) -- (240:8); \draw (270:3) -- (270:8); \draw (300:3) -- (300:8); \draw (330:3) -- (330:8); \end{tikzpicture}
剩下的須要復刻的東西就是原圖中的文字以及標註用的線了。線很容易畫,只要規定了座標後用\draw
命令便可。好比說,我能夠把四條線定義以下,其中的座標和線段的長度純粹是我的偏好
\draw (75:4.5) -- (75:9); \draw (40:7.5) -- (40:9); \draw (50:4.5) -- (50:9); \draw (285:6.5) -- (285:9);
線畫完了,再到每一根線的「終點」標上文字說明,這須要用到TikZ的node
功能。用法很簡單,就是在須要標註文字的座標後,緊跟着關鍵字node
,以及一段用花括號包裹的文本便可
\documentclass{standalone} \usepackage{tikz} \usepackage{xeCJK} \setCJKmainfont{Songti TC} \usetikzlibrary{shapes.geometric, arrows} \definecolor{areaA}{RGB}{236,133,130} \definecolor{areaB}{RGB}{122,127,237} \definecolor{areaC}{RGB}{131,32,139} \definecolor{areaD}{RGB}{0,151,27} \begin{document} \begin{tikzpicture} %% 環的上半部分 \fill[areaA] (4, 0) -- (5, 0) arc (0:180:5) -- (-4, 0) -- (-4, 0) arc (180:0:4); %% 環的下半部分 \fill[areaA] (4, 0) -- (5, 0) arc (360:180:5) -- (-4, 0) -- (-4, 0) arc (180:360:4); %% 給區域B上色。 \fill[areaB] (0, 0) -- (30:8) arc (30:60:8) -- cycle; %% 給區域C上色。 \fill[areaC] (30:4) -- (30:5) arc (30:60:5) -- (60:4) -- (60:4) arc (60:30:4); %% 給區域D上色。 \fill[areaD] (240:6) -- (240:7) arc (240:330:7) -- (330:6) -- (330:6) arc (330:240:6); \draw (0, 0) circle (3); \draw (0, 0) circle (4); \draw (0, 0) circle (5); \draw (0, 0) circle (6); \draw (0, 0) circle (7); \draw (0, 0) circle (8); \draw (0:3) -- (0:8); \draw (30:3) -- (30:8); \draw (60:3) -- (60:8); \draw (90:3) -- (90:8); \draw (120:3) -- (120:8); \draw (150:3) -- (150:8); \draw (180:3) -- (180:8); \draw (210:3) -- (210:8); \draw (240:3) -- (240:8); \draw (270:3) -- (270:8); \draw (300:3) -- (300:8); \draw (330:3) -- (330:8); \draw (75:4.5) -- (75:9) node {磁道}; \draw (40:7.5) -- (40:9) node {扇面}; \draw (50:4.5) -- (50:9) node {扇區}; \draw (285:6.5) -- (285:9) node {簇}; \end{tikzpicture} \end{document}
須要留意的是,我在源代碼開頭的位置,引入了xeCJK
宏包(\usepackage{xeCJK}
),而且指定了中文內容用的字體爲宋體(\setCJKmainfont{Songti TC}
),這樣才能成功編譯。
至此,復刻算是完成了。
本文只是管中窺豹,TikZ還能夠畫出其它更復雜更美輪美奐的圖形,有興趣的讀者能夠移步這裏觀賞。此外,TikZ也能夠「編程」,好比下面的兩行代碼便足矣畫出上文中12行代碼才完成的等分線
\foreach \x in {0,30,60,90,120,150,180,210,240,270,300,330} \draw (\x:3) -- (\x:8);
TikZ的更多潛力和樂趣,就由各位讀者本身探索吧。