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 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}
会
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}
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}