3

This my first attempt ever in making an animated image using latex.

I searched a lot here on stackexchange and learned about:

  • defining an array \def\array{{}}
  • ... notation for arrays {1,...,5} = {1,2,3,4,5}
  • draw multiple images with the same code \foreach \i in {...} { \begin{tikzpicture} ...code... \end{tikzpicture}
  • make a computation using \foreach [evaluate = {...computation...}]
  • using conditionals if/else with result = cond?v0:v1 (cond true -> result = v0, cond false -> result = v1).

My aim is to draw two points moving on two lines with different "speeds" (say a and b), to show that if they start at the same time from the same point on the line, then they will meet again at the same point at the moment given by the least common multiple lcm(a, b).

In the example the "speeds", or better the times required for the points to go back to their initial positions, are 4 and 6, so the points will meet again at moment 12.

To make the animation smooth enough, I divided the first line in 24 (= 4*6) equal parts and the second line in 36 (= 6*6) equal parts. So in the initial arrays 0,...,24 mean that the first point moves from left to right, and 23,...,0 that it moves from right to left, and it does this 3 times (3*4 = 12). Analogously the second point move from left to right 0,...,36, and from right to left 35,...,0, and it does this 2 times (2*6 = 12).

I think these part of the code can be improved:

  • the definitions of the initial arrays, is it possible to define them in a compact way? for example by concatenating arrays {0,...,24}+{23,...,0}+{1,...,24}+...
  • to be able to compute the numbers on the left of the lines and above the points I wrote the fill inside a dummy foreach, dummy because the loop is done only 1 time. Is it possibile to compute the numbers directly inside the fill command?
  • the computations of the numbers needed are too clumsy

Is the general structure of the code good or bad? How to improve it?

\documentclass[tikz, 12pt]{standalone}
\usepackage{tikz}
\begin{document}
% define the arrays containing the x axis coords to draw the moving points
\def\four{{0,
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,
23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,
23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,
23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0}}
\def\six{{0,
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,
35,34,33,32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,
35,34,33,32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0}}
% generate an image for each point (each array contain 145 points)
\foreach \i in {0,...,144}
{
\begin{tikzpicture}
% draw a frame so that size of all images is the same
\draw (-1,-2) rectangle (7,1);

% first line
\draw[gray, thick] (0,0) -- (6,0);
% compute the number on the left
% it is a multiple of 4 obtained by multiplying by 4 the integer part of the quotient of the current cycle index divided by the double of 24
\foreach [evaluate = {\multiple = int(4*floor(\i/48))}] \dummy in {1}
    \fill (0,0) node [left]{\multiple};
% draw the point and compute the number above the point
% the number is computed as the integer part of the remainder of the division of the current cycle index divided by the double of 24, divided by 12, and when the remainder is 0 it is added 1
\foreach [evaluate = {\counter = int(int(floor(mod(\i,48)/12))+int(int(\i==0?0:1)*int(4*int(mod(\i,48)==0?1:0))))}] \dummy in {1}
    \fill (\four[\i]/4,0) circle (2pt) node [above]{\counter};
% draw a bar on the line where the number above the point changes
\foreach \x in {0,3,6}
    \draw (\x,0.1) -- (\x,-0.1);

% second line
\draw[gray, thick] (0,-1) -- (6,-1);
% compute the number on the left
\foreach [evaluate = {\multiple = int(6*floor(\i/72))}] \dummy in {1}
    \fill (0,-1) node [left]{\multiple};
% draw the point and compute the number above the point
\foreach [evaluate = {\counter = int(int(floor(mod(\i,72)/12))+int(int(\i==0?0:1)*int(6*int(mod(\i,72)==0?1:0))))}] \dummy in {1}
    \fill (\six[\i]/6,-1) circle (2pt) node [above]{\counter};
% draw a bar on the line where the number above the point changes
\foreach \x in {0,2,4,6}
    \draw (\x,-1.1) -- (\x,-0.9);
\end{tikzpicture}
}
\end{document}

enter image description here

3

Regarding your first question, I propose expl3 code to automatically generate the coordinate sequences. Your commands \four and \six can then be defined with these two lines:

\defineCoordsSeq{\four}{0}{24}{3}
\defineCoordsSeq{\six}{0}{36}{2}

I prefer renaming them to \fourSeq and \sixSeq to reduce the risk of overwriting macros from “someone else”; so, that's what you are going to see in my complete example below.

Regarding your second question, you can use:

  • The let ... in ... operation which performs definitions local to the path under construction. With let \n〈number register〉={〈formula〉}, the 〈formula〉 is evaluated using \pgfmathparse. You can use \n1, \n2, etc. instead of, e.g., \n{multiple}, in case you prefer writing terse code. See The Let Operation in the TikZ & PGF manual for details.

  • \pgfmathsetmacro{〈macro〉}{〈formula〉} or \pgfmathtruncatemacro{〈macro〉}{〈formula〉}. The latter stores the result as an integer written with no decimal separator, which is desirable in some or all of your uses here. In either case, the macro definition is local to the current TeX group.

My code below shows examples of both of these techniques. I used \pgfmathtruncatemacro when you had long formulas with an outer int() enclosing the whole; \pgfmathtruncatemacro allows us to get rid of this outer int() call.

Regarding your third question, I believe that several of your int() calls could be removed, but as I don't really know the internals of pgfmath, I prefer letting someone else address it.

Here is my code:

\documentclass[tikz]{standalone}
\usepackage{xparse}
\usetikzlibrary{calc}           % for the let ... in ... operation

\ExplSyntaxOn

% #1: seq variable
% #2: minimum value
% #3: maximum value
% #4: number of back-and-forths
\cs_new_protected:Npn \soundwave_fill_coords_sequence:Nnnn #1#2#3#4
  {
    \seq_clear:N #1

    \int_step_inline:nn {#4}
      {
        \int_step_inline:nnnn {#2} { 1 } { (#3) - 1 }
          { \seq_put_right:Nn #1 {####1} }
        \int_step_inline:nnnn {#3} { -1 } { (#2) + 1 }
          { \seq_put_right:Nn #1 {####1} }
      }
    \seq_put_right:Nn #1 {#2}
  }

\seq_new:N \l__soundwave_tmp_seq

\cs_new_protected:Npn \soundwave_define_coords_seq_as_command:Nnnn #1#2#3#4
  {
    \soundwave_fill_coords_sequence:Nnnn \l__soundwave_tmp_seq {#2} {#3} {#4}
    \edef #1 { { \seq_use:Nn \l__soundwave_tmp_seq { , } } }
  }

% #1: control sequence token
% #2: minimum value
% #3: maximum value
% #4: number of back-and-forths
\NewDocumentCommand \defineCoordsSeq { m m m m }
  {
    \soundwave_define_coords_seq_as_command:Nnnn #1 {#2} {#3} {#4}
  }

\ExplSyntaxOff

\defineCoordsSeq{\fourSeq}{0}{24}{3}
\defineCoordsSeq{\sixSeq}{0}{36}{2}

\begin{document}

% Generate an image for each point (each array contain 145 points).
\foreach \i in {0,...,144}
{
\begin{tikzpicture}
% Draw a frame so that size of all images is the same.
\draw (-1,-2) rectangle (7,1);

% First line
\draw[gray, thick] (0,0) -- (6,0);
% Compute the number on the left.
% It is a multiple of 4 obtained by multiplying by 4 the integer part of the
% quotient of the current cycle index divided by the double of 24.
\fill let \n{multiple} = { int(4*floor(\i/48)) }
      in (0,0) node [left]{\n{multiple}};
% Draw the point and compute the number above the point.
% The number is computed as the integer part of the remainder of the division
% of the current cycle index divided by the double of 24, divided by 12, and
% when the remainder is 0 it is added 1.
\pgfmathtruncatemacro{\counter}{
  int(floor(mod(\i,48)/12)) + int(int(\i==0?0:1)*int(4*int(mod(\i,48)==0?1:0)))
} % this definition of \counter lasts for the duration of the current group
\fill (\fourSeq[\i]/4,0) circle (2pt) node [above]{\counter};
% Draw a bar on the line where the number above the point changes.
\foreach \x in {0,3,6}
    \draw (\x,0.1) -- (\x,-0.1);

% Second line
\draw[gray, thick] (0,-1) -- (6,-1);
% Compute the number on the left.
\fill let \n{multiple} = { int(6*floor(\i/72)) }
      in (0,-1) node [left]{\n{multiple}};
% Draw the point and compute the number above the point.
\pgfmathtruncatemacro{\counter}{
  int(floor(mod(\i,72)/12)) + int(int(\i==0?0:1)*int(6*int(mod(\i,72)==0?1:0)))
}
\fill (\sixSeq[\i]/6,-1) circle (2pt) node [above]{\counter};
% Draw a bar on the line where the number above the point changes.
\foreach \x in {0,2,4,6}
    \draw (\x,-1.1) -- (\x,-0.9);
\end{tikzpicture}
}

\end{document}

The result looks identical to yours, except that I removed the 12pt option of \documentclass in order to keep the code as close to minimal as possible.

| improve this answer | |

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.