10

I have a bunch of paths for a Kanji character and a simple one named test.svg is given as follows.

<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
  <style>
      path {
          fill: none;
          stroke: black;
          stroke-width: 3;
      }
  </style>

  <path d="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" />

  <path d="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" />

  <path d="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" />

  <path d="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" />

  <path d="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" />

  <path d="M66.62,77.39c4.52,3.23,11,12.73,13.06,18.82" />

</svg>

Question

Is it possible to import these paths as an animated PDF with animate such that each frame shows a progressive step to write the character. For example, the first frame only shows the first path, the second frame shows the first two paths, etc.

Edit

An animation in an HTML for testing.

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<body>
    <object type="image/svg+xml" data="test.svg"></object>

    <p><button class="animate">Animate</button></p>
    <script>
        (function () {
            var button = document.querySelector('.animate');
            button.onclick = function (event) {
                var object = document.querySelector("object");
                var doc = object.contentDocument;
                var paths = doc.querySelectorAll('path');
                for (var i = 0; i < paths.length; i++) {
                    var path = paths[i];
                    var length = path.getTotalLength();
                    // Clear any previous transition
                    path.style.transition = path.style.WebkitTransition = 'none';
                    // Set up the starting positions
                    path.style.strokeDasharray = length + ' ' + length;
                    path.style.strokeDashoffset = length;
                    // Trigger a layout so styles are calculated & the browser
                    // picks up the starting position before animating
                    path.getBoundingClientRect();
                    // Define our transition
                    path.style.transition = path.style.WebkitTransition = 'stroke-dashoffset 2s ease-in-out ' + (2 * i) + 's';
                    // Go!
                    path.style.strokeDashoffset = '0';
                }
            };
        }());
    </script>
</body>
</html>
  • The HTML animation does not do anything for me in Firefox and throws an error saying 'doc is null" – marczellm Feb 10 at 17:03
  • I personally tend to go retro and would open in SumatraPDF 29 strokes then in SumatraPDF save that as 29 page PDF and opening either the pdf or 29 frame gif use my space bar to step through at my own pace. since programmed animation (javascript or otherwise) is a distinct no no – user170109 Feb 13 at 2:49
5
+500

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.

Unix:

svgpath2ps.pl test.svg > single.ps
ps2pdf single.ps > multi.ps 2>&1 # also writes `single.pdf' which we do not use further
pd2pdf 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
pd2pdf multi.ps
pdflatex animatepath.tex
pdflatex animatepath.tex

The SVG parser svgpath2ps.pl:

#!/usr/bin/perl
$header=0;
$strokewd=1;
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 (/<path d=/) {
    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",
            "/printany {0 cvs print flush} def /printany load 0 256 string put\n",
            "/typepath {flattenpath{\n",
            "  gsave newpath curpath {exec} forall moveto .getpath /curpath exch def grestore\n",
            "}{\n",
            "  gsave newpath curpath {exec} forall lineto .getpath /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\\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/^[^"]*"([^"]*)".*$/$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,preview]{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
  • @ArtificialStupidity . Thanks a lot! While it works quite well as PDF animation in Acrobat Reader and as SVG in Blink-based Web browsers, I am still not quite satisfied with its performance in Firefox. I am still trying to improve it (by removing the need of a timeline which seems to slow down the animation). – AlexG Feb 15 at 8:31
  • @ArtificialStupidity . Done. Now, the animation performs nicely in all popular bowsers. – AlexG Feb 15 at 15:51
  • 1
    Brilliant detailed description, of steps needed , puts mine to shame. +1 – user170109 Feb 15 at 15:55
  • I did not get notified with your @ArtificialStupidity markdown. Thanks! – Money Oriented Programmer Feb 15 at 16:47
6

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}
  • How to load it from external SVG files because your current answer needs another step to prepare the input file? – Money Oriented Programmer Feb 13 at 1:43
  • @ArtificialStupidity Just copy the paths into \draw svg. You could also write an LPEG parser, to parse out the paths. – Henri Menke Feb 13 at 1:49
6

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

5

I started from Henri Menke's answer and implemented the drawing automatically using lualatex:

  • first install lua package xml2lua for xml parsing, using command luarocks install xml2lua
  • this package must be loaded, but it is not in texmf tree so you must load luapackageloader package and add to variable package.path the directory where xml2lua was installed
  • the code parses the svg file as an xml file
  • then it extracts various informations (bounding box, styles) and the paths
  • then it generates the latex animation

Note that, in order to have the good scale, the x and y must be put to 1pt.

Here is the program to be compiled with lualatex:

  \documentclass{standalone}

\usepackage{tikz}
\usetikzlibrary{svg.path}
\usepackage{animate}
\usepackage{luacode}
\usepackage{luapackageloader}

% execute before: sudo luarocks install xml2lua 
\begin{luacode*}
package.path="/usr/share/lua/5.3/?.lua;"..package.path -- change if necessary
xml2lua = require("xml2lua")
local handler = require("xmlhandler.tree")
local parser = xml2lua.parser(handler)
function animate_kanji_character(filename)
    local paths={} 
    local line_width=1
    local color="black"

    -- read svg file
    local file = io.open(filename, "rb") 
    if not file then return nil end
    local content = file:read "*a"
    file:close()
    parser:parse(content)

    local svg=handler.root.svg
    -- extract the boundingbox
    local bbox=""
    for x1,y1,x2,y2 in string.gmatch(svg._attr.viewBox,"(%d+)%s+(%d+)%s+(%d+)%s+(%d+)") do
        bbox="("..x1..","..y1..") rectangle ("..x2..","..y2..")"
    end
    -- extract styles (color and line width)
    for clr in string.gmatch(svg.style,"stroke: (%a*)") do
        color=clr
    end
    for lw in string.gmatch(svg.style,"stroke.width: (%d*)") do
        line_width=tonumber(lw,10)
    end
    -- store paths
    for i,path in pairs(svg.path) do
        paths[i]=path._attr.d -- we store the i-th path
    end

    -- latex printing
    tex.print("\\begin{animateinline}[poster="..(#paths-1)..",autoplay,loop,begin={\\begin{tikzpicture}[y=1pt,x=1pt,yscale=-1] \\useasboundingbox "..bbox..";},end={\\end{tikzpicture}}]{1}")
    -- print paths
    for i=1,#paths do
        if i>1 then tex.print("\\newframe") end
        for j=1,i do
            tex.print("\\draw[color="..color..",line width="..line_width.."pt] svg {"..paths[j].."};")
        end
    end
    tex.print("\\end{animateinline}")
end
\end{luacode*}


\begin{document}

\directlua{animate_kanji_character("test.svg")}

\end{document}

enter image description here

  • Does xml2lua not support CSS queries? Parsing out values by hand is quite fragile. – Henri Menke Feb 13 at 2:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.