Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4-segment Bezier circle inaccuracy #817

Open
jmwilson opened this issue Jan 8, 2020 · 8 comments
Open

4-segment Bezier circle inaccuracy #817

jmwilson opened this issue Jan 8, 2020 · 8 comments
Milestone

Comments

@jmwilson
Copy link

jmwilson commented Jan 8, 2020

PGF uses a 4-segment approximation to draw circles. It's possible for this to show inaccuracies visible in publication at typical page-size dimensions when small and large radius circles are mixed and expected to intersect precisely. Suggestion: upgrade the circle & ellipse code to use an 8-segment approximation.

Minimal working example (MWE)

\documentclass[tikz,border=5pt]{standalone}
\begin{document}
\begin{tikzpicture}[scale=10]
  \draw (0,0) circle [radius=1];
  \clip (0,0) circle [radius=1];
  \draw (1,1/5) circle [radius={1/5}];
  \draw [cyan] (-1,5) circle [radius=5];
\end{tikzpicture}
\end{document}

These circles and arcs should intersect at a single point, but they are visibly off on the scale of a letter/A4 page.

Better results are achieved with an 8-segment approximation. The control point used is 4/3 tan (pi/16) (= 0.265216...) instead of 4/3 tan (pi/8) (=0.552284... as used in \pgfpathellipse)

\documentclass[tikz,border=5pt]{standalone}
\tikzset{
  bettercircle/.pic = {
    \draw (1,0) .. controls (1,0.265216) and (0.894643,0.519570) .. (0.707107,0.707107) [rotate=45]
                .. controls (1,0.265216) and (0.894643,0.519570) .. (0.707107,0.707107) [rotate=45]
                .. controls (1,0.265216) and (0.894643,0.519570) .. (0.707107,0.707107) [rotate=45]
                .. controls (1,0.265216) and (0.894643,0.519570) .. (0.707107,0.707107) [rotate=45]
                .. controls (1,0.265216) and (0.894643,0.519570) .. (0.707107,0.707107) [rotate=45]
                .. controls (1,0.265216) and (0.894643,0.519570) .. (0.707107,0.707107) [rotate=45]
                .. controls (1,0.265216) and (0.894643,0.519570) .. (0.707107,0.707107) [rotate=45]
                .. controls (1,0.265216) and (0.894643,0.519570) .. (0.707107,0.707107);
  }
}
\begin{document}
\begin{tikzpicture}[scale=10, transform shape]
  \draw (0,0) pic {bettercircle};
  \clip (0,0) circle [radius=1];
  \draw (1,1/5) pic [scale={1/5}] {bettercircle};
  \draw [cyan] (-1,5) pic [scale=5] {bettercircle};
\end{tikzpicture}
\end{document}
@hmenke
Copy link
Member

hmenke commented Jan 8, 2020

upgrade the circle & ellipse code to use an 8-segment approximation.

I fear that this is not going to be possible because of the way intersections work. They are enumerated by path time within path segments, so changing the number of segments could change the order of intersections which might break a lot of drawings. To reasonably tackle this we'd first have to resolve #666.

@josephwright
Copy link
Contributor

josephwright commented Jan 10, 2020

I've just checked using l3draw (using a more accurate FPU): I see the same slight inaccuracy.

\documentclass[border=5pt]{standalone}
\usepackage{l3draw}
\begin{document}
\ExplSyntaxOn 
\draw_begin:
  \draw_transform_scale:n { 10 }
  \draw_path_circle:nn { 0 , 0 } { 1cm }
  \draw_path_use:n { stroke , clip }
  \draw_path_circle:nn { 1cm , (1/5)cm } { (1/5)cm }
  \draw_path_use_clear:n { stroke }
  \draw_color:n { cyan }
  \draw_path_circle:nn { -1cm , 5cm } { 5cm }
  \draw_path_use_clear:n { stroke }
\draw_end:
\ExplSyntaxOff
\end{document}

(You'll need the fix in latex3/latex3#660 to make this work).

So I think it is due to the use of a four-part approximation, not due to the inaccuracies in calculation. I might look to adjust l3draw to use more segments.


From @muzimuzhi: \draw_color:n is renamed to \color_select:n in l3experimental 2020-08-07. See also latex3/latex3#799.

@hmenke
Copy link
Member

hmenke commented Jan 10, 2020

It's just due to the fact that you can't draw a perfect circle with Bézier curves. Even with more segments it will not match.

BTW, MetaPost also only uses four segments.

@jmwilson
Copy link
Author

Increasing the number of segments yields diminishing returns, though the 8-segment curve still looks like a perfect intersection at 64x magnification (worst case here is closer to r=1/9 and r=9), whereas the 4-segment curve is off by nearly a line width. The r=1/5 and r=5 combination places the angle subtended by the unit circle from the horizontal to the point of intersection very close to 22.5°, which is where the error is a maximum in the 4-segment approximation.

@ilayn
Copy link
Member

ilayn commented Jan 11, 2020

Scaling up an approximation uniformly, is not possible regardless of segment number but of course it gets better. It is also known that the approximation error is worst at the plus/minus angle as you mentioned. Here is a precise calculation of those angles (see the plot) : http://spencermortensen.com/articles/bezier-circle/

However doubling the number of PDF objects for a minute improvement doesn't look like a feasible return to me. Also once you know this fact, only rotating the cyan circle would be a quite convincing intersection:

\begin{tikzpicture}[scale=10]
  \clip[postaction={draw, thick}] (0,0) circle [radius=1];
  \draw (1,1/5) circle [radius={1/5}];
  \draw[cyan, rotate around={22.5:(-1,5)}] (-1,5) circle [radius=5];
\end{tikzpicture}

@hmenke hmenke added this to the 3.1.6 milestone Jan 26, 2020
@hmenke
Copy link
Member

hmenke commented Jan 26, 2020

I've thought about this and I am not going to change the default circle. Too many drawings rely on this exact shape. However, I can offer adding this as a library solution, something like \usetikzlibary{bettercircles} and make the number of segments a user-defined value, so that you can use 100 segments if you want.

@kpym
Copy link
Contributor

kpym commented May 6, 2020

I agree that the circle command should stay with 4 segments (almost all softwares do this). If somebody needs a more precise circle it can use pic (as shown in the OP), or to path, or simple macro to do this. In 99.9% of the time 4 segments is enough, IMO. Moving to 8 (or more) will make all produced PDFs bigger and will make TikZ more complex and slower.

Remark : the TikZ code use 0.55228475 as approximation of (4/3)*tan(pi/8) = 4*(sqrt(2)-1)/3 = 0.552284749831 which is used to approximate the circle by 4 curves in a way that the middle of all this curves lies on the circle. But in this way the curve is always outside of the circle. So this is not an optimal approximation. Replacing the value 0.552284749831 by slightly smaller (but how much ?) we may produce a curve that has smaller deviation from the circle.

@kpym
Copy link
Contributor

kpym commented May 6, 2020

I have made some python simulations to find the best replacement of 0.55228475 to minimize the radial deviation. I found the value (up to eight digits) of 0.55191502. This is better than the original value but is not good enough for the OP request.

Here is the fixed \pgfpathellipse and a proof by example:

\documentclass[tikz,border=5pt]{standalone}
\usetikzlibrary{spy}
% replace 0.55228475 -> 0.55191502 for minimal radial deviation
% the OP example looks better with 0.5513 which is very far from optimal in general
\makeatletter
\def\bez@circle{0.55191502}
\def\pgfpathellipse#1#2#3{%
  \pgfpointtransformed{#1}% store center in xc/yc
  \pgf@xc=\pgf@x%
  \pgf@yc=\pgf@y%
  \pgfpointtransformed{#2}%
  \pgf@xa=\pgf@x% store first axis in xa/ya
  \pgf@ya=\pgf@y%
  \advance\pgf@xa by-\pgf@pt@x%
  \advance\pgf@ya by-\pgf@pt@y%
  \pgfpointtransformed{#3}%
  \pgf@xb=\pgf@x% store second axis in xb/yb
  \pgf@yb=\pgf@y%
  \advance\pgf@xb by-\pgf@pt@x%
  \advance\pgf@yb by-\pgf@pt@y%
  {%
    \advance\pgf@xa by\pgf@xc%
    \advance\pgf@ya by\pgf@yc%
    \pgf@nlt@moveto{\pgf@xa}{\pgf@ya}%
  }%
  \pgf@x=\bez@circle\pgf@xb% first arc
  \pgf@y=\bez@circle\pgf@yb%
  \advance\pgf@x by\pgf@xa%
  \advance\pgf@y by\pgf@ya%
  \advance\pgf@x by\pgf@xc%
  \advance\pgf@y by\pgf@yc%
  \edef\pgf@temp{\pgf@xc\the\pgf@x\pgf@yc\the\pgf@y}%
  \pgf@x=\bez@circle\pgf@xa%
  \pgf@y=\bez@circle\pgf@ya%
  \advance\pgf@x by\pgf@xb%
  \advance\pgf@y by\pgf@yb%
  {%
    \advance\pgf@x by\pgf@xc%
    \advance\pgf@y by\pgf@yc%
    \advance\pgf@xb by\pgf@xc%
    \advance\pgf@yb by\pgf@yc%
    \pgf@temp%
    \pgf@nlt@curveto{\pgf@xc}{\pgf@yc}{\pgf@x}{\pgf@y}{\pgf@xb}{\pgf@yb}%
  }%
  \pgf@xa=-\pgf@xa% flip first axis
  \pgf@ya=-\pgf@ya%
  \pgf@x=\bez@circle\pgf@xa% second arc
  \pgf@y=\bez@circle\pgf@ya%
  \advance\pgf@x by\pgf@xb%
  \advance\pgf@y by\pgf@yb%
  \advance\pgf@x by\pgf@xc%
  \advance\pgf@y by\pgf@yc%
  \edef\pgf@temp{\pgf@xc\the\pgf@x\pgf@yc\the\pgf@y}%
  \pgf@x=\bez@circle\pgf@xb%
  \pgf@y=\bez@circle\pgf@yb%
  \advance\pgf@x by\pgf@xa%
  \advance\pgf@y by\pgf@ya%
  {%
    \advance\pgf@x by\pgf@xc%
    \advance\pgf@y by\pgf@yc%
    \advance\pgf@xa by\pgf@xc%
    \advance\pgf@ya by\pgf@yc%
    \pgf@temp%
    \pgf@nlt@curveto{\pgf@xc}{\pgf@yc}{\pgf@x}{\pgf@y}{\pgf@xa}{\pgf@ya}%
  }%
  \pgf@xb=-\pgf@xb% flip second axis
  \pgf@yb=-\pgf@yb%
  \pgf@x=\bez@circle\pgf@xb% third arc
  \pgf@y=\bez@circle\pgf@yb%
  \advance\pgf@x by\pgf@xa%
  \advance\pgf@y by\pgf@ya%
  \advance\pgf@x by\pgf@xc%
  \advance\pgf@y by\pgf@yc%
  \edef\pgf@temp{\pgf@xc\the\pgf@x\pgf@yc\the\pgf@y}%
  \pgf@x=\bez@circle\pgf@xa%
  \pgf@y=\bez@circle\pgf@ya%
  \advance\pgf@x by\pgf@xb%
  \advance\pgf@y by\pgf@yb%
  {%
    \advance\pgf@x by\pgf@xc%
    \advance\pgf@y by\pgf@yc%
    \advance\pgf@xb by\pgf@xc%
    \advance\pgf@yb by\pgf@yc%
    \pgf@temp%
    \pgf@nlt@curveto{\pgf@xc}{\pgf@yc}{\pgf@x}{\pgf@y}{\pgf@xb}{\pgf@yb}%
  }%
  \pgf@xa=-\pgf@xa% flip first axis once more
  \pgf@ya=-\pgf@ya%
  \pgf@x=\bez@circle\pgf@xa% fourth arc
  \pgf@y=\bez@circle\pgf@ya%
  \advance\pgf@x by\pgf@xb%
  \advance\pgf@y by\pgf@yb%
  \advance\pgf@x by\pgf@xc%
  \advance\pgf@y by\pgf@yc%
  \edef\pgf@temp{\pgf@xc\the\pgf@x\pgf@yc\the\pgf@y}%
  \pgf@x=\bez@circle\pgf@xb%
  \pgf@y=\bez@circle\pgf@yb%
  \advance\pgf@x by\pgf@xa%
  \advance\pgf@y by\pgf@ya%
  {%
    \advance\pgf@x by\pgf@xc%
    \advance\pgf@y by\pgf@yc%
    \advance\pgf@xa by\pgf@xc%
    \advance\pgf@ya by\pgf@yc%
    \pgf@temp%
    \pgf@nlt@curveto{\pgf@xc}{\pgf@yc}{\pgf@x}{\pgf@y}{\pgf@xa}{\pgf@ya}%
  }%
  \pgf@nlt@closepath%
  \pgf@nlt@moveto{\pgf@xc}{\pgf@yc}%
}

\begin{document}
\begin{tikzpicture}[scale=10, spy using outlines={circle, size=35mm, connect spies}]
  \draw (0,0) circle [radius=1];
  \clip (0,0) circle [radius=1.1];
  \draw (1,1/5) circle [radius={1/5}];
  \draw[radius=5] [cyan] (-1,5) circle;
  \coordinate(A) at (12/13,5/13);
  \spy[magnification=5,green]
    on (A)
    in node [left] at (.5,.5);
\end{tikzpicture}
\end{document}

It may be good to replace 0.55228475 by 0.55191502 in pgfcorepathconstruct.code.tex to have better circle approximation. But we can also find this value in \pgf@arc and in Round cap so should we change it everywhere ?

If we do this, we should probably replace 1.333333333 in \pgf@arc in a proportional way by 1.33244073.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

5 participants