2

I am drawing curved arrows in tikz-cd, using the method devised by @AndréC in this answer. There are situations where I want to shorten these arrows, proportional to their arc length. However, the naïve solution (using shorten) is not suitable, as it creates distorted curves (as discussed, for instance, in this question). As an example (Example 1), the following two curves have the same input path, but the red curve has been shortened. What should happen is the red curve should overlay the black curve.

bad shortened curve

Note that it is not just the position of the shortened curve that is incorrect: the shape is incorrect too: in the example (Example 2) below, I've positioned the curves so they have the same apex, but the shortened curve does not overlay the original (the amount of distortion depends on the curve width and height).

overlaid bad curve

The other issue with shorten is that it requires an absolute length to shorten by, whereas I want to specify it proportionally with respect to the length of the curve itself.

I attempted to manually draw an arrow head with decorations.markings, but this does not properly calculate the right size for the arrowhead (or take into account the existing style of the head or tail). In the example below (Example 3), the arrow head further up the curve should be the same size as the one at the tip.

bad manual head

My idea was to shorten this arrow using a custom dash pattern, but this seems like a hack that is likely in practice to fail in edge cases. Ideally, the curve path itself should be changed, which would handle the existing style/arrowheads/tails/etc. The option pgfpathcurvebetweentime seemed like a possible solution, but I could not figure out how to integrate it with the custom curve style in the two examples.

How might one add an option to the custom curve style to allow the curve to be shortened (e.g. curve={height=-40pt, shorten=0.2})? In practice, shortening symmetrically from both ends is usually sufficient, but having options to shorten the start and end differently would be even better if it doesn't add much extra complexity.

For a demonstration of what I intend by "shortening a curved arrow", see @Thruston's example.

Example 1

\documentclass{article}
\usepackage{tikz-cd}
\usetikzlibrary{calc}

\begin{document}

{\tikzset{curve/.style={settings={#1},to path={(\tikztostart) .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) .. (\tikztotarget)\tikztonodes}}, settings/.code={\tikzset{quiver/.cd,#1} \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}}, quiver/.cd,pos/.initial=0.35,height/.initial=0}

[\begin{tikzcd} {\bullet} & {\bullet} \arrow[from=1-1, to=1-2, curve={height=-40pt}, shorten <= 30pt, shorten >= 30pt, color=red] \arrow[from=1-1, to=1-2, curve={height=-40pt}] \end{tikzcd}]}

\end{document}

Example 2

\documentclass{article}
\usepackage{tikz-cd}
\usetikzlibrary{calc}

\begin{document}

{\tikzset{curve/.style={settings={#1},to path={(\tikztostart) .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) .. (\tikztotarget)\tikztonodes}}, settings/.code={\tikzset{quiver/.cd,#1} \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}}, quiver/.cd,pos/.initial=0.35,height/.initial=0}

[\begin{tikzcd} {\bullet} &&& {\bullet} \arrow[from=1-1, to=1-4, curve={height=-60pt}, shorten <= 30pt, shorten >= 30pt, color=red, yshift=-11pt] \arrow[from=1-1, to=1-4, curve={height=-60pt}] \end{tikzcd}]}

\end{document}

Example 3

\documentclass{article}
\usepackage{tikz-cd}
\usetikzlibrary{calc}
\usetikzlibrary{decorations.markings}

\begin{document}

{\tikzset{curve/.style={settings={#1},to path={(\tikztostart) .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) .. (\tikztotarget)\tikztonodes}, % Arrow head and tail decoration={ markings, mark=at position 0.8 with {\arrow{>}}} , postaction={decorate}}, % settings/.code={\tikzset{quiver/.cd,#1} \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}}, quiver/.cd,pos/.initial=0.35,height/.initial=0}

[\begin{tikzcd} {\bullet} & {\bullet} \arrow[Rightarrow, from=1-1, to=1-2, curve={height=-40pt}] \end{tikzcd}]}

\end{document}

varkor
  • 539
  • 2
  • 12
  • Isn't it enough to move this arrow with e.g. yshift=5pt? – AndréC Oct 28 '20 at 12:02
  • @AndréC: unfortunately not: the actual curve shape is incorrect. I've added an example in the question. In some cases, it's close enough not to be noticeable, but that's not always the case. I also don't know of a way to deterministically calculate how much to shift by to align the apices, even if this was sufficient. – varkor Oct 28 '20 at 12:25
  • What are the cases that prevent you from using such a move? – AndréC Oct 28 '20 at 12:44
  • I'm outputting TikZ code from a diagram editor, so the TikZ diagram needs to match the original diagram as closely as possible, for arbitrary diagrams. This means I can't hard-code offsets, and need to be confident that the output will be reasonable regardless of the shape of the curve. – varkor Oct 28 '20 at 13:22
  • I can add a parameter that shifts the curve perpendicular to the path, would that be okay with you? – AndréC Oct 28 '20 at 14:32
  • I have not fully read or understood the problem - because I do not use tikz-cd. Can you use this way or shortening to anything: https://tex.stackexchange.com/questions/577430/is-there-an-empty-arrow-relative-length-to-sep-option – hpekristiansen Jan 03 '21 at 20:11
  • @hpekristiansen: that's a really interesting approach; I hadn't seen sep used like that. I think if you receive an answer to your question (e.g. about passing a relative length to sep), it may also apply to my question. – varkor Jan 03 '21 at 22:36

6 Answers6

5

Update: 2023-04-8: This is now in the version of spath3 available on CTAN

Now that this is implemented in the version of spath3 on CTAN, the code below can be considerably simplified.

\documentclass{article}
%\url{https://tex.stackexchange.com/q/568648/86}
\usepackage{tikz-cd}
\usetikzlibrary{
  calc,
  spath3
}

\def\pv#1{% \pgfkeysvalueof{/tikz/quiver/#1}% }

\tikzset{ curve/.style={ quiver/.cd, #1, /tikz/.cd, to path={ (\tikztostart) .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) .. (\tikztotarget)\tikztonodes} }, quiver/.cd, pos/.initial=0.35, height/.initial=0 }

\begin{document}

\begin{tikzpicture} \draw[spath/save=curve, ultra thick] (0,0) .. controls +(1,2) and +(-1,2) .. (3,0); \tikzset{ % spath/split at keep start={curve}{.9}, % spath/split at keep end={curve}{.1/.9}, spath/split at keep middle={curve}{.1}{.9}, } \draw[spath/use={curve},red]; \end{tikzpicture}

\begin{tikzcd} {\bullet} & {\bullet} & {\bullet} \arrow[ from=1-1, to=1-2, curve={ height=-40pt } ] \arrow[ from=1-2, to=1-3, curve={ height=-40pt }, /tikz/spath/at end path construction={ \tikzset{spath/split at keep middle={current}{.1}{.9}} }, color=red, ] \end{tikzcd}

\end{document}

This produces:

Shortened curves along their paths


The capability to do this is essentially in my spath3 package. The existing user-level code shortens paths by a dimension so I needed to do a bit of coding to add the required functionality to the user interface, this will probably end up in the library in some form or other as it feels quite useful (I just need to think about consistency between the various commands that I have). So don't be alarmed by the stuff in the preamble - that'll end up in the package eventually.

\documentclass{article}
%\url{https://tex.stackexchange.com/q/568648/86}
\usepackage{tikz-cd}
\usetikzlibrary{
  calc,
  spath3
}

\makeatletter \ExplSyntaxOn

\cs_new_protected_nopar:Npn \spath_discard_after:Nn #1#2 { \spath_split_at:NNVn \l_tmpa_tl \l_tmpb_tl #1 {#2} \tl_set_eq:NN #1 \l_tmpa_tl }

\cs_new_protected_nopar:Npn \spath_discard_before:Nn #1#2 { \spath_split_at:NNVn \l_tmpa_tl \l_tmpb_tl #1 {#2} \tl_set_eq:NN #1 \l_tmpb_tl }

\cs_new_protected_nopar:Npn \spath_discard_outside:Nnn #1#2#3 { \spath_split_at:NNVn \l_tmpa_tl \l_tmpb_tl #1 {#3} \spath_split_at:NNVn \l_tmpa_tl \l_tmpb_tl \l_tmpa_tl {(#2)/(#3)} \tl_set_eq:NN #1 \l_tmpb_tl }

\cs_generate_variant:Nn \spath_discard_after:Nn {cn} \cs_generate_variant:Nn \spath_discard_before:Nn {cn} \cs_generate_variant:Nn \spath_discard_outside:Nnn {cnn}

\tl_new:N \l__tikzspath_tikzpath_finish_tl

\cs_new_protected_nopar:Npn \spath_at_end_of_path_construction: { \tl_use:N \l__tikzspath_tikzpath_finish_tl } \tl_put_left:Nn \tikz@finish {\spath_at_end_of_path_construction:}

\tikzset{ spath/.cd, at~ end~ path~ construction/.code={% \tl_put_right:Nn \l__tikzspath_tikzpath_finish_tl {#1} }, discard~ after~ point/.code~ 2~ args={ __tikzspath_maybe_current_path_reuse:nnn { __tikzspath_check_path:nnn { \spath_discard_after:cn } } {#1} { {} {#2} } }, discard~ before~ point/.code~ 2~ args={ __tikzspath_maybe_current_path_reuse:nnn { __tikzspath_check_path:nnn { \spath_discard_after:cn } } {#1} { {} {#2} } }, discard~ outside~ points/.code~ n~ args={3}{ __tikzspath_maybe_current_path_reuse:nnn { __tikzspath_check_path:nnn { \spath_discard_outside:cnn } } {#1} { {} {#2} {#3} } }, }

\ExplSyntaxOff \makeatother

\def\pv#1{% \pgfkeysvalueof{/tikz/quiver/#1}% }

\tikzset{ curve/.style={ quiver/.cd, #1, /tikz/.cd, to path={ (\tikztostart) .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) .. (\tikztotarget)\tikztonodes} }, quiver/.cd, pos/.initial=0.35, height/.initial=0 }

\begin{document}

\begin{tikzcd} {\bullet} & {\bullet} & {\bullet} \arrow[ from=1-1, to=1-2, curve={ height=-40pt } ] \arrow[ from=1-2, to=1-3, curve={ height=-40pt }, /tikz/spath/at end path construction={ \tikzset{spath/discard outside points={current}{.1}{.9}} }, color=red, ] \end{tikzcd}

\end{document}

Path shortened by parameter

NB The link above is to the github repository as the development version is a little ahead of the ctan at the moment.

Andrew Stacey
  • 153,724
  • 43
  • 389
  • 751
  • This looks great: from a little testing, it seems to work well with existing arrow styles (e.g. Rightarrow). If it can be made more user-friendly with TikZ keys (once you've figured out the best API), it will be perfect. It seems like it could be a stand-in for the shorten keys, as it essentially corrects their behaviour. – varkor Nov 20 '21 at 18:08
  • @varkor Thanks. If you encounter any bizarre behaviour in testing please do let me know (via github, or my contact details are in the documentation). There's already keys for shortening correctly via a dimension. The difference here is that it is using a parameter which I I didn't originally have. – Andrew Stacey Nov 20 '21 at 18:36
  • I'll accept this answer, as it achieves what I want, though please let me know when you add a convenient API for this feature! – varkor Nov 21 '21 at 15:05
  • @varkor Will do. I have a few updates nearly ready to go on this library so it shouldn't be too long. Just a few things to test first to make sure it's stable. – Andrew Stacey Nov 21 '21 at 15:19
  • is this now supported in spath3 (without needing the complicated preamble)? – varkor Apr 07 '23 at 14:48
  • 1
    @varkor almost certainly, I just need a few minutes at my computer to revisit it with the latest version. Thanks for the reminder. – Andrew Stacey Apr 08 '23 at 12:58
4

Is this what you mean?

enter image description here

This is done in Metapost, purely for comparison, and in the hope that some one else here can show us how to do the equivalent in TikZ.

\documentclass[border=5mm]{standalone}
\usepackage{luamplib}
\begin{document}
\mplibtextextlabel{enable}
\begin{mplibcode}
input colorbrewer-rgb
beginfig(1);
    z0 = -z1 = 34 left;
    path a; a = z0 .. controls (-13, 55) and (13, 55) .. z1;
    ahangle := 30;
    ahlength := 2;
    for s=1 upto 8:
        drawarrow a 
            cutbefore fullcircle scaled 10s shifted z0
            cutafter  fullcircle scaled 10s shifted z1
            withcolor Blues[9][s];
    endfor
    drawdot z0 withpen pencircle scaled dotlabeldiam;
    drawdot z1 withpen pencircle scaled dotlabeldiam;
endfig;
\end{mplibcode}
\end{document}

(Compile with lualatex).

What I have done each is draw the same curve each time, but with more of it "cut off" at each end.

Thruston
  • 42,268
  • Yes, thank you, this is precisely what I mean. I will add a link to your answer in the original question for clarity. – varkor Oct 29 '20 at 13:12
  • Great answer...Thruston and don't forget that I'm still waiting for your answer on the feymmp package arrows. Otherwise I won't vote for you any more ahahah. – Sebastiano Oct 29 '20 at 22:13