Edited at

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

More than 1 year has passed since last update.


この記事は TeX & LaTeX Advent Calendar 2016 の20日目の記事です。

昨日は wtsnjp さんでした。明日も wtsnjp さんです。


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

1.jpg

(ご覧のとおり,本家に似せる努力は大してしていません。)


成果物

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

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

%%%依存するパッケージの読込

\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


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



サンプル

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


\notefill のサンプルソース

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

1.jpg


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による描画にはいっているためである(詳細は後述)。

3.jpg


sample.tex

\documentclass[dvipdfmx,21pt,a4paper,usemag,papersize,tombow]{jsarticle}

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

\begin{document}

hogehoge (21pt)

\notefill

\end{document}



\note のサンプルソース

5.jpg


sample.tex

\documentclass[dvipdfmx,21pt,a4paper,usemag,papersize,tombow]{jsarticle}

\usepackage{lipsum}

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

\begin{document}

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

\note{5}

\lipsum[1]

\end{document}


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

5.jpg


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回タイプセットが必要になる)。

5.jpg


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環境など,インデントが深く取られる環境内にこれらの制御綴を配置すると,自動でノート罫線が適当な長さで出力される。

5.jpg


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は「その罫線がページ下端いっぱいまで到達するように用いたとき,見開きで左右のノート罫線の位置が一致する」ように工夫している。すなわち,ページの下端から罫線を積み上げていくように描画を行う。これにより,ページ上部の文章量がいくらでも,左右のノート罫線の位置にズレが無いように描画できる。

9.jpg

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


実装について

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


\note の実装


罫線間隔の算出

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

%%罫線間隔の算出

\@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を用いて以下のような計算を実行している。

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


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


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

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

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


ノート罫線の描画

%%ノート罫線描画

\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と共通の部分も多いが,そこの説明は省略する。


ノート描画幅の決定

%%上端の座標取得

\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の挙動についてはこの記事で詳しく追究した。

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

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

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

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

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


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

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

\global\advance\VDNT@uniqe\@ne

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





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



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



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



  4. 手っ取り早くtrueptなどを用いると,コンパイルが通らなくなる。TikZ の描画命令ではtrue付きの寸法を用いて座標を指定することができない(ようである)。