Friday 22 July 2011

Detailed Tikz Animations

I set out my workflow to create animations using the Tikz graphical language in a previous post. What follows is, as promised, the individual Ubuntu commands needed to actually implement that workflow:

First of all we need to create a decent image in ktikz. This code works well for this purpose:

\usetikzlibrary{calc,through, intersections,decorations.text}
\begin{tikzpicture} [scale=3]
\def\myangle{117};
\clip (-1.5,-1.5) rectangle (1.5,1.5);
\coordinate (A) at (0,0);
\coordinate (B) at (1,0);
\node [blue, name path=blue_circle,draw,circle through=(B)] at (A) {};
\draw [black, fill] (A) circle (1pt) node [below] {\tiny centre};
\draw [red, dashed] (A) -- (B);
\path [name path=radius, rotate=\myangle] (A) -- ++(1.5,0);
\draw [red, ->] ($(A)+(0.5,0)$) arc (0:\myangle:0.5cm);
\path [decorate,decoration={raise=-5pt, text along path, text={|\tiny|angle ||}, text align=center, text color=red, reverse path}](0.5,0) arc (0:\myangle:0.5cm);
\draw [name intersections={of=blue_circle and radius, by=C}] [orange, ->] (A) --  (C) node [pos=0.7, sloped, above] {\tiny radius};
\end{tikzpicture}

What does all that do anyway? Well not is not the time for a full PGF/Tikz tutorial, but some illustration may be of use:

\usetikzlibrary{calc,through, intersections,decorations.text}
That line tells the Tikz software that it is going to need the extra routines mentioned in the squiggly brackets to draw the picture. The libraries mentioned are calc, to enable mathematical routines to be used on co-ordinates (like start here and move two times the number you first thought of to the left), through, to draw objects through other objects, intersections, to let us calculate the point where two lines cross, and decorations.text, which lets us draw text along squiggly lines.

\begin{tikzpicture} [scale=3]
This [begin]s the code to draw the [tikz] [picture]. The whole picture is at [scale] [3] (where 1 would be normal) so everything is nice and big and clear.

\def\myangle{117}
This is the key to our whole animation. We are setting a variable which we can then change using sed (aaaargh).

\clip (-1.5,-1.5) rectangle (-1.5,-1.5);
We tell the system we are only interested in the [rectangle] shaped region designated by the two corners [(-1.5,-1.5)] and [(-1.5,-1.5)], so it doesn't need to draw anything that falls outside that region.

\coordinate (A) at (0,0);
\coordinate (B) at (1,0);
These commands do the same thing for two different points. We are basically just giving a name to to points [(0,0)] and [(1,0)]. This means that in the future when we want to refer to the points we can just use the letters [A] and [B] instead of remembering the exact positions.

\node [blue, name path=blue_circle,draw,circle through=(B)] at (A) {};
That is the first really scary command. All it does is draw a circle. Why is it not in the form [draw] a [circle]? Well, we are using the [through] library here, which only works with nodes. So we have to stick a node down [at (A)] with [{}] no text attached to it. This is just a hypothetical point. However, we then [draw] around the node a [circle], coloured [blue], [name]d [blue_circle] which passes [through] the point called [B].

\draw [black, fill] (A) circle (1pt) node [below] {\tiny centre};
This command DOES just [draw] a [circle]. The circle is coloured [black] and is [fill]ed in, so it looks like a dot. It is centred at point [A], and has a radius of [1pt], which is a tenth of a unit long. We also attach a [node] to the circle (instead of the other way round like last time). This node is positioned [below] point A. The node has the text [centre] attached to it in a [tiny] font size.

\draw [red, dashed] (A) -- (B);
This is perhaps the least scary command we have seen so far. This [draw]s a [red] [dashed] line ([--]) between the points [A] and [B]. Simples. You can see, though, why we would want to define A and B earlier, since we are using them so often. It would be a pain to have to keep refering to the exact position all the time.

\path [name path=radius, rotate=\myangle] (A) -- ++(1.5,0);
This command looks quite different. It marks an invisible [path] on the diagram. Why invisible? Well there is no [draw] command. What's the point in this? Wait and see. The path is [name]d [radius]. It is a line ([--]) from the point [A] to a point which is [1.5] units further [++] from the centre in along the x axis and [0] units further from the centre along the y axis. In other words, you take the point A, which is (0,0), and you add [++] 1.5 to the x co-ordinate (the first 0) and 0 to the y co-ordinate (the second 0). All this does is marks an invisible line one and a half units long along the x axis. But that it not all we do! We then [rotate] the line by [\myangle] degrees. If you recall, this was the variable we set right at the beginning. The idea is that when the variable is changed by sed (aaaargh) it will filter down to this instruction as well.

\draw [red, ->] ($(A)+(0.5,0)$) arc (0:\myangle:0.5cm);
OK, another scary one. This at least [draw]s something. It actually draws an [arc]. The arc is coloured [red] and has a [>] at the end of it. If it had a [>] at the beginning the command would have a [<] in it instead. The arc starts at the point (A) [+] [0.5] units along the x axis. This uses the calc library to work out the starting co-ordinate for the arc from A. To use the calc library you put the calculation itself in between the [$] symbols. Because the calculation returns a bare co-ordinate, you need to have the whole thing in brackets as well. The arc itself is from [0] degrees on a circle of radius [0.5cm] to [\myangle] degrees (there it is again). This will draw a varyingly large section of a circle that is still centred on the point (A). Why centred on point (A)? Well the zero degree point of the circle is drawn half a unit away from (A) and the radius is half a unit long. So automatically it centres on (A).
\path [decorate,decoration={raise=-5pt, text along path, text={|\tiny|angle ||}, text align=center, text color=red, reverse path}] ($(A)+(0.5,0)$) arc (0:\myangle:0.5cm);
That is fairly horrible. First of all, ignore the bit in square brackets. This command starts by marking another of these invisible paths. The path is actually exactly the same [($(A)+(0.5,0)$) arc (0:\myangle:0.5cm)] as we drew in red last time. Why a new command? Well the bit in square brackets at the beginning does not play well with actually drawing the arc.

So what's in the square brackets? These commands use the decorations.text library to draw [text] [along] the [path] of the arc. This means the text rounds itself to the arc which is pleasing to look at. The text is [raise]s by [-5pt] or half a unit, from the path itself. It is [align]ed to the [centre] of the path, coloured [red] and [reverse]d (otherwise it would appear upside down). The text itself is [angle] and is in a [tiny] font size. It is a complete mystery to me why these text settings uses a different format to the text settings for nodes, but he ho, that's life.

\draw [name intersections={of=blue_circle and radius, by=C}] [orange, ->] (A) -- (C) node [pos=0.7, sloped, above] {\tiny radius}
This is our last drawing command. What this does is [draw]s a line [--] between points [A] and [C]. Hang on though, where is point C? Well, look at the bit in square brackets at the very beginning of that. That bit uses the intersections library to [name] the [intersection] [of] the [blue_circle] and the invisible line called [radius]. The name given is [C]. This becomes a co-ordinate we can use just like A and B, although we never need to know exactly what its position is. So we can merrily change the size of the angle, and this command will always know where the radius meets the circle.

The line we end up drawing is [orange], just to be different, and has a [>] bit on the end of it. It has a node [above] the line, which is [sloped] along the line. The slope works with lines, but not with arcs, which is why we use the decorations library for the arc. The node is [pos]itioned 70% [0.7] of the way to point C. The node contains the text [radius] written in [tiny] font size.

\end{tikzpicture}
This line [end]s the code to draw the [tikz] [picture].

OK, good. What does that draw if myangle is 117? It draws this:


Hopefully you can see that it is a nice blue circle, with a line drawn at 117 degrees from the horizontal dashed red line, with an arc half the size of the blue circle in red marking the size of the angle.

Right, how do we animate this? First we need to generate a file which can be read by the Ubuntu command line program [pdflatex]. This has to be a proper Latex file, not just a series of Tikz commands like above. So we need to top and tail our command to generate the proper latex file. We add these commands at the beginning:

\documentclass{article}
\usepackage{tikz}
\begin{document}

And this command at the end:

\end{document}

We also need to change the 117 angle to something that we can easily find and replace using sed (aaargh). A random collection of letters should mean that we do not get any false matches. So we end up with:

\documentclass{article}
\usepackage{tikz}
\begin{document}
\usetikzlibrary{calc,through, intersections,decorations.text}
\begin{tikzpicture} [scale=3]
\def\myangle{xyzzy};
\clip (-1.5,-1.5) rectangle (1.5,1.5);
\coordinate (A) at (0,0);
\coordinate (B) at (1,0);
\node [blue, name path=blue_circle,draw,circle through=(B)] at (A) {};
\draw [black, fill] (A) circle (1pt) node [below] {\tiny centre};
\draw [red, dashed] (A) -- (B);
\path [name path=radius, rotate=\myangle] (A) -- ++(1.5,0);
\draw [red, ->] ($(A)+(0.5,0)$) arc (0:\myangle:0.5cm);
\path [decorate,decoration={raise=-5pt, text along path, text={|\tiny|angle ||}, text align=center, text color=red, reverse path}](0.5,0) arc (0:\myangle:0.5cm);
\draw [name intersections={of=blue_circle and radius, by=C}] [orange, ->] (A) --  (C) node [pos=0.7, sloped, above] {\tiny radius};
\end{tikzpicture}
\end{document}

You copy that code, and paste it into [gedit] and save it to a file called, say, [master_frame.tex] in your [~] home directory. Or you could paste this command into a terminal window, which does the same thing:

cat > ~/master_frame.tex << "EOF"
\documentclass{article}
\usepackage{tikz}
\begin{document}
\usetikzlibrary{calc,through, intersections,decorations.text}
\begin{tikzpicture} [scale=3]
\def\myangle{xyzzy};
\clip (-1.5,-1.5) rectangle (1.5,1.5);
\coordinate (A) at (0,0);
\coordinate (B) at (1,0);
\node [blue, name path=blue_circle,draw,circle through=(B)] at (A) {};
\draw [black, fill] (A) circle (1pt) node [below] {\tiny centre};
\draw [red, dashed] (A) -- (B);
\path [name path=radius, rotate=\myangle] (A) -- ++(1.5,0);
\draw [red, ->] ($(A)+(0.5,0)$) arc (0:\myangle:0.5cm);
\path [decorate,decoration={raise=-5pt, text along path, text={|\tiny|angle ||}, text align=center, text color=red, reverse path}](0.5,0) arc (0:\myangle:0.5cm);
\draw [name intersections={of=blue_circle and radius, by=C}] [orange, ->] (A) --  (C) node [pos=0.7, sloped, above] {\tiny radius};
\end{tikzpicture}
\end{document}
EOF

When you run [pdflatex] it takes the master_frame.tex file and turns it into a pdf document. This is not what we want. We want a series of jpegs. We also do not want an A4 page, which is what [pdflatex] is going to give us. So what we do is convert a test sample, work out what crop we need to apply (to get rid of the unnecessary areas) and note that down somewhere. If we simply run pdflatex on our master frame it is going to fail, because it will try and draw an angle of size xyzzy, which makes no sense. So first lets create our test frame:

sed 's/{xyzzy}/{'117'}/' ~/master_frame.tex > ~/test.tex
And we now generate the pdf file from it:

pdflatex ~/test.tex

That should work. It it doesn't, something has gone wrong with your installation of latex and pgf. Go and look at my post on how to do that to make sure you have covered all the bases. To display the pdf you can open it in any pdf viewer - it is just a pdf. However, we need a jpeg, so let's convert it:

convert -density 300 test.pdf test.jpg

The [convert] command makes sense, but what about the [density 300] stuff? Well the pdf we have made is of vector graphics which can scale without losing detail to any resolution you like. A jpeg image is essentially raster graphics which has a fixed resolution. To convert vector to raster you need to tell the vector image what resolution to scale to. here we have chosen [300] dpi.

We now need to get our cropping parameters. To get these run the [display] command (from the imagemagick package I think):

display test.jpg

When the image opens, left click anywhere on it to bring up a menu. Select the Transform drop down menu and then click on crop. This lets you draw a box around the image. While you are doing this, look in the top left corner of the display window. You will see numbers changing as you draw your box. When you are satisfied that it fits the image and no more, note down those numbers AND symbols. For me they were [724x724+789+692]. Now close the display window. We are done with it. If it fights back, click on your terminal window and hit [CTRL+C] to shut it down. Now let's test out cropping parameters by re-doing a conversion to jpeg:

convert -density 300 -crop 724x724+789+692 test.pdf test.jpg

Note that the numbers and symbols we noted in the last step just get pasted in here, after the [crop] command. Nice and logical. If you now do the [display test.jpg] command again, you should find a nicely cropped picture of our diagram. If not, go back and check your cropping parameters are correct.

We are now going to generate a whole lot of [.tex] files - one for each frame of the animation we are going to make. It would probably be best to put these in their own directory:

mkdir ~/frames

You can now generate all your frames by running this command:

for angle in {1..360}; do  sed 's/{xyzzy}/{'$angle'}/' ~/master_frame.tex > ~/frames/$angle.tex; echo $angle; done

That looks pretty horrendous, doesn't it? Well, what is happening here is a loop. We define a variable at the beginning of the command called [angle]. We then run through the hold command setting the variable to every number from [1] to [..] [360]. The variable is then passed to the next section of the command which [do]es the [sed] (aaargh) command on the [~/master_frame.tex] file. The sed command spits out the file, but changes every instance of the text [{xyzzy}] to the same squiggly brackets but with the value assigned to angle in between the brackets. This sets our angle size for the frame. The result from the sed command is sent [>] to a [.tex] file in the [~/frames/] folder named after the size of angle in the file. The command then reports [echo] which frame it has completed, and [done] goes back to the beginning for a new number.

We now need to move into the frames folder for the next bit:

cd ~/frames

Now if you look at the contents of the frames directory they will not all be three digit file names, meaning they count up from 1 to 99 to 360. The leading zeros are missing, i.e. they are not 001 to 099 to 360 and our system will break. You can fix that, by adding the leading zeros with these commands:

for file in ?.tex; do cat $file > 00$file; rm $file; echo $file; done
for file in ??.tex; do cat $file > 0$file; rm $file; echo $file; done

Why could we not just generate angles from 001..360? If we do that pdflatex thinks we are using base 8, or octal numbers, and misses out every ninth and tenth frame. We can now do the mass generation of all the pdf files. This will take a while. The command is as follows:

for file in ???.tex; do pdflatex -interaction=batchmode $file; echo $file; done

This is another loop, that picks up every [.tex] file with three characters in its name, and sticks it into the [pdflatex] command. Next we need to use our cropping details to produce the jpeg files:

for file in *.pdf; do convert -density 300 -crop 724x724+789+692  $file ${file%.???}.jpg; echo $file; done

This just uses a similar loop to the last command. It applies to all the [.pdf] files in the folder - so make sure that you started with an empty one. The percentage symbol is clever, it deletes the extension and replaces it with [.jpg] so the output of the command is a whole lot of [.jpg] files with the same name as the [.pdf] files which generated them. If we are really clever, we can just duplicate all our frames in reverse order to make the animation reverse when it gets to the end. This creates an unending loop, but is not strictly necessary with our animation here which is essentially circular.

marker=360; for file in *.jpg; do frame=$(($marker+360)); cp $file $frame.jpg; marker=$(($marker -1)); echo $marker; done

That is absolutely a nightmare. That it does is set a complicated loop in place counting back from 360 to 1. Why? Well we want the 360 frames we have already to be played in reverse. So we want 361 to be the same as 360, and 362 to be 359 and so on down to 1 being 720. So we start at 360 and count back. The [do] command sets the [frame] variable to be the marker PLUS 360. So when the marker is 360 it is 720. However, the [file] variable is going through the directory in numerical order. So the next command [c]o[p]ies the [file] to the frame number. The loop then reduces the marker by one and goes back to the beginning. The sequence looks like this:

Marker-File-Maths-Frame
360-1-(360+360)-720
359-2-(359+360)-719
358-3-(358+360)-718
...
2-359-(2+360)-362
1-360-(1+360)-361

It is a bit of a wrestle, but it gets there.

Now we have all our jpeg frames, we can use [ffmpeg] to compress them into an mp4 video file like so:

ffmpeg -r 25 -b 2500k -i %3d.jpg -pass 1 -y circle.mp4 && ffmpeg -r 25 -b 2500k -i %3d.jpg -pass 2 -y circle.mp4

That's another horror, but it breaks down like this: It uses the [ffmpeg] command set at a f[r]ame rate of [25] per second, and a [b]itrate of [2500k] per second, and takes its [i]nput as every [.jpg] file in the current folder with a three digit name. It runs a first [1] [pass] through the file to see where best to use its bitrate and saves the output to [circle.mp4] automatically answering [y]es to any question about overwriting that file. It then immediately [&&] runs a second [2] [pass] with the same input and settings to produce the final output in the file called [circle.mp4].

You can now view your creation by running:

mplayer circle.mp4 -loop 0

It should look like this:



Once you have done it once, it is useful to be able to tweak one entry in the master frame file and then run all the commands that follow automatically. You can do that with this command block. It first cleans out the ~/frames folder of everything, and then runs all the commands above, one after the other:

cd ~/frames && rm * && cd ~ && for angle in {1..360}; do  sed 's/{xyzzy}/{'$angle'}/' ~/master_frame.tex > ~/frames/$angle.tex; echo $angle; done && cd ~/frames && for file in ?.tex; do cat $file > 00$file; rm $file; echo $file; done && for file in ??.tex; do cat $file > 0$file; rm $file; echo $file; done && for file in ???.tex; do pdflatex -interaction=batchmode $file; echo $file; done && for file in *.pdf; do convert -density 300 -crop 724x724+789+692  $file ${file%.???}.jpg; echo $file; done && marker=360; for file in *.jpg; do frame=$(($marker+360)); cp $file $frame.jpg; marker=$(($marker -1)); echo $marker; done && ffmpeg -r 25 -b 2500k -i %3d.jpg -pass 1 -y circle.mp4 && ffmpeg -r 25 -b 2500k -i %3d.jpg -pass 2 -y circle.mp4 && mplayer circle.mp4 -loop 0

5 comments:

  1. http://phys23p.sl.psu.edu/phys_anim/mech/embeder2.17030.html


    hi, is it possible to do something like this on tikz? branco2000@yahoo.com

    ReplyDelete
  2. Hi Dami,

    Yes you can certainly do that in TikZ using the procedure I outlined above. You just need to get creative with variable names. Here, for instance, is the gemetric proof of the sum and difference formulas that I cobbled together:

    http://youtu.be/cHvfIwDtJLM

    ReplyDelete
  3. Silly me, you probably want the code for that animation. You can grab it here:

    http://dl.dropbox.com/u/969147/sumdiff.tex

    ReplyDelete
  4. What you want to concentrate on is how the intersections package for tikz works. Combine that with invisible paths and you should be able to get it to function.

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete