12

In some mathematical figures it's nice to vary curve thickness smoothly along a path. That's for reproducing hand drawn mathematical figures made with inking liners.

curvethicknessvaries

\documentclass{article}\usepackage{tikz}

\def\ltrstroke #1,#2 #3,#4 #5,#6 #7,#8%
    {\foreach \n in {0,0.01,0.02,...,0.09}{\path[line width=1pt,rounded corners=48pt,line cap=round,draw]%
       (#1+\n/1,#2+\n/1)--(#3+\n/4,#4+\n/4)--(#5+\n/3,#6-\n/3)--(#7+\n/4,#8-\n/4);}}

\def\ltrinking #1,#2 #3,#4 #5,#6 #7,#8%
    {\foreach \n in {0,0.01,0.02,...,0.09}{\path[line width=2pt,rounded corners=48pt,draw]%
       (#1+\n/.4,#2+\n/.4)--(#3+\n/.6,#4+\n/.6)--(#5+\n/1,#6-\n/1)to[bend left](#7+\n/4,#8-\n/4)--cycle;}\path[line width=2pt,rounded corners=48pt](#1,#2)--(#3,#4)--(#5,#6)to[bend left](#7,#8)--cycle;}

\begin{document}\begin{tikzpicture}[bend angle=8];
    \ltrinking 0,0 4,4 8,3 4,-1 
    \ltrstroke 0,5 0.2,5.2 3,8 2,6
\end{tikzpicture}\end{document}

One procedure which does that simply flares path coordinates by adding deviations in multiples. These increase or decrease depending where on the line they are. Also this is done recursively over a set of deviations. We plot the many overlapping curves. The result is shown above.

QUESTION: The problem is \def accepts <10 inputs. So how do I extend \def for arbitrary even number of arguments >10 that is a multiple of another number? We require this to control lines in such drawings. Each x and y argument is separately transformed by the for each loop as a function of the hand fixed numerical parameters ps.

Please suggest how to input the coordinates in a tuple form, not in a form {x1}{y1}{p1}{x2}{y2}{p2}... That becomes confusing when all the inputs are given numbers and the curve is edited, and it becomes ambiguous if errors are input made. Treat |x,y,p| as one argument, extract the x and y and p.

The pens do make paths of varying width in TeX plus MetaPost:

mpst

\documentclass{article}\usepackage[shellescape,latex]{gmp}
\begin{document}\begin{figure}\centering\begin{mpost}
    pen mypen; mypen = pencircle scaled 1in xscaled .08 yscaled .32 rotated 180;
    pickup mypen; draw (0,0)..(100,-32)..(192,192)..(256,64);
\end{mpost}\caption{testing}\end{figure}\end{document}

Tikz is more convenient for combining this with equation work and outputs this faster. It also allows treating parts of other outputs as graphic nodes and has powerful libraries. Metapost works best with equation defined pens and curves, or if the ends of the curves need to be sharp and rotated. However, the bezier parameters for each thick pen turn requires tweaking that covaries with each curve. My implementation here requires tweaking at most one parameter.

The n-argument macro from the answer to this question is great. I tried to generalize the answer in this usage; but the commented out version gives undefined control sequence error. The noncommented version works; the \csname x{#1} \endcsname code is probably not valid. What does one replace it with?

curvo

\documentclass{article}\usepackage{tikz}
\newcount\tmpnum
\def\scanargs #1#2#3;{\let\tmp=#1\tmpnum=0 \scanargsA #3|,,|}
\def\scanargsA #1,#2,#3|{\ifx,#1,\expandafter\tmp \else
   \advance\tmpnum by1
   \expandafter\def\csname x:\the\tmpnum\endcsname{#1}%
   \expandafter\def\csname y:\the\tmpnum\endcsname{#2}%
   \expandafter\def\csname z:\the\tmpnum\endcsname{#3}%
   \expandafter\scanargsA \fi}
\def\x#1{\csname x:#1\endcsname}
\def\y#1{\csname y:#1\endcsname}
\def\z#1{\csname z:#1\endcsname}

\def\arcpart#1{({\csname x#1 \endcsname}+\n/{\csname z#1 \endcsname},{\csname y#1 \endcsname}-\n/{\csname z#1 \endcsname})\to}

\def\ltrinking{\foreach \n in {0,.01,.02,...,1}{%
\path[line width=2pt,rounded corners=48pt,draw]%
(\x1+\n/\z1,\y1+\n/\z1)--(\x2+\n/\z2,\y2+\n/\z2)--(\x3+\n/\z3,\y3-\n/\z3)--(\x4+\n/\z4,\y4-\n/\z4)--(\x5+\n/\z5,\y5-\n/\z5)--(\x6+\n/\z6,\y6-\n/\z6)--cycle;%
}}

\begin{document}\begin{tikzpicture}[bend angle=8];
    \scanargs\ltrinking|0,8,3|5,4,1|16,9,4|8,4,1|5,1,4|4,-8,1;
%   \scanargs\ltric|7,0,0|0,8,3|5,4,1|16,9,4|8,4,1|5,1,4|4,-8,1;
\end{tikzpicture}\end{document}
  • If you were prepared to write the coordinate as coordinates e.g. (x1,y1), you could just use another TikZ \foreach couldn't you? – cfr May 4 '15 at 0:55
  • Have you considered using metapost? – cfr May 4 '15 at 0:57
  • 2
    Just that with metapost you can use pens and get varying thickness automatically so no workarounds of the sort needed in TikZ are required. I mentioned the (,) syntax because a TikZ foreach loop will handle that syntax specially i.e. it will automatically treat those as coordinates. [Page 910 of the manual says] 'When a list item starts with a ( everything up to the next ) is made part of the item.' – cfr May 4 '15 at 1:14
  • Then use the calc library to add coordinates so that you don't need to worry about splitting the pairs. – cfr May 4 '15 at 1:17
  • 1
    You can send in one argument of a long array and parse it internally to 3n parts. – percusse May 4 '15 at 8:37
8
+100

You can use the macro \scanargs \macro x1,y1 x2,y2 ... xn,yn; and then you can use the scanned arguments in your \macro in the form \x1, \x2, ... \x9, \y9, but \x{10}, \y{22} etc. I show the example using your example:

\documentclass{article}\usepackage{tikz}

\newcount\tmpnum
\def\scanargs #1#2;{\let\tmp=#1\tmpnum=0 \scanargsA #2 {},{} }
\def\scanargsA #1,#2 {\ifx,#1,\expandafter\tmp \else
   \advance\tmpnum by1
   \expandafter\def\csname x:\the\tmpnum\endcsname{#1}%
   \expandafter\def\csname y:\the\tmpnum\endcsname{#2}%
   \expandafter\scanargsA \fi
}
\def\x#1{\csname x:#1\endcsname}
\def\y#1{\csname y:#1\endcsname}

\def\ltrinking {\foreach \n in {0,0.01,0.02,...,0.09}
   {\path[line width=2pt,rounded corners=48pt,draw]
     (\x1+\n/.4,\y1+\n/.4)--(\x2+\n/.6,\y2+\n/.6)--(\x3+\n/1,\y3-\n/1)to[bend left]
     (\x4+\n/4,\y4-\n/4)--cycle;}
    \path[line width=2pt,rounded corners=48pt]
     (\x1,\y1)--(\x2,\y2)--(\x3,\y3)to[bend left](\x4,\y4)--cycle;
}
\def\ltrstroke {\foreach \n in {0,0.01,0.02,...,0.09}
   {\path[line width=1pt,rounded corners=48pt,line cap=round,draw]
      (\x1+\n/1,\y1+\n/1)--(\x2+\n/4,\y2+\n/4)--(\x3+\n/3,\y3-\n/3)--(\x4+\n/4,\y4-\n/4);}
}

\begin{document}\begin{tikzpicture}[bend angle=8];
    \scanargs\ltrinking 0,0 4,4 8,3 4,-1;
    \scanargs\ltrstroke 0,5 0.2,5.2 3,8 2,6;
\end{tikzpicture}
\end{document}

Edit: There is a little "adding value" in egreg's answer: \newdrawingcommand declarator. You can use \newdrawingcommand\lrstroke{...} and then simply \lrstroke arguments in the code without \scanarg explicitly used. If you like such feature then it can be implemented by:

\def\newdrawingcommand#1{%
   \edef#1{\noexpand\scanargs\csname s:\string#1\endcsname}%
   \expandafter\def\csname s:\string#1\endcsname
}
  • This is a great idea. What is the correct way to generalize this to x1,y1,z1|x2,y2,z2|... I have tried it two ways (see the EDIT in the question for the code), but one gets a scanning error till document end, or with the second way, a nonconsecutive arguments error. I am missing something, yes? Also, I made a bounty to add to your answer, useful method in general. – Guido Jorg May 7 '15 at 20:01
  • @GuidoJorg The \scanargs line in your code have to be ended by #3|,,|} (note that my version includes #3space{},{}space}). And the \scanargsA must have its parameter mask in the form #1,#2,#3|, i.e. remove the first |. I din't tried your code, I only report what I see when the expand processor runs in my head. If there will be another problem , please correct the mentioned bugs, comment about this and I will see to your code again. – wipet May 7 '15 at 20:26
  • Yes, exactly. Now the code works. BTW, I'll put the bounty to your answer in a day or two. That way more people see the Q and A and perhaps spread the code a bit. Very nice things can be made this way. – Guido Jorg May 7 '15 at 21:45
  • \def\arcpart#1{({\csname x#1 \endcsname}+\n/{\csname z#1 \endcsname},{\csname y#1 \endcsname}-\n/{\csname z#1 \endcsname})\to} AND \def\ltric{\foreach \n in {0,.01,.02,...,1}{\path[line width=2pt,rounded corners=48pt,draw]\foreach \m in {2,3,...,{\x1+\y1+\z1}}{\arcpart{\m}}cycle;}} to create a similar input loop were the number of points on the path was input and did not require a fixed macro, but something like \scanargs\ltric|7,1,0|0,8,3|5,4,1|16,9,4|8,4,1|5,1,4|4,-8,1; gives an undefined control sequence error. Is there a particular way to operate with say x{#1} unlike x{7} here? – Guido Jorg May 8 '15 at 0:43
  • The colon between x and #1 inside \csname...\endcsname is missing in your example. And you can use \x{#1} or \x{\m} (simply without \csname..\endcsname) if #1 is an index of scanned parameters and \m is a macro expands to such index. – wipet May 8 '15 at 3:30
5

An implementation with expl3, where I define a \newdrawingcommand that takes as arguments a command name and the replacement text; optionally a command based on \foreach can be added, for greater flexibility.

In the replacement text, the various points can be referred to by \x and \y; these macros are available only there (they won't clobber other existing definitions).

\documentclass{article}
\usepackage{xparse}
\usepackage{tikz}

\ExplSyntaxOn
\NewDocumentCommand{\newdrawingcommand}{m O{\guidoforeach} m}
 {
  \cs_new_protected:Npn #1 ##1;
   {
    \group_begin:
    \cs_set_eq:NN \x \guido_x_coord:n 
    \cs_set_eq:NN \y \guido_y_coord:n 
    \guido_parse_arg:n { ##1 }
    #2 { #3 }
    \group_end:
   }
 }

\seq_new:N \l_guido_arg_list_seq
\seq_new:N \l_guido_x_list_seq
\seq_new:N \l_guido_y_list_seq

\cs_new_protected:Npn \guido_parse_arg:n #1
 {
  \seq_clear:N \l_guido_x_list_seq
  \seq_clear:N \l_guido_y_list_seq
  \seq_set_split:Nnn \l_guido_arg_list_seq { ~ } { #1 }
  \seq_map_inline:Nn \l_guido_arg_list_seq
   {
    \tl_if_blank:nF { ##1 }
     {% the last item is empty if ; is preceded by a space
      \seq_put_right:Nx \l_guido_x_list_seq { \clist_item:nn { ##1 } { 1 } }
      \seq_put_right:Nx \l_guido_y_list_seq { \clist_item:nn { ##1 } { 2 } }
     }
   }
 }
\cs_new:Npn \guido_x_coord:n #1 
 {
  \seq_item:Nn \l_guido_x_list_seq { #1 }
 }
\cs_new:Npn \guido_y_coord:n #1 
 {
  \seq_item:Nn \l_guido_y_list_seq { #1 }
 }
\ExplSyntaxOff

\newcommand{\guidoforeach}[1]{%
  \foreach \n in {0,0.01,0.02,...,0.09}{#1}%
}
\newcommand{\guidoforeachdouble}[1]{%
  \foreach \n in {0,0.01,...,0.36}{#1}%
}

\newdrawingcommand{\ltrstroke}[\guidoforeachdouble]{%
  \path[
    line width=1pt,rounded corners=48pt,line cap=round,draw
  ]
  (\x{1}+\n/1,\y{1}+\n/1)--
  (\x{2}+\n/2,\y{2}+\n/2)--
  (\x{3}+\n/3,\y{3}-\n/3)--
  (\x{4}+\n/4,\y{4}-\n/4);
}

\newdrawingcommand{\ltrinking}{%
  \path[
    line width=2pt,rounded corners=48pt,draw
  ](\x{1}+\n/.4,\y{1}+\n/.4)--
   (\x{2}+\n/.6,\y{2}+\n/.6)--
   (\x{3}+\n/1,\y{3}-\n/1) to 
   [bend left](\x{4}+\n/4,\y{4}-\n/4)--cycle;
  \path[
    line width=2pt,rounded corners=48pt
  ](\x{1},\y{1})--(\x{2},\y{2})--(\x{3},\y{3}) to
   [bend left](\x{4},\y{4})--cycle;
}

\begin{document}

\begin{tikzpicture}[bend angle=8];
  \ltrinking 0,0 4,4 8,3 4,-1 ;
  \ltrstroke 0,5 0.2,5.2 3,8 2,6 ;
\end{tikzpicture}
\end{document}

The \guido_parse_arg:n function splits (at blanks) the argument, which should be terminated by a trailing semicolon, into comma separated pairs (at blanks); then each item in a pair is added either to the list of x-coordinates or to the list of y-coordinates. Calling \x{k} will access the x-coordinate of the k-th point and similarly for \y.

In the definition of \ltrstroke I've used a different \foreach loop, just to show the usage. If you remove the optional argument, the \foreach loop defaults to \guidoforeach. This might be useful for debugging, without modifying the main replacement text.

In the argument to a drawing macro you're allowed to have a trailing space before the semicolon, but no spaces around the commas.

enter image description here


With a change in syntax we can accommodate for n-tuples, where n is arbitrary. I've shown the example with the first command, where the four points are specified instead as two quadruples.

\documentclass{article}
\usepackage{xparse}
\usepackage{tikz}

\ExplSyntaxOn
\NewDocumentCommand{\newdrawingcommand}{m O{\guidoforeach} m}
 {
  \cs_new_protected:Npn #1 ##1;
   {
    \group_begin:
    \cs_set_eq:NN \x \guido_x_coord:n
    \cs_set_eq:NN \y \guido_y_coord:n
    \cs_set_eq:NN \z \guido_z_coord:n
    \cs_set_eq:NN \p \guido_coord:nn
    \guido_parse_arg:n { ##1 }
    #2 { #3 }
    \group_end:
   }
 }

\seq_new:N \l_guido_arg_list_seq
\prop_new:N \l_guido_point_list_prop

\cs_new_protected:Npn \guido_parse_arg:n #1
 {
  % clear the list of points
  \prop_clear:N \l_guido_point_list_prop
  % split the arg list at |
  \seq_set_split:Nnn \l_guido_arg_list_seq { | } { #1 }
  % add each tuple to the property list
  \int_step_inline:nnnn { 1 } { 1 } { \seq_count:N \l_guido_arg_list_seq }
   {
    \__guido_add_point:nx { ##1 } { \seq_item:Nn \l_guido_arg_list_seq { ##1 } }
   }
 }
\cs_new_protected:Npn \__guido_add_point:nn #1 #2
 {
  \int_step_inline:nnnn { 1 } { 1 } { \clist_count:n { #2 } }
   {
    \prop_put:Nnx \l_guido_point_list_prop { ##1 , #1 }
     {
      \clist_item:nn { #2 } { ##1 }
     }
   }
 }
\cs_generate_variant:Nn \__guido_add_point:nn { nx }

\cs_new:Npn \guido_coord:nn #1 #2
 {
  \prop_item:Nn \l_guido_point_list_prop { #1,#2 }
 }

\cs_new:Npn \guido_x_coord:n #1 
 {
  \guido_coord:nn { #1 } { 1 }
 }
\cs_new:Npn \guido_y_coord:n #1 
 {
  \guido_coord:nn { #1 } { 2 }
 }
\cs_new:Npn \guido_z_coord:n #1 
 {
  \guido_coord:nn { #1 } { 3 }
 }
\ExplSyntaxOff

\newcommand{\guidoforeach}[1]{%
  \foreach \n in {0,0.01,0.02,...,0.09}{#1}%
}
\newcommand{\guidoforeachdouble}[1]{%
  \foreach \n in {0,0.01,...,0.36}{#1}%
}

\newdrawingcommand{\ltrstroke}[\guidoforeachdouble]{%
  \path[
    line width=1pt,rounded corners=48pt,line cap=round,draw
  ]
  (\p{1}{1}+\n/1,\p{2}{1}+\n/1)--
  (\p{1}{2}+\n/2,\p{2}{2}+\n/2)--
  (\p{1}{3}+\n/3,\p{2}{3}-\n/3)--
  (\p{1}{4}+\n/4,\p{2}{4}-\n/4);
}

\newdrawingcommand{\ltrinking}{%
  \path[
    line width=2pt,rounded corners=48pt,draw
  ](\x{1}+\n/.4,\y{1}+\n/.4)--
   (\x{2}+\n/.6,\y{2}+\n/.6)--
   (\x{3}+\n/1,\y{3}-\n/1) to 
   [bend left](\x{4}+\n/4,\y{4}-\n/4)--cycle;
  \path[
    line width=2pt,rounded corners=48pt
  ](\x{1},\y{1})--(\x{2},\y{2})--(\x{3},\y{3}) to
   [bend left](\x{4},\y{4})--cycle;
}

\begin{document}

\begin{tikzpicture}[bend angle=8];
  \ltrinking 0,4,8,4 | 0,4,3,-1 ;
  \ltrstroke 0,5 | 0.2 , 5.2 | 3 , 8 | 2,6 ;
\end{tikzpicture}
\end{document}

It would have been possible to still delimit n-tuples by spaces, but it would be more difficult to check correctness; in the example, I show that with this syntax, spaces are essentially ignored.

The output is the same as before, of course.

  • Very nice. This Q may end up with two bounties (100,15), the larger one to which ever answer in the next day or two. (I have to digitalize diagrams for several papers I've been putting off finishing.) Currently foreach looping the path definition in each answer to get a path definition macro defined by number of nodes. – Guido Jorg May 7 '15 at 22:17
  • 4
    Don't worry about bounties. I already have plenty of rep. ;-) This is just to show the advantages of a well defined programming environment that doesn't require to reinvent the wheel each time. – egreg May 7 '15 at 22:19
  • 2
    Sorry, using \csname...\endcsname is not reinventing wheel. This is normal praxis when using TeX. If people know this normal praxis then the life with TeX would be better and simple. – wipet May 8 '15 at 3:36
  • 1
    @GuidoJorg I added a version for doing arbitrary tuples (with a single parser, of course). – egreg May 8 '15 at 10:45
  • @wipet I do have to say though: I know a fair bit of TeX and understand how it works as a language. (Category codes are still up-and-coming, but that's unrelated.) Even with that understanding, The thought process is much simpler to follow with expl3 syntax. Note: If it's LaTeX you're against, expl3 isn't properly LaTeX :) It's just a programming environment. – Sean Allred May 8 '15 at 11:17
4

I missed this first time round, but while it has nothing to do with the question about multiple arguments, the calligraphy TikZ library (which originated in 'Poster' fountain pen nib style text) can draw paths with varying thickness. When combined with the awesome hobby package (from Curve through a sequence of points with Metapost and TikZ), it is quite straightforward to produce nicely curved lines of varying width.

\documentclass{article}
%\url{https://tex.stackexchange.com/q/242025/86}
\usepackage{tikz}
\usetikzlibrary{calligraphy,hobby}
\begin{document}
\begin{tikzpicture}[use Hobby shortcut,line width=2pt]
\pen (0,0);
\calligraphy[scale=.02,heavy] (0,0)..(100,-32)..(192,192)..(256,64);
\end{tikzpicture}
\end{document}

varying width line

  • Nice! Will being using this too. What operation does the hobby package do in this case btw? – Guido Jorg Nov 7 '16 at 17:50
  • 1
    @GuidoJorg The hobby package provides the (coordinate) .. (coordinate) .. (coordinate) syntax which uses an algorithm to find a smooth path through the specified coordinates without specifying control points. It's useful in this case for getting a nice "flow" to the line. – Loop Space Nov 7 '16 at 17:51
  • One of these days I will finish the pgfmath version :P (keep saying this for the last a few years) – percusse Nov 7 '16 at 20:38
2

This just shows an alternative formulation for parsing the tuples which adapts some of the ideas presented in other answers. If the (badly named) macro dotuples is called like this:

\dotuples{x,y,z}{\n}{1,2,3 | 4,5,6 | 7,8,9 | 10,11,12}

Then \x0 is defined as 1, \y0 is defined as 2 and \z0 is defined as 3. This continues over all the tuples, so \z3 is 12. At the end the macro \n contains the index of the last tuple (in this case 3).

The first argument to \dotuples specifies the names of the variables to store the tuples in and also the number of elements expected in each tuple, it can be extended arbitrarily so one could say:

\dotuples{a,b,c,d,e,f}{\n}{1,2,3,4,5,6 | 11,12,13,14,15,16}

Nothing clever is done with the tuple elements so they can contain spaces (which may or may not be desirable). It should be fairly straightforward to change the delimiter | to something else (marginally less straightforward if the required delimiter was a space).

\documentclass[varwidth,border=5pt]{standalone}
\usepackage{pgf,pgffor}
\makeatletter
\def\dotuple#1#2#3{%
  \pgfutil@tempcnta=0\relax%
  \pgfutil@for\@tmp:={#1}\do{%
    \expandafter\edef\csname tuple@var@\the\pgfutil@tempcnta\endcsname{\@tmp}%
    \expandafter\@makevar\expandafter{\@tmp}%  
    \advance\pgfutil@tempcnta by1\relax%
  }%
  \def\tuple@count@var{#2}%
  \pgfutil@tempcnta=0\relax%
  \@dotuple#3||%
}

\def\@makevar#1{\expandafter\def\csname#1\endcsname##1{\csname#1@##1\endcsname}}
\def\@dotuple#1|{%
  \def\@tmp{#1}%
  \ifx\@tmp\pgfutil@empty%
    \let\@next=\relax%
    \advance\pgfutil@tempcnta by-1\relax%
    \expandafter\edef\tuple@count@var{\the\pgfutil@tempcnta}%
  \else%
    \pgfutil@tempcntb=0\relax%
    \pgfutil@for\@tmp:={#1}\do{%
       \edef\@tmpvar{\csname tuple@var@\the\pgfutil@tempcntb\endcsname}%
       \expandafter\edef\csname\@tmpvar @\the\pgfutil@tempcnta\endcsname{\@tmp}%
       \advance\pgfutil@tempcntb by1\relax%
    }%
    \advance\pgfutil@tempcnta by1\relax%
    \let\@next=\@dotuple%
  \fi%
  \@next}

\begin{document} 
\dotuple{x,y,p}{\n}{ 1,2,3 | 4.5,6,7 | 8,9.10,11 | 12.13,14,-15.16}
\ttfamily%
\foreach \i in {0,...,\n}{ x[\i]=\x\i, y[\i]=\y\i, p[\i]=\p\i \par }
\end{document}

enter image description here

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.