マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

tcspingifで“もっともっと”ゆきだるまを動かす

今日は「ホウキ付ゆきだるま☃」の日!

f:id:zrbabbler:20200818191245g:plain

先日の「ゆきだるま☃の日」にはLaTeXで「回転ゆきだるま☃画像」を作成するためのtcspingifというスクリプトを紹介した。tcspingifを使って作れる「回転☃」は「自転☃」であるが、少し(あるいは、かなり)頑張れば、上図のような「公転☃」、あるいはもっと別の動きを伴った「動く☃」のGIF画像をつくることもできる。その方法について解説する。

tcspingifはなぜ動くのか

tcfaspinパッケージはanimateパッケージの機能を使って「動くPDF文書」を作っているが、「動くPDF文書」は画像に変換できない。では、tcspingifはなぜGIF画像を作れるのかというと、実はtcfaspinの中に秘密がある。

tcfaspinを読み込む前に次のようなオマジナイを実行しておく。

\chardef\faStopTicks=18 \chardef\faAllTicks=32

TeX言語のコードなので理解する必要は全くないのだが、フインキとしては「\faStopTicks\faAllTicksという2つの“変数”を定義して、それぞれ18と32を“代入”する」のようなことを行っている1。このオマジナイが実行済である場合、tcfaspinパッケージはanimateを読み込まなくなり、また、\faSpin命令の動作が変わり、単に「32フレーム中の18フレーム目」の状態を出力するようになる。この場合に出力されるPDF文書はanimateを使っていないため普通にImageMagickで画像に変換できる。

この「tcfaspinの裏仕様」を利用して、tcfaspinは次のような手順で「動くGIF画像」を作っている。(フレーム数が32だとする。)

  • n=0~31の各々について:
    • 上記のオマジナイ(ただし\faStopTicksをnとする)を実行した上で対象のLaTeXソースをコンパイルして、普通のPDF文書を作る。
    • ImageMagickでPDF文書をPNG画像に変換する。これが「nフレーム目のPNG画像」となる。
  • 再びImageMagickを用いて、「0~31フレーム目のPNG画像」をアニメGIF画像に変換する。

tcfaspinしないでtcspingifする

このtcfaspinの実行手順をよく見ると、それ自体はtcfaspinとは無関係であることがわかる。すなわち、対象のLaTeX文書は別にtcfaspinを読み込んでいる必要はなく、単に「オマジナイが実行されて、“変数”が定義されている」という点だけが“普通のコンパイル”と異なるわけである。

これを利用すると、tcspingifを“流用”して好きな動き方をするアニメGIF画像をつくることができる。すなわち、LaTeX文書の内容を、「\faAllTicksフレーム中の\faStopTicksフレーム目」の状態を出力するようにすればよいのである。

pgfmathパッケージ2の機能を利用すると、“変数”の値を読み出すことができる3

% "変数" \faStopTicks の値を \vN に代入する
\pgfmathsetmacro{\vN}{\number\faStopTicks}
% \faStopTicks ÷ \faAllTicks の値を \vT に代入する
\pgfmathsetmacro{\vT}{\number\faStopTicks/\number\faAllTicks}

要するに要するに

以下のような感じでコードを書けばよい。

\documentclass{standalone}
\usepackage{tikz,pgfmath}
% \vT は"アニメの開始を0、終了を1とした場合の現在時刻"の値
\pgfmathsetmacro{\vT}{\number\faStopTicks/\number\faAllTicks}
% \vN は"現在何フレーム目か"の値
\pgfmathsetmacro{\vN}{\number\faStopTicks}
% (その他諸々の設定...)
\begin{document}
\begin{tikzpicture}
% \vT や \vN の値を利用して"現在のフレーム"を描画する
\end{tikzpicture}
\end{document}

※もちろん、このようにtcspingifを“流用”したLaTeX文書は「tcspingif専用」であり、普通にコンパイルして「動くPDF文書」をつくることはできないことに注意。
※tcfaspinを読み込まない場合、tcfaspinの設定からフレーム数を取得することができないため、tcspingifで--ticks(-t)オプションの指定が必須になる。

ゆきだるま☃を動かしてみる

※以下では、前回と同じく「DPI値を72にした(tcspingifで-d 72を指定)上でTikZでx=1bp,y=1bpを指定して座標の単位をピクセルと一致させる」設定を利用する。

手始めに、ゆきだるま☃を単純に横に動かしてみる。具体的に「600×120ピクセルの画像で、ゆきだるま☃を(60,60)から(540,60)に動かす」ことにする。先ほど示した雛形の\vT(現在時刻)を使うことにすると、ゆきだるま☃を置く点は「始点・終点間の\vT倍内分点」となる。calcライブラリの内分点表記を利用すると、tikzpicture内は以下のように書ける。

\node at ($(60,60)!\vT!(540,60)$) {
  \scsnowman[scale=15,hat,snow,arms,buttons,muffler=red]};

全体のソースは以下のようになる。

[sample01.tex]
% ↓以下のコマンドで変換
% tcspingif -e pdflatex -d 72 -t 32 -b 1 sample01.tex
\documentclass{standalone}
\usepackage{xcolor,tikz,pgfmath}
\usepackage{scsnowman}% ゆきだるま☃!
\usetikzlibrary{calc}% 内分点表記を使う
% \vT は"現在時刻"(0~1の範囲)
\pgfmathsetmacro{\vT}{\number\faStopTicks/\number\faAllTicks}
\begin{document}
\begin{tikzpicture}[x=1bp,y=1bp]
\useasboundingbox (0,0) rectangle (600,120);
\node at ($(60,60)!\vT!(540,60)$) {
  \scsnowman[scale=15,hat,snow,arms,buttons,muffler=red]};
\end{tikzpicture}
\end{document}

このLaTeXソースを以下のコマンドで画像に変換する。

tcspingif -e pdflatex -d 72 -t 32 -b 1 sample01.tex

※以下、ソースリストの先頭のコメントに変換用のコマンドラインを載せる。

実際に画像を表示して結果を確認してみよう。

f:id:zrbabbler:20200818191540g:plain

これで「自分で動きを定義する」ことができた。あとは“\vTと描画の間の処理”を適宜増やしていくことで、好きな動きを表現するだけである。

まずは「ゆきだるま☃が始点と終点の間を往復する」ようにしてみよう。pgfmathの式の三項演算子を利用して、「\vTが0→1を動くと、\vAが0→1→0と動く」ような新たな変数\vAを定義する。

\cLet{\vA}{(\vT<0.5) ? 2*\vT : 2-2*\vT}

あとは、\nodeの座標において、\vTの代わりに\vAを使う。

\node at ($(60,60)!\vA!(540,60)$) {

全体のソースは以下の通り。ここで、\pgfmathsetmacroという命令名は長いので、\cLetというマクロで別名定義している。

[sample02.tex]
% tcspingif -e pdflatex -d 72 -t 32 -b 1 sample02.tex
\documentclass{standalone}
\usepackage{xcolor,tikz,pgfmath,scsnowman}
\newcommand{\cLet}{\pgfmathsetmacro}
\usetikzlibrary{calc}
\cLet{\vT}{\number\faStopTicks/\number\faAllTicks}
\cLet{\vA}{(\vT<0.5) ? 2*\vT : 2-2*\vT}
\begin{document}
\begin{tikzpicture}[x=1bp,y=1bp]
\useasboundingbox (0,0) rectangle (600,120);
\node at ($(60,60)!\vA!(540,60)$) {
  \scsnowman[scale=15,hat,snow,arms,buttons,muffler=red]};
\end{tikzpicture}
\end{document}

f:id:zrbabbler:20200818191556g:plain

今度は「ゆきだるま☃の動きをもう少し自然にする」ために、水平位置の変数\vA\vTの二次関数に変えてみる。

\cLet{\vB}{(\vT<0.5) ? 2*\vT : 2-2*\vT}
\cLet{\vA}{(\vA<0.5) ? 2*\vB*\vB : -2*\vB*\vB+4*\vB-1}
[sample03.tex]
% tcspingif -e pdflatex -d 72 -t 32 -b 1 sample03.tex
\documentclass{standalone}
\usepackage{xcolor,tikz,pgfmath,scsnowman}
\newcommand{\cLet}{\pgfmathsetmacro}
\usetikzlibrary{calc}
\cLet{\vT}{\number\faStopTicks/\number\faAllTicks}
\cLet{\vB}{(\vT<0.5) ? 2*\vT : 2-2*\vT}
\cLet{\vA}{(\vA<0.5) ? 2*\vB*\vB : -2*\vB*\vB+4*\vB-1}
\begin{document}
\begin{tikzpicture}[x=1bp,y=1bp]
\useasboundingbox (0,0) rectangle (600,120);
\node at ($(60,60)!\vA!(540,60)$) {
  \scsnowman[scale=15,hat,snow,arms,buttons,muffler=red]};
\end{tikzpicture}
\end{document}

f:id:zrbabbler:20200818191616g:plain

さらに、ゆきだるま☃に回転を加えてみよう。これは\node命令にrotateパラメタを追加するだけである。

[sample04.tex]
% tcspingif -e pdflatex -d 72 -t 32 -b 1 sample04.tex
\documentclass{standalone}
\usepackage{xcolor,tikz,pgfmath,scsnowman}
\newcommand{\cLet}{\pgfmathsetmacro}
\usetikzlibrary{calc}
\cLet{\vT}{\number\faStopTicks/\number\faAllTicks}
\cLet{\vB}{(\vT<0.5) ? 2*\vT : 2-2*\vT}
\cLet{\vA}{(\vA<0.5) ? 2*\vB*\vB : -2*\vB*\vB+4*\vB-1}
\begin{document}
\begin{tikzpicture}[x=1bp,y=1bp]
\useasboundingbox (0,0) rectangle (600,120);
\node[rotate=-360*\vA] at ($(60,60)!\vA!(540,60)$) {
  \scsnowman[scale=15,hat,snow,arms,buttons,muffler=red]};
\end{tikzpicture}
\end{document}

f:id:zrbabbler:20200818191635g:plain

もっと動かしてみる

某ZR氏のツイッタァーでよくみるやつ

「無限に並んだゆきだるま☃が流れている」画像。

  • ゆきだるま☃を8つ横に並べる。
  • その上で、ゆきだるま☃6つ分の大きさの“ビューポート”を右方向に移動させる。これで、ゆきだるま☃が左に流れているように見える。
  • 開始時と終了時の状態を同じにしておくと、アニメガ反復したときに、ゆきだるま☃が滞りなく流れているように見える。
[sample05.tex]
% tcspingif -e pdflatex -d 72 -t 32 -b 1.5 sample05.tex
% ↑少し速度を上げた
\documentclass{standalone}
\usepackage{xcolor,tikz,pgfmath,scsnowman}
\newcommand{\cLet}{\pgfmathsetmacro}
\cLet{\vT}{\number\faStopTicks/\number\faAllTicks}
\begin{document}
\begin{tikzpicture}[x=1bp,y=1bp]
% ビューポート左下座標を (0,0)→(200,0) と動かす.
% "+(...)"は相対指定.
\useasboundingbox (200*\vT,0) rectangle +(600,120);
% 赤マフラー☃と青マフラー☃を交互に4回描く
\foreach \vJ in {0,...,3} {
  \node at (50+200*\vJ,60) {
    \scsnowman[scale=14,hat,snow,arms,buttons,muffler=red]};
  \node at (150+200*\vJ,60) {
    \scsnowman[scale=14,hat,arms,buttons,muffler=blue,
        sweat,mouthshape=tight]};
}
\end{tikzpicture}
\end{document}

f:id:zrbabbler:20200818191651g:plain

増殖ゆきだるま☃

“現在時刻”ではなく“現在何フレーム目か”の値を利用した例。

% \vN は"現在何フレーム目か"の値
\pgfmathsetmacro{\vN}{\number\faStopTicks}
  • フレーム数を5とする。
  • nフレーム目(n=0~4)で「n+1個のゆきだるま☃」を描く。
[sample06.tex]
% tcspingif -e pdflatex -d 72 -t 5 -b 1 sample06.tex
% ↑フレーム数(-t)は5で固定
\documentclass{standalone}
\usepackage[svgnames]{xcolor}
\usepackage{tikz,pgfmath,scsnowman}
\newcommand{\cLet}{\pgfmathsetmacro}
% \vN は"現在何フレーム目か"の値
\cLet{\vN}{\number\faStopTicks}
\begin{document}
\begin{tikzpicture}[x=1bp,y=1bp]
\useasboundingbox (0,0) rectangle (600,120);
\foreach \vJ in {0,...,\vN} {% 上限が \vN
  \node at (60+120*\vJ,60) {
    \scsnowman[scale=15,muffler=Red,hat=Green,
        buttons=RoyalBlue,arms=Brown,snow=SkyBlue]};
}
\end{tikzpicture}
\end{document}

f:id:zrbabbler:20200818191730g:plain

ゆきだるま☃ウェーブ

pgfmathでは初等関数が利用できるので、sin関数を使った例。

[sample07.tex]
% tcspingif -e pdflatex -d 72 -t 32 -b 1.5 sample07.tex
\documentclass{standalone}
\usepackage{xcolor,tikz,pgfmath,scsnowman}
\newcommand{\cLet}{\pgfmathsetmacro}
\cLet{\vT}{\number\faStopTicks/\number\faAllTicks}
\begin{document}
\begin{tikzpicture}[x=1bp,y=1bp]
\useasboundingbox (0,0) rectangle (600,120);
\foreach \vJ in {0,...,9} {
  % xcolorの混合率の値, 100→0 と動く.
  \cLet{\vC}{100*(1-\vJ/9)}
  % 垂直変位. 水平位置と時刻で決まる.
  \cLet{\yD}{30*sin(360*(\vJ/9+\vT))}
  \node at (30+60*\vJ,60+\yD) {
    \scsnowman[scale=9,arms,buttons,
      muffler=red!\vC!blue,hat=red!\vC!blue!75!black]};
}
\end{tikzpicture}
\end{document}

f:id:zrbabbler:20200818191705g:plain

もっともっと動かしてみる

過去にツイッタァーなどで披露した「動く☃画像」のソースを出しておく。

世界平和

[sample08.tex]
% tcspingif -e pdflatex -d 200 -t 32 -b 0.5 sample08.tex
% ↓200dpiで480×480ピクセルになるように調整
\documentclass[margin=1.36bp]{standalone}
\usepackage{xcolor,tikz,pgfmath}
\usepackage{scsnowman,tikzducks}
\definecolor{mc0}{rgb}{0.8,0,0}
\definecolor{mc1}{rgb}{0.5,0.3,0}
\definecolor{mc2}{rgb}{0.2,0.6,0}
\definecolor{mc3}{rgb}{0,0.7,0.1}
\definecolor{mc4}{rgb}{0,0.4,0.4}
\definecolor{mc5}{rgb}{0,0.1,0.7}
\definecolor{mc6}{rgb}{0.2,0,0.6}
\definecolor{mc7}{rgb}{0.5,0,0.3}
\scsnowmandefault{hat,arms,buttons,snow,scale=6}
\newcommand{\cLet}{\pgfmathsetmacro}
\cLet{\vT}{\number\faStopTicks/\number\faAllTicks}
\cLet{\vD}{\vT*360}
\pagecolor{black!0.5}
\begin{document}%↓座標単位は既定のまま(1cm)
\begin{tikzpicture}
\useasboundingbox (-3,-3) rectangle (3,3);
\node at (-  0+\vD:2) {\scsnowman[muffler=mc0]};
\node at (- 45+\vD:2) {\scsnowman[muffler=mc1]};
\node at (- 90+\vD:2) {\scsnowman[muffler=mc2]};
\node at (-135+\vD:2) {\scsnowman[muffler=mc3]};
\node at (-180+\vD:2) {\scsnowman[muffler=mc4]};
\node at (-225+\vD:2) {\scsnowman[muffler=mc5]};
\node at (-270+\vD:2) {\scsnowman[muffler=mc6]};
\node at (-315+\vD:2) {\scsnowman[muffler=mc7]};
\node at (0,0) {\tikz[scale=0.4]{\duck}};
\end{tikzpicture}
\end{document}

f:id:zrbabbler:20200818191843g:plain

フルパワー清涼感

これは普通にtcfaspinを使っている。

[sample09.tex]
% tcspingif -e pdflatex -d 72 -t 32 -b 3 sample09.tex
\documentclass{standalone}
\usepackage{ifthen,xcolor,tikz,pgfmath,scsnowman}
\newcommand{\cLet}{\pgfmathsetmacro}
\pagecolor{blue!30!green!6}%←背景色
\colorlet{coolhat}{blue!80!green!75!black}
\colorlet{coolmuffler}{blue!50!green}
\colorlet{coolarms}{green!30!black!50}
\colorlet{coolsnow}{blue!80!green!40}
\usepackage{tcfaspin}%←回します☃
\newcommand\cESnowman[1]{%
  \cPutSnowman{muffler=coolmuffler,hat=coolhat,#1}}
\newcommand\cOSnowman[1]{%
  \cPutSnowman{muffler=coolhat,hat=coolmuffler,#1}}
\newcommand\cPutSnowman[1]{%
  \node at (\vX*100+50,\vY*100+50) {%
    \faSpin{\scsnowman[%
      scale=10,arms=coolarms,snow=coolsnow,buttons=coolsnow,
      #1]}};}
\begin{document}
\begin{tikzpicture}[x=1bp,y=1bp]
\useasboundingbox (0,0) rectangle (600,600);
\foreach \vX in {0,1,...,5}
  \foreach \vY in {0,1,...,5} {
    % pgfmathの条件演算子だけだと実行の分岐ができないので
    % ifthenを併用する. ただし, \ifthenelse の条件文には
    % 式が書けず不便なので, "pgfmathで真偽値を求めて変数に
    % 入れて \ifthenelse でそれを見る"ことにする.
    \cLet{\vBroom}{\vX==5 && \vY==2}
    \cLet{\vNote}{\vX==4 && \vY==4}
    \cLet{\vOdd}{isodd(\vX+\vY)}
    \ifthenelse{\vBroom>0}{% ホウキ付き
      \cOSnowman{broom=coolarms!75!black}
    }{\ifthenelse{\vNote>0}{% ♪付き
      \cESnowman{note=coolsnow!90}
    }{\ifthenelse{\vOdd>0}{
      \cOSnowman{}
    }{\cESnowman{}}}}
  }
\end{tikzpicture}
\end{document}

f:id:zrbabbler:20200818192013g:plain

ホウキ付きゆきだるま☃の日のやつ

[sample10.tex]
% tcspingif -e pdflatex -d 72 -t 64 -b 0.5 sample10.tex
\documentclass{standalone}
\usepackage{xcolor,tikz,pgfmath,scsnowman}
\usepackage{type1cm}
%↓アホなフォント
\newcommand*{\fAho}{\usefont{OT1}{cmfr}{m}{it}{}}
\newcommand*{\cSize}[1]{\fontsize{#1}{0}\selectfont}
\definecolor{mydbrown}{rgb}{0.3,0.0,0.0}
\definecolor{mylbrown}{rgb}{0.7,0.5,0.0}
\newcommand{\cLet}{\pgfmathsetmacro}%
\cLet{\vT}{\number\faStopTicks/\number\faAllTicks}
\cLet{\vOR}{6}% ☃の軌道の半径
\cLet{\yW}{-2}% ☃のy座標
\cLet{\vD}{2}% キャンバスの位置(z=−2にある)
\cLet{\vA}{\vT*360+2}% 軌道上の角度
%↑開始時に中央に見えるように開始地点を補正している
\cLet{\zW}{\vOR*(1-cos(\vA))}% ☃のz座標
\cLet{\xW}{\vOR*(sin(\vA))}% ☃のx座標
\cLet{\vCS}{\vD/(\vD+\zW)}% (一点透視による倍率)
\cLet{\xC}{\vCS*\xW}% ☃のキャンバス上x座標
\cLet{\yC}{\vCS*\yW}% ☃のキャンバス上y座標
\cLet{\vSS}{\vCS*24}% ☃のキャンバス上の大きさ
%↑実際の大きさ(scale)が24
\newcommand{\cSnowman}[1]{%
  \scsnowman[scale=#1,
    arms=mydbrown,muffler=red,broom=mylbrown]}
\begin{document}
\begin{tikzpicture}[x=1bp,y=1bp]
  \useasboundingbox (0,0) rectangle (256,320);
  \node at (\xC*50+128,\yC*50+286) {\cSnowman{\vSS}};
  %↓例の日めくりカレンダーのアレ
  \node at (128,260) {%
    \cSize{29}\fAho August};
%  \node at (128,168) {%
%    \cSize{190}\fAho 18};
  \node[text=black!50] at (128,68) {%
    \cSize{10}\fAho Broom Snowman Day};
  \node at (128,33) {%
    \cSize{33}\fAho Tuesday};
\end{tikzpicture}
\end{document}

f:id:zrbabbler:20200818191245g:plain

Happy Snowman’s Day☃

[snowmansday2020.tex]
% tcspingif.pl -e pdflatex -t 64 -d 108 snowmansday2020.tex
\documentclass{standalone}
\usepackage[T1]{fontenc}
\usepackage{lmodern,pifont}
\usepackage{xcolor,scsnowman,tikzducks,pgfmath}
\newcommand*{\cLet}{\pgfmathsetmacro}
\setlength{\unitlength}{1bp}
%↓背景色
\colorlet{bgcolor}{blue!30!green!10}
\pagecolor{bgcolor}
%
\cLet{\xCS}{80}\cLet{\yCS}{320}% キャンバスサイズ
\cLet{\vT}{\number\faStopTicks/\number\faAllTicks}
\cLet{\vST}{(\vT-0.5)*2}
\cLet{\yH}{\yCS-\xCS}
\cLet{\xT}{\xCS/2}% ☃のx座標
%↑ただし実際にはずらして(2つ)置いている
\cLet{\yT}{\yH+\xCS/2-\yH*\vST*\vST}% ☃のy座標
%↑時刻の二次関数
\cLet{\vEA}{exp(\vST*5)}\cLet{\vEB}{exp(\vST*(-5))}
\cLet{\vRA}{((\vEA-\vEB)/(\vEA+\vEB)*0.5+0.5)*4}% 回転
%↑時刻をアフィン変換してtanhしたもの. 4回転している.
\cLet{\vRB}{mod(round(\vRA*1800)/5,360)}% 回転(度数)
%↑0.2度の整数倍に丸めた
%
\newcommand{\cTwo}{% アヒル的"2"
  \makebox(0,0){\tikz[scale=0.24]{\duck[%
    body=blue!50!green,bill=blue!50!green]}}}
\newcommand{\cZero}{% 雪結晶的"0"
  \fontsize{16}{0}\selectfont
  %↓重ね書きはイマイチ
  \makebox(0,1){\color{blue!50!green}\ding{108}}%
  \makebox(1,0){\color{bgcolor}\scalebox{0.8}{\ding{93}}}}
%
\begin{document}%↓なんとpicture環境
\begin{picture}(400,360)(0,-20)
\put(120,\yT){\makebox(0,0){\rotatebox[origin=c]{-\vRB}{%
  \scsnowman[hat=blue!80!green!75!black,
    muffler=blue!50!green,
    arms=green!30!black!50,
    snow=blue!80!green!40,
    scale=12]}}}
\put(280,\yT){\makebox(0,0){\rotatebox[origin=c]{\vRB}{%
  \scsnowman[hat=blue!50!green,
    muffler=blue!80!green!75!black,
    arms=green!30!black!50,
    snow=blue!80!green!40,
    scale=12]}}}
\put(200,260){\makebox(0,0){\textcolor{blue!50!green!60}{%
  \fontsize{48}{0}\sffamily\bfseries\ding{100}}}}
\put(200,200){\makebox(0,0){\textcolor{blue!85!black}{%
  \fontsize{28}{40}\sffamily\bfseries\itshape
  {Happy Snowman's Day}}}}
\put(200,144){\cTwo}
\put(200,112){\cZero}
\put(200,080){\cTwo}
\put(200,048){\cZero}
\end{picture}
\end{document}

f:id:zrbabbler:20200818192052g:plain


  1. TeX言語者向け解説】整数定数として用いるために、制御綴\faStopTicksを値18をもつchardefトークンと定義している。\faAllTicksも同様。

  2. TikZを読み込むとpgfmathが内部で読み込まれるので、この機能を利用するのが手っ取り早いだろう。(そして、scsnowmanパッケージはTikZを内部で読み込む。)

  3. TeX言語者向け解説】\faStopTicksはchardefトークンであるため\number\faStopTicksを1回展開するとその整数値の十進数字列に展開される。pgfmathの式の中で「数字列に展開されるトークン列は数値として扱われるため、この方法で値が読み出せるわけである。ちなみに、chardefトークンそのものはpgfmathの式の中で数値として扱われない。