[Tex/LaTex] More on “Cylinder shading with pgf TiKZ”

shadingtikz-pgf

In his answer to Cylinder shading with PGF/TikZ, Jake provides a code to draw a shaded cylinder with a not shaded top.

This code draws a cylinder node (from shapes.geometric library) and after that, with a second draw command, an ellipse is drawn over it.

I've tried to join both steps within a mycylinder/.style with an append after command option without any success. I still don't completely understand what \pgfinterruptpath, \pgfextra do, so may be my code is not correct. I imagine that covering ellipse must be drawn after shading the cylinder but I don't know how to do it. Could you explain what's wrong?

\documentclass[tikz,border=1mm]{standalone}

\usetikzlibrary{calc,fit,backgrounds,positioning,arrows,shapes.geometric}

\begin{document}

\begin{tikzpicture}[font=\sffamily\small,
   >=stealth',
   mycylinder/.style={
draw,
  shape=cylinder,
  alias=cyl, % Will be used by the ellipse to reference the cylinder
  aspect=1.5,
  minimum height=3cm,
  minimum width=2cm,
  left color=blue!30,
  right color=blue!60,
  middle color=blue!10, % Has to be called after left color and middle color
  outer sep=-0.5\pgflinewidth, % to make sure the ellipse does not draw over the lines
  shape border rotate=90,
  append after command={%
    \pgfextra{%
       \pgfinterruptpath
%          \begin{pgfonlayer}{foreground layer}
          \fill [blue!10] let
             \p1 = ($(\tikzlastnode.before top)!0.5! (\tikzlastnode.after top)$),
             \p2 = (\tikzlastnode.top),
             \p3 = (\tikzlastnode.before top),
             \n1={veclen(\x3-\x1,\y3-\y1)},
             \n2={veclen(\x2-\x1,\y2-\y1)},
             \n3={atan2((\y2-\y1),(\x2-\x1))}
          in 
             (\p1) ellipse [x radius=\n1, y radius = \n2, rotate=\n3];
%          \end{pgfonlayer}
       \endpgfinterruptpath%
    }
  }
 }
]

% Left cylinder. Wrong one.
% I would like to draw right cylinder with only one command.
\path node [mycylinder, label=below:Wrong] (disc) {};

% Right cylinder. Correct one but with two commands.
\path node [mycylinder, right=1cm of disc, label=below:Good] (disc2) {};
\fill [blue!10] let
  \p1 = ($(cyl.before top)!0.5!(cyl.after top)$),
  \p2 = (cyl.top),
  \p3 = (cyl.before top),
  \n1={veclen(\x3-\x1,\y3-\y1)},
  \n2={veclen(\x2-\x1,\y2-\y1)},
  \n3={atan2((\y2-\y1),(\x2-\x1))}
 in 
  (\p1) ellipse [x radius=\n1, y radius = \n2, rotate=\n3];

\end{tikzpicture}
\end{document}

enter image description here

Best Answer

TikZ already includes the possibility to insert a separate path in the current one: the edge. (Unfortunately, you cannot use pgfonlayer here. But as the argument append after command will be executed after the node has been placed, this shouldn’t be an issue here.)

Since the CVS version swapped the arguments to the atan2 function (atan2(x, y) to atan2(y, x)), I also included a small block in the preamble to sort this out and define the functions atanXY and atanYX.

I also chose to not change the outer seps but instead to subtract \pgflinewidth directly from the radius. It is an annoyance that these values cannot be accessed after the node.

Code

\documentclass[tikz]{standalone}
\usetikzlibrary{calc,shapes.geometric}
\pgfmathparse{atan2(0,1)}
\ifdim\pgfmathresult pt=0pt % atan2(y, x)
  \tikzset{declare function={atanXY(\x,\y)=atan2(\y,\x);atanYX(\y,\x)=atan2(\y,\x);}}
\else                       % atan2(x, y)
  \tikzset{declare function={atanXY(\x,\y)=atan2(\x,\y);atanYX(\y,\x)=atan2(\x,\y);}}
\fi
\begin{document}
\begin{tikzpicture}[font=\sffamily\small,
   mycylinder/.style={draw, shape=cylinder, aspect=1.5, minimum height=+3cm,
    minimum width=+2cm, left color=blue!30, right color=blue!60, middle color=blue!10,
    shape border rotate=90, append after command={%
      let \p{cyl@center} = ($(\tikzlastnode.before top)!0.5! (\tikzlastnode.after top)$),
          \p{cyl@x}      = ($(\tikzlastnode.before top)-(\p{cyl@center})$),
          \p{cyl@y}      = ($(\tikzlastnode.top)       -(\p{cyl@center})$)
      in (\p{cyl@center}) edge[draw=none, fill=blue!10, to path={
        ellipse [x radius=veclen(\p{cyl@x})-1\pgflinewidth,
                 y radius=veclen(\p{cyl@y})-1\pgflinewidth,
                 rotate=atanXY(\p{cyl@x})]}] () }}]
\node[mycylinder, label=below:Better?] {};
\end{tikzpicture}
\end{document}

Output

enter image description here

Another idea.

The cylinder shape already has the option to fill the two parts differently with the options

  • cylinder body fill=<color>,
  • cylinder end fill=<color> and the switch
  • cylinder uses custom fill.

Unfortunately, the shading in

cylinder end fill=blue!10,  cylinder uses custom fill,
preaction={draw=red, left color=blue!30, right color=blue!60, middle color=blue!10}

will be drawn on top of the cylinder end fill (which is done in a behindbackgroundpath) even though the shading itself is in a preaction.

It may be possible to solve this with a custom shape.

The keys Cylinder end shade and Cylinder body shade actually are implemented by setting their content with \tikzset and using \tikz@finish (similar how the backgroundpath and the foregroundpath are applied).

Code

\documentclass[tikz]{standalone}
\usetikzlibrary{shapes.geometric}
\pgfset{
  Cylinder end fill/.initial=,
  Cylinder body fill/.initial=,
  Cylinder end shade/.initial=,
  Cylinder body shade/.initial=}
\makeatletter
\pgfdeclareshape{Cylinder}{%
  \inheritsavedanchors[from=cylinder]%
  \inheritbackgroundpath[from=cylinder]%
  \inheritanchorborder[from=cylinder]%
  \inheritanchor[from=cylinder]{center}\inheritanchor[from=cylinder]{shape center}%
  \inheritanchor[from=cylinder]{mid}\inheritanchor[from=cylinder]{mid east}%
  \inheritanchor[from=cylinder]{mid west}\inheritanchor[from=cylinder]{base}%
  \inheritanchor[from=cylinder]{base east}\inheritanchor[from=cylinder]{base west}%
  \inheritanchor[from=cylinder]{north}\inheritanchor[from=cylinder]{south}%
  \inheritanchor[from=cylinder]{east}\inheritanchor[from=cylinder]{west}%
  \inheritanchor[from=cylinder]{north east}\inheritanchor[from=cylinder]{south west}%
  \inheritanchor[from=cylinder]{south east}\inheritanchor[from=cylinder]{north west}%
  \inheritanchor[from=cylinder]{before top}\inheritanchor[from=cylinder]{top}%
  \inheritanchor[from=cylinder]{after top}\inheritanchor[from=cylinder]{before bottom}%
  \inheritanchor[from=cylinder]{bottom}\inheritanchor[from=cylinder]{after bottom}%
  \behindbackgroundpath{%
    \ifpgfcylinderusescustomfill%
      \getcylinderpoints%
      \pgf@x\xradius\relax%
      \advance\pgf@x-\outersep\relax%
      \edef\xradius{\the\pgf@x}%
      \pgf@y\yradius\relax%
      \advance\pgf@y-\outersep\relax%
      \edef\yradius{\the\pgf@y}%
      {%
        \pgftransformshift{\centerpoint}%
        \pgftransformrotate{\rotate}%
        \pgfpathmoveto{\afterbottom}%
        \pgfpatharc{90}{270}{\xradius and \yradius}%
        \pgfpathlineto{\beforetop\pgf@y-\pgf@y}%
        \pgfpatharc{270}{90}{\xradius and \yradius}%
        \pgfpathclose%
        \edef\pgf@temp{\pgfkeysvalueof{/pgf/Cylinder body fill}}%
        \ifx\pgf@temp\pgfutil@empty
          \edef\pgf@temp{\pgfkeysvalueof{/pgf/Cylinder body shade}}%
          \ifx\pgf@temp\pgfutil@empty
            \pgfusepath{discard}%
          \else % make shading:
            \begingroup
            \expandafter\tikzset\expandafter{\pgf@temp}
            \tikz@finish
          \fi
        \else
          \pgfsetfillcolor{\pgf@temp}%
          \pgfusepath{fill}%
        \fi
        %
        \pgfpathmoveto{\beforetop}%
        \pgfpatharc{90}{-270}{\xradius and \yradius}%
        \pgfpathclose
        \edef\pgf@temp{\pgfkeysvalueof{/pgf/Cylinder end fill}}%
        \ifx\pgf@temp\pgfutil@empty
          \edef\pgf@temp{\pgfkeysvalueof{/pgf/Cylinder end shade}}%
          \ifx\pgf@temp\pgfutil@empty
            \pgfusepath{discard}%
          \else % make shading: 
            \begingroup
            \expandafter\tikzset\expandafter{\pgf@temp}
            \tikz@finish
          \fi
        \else
          \pgfsetfillcolor{\pgf@temp}%
          \pgfusepath{fill}%
        \fi
      }%
    \fi
  }%
}
\makeatother
\begin{document}

\begin{tikzpicture}[font=\sffamily\small, opacity=1,
   mycylinder/.style={shape=Cylinder, aspect=1.5, minimum height=+3cm, draw,
     cylinder uses custom fill, Cylinder end fill=blue!10,
     Cylinder body shade={left color=blue!30, right color=blue!60, middle color=blue!10},
     minimum width=+2cm,  shape border rotate=90,
  }]
\node[mycylinder, label=below:Betterer?] {};
\end{tikzpicture}
\end{document}

Output

enter image description here