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

  • 11
    いいね
  • 0
    コメント

この記事は 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\noteLineHeight

\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付きの寸法を用いて座標を指定することができない(ようである)。