How to animate SVG paths as an animated PDF with animate?

This solution animates the strokes as if they were drawn with a pen, as requested by OP.

Update: SVG parser improved for better animation performance (standalone PDF and SVG animations) and output file size; LaTeX input simplified.

Click the image to run the animation interactively:

None of the known TeX engines supports SVG directly. So we have to convert it to a suitable format. For this purpose, an SVG parser was written in Perl that converts the SVG paths to Postscript. It only supports M, m, L, l, C and c SVG path operators. However, these seem to be sufficient for the present task.

The generated Postscript code makes use of the flattenpath operator which converts curves to sequences of straight lines when run through Ghostscript.

Ghostscript is run 2 times to get from a single-page Postscript file to a multi-page PDF file with the growing strokes.

In a final step the multipage PDF is assembled to a standalone animated PDF or SVG using \animategraphics.

These are the necessary steps, to be run in the terminal, starting from test.svg. (More examples can be found here: https://github.com/KanjiVG/kanjivg/releases .)

Unix:

svgpath2ps.pl test.svg > single.ps
ps2pdf single.ps > multi.ps 2>&1 # also writes `single.pdf' which we do not use further
ps2pdf multi.ps # creates `multi.pdf' we animate in the next step
pdflatex animatepath.tex
pdflatex animatepath.tex

Windows/DOS (Of course, perl.exe must be available on the system):

perl svgpath2ps.pl test.svg > single.ps
ps2pdf single.ps > multi.ps
ps2pdf multi.ps
pdflatex animatepath.tex
pdflatex animatepath.tex

The SVG parser svgpath2ps.pl:

#!/usr/bin/perl
$header=0;
$strokewd=1;
$strokelc=0;
$strokelj=0;
while(<>){
  chomp;
  if (/viewBox\s*=\s*"([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)"/){($ulx,$uly,$lrx,$lry)=($1,$2,$3,$4);}
  if (/width\s*=\s*"([0-9]+)"/){$wd=$1;}
  if (/height\s*=\s*"([0-9]+)"/){$ht=$1;}
  if (/stroke-width:\s*([^;\s]+)/){$strokewd=$1;}
  if (/stroke-linejoin:\s*round/){$strokelj=1;}
  if (/stroke-linejoin:\s*bevel/){$strokelj=2;}
  if (/stroke-linecap:\s*square/){$strokelc=2;}
  if (/stroke-linecap:\s*round/){$strokelc=1;}
  if (/<path.* d\s*=\s*/) {
    if (!$header){
      $header=1;
      print "%!PS-Adobe-3.0\n",
            "%%BoundingBox: $ulx $uly $lrx $lry\n",
            "%%EndComments\n",
            "/curpage 0 def\n",
            "/curpath [{}] def\n",
            "/setbbox {pop pop pop pop} def\n",
            "/printany {0 cvs print flush} def /printany load 0 256 string put\n",
            "/typepath {flattenpath{\n",
            "  gsave newpath curpath {exec} forall moveto false upath [ exch ] /curpath exch def grestore\n",
            "}{\n",
            "  gsave newpath curpath {exec} forall lineto false upath [ exch ] /curpath exch def grestore\n",
            "  /curpage curpage 1 add def\n",
            "  (\\n%%Page: ) print curpage printany ( ) print curpage printany (\\n) print\n",
            "  (0 $lry translate 1 -1 scale $strokewd setlinewidth $strokelj setlinejoin $strokelc setlinecap\\n) print\n",
            "  curpath {{printany ( ) print} forall} forall (stroke showpage) print\n",
            "}{}{} pathforall} def\n",
            "<</PageSize [$wd $ht]>> setpagedevice\n",
            "0 $lry translate 1 -1 scale $strokewd setlinewidth\n",
            "(%!PS-Adobe-3.0\\n) print\n",
            "(%%BoundingBox: $ulx $uly $lrx $lry\\n) print\n",
            "(%%EndComments\\n) print\n",
            "(<</PageSize [$wd $ht]>> setpagedevice) print\n";
    }
    s/^.*d\s*=\s*"([^"]*)".*$/$1/;
    s/-/ -/g;
    s/,/ /g;
    s/M\s?([0-9.-]+) ([0-9.-]+)/ $1 $2 moveto /g;
    s/m\s?([0-9.-]+) ([0-9.-]+)/ $1 $2 rmoveto /g;
    s/L\s?([0-9.-]+) ([0-9.-]+)/ $1 $2 lineto /g;
    s/l\s?([0-9.-]+) ([0-9.-]+)/ $1 $2 rlineto /g;
    s/C\s?([0-9.-]+) ([0-9.-]+) ([0-9.-]+) ([0-9.-]+) ([0-9.-]+) ([0-9.-]+)/ $1 $2 $3 $4 $5 $6 curveto /g;
    s/c\s?([0-9.-]+) ([0-9.-]+) ([0-9.-]+) ([0-9.-]+) ([0-9.-]+) ([0-9.-]+)/ $1 $2 $3 $4 $5 $6 rcurveto /g;
    s/^\s*//g;
    print;
    print "typepath stroke\n";
  }else{next;}
}
print "(\\n%%EOF\\n) print\n";
print "%%EOF\n";

The LaTeX-input animatepath.tex:

%\documentclass[dvisvgm]{standalone}
\documentclass{standalone}
\usepackage{animate,graphicx}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Adjust this if necessary!
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% basename (!) of PDF file with stroke segments
\def\fileWithStrokes{multi}
% frame rate
\def\FPS{20}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\begin{document}
  \animategraphics[
    controls={play,stop},
    buttonsize=2em,
    poster=last,
  ]{\FPS}{\fileWithStrokes}{}{}
\end{document}

Animated SVG (at the top of this answer) produced with:

latex animatepath.tex
latex animatepath.tex
dvisvgm --bbox=papersize --zoom=2 animatepath.dvi

Ok I cheated I did include the supplied SVG (and another) and I could have split the SVG to generate an inline animation

However this is a proof of concept that animated GIF or simple thumb-book flipping of pages is fit for wider audience.

IF we take a 会 GIF enter image description here such as https://www.learnchineseez.com/characters/learn-to-write-chinese/images/common-chinese-character-20.gif or a sequence of frames using the supplied SVG we can simply step through them manually at a pace to suit the user

Sequential gif pages will work in many image or web browsers without requiring javascript or other plugins .If they are stored as an image in a zip and renamed to .cbz they will work in e-book readers such as SumatraPDF which can also view them as static sequential GIF or PNG or even PDF ….

IF we are to animate them in a PDF wrapper then there are a select few readers that will allow java-script. I could not get animation to work in my acrobat reader but it did work in Foxit reader.

\documentclass[a4paper]{report}
\usepackage{graphicx}
\usepackage{svg}
\svgpath{{foo/}}
\usepackage{animate}

\begin{document}
\begin{figure}[htbp]
  \centering  \includesvg[width=\linewidth]{logo}  \par \addvspace{\baselineskip}
  \includesvg[width=0.3\linewidth]{Join} \caption{ To Join or meet}
\end{figure} 
\animategraphics[autoplay,loop]{1}{./foo/bar-}{0}{63}
\end{document}

会

enter image description here


I think this one comes close to what you want to achieve. Dynamically drawing the strokes is certainly possible but would require a lot more work.

Only works with Adobe Reader.

\documentclass{article}
\usepackage{animate,tikz}
\usetikzlibrary{svg.path}
\begin{document}
\begin{animateinline}[
  palindrome,
  autoplay,
  controls,
  begin={
    \begin{tikzpicture}[yscale=-1,very thick]
      \useasboundingbox (0,0) rectangle (3.8,4);
  },
  end={\end{tikzpicture}}
]{2}
\multiframe{6}{i=1+1}{%
  \ifnum\i>0
    \draw svg {M52.25,14c0.25,2.28-0.52,3.59-1.8,5.62c-5.76,9.14-17.9,27-39.2,39.88};
  \fi
  \ifnum\i>1
    \draw svg {M54.5,19.25c6.73,7.3,24.09,24.81,32.95,31.91c2.73,2.18,5.61,3.8,9.05,4.59};
  \fi
  \ifnum\i>2
    \draw svg {M37.36,50.16c1.64,0.34,4.04,0.36,4.98,0.25c6.79-0.79,14.29-1.91,19.66-2.4c1.56-0.14,3.25-0.39,4.66,0};
  \fi
  \ifnum\i>3
    \draw svg {M23,65.98c2.12,0.52,4.25,0.64,7.01,0.3c13.77-1.71,30.99-3.66,46.35-3.74c3.04-0.02,4.87,0.14,6.4,0.29};
  \fi
  \ifnum\i>4
    \draw svg {M47.16,66.38c0.62,1.65-0.03,2.93-0.92,4.28c-5.17,7.8-8.02,11.38-14.99,18.84c-2.11,2.25-1.5,4.18,2,3.75c7.35-0.91,28.19-5.83,40.16-7.95};
  \fi
  \ifnum\i>5
    \draw svg {M66.62,77.39c4.52,3.23,11,12.73,13.06,18.82};
  \fi
}
\end{animateinline}
\end{document}

enter image description here


You could use LPEG to parse the paths out of the SVG file. Requires LuaTeX.

\documentclass{article}
\usepackage{animate,tikz}
\usetikzlibrary{svg.path}
\directlua{
local lpeg = require"lpeg"
local C, Ct, P, S = lpeg.C, lpeg.Ct, lpeg.P, lpeg.S

local ws = S" \string\t\string\r\string\n"^0

local quot = P"\string\""
local opts = P"d" * ws * P"=" * ws * quot * C((1 - quot)^0) * quot
local path = P"<path" * ws * opts * ws * P"/>"
local other = (1 - path)^0
local svg = Ct(other * path * (other * path)^0)

local f = io.open("test.svg"):read("*all")
data = lpeg.match(svg,f)
}
\begin{document}
\begin{animateinline}[
  palindrome,
  autoplay,
  controls,
  begin={
    \begin{tikzpicture}[yscale=-1,very thick]
      \useasboundingbox (0,0) rectangle (3.8,4);
  },
  end={\end{tikzpicture}}
]{2}
\multiframe{6}{i=1+1}{%
  \directlua{
    for i = 1,\i\space do
        tex.sprint([[\string\draw\space svg {]] .. data[i] .. [[};]])
    end
    }
}
\end{animateinline}
\end{document}

There is also an XML parser in TeX Live called luaxml. Also requires LuaTeX.

\documentclass{article}
\usepackage{animate,tikz}
\usetikzlibrary{svg.path}
\directlua{
local dom = require"luaxml-domobject"
local f = io.open("test.svg"):read("*all")
local obj = dom.parse(f)
data = obj:query_selector("path")
}
\begin{document}
\begin{animateinline}[
  palindrome,
  autoplay,
  controls,
  begin={
    \begin{tikzpicture}[yscale=-1,very thick]
      \useasboundingbox (0,0) rectangle (3.8,4);
  },
  end={\end{tikzpicture}}
]{2}
\multiframe{6}{i=1+1}{%
  \directlua{
    for i = 1,\i\space do
        tex.sprint([[\string\draw\space svg {]] .. data[i]:get_attribute("d") .. [[};]])
    end
    }
}
\end{animateinline}
\end{document}

Tags:

Animate