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 …curveatangle
s to the …curveattime
s. (This technically does do work twice because the …curveattime
s 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 arc
s 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
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.