5

This is a continuation of STeX Exchange 552946.

I've been trying to get some 3D effects on tikz-cd, and got an amazing answer by @ZhiyuanLck. The only remaining problem is getting text/arrows to be drawn along curved surfaces (or look that way). Is it possible to achieve such an effect?

For instance, how would we do that for the \Rightarrows and their labels \theta_f and \theta_g in the diagram below?

enter image description here

(This diagram is the ice cream cone condition for lax slice bicategories, as in Section 7.1 of Johnson–Yau's new book on bicategories.)

Compilable code for this diagram:

\documentclass[english,11pt]{standalone}
\RequirePackage{luatex85}
\usepackage{tikz}
\usepackage{tikz-cd}
\usetikzlibrary{3d}
\makeatletter
\tikzset{
  plane/.code args={#1and#2}{
    \tikz@scan@one@point\pgf@process#1
    \edef\temp@a{(\the\pgf@x, \the\pgf@y)};
    \tikz@scan@one@point\pgf@process#2
    \edef\temp@b{(\the\pgf@x, \the\pgf@y)};
    \pgfkeysalso{
      plane x={\temp@a},
      plane y={\temp@b},
      canvas is plane,
    }
  },
}
\makeatother
\usepackage{libertine}
\usepackage{mathtools}
\usepackage[libertine]{newtxmath}
\tikzcdset{
    arrow style=tikz,
    %diagrams={>={Straight Barb[scale=1.5]}}
    diagrams={>={Stealth[round,length=4pt,width=4.95pt,inset=2.75pt]}}
}
\begin{document}
\newsavebox{\BoxNodeOne}
\savebox{\BoxNodeOne}{
    \begin{tikzcd}[row sep={4.5em,between origins}, column sep={4.5em,between origins}, ampersand replacement=\&]
        {}
        \arrow[r, "F(A)"{plane={(1,0) and (0,0.7)}},phantom]
        \&
        {}
    \end{tikzcd}
} 
\newsavebox{\BoxNodeTwo}
\savebox{\BoxNodeTwo}{
    \begin{tikzcd}[row sep={4.5em,between origins}, column sep={4.5em,between origins}, ampersand replacement=\&]
        {}
        \arrow[r, "F(B)"{plane={(1,0) and (0,0.7)}},phantom]
        \&
        {}
    \end{tikzcd}
} 
\newsavebox{\BoxOne}
\savebox{\BoxOne}{
    \begin{tikzcd}[row sep={4.5em,between origins}, column sep={4.5em,between origins}, ampersand replacement=\&]
        {}
        \\
        \arrow[u, Rightarrow]
        {}
    \end{tikzcd}
} 
\newsavebox{\BoxTwo}
\savebox{\BoxTwo}{
    \begin{tikzcd}[row sep={3.6em,between origins}, column sep={3.6em,between origins}, ampersand replacement=\&]
        {}
        \&
        {}
        \\
        {}
        \arrow[ru, Rightarrow, bend right=35]
        \&
        {}
    \end{tikzcd}
} 
\begin{tikzcd}[row sep={14.4em,between origins}, column sep={6.3em,between origins}, ampersand replacement=\&]
    {\hspace{+1.25em}\usebox{\BoxNodeOne}}
    \arrow[rr, "F(g)"{name=3,description,plane={(1,0) and (0,0.7)}},bend left=30]
    \arrow[rr, "F(f)"{name=2,description,plane={(1,0) and (0,0.7)}},bend right=30]
    \arrow[rd, "\phi_{A}"'{name=1},start anchor={[xshift=+0.5em]}]
    \&
    \&
    {\hspace{-1.25em}\usebox{\BoxNodeTwo}}
    \arrow[ld, "\phi_{B}",start anchor={[xshift=-0.5em]}]
    \\
    {}
    \&
    X
    \&
    {}
    % 2-Arrows
    \arrow[from=1,to=1-3,"\theta_{f}"{description,yshift=-0.2em},shorten=2.5em,Rightarrow,xshift=-1.0em,yshift=-1.0em,bend right=15]
    \arrow[from=2,to=3,"\usebox{\BoxOne}"{plane={(1,0) and (0,0.6)}},shorten=0.5em,phantom]
    \arrow[from=2,to=3,"\scalebox{0.75}{$F(\alpha)$}"{description,plane={(1,0) and (0,0.6)}},shorten=0.5em,phantom]
\end{tikzcd}
=
\begin{tikzcd}[row sep={14.4em,between origins}, column sep={6.3em,between origins}, ampersand replacement=\&]
    {\hspace{+1.25em}\usebox{\BoxNodeOne}}
    \arrow[rr, "F(g)"{name=3,description,plane={(1,0) and (0,0.7)}},bend left=30]
    \arrow[rd, "\phi_{A}"'{name=1},start anchor={[xshift=+0.5em]}]
    \&
    \&
    {\hspace{-1.25em}\usebox{\BoxNodeTwo}}
    \arrow[ld, "\phi_{B}",start anchor={[xshift=-0.5em]}]
    \\
    \&
    X
    \&
    % 2-Arrows
    \arrow[from=1,to=1-3,"\theta_{g}"{description,yshift=+0.15em},shorten=1.5em,Rightarrow,bend left=15,xshift=+0.25em,yshift=0.5em]
\end{tikzcd}
\end{document}
2
  • I don't understand your question. For which part in your compilable example, you want to improve its output or simplify its corresponding input? Jul 17, 2020 at 2:05
  • @muzimuzhiZ I want a way to make arrows and their labels in tikz-cd look like they were drawn along a curved surface, such as the surface of the cone above, e.g. like the text in this cone.
    – Théo
    Jul 17, 2020 at 2:17

1 Answer 1

12
+50

The main idea is to combine text along path of library decorations.text with canvas is plane of library 3d. In my last answer, I have shown how to place node on specified plane by set x unit vector and y unit vector. Decoration text along path put every character in a \qboxsynced, which means before inserting the text, the current coordinate transformation matrix is applied to the current canvas transformation matrix.

I define a new decoration 3d text along path. Its main code is from decoration text along path, and I add some code that is from \tikz@canvas@is@plane in tikzlibrary3d.code.tex

\def\tikz@canvas@is@plane{
  \pgf@process{\tikz@plane@x}%
  \pgf@xa=\pgf@x%
  \pgf@ya=\pgf@y%
  \pgf@process{\tikz@plane@y}%
  \pgf@xb=\pgf@x%
  \pgf@yb=\pgf@y%
  \pgf@process{\tikz@plane@origin}%
  \edef\pgf@marshal{\noexpand\tikz@addtransform{%
      \noexpand\pgftransformtriangle
      {\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}
      {\noexpand\pgfqpoint{\the\pgf@xa}{\the\pgf@ya}}
      {\noexpand\pgfqpoint{\the\pgf@xb}{\the\pgf@yb}}
      \noexpand\pgftransformscale{0.035146}%
      \noexpand\pgfsetxvec{\noexpand\pgfpoint{1cm}{0cm}}%
      \noexpand\pgfsetyvec{\noexpand\pgfpoint{0cm}{1cm}}%
      \noexpand\pgfsetzvec{\noexpand\pgfpoint{0cm}{0cm}}%
    }}%
  \pgf@marshal%
}%

Next thing is to applied correct canvas transformation before \pgfqboxsynced\pgf@hbox . The code looks like

  \pgftransformtriangle%
  {\pgfpointxy{0}{0}}%
  {\pgfpointxy{1}{0}}%
  {\pgfpointpolarxy{<some angle>}{1}}%
  \pgftransformscale{0.035146}%
  \pgfsetxvec{\pgfpointxy{1}{0}}%
  \pgfsetyvec{\pgfpointxy{0}{1}}%
  \pgfsetzvec{\pgfpointxy{0}{0}}%

It seems that almost nothing has been changed. Note that before this piece of code, all the transformation has been applied, which means \pgfpoingxy{1}{0} points to the direction of the path.

The key point is the y unit vector, as is said above, if <some angle> is 90, then y unit vector is perpendicular to x unit vector, which is the default behavior.

By setting y unit vector, there are two ways to make text appear to be placed on a curved plane:

  • set y unit vector to a constant unit vector
  • set y unit vector to a vector that points from the segment to some point

First way is easy to implement, just set <some angle> to <angle of constant y unit vector> - \pgfdecoratedangle (minus \pgfdecoratedangle to invert the segement rotation).

Second way need calculate angle of different y unit vector before decoration segment is decorated. This work can be done in persistent precomputation of state typeset

  persistent precomputation={
     \pgfmathanglebetweenpoints%
     {\pgfpointlineatdistance{\pgfdecoratedinputsegmentcompleteddistance}{\pgf@decorate@inputsegment@first}{\pgf@decorate@inputsegment@last}}%
     {\pgf@decorate@rel@point}
     \xdef\pgf@decorate@yvec@angle{\pgfmathresult}
  }

At last, define the user interface

\pgfkeys{%
  /pgf/decoration/.cd,
  3d raise/.store in=\tikz@lib@dec@te@threedimraisevar,
  3d raise=0pt,
  yvec/.code={\tikz@handle@vec{\tikz@lib@dec@te@yvec@point}{\tikz@lib@dec@te@yvec@angle}#1\relax}
  yvec/.initial=90,
}

\tikzset{
  mytext/.style={
    postaction=decorate,
    decoration={
      3d text along path,
      3d raise=.8ex,
      text align={align=center},#1
    }
  }
}

Then you can use mytext={...} to specify a piece of text on curved plane, all original keys of /pgf/decoration is supported and two new keys are added, you can set y unit vector to a constant vector by yvec=<angle> or to a vector point to some point by yvec={<point>}. Also, use 3d raise=<dimen> to get a correct shift in y unit vector.

Here is some examples and complete code: enter image description here

main.tex

\documentclass[tikz, border=1cm]{standalone}
\usetikzlibrary{decorations,decorations.text}
\input{text.tex}

\tikzset{
  mytext/.style={
    postaction=decorate,
    decoration={
      3d text along path,
      3d raise=.8ex,
      text align={align=center},#1
    }
  }
}

\begin{document}
\begin{tikzpicture}
\draw [mytext={text along path, text={Do you know $x + y = z$? You don't know, what a pity!}}] (0, 0) arc (90:90+360:4cm and 1cm);
\draw [yshift=2.5cm, mytext={text={Do you know $x + y = z$? You don't know, what a pity!}, yvec={(0,12)}}] (0, 0) arc (90:90+360:4cm and 1cm);
\draw [yshift=5cm, mytext={text={Do you know $x + y = z$? You don't know, what a pity!}, yvec=90}] (0, 0) arc (90:90+360:4cm and 1cm);
\draw [yshift=6cm, mytext={text={Do you know $x + y = z$? You don't know, what a pity!}, yvec=120}] (0, 0) to[relative, out=80, in=-80, distance=7cm] (0, 4);
\node at (0, -1) {ellipse plane};
\node at (0, 1.5) {point to curve plane};
\node at (0, 4) {shifted curve plane};
\end{tikzpicture}
\end{document}

text.tex

\makeatletter
\newif\iftikz@lib@dec@te@yvecispoint
\let\tikz@lib@dec@te@yvecendpoint=\pgfutil@empty
\def\tikz@lib@dec@te@yvecangle{90}
\let\tikz@lib@dec@te@threedimraisevar=\pgfutil@empty
% keys for 3d text
\pgfkeys{%
  /pgf/decoration/.cd,
  3d raise/.store in=\tikz@lib@dec@te@threedimraisevar,
  3d raise=0pt,
  yvec/.code={\tikz@handle@vec{\tikz@lib@dec@te@yvec@point}{\tikz@lib@dec@te@yvec@angle}#1\relax}
}

% Parse yvec
% If #1 is an angle, save the angle as the direction of yvec
% If #1 is a point, parse the point and save it in
% `\tikz@lib@dec@te@yvecendpoint'
\def\tikz@lib@dec@te@yvec@angle#1{%
  \tikz@lib@dec@te@yvecispointfalse%
  \def\tikz@lib@dec@te@yvecangle{#1}%
}%
\def\tikz@lib@dec@te@yvec@point#1{%
  \tikz@lib@dec@te@yvecispointtrue%
  \def\tikz@lib@dec@te@yvecendpoint{#1}%
}%


% 3d text along path
\pgfdeclaredecoration{3d text along path}{initial}{%
    \state{initial}[
        width=+0pt, next state=left indent,
        persistent precomputation={%
            \edef\pgf@lib@dec@text@indent@left{\pgfkeysvalueof{/pgf/decoration/text align/left indent}}%
            \edef\pgf@lib@dec@text@indent@right{\pgfkeysvalueof{/pgf/decoration/text align/right indent}}%
            \edef\pgf@lib@dec@text@align{\pgfkeysvalueof{/pgf/decoration/text align/align}}%
            \pgfdecoratedremainingdistance=\pgfdecoratedpathlength%
        \advance\pgfdecoratedremainingdistance by-\pgf@lib@dec@text@indent@right\relax%
        \edef\pgfdecoratedpathlength{\the\pgfdecoratedremainingdistance}%
        \pgf@lib@dec@text@getwidth%
        \pgf@x=\pgf@lib@dec@text@width\relax%
        \pgf@y=\pgfdecoratedremainingdistance%
        \ifpgf@lib@dec@text@fit%
            \advance\pgf@y by-\pgf@lib@dec@text@indent@left\relax%
            \advance\pgf@y by-\pgf@x%
            \ifpgf@lib@dec@text@stretch@spaces%
                \def\pgf@lib@dec@text@character@shift{0pt}%
                \divide\pgf@y by\pgf@lib@dec@space@count\relax%
                \edef\pgf@lib@dec@text@space@shift{\the\pgf@y}%
            \else%
                \c@pgf@counta=\pgf@lib@dec@character@count\relax%
                \advance\c@pgf@counta by-1\relax%
                \divide\pgf@y by\c@pgf@counta\relax%
                \edef\pgf@lib@dec@text@character@shift{\the\pgf@y}%
                \def\pgf@lib@dec@text@space@shift{0pt}%
            \fi%
            \ifdim\pgf@y<0pt\relax%
                \pgf@lib@dec@text@fitfalse%
                \pgf@lib@dec@text@stretch@spacesfalse%
                \def\pgf@lib@dec@text@character@shift{0pt}%
                \def\pgf@lib@dec@text@space@shift{0pt}%
            \fi%
        \else%
            \def\pgf@lib@dec@text@character@shift{0pt}%
            \def\pgf@lib@dec@text@space@shift{0pt}%
                \ifx\pgf@lib@dec@text@align\pgf@lib@dec@text@left@text%
                \else%
                    \ifx\pgf@lib@dec@text@align\pgf@lib@dec@text@right@text%
                        \advance\pgf@y by-\pgf@x%
                        \edef\pgf@lib@dec@text@indent@left{\the\pgf@y}%
                    \else%
                        \advance\pgf@y by-\pgf@x%
                        \advance\pgf@y by-\pgf@lib@dec@text@indent@left\relax%
                        \pgf@y=0.5\pgf@y%
                        \advance\pgf@y by\pgf@lib@dec@text@indent@left\relax%
                        \edef\pgf@lib@dec@text@indent@left{\the\pgf@y}%
                    \fi%
                \fi%
        \fi%
        \let\pgfdecorationrestoftext=\pgfdecorationtext%
    }]{}%
\state{left indent}[width=+\pgf@lib@dec@text@indent@left, next state=scan]{}%
%
\state{scan}[
    width=+0pt,
    next state=before typeset,
    persistent precomputation={
        \pgf@lib@dec@text@scanchar%
        \ifvoid\pgf@lib@dec@text@box%
            \setbox\pgf@lib@dec@text@box\hbox{}%
            \wd\pgf@lib@dec@text@box16383pt\relax%
        \fi%
}]{}%
%
\state{before typeset}[width=+.5\wd\pgf@lib@dec@text@box, next state=typeset]{}%
%
\state{typeset}[width=+0pt, next state=after typeset,
  persistent precomputation={
    \iftikz@lib@dec@te@yvecispoint
      \pgfmathanglebetweenpoints%
      {\pgfpointlineatdistance{\pgfdecoratedinputsegmentcompleteddistance}{\pgf@decorate@inputsegment@first}{\pgf@decorate@inputsegment@last}}%
      {\tikz@lib@dec@te@yvecendpoint}%
      \edef\tikz@lib@dec@te@yvecangle{\pgfmathresult}
    \fi
  }
]
{%
  \pgftransformxshift{+-.5\wd\pgf@lib@dec@text@box}%
  \setbox\pgf@hbox\hbox{\copy\pgf@lib@dec@text@box}%
  \pgftransformtriangle%
  {\pgfpointxy{0}{0}}%
  {\pgfpointxy{1}{0}}%
  {\pgfpointpolarxy{\tikz@lib@dec@te@yvecangle-\pgfdecoratedangle}{1}}%
%   {\pgfpointpolarxy{\tikz@lib@dec@te@yvecangle-\pgfdecoratedangle}{1}}%
  \pgftransformscale{0.035146}%
  \pgfsetxvec{\pgfpointxy{1}{0}}%
  \pgfsetyvec{\pgfpointxy{0}{1}}%
  \pgfsetzvec{\pgfpointxy{0}{0}}%
  \pgftransformshift{\pgfpoint{0pt}{\tikz@lib@dec@te@threedimraisevar}}
  \pgfqboxsynced\pgf@hbox%
}%
\state{after typeset}[width=+.5\wd\pgf@lib@dec@text@box, next state=shift,
    persistent precomputation={%
    \ifpgf@lib@dec@text@fit%
        \ifpgf@lib@dec@text@stretch@spaces%
            \ifpgf@lib@dec@text@scan@space%
                \let\pgf@lib@dec@text@shift=\pgf@lib@dec@text@space@shift%
            \else%
                \def\pgf@lib@dec@text@shift{0pt}%
            \fi%
        \else%
            \let\pgf@lib@dec@text@shift=\pgf@lib@dec@text@character@shift%
          \fi%
        \else%
            \def\pgf@lib@dec@text@shift{0pt}%
      \fi%
  }]{}%
\state{shift}[width=+\pgf@lib@dec@text@shift, next state=scan]{}%
\state{final}{}%
}%
\makeatother

2
  • Strictly speaking, emulating a conical surface needs nonlinear transformation, but pgf can only apply linear transformation to text. See pgfmanual sec. 109.4.1. Jul 18, 2020 at 20:23
  • Hi @ZhiyuanLck! Thanks for answering my question; this is (again!) completely amazing! :)
    – Théo
    Jul 23, 2020 at 3:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.