在最初學習 LaTeX 插圖的時候,我就發現,基本的 LaTeX 手段不支持 GIF 格式的動圖。雖然一直保持對此的好奇,但是因為沒有實際需要,再加上「論文等文稿不適合插入動畫」的論調,所以一直沒有去探究可行性和解決辦法。 前段時間,因為制作一個幻燈片(離散卷積和卷積神經網絡)的需要,不得不插入動畫以演示「卷積」的過程和效果。于是就借此機會,摸索了如何在 LaTeX 中插入動畫。此文是對上述過程的歸納總結。 本文主要介紹兩部分內容

  • 如何在 LaTeX 中插入 GIF 格式的動圖;
  • 如何在 LaTeX 中插入 TikZ 代碼繪制的動畫。
以及介紹一些運用動畫效果實現的黑科技效果。  

主角登場——animate 宏包

要在 LaTeX 中插入動畫,首先要考慮輸出文件類型是否支持這樣的需求。否則,插入動畫就變成了無根之木、無源之水。 目前來說,主流的 LaTeX 輸出格式是 PDF。PDF 的全稱是 Portable Document Format。它是最早由 Adobe 公司提出的文檔格式標準;因其優良的特性,現已逐漸發展成為固定格式文本交換的事實標準。 自 2000 年始,1.3 版本的 PDF 開始支持 JavaScript。而后,相關特性在后續版本中不斷完善。因此,若是 PDF 瀏覽器支持相關 API,則可以利用 JavaScript 在 PDF 文稿中做到很多事情——當然,包括了動畫。因此,在 PDF 中插入動畫是可能的。 Alexander Grahn 根據上述 API,開發了 animate 宏包。該宏包利用 JavaScript,允許用戶在 LaTeX 文稿中插入動畫,并在支持 JavaScript 的 PDF 閱讀器中查看。特別喜人的是,animate 宏包支持目前最流行的幾種編譯方式;因此,你無須像使用 media9 之類的宏包那樣,被編譯方式絆住腳。目前 animate 支持的編譯方式有
  • pdfLaTeX / LuaLaTeX
  • LaTeX -> dvips -> ps2pdf / LaTeX -> dvipdfmx
  • XeLaTeX -> xdvipdfmx
當然,支持這些特性的 PDF 閱讀器則比較少。目前已知的有
  • Adobe Acrobat / Reader
  • PDF-XChange
  • Foxit Reader

使用 animate 宏包

此處不表如何安裝 animate 宏包,我們來看如何使用 animate 宏包。 和其它宏包一樣,在 LaTeX 中使用 \usepackage[<options>]{animate} 即可引入 animate 宏包。唯獨需要注意的有三點
  • 必須在引入 animate 宏包之前,顯式地引入 graphicx 宏包;
  • 若希望使用 LaTeX -> dvipdfmx 這一工具鏈,則需要給 graphicxanimate 宏包都加上 dvipdfmx 選項(原因);
  • 和交叉引用中遇到的問題一樣,使用 animate 宏包創建動畫,也需要兩次編譯(第一次創建 JavaScript 內容,第二次在具體位置插入內容)。
animate 宏包支持不少參數。不過,僅有 dvipdfmxxetex 兩個驅動選項只能在載入宏包時使用——而其中僅有 dvipdfmx 是必須的。宏包支持的其他參數,具體的命令、環境也都支持。若是在載入宏包是提供這些參數,相當于給命令、環境設置了「默認值」。因此,這部分參數放在之后具體介紹。

使用 animate 宏包插入 GIF 動圖——\animategraphics

animate 提供了 \animategraphics 命令,用于插入「一系列」的圖片,而后將他們組成動畫——相當于插入了動圖。 具體來說,其命令是
\animategraphics[<options>]{<frame rate>}{<file basename>}{<first>}{<last>}
此處 options 是命令的參數,主要用于控制動畫的各種效果,具體參數將在下一節中介紹。先前我們講過這部分參數大都也可用于宏包選項。frame rate 的單位是 Hz,表示 1 秒鐘內,「放映」多少幀。 前面說該命令用于插入一系列圖片,animate 宏包要求這一系列圖片有共同的文件名前綴,而后以數字編號表述其順序。file basename 選項用于記錄該前綴;firstlast 則是這一系列圖片編號的起止。 關于文件名后綴,animate 也做了具體要求。首先,animate 要求插入的圖片,后綴名必須是小寫。其次,animate 對文件搜索順序做了規定(實際上是 graphicx 的規定):
  • pdfLaTeX / LuaLaTeX:pdf, mps, png, jpg, jpeg, jbig2, jb2, jp2, j2k, jpx
  • XeLaTeX / LaTeX -> dvipdfmx:pdf, mps, eps, ps, png, jpg, jpeg, bmp
  • LaTeX -> dvips -> ps2pdf:eps, mps, ps
注意到兩件事情:一,上述后綴名中沒有 gif,這意味著不能直接插入 GIF 格式的動圖;二,前面提到,使用 animate 宏包插入動圖實際是插入一系列的圖片,這意味著我們需要將 GIF 格式的動圖,預先轉換成一系列符合要求的格式之圖片。 轉換格式需要用到 ImageMagick 這一開源的工具。安裝它,Mac 用戶可以使用 brew install ImageMagick,Linux 用戶可以使用各自的包管理器,Windows 用戶則需要下載安裝。 安裝好 ImageMagick 之后,我們就可以用它提供的 convert 命令將 GIF 格式的動圖逐幀地切分成一系列圖片了。假設你的目標圖片是 foo.gif,那么使用如下命令可以得到一系列圖片:foo-0.png, foo-1.png, foo-2.png, …
convert foo.gif -coalesce foo.png
而后,我們就可以用 animate 提供的 \animategraphics 命令插入動圖了。
\documentclass{article}
\usepackage{graphicx}
\usepackage{animate}
\begin{document}
\animategraphics{24}{foo-}{0}{300}
\end{document}
 
圖片引用自 Wikipedia

animate 選項

animate 宏包提供的選項,按照適用范圍可以分為三類:只能用于宏包的選項、只能用于接口的選項(命令和環境)、二者皆適用的選項。現分別介紹。
這里僅介紹其中重要的部分,未盡之詳細,請參考 animate 的宏包文檔。

只能用于宏包的選項

  • dvipdfmx:驅動選項,表示用戶希望使用 LaTeX -> dvipdfmx 進行編譯。
  • xetex:驅動選項,表示用戶希望使用 XeLaTeX -> xdvipdfmx 進行編譯。

只能用于接口的選項

這里的接口指的是 animate 宏包提供的用戶接口。例如我們已經見過的 \animategraphics 命令,以及下一節會介紹的 animateinline 環境。
  • label=<label text>:為 animate 對象指定唯一的標簽,可用于之后的 JavaScript 控制。
  • every=<num>:只為每個第 <num> 幀構建動畫,而忽略剩余的幀。

二者皆適用的選項

  • type=<type name>:使用指定的圖片類型(而不按照前面提到的順序搜索)。
  • poster[= first | <num> | last | none]:指定用于打印和默認展示的動畫幀,默認是第一幀。
  • autopause:當動畫所在頁不再呈現時,自動暫停動畫。
  • autoresume:當被暫停的動畫重新呈現時,自動恢復播放。
  • autoplay:當動畫所在頁在 PDF 閱讀器中呈現時,自動播放動畫。
  • loop:播放到最后一幀時,從第一幀開始繼續播放;如此往復。
  • palindrome:播放到最后一幀時,逐幀倒退;如此往復。
  • step:忽略 frame rate,只在每次點擊鼠標時播放一幀。
  • width=<h-size>, height=<v-size>, totalheight=<v-size>, keepaspectratio:按絕對長度縮放動畫的大小。
  • scale=<factor>:按比例縮放動畫的大小。
  • controls:展示用于控制動畫的按鈕。
  • begin=<begin content>, end=<end content>:僅用于 animateinline 環境,在每一幀的內容前后添加相應內容。

使用 animate 插入用戶繪制的動畫——animateinline 環境

之前我們介紹了如何使用 ImageMagick 拆分 GIF 動圖,而后用 \animategraphics 將拆分得到的一系列圖片在 LaTeX 中插入 PDF 文檔,變成動畫。然而,這可能存在幾個問題
  • 從它處獲取的 GIF 動圖可能侵犯他人版權;
  • 自行制作 GIF 動圖成本較高——不如直接用 TikZ 等工具繪制。
因此,這就引出了更高級的主題:使用 animate 宏包,插入用戶自行繪制的動畫。這需要引入一個新的用戶接口——animateinline 環境。它的語法是這樣的:
\begin{animateinline}[<options>]{<frame rate>}
... typeset material ...
\newframe[<frame rate>]
... typeset material ...
\newframe*[<frame rate>]
... typeset material ...
\newframe
\multiframe{<number of frames>}{[<variables>]}{
... repeated (parameterized) material ... }
\end{animateinline}
顯而易見,animateinline 環境的語法比 \animategraphics 要復雜得多。不過,仔細看的話,其實是有想通之處的。 首先,和 \animategraphics 命令一樣,animateinline 環境允許用戶通過 optionsframe rate 控制動畫的基本行為。不同之處在于,\animategraphics 的動畫內容是固定死的——由圖片提供,而 animateinline 的動畫內容則需要用戶在 LaTeX 代碼中逐幀繪制。因此,animateinline 環境提供了三個命令來輔助和控制這些內容。 \newframe\newframe* 的作用正如其名:結束上一幀并開始下一幀。唯一的不同在于,\newframe 會立即開始下一幀,而 \newframe* 則會暫停并等待用戶的點擊再開始下一幀。它們接受一個可選參數,以便在動畫的中途改變 frame rate\multiframe 則更為強大,它能提供類似循環的功能,并將「循環變量」傳遞到 \multiframe 內部供內部繪圖命令使用。具體來說,首先我們需要給出循環的次數 number of frames,而后指定循環變量 variables。循環變量的書寫格式如下
<variable name>=<initial value>+<increment>
這里,variable name 由若干個字母組成,不含 LaTeX 命令的反斜線。需要注意的是,variable name 的首字母是有意義的,它決定了變量的類型。
  • 整數:i, I
  • 浮點數:n, N, r, R
  • 長度:d, D
initial value 表示循環變量的初始值,而 increment 表示循環變量在每次循環末尾自增的值。 因此,我們可以寫出如下代碼
\multiframe{10}{iAngle=0+10, dLineWidth=3pt+-0.1pt}{
... repeated (parameterized) material ... }
這樣,循環會執行 10 次,同時帶有兩個循環變量:整型變量 iAngledLineWidth。前者從 0 開始,每次增加 10;后者從 3pt 開始,每次減少 0.1pt。而后,在循環體中(\multiframe 命令的內部),我們就可以使用 \iAngle\dLineWidth 獲得循環變量的值了。
\documentclass{article}
\usepackage{graphicx}
\usepackage{animate}
\usepackage{tikz}
\usetikzlibrary{positioning}
\tikzset{global scale/.style={
    scale=#1,
    every node/.append style={scale=#1}
  }
}
\tikzset{global xscale/.style={
    xscale=#1,
    every node/.append style={xscale=#1}
  }
}
\tikzset{global yslant/.style={
    yslant=#1,
    every node/.append style={yslant=#1}
  }
}
\newcommand{\twodimdrawcontent}[2]{%
\useasboundingbox (0, -6) rectangle (16.1, 3);
\begin{scope}[global yslant = -0.6, xshift = 1cm, yshift = -1cm]
  \node (A) at (0, 0) {};
  \node (B) at (3, 0) {};
  \node (C) at (3, 3) {};
  \node (D) at (0, 3) {};
  \draw (0,0) grid (3,3);
\end{scope}
\node[anchor = north, xscale = 2] at (2, -5) {Kernel};

\begin{scope}[xshift = 6cm, global yslant = -0.6, yshift = -2cm]
  \node (E) at (0cm + #1cm, 2cm - #2cm) {};
  \node (F) at (3cm + #1cm, 2cm - #2cm) {};
  \node (G) at (3cm + #1cm, 5cm - #2cm) {};
  \node (H) at (0cm + #1cm, 5cm - #2cm) {};
  \draw[fill, blue!20] (0cm + #1cm, 2cm - #2cm) rectangle (3cm + #1cm, 5cm - #2cm);
  \draw[fill, blue!50] (1cm + #1cm, 3cm - #2cm) rectangle (2cm + #1cm, 4cm - #2cm);
  \draw (0,0) grid (5,5);
\end{scope}
\node[anchor = north, xshift = 6cm, xscale = 2] at (2, -5) {Input};

\begin{scope}[xshift = 12cm, global yslant = -0.6, xshift = 1cm, yshift = -1cm]
  \node (I) at (0.5cm + #1cm, 2.5cm - #2cm) {};
  \draw[fill, red!20] (0cm + #1cm, 2cm - #2cm) rectangle (1cm + #1cm, 3cm - #2cm);
  \draw (0,0) grid (3, 3);
\end{scope}
\node[anchor = north, xshift = 12cm, xscale = 2] at (2, -5) {Output};

\draw[dashed, blue] (A.center) -- (E.center);
\draw[dashed, blue] (B.center) -- (F.center);
\draw[dashed, blue] (C.center) -- (G.center);
\draw[dashed, blue] (D.center) -- (H.center);

\draw[dashed, red] (E.center) -- (I.center);
\draw[dashed, red] (F.center) -- (I.center);
\draw[dashed, red] (G.center) -- (I.center);
\draw[dashed, red] (H.center) -- (I.center);
}
\begin{document}
\begin{animateinline}[
    loop,autopause,controls,
    buttonsize=1.2em,
    buttonbg=0.6:0.6:1,buttonfg=0.2:0.2:1,
    begin={\begin{tikzpicture}[global scale = 0.7, global xscale = 0.5, on grid]},
    end={\end{tikzpicture}}]{1.8}
    \multiframe{3}{icol=0+1}{%
      \xdef\icol{\icol}
      \xdef\irow{0}
      \whiledo{\lengthtest{\irow sp < 2sp}}{
        \twodimdrawcontent{\irow}{\icol}
        \newframe
        \pgfmathsetmacro{\irow}{\irow + 1}
        \xdef\irow{\irow}
      }
      \twodimdrawcontent{\irow}{\icol}
    }
\end{animateinline}
\end{document}
繪圖的核心代碼定義在 \twodimdrawcontent 這一命令當中。該命令的內容完全是 TikZ 的語法,不在此篇的涵蓋范疇中。因此不表。唯一需要注意的是,我們使用了 \useasboundingbox (0, -6) rectangle (16.1, 3); 限定每一個動畫幀的大小。這是因為 animateinline 會根據第一幀的大小來確定動畫的大小;若是每一幀大小不同,則可能出現某些幀顯示不全的現象。 我們仔細看 animateinline 環境中的參數。loop, autopause, controls 我們很熟悉了;buttomsize, buttonbg, buttonfg 顧名思義,是用來調整按鈕的樣式的;beginend 則在每一幀的內容前后加上了 tikzpicture 環境。這樣,我們可以直接使用 \twodimdrawcontent 來繪制動畫幀。 animateinline 環境的幀率(frame rate)是 1.8,這意味著每秒會播放 1.8 個動畫幀(每 5 秒播放 9 幀)。具體的動畫幀內容,則由 \multiframe 給出。 \multiframe 循環 3 次,對應循環變量為 icol。這是一個整型變量,從 0 開始每次自增 1。在循環體內,我們首先定義 \xdef\icol{\icol};這是因為,\multiframe 給出的循環變量,若直接傳給 \twodimdrawcontent 則無法正確展開。隨后我們將 \irow 定義為 0。接下來,我們使用 \whiledo 的循環。此處不使用 \multiframe 的原因是它無法嵌套。這一循環的變量是 \irow。它從 0 開始,每次循環末尾由 \pgfmathsetmacro{\irow}{\irow + 1}, \xdef\irow{\irow} 自增 1。\whiledo 循環兩次,內部用 \twodimdrawcontent{\irow}{\icol}, \newframe 制作出一幀動畫。最后在 \whiledo 循環的外部,畫出第三幀。 如此,就能得到我們在前文中的動畫效果了。
偏好使用 PSTricks 的用戶,可以參考 animate 宏包文檔里的示例。

一點彩蛋

\documentclass{article}
\usepackage{graphicx}
\usepackage{animate}
\newcommand{\myemph}[1]{%
  \begin{animateinline}[autoplay, loop]{.5}
  \emph{#1}
  \newframe[1.5]
  \relax
  \end{animateinline}}
\begin{document}
This is \myemph{important}!
\end{document}
選自:https://liam0205.me/2017/08/10/importing-animate-in-LaTeX/