TikZ で“インラインな”図を描く


これは「TeX & LaTeX Advent Caleandar 2014」の18日目の記事です。
(17日目は ワトソンさん です。19日目は y_minoda さんです。)

TikZ、スゴイですね! (色んな意味で)

というわけで、「完全攻略! TikZ」というネタをやろうかなと思ったのですが、冷静に考えてみると来年のクリスマスに間に合いそうにない。というわけで、方針転換して、TikZをチョットだけ(マニュアルの1ページ分くらい)解説することにします。トピックは「 TikZの図の外見のサイズ 」です。

外見のサイズ is 何

よくLaTeXの解説では、図は(\includegraphics したものでも picture 環境でも)「1つの大きな文字のように扱われる」とされています。この説明に沿うと「図の外見のサイズ」とはその“文字”の幅・高さ・深さのことを指します。ほとんどの場合、“図の内容”はその“文字の領域”の内側にありますが、外側にはみ出すことも可能で、(特に指示がない限りは)外側の部分はそのまま“領域からはみ出して”(場合によっては周りの文字に重なって)描画されることになります。

図の外見のサイズを正しく把握することは、特に図を行内で(“インライン”で)文字と混在させる場合に問題になります。いくつか例となる問題を挙げてみます。

【例題1】 次のコードを組版した場合、文字「ABC」「XYZ」とTikZの図(線分1つ)の位置関係はどうなるか?

ABC\tikz\draw (1em,-2em)--(2em,-1em);XYZ

【例題2】 下図の中の右側の“変な矢印”をTikZで描画したい。その際に左側の矢印(これはcmsyの \longrightarrow)と“位置が揃う”ようにしたい。cmsy(LaTeXの既定の数式記号フォント)の矢印(の横棒)の垂直位置が 0.25em、線幅が 0.04em であることが解っている。この情報を基にして“位置が揃う”ように“変な矢印”を描画するコードを書け。

「なんだ、こんなの簡単じゃん!」という人は、今日のACの記事は残念だったということで、明日の記事に期待しましょう。

ここからは残りの人のための解説です。

picture環境の外見のサイズ

TikZの話の前に、LaTeX標準のpicture環境では「外見のサイズ」がどう決まっていたかを復習しておきましょう。
\unitlength の値は1pt(LaTeX既定値)とします。)

\begin{picture}(60,48)
  %...中身
\end{picture}

このように、picture環境ではその引数に「外見のサイズ」を明示指定するという約束でした。なので、環境の中に何が書いてあっても、この図の「外見のサイズ」は「幅=60pt、高さ=48pt、深さ=0pt」(深さは常にゼロになる)となります。

ところで、picture環境では、2つ目の引数を与えることで、座標の値をずらすことができます。

Hello
\begin{picture}(40,40)(-20,-20)
  \put(-20,-20){\framebox(40,40){}}% 外見の領域全体を囲む枠
  \put(0,0){\circle*{4}}% 原点の位置
\end{picture}
\TeX

注意すべきことは、この「座標をずらす設定」は外見のサイズに影響を与えない、ということで、上掲のpicture環境の図の縦のサイズはやはり「高さ=40pt、深さ=0pt」となります。環境内でY座標が負である領域も(外見の領域内であれば)外側の世界のベースラインより上にあるわけです。当然ですが、このような場合は、環境内での原点((0,0))は外側のベースラインの上にはありません。

ここで述べたpicture環境の事情は、epic+eepic パッケージおよび pict2e パッケージでpictureを拡張した場合、あるいは PSTricks パッケージの pspicture 環境にも当てはまります。

TikZの図の外見の大きさ

さて、話をTikZに戻しましょう。
(TikZパラメタ値 x=1cm, y=1cm(既定値)を仮定します。)

\begin{tikzpicture}[thick]
  \fill (-1,2) circle [radius=0.5];% まる
  \draw (-1,-1)--(-0.5,0)--(0,-1)--cycle;% さんかく
  \draw (1,0.5) rectangle (2,1.5);% しかく
\end{tikzpicture}

TikZではpicture環境と異なり「外見の領域」(TikZではこれを“バウンディングボックス(bounding box)”と呼ぶ)を明示的に指定する必要がありません。これは、TikZが(PGFが)「バウンディングボックスを自動的に算出する」機能をもっているからです。そこでは、「描画されたもの全てを含む最小の矩形領域」(を近似的に求めたもの)をバウンディングボックスと見なしています。

バウンディングボックスの様子が判るように、その領域を半透明で塗りつぶしてみます。(現在のバウンディングボックスが current bounding box という名前のノードとして取得できることを利用している。)

\huge Hello % 字を大きく
\begin{tikzpicture}[thick]
  \fill (-1,2) circle [radius=0.5];% まる
  \draw (-1,-1)--(-0.5,0)--(0,-1)--cycle;% さんかく
  \draw (1,0.5) rectangle (2,1.5);% しかく
  % 以下は"現在のbounding boxの矩形を描く"命令
  \fill[green, opacity=0.2]
    (current bounding box.south west)
    rectangle (current bounding box.north east);
\end{tikzpicture}
\TeX

これを見ると、図のバウンディングボックスと外側のベースラインとの関係は、picture環境の時と同じであることが解ります。すなわち、バウンディングボックスの矩形の座標は (‐1.5cm,‐1cm)―(2cm,2.5cm) です(実際は線幅があるので若干外側に広い)が、これはベースラインのすぐ上に載っていると見なされるので、「外見のサイズ」は「幅=3.5cm、高さ=3.5cm、深さ=0pt」となるわけです。
※パラメタ xy の値が 1cm(既定値)なので、(-1,2) のような座標表記は実際には (‐1cm,2cm) を意味します。(直接 (-1cm,2cm) のようにも記述できます。)

ここまでの説明を理解したならば、【例題1】のコードの実際の組版結果が次のようになることは容易に予想できることでしょう。線分はY座標が負の位置にありますが、実際にはそれはベースラインの上に配置されます。

外見のサイズの調整

このままでは、【例題2】のような「周りのテキストと位置を揃えた」描画を行うことは困難です。環境内のY座標のどの値が外の世界のベースラインと一致するかが予測できないからです。

これを解決する一つの手段として、「picture環境と同様にバウンディングボックスを明示する」という方法が考えられます。TikZでは、use as bounding box オプション付の \path 命令(またはその省略形である \useasboundingbox 命令)でバウンディングボックスの明示指定ができます。

\huge Hello
\begin{tikzpicture}[thick]
  % 下端のY座標が0になるようにバウンディングボックス明示指定. 
  % これでY座標の0がベースラインと一致する.
  \useasboundingbox (-1.5,0) rectangle (2,2.5);
  \fill (-1,2) circle [radius=0.5];% まる
  \draw (-1,-1)--(-0.5,0)--(0,-1)--cycle;% さんかく
  \draw (1,0.5) rectangle (2,1.5);% しかく
  \fill[red] (0,0) circle [radius=3pt];% 原点
\end{tikzpicture}
\TeX

ただこの方法だと、Y座標が負の領域は「バウンディングボックスをはみ出している」(「外見の深さ」は相変わらずゼロである)ことになるので、段落内で用いるには不都合です。(下の行と重なってしまいます。)

実は、TikZには、環境内の座標と外側のベースラインの関係を調節するための機構を持っています。それは baseline パラメタ です。

baselineパラメタの利用

baseline パラメタは次のようにtikzpicture環境(または \tikz 命令)に対して与えられます。パラメタの値には単位付きな数値(つまり長さ、1cm 等)を指定します。

\begin{tikzpicture}[baseline=値]

この指定により、環境内でのY座標が である垂直位置が外側のベースラインと一致するようになります。
※ TikZの座標値は本質的には“長さ”であることに改めて注意。

\huge Hello
% Y座標が0ptの垂直位置をベースラインと一致させる
\begin{tikzpicture}[thick, baseline=0pt]
  \fill (-1,2) circle [radius=0.5];% まる
  \draw (-1,-1)--(-0.5,0)--(0,-1)--cycle;% さんかく
  \draw (1,0.5) rectangle (2,1.5);% しかく
  \fill[red] (0,0) circle [radius=3pt];% 原点
\end{tikzpicture}
\TeX

活用事例: 変な矢印

baseline パラメタを利用すると【例題2】の“変な矢印”が実現できますね。早速やってみましょう。baseline=0pt でベースライン位置をY座標の0ptに固定します。その上で、Y座標 0.25em の位置に矢印を描けばよいわけです。コイル状のアレはパスに対する装飾(decoration)として指定します。(よくTikZ屋さんが嬉しがってやっているアレです。)矢印の“矢じり”は \longrightarrow と合わせたいので、形状 to を選択します。

%% (プレアンブルで)
\usetikzlibrary{decorations.pathmorphing}% coil 装飾
%% \myCoilArrow{長さ} : 変な矢印
\newcommand{\myCoilArrow}[1]{%
\tikz[baseline=0pt, x=1em, y=1em]
  \draw[-to, line width=0.04em, decorate,
    decoration={pre length=0.4em, post length=0.4em,
      coil, segment length=0.2em, amplitude=0.2em}]
    (0,0.25)--(#1,0.25);
}
%% (文書本体で)
.tex $\longrightarrow$ .dvi $\myCoilArrow{3em}$ .pdf

はいできあがり! ……えっ、“矢じり”が小さい? 確かに to\to の矢印の矢じりを模した形なのですが、どうやら作者は大きさまで \to に合わせようとは考えなかったようです。大きさまで揃える(冒頭の例題の図のようにする)には、新たに矢じり形状を(下位レイヤーで)定義する必要があるようです。(バッドノウハウは考えられますが……。)例題の図は実際に形状定義を行って作りました。この記事の話ではそこは本質でないので勘弁してください……。

baseline の別の指定方法

ところで、この baseline パラメタですが、値として座標を指定することもできて、この場合はその点のY座標(長さ値)を指定したのと等価です。

% これは baseline=1cm と同値.
\begin{tikzpicture}[baseline={(3,1)}]

もちろん、このように具体的な数値の座標を指定したのでは余り利点がないですが、これにはもっと面白い使い方があります。

“気づかれない”TikZ

(単一行のテキストを含む)ノードに T という名前が付いている時、座標 (T.base) のY成分はノード内のテキストのベースラインの位置を表します。これと、さっきの baseline 指定を組み合わせると、“図の外側と内側”のベースラインを合わせることが可能になります。

Hello,
\tikz[baseline=(T.base)]
  \node[draw=green, fill=green!20, text=blue]
    (T) at (0,0) {Ti\emph{k}Z};
world!

※ この例で、“現在座標”の初期値は (0,0) なので、at (0,0) の指定は不要です。まあこの場合、座標は何でもいいわけですが。

ここで、ノードの“存在”を示している枠線や塗り潰しなどの要素を全部除いてしまうと、まるで、「テキストをそのまま置いた」ように見えるはずです。

Hello,
\tikz[baseline=(T.base)]
  % draw も fill も無しにする
  \node (T) {Ti\emph{k}Z};
world!

Hello, Ti\emph{k}Z world! % 比較用

いや、まだ余計な隙間が空いていますね……。これも取り除きましょう。

Hello,
\tikz[baseline=(T.base)]
  % inner sep (パディング), outer sep (マージン) をゼロに
  \node[inner sep=0pt, outer sep=0pt] (T) {Ti\emph{k}Z};
world!

Hello, Ti\emph{k}Z world! % 比較用

これで見事に「TikZに見えない」ようになりましたね!

活用事例: 和文の斜体

これに対して、きっと「別にそんなことをしても役に立たないじゃん」と思われたでしょう。確かにこのままでは全く有用ではありません。しかし、この状態でもう一度TikZの機能を“足して”いくことができます。ということは、 TikZでテキスト装飾! というネタに発展させることができます。例えば、直前の例で \nodescale オプション(あるいは xscaleyscale)を付けることで文字を拡大縮小させる、つまり“\scalebox の代用”ができます。

「既存の機能の代用なんか要らない」、ですか? では「和文を斜体にする」というのはどうでしょう? graphicx パッケージではテキストの回転や拡大縮小はできますが、斜体変形はサポートしていません。一応、「回転と拡大縮小を組み合わせると斜体変形ができる」というネタはありますが、そこで使うパラメタの値の計算が自明ではありません。一方で、“TikZでテキスト装飾”を使うと、slant を素直に指定するだけで済みます。

Ti\emph{k}Zで
\tikz[baseline=(T.base)]
  \node[inner sep=0pt, outer sep=0pt, xslant=0.25]
    (T) {とっても素敵な};% ひどい……
テキスト装飾!

活用事例: テキストにグラデーション

TikZはスゴいパッケージです。なので、“TikZで装飾”を応用すると、“スゴい装飾”ができてしまいます。例として、「文字の色をグラデーションさせる」ことにしましょう。

\documentclass[a4paper]{article}
\usepackage{tikz}
\usetikzlibrary{fadings}% フェーディング利用
%% \myFading{<先頭の色>}{<末尾の色>}{<テキスト>} :
%    テキストにグラデーションをかける.
\newcommand*{\myFading}[3]{{%
  % 文字の部分だけを透明にしたフェーディング
  % 'mytext'を作る.
  \begin{tikzfadingfrompicture}[name=mytext]
    \node[rectangle, inner sep=1pt, outer sep=0pt,
      fill=transparent, text=transparent!0] {#3};
  \end{tikzfadingfrompicture}%
  % 描画開始
  \begin{tikzpicture}[baseline=(T.base)]
    % テキストの全体の大きさを持つノード 'T' を
    % 先に作る必要があるので, 全透明でテキストを
    % 書いたノードを作る.
    % (これでPDFにテキスト情報が入る.)
    \node[rectangle, inner sep=0pt, outer sep=0pt,
      opacity=0] (T) {#3};
    % 線形グラデーションで塗り潰したTと同じ大きさの
    % 長方形を描き, その際に先に作ったフェーディング
    % 'mytext' を適用する.
    \shade[left color=#1, right color=#2,
      % フェーディングのスケールを抑止する.
      % (mytextの定義では"念のため"1ptの隙間を入れた)
      path fading=mytext, fit fading=false,
      % この長方形を外見の領域とする.
      use as bounding box]
      (T.south west) rectangle (T.north east);
  \end{tikzpicture}%
}}
\begin{document}
\Large\sffamily\fontseries{sbc}\selectfont
Thanks {\TeX}, \myFading{black}{white}{and Good-bye, Good-bye, Good-bye.}
\end{document}

やっぱりTikZはスゴイですね!!

最後に、復習のための練習問題を出しておきます。

練習問題

下の図のように、テキストにバツ印を付けて出力するための命令 \myCrossedOut を定義しなさい。

命令の書式は、\myCrossedOut{色名}{テキスト} とします。つまり、先の出力は以下のようなソースによって出力されたものです。

ここで早速\myCrossedOut{red}{dvipdfm}%
\textcolor{red}{dvipdfmx}を起動する。