Sweetlemon 的博客

Sweetlemon 的博客

NOI Linux 绘图速成

posted on 2020-08-11 12:41:31 | under 工具 |

众所周知,NOI Linux 下的基础设施非常不完善,甚至连画图软件都没有。而快速分析提答输入数据的方法主要有肉眼观察、看压缩率、可视化分析等,前面两种方法在 NOI Linux 下都可以实现,但可视化分析却不那么容易做到。

由于 NOI Linux 下的 Python 没有 tkinter,用 Python 的 turtle 画图的尝试也失败了。再三观察后,我发现了 Firefox 这个可以利用的工具,于是我们可以用 HTML 5 的 canvas 实现画图!

后来经过逆流之时大佬点拨,《编程珠玑 续》里提到了一个叫 pic 的“小语言”,可以用来绘图;而 NOI Linux 下居然恰好有 pic 和 groff,可以把绘出的图生成 ps(PostScript)文件,用文档查看器查看!于是我们又有了一种快捷的绘图方法。

事实上,NOI Linux 下似乎还有很多有趣的工具,比如 GNU M4。如果想探索,可以到 /usr/bin 下面找,已知的简单工具有质因数分解 factor 等。

Canvas

基本框架

<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> 标签中,widthheight 属性可以指定宽度和高度,border CSS 属性为 canvas 添加边框,更利于观察。

接下来的 <script> 才是绘图的主角。首先通过 getElementById 方法找到 Canvas DOM,再用 getContext("2d") 得到一个“CanvasRenderingContext2D”对象,可以理解为绘图的工具对象,或者说是一支“画笔”。

设置颜色

合理设置颜色有助于分析。以下两行代码分别可以设置线条颜色和填充颜色。

cxt.strokeStyle="#39C5BB"; //线条颜色
cxt.fillStyle="#66CCFF"; //填充颜色

绘制矩形

绘制矩形比较简单,以下三行代码分别可以绘制一个矩形边框、填充一个矩形区域、清空一个矩形区域。

cxt.strokeRect(x1,y1,width,height); //绘制一个矩形边框,左上角是 (x1,y1),宽度(x 方向)是 width,高度是 height
cxt.fillRect(x1,y1,width,height); //填充一个矩形区域,参数含义同上
cxt.clearRect(x1,y1,width,height); //清空一个矩形区域,参数含义同上

绘制直线

绘制直线需要用到“路径”,简单地说,就是把笔放在纸上,不提笔而移动笔尖,那么就绘出了笔移动的路径。关于“路径”的更多内容,可以参考 W3School

保险起见,绘制一条路径前可以调用 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)$ 的直线可以这么写。

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) 来画圆。x0y0 表示圆心,radius 表示半径,s_anglee_angle 分别表示这条圆弧开始的角度和结束的角度,direction 表示转的方向。因为我们画的是整个圆,因此 s_angle 可以填 0e_angleMath.PI*2directiontruefalse 都可以(不填也可以)。

接下来可以 cxt.fill()cxt.stroke()fill 会填充整个圆,stroke 只画圆周。

因此画一个圆可以这么写。

cxt.beginPath();
cxt.arc(x0,y0,radius,0,Math.PI*2);
cxt.fill(); // 填充
//cxt.stroke(); // 只画轮廓

坐标系缩放

还有一个很有用的函数是 cxt.scale(kx,ky),它将整个坐标系进行缩放。

例如,如果调用 cxt.scale(2,2) 且代码中的坐标不变,画出来的图会变大。另外,kxky 为负值会产生对折效果。

有了这个函数,我们就可以更加方便地处理题目中动辄 $10^9$ 的坐标了。

这些函数基本能满足提交答案题可视化的需要,因此不再介绍其他函数;关于 canvas 的文档网上有很多,可以自行参考。

pic

有了 canvas,为什么还要用这个呢?因为简单!

pic 不需要输入 canvas 麻烦的框架,不需要打开浏览器;利用它强大的宏功能,你甚至可以直接在输入文件前后加几行,直接画图!

但是 pic 的资料极其匮乏,也许是这就是上古软件吧,能参考的基本上只有 Linux manual page、《编程珠玑 续》和 一篇英文论文,原生中文资料仅有一篇博文,还是拿来画流程图的。

基本框架

pic 的基本框架十分简单,只有两行。

.PS
.PE

没错,文件开头加上 .PS,文件结尾加上 .PE。或者说,pic 处理器只会关注 .PS.PE 之间的文件内容。

那么如何把 pic 这个文本文件变成图像呢?打开终端,执行指令 groff -p input_filename > output_filename,就可以把 pic 语法的文件(input_filename)“编译”成 ps 文件(output_filename)。参数 -p 告诉 groff,要用 pic 预处理。整个处理流程如下图(引自那篇英文论文)。

处理流程

“编译”出来的 ps 文件可以直接用文档查看器打开。

顺便说一句,上面这个图就是用 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 中找到了自定义纸张大小的方法(在这个链接的页面中搜索 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 了。它也支持很多表达式,甚至有内置函数,可以参考论文

分支和循环

pic 甚至支持分支和循环,仍然可以参考论文

宏处理与 copy 指令

对 OIer 最有用的指令来了。copy thru % 某个宏 % until "某个词" 可以批量宏替换!

如果你还没有认识到这意味着什么,来一个论文里的例子。

.PS
copy thru % circle at ( $1,$2) % until "END"
1 2
3 4
5 6
END
box
.PE

这段代码会被处理成下面这样。

.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 版,平时也可以用(大雾)。