【日报】TeX 宏包 TikZ 入门指北(一)

囧仙

2021-02-10 22:52:23

Tech. & Eng.

引子

那么恭喜你,通过使用 \textrm{Ti}\textit{k}\textrm{Z} 宏包,你只需要写一些简洁易懂的代码,就可以实现高效精准地绘制出一张令人满意的矢量图。

文档狂魔 $\text{Till Tantau}$ 已经准备了一份内容详实的 $1300+$ 页的用户手册(其中的教程部分,就已经达到了 $300$ 页)。本文只是作为一篇 $\textrm{Ti}\textit{k}\textrm{Z}$ 指北,讲解其**最基础**、**最简单**的应用,以让读者简单领略到它的魅力。本文会使用大量插图,因此可能带来一定卡顿,请读者谅解。由于使用 $\text{pdf}\TeX$ 生成的文档都是 $\text{pdf}$ 类型,因此图片都是使用 [$\textsf{pdf2png.com}$](https://pdf2png.com/) 转换成 $\text{png}$ 类型后,再上传到洛谷图床的,图片质量存在一定损失(毕竟原来是矢量图嘛)。同时为了防止图片占据过大篇幅,因此进行了一些缩小。如果想要查看完整图片,请使用“在新标签页打开图片”。 > 热知识:$\textrm{Ti}\textit{k}\textrm{Z}$ 的全称,为: > $$\large\text{Ti\textit{k}Z ist \textit{kein} Zeichenprogramm}$$ > 译为中文即为“ $\textrm{Ti}\textit{k}\textrm{Z}$ 不是一个绘图软件”,与 $\text{GNU}$ 的全称 $\text{``GNU's Not Unix''}$ 有异曲同工之妙。这些程序员就喜欢玩这种文字游戏。 ## 准备工作 自然而然地,作为 $\TeX$ 系软件($\TeX,\LaTeX$ 等等)的宏包,你需要准备 $\TeX,\LaTeX,\text{Con}\TeX\text{t}$ **其中之一**。接着你需要[下载 $\textrm{Ti}\textit{k}\textrm{Z}$ 宏包](https://www.ctan.org/pkg/pgf)(有些安装包里会自带,比如 $\TeX\ \text{Live},\text{Mik}\TeX$)。在下文里,默认使用的是 $\LaTeX$。不同软件的宏包导入等等可能略有不同,可能需要参考官方的用户手册。由于该文章是用来介绍 $\textrm{Ti}\textit{k}\textrm{Z}$ 的,因此不会说明怎么安装 $\LaTeX$。 --- $\textrm{Ti}\textit{k}\textrm{Z}$ 可以在各种各样的 $\text{document class}$ 里面被使用。如果你是要写一篇文章,那么用 $\verb!article!$ 就足矣;如果你只是为了生成一张独立的图片,那么我推荐使用 $\verb!standalone!$,这样生成的文档就是一张图片了。对于后者,这里还推荐加上一个 $\verb!border=Xpt!$(其中 $\verb!X!$ 代指你希望得到的图片的白色边框的宽度),这样可以使得生成的图片不至于与文章内容过度紧密。例如: ```latex \documentclass[border=4pt]{standalone} %生成为单独图片,宽度为 4pt % \documentclass{article} %生成为单独的文档 ``` 接下来,肯定是要导入该宏包了: ```latex \usepackage{tikz} ``` 当然了,由于 $\textrm{Ti}\textit{k}\textrm{Z}$ 的体量过于庞大,有时候你得用到其名下的一些子库,例如: ```latex \usetikzlibrary{arrows} \usetikzlibrary{shadows} ``` 一张图片应该在一个单独的环境里。$\textrm{Ti}\textit{k}\textrm{Z}$ 所提供的图片环境为 $\verb!tikzpicture!$。当然了,你也可以直接使用一条 $\verb!tikz!$ 指令来创建一个环境。 ```latex \begin{tikzpicture} % do something... \end{tikzpicture} \tikz % do someting... ``` 以下绘制图片的代码,**若无特殊说明则都在** $\verb!tikzpicture!$ **环境下**。同时为了防止图片过小,加上了 $\verb![scale=1.5]!$ 用来放大图片。 > 为了调制颜色,我们还会用到 $\verb!xcolor!$ 宏包,它可以混合不同颜色得到新的颜色;同时为了支持中文,需要用到 $\verb!ctex!$ 宏包;为了使用数学模式的一些记号,需要用到 $\verb!amsmath!$ 宏包。这三个宏包不是本文的重点,而且不会妨碍文章的理解。 ## 从平面直角坐标系说起 这是一个经典的例子(毕竟 $\textrm{Ti}\textit{k}\textrm{Z}$ 内部也实现了一套自己独有的坐标系系统)。假如我们现在需要绘制这样的一张图片: ![img](https://cdn.luogu.com.cn/upload/image_hosting/yfqzsmbr.png) > 该图是用来说明单位圆与三角函数之间的关系的。放在这里是用来介绍 $\textrm{Ti}\textit{k}\textrm{Z}$ 的简要绘图方式。 ### 绘制线段 一幅图片,当然要由线段组成了。在 $\textrm{Ti}\textit{k}\textrm{Z}$ 内部,实现了一套坐标系的规则。也就是说,我们可以用一个二元组 $(x,y)$ 描述该平面直角坐标系上的一个点。若想绘制线段,你只需要使用 $\verb!\draw!$ 命令,将两个坐标用两个连字符(或者说,减号)相连,那么生成的图片上就能看到它们的连线了。此外,**你可以使用一些参数调整线段的宽度**。 ![img](https://cdn.luogu.com.cn/upload/image_hosting/ioy3u49f.png) (由于篇幅限制,我把三张图片放在了一张图片里显示) ```latex \draw [very thin] (-1.8,-1.5) -- (1.8,-1.5); % 最细的线 \draw [thin] (-1.8,-0.5) -- (1.8,-0.5); % 较细的线 \draw [thick] (-1.8, 0.5) -- (1.8, 0.5); % 较粗的线 \draw [very thick] (-1.8, 1.5) -- (1.8, 1.5); % 最粗的线 ``` 第一张图片展示了如何使用预设的四种宽度生成不同粗细的线段。 > 当然了,在 $\textrm{Ti}\textit{k}\textrm{Z}$ 里存在一些方式自定义线段的宽度。事实上,对于箭头等内容,$\textrm{Ti}\textit{k}\textrm{Z}$ 也提供了一套内容丰富的方法进行自定义。但是由于本文属于入门文章,因此这些相对复杂的内容不会提及。下同。 第二张图里,生成了一些虚线和带有箭头的线段。这是通过在参数里添加一些带有**装饰**成分的参数来实现的。这部分的代码如下: ```latex \draw [thick,dashed,-] (-1.8,-1.5) -- (1.8,-1.5); % 虚线,无箭头 \draw [thick,dashed,->] (-1.8,-0.5) -- (1.8,-0.5); % 虚线,右箭头 \draw [thick,<->] (-1.8, 0.5) -- (1.8, 0.5); % 左右都有箭头 \draw [thick,<<<-><<>>] (-1.8, 1.5) -- (1.8, 1.5); % 一大堆箭头 ``` 可以发现,使用 $\verb!dashed!$ 标记的线段就是虚线,而通过加上一些左右括号,就可以指明线段左右两端的箭头应该是什么样了。 对于第三张图,这里展示了如何一次性画出多条头尾相接的线段(其实是一条路径。在这里可以称呼为折线)。它的方法也很简单,只要在上个坐标后面继续加上新的坐标,以此类推。 ```latex \draw[->] (-1.5,-1.5) -- (0.6,1.5) -- (1.2,-1.5) -- (-1.5,1.5); % 连接多个点 ``` ### 辅助网格 在目标图片 $y$ 轴的右侧,出现了淡灰色的用于辅助定位的网格。使用刚刚讲解的绘制线段的方法,当然可以一条条地画出这样的线段,但这无疑是极为繁琐的。事实上,在 $\textrm{Ti}\textit{k}\textrm{Z}$ 里提供了一种方式**快速地**绘制出一个矩形区域内的网格: ![img](https://cdn.luogu.com.cn/upload/image_hosting/1j763vbm.png) ```latex \draw [step=0.5,help lines] (-2.1,-2.1) grid (2.1,2.1); ``` 其中,$\verb!help lines!$ 指明了网格线的样式(粗细、颜色等等),这是被预定义好的。接着我们指定了网格线的范围,即左下角为 $(-2.1,-2.1)$,右上角为 $(2.1,2.1)$ 的矩形区域(使用 $\verb!grid!$ 进行连接)。最后,我们指定每两条平行的网格线之间的距离 $\verb!step!$。它被设定为 $0.5$ 个单位长度。 ### 标上序号 我们发现:坐标系上除了有坐标轴和网格线,还有标在 $x$ 轴正半轴顶端的字母 $x$、标在 $y$ 轴正半轴顶端的字母 $y$。如何在图片上书写他们呢?事实上,除了 $\verb!grid!$,$\textrm{Ti}\textit{k}\textrm{Z}$ 还提供了一种可以写在 $\verb!\draw!$ 指令参数里的关键词 $\verb!node!$,可以用来在当前位置(把绘制过程想象为用钢笔描线,那么当前位置就是笔尖所在位置)专门用来绘制这种节点。例如, ![img](https://cdn.luogu.com.cn/upload/image_hosting/3yle9lfi.png) ```latex \draw[help lines,step=.3] (0,-1.1) grid (2*pi+.1,1.1); \draw[->,thick] (-2.7,0) -- (2*pi+.2,0) node[anchor=north] {$x$}; \draw[->,thick] (0,-1.2) -- (0,1.2) node[anchor=east] {$y$}; ``` 注意这句话:`[anchor=east]`。它是给 $\verb!node!$ 的一个可选参数,也就是**当前位置**应该在**该节点**的哪个方向(上北下南左西右东,和地图差不多。此外,不要搞混相对位置关系)。后面的花括号里,就是该节点上面的文字。可以为空,但是需要保留空的花括号。 此外,直接使用 $\verb!\node!$ 指令也可以定义一个节点。使用关键词 $\verb!at!$ 指定当前位置的坐标。例如,`\node at (1,1) {$a$};`。下文中我们将会用到它。 ### 循环 我们确实是部分解决了标号的问题。但是坐标轴上的序号可能很多,一个个标定可能相对复杂,况且调整单位间距、坐标轴长度后进行修改会变得很麻烦。幸好,我们可以使用 $\verb!\foreach!$ 指令进行简单的循环。给出一个例子: ![img](https://cdn.luogu.com.cn/upload/image_hosting/ljp33at8.png) ```latex \foreach \x in {-1.8,-1.5,...,1.8}{ \filldraw[thick,draw=red!30!black,fill=red!50!white] (\x,\x) circle [radius=.12]; } ``` > $\verb!\filldraw!$ 的用法在下面会讲解。在这里你只需要知道这句话可以在 $(\verb!\x!,\verb!\x!)$ 处绘制一个实心圆即可。 我们定义了循环变量 $\verb!\x!$(注意要有反斜杠),它的取值是 $-1.8,-1.5,\cdots,1.8$。然后我们在一对花括号里使用该循环变量进行了一些运算。取值范围内部存在一种等差数列的机制。上述取值范围相当于如下 $\text{c++}$ 代码片段: ```cpp for(int x=-1.8,d=(-1.5)-(-1.8);x<=1.8;x+=d) ``` > 当然了,对于公差为负值的情况,那就等价于把小于等于符号换成了大于等于。 使用它,我们可以轻易地执行重复性的工作。那么我们可不可以进行循环上面的嵌套呢?答案是可以的。请看下面一个例子: ![img](https://cdn.luogu.com.cn/upload/image_hosting/xvas741o.png) ```latex \foreach \x in {-1.6,-0.8,...,1.6} \foreach \y in {-1.6,-0.8,...,1.6}{ \draw (\x-.3,\y-.3) rectangle (\x+.3,\y+.3); \node at (\x,\y+.14) {$\x$}; \node at (\x,\y-.14) {$\y$}; } ``` > 该例子同样展示了,在没有花括号的情况下它的作用域是紧接着的一句代码。这和 $\text{c++}$ 十分类似。 不过,$\verb!\foreach!$ 还提供了一种特殊的语法。我们可以一边让 $x$ 进行循环,在另外一边定义一个变量 $y$ 和 $x$ 同步进行循环。也就是当 $x$ 取某个值时,$y$ **同步地**取另外一个值,最终循环次数和 $x$ 相同。例如这样一个例子: ![img](https://cdn.luogu.com.cn/upload/image_hosting/t5pk6zq2.png) ```latex \foreach \x/\y in {-1.6/-0.8,-0.8/1.6,0,0.8/-1.6,1.6/0.8} \filldraw[fill=green!40!white,draw=green!60!black] (\x,\y) circle[radius=.3]; ``` 使用一个斜杠隔开了 $\verb!\x!$ 和 $\verb!\y!$,这样使得它们同步循环。取值范围内也同样使用了斜杠,分割开了两者的取值。注意到,当 $x$ 取 $0$ 的时候我们并没有写出 $y$ 的取值,此时 $y$ 的值会变得和 $x$ 一样。 我们成功绘制出了目标图像的第二部分: ![img](https://cdn.luogu.com.cn/upload/image_hosting/37vp1xbe.png) ```latex \draw[help lines,step=.3] (0,-1.1) grid (2*pi+.1,1.1); \draw[->,thick] (-2.7,0) -- (2*pi+.2,0) node[anchor=north] {$x$}; \draw[->,thick] (0,-1.2) -- (0,1.2) node[anchor=east] {$y$}; \draw (-.05, 1) -- (.05, 1) node[anchor=east] at(0, 1) {$1$}; \draw (-.05,-1) -- (.05,-1) node[anchor=east] at(0,-1) {$-1$}; \foreach \x/\y in {0.5/\frac{1}{2},1/,1.5/\frac{3}{2},2/2} \draw (\x*pi,-.05) -- (\x*pi,.05) node at(\x*pi,-.2) {$\y\pi$}; ``` > $\verb!\foreach!$ 里支持一些简单的运算。例如,上述代码中我们用 $\verb!\x*pi!$ 计算出了 $x\cdot \pi$ 的值。 ### 绘制图形 那么我们的主角,单位圆登场了。在 $\textrm{Ti}\textit{k}\textrm{Z}$ 里有这样的一套**已经被定义**的方法用来绘制不同的图形,例如圆形、矩形、椭圆。下面将会讲解它们的参数: ![img](https://cdn.luogu.com.cn/upload/image_hosting/u4e4ynph.png) ```latex \draw (0,0) circle[radius=1.5]; ``` 对于圆形,我们首先规定了它的圆心坐标 $(0,0)$,接着取它的半径为 $1.5$ 个单位长度。那么我们绘制出了以 $(0,0)$ 为圆心,$1.5$ 单位长度为半径的圆。 ```latex \draw (-1.2,-1.5) rectangle (1.2,1.5); ``` 对于矩形,我们首先给出了它的一个顶点的坐标 $(-1.2,-1.5)$,接着给出它的另外一个顶点坐标 $(1.2,1.5)$,那么就在这两个坐标之间,绘制出了这样一个矩形。 ```latex \draw (0,0) ellipse[x radius=1.8,y radius=1.1]; ``` 最后是椭圆。同样地,它的中心为 $(0,0)$,接着给出了它的长半轴为 $1.8$ 单位长度,短半轴为 $1.1$ 单位长度,绘制出了这样的一个椭圆。 ### 填充颜色 有时候我们想要给画出的线段上色,又有时我们想要给线段当中的区域涂色(但是线段不上色),也有时候两者都要涂色。光使用上文的代码达不到目的。$\textrm{Ti}\textit{k}\textrm{Z}$ 提供了 $\verb!fill!$ 用于填充路径当中区域的颜色,而 $\verb!filldraw!$ 则可以兼顾两者: ![](https://cdn.luogu.com.cn/upload/image_hosting/1c1yqtiv.png) 对于第一张图,我们通过使用 $\verb!draw!$ 指令,$\verb!color!$ 选项给线段上了色。 ```latex \draw[color=red,very thick,->] (0,0) -- (1.5,0) -- (1.5,1.5) -- (-1.5,1.5) -- (-1.5,-1.5) -- (1.5,-1.5); ``` 对于第二张图,我们通过使用 $\verb!fill!$ 指令,$\verb!color!$ 选项给内部区域上了色。 ```latex \fill[color=red!30!white] circle [radius=1.5]; ``` 对于第二张图,我们通过使用 $\verb!filldraw!$ 指令,用 $\verb!draw!$ 选项给线段上了色,用 $\verb!fill!$ 选项给内部区域上了色。 ```latex \filldraw[fill=red!30!white,draw=red!50!black,very thick] circle [radius=1.5]; ``` ### 极坐标系 除了上述提到的笛卡尔坐标系,$\textrm{Ti}\textit{k}\textrm{Z}$ 额外支持了以原点为中心,$x$ 轴正半轴为极坐标轴的极坐标系。与使用一对实数对来描述坐标不同,我们通过使用极角和该点离原点的距离描述了它的位置。例如: ![img](https://cdn.luogu.com.cn/upload/image_hosting/cranbaxi.png) ```latex \draw[help lines,step=.4] (-2,-2) grid (2,2); \draw[help lines] (0,0) circle[radius=.5] circle[radius=1] circle[radius=1.5] circle[radius=2]; \draw[thick,->] (0,0) -- (2,0); \node at (2,-.2) {$x$}; \draw[thick,color=red,->] (0:0)--(30:2) node[anchor=north]{$(2,30^\circ)$}; \draw[thick,color=red,->] (0:0)--(110:1.5) node[anchor=south]{$(1.5,110^\circ)$}; \draw[thick,color=red,->] (0:0)--(-90:1.5) node[anchor=north]{$(1.5,-90^\circ)$}; ``` 由此我们可以得到一种绘制角度的方法。当我们知道了该角度的起始度数和终止度数,以及它的半径长度后,就能描绘一个角度: ```latex \draw (0,0) arc [start angle=0,end angle=30,radius=.3]; ``` 由于与圆之类的类似,这里就不给出演示图片了。 此外,我们提到的形如 $\verb!north!,\verb!south!,\verb!west!,\verb!east!$ 等词,是可以用对应的极角来替代的。例如,这四个单词对应的极角分别为 $90,-90,180,0$。非常方便的是,$\textrm{Ti}\textit{k}\textrm{Z}$ 当中的极角会自动取模变为 $[0,360)$ 之间。换句话说,形如 $x+360k\quad(k\in\Bbb Z)$ 的角度与 $x$ 等价。 ### 位移 上文我们也提到,描绘一条路径的过程可以看作钢笔沿着规定的轨迹书写的过程。那么有没有办法更改当前笔尖所在位置呢?答案是可以的。请看下面这个例子: ![](https://cdn.luogu.com.cn/upload/image_hosting/1g8w2p1p.png) ```latex \draw[->,very thick,color=red] (0,0) -- +( 60:1.5) -- +(120:1.5) -- +(180:1.5) -- +(240:1.5) -- +(300:1.5) -- +(360:1.5); \draw[->,very thick,color=blue] (0,0) -- ++(2, 0) -- ++(0,1) -- ++(-4,0) -- ++(0,-2) -- ++(4,0); \draw[->,very thick,color=orange] (0,0) -- ++(-1,0) -- +(30:1.5) -- +(-30:1.5) -- ++( 0,0); ``` 在坐标前使用了加号后,**钢笔不会移动**,但是此处描述的点则是钢笔当前位置**加上了**该坐标(理解为向量加法),例如红色线段。而使用了两个加号,**钢笔跟着移动**,移动到的位置即为原先位置加上了该坐标(同样是向量加法),例如蓝色线段。这两者是可以混合使用的,例如橙色线段。 ### 采样描点 目标图片当中绘制了一幅 $\sin$ 函数图像。根据 $\textrm{Ti}\textit{k}\textrm{Z}$ 所预设的几个图案,可能无法绘制出对应的曲线。但是强大的 $\textrm{Ti}\textit{k}\textrm{Z}$ 提供了另外一种方法:根据对应的函数表达式在函数上取样本点,再使用线段依次连接,此时我们就能得到对应的函数图像。例如, ![img](https://cdn.luogu.com.cn/upload/image_hosting/3t08gpy0.png) ```latex \draw[help lines,step=.3] (0,-1.1) grid (2*pi+.1,1.1); \draw[->,thick] (-2.7,0) -- (2*pi+.2,0) node[anchor=north] {$x$}; \draw[->,thick] (0,-1.2) -- (0,1.2) node[anchor=east] {$y$}; \draw (-.05, 1) -- (.05, 1) node[anchor=east] at(0, 1) {$1$}; \draw (-.05,-1) -- (.05,-1) node[anchor=east] at(0,-1) {$-1$}; \foreach \x/\y in {0.5/\frac{1}{2},1/,1.5/\frac{3}{2},2/2} \draw (\x*pi,-.05) -- (\x*pi,.05) node at(\x*pi,-.2) {$\y\pi$}; \draw[thick] (-1.5,0) circle[radius=1]; \filldraw[draw=green!50!black,fill=green!50!white] (-1.5,0) -- +(.3,0) arc [start angle=0,end angle=30,radius=.3] -- cycle; \path (-1.5,0) ++ (15:.5) node[color=green!80!black] {$30^\circ$}; \draw[samples=50,domain=0:2*pi,very thick,color=red!70!white] plot(\x,{sin(\x r)}); ``` > 这里用到了 $\verb!cycle!$ 关键词来闭合一条路径。它会使得当前位置自动与路径的起点相连接。 绘制正弦函数,用了这样一段代码: ```latex \draw[samples=50,domain=0:2*pi,very thick,color=red!70!white] plot(\x,{sin(\x r)}); ``` 我们采集了 $50$ 个样本点。定义域为 $[0,2\pi]$,函数是 $\sin(x)$。要注意的是,$\sin$ 函数默认接受的是角度制($[0,360^\circ)$ 为一圈),而我们的 $x$ 的值采用的是弧度制($[0,2\pi)$ 为一圈),因此需要加上符号 $\verb!r!$ 将其进行转换。 一些其他的例子: ![img](https://cdn.luogu.com.cn/upload/image_hosting/lqobryks.png) ```latex \draw[samples=50,domain=-1.5:1.5] plot(\x,{\x*\x-1.2}); \draw[samples=50,domain=-1.5:1.2] plot(\x,{exp(\x)-2}); \draw[samples=50,domain=-1.5:1.5] plot(\x,{sin(\x r)}); ``` --- 在绘制了正弦函数以后,再加上一些线段,得出目标图像: ![img](https://cdn.luogu.com.cn/upload/image_hosting/yfqzsmbr.png) ```latex \documentclass[border=4pt]{standalone} \usepackage{xcolor,amsmath,tikz,lmodern} \begin{document} \begin{tikzpicture}[scale=2] \draw[help lines,step=.3] (0,-1.1) grid (2*pi+.1,1.1); \draw[->,thick] (-2.7,0) -- (2*pi+.2,0) node[anchor=north] {$x$}; \draw[->,thick] (0,-1.2) -- (0,1.2) node[anchor=east] {$y$}; \draw (-.05, 1) -- (.05, 1) node[anchor=east] at(0, 1) {$1$}; \draw (-.05,-1) -- (.05,-1) node[anchor=east] at(0,-1) {$-1$}; \foreach \x/\y in {0.5/\frac{1}{2},1/,1.5/\frac{3}{2},2/2} \draw (\x*pi,-.05) -- (\x*pi,.05) node at(\x*pi,-.2) {$\y\pi$}; \draw[thick] (-1.5,0) circle[radius=1]; \filldraw[draw=green!50!black,fill=green!50!white] (-1.5,0) -- +(.3,0) arc [start angle=0,end angle=30,radius=.3] -- cycle; \path (-1.5,0) ++ (15:.5) node[color=green!80!black] {$30^\circ$}; \draw[thick] (-1.5,0) -- +(30:1) -- (pi/6,.5); \draw[thick,dashed] (pi/6,.5) -- (pi/6,0); \node at (pi/6,-.2) {$\frac{1}{6}\pi$}; \draw[samples=50,domain=0:2*pi,very thick,color=red!70!white] plot(\x,{sin(\x r)}); \end{tikzpicture} \end{document} ``` ## 简单数学计算 从这里开始,我们将会引入一些 $\textrm{Ti}\textit{k}\textrm{Z}$ 的库文件($\text{libraries}$),帮助我们进行一些向量间、图形间的运算。在本章节里,我们希望实现这样的一副图案: ![img](https://cdn.luogu.com.cn/upload/image_hosting/aphrgsdb.png) > 这是经典的五点共圆问题。在平面上随机生成一个五角星,它的顶点分别为 $A,B,C,D,E$。接着分别以五个角上的三角形作出五个**外接圆**,外接圆之间会产生一些交点。可以证明,外侧的五个交点**共圆**。 看上去我们要做的事情实在是太多了……不着急,让我们馒馒来,一步步写出生成这幅图片的代码。 ### 生成随机点 尽管我们确实可以拿起计算器生成一些随机数并手动输入,但显然这种方法对于要求的随机数较多的情况下是不合适的。$\textrm{Ti}\textit{k}\textrm{Z}$ 里提供了一个函数 $\verb!rand!$,它可以随机生成 $[-1,1)$ 内的随机数。同样地,这里给出一个例子: ![img](https://cdn.luogu.com.cn/upload/image_hosting/7s7vmka5.png) 这幅图片描绘了在 $(-1,1),(1,1),(1,-1),(-1,-1)$ 为顶点的正方形内随机生成 $501$ 个随机点的情况。代码如下: ```latex \draw (-1,-1) rectangle (1,1); \draw[help lines,step=.2] (-1.3,-1.3) grid (1.3,1.3); \foreach \x in {0,0.1,...,50}{ \fill[color=red!50!] (rand,rand) circle[radius=.02]; } ``` 每当 $\verb!rand!$ 被用于计算当中,它就会返回一个随机数。 在 $\textrm{Ti}\textit{k}\textrm{Z}$ 里,我们可以通过 $\verb!\coordinate!$ 指令来声明一个坐标,同时可以给该坐标一个名字作为标识。此外,通过可选选项 $\verb!label!$,可以显式地写出该坐标的标签,格式为 $\verb![label=方向:内容]!$。这里面的方向可以为 $\verb!left!,\verb!right!,\verb!above!,\verb!below!$ 等方位代词,也可以是一个角度($45,57,200$ 等)。注意与之前提到的 $\verb!anchor!$ 相区别。结合 $\verb!rand!$ 函数,我们可以生成五角星的五个点: ![img](https://cdn.luogu.com.cn/upload/image_hosting/3x58fp7q.png) ```latex \coordinate (O) at(0,0); \coordinate[label=90+72: $A$] (A) at (90+72 +rand*5:2+rand*.2); \coordinate[label=90+144:$B$] (B) at (90+144+rand*5:2+rand*.2); \coordinate[label=90+216:$C$] (C) at (90+216+rand*5:2+rand*.2); \coordinate[label=90+288:$D$] (D) at (90+288+rand*5:2+rand*.2); \coordinate[label=90+360:$E$] (E) at (90+360+rand*5:2+rand*.2); ``` 同时还可以把它们连边: ![img](https://cdn.luogu.com.cn/upload/image_hosting/igovz228.png) 看上去一切都进展地很顺利……然后呢?我们怎样做出交点,怎样做出外接圆,又怎样选取最外面的一圈点画出最终的圆呢? ### 交点 这里说明一下之前提到的 $\verb!\draw!,\verb!\fill!,\verb!\filldraw!$ 的本质。它们都是 $\verb!\path!$ 指令的**变体**(事实上,$\verb!\node!$ 也可以看作继承了 $\verb!\path!$ 的相关特性),区别在于 $\verb!\draw!$ 指令必然会画出那条线段,$\verb!fill!$ 指令必然会填充线段中间的内容,$\verb!filldraw!$ 则会把两件事情都干了。$\verb!\path!$ 指令可以有两个可选参数 $\verb!draw!$ 和 $\verb!fill!$,决定了是否绘制线段、是否填充内容。 为什么要提这个呢?因为我们所说的交点,就是两个 $\verb!path!$ 上的公共点。请看下面这个例子: ![img](https://cdn.luogu.com.cn/upload/image_hosting/evg2nq6y.png) ```latex % 需要在 \begin{document} 前加上 \usetikzlibrary{intersections} \draw[name path=circ] (-1.5,0) circle[radius=1.8]; \draw[name path=rect] (0,1.8) rectangle (3,-1.8); \path[name intersections={of=circ and rect}]; \coordinate [label=180:$P$](P) at (intersection-1); \coordinate [label=180:$Q$](Q) at (intersection-2); ``` 这里,我们需要引入库 $\verb!intersections!$。它可以被用来处理交点。 我们首先是绘制了两个 $\verb!path!$,**并且分别命名为** $\verb!circ!$ 和 $\verb!rect!$,即我们需要求出交点的圆形和矩形。接着,通过使用 `name intersections={of=circ and rect}`,我们让 $\textrm{Ti}\textit{k}\textrm{Z}$ 计算了两个 $\verb!path!$ 一系列的交点。所有交点自动被**依次命名为** $\verb!intersection-1!,\verb!intersection-2!,\cdots$。被命名的交点个数,恰好是这两个 $\verb!path!$ 所有的交点个数。接着这段话定义了点 $P,Q$,位置分别是这两个交点。 值得注意的是,交点的序号可能是随机生成的(也就是说,尽管这张图片上 $P$ 在 $Q$ 的上方,但我们不能总是保证这样的语法生成的 $P$ 一定在 $Q$ 的上方)。这也就是为什么有时候我们得用一些判断语句来判断我们到底需要哪些点。 ### 向量运算 通过一些语法,我们可以进行向量之间的计算: ![img](https://cdn.luogu.com.cn/upload/image_hosting/90p3nw71.png) ```latex % 需要在 \begin{document} 前加上 \usetikzlibrary{calc} \coordinate[label=210:$\color{red} A$] (A) at (-3,-1); \coordinate[label= 0:$\color{red} B$] (B) at (2.5,1.5); \coordinate[label= 0:$\color{blue}C$] (C) at (2,-1.5); \draw [thick,color=red!50!] (A) -- (B); \node [anchor=260,color=red] at ($(A)!.5!(B)$) {$M$}; \node [anchor=260,color=red] at ($(A)!.3!(B)$) {$P$}; \node [anchor=260,color=red] at ($(A)!.7!(B)$) {$Q$}; \draw [thick,color=blue!50!] ($(A)!.3!(B)$) -- (C); \node [anchor= 50,color=blue] at ($(A)!.3!(B)!.5!(C)$) {$X$}; \draw [thick,color=orange] ($(A)!.5!(B)$) -- ($(A)!.5!(B)!10mm!270:(A)$) node[anchor=0] {$H$}; ``` 知识点比较多,让我们慢慢讲。 $\textrm{Ti}\textit{k}\textrm{Z}$ 里定义了一种特殊的句式符号 $\mathtt{(\$\cdots\$)}$ 用来执行向量间的运算。例如,`($0.5*(1,1)+0.5*(2,2)$)` 能够得到 $(1,1)$ 与 $(2,2)$ 中点的坐标。注意,作为系数的数字必须要在坐标前面,放在后面可能会出现错误。不过,$\textrm{Ti}\textit{k}\textrm{Z}$ 还支持一些类似于 $\verb!xcolor!$ 宏包,来“混合坐标”。 - 这个例子里 $M,P,Q$ 三个点坐标的诞生,验证了 $\mathtt{(\$坐标1!数值!坐标2\$)}$ 的语法可以混合两个坐标,新的坐标在这两个坐标连线的直线上,偏向坐标 $1$ 的程度取决于数值的大小(当然,数值是可以小于 $0$ 或者大于 $1$ 的。这也就是为什么说“新的坐标在这两个坐标连线的直线上”而非线段上)。 - 一种扩展语法是,$\mathtt{(\$坐标1!数值!角度:坐标2\$)}$。它的作用却不再是起到“混合”作用,而是将坐标 $1$ 到坐标 $2$ 的线段,绕着坐标 $1$ 旋转了该角度,同时它的长度变为该数值。橘黄色的线段可以作为例子。 - 容易发现,这两种运算是**左结合**的。也就是说,从左往右进行运算,上一个运算的结果将会作为下一个运算的一个参数(即上文提到的坐标 $1$)。橘黄色的线段和蓝色的线段都可以作为例子。 有了这些语法,我们可以轻易地求出一条线段的中垂线啦。 ### 外接圆 综合使用刚刚讲述的求交点的方法以及向量间的简单运算,我们可以得到两个点组成的线段的中垂线,并且计算出三角形两边中垂线的交点,作为求得的外心。 > 其实这种求出圆心的方式存在缺陷。如果求出来的中垂线不够长/方向不对,可能是**不能形成交点**的,然后就会报错……直接使用公式计算出外心可能更为安全。不过在我们的讨论范围内,认为生成的中垂线足够长,而不会发生这些问题。 但是光有外心不够啊,我们要的是那个圆。这时候万能的 $\textrm{Ti}\textit{k}\textrm{Z}$ 提供了库 $\verb!through!$,它可以提供让圆形通过指定坐标的方式。 ![img](https://cdn.luogu.com.cn/upload/image_hosting/2sjvxrw4.png) ```latex % 需要在 \begin{document} 前加上 \usetikzlibrary{through,intersections} \def\circumcircle#1#2#3#4{ \path [name path=line1,draw,thick,dashed] ($(#1)!.5!(#2)$) -- ($(#1)!.5!(#2)!1.5cm!90:(#2)$); \path [name path=line2,draw,thick,dashed] ($(#2)!.5!(#3)$) -- ($(#2)!.5!(#3)!1.8cm!90:(#3)$); \path [name intersections={of=line1 and line2}]; \node [draw,circle through=(#1),thick,color=green!70!black,#4] at (intersection-1) {}; } \clip (-6,-2.5) rectangle (6,2); \coordinate[label=210:$A$] (A) at (-1.4,-.8); \coordinate[label=-30:$B$] (B) at ( 1.5,-.9); \coordinate[label= 15:$C$] (C) at ( 1.1,1.2); \filldraw[thick,draw=red!50!black,fill=red!20!] (A) -- (B) -- (C) -- cycle; \circumcircle{A}{B}{C}{}; ``` > 这里使用了宏来求出三个点的外接圆,主要是为最终代码编写做足准备。宏里面的第四个参数用来给予外接圆额外的信息。 在我们求得了外心 $\verb!intersection-1!$ 之后,定义了一个以 $\verb!intersection-1!$ 为中心的节点。它的形状是圆,并且这个圆经过三角形的一个顶点(这里取的是传入的第一个点)。这样做以后,费尽千辛万苦的我们终于能够求得那五个三角形的外接圆了! 检查一下我们的进度: ![img](https://cdn.luogu.com.cn/upload/image_hosting/m66th2e4.png) ```latex % 需要在 \begin{document} 前加上 \usetikzlibrary{through,calc,intersections} \def\circumcircle#1#2#3#4{ \path [name path=line1] ($(#1)!.5!(#2)$) -- ($(#1)!.5!(#2)!2cm!90:(#2)$); \path [name path=line2] ($(#2)!.5!(#3)$) -- ($(#2)!.5!(#3)!2cm!90:(#3)$); \path [name intersections={of=line1 and line2}]; \node [draw,circle through=(#1),thick,color=green!70!black,#4] at (intersection-1) {}; } \clip (-4,-2.3) rectangle (4,2.4); \coordinate (O) at(0,0); \coordinate[label=90+72: $A$] (A) at (90+72 +rand*5:2+rand*.2); \coordinate[label=90+144:$B$] (B) at (90+144+rand*5:2+rand*.2); \coordinate[label=90+216:$C$] (C) at (90+216+rand*5:2+rand*.2); \coordinate[label=90+288:$D$] (D) at (90+288+rand*5:2+rand*.2); \coordinate[label=90+360:$E$] (E) at (90+360+rand*5:2+rand*.2); \draw [color=blue!50!black,thick] (A) -- (D) -- (B) -- (E) -- (C) -- cycle; \path [name path=lineAD] (A) -- (D); \path [name path=lineDB] (D) -- (B); \path [name path=lineBE] (B) -- (E); \path [name path=lineEC] (E) -- (C); \path [name path=lineCA] (C) -- (A); \path [name path=lineADB] (A) -- (D) -- (B); \path [name path=lineDBE] (D) -- (B) -- (E); \path [name path=lineBEC] (B) -- (E) -- (C); \path [name path=lineECA] (E) -- (C) -- (A); \path [name path=lineCAD] (C) -- (A) -- (D); \path [name intersections={of=lineADB and lineEC}]; \circumcircle{D}{intersection-1}{intersection-2}{name path=circ1}; \path [name intersections={of=lineDBE and lineCA}]; \circumcircle{B}{intersection-1}{intersection-2}{name path=circ2}; \path [name intersections={of=lineBEC and lineAD}]; \circumcircle{E}{intersection-1}{intersection-2}{name path=circ3}; \path [name intersections={of=lineECA and lineDB}]; \circumcircle{C}{intersection-1}{intersection-2}{name path=circ4}; \path [name intersections={of=lineCAD and lineBE}]; \circumcircle{A}{intersection-1}{intersection-2}{name path=circ5}; ``` ### 程序片段 一个尴尬的事实摆在我们面前:我们不知道两个外接圆的两个交点当中,到底哪个是我们想要的。尽管通过人眼可以调整最终选择的点,但是奈何该代码随机生成五角星,可能下一个生成的五角星就会打乱原先的布局。通过使用一些代码决定最终选择的点尤为重要。 通过使用 $\textrm{Ti}\textit{k}\textrm{Z}$ 下的库 $\verb!math!$,可以给我们一套解决方案。 ![img](https://cdn.luogu.com.cn/upload/image_hosting/bqmedx6c.png) ```latex % 需要在 \begin{document} 前加上 \usetikzlibrary{math} \tikzmath{ int \i,\j,\t,\a,\b; for \i in {0,...,10}{ for \j in {0,...,3}{ if (\i==0)||(\j==0) then{ \a{\i,\j}=1; } else { \t1=\i-1; \t2=\j-1; \t3=\i-1; \t4=\j; \a{\i,\j}=\a{\t1,\t2}+\a{\t3,\t4}; }; \b{\i,\j}=10+10*log2(\a{\i,\j}+5); }; }; } \foreach \i in {0,...,10} \foreach \j in {0,...,3}{ \filldraw[thick,draw=orange,fill=orange!\b{\i,\j}] (\i/1.25-4.4,\j/1.25-1.0) rectangle (\i/1.25-3.6,\j/1.25-1.8); \node at (\i/1.25-4,\j/1.25-1.4) {$\a{\i,\j}$}; } ``` 这段代码简要介绍了 $\verb!\tikzmath!$ 指令。它里面支持**定义变量**,也支持一些现代编程语言必有的结构(选择语句、循环语句等)。值得关注的是,所谓的变量名不代表定义了这**一个变量**,而是一簇带有这个名字的变量。这很好地解决了数组的定义问题(某些方面上这有点类似 $\text{Python}$ 一类的解释语言,比如你不需要指明数组的范围)。在一个变量名后面可以打上标签(比如该代码片段里的 $\verb!t1!$ 等),不同标签的变量存储的值不同。 变量类型一共有三种:$\verb!integer!,\verb!real!,\verb!coordinate!$。此外还可以使用 $\verb!let!$ 语句原封不动地用一个变量指明某内容(类似于宏),这里就不细致展开了。对于 $\verb!coordinate!$ 类型的变量名,在定义它之后就附带拥有了两个子变量分别表示它的 $x$ 值和 $y$ 值。例如,写了 `coordinate \w` 之后,使用 `\wx`,`\wy` 可以访问变量 $w$ 的横坐标、纵坐标;使用 `\w1x`,`\w1y` 可以访问变量 $w_1$ 的横坐标、纵坐标。 尽管是在 $\verb!tikzmath!$ 里定义的变量,但是可以作用于全局。不过由于宏本身就可以被重复定义,所以不会造成什么困扰。这段代码通过使用数组生成了一些组合数。利用的是: $$ \binom{n}{m}=\binom{n-1}{m}+\binom{n-1}{m-1} $$ 此外,一个变量在赋值之前是不可访问的。 于是,我们得出了五点共圆的最终代码: ```latex \documentclass[border=4pt]{standalone} \usepackage{xcolor,amsmath,ctex,tikz,lmodern} \usetikzlibrary{calc,through,intersections,math} \begin{document} \begin{tikzpicture}[scale=1.5] \def\circumcircle#1#2#3#4{ \path [name path=line1] ($(#1)!.5!(#2)$) -- ($(#1)!.5!(#2)!2cm!90:(#2)$); \path [name path=line2] ($(#2)!.5!(#3)$) -- ($(#2)!.5!(#3)!2cm!90:(#3)$); \path [name intersections={of=line1 and line2}]; \node [draw,circle through=(#1),thick,color=green!70!black,#4] at (intersection-1) {}; } \clip (-4,-2.3) rectangle (4,2.4); \coordinate (O) at(0,0); \coordinate[label=90+72: $A$] (A) at (90+72 +rand*5:2+rand*.2); \coordinate[label=90+144:$B$] (B) at (90+144+rand*5:2+rand*.2); \coordinate[label=90+216:$C$] (C) at (90+216+rand*5:2+rand*.2); \coordinate[label=90+288:$D$] (D) at (90+288+rand*5:2+rand*.2); \coordinate[label=90+360:$E$] (E) at (90+360+rand*5:2+rand*.2); \draw [color=blue!50!black,thick] (A) -- (D) -- (B) -- (E) -- (C) -- cycle; \path [name path=lineAD] (A) -- (D); \path [name path=lineDB] (D) -- (B); \path [name path=lineBE] (B) -- (E); \path [name path=lineEC] (E) -- (C); \path [name path=lineCA] (C) -- (A); \path [name path=lineADB] (A) -- (D) -- (B); \path [name path=lineDBE] (D) -- (B) -- (E); \path [name path=lineBEC] (B) -- (E) -- (C); \path [name path=lineECA] (E) -- (C) -- (A); \path [name path=lineCAD] (C) -- (A) -- (D); \path [name intersections={of=lineADB and lineEC}]; \circumcircle{D}{intersection-1}{intersection-2}{name path=circ1}; \path [name intersections={of=lineDBE and lineCA}]; \circumcircle{B}{intersection-1}{intersection-2}{name path=circ2}; \path [name intersections={of=lineBEC and lineAD}]; \circumcircle{E}{intersection-1}{intersection-2}{name path=circ3}; \path [name intersections={of=lineECA and lineDB}]; \circumcircle{C}{intersection-1}{intersection-2}{name path=circ4}; \path [name intersections={of=lineCAD and lineBE}]; \circumcircle{A}{intersection-1}{intersection-2}{name path=circ5}; \path [name intersections={of=circ1 and circ3,by={a,b}}]; \path [name intersections={of=circ3 and circ5,by={c,d}}]; \path [name intersections={of=circ5 and circ2,by={e,f}}]; \tikzmath{ coordinate \u,\v; \u=(a);\v=(b); if veclen(\ux,\uy)<veclen(\vx,\vy) then {let \p=b;} else {let \p=a;}; \u=(c);\v=(d); if veclen(\ux,\uy)<veclen(\vx,\vy) then {let \q=d;} else {let \q=c;}; \u=(e);\v=(f); if veclen(\ux,\uy)<veclen(\vx,\vy) then {let \r=f;} else {let \r=e;}; } \circumcircle{\p}{\q}{\r}{color=red}; \end{tikzpicture} \end{document} ``` > 注:$\verb!veclen!$ 函数可用来求横纵坐标对应的点到原点的距离,即: > $$\operatorname{veclen}(x,y)=\sqrt{x^2+y^2}$$ ![img](https://cdn.luogu.com.cn/upload/image_hosting/aphrgsdb.png) ## 关系图初探 在上文中,我们已经介绍过了 $\verb!\node!$ 的简单用法。这里将会给出一些关于节点的更加深入的东西。在本章节中,我们将会实现这样一张图案: ![img](https://cdn.luogu.com.cn/upload/image_hosting/hvnt9ca9.png) > 这张图片可用来揭示 $\text{C++}$ 提供的文件 $\textsf{IO}$ 头文件之间的关系。具体可以参见我的[另外一篇文章](https://www.luogu.com.cn/blog/over-knee-socks/post-talk-about-fileio-of-c-and-cpp)。 ### 节点拓展 ![img](https://cdn.luogu.com.cn/upload/image_hosting/egpu87bt.png) 同样地,这三张图片知识量比较大,下文一一讲解。 这张图里有三种不同的节点的样式,同时还有一种固定样式的线段。如果每次在写 $\verb!\node!,\verb!\draw!$ 之前,都要往里面塞一大堆样式,会显得很臃肿;不过,$\textrm{Ti}\textit{k}\textrm{Z}$ 里提供了一种方法让用户将一些样式设置为一种样式。 ```latex \tikzset{ style1/.style= {color=orange,fill=orange!20,thick,draw,rectangle}, style2/.style= {color= blue,fill= blue!20,thick,draw,circle}, style3/.style= {color= green,fill= green!20,thick,draw,ellipse, minimum width=10mm,minimum height=10mm}, link/.style= {color=red,very thick,dashed,->} } ``` 那么,当我们想要画一个样式 $1$ 的节点时,就只用写类似 `\node[style1]` 这样的格式即可。 --- 有时候,我们想要绘制的节点应当有个**最小的大小**。不然,在填充的内容不够大的时候,节点就会显得大小不一。例如,第一张图片最下方的那个节点,就是未填充任何内容时生成的。设置起来也很简单,通过使用 $\verb!minimum size!$ 可以设定节点的最小大小(宽度和高度);通过使用 $\verb!minimum height!$ 可以设定节点的最小高度;通过使用 $\verb!minimum width!$ 可以设定节点的最小宽度。 ```latex \node[style1] at (0,-1.5) {}; \node[style1,minimum size=15mm] at (0,0) {}; \node[style1, minimum width =20mm, minimum height= 8mm] at(0,1.5){}; ``` --- 在定义节点的语句里,可以用圆括号定义出它的名称。例如,`\node at(0,0) (A) {};` 定义了一个位于 $(0,0)$ 的名字为 $\textrm{A}$ 的节点。使用 $\verb!draw!$ 指令将两个节点的名字用线段连接,就相当于连接了这两个节点。但有时候我们希望线段从节点的上边缘/下边缘/左边缘/右边缘开始延申,此时可以使用类似 `(A.east)` 的方式得到在 $\textrm{A}$ 的右边缘上的点的坐标。例如,上文中的第二张图。 ```latex \node[style2,minimum size=8mm] at (0,0) (O) {}; \draw[link] (O.north) -- +(0,1) node[anchor=-90]{\kaishu 北}; \draw[link] (O.east) -- +(1,0) node[anchor=180]{\kaishu 东}; \draw[link] (O.south) -- +(0,-1) node[anchor= 90]{\kaishu 南}; \draw[link] (O.west) -- +(-1,0) node[anchor= 0]{\kaishu 西}; ``` --- 除了最小节点大小以外,有时我们还需要对节点内的文字到边框的距离进行设置。如: ![img](https://cdn.luogu.com.cn/upload/image_hosting/1hmq6rf7.png) ```latex \tikzset{fix/.style= {text height=1.5ex,text depth=.25ex} } \node[thick,draw] at(-1.5,.5) (A){ggggggg}; \node[thick,draw] at( 1.5,.5) (B){mmmmmmm}; \draw[thick,dashed] (A.north) -| (B.north); \draw[thick,dashed] (A.south) -| (B.south); \node[fix,thick,draw] at(-1.5,-.5) (C){ggggggg}; \node[fix,thick,draw] at( 1.5,-.5) (D){mmmmmmm}; \draw[thick,dashed] (C.north) -| (D.north); \draw[thick,dashed] (C.south) -| (D.south); ``` 如果不进行设置,则会导致尽管节点的大小一致,但是其中的文字高低不平的问题。解决方法很简单,手动设置文字的高度和深度即可。 --- 我们当然可以使用 $\verb!at!$ 语句来指定一个节点的位置。但有时我们希望得到的是节点的**相对位置**。这时需要使用 $\verb!positioning!$ 库,来支持使用相对位置描述节点的位置。例如上文中的第三张图: ```latex \node[style3] at(0,0) (A) {}; \node[style3,below=of A] (D) {\kaishu 下}; \node[style3,above=of A] (U) {\kaishu 上}; \node[style3,left =of A] (L) {\kaishu 左}; \node[style3,right=of A] (R) {\kaishu 右}; \node[style3,below left=of A] (DL) {\kaishu 左下}; \node[style3,above left=of A] (UL) {\kaishu 左上}; \node[style3,below right=of A] (DR) {\kaishu 右下}; \node[style3,above right=of A] (UR) {\kaishu 右上}; ``` > 注:第三张图里的节点是椭圆形状,需要引入 $\verb!shapes.geometric!$ 库。 当然,有时候相对位置并不完全满足我们的要求。此时可以通过增加 $\verb!xshift!,\verb!yshift!$ 选项在 $x$ 方向、$y$ 方向上增加一些位移。 ![img](https://cdn.luogu.com.cn/upload/image_hosting/3wclntha.png) ```latex % 需要在 \begin{document} 前加上 \usetikzlibrary{shapes.geometric,shapes.misc} \tikzset{ stream/.style={ text height=1.5ex,text depth=.25ex, rectangle, minimum width =15mm, minimum height= 6mm, thick, draw=orange, fill=orange!10, font=\rmfamily }, streambuf/.style={ text height=1.5ex,text depth=.25ex, ellipse, minimum width =25mm, minimum height= 6mm, thick, draw=blue, fill=blue!10, font=\rmfamily }, null/.style={ minimum width =15mm, minimum height= 6mm } } \node[stream] (iosbase) {ios\_base}; \node[stream,below=of iosbase] (ios) {ios}; \node[null,below=of ios] (null1) {}; \node[stream,left =of null1,xshift= 8mm] (istream) {istream}; \node[stream,right=of null1,xshift=-8mm] (ostream) {ostream}; \node[stream,below=of null1] (iostream) {iostream}; \node[stream,below=of iostream] (stringstream) {stringstream}; \node[stream,left =of stringstream] (istringstream) {istringstream}; \node[stream,right=of stringstream] (ostringstream) {ostringstream}; \node[stream,below=of stringstream] (fstream) {fstream}; \node[stream,below=of istringstream] (ifstream) {ifstream}; \node[stream,below=of ostringstream] (ofstream) {ofstream}; \node[streambuf,right=of ios ,xshift= 40mm] (streambuf) {streambuf}; \node[streambuf,below=of streambuf,yshift=-23mm] (stringbuf) {stringbuf}; \node[streambuf,below=of stringbuf,yshift= 1mm] (filebuf) {filebuf}; ``` > 这张图实现的时候,为了防止 $\verb!istream!,\verb!ostream!,\verb!iostream!$ 出现一些位置错乱,因此在 $\verb!ios!$ 下方设置了一个隐形的空节点。以此为中心,确定了 $\verb!istream!,\verb!ostream!,\verb!iostream!$ 三者的位置。 ### 边 光有节点可不行。我们需要使用一些边来连接已经绘制出的节点。 ![img](https://cdn.luogu.com.cn/upload/image_hosting/5zunjzdt.png) ```latex % 需要在 \begin{document} 前加上 \usetikzlibrary{shapes.geometric} \tikzset{every node/.style= {minimum height=8mm,minimum width=10mm,draw,thick,ellipse} } \node at ( 0, 0) (A) {A}; \node at ( 2, 0) (B) {B}; \node at ( 1.5,-1.5) (C) {C}; \node at (-1.5,-1.5) (D) {D}; \node at (-1.5, 1) (E) {E}; \path (A) [->]edge (B) [->]edge (C) [->]edge (D) [->]edge(E); \path (D) [bend left ,->] edge(E) (C) [bend right,->] edge(B); \draw [->] (E.east) -| (B.north); ``` 通过在两个节点直接加上 $\verb!edge!$,$\textrm{Ti}\textit{k}\textrm{Z}$ 会为我们连接当前节点和下一个节点。 > 和使用 $\verb!--!$ 的区别在于,使用 $\verb!--!$ 会导致当前节点的改变(或者说,绘图的钢笔笔尖发生移动)。写一大堆 `(A) -- (B) (A) -- (C)` 这样的语句是很麻烦的。同时 $\verb!edge!$ 还可以使用一些新的可选参数(或者说,$\verb!edge!$ 相当于是独立于 $\verb!path!$ 的个体)。 $\verb!edge!$ 还提供了一些可选参数,如这段代码提到的 $\verb!bend left!$ 和 $\verb!bend right!$。它会指定线段应该向左弯曲还是向右弯曲。 当然,有时 $\verb!edge!$ 的自动连边并不能完全满足我们的要求。此时可以配合 $\verb!draw!$ 之类的方式进行手动连接。 ### 图层 如果你使用过 $\text{Photoshop}$ 一类绘图软件,肯定会接触“图层”这个概念。一张图片由多个图层叠加形成,每个图层上都绘有一些图案。$\textrm{Ti}\textit{k}\textrm{Z}$ 也提供了图层的实现方式。限于篇幅,这里就不展开了。这里只会用到“背景图层”(需要调用 $\verb!backgrounds!$ 库)。 ![img](https://cdn.luogu.com.cn/upload/image_hosting/ek26nwam.png) 此处的灰色矩形就是绘制在背景上的。 ```latex % 需要在 \begin{document} 前加上 % \usetikzlibrary{shapes.geometric,backgrounds} \begin{scope} \tikzset{every node/.style= {minimum height=8mm,minimum width=10mm,draw,thick,ellipse} } \node at ( 0, 0) (A) {A}; \node at ( 2, 0) (B) {B}; \node at ( 1.5,-1.5) (C) {C}; \node at (-1.5,-1.5) (D) {D}; \node at (-1.5, 1) (E) {E}; \path (A) [->]edge (B) [->]edge (C) [->]edge (D) [->]edge(E); \path (D) [bend left ,->] edge(E) (C) [bend right,->] edge(B); \draw [->] (E.east) -| (B.north); \end{scope} \begin{scope}[on background layer] \fill[color=black!10] (-3,-2) rectangle (3,1.5); \end{scope} ``` > 补充说明,$\verb!-|!$ 符号与 $\verb!--!$ 的区别在于,$\verb!-|!$ 会先绘制一条水平的线段,再绘制一条竖直的线段连接两个点。同理还有 $\verb!|-!$ 符号(先绘制竖直线段再绘制水平线段)。 这里出现了一个之前未说明的环境 $\verb!scope!$。该环境相当于一个“盒子”,可以将环境内部的所有内容组织在一起。比如使用 $\verb!on background layer!$,可以将该盒子里所有的语句都放在背景图层上。 为了给绘制最终的背景做准备,我们先将之前绘制的节点和线段放在一个 $\verb!scope!$ 里。 ![img](https://cdn.luogu.com.cn/upload/image_hosting/5grpl2y1.png) ```latex % 需要在 \begin{document} 前加上 % \usetikzlibrary{shapes.geometric,positioning} \tikzset{ % 此处同上,故省略 } \begin{scope} \node[stream] (iosbase) {ios\_base}; \node[stream,below=of iosbase] (ios) {ios} edge [link] (iosbase); \node[null,below=of ios] (null1) {}; \node[stream,left =of null1,xshift= 8mm] (istream) {istream} edge [link,bend left] (ios); \node[stream,right=of null1,xshift=-8mm] (ostream) {ostream} edge [link,bend right] (ios); \node[stream,below=of null1] (iostream) {iostream} edge [link] (istream) edge [link] (ostream); \node[stream,below=of iostream] (stringstream) {stringstream} edge [link] (iostream); \node[stream,left =of stringstream] (istringstream) {istringstream} edge [link,bend left] (istream); \node[stream,right=of stringstream] (ostringstream) {ostringstream} edge [link,bend right] (ostream); \node[stream,below=of stringstream] (fstream) {fstream}; \node[stream,below=of istringstream] (ifstream) {ifstream} edge [link,bend right] (istream); \node[stream,below=of ostringstream] (ofstream) {ofstream} edge [link,bend left] (ostream); \node[streambuf,right=of ios ,xshift= 40mm] (streambuf) {streambuf}; \node[streambuf,below=of streambuf,yshift=-23mm] (stringbuf) {stringbuf} edge [link,color=cyan] (streambuf); \node[streambuf,below=of stringbuf,yshift= 1mm] (filebuf) {filebuf} edge [link,color=cyan,bend left] (streambuf); \end{scope} ``` 同时我们加上一些需要手动连接的边。此时需要使用 $\verb!calc!$ 库来进行一些简单的运算。 ![img](https://cdn.luogu.com.cn/upload/image_hosting/vcmq00r7.png) ```latex % 需要在 \begin{document} 前加上 % \usetikzlibrary{shapes.geometric,calc,positioning} \draw [relate] (stringbuf) -| ($(istringstream.north) + (.1,0)$); \draw [relate] (stringbuf) -| ($(ostringstream.north) + (.1,0)$); \draw [relate] (stringbuf) -| ($ (stringstream.north) + (.1,0)$); \node[streambuf,below=of stringbuf,yshift= 1mm] (filebuf) {filebuf} edge [link,color=cyan,bend left] (streambuf); \draw [relate] (filebuf) -| ($(ifstream.north) + (.1,0)$); \draw [relate] (filebuf) -| ($(ofstream.north) + (.1,0)$); \draw [relate] (filebuf) -| ($ (fstream.north) + (.1,0)$); ``` ### 适应 在刚刚填充背景的过程中,我们直接使用了两个坐标 $(-3,-2)$ 和 $(3,1.5)$ 决定背景处的矩形的位置。$\textrm{Ti}\textit{k}\textrm{Z}$ 提供了 $\verb!fit!$ 库,可以根据一些给定的节点,使得生成的节点能够囊括它们。例如, ![img](https://cdn.luogu.com.cn/upload/image_hosting/1trsk1zd.png) ```latex % 需要在 \begin{document} 前加上 % \usetikzlibrary{shapes.geometric,fit,backgrounds} \begin{scope} % 与上个例子相同,已省略 \end{scope} \begin{scope}[on background layer] \node[ fill=black!10,thick,draw=black, fit=(A) (B) (C) (D) (E) ] (BG) {}; \end{scope} ``` 省去了我们手动找点的麻烦。 --- 加上背景,大功告成! ![img](https://cdn.luogu.com.cn/upload/image_hosting/hvnt9ca9.png) ```latex \documentclass[border=4pt]{standalone} \usepackage{xcolor,amsmath,ctex,tikz,lmodern} \usetikzlibrary{backgrounds,fit,calc,positioning} \usetikzlibrary{shapes.geometric,shapes.misc} \begin{document} \begin{tikzpicture}[scale=1.5, stream/.style={ text height=1.5ex,text depth=.25ex, rectangle, minimum width =15mm, minimum height= 6mm, thick, draw=orange, fill=orange!10, font=\rmfamily }, streambuf/.style={ text height=1.5ex,text depth=.25ex, ellipse, minimum width =25mm, minimum height= 6mm, thick, draw=blue, fill=blue!10, font=\rmfamily }, null/.style={ minimum width =15mm, minimum height= 6mm }, link/.style={ ->,color=orange,very thick }, relate/.style={ <->,color=cyan!80,dashed,very thick } ] \begin{scope} \node[stream] (iosbase) {ios\_base}; \node[stream,below=of iosbase] (ios) {ios} edge [link] (iosbase); \node[null,below=of ios] (null1) {}; \node[stream,left =of null1,xshift= 8mm] (istream) {istream} edge [link,bend left] (ios); \node[stream,right=of null1,xshift=-8mm] (ostream) {ostream} edge [link,bend right] (ios); \node[stream,below=of null1] (iostream) {iostream} edge [link] (istream) edge [link] (ostream); \node[stream,below=of iostream] (stringstream) {stringstream} edge [link] (iostream); \node[stream,left =of stringstream] (istringstream) {istringstream} edge [link,bend left] (istream); \node[stream,right=of stringstream] (ostringstream) {ostringstream} edge [link,bend right] (ostream); \node[stream,below=of stringstream] (fstream) {fstream}; \draw[link] (fstream.east) -- ++(4mm,0) |- (iostream.east); \node[stream,below=of istringstream] (ifstream) {ifstream} edge [link,bend right] (istream); \node[stream,below=of ostringstream] (ofstream) {ofstream} edge [link,bend left] (ostream); \node[streambuf,right=of ios ,xshift= 40mm] (streambuf) {streambuf} edge [relate] (ios); \node[streambuf,below=of streambuf,yshift=-23mm] (stringbuf) {stringbuf} edge [link,color=cyan] (streambuf); \draw [relate] (stringbuf) -| ($(istringstream.north) + (.1,0)$); \draw [relate] (stringbuf) -| ($(ostringstream.north) + (.1,0)$); \draw [relate] (stringbuf) -| ($ (stringstream.north) + (.1,0)$); \node[streambuf,below=of stringbuf,yshift= 1mm] (filebuf) {filebuf} edge [link,color=cyan,bend left] (streambuf); \draw [relate] (filebuf) -| ($(ifstream.north) + (.1,0)$); \draw [relate] (filebuf) -| ($(ofstream.north) + (.1,0)$); \draw [relate] (filebuf) -| ($ (fstream.north) + (.1,0)$); \end{scope} \begin{scope}[on background layer] \node[ fill=yellow!10,very thick,dashed,draw=yellow, label=north:{\textbf{streams}}, fit=(iosbase) (ifstream) (ofstream) (istringstream) (ostringstream) ] (streams) {}; \node[ fill=cyan!10,very thick,dashed,draw=cyan, label=north:{\textbf{bufs}}, fit=(streambuf) (stringbuf) (filebuf) ] (bufs) {}; \end{scope} \end{tikzpicture} \end{document} ``` ## 最后的话 再次强调,本文只是一个入门指北。有些较为复杂的内容未加以深究,有兴趣的读者可以自行参阅 $\textrm{Ti}\textit{k}\textrm{Z}$ 用户手册,这里不再赘述。 希望这篇文章可以让你初步领略到 $\textrm{Ti}\textit{k}\textrm{Z}$ 绘图的魅力。 ## 参考资料 - [$\textrm{PGF manual - }\textit{Till Tantau}$](https://mirrors.hit.edu.cn/CTAN/graphics/pgf/base/doc/pgfmanual.pdf)。