この記事は TeX & LaTeX Advent Calendar 2016 の20日目の記事です。
昨日は wtsnjp さんでした。明日も wtsnjp さんです。
最近流行りのドット入り罫線ノートを,LaTeX文書内で出力するようなマクロを制作した。TeXで書き込み式の教材プリントなどを制作する際に,役立つかもしれない。(あくまでアドベントネタであり,品質は保証できませんが…。)
(ご覧のとおり,本家に似せる努力は大してしていません。)
#成果物
出力する行数を直接指定する\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 を示している。
\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による描画にはいっているためである(詳細は後述)。
\documentclass[dvipdfmx,21pt,a4paper,usemag,papersize,tombow]{jsarticle}
%%%ここで上記のソースを(何らかの形で)読み込む
\begin{document}
hogehoge (21pt)
\notefill
\end{document}
###\note のサンプルソース
\documentclass[dvipdfmx,21pt,a4paper,usemag,papersize,tombow]{jsarticle}
\usepackage{lipsum}
%%%ここで上記のソースを(何らかの形で)読み込む
\begin{document}
{\LARGE 東大生}のノートは{\LARGE 必ず美しい}とは限らない。ここから5行のドット入りノート罫線。
\note{5}
\lipsum[1]
\end{document}
なお,\note
に与えるノート行数があまりにも大きいと,ページ内に収まらなくなってしまう。
\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回タイプセットが必要になる)。
\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
環境など,インデントが深く取られる環境内にこれらの制御綴を配置すると,自動でノート罫線が適当な長さで出力される。
\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
は「その罫線がページ下端いっぱいまで到達するように用いたとき,見開きで左右のノート罫線の位置が一致する」ように工夫している。すなわち,ページの下端から罫線を積み上げていくように描画を行う。これにより,ページ上部の文章量がいくらでも,左右のノート罫線の位置にズレが無いように描画できる。
一方,\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
の特色は,以下のようにまとめられる。
- 「
\vfill
のような振る舞いをする」\note
として機能する。すなわち,\notefill
を配置した位置に\vfill
を置いたと仮定した場合に,確保されるスペースに収まるだけ罫線を出力する。 - 罫線を「下から積み上げるように」描画する。これにより,見開きページで
\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
を実行している。