8

I would like to draw a tangent line to a curve, and I know there are elegant solutions to do so (eg. How to draw tangent line of an arbitrary point on a path in TikZ). However, these approaches usually draw the tangent at a point that is specified by its relative position on the curve. Instead, what I would like to do is give a specified angle and have tikz find the position itself. (I know there could be multiple, but for my purposes I don't have that issue.)

Here is a MWE:

\documentclass[tikz]{standalone}
\usepackage{tikz}

\begin{document}
\begin{tikzpicture}
    \draw (0,6) -- (0,0) -- (9,0);
    \draw[blue] (0,5) to[out=-5,in=100] (8,0);
    \begin{scope}[shift={(5.4,3.6)}]
        \draw[red,rotate=-30] (-2,0) -- (2,0);
        \draw (0,0) circle (3pt);
    \end{scope}
\end{tikzpicture}
\end{document}

Essentially, what I'm after is how to automate the search for the "shift" parameter, which I here just eyeballed with trial & error.

3
  • 1
    While PGF can calculate the angle for a relative poisition (time) of a curve it doesn't provide a function to calculate the inverse. You will need some mathematical formula (first derivative) for that, I believe. You could also try a bunch of points, say 0, .05, …, .95, .1 and choose the one that's closest to the desired angle. It gets even more interesting when you're not looking at only one Bézier curve but a path comprised out of multiple segments. Commented Jan 11, 2023 at 14:36
  • Basically, you need to do the inverse of this answer, i.e. finding a t for a specific angle. Commented Jan 11, 2023 at 14:43
  • Because I need two parallel lines in my case, it's easier to eyeball the shift parameter rather than using tangents and estimating the pos parameter. I can keep doing it this way, I was just wondering if there is a more elegant approach. Thanks though!
    – mimuller
    Commented Jan 11, 2023 at 14:50

4 Answers 4

6

The bezier bounding box from the TikZ library bbox can do these types of calculations, but it is not meant for this purpose, so it takes some tricks to extract the coordinate.

\documentclass[tikz, border=1cm]{standalone}
\usetikzlibrary{bbox, intersections}
\begin{document}
\begin{tikzpicture}
\newcommand{\mya}{-30}
\draw (0,6) -- (0,0) -- (9,0);
\draw[blue] (0,5) to[out=-5, in=100] (8,0);
\begin{scope}[local bounding box=lbb, bezier bounding box]
\path[name path=curve, rotate=-\mya] (0,5) to[out=-5, in=100] (8,0);
\path[name path=max] ([yshift=-0.001pt]lbb.north west) -- ([yshift=-0.001pt]lbb.north east);
\path[name intersections={of=max and curve}] (intersection-1) coordinate(c);
\end{scope}
\begin{scope}[transform canvas={rotate=\mya}]
\draw[red] (c) +(-2,0) -- +(2,0);
\draw (c) circle[radius=3pt];
\end{scope}
\end{tikzpicture}
\end{document}

A curve with a tangent point and line

2
  • Thanks!! This looks really promising – it's by far the least custom code of the solutions given so far. Would you please be able to explain what the two last lines in the scope do, so that I'm better able to adapt the code if needed?
    – mimuller
    Commented Jan 12, 2023 at 1:31
  • 1
    The code rotates the curve by the given angle, finds the bounding box, and finds the intersection of the curve and this box -that is the maxima of the rotated curve. The maxima point is rotated back on the original curve and circle and line is drawn. You can change \paths to \draws to see what is happening. Commented Jan 12, 2023 at 7:21
5

enter image description here

I propose a solution that uses a decoration and a pic element, the tangent line.

  1. The decoration phSTrajectory takes one argument (the number of points) and creates equally spaced points P_i along the curve and the tips U_i of the tangent unit vectors.

  2. The pic element tangent takes two arguments, the angle of the tangent we are looking for (it must exist) and the number of points, and draws the tangent line (an approximation).

In the figure, ther is your drawing of the tangent at -30 degrees in red, and the corresponding pic in black. I added the tangent with sloppe -65 in thick red.

Remark I marked the corresponding point (a filled circle) for visual verification.

The code

\documentclass[11pt, margin=.5cm]{standalone}
\usepackage{tikz}
\usetikzlibrary{math, calc, decorations.markings}

\begin{document}
\tikzset{%
  phSTrajectory/.style={% nb of steps
    decoration={%
      markings,
      mark=between positions 0 and 1 step 1/#1 with {
        \tikzmath{%
          {
            \path (0, 0) coordinate[name=P_\pgfkeysvalueof{%
              /pgf/decoration/mark info/sequence number}];
            \path (1, 0) coordinate[name=tipU_\pgfkeysvalueof{%
              /pgf/decoration/mark info/sequence number}];
          };
        }
      }
    },
    postaction=decorate
  },
  pics/tangent/.style 2 args={% angle / numbrer of points
    code={
      \tikzmath{%
        integer \j;
        real \a;
        \j = -1;
        coordinate \M;
        for \i in {1, ..., #2}{%
          \M = ($(tipU_\i) -(P_\i)$);
          \a = {atan2(\My, \Mx)};
          if \a<-#1 then {%
            if \j==-1 then {%
              \j = \i -1;
              {
                \draw ($(P_\j)!-1!(tipU_\j)$) -- ($(P_\j)!1!(tipU_\j)$);
                \filldraw[blue] (P_\j) circle (1pt);
              };
            } else {};
          } else {};
        };
      }
    }
  }
}
\begin{tikzpicture}[evaluate={\N = int(220);}]
  \draw (0, 6) -- (0, 0) -- (9, 0);
  \draw[blue, phSTrajectory={\N}] (0, 5) to[out=-5, in=100] (8, 0);
  \path pic {tangent={30}{\N}};
  \path pic[red, very thick] {tangent={65}{\N}};

  \begin{scope}[shift={(5.4, 3.6)}]
    \draw[red, rotate=-30] (-2, 0) -- (2, 0);
    \draw (0, 0) circle (3pt);
  \end{scope}
\end{tikzpicture}
\end{document}
2
  • Am I understanding right that this basically computes the angle at many points to find the one closest to what I'm looking for?
    – mimuller
    Commented Jan 12, 2023 at 1:36
  • 1
    Indeed. It is close to the realm of numerical analysis. In case you need a better precision, some work must be done. I noticed that one can not keep increasing the number of points of the initial division.
    – Daniel N
    Commented Jan 12, 2023 at 4:28
4

You can force the curve to pass through the tangency point T with a known angle. Like this:

\documentclass[tikz]{standalone}

\begin{document}
\begin{tikzpicture}
  \coordinate (T) at (5.4,3.6);
  \draw (0,6) -- (0,0) -- (9,0);
  \draw[blue] (0,5) to[out=-5,in=150] (T) to [out=-30,in=100] (8,0);
  \draw[red] (T) ++ (150:3) --++ (-30:6);
  \draw (T) circle (3pt);
\end{tikzpicture}
\end{document}

enter image description here

1
  • Thanks! I've used this trick before.. alas, in this case it isn't really workable in my code because I've already used up the degrees of freedom for the curve itself.
    – mimuller
    Commented Jan 12, 2023 at 1:28
4

As suggested by my comment, here's a mathematical solution that works like \pgfpointcurveattime and \pgftransformcurveattime but with …atangle.

For TikZ, I'm using pos angle = <angle> that switches out the curve timer for the curve angle timer.

The core of this answer is \pgfpointcurveatangle@ which will “return” the time t as \@time which then will be forwarded by the …curveatangles to the …curveattimes. (This technically does do work twice because the …curveattimes also return the angle for the use with sloped.)

Instead of PGFMath, I'm using \fpeval since I've run into problems with basic PGFmath.
This probably can be converted so that it uses the fpu library but I don't have much experience with it.


Using pos angle with any other path than an explicit Bézier should do nothing. “Explicit” means that it will only work with .. controls or the various out/in options.
While arcs are drawn as Bézier curves on the lowest level, they use a different timer and won't handle the angle timer.

Lastly, I've added a node to show what PGF thinks the angle of the tangent is which, expectedly, is a bit off.


To use it, just place a coordinate, node or pic along a curve and instead of pos (or its implicit versions like midway) use pos angle. If you want the node or pic also angled use sloped and possibly allow upside down.

In your case, I've added a pic called cs which just places three coordinates named <name>, <name>-x and <name>-y (where <name> is the name of the pic). The pic cs style can then be used to shift and rotate the coordinate system as if you were drawing in the pic.

Then you can draw your tangent as such:

\draw[blue] (0,5) to[out=-5,in=100]
  pic[pos angle=-30, sloped] (a) {cs} (8,0);
\draw[pic cs=a, red] (-2,0) -- (2,0);
\draw (a) circle[radius=3pt];

Don't forget allow upside down otherwise your cs might be rotated.
With

pics/cs/.prefix style={/tikz/allow upside down, /tikz/sloped}

you can make every cs pic automatically sloped and upside down.

Code

\documentclass[tikz,convert]{standalone}
\makeatletter
%% Utils
\def\pgfextractxy#1{\pgf@process{#1}\pgfgetlastxy}
\newcommand*\fpUnlessIf[1]{
  \edef\pgfmathresult{\fpeval{#1}}%
  \ifnum\pgfmathresult=1 \expandafter\@gobble
                    \else\expandafter\@firstofone\fi}
\newcommand*\fpsetmacro[2]{\edef#1{\fpeval{#2}}\typeout{#1 = #2}}
%% PGF
\def\pgfpointcurveatangle@#1#2#3#4#5{%
  \def\@time{1}\fpsetmacro\@tan{tand(#1)}%
  \pgfextractxy{#2}\@Ax\@Ay \pgfextractxy{#3}\@Bx\@By
  \pgfextractxy{#4}\@Cx\@Cy \pgfextractxy{#5}\@Dx\@Dy
  \fpsetmacro\@divisor{
    -3*\@Bx*\@tan+3*\@Cx*\@tan-\@Dx*\@tan-\@Ax*\@tan -\@Ay+3*\@By-3*\@Cy+\@Dy}%
  \fpUnlessIf{\@divisor==0}{% divide by 0?
    \fpsetmacro\@divnosqrt{%% calculate dividend (no sqrt part)
      -2*\@Bx*\@tan+\@Cx*\@tan+\@Ax*\@tan-\@Ay+2*\@By-\@Cy}%
    \fpsetmacro\@divsqrt{%% calculate divident (sqrt part)
      (-4*\@Bx*\@tan+2*\@Cx*\@tan+2*\@Ax*\@tan -2*\@Ay+4*\@By-2*\@Cy)^2
      -4*(\@Bx*\@tan-\@Ax*\@tan+\@Ay-\@By)
      *(3*\@Bx*\@tan-3*\@Cx*\@tan+\@Dx*\@tan-\@Ax*\@tan+\@Ay-3*\@By+3*\@Cy-\@Dy)}%
    \fpUnlessIf{\@divsqrt<0}{% sqrt(neg)?
      \fpsetmacro\@time{(.5*sqrt(\@divsqrt)+\@divnosqrt)/\@divisor}%
      \fpUnlessIf{\@time>=0&&\@time<=1}{%
        \fpsetmacro\@time{(-.5*sqrt(\@divsqrt)+\@divnosqrt)/\@divisor}}%
    }}}
\def\pgfpointcurveatangle#1#2#3#4#5{%
  \pgfpointcurveatangle@{#1}{#2}{#3}{#4}{#5}%
  \pgfpointcurveattime{\@time}{#2}{#3}{#4}{#5}}
\def\pgftransformcurveatangle#1#2#3#4#5{%
  \pgfpointcurveatangle@{#1}{#2}{#3}{#4}{#5}%
  \pgftransformcurveattime{\@time}{#2}{#3}{#4}{#5}}
% TikZ
\newif\iftikz@angletimer
\def\tikz@timer@curve{%
  \iftikz@angletimer
    \pgftransformcurveatangle{\tikz@time@angle}{\tikz@timer@start}
     {\tikz@timer@cont@one}{\tikz@timer@cont@two}{\tikz@timer@end}%
  \else
    \pgftransformcurveattime{\tikz@time}{\tikz@timer@start}
      {\tikz@timer@cont@one}{\tikz@timer@cont@two}{\tikz@timer@end}%
  \fi}%
\tikzset{
  pos angle/.code=%
    \edef\tikz@time@angle{#1}%
    \ifx\tikz@time@angle\pgfutil@empty\else
      \pgfmathsetmacro\tikz@time@angle{\tikz@time@angle}\fi
    \tikz@angletimertrue,
  pos/.append code=\tikz@angletimerfalse}
\makeatother
\usetikzlibrary{calc}
\tikzset{
  cs/.pic={
   \coordinate (-x) at (1,0) coordinate (-y) at (0,1) coordinate () at (0,0);},
  pic cs/.style={shift={(#1)}, x={($(#1-x)-(#1)$)}, y={($(#1-y)-(#1)$)}}}
\begin{document}
\begin{tikzpicture}
    \draw (0,6) -- (0,0) -- (9,0);
    \draw[blue] (0,5) to[out=-5,in=100]
      pic[pos angle=-30, sloped] (a) {cs} (8,0);
    \draw[pic cs=a, red] (-2,0) -- (2,0);
    \draw (a) circle[radius=3pt];
    \node[below left] at (a) {
          \pgfmathanglebetweenpoints{\pgfpointanchor{a}{center}}
                                    {\pgfpointanchor{a-x}{center}}
          \pgfmathresult};
\end{tikzpicture}
\end{document}

Output

enter image description here

2
  • I realize now that there's \fp_compare… which might be better than my “dirty” \fpUnlessIf but too late … Commented Jan 11, 2023 at 23:12
  • 1
    Woah! Impressive!! I feel a bit bad because with a similar amount of algebra, I could have just figured out the correct parametrization of the curve in my figure. But much appreciated!
    – mimuller
    Commented Jan 12, 2023 at 1:34

You must log in to answer this question.

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