33

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 (40x20 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

7
  • I think you need to rephrase this as a question. E.g. How do I make a superellipse node shape in tikz? Dec 20, 2012 at 11:45
  • What does r{up} and (<expr1>,<expr2>){t-r} means? I’m guessing: From point r, direction up and from point (<expr1>,<expr2>) to the point t-r ( = (r1 - t1,r2 - r1), p1 being the x-coordinate and p2 being the y-coordinate of point p. Dec 20, 2012 at 11:49
  • @AndrewSwann Yes, I was aware that it didn't look much as a question, but didn't find the appropiate words. Thank you for the suggestion!
    – JLDiaz
    Dec 20, 2012 at 11:49
  • And 1/2[<point1>,<point2>] obviously (?) is the point between <point1> and <point2> but what is <factor>[<value1>,<value2>]? Dec 20, 2012 at 11:52
  • @Qrrbrbirlbel Parenthesis delimite coordinate points. Braces delimit direction vectors. up is a predefined vector equivalent to {0,1}. r, l, t, d are variables containing coordinates of points (right, left, top and down). Also, expression a[b,c] is equivalent to tikz's ($(b)!a!(c)$) when b and c are points, or to b + a*(c-b) when they are numbers.
    – JLDiaz
    Dec 20, 2012 at 11:52

3 Answers 3

22

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} 
3
  • Can you explain your fixes to the exp and the pow functions? I also have tried plotting the path with that formula, and failed due to mathematical problems. Dec 20, 2012 at 19:34
  • @Qrrbrbirlbel: The fix for the exp function is the one from tex.stackexchange.com/a/31791/2552, it's a known bug that's been fixed in the CVS version. The pow function had an error when 0 is raised to a non-integer power, complaining that it can't take the logarithm of 0, while the correct result would just be 0.
    – Jake
    Dec 20, 2012 at 19:38
  • Great! Your code addresses all points in my question, so I'm accepting it. However, I would prefer if the "parameter" behaved like Knuth's "superness". I tried for high values of superellipse parameter (30, 40) and the corners are still rounded, and some small artifacts begin to appear. Also, I guess the implementation is computationally intensive, so an approximation based on bezier patches would be still welcome.
    – JLDiaz
    Dec 20, 2012 at 23:32
8

This is not an answer I had when I asked the question. I came to it after seeing Qrrbrbirlbel attempts.

I noticed that the proposed solutions used only four points to draw the curve, while the original metapost path traversed eight points, which were the east, north, west and south anchors of the rectangle, plus four additional intermediate points computed from those anchors and the given "superness". These extra points are close to the corners

So I tried using tikz to simply plot the smooth cycle curve which passes through those eight points.

\newcommand*{\superellipse}[3][draw]{% #1 = styles
                                     % #2 = node
                                     % #3 = superness
    \coordinate (ne) at ($(#2.center)!#3!(#2.north east)$);
    \coordinate (nw) at ($(#2.center)!#3!(#2.north west)$);
    \coordinate (sw) at ($(#2.center)!#3!(#2.south west)$);
    \coordinate (se) at ($(#2.center)!#3!(#2.south east)$);
    \path[#1]
       plot[smooth cycle,tension=0.4] coordinates{ 
         (#2.east) (ne) (#2.north) (nw) (#2.west) (sw) (#2.south) (se)};
}

To my surprise, the matching is almost perfect without needing to compute any control point nor entry or exit angles!. I only had to change the default tension to 0.4.

Result

However, this solution is not still satisfactory for different values of "superness", since the tension should vary depending on the superness in a relation which is still unknown. For example, as the superness approaches to 1, the tension should approach to 0.

Also, still remains to be done to make this idea into a tikz shape. I'm not sure if plot keyword can be used at that level.

Update

I came with a completely ad-hoc equation to get the required tension for the curve, for any given "superness" between 0.5 and 1.0. When superness is 1, the result should be a rectangle, which requires tension=0. When superness is 0.5, the result should be a diamond, which also requires tension=0. For any superness between 0.5 and 1, the curve should be a more or less rounded superellipse.

Since the tension should be zero at superness=0.5 and superness=1.0, but a positive value in between, I imagined that a parabolic segment could give appropiate values. Tried with a parabola which gives a value close to 0.4 for a superness of 0.9, since this value was found to produce good results. The resulting equation is:

tension = -9.6 * superness^2 + 14.4 * superness - 4.8

This equation gives tension 0 at superness 0.5 and 1.0, as required, and it has its maximum (0.6) at superness = 0.75. This ad-hoc equation gives apparently good results for any superness between 0.5 and 1.0, as the following code shows:

\newcommand*{\superellipse}[3][draw]{% #1 = styles
                                     % #2 = node
                                     % #3 = superness
    \coordinate (ne) at ($(#2.center)!#3!(#2.north east)$);
    \coordinate (nw) at ($(#2.center)!#3!(#2.north west)$);
    \coordinate (sw) at ($(#2.center)!#3!(#2.south west)$);
    \coordinate (se) at ($(#2.center)!#3!(#2.south east)$);
    \pgfmathparse{-9.6*#3*#3 + 14.4*#3 - 4.8}   % <--------- Magic numbers!
    \xdef\tension{\pgfmathresult}
    \path[#1]
       plot[smooth cycle, tension=\tension] coordinates{ 
         (#2.east) (ne) (#2.north) (nw) (#2.west) (sw) (#2.south) (se)};
%    \node {\tension};
}

\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) {};
\foreach \superness in {0.5,0.55, ..., 1.01} {
    \superellipse[draw, opacity=0.6, very thin]{sample}{\superness}
}
\end{tikzpicture}

Different superness

2
  • Ah, very interesting. I think I misinterpreted some points of the MetaPost code, though. Do you need help to transform this in a shape? Dec 20, 2012 at 19:18
  • @Qrrbrbirlbel It would be useful to have a shape version, in order to compare it in terms of performance with Jake's solution. Note that using my approach, you get some anchors "for free" (nw, ne, sw, se). The .<angle> ones should be still computed.
    – JLDiaz
    Dec 20, 2012 at 23:34
6

Admittedly,

  1. I know neither MetaPost, nor its ... operator,
  2. nor TikZ’ .. operator;
  3. neither is this a shape (yet),
  4. nor does a superness of 1 result in a rectangle (actually, a superness of 1 produces an output that resembles more your image than what the .9 produces).

Nor does any .anchor work.
Though, any .<angle> can be faked by intersecting the superellipse's path with the path (<node>.center) -- (<angle>:∞) ( can be substituted by the maximum “radius” of the rectangle (max(<width>,<height>)/2).

Code

\documentclass[tikz,border=2pt]{standalone}
\usetikzlibrary{calc}
\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);
}
\begin{document}
\begin{tikzpicture}
\node[ultra thin,draw,text=black!10] (n) {\LaTeX};
\superellipse[draw,red,opacity=.8]   {n}{.9}
\superellipseA[draw,green,opacity=.6]{n}{.9}
\superellipseB[draw,blue,opacity=.4] {n}{1}
\end{tikzpicture}
\end{document}

Output

enter image description here

3
  • The ... operator means ..tension.atleast1..; how can this be implemented in TikZ I don't know.
    – egreg
    Dec 20, 2012 at 12:11
  • @egreg Hm, maybe the curve to path with its looseness settings and the distances does come closer to MetaPost, as setting the distances prevents the draws to exceed the node’s rectangular box (even with very high looseness factors), though it never becomes a rectangle. Can ..tension.atleast1.. be expressed in a more mathematically way? As far as TikZ is concerned <p1> .. controls <p2> and <p3> .. <p4> is a “cubic Bezier curve”. Dec 20, 2012 at 12:45
  • Thank you @Qrrbrbirlbel, it is a good first approach. I would like a more faithful implementation of the metapost version. Also, concerning anchors, I thought that tikz can only compute intersections between srtaight lines and circles, or between circles.
    – JLDiaz
    Dec 20, 2012 at 16:09

You must log in to answer this question.

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