Fitting text to a shape in TikZ

Here is my third attempt to use automatically shapepar package with TikZ.

The \shapeparnode macro uses six parameters:

  1. (optional - default: empty) a style for the node (font size, color...)
  2. the horizontal margin (a distance)
  3. the vertical margin (a distance)
  4. the left boundary (a continuous vertical path)
  5. the right boundary (a continuous vertical path)
  6. the text (a single paragraph without \par or empty line)

The \shapeparnodeaccuracy can be locally redefined and gives the accuracy to compute the shape (default: 2 lines per em).

All problems are not resolved, but this method seems promising...

Three examples (the first two uses dotted orange curves to show the boundaries):

enter image description here

The preamble (with the commented code of the \shapeparnode macro):

\documentclass{standalone}
\usepackage{lmodern}
\usepackage[T1]{fontenc}
\usepackage{shapepar}
\usepackage{microtype}
\usepackage{lipsum}
\usepackage{tikz}
\usetikzlibrary{calc,fit,intersections}

\def\shapeparnodeaccuracy{2}
\newcommand\shapeparnode[6][]{
  % 6 parameters:
  % style for node (default:empty),
  % h margin, v margin, left path, right path, text (just one paragraph!)

  % name left and right paths and compute there bounding boxes
  \begin{scope}[local bounding box=leftbb]
    \path[name path global=left,xshift=#2] #4;
  \end{scope}
  \node[inner ysep=-#3,inner xsep=0pt,fit=(leftbb)](leftbb){};
  \begin{scope}[local bounding box=rightbb]
    \path[name path global=right,xshift=-#2] #5;
  \end{scope}
  \node[inner ysep=-#3,inner xsep=0pt,fit=(rightbb)](rightbb){};

  % global bounding box
  \path let
  \p1=(leftbb.north west), \p2=(leftbb.south west),
  \p3=(rightbb.north east), \p4=(rightbb.south east)
  in
  \pgfextra{
    \pgfmathsetmacro{\ymin}{(\y1 < \y3) ? \y1 : \y3}
    \pgfmathsetmacro{\ymax}{(\y2 > \y4) ? \y2 : \y4}
    \typeout{ymin \ymin}
    \typeout{ymax \ymax}
  } node[inner sep=0,fit={(\x1,\ymin pt)(\x3,\ymax pt)}](mybb){};

  % compute nb steps
  \path let \p1=(mybb.north), \p2=(mybb.south) in
  \pgfextra{
    \pgfmathsetmacro{\fnthght}{1em/\shapeparnodeaccuracy}
    \pgfmathtruncatemacro{\nbsteps}{(\y1-\y2)/\fnthght}
    \xdef\nbsteps{\nbsteps}
    \typeout{nb steps \nbsteps}
  };

  % horizontal references
  \path (mybb.north) -- (mybb.south)
  \foreach \cnt in {0,1,...,\nbsteps}{
    \pgfextra{\pgfmathsetmacro{\pos}{\cnt/\nbsteps}}
    coordinate[pos=\pos] (ref \cnt)
  };

  % left and right boundaries coordinates
  \foreach \cnt in {0,1,...,\nbsteps}{
    % an horizontal line from left to right
    \path[name path=ltor]
    (mybb.west |- ref \cnt) --  (mybb.east |- ref \cnt);
    % same line from right to left
    \path[name path=rtol]
    (mybb.east |- ref \cnt) -- (mybb.west |- ref \cnt);
    % left boundary
    \path[name intersections={of=rtol and left,by={l \cnt},sort by=rtol}];
    % right boundary
    \path[name intersections={of=ltor and right,by={r \cnt},sort by=ltor}];
  }
  % start point (and initial value of boundshape)
  \path let \p1=(l 0) in 
  \pgfextra{
    \pgfmathsetmacro{\xstart}{\x1}
    \xdef\boundshape{{0}{0}b{\xstart}}
    \xdef\xmin{\xstart}
    \xdef\xmax{\xstart}
  };

  % top and bottom
  \path let \p1=(l 0), \p2=(l \nbsteps) in
  \pgfextra{
    \pgfmathsetmacro{\ystart}{\y1}\xdef\ystart{\ystart}
    \pgfmathsetmacro{\yending}{\y2}\xdef\yending{\yending}
  };
  % incremental definition of boundshape
  \foreach \cnt in {0,1,...,\nbsteps}{
    \path let \p1=(l \cnt), \p2=(r \cnt) in
    \pgfextra{
      \pgfmathsetmacro{\start}{\x1}
      \pgfmathsetmacro{\len}{\x2-\x1}
      \pgfmathsetmacro{\ypos}{\cnt/\nbsteps*(\ystart - \yending)}
      {\let\\=\relax \xdef\boundshape{\boundshape\\{\ypos}t{\start}{\len}}}
      \pgfmathsetmacro{\xmin}{(\xmin < \start) ? \xmin : \start}
      \xdef\xmin{\xmin}
      \pgfmathsetmacro{\xmax}{(\xmax > \start + \len) ? \xmax : \start + \len}
      \xdef\xmax{\xmax}
    };
  }
  % draw the node with text in a shapepar
  \pgfmathsetmacro{\ymax}{\ystart - \yending}
  {\let\\=\relax \xdef\boundshape{\boundshape\\{\ymax}e{0}}}
  \node[#1,text width=\xmax pt - \xmin pt,align=flush left,
  anchor=north west,inner sep=0]
  at (mybb.north west -| \xmin pt,0)
  {\Shapepar[1pt]{\boundshape}#6\par};
}

And the document to build the three examples:

\def\mytext{Lorem ipsum dolor sit amet, consectetur adipiscing
  elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue
  ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis
  dolor. Praesent et diam eget libero egestas mattis sit amet vitae
  augue. Nam tincidunt congue enim, ut porta lorem lacinia
  consectetur. $x = y + z$ arcu vehicula ultricies a non tortor. Lorem
  ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida
  lorem. \textbf{Ut turpis felis}, pulvinar a semper sed, adipiscing id
  dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur
  dapibus enim sit amet elit pharetra tincidunt feugiat nisl
  imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed
  odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu
  rhoncus. \emph{\large Cum sociis natoque} penatibus et magnis dis
  parturient montes, nascetur ridiculus mus. In rutrum accumsan
  ultricies. Mauris vitae nisi at sem facilisis semper ac in
  est. Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut
  tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper
  ultricies.}

\begin{document}%
  \begin{tikzpicture}
    \begin{scope} % first example
      \def\pathone{(0,0) to[out=270,in=90] (2,-3) to[out=270,in=90] (-1,-7) to[out=270,in=90] (1,-10)}%
      \def\pathtwo{[xshift=8.2cm]\pathone}%
      \shapeparnode{1em}{.5em}{\pathone}{\pathtwo}{\mytext}%
      \draw[dotted,orange] \pathone;
      \draw[dotted,orange] \pathtwo;
    \end{scope}
    \begin{scope}[yshift=-11cm] % second example
      \small
      \def\pathone{(0,0) -- (-.5,-7) -- (2,-5.2) to[bend right] (3,-10)}%
      \def\pathtwo{(10,0) --  (7,-5)  to[bend right] (10,-10)}%
      \shapeparnode[text=blue,font=\small\itshape]
      {1em}{.5em}{\pathone}{\pathtwo}{\mytext}%
      \draw[dotted,orange] \pathone;
      \draw[dotted,orange] \pathtwo;
    \end{scope}
    \begin{scope}[yshift=-22cm] % third example
      \footnotesize
      \def\radius{3.1}
      \def\pathone{(3,0)
        arc(90:225:\radius)
        arc (45:-45:\radius/2.415)
        arc(135:270:\radius)}
      \def\pathtwo{(3,-4*\radius)
        arc(-90:0:\radius)
        arc(180:90:\radius)
        arc(270:180:\radius)
        arc(0:90:\radius)}
      \fill[top color=lime,bottom color=orange,middle color=yellow,draw=white]
      \pathone -- \pathtwo -- cycle;
      \shapeparnode[text=black,font=\footnotesize\scshape]
      {2em}{1em}{\pathone}{\pathtwo}{\mytext}%
      %\draw[orange] \pathone;
      %\draw[orange] \pathtwo;
    \end{scope}
\end{tikzpicture}
\end{document}

The last example is from the MWE:

\begin{tikzpicture}  
  \def\pathone{(0,0) to (2,-2) to (2,-4)}
  \def\pathtwo{(4,-4) to [out=up, in=down] (6.5,0)}
  \shapeparnode{1em}{.2em}{\pathone}{\pathtwo}{%
    This is the sort of effect I want to achieve, with text
    automatically constrained by some geometrical form. Here it's just a
    curve, but this could also be the text label of a node.}
  \draw \pathone -- \pathtwo -- cycle;
\end{tikzpicture}

enter image description here


As suggested by some comments, the only approach I know of offering a certain degree of flexibility is using the shapepar package. I made a stab at it:

\documentclass{standalone}

\usepackage{shapepar}
\usepackage{tikz}

\usetikzlibrary{shapes.geometric}

\newcommand\circlenodetext[3]{\node [draw, shape=circle, text width=0cm, inner sep=5mm] at (#1,#2) {\shapepar{\circleshape} #3\par};}
\newcommand\squarenodetext[3]{\node [draw, regular polygon, regular polygon sides=4, text width=0cm, inner sep=0mm] at (#1,#2) {\shapepar{\squareshape} #3\par};}
\newcommand\diamondnodetext[3]{\node [draw, shape=diamond, text width=0cm, inner sep=5mm] at (#1,#2) {\shapepar{\diamondshape} #3\par};}

\begin{document}
    \begin{tikzpicture}
        \circlenodetext{0}{0}{\texttt{lipsum} causes an arithmetic overflow error, so this is what I fill this space with instead}
        \squarenodetext{0}{5}{\texttt{lipsum} causes an arithmetic overflow error, so this is what I fill this space with instead}
        \diamondnodetext{0}{11}{an arith\-metic overflow error, so this is what I fill this space with instead -- so lean back and enjoy it!}
    \end{tikzpicture}
\end{document}

There are three commands, to define three nodes, each of different shape, taking as argument the coordinates where it should be placed and naturally, the text of the paragraph. I used the option text width=0cm to force the node to display random multiline text.

output PDF


With regular shapes this can be achieved rather nicely using MetaFun in ConTeXt MKIV. Details can be found in the MetaFun Manual in section »10.6 Libraries«. Arbitrary shapes are possible but harder to construct.

\useMPlibrary[txt]

\startuseMPgraphic{parshape}
  path p; p := fullcircle scaled 8cm;

  build_parshape(p,5pt,5pt,0,
    \baselinedistance,\strutheight,\strutdepth,\strutheight);

  draw p withpen pencircle scaled 1pt;
\stopuseMPgraphic

\defineoverlay
  [parshape]
  [\useMPgraphic{parshape}]

\starttext

\startshapetext[parshape]
  \input knuth
\stopshapetext

\startframed
  [
    frame=off,
    offset=overlay,
    background=parshape,
  ]
  \getshapetext
\stopframed

\stoptext

enter image description here

Tags:

Tikz Pgf