TeXでドット入り罫線ノートを再現してみた

>この記事は [TeX & LaTeX Advent Calendar 2016](http://www.adventar.org/calendars/1734) の20日目の記事です。
>昨日は [wtsnjp](https://blog.wtsnjp.com/2016/12/19/tex-and-latex/) さんでした。明日も [wtsnjp](https://blog.wtsnjp.com/2016/12/21/obfuscate-tex/) さんです。

最近流行りのドット入り罫線ノートを，LaTeX文書内で出力するようなマクロを制作した。TeXで書き込み式の教材プリントなどを制作する際に，役立つかもしれない。（あくまでアドベントネタであり，品質は保証できませんが…。）


<img width="1000" alt="1.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/4e5f0086-0dbd-5c51-2f57-af5ab865ef1b.jpeg">

~~（ご覧のとおり，本家に似せる努力は大してしていません。）~~

#成果物

出力する行数を直接指定する`\note`という制御綴と，ノート罫線で`\vfill`のように紙面を埋める`\notefill`という制御綴を用意した。

`\note`は，出力行数（2以上の整数）を第一引数として指定する。（`\note{5}`，`\note{20}`のように用いる。）`\notefill`は引数をとらない。

~~~LaTeX
%%%依存するパッケージの読込
\usepackage{tikz}
\usepackage{zref}
\usepackage{zref-savepos}
\usepackage{fp}

%%%ユーザーが指定できるパラメーター
\newdimen\noteLineWidth
\newdimen\dotsRadius
\newdimen\noteLineDistance

\noteLineWidth.5truept\relax%    <- 罫線の太さ
\dotsRadius.8truept\relax%       <- ドットの半径
\noteLineDistance=6truemm\relax% <- 罫線間隔（A罫 : 7truemm，B罫 : 6truemm） 


%%%必要な内部レジスタの用意
\newdimen\VDNT@currentXPos
\newdimen\VDNT@currentYPos
\newdimen\VDNT@Xinterval
\newdimen\VDNT@Yinterval
\newdimen\VDNT@notegoal


%%% \notefillで用いる座標管理用カウンタの準備
\def\VDNT@pkgname{vodnote}
\global\newcount\VDNT@uniqe


%%% \notefill の定義
\newcommand{\notefill}{\par\bgroup
  \parindent\z@
  %%罫線間隔の算出
  \@tempcnta\linewidth
  \@tempcntb\noteLineDistance
  \FPeval\VDNT@dotsNum{round(round(((\the)\@tempcnta/(\the)\@tempcntb)/2:0)*2:0)}%
  \VDNT@Xinterval\dimexpr(\linewidth)/\VDNT@dotsNum\relax
  \VDNT@Yinterval\VDNT@Xinterval
  %%上端の座標取得
  \zsaveposy{\VDNT@pkgname.\the\VDNT@uniqe.TopPos}%
  %%下端の座標取得
  \leavevmode\vfill\leavevmode
  \zsaveposy{\VDNT@pkgname.\the\VDNT@uniqe.BottomPos}%
  %%ノート罫線描画幅の決定
  \VDNT@notegoal=\dimexpr
    \zposy{\VDNT@pkgname.\the\VDNT@uniqe.TopPos}sp
    -\zposy{\VDNT@pkgname.\the\VDNT@uniqe.BottomPos}sp
  \relax
  %%ノート罫線描画
  \noindent\smash{%
    \begin{tikzpicture}
      \VDNT@currentYPos\z@
      \fill[white!70!black] (\VDNT@Xinterval*\VDNT@dotsNum/2,\VDNT@currentYPos+\inv@mag*4pt) -- ++(\inv@mag*3pt,-\inv@mag*4pt) -- ++(-\inv@mag*6pt,0) -- cycle;
      \@whiledim\VDNT@currentYPos<\VDNT@notegoal\do{
        \VDNT@currentXPos\z@
        \draw[white!70!black,line width=\noteLineWidth] (0,\VDNT@currentYPos) -- (\linewidth,\VDNT@currentYPos);
        \foreach \k in{0,1,...,\VDNT@dotsNum}{%
          \VDNT@currentXPos=\dimexpr\VDNT@Xinterval*\k\relax
          \fill[white!70!black] (\VDNT@currentXPos,\VDNT@currentYPos) circle [radius=\dotsRadius];
        }
        \advance\VDNT@currentYPos\VDNT@Yinterval\relax
      }
      \fill[white!70!black] (\VDNT@Xinterval*\VDNT@dotsNum/2,\VDNT@currentYPos-\VDNT@Yinterval-\inv@mag*4pt) -- ++(\inv@mag*3pt,\inv@mag*4pt) -- ++(-\inv@mag*6pt,0) -- cycle;
    \end{tikzpicture}%
  }%
  \egroup
  %%座標管理用カウンタのインクリメント
  \global\advance\VDNT@uniqe\@ne
  \par
}


%%% \note の定義
\newcommand{\note}[1]{\par\bgroup
  %%罫線間隔の算出
  \@tempcnta\linewidth
  \@tempcntb\noteLineDistance
  \FPeval\VDNT@dotsNum{round(round(((\the)\@tempcnta/(\the)\@tempcntb)/2:0)*2:0)}%
  \VDNT@Xinterval\dimexpr\linewidth/\VDNT@dotsNum\relax
  \VDNT@Yinterval\VDNT@Xinterval
  %%ノート罫線描画
  \noindent
    \begin{tikzpicture}
      \VDNT@currentYPos\z@
      \fill[white!70!black] (\VDNT@Xinterval*\VDNT@dotsNum/2,\VDNT@currentYPos+\VDNT@Yinterval+\inv@mag*4pt) -- ++(\inv@mag*3pt,-\inv@mag*4pt) -- ++(-\inv@mag*6pt,0) -- cycle;
      \foreach \i in{1,2,...,#1}{
        \VDNT@currentXPos\z@
        \global\VDNT@currentYPos=\dimexpr\VDNT@Yinterval*\i\relax
        \draw[white!70!black,line width=\noteLineWidth] (0,\VDNT@currentYPos) -- (\linewidth,\VDNT@currentYPos);
        \foreach \k in{0,1,...,\VDNT@dotsNum}{
          \VDNT@currentXPos=\dimexpr\VDNT@Xinterval*\k\relax
          \fill[white!70!black] (\VDNT@currentXPos,\VDNT@currentYPos) circle [radius=\dotsRadius];
        }
      }
      \fill[white!70!black] (\VDNT@Xinterval*\VDNT@dotsNum/2,\VDNT@currentYPos-\inv@mag*4pt) -- ++(\inv@mag*3pt,\inv@mag*4pt) -- ++(-\inv@mag*6pt,0) -- cycle;
    \end{tikzpicture}%
  \egroup
  \par
}
~~~

これをスタイルファイルとして保存し`usepackage`で読み込むか，文書のプリアンブル部に貼り付ければ動作する（プリアンブル部に貼り付ける場合は，このソースの前に`\makeatletter`を書き込み，末尾に`\makeatother`を追加して下さい）。

「ユーザーが指定できるパラメーター」としている部分の寸法を書き換えると，罫線の太さやドットの半径など，ノートの体裁が変更できる。デフォルトでは罫線の太さを 0.5truept，ドットの半径を 0.8truept，罫線間隔を 6truemm（B罫の間隔）としてある[^1]。

[^1]: すべて`true`付きの寸法として指定しているのは，mag による拡大縮小の影響を受けないようにするため。

> 本当はパッケージとして制作するつもりだったのですが，~~アドベントに間に合わず~~大した分量のソースコードではないため，ネタとして提供することにしました。（そのため，名前空間を定義していたりと，やや仰々しい作りになっています。）

#サンプル

サンプルソースを，（出力 PDF）ー（それを与えるソースコード）の順でいくつか示す。

###\notefill のサンプルソース

`\notefill`で正しくノート罫線を出力させるためには，**2回のコンパイルが必要である。**以下のサンプルソースは，いずれも2回コンパイル後に DVI から出力される PDF を示している。

<img width="1000" alt="1.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/4e5f0086-0dbd-5c51-2f57-af5ab865ef1b.jpeg">

~~~LaTeX:sample.tex
\documentclass[dvipdfmx,10pt,a4paper,usemag,papersize]{jsarticle}
\usepackage{lipsum}

%%%ここで上記のソースを（何らかの形で）読み込む

\begin{document}

\notefill

\newpage

\lipsum[1]
\notefill
\lipsum[1]
\notefill

\end{document}
~~~


なお，任意の文字サイズを指定しても，また JS 文書クラスの`usemag`オプションや`tombow`オプションを宣言しても罫線間隔を保ったまま正しく動作する。これは zref-savepos を用いて予めノート上端とノート下端の間の距離を計測し，そのつど出力行数を計算しながらTikZによる描画にはいっているためである（詳細は後述）。 

<img width="550" alt="3.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/91e2e4d6-d015-cfd5-4a6d-489dea1184e8.jpeg">

~~~LaTeX:sample.tex
\documentclass[dvipdfmx,21pt,a4paper,usemag,papersize,tombow]{jsarticle}

%%%ここで上記のソースを（何らかの形で）読み込む

\begin{document}

hogehoge (21pt)

\notefill

\end{document}
~~~



###\note のサンプルソース

<img width="500" alt="5.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/58c283c6-b9ff-a009-fdb7-7530f41435f0.jpeg">


~~~LaTeX:sample.tex
\documentclass[dvipdfmx,21pt,a4paper,usemag,papersize,tombow]{jsarticle}
\usepackage{lipsum}

%%%ここで上記のソースを（何らかの形で）読み込む

\begin{document}

{\LARGE 東大生}のノートは{\LARGE 必ず美しい}とは限らない。ここから5行のドット入りノート罫線。

\note{5}

\lipsum[1]

\end{document}
~~~

なお，`\note`に与えるノート行数があまりにも大きいと，ページ内に収まらなくなってしまう。


<img width="1000" alt="5.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/c2df4eec-43fe-36b6-7d4c-ccf2c4cd43d0.jpeg">

~~~LaTeX:sample.tex
\documentclass[dvipdfmx,21pt,a4paper,usemag,papersize,tombow]{jsarticle}
\usepackage{lipsum}

%%%ここで上記のソースを（何らかの形で）読み込む

\begin{document}

{\LARGE 東大生}のノートは{\LARGE 必ず美しい}とは限らない。ここから5行のドット入りノート罫線。

\note{5}

\lipsum[1]

\note{50}% <- 長すぎて次のページではみ出してしまった。

\end{document}
~~~

この場合，`\notefill`を用いれば，ページ下端までちょうど収まる行数だけノートが出力される（ただしその場合2回タイプセットが必要になる）。

<img width="500" alt="5.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/a74a9068-83aa-f462-913c-7f5d6661e38d.jpeg">

~~~LaTeX:sample.tex
\documentclass[dvipdfmx,21pt,a4paper,usemag,papersize,tombow]{jsarticle}
\usepackage{lipsum}

%%%ここで上記のソースを（何らかの形で）読み込む

\begin{document}

{\LARGE 東大生}のノートは{\LARGE 必ず美しい}とは限らない。ここから5行のドット入りノート罫線。

\note{5}

\lipsum[1]

\notefill

\end{document}
~~~

`\notefill`と`\note`はいずれも罫線を描画する前に，現在位置の`\linewidth`から出力すべき罫線の長さ，一行のドット数を計算する。したがって`enumerate`環境や`itemize`環境など，インデントが深く取られる環境内にこれらの制御綴を配置すると，自動でノート罫線が適当な長さで出力される。

<img width="500" alt="5.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/706b03e8-7d3d-7c56-d727-591ffd6bc635.jpeg">

~~~LaTeX:sample.tex
\documentclass[dvipdfmx,21pt,a4paper,usemag,papersize,tombow]{jsarticle}
\usepackage{lipsum}

%%%ここで上記のソースを（何らかの形で）読み込む

\begin{document}

\lipsum[5]

7行のドット入りノート罫線。

\note{7}

\begin{enumerate}
\item \lipsum[6]

enumrate環境内にドット入りノート罫線を配置する。

\note{6}

\item \lipsum[7]

\notefill

\end{enumerate}
\end{document}
~~~

なお，ノート罫線部分と本文との距離が，`\note`と`\notefill`で微妙に異なるのはその実装の違いにある。詳細は次で述べるが，`\notefill`は「その罫線がページ下端いっぱいまで到達するように用いたとき，見開きで左右のノート罫線の位置が一致する」ように工夫している。すなわち，ページの下端から罫線を積み上げていくように描画を行う。これにより，ページ上部の文章量がいくらでも，左右のノート罫線の位置にズレが無いように描画できる。

<img width="1000" alt="9.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/77110/1e87b169-27f1-1d4d-a1a7-31dd2a8b711f.jpeg">

一方，`\note`は指定された行数だけ罫線を上から下へと描画する。


#実装について

以下では`\note`と`\notefill`の２つの制御綴の実装について説明する。

##\note の実装

###罫線間隔の算出

成果物のソースを抜粋し以下に示す。

~~~LaTeX
%%罫線間隔の算出
\@tempcnta\linewidth
\@tempcntb\noteLineDistance
\FPeval\VDNT@dotsNum{round(round(((\the)\@tempcnta/(\the)\@tempcntb)/2:0)*2:0)}%
\VDNT@Xinterval\dimexpr\linewidth/\VDNT@dotsNum\relax
\VDNT@Yinterval\VDNT@Xinterval
~~~

一行目で，現在時点の行長をsp単位で表した値を`\@tempcnta`に取得し，二行目で`\noteLineDistance`をsp単位で表した値を`\@tempcntb`に取得する。`\noteLineDistance`は，ソース冒頭で用意した寸法レジスタで，ユーザーが罫線間隔を与えるために用いる。

三行目において，一行に配置するドット数（`\VDNT@dotsNum`+1 ）[^2]を計算する。単純に考えると，このドット数は，$\rm\left[\frac{linewidth}{noteLineDistance}\right]$という式で与えられるかに思われるが[^3]，以下の要求と衝突する可能性が排除できない。

「ノートを縦に二分割して二段組として用いるときに便利なよう，線引きのガイドマーク（上下端の罫線の中央にある小さな三角形）をドット上に配置したい。」

つまり，一行に配置するドット数は奇数でなければならず，`\VDNT@dotsNum`は偶数である必要がある。従って，`\FPeval`を用いて以下のような計算を実行している。

~~~math
\mathrm{VDNT@dotsNum}=2\cdot\left[\frac{1}{2}\cdot\frac{\mathrm{linewidth}}{\mathrm{noteLineDistance}}\right]
~~~

>ここではガウス記号（$[\ ]$）を四捨五入を表す記号として導入しました。

さらに四・五行目においてドット間隔（`\VDNT@Xinterval`）と罫線間隔（`\VDNT@Yinterval`）を設定している。これらの値は以下のようにして与えられる。

~~~math
\mathrm{VDNT@Xinterval}=\mathrm{VDNT@Yinterval}=\frac{\mathrm{linewidth}}{\mathrm{VDNT@dotsNum}}
~~~

注意すべきは，**`\VDNT@Xinterval`や`\VDNT@Yinterval`は`\noteLineDistance`と同値ではない**点である。 ドット数は（当然ながら）整数値でなければならず，また上述の理由により奇数である必要がある。従って，この条件を満たしつつ，`\noteLineDistance`に最も近い値が，ドット間隔および罫線間隔として採用される仕組みになっている。


[^2]: 「0から`\VDNT@dotsNum`まで」繰り返し操作を行いドットを配置するので，ドット数は`\VDNT@dotsNum`+1 コとなる

[^3]: ドットを正方形の格子として配置することを前提としている。したがって`\noteLineDistance`がドット間の距離をも表すとみなしている。

###ノート罫線の描画

~~~LaTeX
%%ノート罫線描画
\noindent
  \begin{tikzpicture}
    \VDNT@currentYPos\z@
    %%下端の三角形マーク描画
    \fill[white!70!black] (\VDNT@Xinterval*\VDNT@dotsNum/2,\VDNT@currentYPos+\VDNT@Yinterval+\inv@mag*4pt) -- ++(\inv@mag*3pt,-\inv@mag*4pt) -- ++(-\inv@mag*6pt,0) -- cycle;
    \foreach \i in{1,2,...,#1}{
      \VDNT@currentXPos\z@
      \global\VDNT@currentYPos=\dimexpr\VDNT@Yinterval*\i\relax
　    %%罫線描画
      \draw[white!70!black,line width=\noteLineWidth] (0,\VDNT@currentYPos) -- (\linewidth,\VDNT@currentYPos);
      \foreach \k in{0,1,...,\VDNT@dotsNum}{
        \VDNT@currentXPos=\dimexpr\VDNT@Xinterval*\k\relax
        %%ドット描画
        \fill[white!70!black] (\VDNT@currentXPos,\VDNT@currentYPos) circle [radius=\dotsRadius];       }
    }
    %%上端の三角形マーク描画
    \fill[white!70!black] (\VDNT@Xinterval*\VDNT@dotsNum/2,\VDNT@currentYPos-\inv@mag*4pt) -- ++(\inv@mag*3pt,\inv@mag*4pt) -- ++(-\inv@mag*6pt,0) -- cycle;
  \end{tikzpicture}%
~~~

ノート罫線の描画部分は TikZ で実装した。上のソースコードの概略を示す。

- `\mag`への対策として，ソース内に直打ちしている寸法には全て`\inv@mag`を乗じている[^4]。これにより，例えば JS 文書クラスで`10pt`以外の文字サイズを指定したとしても，罫線類が予期せぬ大きさに拡大・縮小されることを防いでいる。
- `\foreach`を用いて，縦横に繰り返し操作を行い罫線・ドットを出力している。縦方向の繰り返しのループ変数は`\i`，横方向の繰り返しのループ変数は`\k`である。
- `\VDNT@currentXPos`と`\VDNT@currentYPos`には，現在描画中の位置が格納されている。

各描画命令（`\draw`）が何を描画しているのか，詳細をソース中にコメントで示した。従って当該部分をコメントアウトまたは消去することにより，例えばドットや，上下端の三角形を削ったノート罫線を出力する制御綴も作成できる。また，罫線とドットの色が好みでない場合は，`white!70!black`などの色指定部分を操作すれば変更できる。

##\notefill の実装

`\notefill`の特色は，以下のようにまとめられる。

1. 「`\vfill`のような振る舞いをする」`\note`として機能する。すなわち，`\notefill`を配置した位置に`\vfill`を置いたと仮定した場合に，確保されるスペースに収まるだけ罫線を出力する。
2. 罫線を「下から積み上げるように」描画する。これにより，見開きページで`\notefill`を用いた時，左右の罫線がズレることなく描画できる（前章末尾参照）。

これを実現するために，以下のようなソースで罫線を出力するスペースを測定し，罫線を描画する。なお，実装は`\note`と共通の部分も多いが，そこの説明は省略する。

###ノート描画幅の決定

~~~LaTeX
%%上端の座標取得
\zsaveposy{\VDNT@pkgname.\the\VDNT@uniqe.TopPos}%
%%下端の座標取得
\leavevmode\vfill\leavevmode
\zsaveposy{\VDNT@pkgname.\the\VDNT@uniqe.BottomPos}%
%%ノート罫線描画幅の決定
\VDNT@notegoal=\dimexpr
  \zposy{\VDNT@pkgname.\the\VDNT@uniqe.TopPos}sp
  -\zposy{\VDNT@pkgname.\the\VDNT@uniqe.BottomPos}sp
\relax
~~~

`\zsaveposy`は，PDF 上における位置を取得する制御綴であり，zref パッケージの zref-savepos モジュールにより提供される。これは`\pdfsavepos`という pdfTeX のプリミティブを用いて実装されており，`\pdfsavepos`の挙動については[この記事](http://qiita.com/VoD/items/efebe7820b29d9043786)で詳しく追究した。

はじめに，`\zsaveposy{\VDNT@pkgname.\the\VDNT@uniqe.TopPos}`でノート上端の座標を保存する。なお，他のパッケージで zref-savepos を用いていた場合に衝突することの無いよう，名前空間として`\VDNT@pkgname`を保存名の冒頭に与えている。また`\notefill`を複数回用いる場合に備え，`\notefill`使用の度にインクリメントされるカウンタ（`\VDNT@uniqe`）の値を保存名に挟んでいる。これにより，`\notefill`を文書内で複数用いても，「１つ前や後の`\notefill`で用いるべき座標値が誤って参照される」といったことが防げる。

次に，`\vfill`を実行し，ノート描画スペースの下端まで移動する。ここで`\zsaveposy{\VDNT@pkgname.\the\VDNT@uniqe.BottomPos}`を用いてノート下端の座標を保存し，次の演算に移る。

`\notefill`では，ノート描画スペースの幅を格納する寸法レジスタとして，`\VDNT@notegoal`を用いる。これは単純に，以下の式で与えられる。

~~~math
\rm VDNT@notegoal=(VDNT@pkgname.VDNT@uniqe.TopPos)-(VDNT@pkgname.VDNT@uniqe.BottomPos)
~~~

二点間の座標の差を計算し，相対距離を用いる事により，「pdfTeX の pdf モードと dvi モードで PDF 原点位置がずれる」問題や，「e-(u)pTeX で mag を用いると PDF 原点位置がずれる」問題（やはり[この記事](http://qiita.com/VoD/items/efebe7820b29d9043786)で詳しく解説）を回避している。

###座標管理用カウンタのインクリメント

~~~LaTeX
%%座標管理用カウンタのインクリメント
\global\advance\VDNT@uniqe\@ne
~~~

上で述べたように，`\notefill`の定義末尾で座標管理用のカウンタレジスタ（`\VDNT@uniqe`）をインクリメントする。レジスタのスコープの影響を受けないよう，`\global`付きで`\advance`を実行している。

[^4]: 手っ取り早く`truept`などを用いると，コンパイルが通らなくなる。TikZ の描画命令では`true`付きの寸法を用いて座標を指定することができない（ようである）。