4

I'm trying to draw some spirals and asked this question some days ago. The answer by hpekristiansen is great and helps a lot but as it's not clear if the spiral is right- or left-handed by looking at the picture, it looks somewhat weird in my desired context. Today, hpekristiansen asked a question himself on this topic and got a very helpful answer by TikZling. I especially like the answer using a \foreach loop to draw the individual segments. The remaining issue is that I cannot use the double path option as it would be visible on a non-white background, or as in my use-case the rods surrounding the spiral.

The solution to this problem would be to clip the odd paths (starting at the third one) where they are intersected by the even paths. Unfortunately, the \path [clip] in TikZ does only use the center of the path to clip something and has no option to set a line-width that would be clipped altogether. I was therefore wondering if it is possible to expand a path of given line width to a shape as is possible with vector graphic software like Adobe Illustrator or Affinity Designer.

Expand stroke

When drawing the spiral in several sections (left part of the loop and right part of the loop), this would allow using a code similar to the following example:

\documentclass[tikz]{standalone}

\begin{document}
    \begin{tikzpicture}[even odd rule]
        \newcommand{\radiusX}{0.7}
        \newcommand{\radiusY}{1.5}
        \newcommand{\strokeWidth}{0.1}
        \newcommand{\strokeWidthExtra}{0.1}
    
        \newcommand{\background}{({-\radiusX-1},-1) rectangle ({8+\radiusX+1},{2*\radiusY+1})}
        
        \newcommand{\leftArc}{
            (0.5, 0) 
                -- (0, 0) 
                arc (-90:-270:{\radiusX} and {\radiusY}) 
                -- ++(0, -\strokeWidth) 
                arc (90:270:{\radiusX-\strokeWidth} and {\radiusY-\strokeWidth}) 
                -- ++(0.5,0) 
                -- ++(0,-\strokeWidth) 
                -- cycle
        }
        
        \newcommand{\leftArcBig}{
            ({0.5+\strokeWidthExtra}, -\strokeWidthExtra) 
                -- ++({-0.5-\strokeWidthExtra}, 0) 
                arc (-90:-270:{\radiusX+\strokeWidthExtra} and {\radiusY+\strokeWidthExtra}) 
                -- ++(0, {-\strokeWidth-2*\strokeWidthExtra}) 
                arc (90:270:{\radiusX-\strokeWidth-\strokeWidthExtra} and {\radiusY-\strokeWidth-\strokeWidthExtra}) 
                -- ++({0.5+\strokeWidthExtra},0) 
                -- ++(0,{-\strokeWidth+2*\strokeWidthExtra}) 
                -- cycle
        }
        
        \newcommand{\rightArc}{
            (-0.5,0) 
                -- (0,0) 
                arc (-90:90:{\radiusX} and {\radiusY}) 
                -- ++(0,-\strokeWidth) 
                arc (90:-90:{\radiusX-\strokeWidth} and {\radiusY-\strokeWidth}) 
                -- ++(-0.5,0) 
                -- ++(0,-{\strokeWidth}) 
                -- cycle
        }
        
        \newcommand{\rightArcBig}{
            (-{0.5-\strokeWidthExtra},-{\strokeWidthExtra}) 
                -- ++({0.5+\strokeWidthExtra},0) 
                arc (-90:90:{\radiusX+\strokeWidthExtra} and {\radiusY+\strokeWidthExtra}) 
                -- ++(0,{-\strokeWidth-2*\strokeWidthExtra}) 
                arc (90:-90:{\radiusX-\strokeWidth-\strokeWidthExtra} and {\radiusY-\strokeWidth-\strokeWidthExtra}) 
                -- ++({-0.5-\strokeWidthExtra},0) 
                -- ++(0,{-\strokeWidth-2*\strokeWidthExtra}) 
                -- cycle
        }
        
        \shade[clip, top color = gray, bottom color = lightgray] \background;
            
        \begin{scope}
            \fill [black] \rightArc;
            \clip \rightArcBig \background;
                
            \fill [black] \leftArc;
        \end{scope}
        
        \begin{scope}[xshift = 2cm]
            \fill [yellow] \rightArc;
            \fill [yellow, fill opacity = 0.3] \rightArcBig;
            \fill [red] \leftArc;
            \fill [red, fill opacity = 0.3] \leftArcBig;
        \end{scope}
        
        \begin{scope}[xshift = 6cm]
            \fill [black] \leftArc;
            \clip \leftArcBig \background;
                
            \fill [black] \rightArc;
        \end{scope}
        
        \begin{scope}[xshift = 8cm]
            \fill [yellow] \leftArc;
            \fill [yellow, fill opacity = 0.3] \leftArcBig;
            \fill [red] \rightArc;
            \fill [red, fill opacity = 0.3] \rightArcBig;
        \end{scope}
        
    \end{tikzpicture}
\end{document}

Clip path

4
  • Maybe you can use a decoration like the ones under this thread to get what you want. (I think one can also use clips in decorations but I did not check.)
    – user229669
    Commented Nov 29, 2020 at 1:07
  • Even if you could, it wouldn't work as far as I know. As explained, clip uses the center line of a path not the total width of the latter, thus clipping inside the decoration shouldn't work without expanding the stroke first.
    – Sam
    Commented Nov 29, 2020 at 1:30
  • But this is not what I meant. If you create a decoration you can add some reverse clip that spares out some region around the intersection, see e.g. here for an application of such inverted clips. However, having a path expansion that always works is currently not available as far as I know. (If you have a monochromous background, you can work with double or knots.)
    – user229669
    Commented Nov 29, 2020 at 1:34
  • BTW, if you want to know why what you are asking is a tough cookie: pomax.github.io/bezierinfo/#offsetting.
    – user229669
    Commented Nov 29, 2020 at 2:17

2 Answers 2

4

Not really an answer. You are asking whether there is a way to construct the envelope of a path. The answer is that there is no built-in or simple way to accomplish this. Even worse, there is an analytic proof that there is no simple and general way. To appreciate the proof, recall that all TikZ can do is to construct Bézier curves. Note that this does not tell you that there is no not-so-simple way. In fact, the fact that MetaPost and friends have routines for that tell you that it is in principle possible.

Another tool that is able to do that is the viewer. OK, let's let the viewer do the dirty work. This allows one to solve the problem in another way, which is conceptually the same as this post: fadings. Not very convenient, at least not the following implementation, yet a proof of principle. Basically you can convert a gray level to transparency, and thus make a black or white line transparent. This object can be put on top an arbitrary background. (Did I already mention that this implementation is not convenient?)

\documentclass{standalone}
\usepackage{tikz}
\usetikzlibrary{decorations.pathreplacing,fadings}%
\begin{document}
\begin{tikzfadingfrompicture}[name=custom fade]%
\tikzset{path decomposition/.style={%
    postaction={decoration={show path construction,
    lineto code={
      \draw[#1]  (\tikzinputsegmentfirst) -- (\tikzinputsegmentlast);
    },
    curveto code={
      \draw[#1]  (\tikzinputsegmentfirst) .. controls
        (\tikzinputsegmentsupporta) and (\tikzinputsegmentsupportb)
        ..(\tikzinputsegmentlast) ;
    },
    closepath code={
      \draw[#1]  (\tikzinputsegmentfirst) -- (\tikzinputsegmentlast) {closepath};} }
    ,decorate}},
    cv/.style={black, double=white,line width=0.6mm,double distance=1.2mm}}
\draw[cv,samples=201,domain=-2*pi:2*pi,smooth,
 path decomposition={cv,shorten <=-0.05pt,shorten >=-0.05pt}]
 plot (\x, {cos(10*\x r)} , {sin(10*\x r)} );
\end{tikzfadingfrompicture}%
\begin{tikzpicture}
  \shade[clip, top color = gray!50!black, bottom color = gray!10] 
       (0,-2) rectangle (6,2);
 \path[path fading=custom fade,fit fading=false,
      fill=black] (0,-2) rectangle (8,2);
\end{tikzpicture}
\end{document}

enter image description here

5
  • This works like a charm. Would you mind explaining in a bit more detail what exactly you're doing inside the tikzfadingfrompicture environment? I find it quite hard to understand. You somehow manage to convert the line to a shape that can be filled but I don't understand the code.
    – Sam
    Commented Nov 29, 2020 at 10:54
  • @Sam This is nicely explained in section 23.4 Fadings of the pgfmanual 3.1.7.
    – user229669
    Commented Nov 29, 2020 at 14:50
  • I have encountered one major problem. Your code works fine as a standalone and I was fiddling around with it to adjust it to my special needs which all worked fine in a minimal example, but as soon as I include the external library (which of course I use in my thesis) the document won't compile anymore. The error message is very cryptic as it say ./main.tex:54: LaTeX Error: \begin{tikzfadingfrompicture} on input line 49 ended by \end{document}.. As soon as I remove the external library again, it works just fine, so I definitely have not forgotten the \end{tikzfadingfrompicture}...
    – Sam
    Commented Nov 29, 2020 at 15:48
  • @Sam You could try wrapping this stuff in \tikzexternaldisable ... \tikzexternalenable. There are many more things that I did not get to work properly with external such as \tikzmarknode from the tikzmark library.
    – user229669
    Commented Nov 29, 2020 at 15:56
  • To anyone interested: There are many problems with the proposed hack. Whereas it works well in minimal examples, I found - as already mentioned - that it does not work with the external library. Moreover, inside my document the exact copy-pasted code only generates a square of the clip size in the color of the spiral. At the moment I‘m not sure what triggers this issue I‘ll have to look into this. When just saving the pdf generated by the minimal example and including it with includegraphics the coil disappears. So this might be a solution for you, but also it may be not.
    – Sam
    Commented Nov 30, 2020 at 8:05
4

Purely by coincidence, I've been working on some code that might help you. It is designed to split a path at intersection points.

It is based on my spath3(ctan and github) library which provides a structure for manipulating paths after they have been defined but before they have been fixed.

It is very definitely experimental code and subject to change, but it would be useful to have feedback as to whether it makes sense and what would make it useful.

\documentclass{article}

\usepackage{xparse}
\usepackage{tikz}
\usepackage{spath3}
\usetikzlibrary{intersections,hobby,patterns}

\ExplSyntaxOn

\tikzset{
  append~ spath/.code={
    \spath_get_current_path:n {current path}
    \spath_append:nn { current path } { #1 }
    \spath_set_current_path:n { current path }
  },
  set~ spath/.code={
    \spath_set_current_path:n { #1 }
    \spath_get:nnN {#1} {final point} \l__spath_tmpa_tl
    \tl_set:Nx \l__spath_tmpa_tl
    {
      \exp_not:c {tikz@lastx}=\tl_item:Nn \l__spath_tmpa_tl {1}
      \exp_not:c {tikz@lasty}=\tl_item:Nn \l__spath_tmpa_tl {2}
      \exp_not:c {tikz@lastxsaved}=\tl_item:Nn \l__spath_tmpa_tl {1}
      \exp_not:c {tikz@lastysaved}=\tl_item:Nn \l__spath_tmpa_tl {2}
    }
    \tl_use:N \l__spath_tmpa_tl
  },
  shorten~spath~at~end/.code~ 2~ args={
    \spath_shorten:nn {#1} {#2}
  },
  shorten~spath~at~start/.code~ 2~ args ={
    \spath_reverse:n {#1}
    \spath_shorten:nn {#1} {#2}
    \spath_reverse:n {#1}
  },
  shorten~spath~both~ends/.code~ 2~ args={
    \spath_shorten:nn {#1} {#2}
    \spath_reverse:n {#1}
    \spath_shorten:nn {#1} {#2}
    \spath_reverse:n {#1}
  },
  globalise~ spath/.code={
    \spath_globalise:n {#1}
  },
  translate~ spath/.code~ n~ args={3}{
    \spath_translate:nnn {#1}{#2}{#3}
  },
  split~ at~ self~ intersections/.code~ 2~ args={
    \use:c {tikz@addmode}{
      \group_begin:
      \spath_get_current_path:n {spath split tmpa}
      \spath_split_at_self_intersections:nnn {spath split tmpa} {#1} {#2}
      \group_end:
    }
  },
  split~ at~ intersections/.code~ n~ args={5}{
    \spath_split_at_intersections:nnnnn {#1}{#2}{#3}{#4}{#5}
  }
}


\tl_new:N \l__spath_shorten_fa_tl
\tl_new:N \l__spath_shorten_path_tl
\tl_new:N \l__spath_shorten_last_tl
\int_new:N \l__spath_shorten_int
\fp_new:N \l__spath_shorten_x_fp
\fp_new:N \l__spath_shorten_y_fp

\cs_new_nopar:Npn \spath_shorten:nn #1#2
{
  \group_begin:
  \spath_get:nnN {#1} {final action} \l__spath_shorten_fa_tl
  \spath_get:nnN {#1} {path} \l__spath_shorten_path_tl
  \tl_reverse:N \l__spath_shorten_path_tl

  \tl_clear:N \l__spath_shorten_last_tl
  \tl_if_eq:NNTF \l__spath_shorten_fa_tl \g__spath_curveto_tl
  {
    \int_set:Nn \l__spath_shorten_int {3}
  }
  {
    \int_set:Nn \l__spath_shorten_int {1}
  }

  \prg_replicate:nn { \l__spath_shorten_int }
  {
    \tl_put_right:Nx \l__spath_shorten_last_tl
    {
      {\tl_head:N \l__spath_shorten_path_tl}
    }
    \tl_set:Nx \l__spath_shorten_path_tl {\tl_tail:N \l__spath_shorten_path_tl}
    \tl_put_right:Nx \l__spath_shorten_last_tl
    {
      {\tl_head:N \l__spath_shorten_path_tl}
    }
    \tl_set:Nx \l__spath_shorten_path_tl {\tl_tail:N \l__spath_shorten_path_tl}
    \tl_put_right:Nx \l__spath_shorten_last_tl
    {
      \tl_head:N \l__spath_shorten_path_tl
    }
    \tl_set:Nx \l__spath_shorten_path_tl {\tl_tail:N \l__spath_shorten_path_tl}
  }

  \tl_put_right:Nx \l__spath_shorten_last_tl
  {
    {\tl_item:Nn \l__spath_shorten_path_tl {1}}
    {\tl_item:Nn \l__spath_shorten_path_tl {2}}
  }
  \tl_put_right:NV \l__spath_shorten_last_tl \g__spath_moveto_tl
  
  \tl_reverse:N \l__spath_shorten_path_tl

  \fp_set:Nn \l__spath_shorten_x_fp
  {
    \dim_to_fp:n {\tl_item:Nn \l__spath_shorten_last_tl {4}}
    -
    \dim_to_fp:n {\tl_item:Nn \l__spath_shorten_last_tl {1}}
  }
  
  \fp_set:Nn \l__spath_shorten_y_fp
  {
    \dim_to_fp:n {\tl_item:Nn \l__spath_shorten_last_tl {5}}
    -
    \dim_to_fp:n {\tl_item:Nn \l__spath_shorten_last_tl {2}}
  }

  \fp_set:Nn \l__spath_shorten_len_fp
  {
    sqrt( \l__spath_shorten_x_fp * \l__spath_shorten_x_fp +  \l__spath_shorten_y_fp *  \l__spath_shorten_y_fp )
  }

  \fp_set:Nn \l__spath_shorten_len_fp
  {
    (\l__spath_shorten_len_fp - #2)/ \l__spath_shorten_len_fp
  }

  \tl_reverse:N \l__spath_shorten_last_tl
  
  \tl_if_eq:NNTF \l__spath_shorten_fa_tl \g__spath_curveto_tl
  {
    \fp_set:Nn \l__spath_shorten_len_fp
    {
      1 - (1 -\l__spath_shorten_len_fp)/3
    }
    \spath_split_curve:VVNN \l__spath_shorten_len_fp \l__spath_shorten_last_tl
    \l__spath_shorten_lasta_tl
    \l__spath_shorten_lastb_tl
  }
  {
    \spath_split_line:VVNN \l__spath_shorten_len_fp \l__spath_shorten_last_tl
    \l__spath_shorten_lasta_tl
    \l__spath_shorten_lastb_tl
  }

  \prg_replicate:nn {3}
  {
    \tl_set:Nx \l__spath_shorten_lasta_tl {\tl_tail:N \l__spath_shorten_lasta_tl}
  }

  \tl_put_right:NV \l__spath_shorten_path_tl \l__spath_shorten_lasta_tl

  \tl_gset_eq:NN \l__spath_smuggle_tl \l__spath_shorten_path_tl
  \group_end:

  \spath_clear:n {#1}
  \spath_put:nnV {#1} {path} \l__spath_smuggle_tl
}

\cs_generate_variant:Nn \spath_shorten:nn {Vn, VV}
\cs_generate_variant:Nn \spath_reverse:n {V}
\cs_generate_variant:Nn \spath_append_no_move:nn {VV}
\cs_generate_variant:Nn \spath_prepend_no_move:nn {VV}

\cs_new_nopar:Npn \spath_intersect:nn #1#2
{
  \spath_get:nnN {#1} {path} \l__spath_tmpa_tl
  \spath_get:nnN {#2} {path} \l__spath_tmpb_tl
  \pgfintersectionofpaths%
  {%
    \pgfsetpath\l__spath_tmpa_tl
  }{%
    \pgfsetpath\l__spath_tmpb_tl
  }
}

\cs_generate_variant:Nn \spath_intersect:nn {VV, Vn}

\cs_new_nopar:Npn \spath_split_line:nnNN #1#2#3#4
{
  \group_begin:
  \tl_gclear:N \l__spath_smuggle_tl
  \tl_set_eq:NN \l__spath_tmpa_tl \g__spath_moveto_tl
  \tl_put_right:Nx \l__spath_tmpa_tl {
    {\tl_item:nn {#2} {2}}
    {\tl_item:nn {#2} {3}}
  }
  \tl_put_right:NV \l__spath_tmpa_tl \g__spath_lineto_tl
  \tl_put_right:Nx \l__spath_tmpa_tl
  {
    {\fp_to_dim:n
    {
      (1 - #1) * \tl_item:nn {#2} {2} + (#1) * \tl_item:nn {#2} {5}
    }}
    {\fp_to_dim:n
    {
      (1 - #1) * \tl_item:nn {#2} {3} + (#1) * \tl_item:nn {#2} {6}
    }}
  }
  \tl_gset_eq:NN \l__spath_smuggle_tl \l__spath_tmpa_tl
  \group_end:
  \tl_set_eq:NN #3 \l__spath_smuggle_tl
  \group_begin:
  \tl_gclear:N \l__spath_smuggle_tl
  \tl_set_eq:NN \l__spath_tmpa_tl \g__spath_moveto_tl
  \tl_put_right:Nx \l__spath_tmpa_tl
  {
    {\fp_to_dim:n
    {
      (1 - #1) * \tl_item:nn {#2} {2} + (#1) * \tl_item:nn {#2} {5}
    }}
    {\fp_to_dim:n
    {
      (1 - #1) * \tl_item:nn {#2} {3} + (#1) * \tl_item:nn {#2} {6}
    }}
  }
  \tl_put_right:NV \l__spath_tmpa_tl \g__spath_lineto_tl
  \tl_put_right:Nx \l__spath_tmpa_tl {
    {\tl_item:nn {#2} {5}}
    {\tl_item:nn {#2} {6}}
  }
  \tl_gset_eq:NN \l__spath_smuggle_tl \l__spath_tmpa_tl
  \group_end:
  \tl_set_eq:NN #4 \l__spath_smuggle_tl
}

\cs_generate_variant:Nn \spath_split_line:nnNN {nVNN, VVNN}

\int_new:N \l__spath_split_int
\int_new:N \l__spath_splitat_int
\fp_new:N \l__spath_split_fp
\bool_new:N \l__spath_split_bool
\tl_new:N \l__spath_split_path_tl
\tl_new:N \l__spath_split_patha_tl
\tl_new:N \l__spath_split_pathb_tl
\tl_new:N \l__spath_split_intoa_tl
\tl_new:N \l__spath_split_intob_tl
\dim_new:N \l__spath_splitx_dim
\dim_new:N \l__spath_splity_dim

\cs_new_nopar:Npn \spath_split_at:nnnn #1#2#3#4
{
  \group_begin:
  \int_set:Nn \l__spath_splitat_int {\fp_to_int:n {floor(#2) + 1}}
  \fp_set:Nn \l__spath_split_fp {#2 - floor(#2)}
  \int_zero:N \l__spath_split_int
  \bool_set_true:N \l__spath_split_bool

  \spath_get:nnN {#1} {path} \l__spath_split_path_tl
  \tl_clear:N \l__spath_split_patha_tl

  \dim_zero:N \l__spath_splitx_dim
  \dim_zero:N \l__spath_splity_dim

  \bool_until_do:nn {
    \tl_if_empty_p:N \l__spath_split_path_tl
    ||
    \int_compare_p:n { \l__spath_splitat_int == \l__spath_split_int  }
  }
  {
    \tl_set:Nx \l__spath_tmpc_tl {\tl_head:N \l__spath_split_path_tl}
    \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
    \tl_case:Nn \l__spath_tmpc_tl
    {
      \g__spath_lineto_tl
      {
        \int_incr:N \l__spath_split_int
      }
      \g__spath_curvetoa_tl
      {
        \int_incr:N \l__spath_split_int
      }
    }
    \int_compare:nT { \l__spath_split_int < \l__spath_splitat_int  }
    {
      \tl_put_right:NV \l__spath_split_patha_tl \l__spath_tmpc_tl
      
      \tl_put_right:Nx \l__spath_split_patha_tl
      {{ \tl_head:N \l__spath_split_path_tl }}
      \dim_set:Nn \l__spath_splitx_dim {\tl_head:N \l__spath_split_path_tl}
      \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
      
      \tl_put_right:Nx \l__spath_split_patha_tl
      {{ \tl_head:N \l__spath_split_path_tl }}
      \dim_set:Nn \l__spath_splity_dim {\tl_head:N \l__spath_split_path_tl}
      \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
      
    }
  }

  \tl_clear:N \l__spath_split_pathb_tl
  \tl_put_right:NV \l__spath_split_pathb_tl \g__spath_moveto_tl
  \tl_put_right:Nx \l__spath_split_pathb_tl
  {
    {\dim_use:N \l__spath_splitx_dim}
    {\dim_use:N \l__spath_splity_dim}
  }
  \tl_case:Nn \l__spath_tmpc_tl
  {
    \g__spath_lineto_tl
    {
      \tl_put_right:NV \l__spath_split_pathb_tl \l__spath_tmpc_tl
      \tl_put_right:Nx \l__spath_split_pathb_tl
      {{ \tl_head:N \l__spath_split_path_tl }}
      \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
      
      \tl_put_right:Nx \l__spath_split_pathb_tl
      {{ \tl_head:N \l__spath_split_path_tl }}
      \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
      
      \spath_split_line:VVNN \l__spath_split_fp \l__spath_split_pathb_tl
      \l__spath_split_intoa_tl
      \l__spath_split_intob_tl

      \prg_replicate:nn {3} {
        \tl_set:Nx \l__spath_split_intoa_tl {\tl_tail:N \l__spath_split_intoa_tl}
      }

      \tl_put_right:NV \l__spath_split_patha_tl \l__spath_split_intoa_tl
      \tl_put_right:NV \l__spath_split_intob_tl \l__spath_split_path_tl
    }
    \g__spath_curvetoa_tl
    {
      \tl_put_right:NV \l__spath_split_pathb_tl \l__spath_tmpc_tl
      \tl_put_right:Nx \l__spath_split_pathb_tl
      {{ \tl_head:N \l__spath_split_path_tl }}
      \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
      
      \tl_put_right:Nx \l__spath_split_pathb_tl
      {{ \tl_head:N \l__spath_split_path_tl }}
      \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
      
      \prg_replicate:nn {2} {
        
        \tl_put_right:Nx \l__spath_split_pathb_tl
        { \tl_head:N \l__spath_split_path_tl }
        \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
        
        \tl_put_right:Nx \l__spath_split_pathb_tl
        {{ \tl_head:N \l__spath_split_path_tl }}
        \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
      
        \tl_put_right:Nx \l__spath_split_pathb_tl
        {{ \tl_head:N \l__spath_split_path_tl }}
        \tl_set:Nx \l__spath_split_path_tl {\tl_tail:N \l__spath_split_path_tl }
      }

      \spath_split_curve:VVNN \l__spath_split_fp \l__spath_split_pathb_tl
      \l__spath_split_intoa_tl
      \l__spath_split_intob_tl

      \prg_replicate:nn {3} {
        \tl_set:Nx \l__spath_split_intoa_tl {\tl_tail:N \l__spath_split_intoa_tl}
      }

      \tl_put_right:NV \l__spath_split_patha_tl \l__spath_split_intoa_tl
      \tl_put_right:NV \l__spath_split_intob_tl \l__spath_split_path_tl
    }
  }

  \spath_gclear_new:n {#3}
  \spath_gput:nnV {#3} {path} \l__spath_split_patha_tl
  \spath_gclear_new:n {#4}
  \spath_gput:nnV {#4} {path} \l__spath_split_intob_tl
  \group_end:
}

\cs_generate_variant:Nn \spath_split_at:nnnn {VVnn, Vnnn}

\cs_new_nopar:Npn \spath_explode_into_list:nn #1#2
{
  \tl_clear_new:c {l__spath_list_#2}

  \int_zero:N \l__spath_tmpa_int
  \spath_map_segment_inline:nn {#1} {
    \tl_if_eq:NNF ##1 \g__spath_moveto_tl
    {
      \spath_clear_new:n {#2 _ \int_use:N \l__spath_tmpa_int}
      \spath_put:nnV  {#2 _ \int_use:N \l__spath_tmpa_int} {path} ##2
      \tl_put_right:cx {l__spath_list_#2} {{#2 _ \int_use:N \l__spath_tmpa_int}}
      \int_incr:N \l__spath_tmpa_int
    }
  }
}

\tl_new:N \spathselfintersectioncount

\tl_new:N \l__spath_split_tmpa_tl
\tl_new:N \l__spath_split_path_a_tl
\tl_new:N \l__spath_split_path_b_tl
\tl_new:N \l__spath_split_join_a_tl
\tl_new:N \l__spath_split_join_b_tl
\tl_new:N \l__spath_split_first_tl
\tl_new:N \l__spath_split_second_tl

\tl_new:N \l__spath_split_one_tl
\tl_set:Nn \l__spath_split_one_tl {1}
\tl_new:N \l__spath_split_I_tl
\tl_set:Nn \l__spath_split_I_tl {I}

\int_new:N \l__spath_split_count_int
\int_new:N \l__spath_split_intersection_int
\seq_new:N \l__spath_split_segments_seq
\seq_new:N \l__spath_split_segments_processed_seq
\seq_new:N \l__spath_split_segments_middle_seq

\seq_new:N \l__spath_split_joins_seq
\seq_new:N \l__spath_split_joins_processed_seq
\seq_new:N \l__spath_split_joins_middle_seq

\seq_new:N \l__spath_split_intersections_seq

\bool_new:N \l__spath_split_join_bool

% We'll run this on each segment
%
% Arguments:
%  1. Path to split
%  2. Prefix for name of new paths
%  3. List of how to split at intersections
%     A - don't split first path at intersection
%     B - don't split second path at intersection
%     C - split both paths at intersection
%
\cs_new_nopar:Npn \spath_split_at_self_intersections:nnn #1#2#3
{
  \group_begin:
  % The third argument says whether to rejoin segments at the intersections
  \seq_set_split:Nnn \l__spath_split_intersections_seq {} {#3}
  % Clone the path as we'll mess around with it
  \spath_clone:nn {#1} {spath split tmp}
  % Clear the sequence of joining information
  % The join information says whether to rejoin a segment to its predecessor
  \seq_clear:N \l__spath_split_joins_seq
  % Check the last action to see if it is a close path
  \spath_get:nnN {spath split tmp} {final action} \l__spath_split_tmpa_tl
  \tl_if_eq:NNTF \l__spath_split_tmpa_tl \g__spath_closepath_tl
  {
    % Last action is a close, so mark it as needing rejoining
    \seq_put_right:Nn \l__spath_split_joins_seq {1}
  }
  {
    % Last action is not a close, so mark it as needing rejoining
    \seq_put_right:Nn \l__spath_split_joins_seq {0}
  }
  % Remove close paths
  \spath_open_path:n {spath split tmp}
  % Separate into segments (creates a token list)
  \spath_explode_into_list:nn {spath split tmp}{split segments}
  % so convert to a sequence
  \seq_set_split:NnV \l__spath_split_segments_seq {} \l__spath_list_splitsegments

  % Iterate over the number of terms in the sequence, adding the
  % rejoining information
  \int_step_inline:nnnn {1} {1} {\seq_count:N \l__spath_split_segments_seq - 1}
  {
    \seq_put_right:Nn \l__spath_split_joins_seq {1}
  }
  
  % Clear a couple of auxiliaries
  \seq_clear:N \l__spath_split_segments_processed_seq
  \seq_clear:N \l__spath_split_joins_processed_seq
  \int_zero:N \l__spath_split_count_int
  \int_zero:N \l__spath_split_intersection_int

  % Iterate over the sequence
  \bool_while_do:nn
  {
    !\seq_if_empty_p:N \l__spath_split_segments_seq
  }
  {
    % Remove the left-most items for consideration
    \seq_pop_left:NN \l__spath_split_segments_seq \l__spath_split_path_a_tl
    \seq_pop_left:NN \l__spath_split_joins_seq \l__spath_split_join_a_tl

    % Clear some sequences, these will hold any pieces we create from splitting our path under consideration except for the first piece
    \seq_clear:N \l__spath_split_segments_middle_seq
    \seq_clear:N \l__spath_split_joins_middle_seq

    % Put the rejoining information in the processed sequence
    \seq_put_right:NV \l__spath_split_joins_processed_seq \l__spath_split_join_a_tl
    
    % Iterate over the rest of the segments
    \int_step_inline:nnnn {1} {1} {\seq_count:N \l__spath_split_segments_seq}
    {
      % Store the next segment for intersection
      \tl_set:Nx \l__spath_split_path_b_tl {\seq_item:Nn \l__spath_split_segments_seq {##1}}
      % Get the next joining information
      \tl_set:Nx \l__spath_split_join_b_tl {\seq_item:Nn \l__spath_split_joins_seq {##1}}
      % And put it onto our saved stack of joins
      \seq_put_right:NV \l__spath_split_joins_middle_seq \l__spath_split_join_b_tl
      
      % Sort intersections along the first path
      \pgfintersectionsortbyfirstpath
      % Find the intersections of these segments
      \spath_intersect:VV \l__spath_split_path_a_tl \l__spath_split_path_b_tl

      % If we get intersections
      \int_compare:nTF {\pgfintersectionsolutions > 0}
      {
        % Find the times of the first intersection (which will be the first along the segment we're focussing on)
        \pgfintersectiongetsolutiontimes{1}{\l__spath_split_first_tl}{\l__spath_split_second_tl}

        % Ignore intersections that are very near end points
        \bool_if:nT {
          \fp_compare_p:n {
            \l__spath_split_first_tl < .99
          }
          &&
          \fp_compare_p:n {
            \l__spath_split_first_tl > .01
          }
          &&
          \fp_compare_p:n {
            \l__spath_split_second_tl < .99
          }
          &&
          \fp_compare_p:n {
            \l__spath_split_second_tl > .01
          }
        }
        {
          % We have a genuine intersection
          \int_incr:N \l__spath_split_intersection_int
        }

        % Do we split the first path?
        \bool_if:nT {
          \fp_compare_p:n {
            \l__spath_split_first_tl < .99
          }
          &&
          \fp_compare_p:n {
            \l__spath_split_first_tl > .01
          }
        }
        {
          % Split the first path at the intersection
          \spath_split_at:VVnn \l__spath_split_path_a_tl \l__spath_split_first_tl {split \int_use:N \l__spath_split_count_int}{split \int_eval:n { \l__spath_split_count_int + 1}}

          % Put the latter part into our temporary sequence
          \seq_put_left:Nx \l__spath_split_segments_middle_seq {split \int_eval:n{ \l__spath_split_count_int + 1}}
          % Mark this intersection in the joining information
          % Label the breaks as "IA#" and "IB#"
          \seq_put_left:Nx \l__spath_split_joins_middle_seq {IA \int_use:N  \l__spath_split_intersection_int }
          
          % Replace our segment under consideration by the initial part
          \tl_set:Nx \l__spath_split_path_a_tl {split \int_use:N \l__spath_split_count_int }
          % Increment our counter
          \int_incr:N \l__spath_split_count_int
          \int_incr:N \l__spath_split_count_int
        }

        % Do we split the second path?
        \bool_if:nTF {
          \fp_compare_p:n {
            \l__spath_split_second_tl < .99
          }
          &&
          \fp_compare_p:n {
            \l__spath_split_second_tl > .01
          }
        }
        {
          % Split the second segment at the intersection point
          \spath_split_at:VVnn \l__spath_split_path_b_tl \l__spath_split_second_tl {split \int_use:N \l__spath_split_count_int}{split \int_eval:n { \l__spath_split_count_int + 1}}

          % Add these segments to our list of segments we've considered
          \seq_put_right:Nx \l__spath_split_segments_middle_seq {split \int_eval:n{ \l__spath_split_count_int}}
          \seq_put_right:Nx \l__spath_split_segments_middle_seq {split \int_eval:n{ \l__spath_split_count_int + 1}}
          \seq_put_right:Nx \l__spath_split_joins_middle_seq {IB \int_use:N \l__spath_split_intersection_int}
          
          % Increment the counter
          \int_incr:N \l__spath_split_count_int
          \int_incr:N \l__spath_split_count_int
        }
        {
          % If we didn't split the second segment, we just put the second segment on the list of segments we've considered
          \seq_put_right:NV \l__spath_split_segments_middle_seq \l__spath_split_path_b_tl
        }

      }
      {
        % If we didn't split the second segment, we just put the second segment on the list of segments we've considered
        \seq_put_right:NV \l__spath_split_segments_middle_seq \l__spath_split_path_b_tl
      }

    }
    % Having been through the loop for our segment under consideration, we replace the segment list since some of them might have been split and add any remainders of the segment under consideration
    \seq_set_eq:NN \l__spath_split_segments_seq \l__spath_split_segments_middle_seq
    \seq_set_eq:NN \l__spath_split_joins_seq \l__spath_split_joins_middle_seq

    % We add the initial segment to our sequence of dealt with segments
    \seq_put_right:NV \l__spath_split_segments_processed_seq \l__spath_split_path_a_tl
  }

  \seq_clear:N \l__spath_split_segments_seq
  
  \tl_set:Nx \l__spath_split_path_a_tl {\seq_item:Nn \l__spath_split_segments_processed_seq {1}}
  
  \int_step_inline:nnnn {2} {1} {\seq_count:N \l__spath_split_segments_processed_seq}
  {
    % Get the next path and joining information
    \tl_set:Nx \l__spath_split_path_b_tl {\seq_item:Nn \l__spath_split_segments_processed_seq {##1}}
    \tl_set:Nx \l__spath_split_join_b_tl {\seq_item:Nn \l__spath_split_joins_processed_seq {##1}}

    % Do we join this to our previous path?
    \bool_set_false:N \l__spath_split_join_bool

    % If it came from when we split the original path, join them
    \tl_if_eq:NNT \l__spath_split_join_b_tl \l__spath_split_one_tl
    {
      \bool_set_true:N \l__spath_split_join_bool
    }

    % Is this a labelled intersection?
    \tl_set:Nx \l__spath_split_tmpa_tl {\tl_head:N \l__spath_split_join_b_tl}
    \tl_if_eq:NNT \l__spath_split_tmpa_tl \l__spath_split_I_tl
    {
      % Strip off the "I" prefix
      \tl_set:Nx \l__spath_split_tmpa_tl {\tl_tail:N \l__spath_split_join_b_tl}

      % Next letter is "A" or "B"
      \tl_set:Nx \l__spath_split_join_b_tl {\tl_head:N \l__spath_split_tmpa_tl}

      % Remainder is the intersection index
      \int_compare:nTF {\tl_tail:N \l__spath_split_tmpa_tl <= \seq_count:N \l__spath_split_intersections_seq}
      {
        \tl_set:Nx \l__spath_split_join_a_tl {\seq_item:Nn \l__spath_split_intersections_seq {\tl_tail:N \l__spath_split_tmpa_tl}}
      }
      {
        % Default is to rejoin neither segment
        \tl_set:Nn \l__spath_split_join_a_tl {C}
      }

      \tl_if_eq:NNT \l__spath_split_join_a_tl \l__spath_split_join_b_tl
      {
        \bool_set_true:N \l__spath_split_join_bool
      }
      
    }

    \bool_if:NTF \l__spath_split_join_bool
    {
      % Yes, so append it
      \spath_append_no_move:VV \l__spath_split_path_a_tl \l__spath_split_path_b_tl
    }
    {
      % No, so put the first path onto the stack
      \seq_put_right:NV \l__spath_split_segments_seq \l__spath_split_path_a_tl

      % Swap out the paths
      \tl_set_eq:NN \l__spath_split_path_a_tl \l__spath_split_path_b_tl
    }
  }

  % Do we need to add the first path to the last?
  \tl_set:Nx \l__spath_split_join_a_tl {\seq_item:Nn \l__spath_split_joins_processed_seq {1}}

  \tl_if_eq:NNTF \l__spath_split_join_a_tl \l__spath_split_one_tl
  {
    \tl_set:Nx \l__spath_split_path_b_tl {\seq_item:Nn \l__spath_split_segments_processed_seq {1}}
    \spath_prepend_no_move:VV \l__spath_split_path_b_tl \l__spath_split_path_a_tl
    
  }
  {
    \seq_put_right:NV \l__spath_split_segments_seq \l__spath_split_path_a_tl
  }

  % Put our paths into a list
  \int_zero:N \l__spath_split_count_int
  \seq_map_inline:Nn \l__spath_split_segments_seq
  {
    \int_incr:N \l__spath_split_count_int
    \spath_gclone:nn {##1} {#2~\int_use:N \l__spath_split_count_int}
  }
  \tl_gset:NV \spathselfintersectioncount \l__spath_split_count_int
  \group_end:
}

\ExplSyntaxOff

\begin{document}

\begin{tikzpicture}[use Hobby shortcut]

\shade[left color=cyan, right color=magenta, shading angle=90] (-.5,-.2) rectangle (7.5,2.2);
\fill[pattern=bricks, pattern color=white] (-.5,-.2) rectangle (7.5,2.2);

\path
[
split at self intersections={coil}{AAAAAAAAAAAAAAAA}
] ([out angle=0]0,0)
.. +(.85,1) .. +(.25,2) .. +(-.35,1) .. ++(.5,0)
.. +(.85,1) .. +(.25,2) .. +(-.35,1) .. ++(.5,0)
.. +(.85,1) .. +(.25,2) .. +(-.35,1) .. ++([in angle=180].5,0)
;

\foreach \k in {1,..., \spathselfintersectioncount} {
  \tikzset{shorten spath both ends={coil \k}{2pt}, globalise spath=coil \k}
}

\foreach \k in {1,..., 4} {
  \draw[set spath=coil \k];
}

\foreach[evaluate=\l as \xshift using \l*.5cm] \l in {0,...,10} {
  \foreach \k in {5,..., 9} {
    \draw[translate spath={coil \k}{\xshift pt}{0pt},set spath=coil \k];
  }
}

\draw[translate spath={coil 10}{5cm}{0pt},set spath=coil 10];

\end{tikzpicture}
\end{document}

Obviously, the vast majority of that will eventually find its way into the spath3 package and the key part is in the tikzpicture at the end. What this does is take the basic path and split it where it self-intersects. It then shortens these pieces to create the gaps. These pieces can then be reused (with translation) to create the coil. The result is the following image, with the background to show that there's no double trickery going on here.

Coil split at intersections

5
  • This looks really great!!! Seems like you have what this feature request is asking for. (To bad I cannot vote. To vote one needs an account, and to have an account one has to accept the terms...)
    – user229669
    Commented Nov 29, 2020 at 16:01
  • Hadn't come across that issue, I've left a comment there pointing to my library. Odd that it wasn't mentioned as the spath3 library has been around for quite a long time. Commented Nov 29, 2020 at 16:29
  • It seems to me that the above keys like append path are much more tailored towards the end user than what the spath3 library currently offers, so I really like "Obviously, the vast majority of that will eventually find its way into the spath3 library". ;-)
    – user229669
    Commented Nov 29, 2020 at 16:42
  • @TikZling I've been slowly adding keys to expose some of the core functionality, more or less as I answer questions on this site that use it! I'm slightly hampered by lack of test code, though, as "works for me" isn't really a good enough standard to send something to CTAN! Commented Nov 29, 2020 at 16:46
  • Yes, I understand and agree. (You could perhaps post something on topanswers.xyz/tex, there are better ways of having code and feedback side by side, then users on this site could test things a bit before you upload to CTAN.)
    – user229669
    Commented Nov 29, 2020 at 16:54

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .