NOI Linux 绘图速成

Sweetlemon

2020-08-11 12:41:31

Personal

众所周知,NOI Linux 下的基础设施非常不完善,甚至连画图软件都没有。而快速分析提答输入数据的方法主要有肉眼观察、看压缩率、可视化分析等,前面两种方法在 NOI Linux 下都可以实现,但可视化分析却不那么容易做到。 由于 NOI Linux 下的 Python 没有 tkinter,用 Python 的 turtle 画图的尝试也失败了。再三观察后,我发现了 Firefox 这个可以利用的工具,于是我们可以用 HTML 5 的 canvas 实现画图! 后来经过[逆流之时](https://github.com/countercurrent-time)大佬点拨,《编程珠玑 续》里提到了一个叫 pic 的“小语言”,可以用来绘图;而 NOI Linux 下居然恰好有 pic 和 groff,可以把绘出的图生成 ps(PostScript)文件,用文档查看器查看!于是我们又有了一种快捷的绘图方法。 事实上,NOI Linux 下似乎还有很多有趣的工具,比如 [GNU M4](http://www.gnu.org/software/m4/)。如果想探索,可以到 `/usr/bin` 下面找,已知的简单工具有质因数分解 `factor` 等。 ### Canvas #### 基本框架 ```html <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> <canvas id="cvs" width="1000" height="800" style="border:solid;"></canvas> <script type="text/javascript"> var cv=document.getElementById("cvs"); var cxt=cv.getContext("2d"); //Let's start drawing! </script> </body> </html> ``` `<head>` 中的 `<meta>` 为编码类型,当然加上这个和绘图没有太大的关联。 `<canvas>` 标签中,`width` 和 `height` 属性可以指定宽度和高度,`border` CSS 属性为 canvas 添加边框,更利于观察。 接下来的 `<script>` 才是绘图的主角。首先通过 `getElementById` 方法找到 Canvas DOM,再用 `getContext("2d")` 得到一个“CanvasRenderingContext2D”对象,可以理解为绘图的工具对象,或者说是一支“画笔”。 #### 设置颜色 合理设置颜色有助于分析。以下两行代码分别可以设置线条颜色和填充颜色。 ```javascript cxt.strokeStyle="#39C5BB"; //线条颜色 cxt.fillStyle="#66CCFF"; //填充颜色 ``` #### 绘制矩形 绘制矩形比较简单,以下三行代码分别可以绘制一个矩形边框、填充一个矩形区域、清空一个矩形区域。 ```javascript cxt.strokeRect(x1,y1,width,height); //绘制一个矩形边框,左上角是 (x1,y1),宽度(x 方向)是 width,高度是 height cxt.fillRect(x1,y1,width,height); //填充一个矩形区域,参数含义同上 cxt.clearRect(x1,y1,width,height); //清空一个矩形区域,参数含义同上 ``` #### 绘制直线 绘制直线需要用到“路径”,简单地说,就是把笔放在纸上,不提笔而移动笔尖,那么就绘出了笔移动的路径。关于“路径”的更多内容,可以参考 [W3School](https://www.w3school.com.cn/jsref/dom_obj_canvasrenderingcontext2d.asp)。 保险起见,绘制一条路径前可以调用 `cxt.beginPath()`,这会开始一条新的路径,并把笔放到 $(0,0)$。 接下来调用 `cxt.moveTo(x1,y1)`,相当于提起笔尖,将笔移到 $(x_1,y_1)$ 再放下笔。 `cxt.lineTo(x2,y2)` 可以将笔从当前位置沿直线移动到 $(x_2,y_2)$,从而画出一条直线。 `cxt.stroke()` 相当于将刚才画的轮廓显示到画板上。 因此画一条 $(x_1,y_1)$ 到 $(x_2,y_2)$ 的直线可以这么写。 ```javascript cxt.beginPath(); cxt.moveTo(x1,y1); cxt.lineTo(x2,y2); cxt.stroke(); ``` #### 绘制圆 这里仍然要用到“路径”。 首先 `cxt.beginPath()`,接着不需要 `moveTo`,而是直接使用 `cxt.arc(x0,y0,radius,s_angle,e_angle,direction)` 来画圆。`x0` 和 `y0` 表示圆心,`radius` 表示半径,`s_angle` 和 `e_angle` 分别表示这条圆弧开始的角度和结束的角度,`direction` 表示转的方向。因为我们画的是整个圆,因此 `s_angle` 可以填 `0`,`e_angle` 填 `Math.PI*2`,`direction` 填 `true` 或 `false` 都可以(不填也可以)。 接下来可以 `cxt.fill()` 或 `cxt.stroke()`,`fill` 会填充整个圆,`stroke` 只画圆周。 因此画一个圆可以这么写。 ```javascript cxt.beginPath(); cxt.arc(x0,y0,radius,0,Math.PI*2); cxt.fill(); // 填充 //cxt.stroke(); // 只画轮廓 ``` #### 坐标系缩放 还有一个很有用的函数是 `cxt.scale(kx,ky)`,它将整个坐标系进行缩放。 例如,如果调用 `cxt.scale(2,2)` 且代码中的坐标不变,画出来的图会变大。另外,`kx` 或 `ky` 为负值会产生对折效果。 有了这个函数,我们就可以更加方便地处理题目中动辄 $10^9$ 的坐标了。 这些函数基本能满足提交答案题可视化的需要,因此不再介绍其他函数;关于 canvas 的文档网上有很多,可以自行参考。 ### pic 有了 canvas,为什么还要用这个呢?因为简单! pic 不需要输入 canvas 麻烦的框架,不需要打开浏览器;利用它强大的宏功能,你甚至可以直接在输入文件前后加几行,直接画图! 但是 pic 的资料极其匮乏,也许是这就是上古软件吧,能参考的基本上只有 [Linux manual page](https://www.man7.org/linux/man-pages/man1/pic.1.html)、《编程珠玑 续》和 [一篇英文论文](http://floppsie.comp.glam.ac.uk/Glamorgan/gaius/web/pic.html),原生中文资料仅有[一篇博文](https://blog.51cto.com/lavenliu/1663810),还是拿来画流程图的。 #### 基本框架 pic 的基本框架十分简单,只有两行。 ```pic .PS .PE ``` 没错,文件开头加上 `.PS`,文件结尾加上 `.PE`。或者说,pic 处理器只会关注 `.PS` 和 `.PE` 之间的文件内容。 那么如何把 pic 这个文本文件变成图像呢?打开终端,执行指令 `groff -p input_filename > output_filename`,就可以把 pic 语法的文件(input_filename)“编译”成 ps 文件(output_filename)。参数 `-p` 告诉 groff,要用 pic 预处理。整个处理流程如下图(引自那篇英文论文)。 ![处理流程](http://floppsie.comp.glam.ac.uk/Glamorgan/gaius/web/grohtml-128821.png) “编译”出来的 ps 文件可以直接用文档查看器打开。 顺便说一句,上面这个图就是用 pic 画的,源码如下(同样引自那篇论文)。 ```pic .PS ellipse "document"; arrow; box "\fIgpic\fP(1)" arrow; box width 1.2 "\fIgtbl\/\fP(1) or \fIgeqn\/\fP(1)" "(optional)" dashed; arrow; box "\fIgtroff\/\fP(1)"; arrow; ellipse "PostScript" .PE ``` 怎么样,是不是很短?想学吧(大雾)。 下面的内容主要是绘制几何对象而不是绘制流程图,因此主要采用绝对坐标。pic 的绝对坐标类似平面直角坐标系的第一象限,以左下角为原点。奇妙的是,这个坐标系似乎会自动缩放,所以即使输入的数据是 $10^9$ 也没关系。 但是还有一个问题,默认输出的纸张大小是 A4,所以如果横纵坐标比例不是 $1:\sqrt{2}$,就很难画好。 怎么办呢?我们可以自定义纸张大小啊。 查阅了许多资料,我终于从 [groff_font 的 man pages](https://www.freebsd.org/cgi/man.cgi?query=groff_font&sektion=5) 中找到了自定义纸张大小的方法(在这个链接的页面中搜索 `papersize` 即可看到相关资料)。只需要在生成时使用 `groff -p -P-p25c,25c input_filename > output_filename` 即可自定义 $25\mathrm{cm}\times 25\mathrm{cm}$ 的正方形纸张。也可以修改 `-P-p` 中的参数, `-P` 的意思是让 `groff` 把这个参数传递给“打印设备”(类似 gcc 的 `-Wl`),`-p` 指定 papersize,`c` 表示厘米。 #### 绘制矩形 矩形可以用 `box` 来描述。`box width w height h at (x1,y1); ` 生成一个**中心**在 $(x_1,y_1)$,宽度为 $w$,高度为 $h$ 的矩形。 然而我们描述矩形常常用的是左上角坐标,而 pic 支持算术表达式,所以可以写 `box width w height h at (x1+w/2,y1+h/2)` 来生成左上角在 $(x_1,y_1)$ 的矩形。 如果想要边框颜色怎么办?可以写 `box ... outline "边框颜色"`,如 `box width 3 height 2 at (5,6) outline "red"` 生成一个红色矩形边框。 如果想要填充怎么办?可以写 `box ... shaded "填充颜色"`,如 `box ... shaded "green" ` 生成填充了绿色的矩形。另外,如果用 `color "某颜色"`,就会同时设置边框颜色和填充颜色。 如果想要自定义颜色怎么办?这个需求可真够个性化,可以用 `.defcolor miku rgb #39C5BB` 这样的语法,然后就可以 `box ... color "miku"` 了。 #### 绘制直线 要画直线很简单,`line from (x1,y1) to (x2,y2)` 会画一条 $(x_1,y_1)$ 到 $(x_2,y_2)$ 的直线。上面设置颜色的方法仍然适用。 #### 绘制圆 画圆同样不难,`circle at (x0,y0) radius r` 就能画出圆心在 $(x_0,y_0)$,半径为 $r$ 的圆。上面设置颜色的方法仍然适用。 #### 变量和函数 pic 支持变量定义,直接像 Python 一样写就可以了,比如 `a=2`,后面就可以直接调用 `a` 了。它也支持很多表达式,甚至有内置函数,可以参考[论文](http://floppsie.comp.glam.ac.uk/Glamorgan/gaius/web/pic-13.html)。 #### 分支和循环 pic 甚至支持分支和循环,仍然可以参考[论文](http://floppsie.comp.glam.ac.uk/Glamorgan/gaius/web/pic-16.html)。 #### 宏处理与 copy 指令 对 OIer 最有用的指令来了。`copy thru % 某个宏 % until "某个词"` 可以批量宏替换! 如果你还没有认识到这意味着什么,来一个论文里的例子。 ```pic .PS copy thru % circle at ($1,$2) % until "END" 1 2 3 4 5 6 END box .PE ``` 这段代码会被处理成下面这样。 ```pic .PS circle at (1,2) circle at (3,4) circle at (5,6) box .PE ``` 发现了什么?我们只需要在输入文件前面加一个简短的 copy,最后加一个结束符,就可以自动生成画图的指令! 再解释一下宏的作用过程。每一行会调用一次宏,调用时把行里的字符用空格(准确地说是空白字符)分割成若干部分,作为参数传给宏,而宏最多可以接受 9 个参数,用 `$1` 这样的格式使用。 此外,还可以用 `copy "文件名" thru % 宏 %` 的格式,引用文件中的内容。这样能够减少对输入文件的修改。但是要注意,文件中不要出现 Windows 风格换行符 `\r`。 关于 pic 的介绍就到此结束,更多信息可以参考上面给出的几篇文献。 另外,groff 官网也提供了 Windows 版,平时也可以用(大雾)。