[Tex/LaTex] How to make a superellipse node shape in tikz

metaposttikz-pgf

A superellipse is a kind of closed curve which can be used as a "intermediate" shape between ellipse and rectangle. A parameter can control its "roundness". I find it a pleasant alternative to the typical "rounded corners" rectangle.

A convenient approximation of the curve can be achieved using bezier patches. In "Metafont book", Knuth defines the following approximation (p. 267), indistinguishable of a real superellipse for practical purposes:

The five parameters to ‘superellipse’ are the right, the top, the left, the bottom,
and the superness.

def superellipse(expr r,t,l,b,s)=
  r{up}...(s[xpart t,xpart r],s[ypart r,ypart t]){t-r}...
  t{left}...(s[xpart t,xpart l],s[ypart l,ypart t]){l-t}...
  l{down}...(s[xpart b,xpart l],s[ypart l,ypart b]){b-l}...
  b{right}...(s[xpart b,xpart r],s[ypart r,ypart b]){r-b}...cycle enddef;

The following metapost code provides a more convenient way to specify the superellipse by the four corners of the rectangle which contains it:

def my_superellipse(expr sw, nw, ne, se, factor) = 
   superellipse(1/2[se,ne], 1/2[nw,ne], 1/2[nw,sw], 1/2[sw,se], factor)
enddef;

An example of use (metapost code):

beginfig(1)
  z1=(0,0);   % z1, z2, z3 and z4 are four corners of a rectangle
  z3=(40,20);
  x2=x1; x4=x3; y2=y3; y4=y1;

  % Draw a red circle at those points
  for i = 1 upto 4:
    draw z[i] withpen pencircle scaled 2 withcolor red;
  endfor

  % Draw the rectangle through those points
  draw z1 -- z2 -- z3 -- z4 -- cycle withpen pencircle scaled .1;

  % Draw the superellipse, with 0.9 "superness" (the closer to 1, the more squarish)
  draw my_superellipse(z1,z2,z3,z4, .9);
  % Some text inside
  label(btex \LaTeX etex, 1/2[z1,z3]);
endfig;

This is the result:

Result

I thought that this would be a nice addition to tikz node shapes, but I don't know how to translate the metapost syntax to the required control points of the superellipse curve, and I don't have experience in defining new node shapes for tikz.

Also note that the resulting node has obvious north, south, east and west anchors, but it would be trickier to find the coordinates of north west, etc. not to mention the more general anchors node.angle.

Update

Some clarifications about the question:

  1. The formula of the superellipse is well known (see Wikipedia article linked from the first line of the question), however I would prefer do not plot that formula, since it would require to decide the sampling frequency, and do all the computations. I'm interested instead in drawing an approximation using tikz syntax such as .. or to, or pgf primitives such as \pgfpathcurveto. This would require to compute only the control points.

  2. I'm aware of the hobby package by Andrew Stacey (I was indeed the author of a python script to convert a subset of the metapost syntax to tikz, which started it all). However I think this solution is a bit overkill, because it contains a general parser for the metapost syntax, while the superellipse curve has a strong symmetry which could simplify a lot the calculations (although I don't see still how).

  3. The ultimate goal is to define a new node shape. This require the use of low-level pgf primitives and I don't know how hobby package relates to that level of pgf/tikz.

Comparison of proposed solutions

In order to compare the proposed solutions with the reference superellipse drawn by
metapost code above, I have obtained the explicit control points of the curve.

The following code defines a macro \metapostsuperellipse containing this path in tikz syntax, and defines a rectangular node of appropiate dimensions (40×20 pt), which can be decorated with the proposed solutions.

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

% These numbers were computed by metapost, and obtained using tracingchoices option    
\newcommand{\metapostsuperellipse}{
    (40,10) .. controls (40,13.43742) and (39.99951,18.00012)
 .. (37.99988,18.99994) .. controls (36.00024,19.99976) and (26.11214,20)
 .. (20,20)..controls (13.88786,20) and (3.99976,19.99976)
 .. (2.00012,18.99994) .. controls (0.00049,18.00012) and (0,13.43742)
 .. (0,10)..controls (0,6.56258) and (0.00049,1.99988)
 .. (2.00012,1.00006) .. controls (3.99976,0.00024) and (13.88786,0)
 .. (20,0)..controls (26.11214,0) and (36.00024,0.00024)
 .. (37.99988,1.00006) .. controls (39.99951,1.99988) and (40,6.56258)
 .. (40,10);
}

% Solutions proposed by Qrrbrbirlbel
\newcommand*{\superellipse}[3][draw]{% #1 = styles
                                     % #2 = node
                                     % #3 = superness
    \pgfmathsetmacro\looseness{#3}
    \path[#1]
            (#2.east) .. controls  ($(#2.east)!\looseness!(#2.north east)$) and ($(#2.north)!\looseness!(#2.north east)$).. (#2.north)
                      .. controls ($(#2.north)!\looseness!(#2.north west)$) and  ($(#2.west)!\looseness!(#2.north west)$).. (#2.west)
                      .. controls  ($(#2.west)!\looseness!(#2.south west)$) and ($(#2.south)!\looseness!(#2.south west)$).. (#2.south)
                      .. controls ($(#2.south)!\looseness!(#2.south east)$) and  ($(#2.east)!\looseness!(#2.south east)$).. (#2.east)
                      ;
}

\newcommand*{\superellipseA}[3][draw]{% #1 = styles
                                      % #2 = node
                                      % #3 = superness
    \pgfmathsetmacro\looseness{2*#3}
    \path[curve to, looseness=\looseness, #1]
        let \p1 = (#2.east),
            \p2 = (#2.north),
            \n1 = {\x1-\x2}, % distance in x direction
            \n2 = {\y2-\y1}  % distance in y direction
            in
                (#2.east)  to[out=90,  in=0,   out max distance=\n2, in max distance=\n1]
                (#2.north) to[out=180, in=90,  out max distance=\n1, in max distance=\n2]
                (#2.west)  to[out=270, in=180, out max distance=\n2, in max distance=\n1]
                (#2.south) to[out=0,   in=270, out max distance=\n1, in max distance=\n2] (#2.east);
}
\newcommand*{\superellipseB}[3][draw]{% #1 = styles
                                      % #2 = node
                                      % #3 = superness
    \pgfmathsetmacro\looseness{2*#3}
    \path[curve to, looseness=\looseness, #1]
        let \p1 = (#2.east),
            \p2 = (#2.north),
            \n1 = {min({\x1-\x2},{\y2-\y1})} % minimum of distances
            in 
                (#2.east)  to[out=90,  in=0,   max distance=\n1]
                (#2.north) to[out=180, in=90,  max distance=\n1]
                (#2.west)  to[out=270, in=180, max distance=\n1]
                (#2.south) to[out=0,   in=270, max distance=\n1] (#2.east);
}

\tikzset{node box/.style={rectangle, minimum height=20pt, minimum width=40pt, draw, ultra thin, anchor=south west, green}
}


% Pictures to compare proposed solutions with reference (in red)
\begin{tikzpicture}[x=1pt, y=1pt]
\node[node box] (sample) {};
\draw[red, opacity=.5] \metapostsuperellipse;
\superellipse[draw, opacity=0.6, very thin]{sample}{.9}
\end{tikzpicture}

\begin{tikzpicture}[x=1pt, y=1pt]
\node[node box] (sample) {};
\draw[red, opacity=.5] \metapostsuperellipse;
\superellipseA[draw, opacity=0.6, very thin]{sample}{.9}
\end{tikzpicture}

\begin{tikzpicture}[x=1pt, y=1pt]
\node[node box] (sample) {};
\draw[red, opacity=.5] \metapostsuperellipse;
\superellipseB[draw, opacity=0.6, very thin]{sample}{.9}
\end{tikzpicture}
\end{document}

Which gives:

Comparison

Best Answer

Here's a node shape that uses the parametric representation of the superellipse. All standard anchors and the border anchors are defined. The rectangularness is controlled using the key superellipse parameter. A value of 1 is a diamond.

\tikz{
    \foreach \parameter in {0.4,0.6,0.8,1,2,...,10}
         \node [minimum width=4cm, minimum height=2cm, draw, red,text=black, superellipse, superellipse parameter=\parameter] (a) {};


\begin{tikzpicture}
\node [minimum width=4cm, minimum height=2cm, draw, superellipse, superellipse parameter=4] (a) {};
\foreach \angle in {5,10,...,360} 
 \draw [orange] (a.\angle) -- (\angle:5cm);
\end{tikzpicture}

\documentclass[border=5mm]{standalone}
\usepackage{tikz}
\usetikzlibrary{shapes.geometric, intersections}


\makeatletter
% fixed exp function.
%
\makeatletter
\let\pgfmath@function@exp\relax % undefine old exp function
\pgfmathdeclarefunction{exp}{1}{%   
  \begingroup
    \pgfmath@xc=#1pt\relax
    \pgfmath@yc=#1pt\relax
    \ifdim\pgfmath@xc<-9pt
      \pgfmath@x=1sp\relax
    \else
      \ifdim\pgfmath@xc<0pt
        \pgfmath@xc=-\pgfmath@xc
      \fi
      \pgfmath@x=1pt\relax
      \pgfmath@xa=1pt\relax
      \pgfmath@xb=\pgfmath@x
      \pgfmathloop%
        \divide\pgfmath@xa by\pgfmathcounter
        \pgfmath@xa=\pgfmath@tonumber\pgfmath@xc\pgfmath@xa%
        \advance\pgfmath@x by\pgfmath@xa
      \ifdim\pgfmath@x=\pgfmath@xb
      \else
        \pgfmath@xb=\pgfmath@x
      \repeatpgfmathloop%
      \ifdim\pgfmath@yc<0pt
        \pgfmathreciprocal@{\pgfmath@tonumber\pgfmath@x}%
        \pgfmath@x=\pgfmathresult pt\relax
      \fi
    \fi
    \pgfmath@returnone\pgfmath@x%
  \endgroup
}

\let\pgfmath@function@pow\relax % undefine old exp function
\pgfmathdeclarefunction{pow}{2}{%
  \begingroup%
    \pgfmath@xa=#1pt%
    \pgfmath@xb=#2pt%
    \ifdim\pgfmath@xa=0pt
        \pgfmath@x=0pt\relax
    \else
    \afterassignment\pgfmath@x%
        \expandafter\c@pgfmath@counta\the\pgfmath@xb\relax%
        \ifnum\c@pgfmath@counta<0\relax%
            \c@pgfmath@counta=-\c@pgfmath@counta%
            \pgfmathreciprocal@{#1}%
            \pgfmath@xa=\pgfmathresult pt\relax%
        \fi
        \ifdim\pgfmath@x=0pt\relax%
            \pgfmath@x=1pt\relax%
            \pgfmathloop%
                \ifnum\c@pgfmath@counta>0\relax%
                    \ifodd\c@pgfmath@counta%
 \pgfmath@x=\pgfmath@tonumber{\pgfmath@x}\pgfmath@xa%
                    \fi
                    \ifnum\c@pgfmath@counta>1\relax%
 \pgfmath@xa=\pgfmath@tonumber{\pgfmath@xa}\pgfmath@xa%
                    \fi%
                    \divide\c@pgfmath@counta by2\relax%
            \repeatpgfmathloop%
        \else%
            \pgfmathln@{#1}%
            \pgfmath@x=\pgfmathresult pt\relax%
            \pgfmath@x=\pgfmath@tonumber{\pgfmath@xb}\pgfmath@x%
            \pgfmathexp@{\pgfmath@tonumber{\pgfmath@x}}%
            \pgfmath@x=\pgfmathresult pt\relax%
        \fi%
    \fi
    \pgfmath@returnone\pgf@x%
    \endgroup%
}

\pgfkeys{
    /pgf/superellipse parameter/.store in=\pgf@superellipse@param,
    /pgf/superellipse parameter/.default=2,
    /pgf/superellipse parameter
}

\newcommand{\pointonsuperellipse}[3]{ % cornerpoint, parameter, directionpoint
    \pgf@process{#1}
    \edef\size@x{\the\pgf@x}%
    \edef\size@y{\the\pgf@y}%
    \pgfintersectionofpaths
        {
            \pgfpathmoveto{\centerpoint}
            \pgfpathlineto{
                \pgfpointborderrectangle{#3}{#1}
            }
            \pgfpathclose
        }
        {
            \pgfplothandlercurveto
            \pgfplotfunction{\x}{-180,-170,...,170}{
                \pgfpoint{
                    abs(1 * cos(\x))^(2/#2)*( (cos(\x)>0)*2-1 ) * \size@x
                }{
                    abs(1 * sin(\x))^(2/#2)*( (sin(\x)>0)*2-1 ) * \size@y
                }
        }
        \pgfpathclose
    }
    \pgfpointintersectionsolution{1}
}

\makeatletter
\pgfdeclareshape{superellipse}
%
% Draws a circle around the text
%
{
\savedmacro\superellipseparameter{\edef\superellipseparameter{\pgf@superellipse@param}}
  \savedanchor\centerpoint{%
    \pgf@x=.5\wd\pgfnodeparttextbox%
    \pgf@y=.5\ht\pgfnodeparttextbox%
    \advance\pgf@y by-.5\dp\pgfnodeparttextbox%
  }
  \savedanchor\radius{%
    %
    % Caculate ``height radius''
    %
    \pgf@y=.5\ht\pgfnodeparttextbox%
    \advance\pgf@y by.5\dp\pgfnodeparttextbox%
    \pgfmathsetlength\pgf@yb{\pgfkeysvalueof{/pgf/inner ysep}}%
    \advance\pgf@y by\pgf@yb%
    %
    % Caculate ``width radius''
    %
    \pgf@x=.5\wd\pgfnodeparttextbox%
    \pgfmathsetlength\pgf@xb{\pgfkeysvalueof{/pgf/inner xsep}}%
    \advance\pgf@x by\pgf@xb%
    %
    % Adjust
    %
    \pgf@x=1.4142136\pgf@x%
    \pgf@y=1.4142136\pgf@y%
    %
    % Adjust hieght, if necessary
    %
    \pgfmathsetlength\pgf@yc{\pgfkeysvalueof{/pgf/minimum height}}%
    \ifdim\pgf@y<.5\pgf@yc%
      \pgf@y=.5\pgf@yc%
    \fi%
    %
    % Adjust width, if necessary
    %
    \pgfmathsetlength\pgf@xc{\pgfkeysvalueof{/pgf/minimum width}}%
    \ifdim\pgf@x<.5\pgf@xc%
      \pgf@x=.5\pgf@xc%
    \fi%
    %
    % Add outer sep
    %
    \pgfmathsetlength{\pgf@xb}{\pgfkeysvalueof{/pgf/outer xsep}}%
    \pgfmathsetlength{\pgf@yb}{\pgfkeysvalueof{/pgf/outer ysep}}%
    \advance\pgf@x by\pgf@xb%
    \advance\pgf@y by\pgf@yb%
  }
  \savedmacro\test{\def\test{2}}

  %
  % Anchors
  %
  \anchor{center}{\centerpoint}
  \anchor{mid}{\centerpoint\pgfmathsetlength\pgf@y{.5ex}}
  \anchor{base}{\centerpoint\pgf@y=0pt}
  \anchor{north}
  {
    \pgf@process{\radius}
    \pgf@ya=\pgf@y%
    \pgf@process{\centerpoint}
    \advance\pgf@y by\pgf@ya
  }
  \anchor{south}
  {
    \pgf@process{\radius}
    \pgf@ya=\pgf@y%
    \pgf@process{\centerpoint}
    \advance\pgf@y by-\pgf@ya
  }
  \anchor{west}
  {
    \pgf@process{\radius}
    \pgf@xa=\pgf@x%
    \pgf@process{\centerpoint}
    \advance\pgf@x by-\pgf@xa
  }
  \anchor{mid west}
  {%
    \pgf@process{\radius}
    \pgf@xa=\pgf@x%
    \pgf@process{\centerpoint}
    \advance\pgf@x by-\pgf@xa%
    \pgfmathsetlength\pgf@y{.5ex}
  }
  \anchor{base west}
  {%
    \pgf@process{\radius}
    \pgf@xa=\pgf@x%
    \pgf@process{\centerpoint}
    \advance\pgf@x by-\pgf@xa%
    \pgf@y=0pt
  }
  \anchor{north west}
  {
    \pgf@process{\radius}
    \def\angle{135}
    \pgf@xa=\pgf@x%
    \pgf@ya=\pgf@y%  
    \pgf@process{\pgfpoint{
        abs(cos(\angle))^(2/\superellipseparameter)*( (cos(\angle)>0)*2-1 ) * \pgf@xa
     }{
        abs(sin(\angle))^(2/\superellipseparameter)*( (sin(\angle)>0)*2-1 ) * \pgf@ya
    }}
    \pgf@xb=\pgf@x%
    \pgf@yb=\pgf@y%  
     \pgf@process{\centerpoint}
    \advance\pgf@x by \pgf@xb
    \advance\pgf@y by \pgf@yb
  }
  \anchor{south west}
  {
    \pgf@process{\radius}
    \def\angle{-135}
    \pgf@xa=\pgf@x%
    \pgf@ya=\pgf@y%  
    \pgf@process{\pgfpoint{
        abs(cos(\angle))^(2/\superellipseparameter)*( (cos(\angle)>0)*2-1 ) * \pgf@xa
     }{
        abs(sin(\angle))^(2/\superellipseparameter)*( (sin(\angle)>0)*2-1 ) * \pgf@ya
    }}
    \pgf@xb=\pgf@x%
    \pgf@yb=\pgf@y%  
     \pgf@process{\centerpoint}
    \advance\pgf@x by \pgf@xb
    \advance\pgf@y by \pgf@yb
  }
  \anchor{east}
  {%
    \pgf@process{\radius}
    \pgf@xa=\pgf@x%
    \pgf@process{\centerpoint}
    \advance\pgf@x by\pgf@xa
  }
  \anchor{mid east}
  {%
    \pgf@process{\radius}
    \pgf@xa=\pgf@x%
    \pgf@process{\centerpoint}
    \advance\pgf@x by\pgf@xa%
    \pgfmathsetlength\pgf@y{.5ex}
  }
  \anchor{base east}
  {%
    \pgf@process{\radius}
    \pgf@xa=\pgf@x%
    \pgf@process{\centerpoint}
    \advance\pgf@x by\pgf@xa%
    \pgf@y=0pt
  }
  \anchor{north east}
  {
    \pgf@process{\radius}
    \def\angle{45}
    \pgf@xa=\pgf@x%
    \pgf@ya=\pgf@y%  
    \pgf@process{\pgfpoint{
        abs(cos(\angle))^(2/\superellipseparameter)*( (cos(\angle)>0)*2-1 ) * \pgf@xa
     }{
        abs(sin(\angle))^(2/\superellipseparameter)*( (sin(\angle)>0)*2-1 ) * \pgf@ya
    }}
    \pgf@xb=\pgf@x%
    \pgf@yb=\pgf@y%  
     \pgf@process{\centerpoint}
    \advance\pgf@x by \pgf@xb
    \advance\pgf@y by \pgf@yb
  }
  \anchor{south east}
  {
    \pgf@process{\radius}
    \def\angle{-45}
    \pgf@xa=\pgf@x%
    \pgf@ya=\pgf@y%  
    \pgf@process{\pgfpoint{
        abs(cos(\angle))^(2/\superellipseparameter)*( (cos(\angle)>0)*2-1 ) * \pgf@xa
     }{
        abs(sin(\angle))^(2/\superellipseparameter)*( (sin(\angle)>0)*2-1 ) * \pgf@ya
    }}
    \pgf@xb=\pgf@x%
    \pgf@yb=\pgf@y%  
     \pgf@process{\centerpoint}
    \advance\pgf@x by \pgf@xb
    \advance\pgf@y by \pgf@yb
  }
  \anchorborder{
    \edef\externalx{\the\pgf@x}%
    \edef\externaly{\the\pgf@y}%
    \pgf@process{\radius}%
    \pgf@xa=\pgf@x%
    \pgf@ya=\pgf@y%
    \pointonsuperellipse{\pgfpoint{\pgf@xa}{\pgf@ya}}{\superellipseparameter}{\pgfpoint{\externalx}{\externaly}}
    \pgf@xa=\pgf@x%
    \pgf@ya=\pgf@y%
    \centerpoint%
    \advance\pgf@x by\pgf@xa%
    \advance\pgf@y by\pgf@ya%
  }


  \backgroundpath
  {
    \pgf@process{\radius}%
    \pgfutil@tempdima=\pgf@x%
    \pgfutil@tempdimb=\pgf@y%
    \pgfmathsetlength{\pgf@xb}{\pgfkeysvalueof{/pgf/outer xsep}}%
    \pgfmathsetlength{\pgf@yb}{\pgfkeysvalueof{/pgf/outer ysep}}%
    \advance\pgfutil@tempdima by-\pgf@xb%
    \advance\pgfutil@tempdimb by-\pgf@yb%
    {
        \pgftransformshift{\centerpoint}
        \pgfplothandlercurveto
        \pgfplotfunction{\x}{-180,-170,...,170}{
            \pgfpoint{
                abs(1 * cos(\x))^(2/\pgf@superellipse@param)*( (cos(\x)>0)*2-1 ) * \pgfutil@tempdima
            }{
                abs(1 * sin(\x))^(2/\pgf@superellipse@param)*( (sin(\x)>0)*2-1 ) * \pgfutil@tempdimb
            }
        }
        \pgfpathclose
        \pgfgetpath\test
        \pgfusepath{stroke}
    }
  }
}
\def\n{3}
\begin{document}
\begin{tikzpicture}
\node [minimum width=4cm, minimum height=2cm, draw, superellipse, superellipse parameter=4] (a) {};
\foreach \angle in {5,10,...,360} 
 \draw [orange] (a.\angle) -- (\angle:5cm);
\end{tikzpicture}
\end{document} 
Related Question