--- jupyter: description: 'The Bézier circle with planex2d: four cubic Bézier quarters with the handle 4/3(√2 − 1), how far they run from the circle (at most 0.027 % of the radius), the handle with the smallest largest deviation, and the rational quadratic quarter that is exactly the circle.' --- # The Bézier circle How close does a Bézier curve come to a circle? Fonts, SVG paths and most drawing programs draw a circle as four cubic Bézier curves, one per quarter. The two inner control points of each quarter sit on the tangents at its ends, a distance $k r$ out. A cubic polynomial cannot follow a circle exactly. The usual choice of handle, $$k = \tfrac{4}{3}\left(\sqrt{2} - 1\right) \approx 0.5523,$$ puts the middle of each quarter on the circle, and everywhere else the curve runs outside it, by at most 0.027 % of the radius. CAD systems store circles as rational B-splines instead, and a rational quadratic quarter is exactly the circle. The example draws the four cubic quarters with `px.BSpline`, measures how far they run from the circle, finds the handle with the smallest largest deviation and checks the rational quarter. This page is part of a collection of [geometry and design examples](geometry-and-design.md) using planex2d. .. rst-class:: px-example-links `Open this example in the playground `__, the interactive code editor in the browser, to change the code and run it. ![The deviation of four cubic Bézier quarters from the circle, magnified 300 times, with their control points](../_static/gallery/bezier-circle.png){.result} ```python import math import numpy as np from scipy.optimize import minimize_scalar import planex as px ``` ```python nbsphinx="hidden" from docs_figures import Chart, Row ``` ## Four cubic quarters With the radius $r = 10$, the quarter from $(r, 0)$ to $(0, r)$ has the control points $(r, 0)$, $(r, k r)$, $(k r, r)$ and $(0, r)$. A cubic Bézier curve is a B-spline of degree 3 with the knots 0, 0, 0, 0, 1, 1, 1, 1. The other three quarters are the first one turned by right angles, and together they close the circle. planex2d computes the area inside them from the exact curves (Figure 1): ```python r = 10 k = 4 / 3 * (math.sqrt(2) - 1) def quarter(k): """the control points of the cubic quarter from (r, 0) to (0, r) with handles k r long""" return np.array([(r, 0), (r, k * r), (k * r, r), (0, r)]) def turned(points, j): """the points turned j right angles about the center""" c, s = math.cos(j * math.pi / 2), math.sin(j * math.pi / 2) return [(c * x - s * y, s * x + c * y) for x, y in points] def circle(k): """the face inside four cubic quarters""" return px.Face([px.BSpline(turned(quarter(k), j), [0, 0, 0, 0, 1, 1, 1, 1], 3) for j in range(4)]) bezier = circle(k) print("k", k, "| area", bezier.mass, "| π r²", math.pi * r**2, "| relative", bezier.mass / (math.pi * r**2) - 1) # px.Plot(bezier) ``` ```python nbsphinx="hide-input" controls = [p for j in range(4) for p in turned(quarter(k), j)] handles = [px.FromPolyline(turned(quarter(k), j)) for j in range(4)] Row((bezier, "Four cubic quarters", {"dots": controls, "dashed": handles}), caption="The Bézier circle, its control points and handles (dashed)") ``` ## How far from the circle A point of the curve is the sum of its control points weighted by the Bernstein polynomials $(1 - t)^3$, $3t(1 - t)^2$, $3t^2(1 - t)$ and $t^3$, so its distance from the center is known at every $t$. At the middle, $t = 1/2$, the curve is on the circle. Everywhere else it is outside, most at $t \approx 0.21$ and $0.79$ (Figure 2): ```python t = np.linspace(0, 1, 20001) bernstein = np.array([(1 - t)**3, 3 * t * (1 - t)**2, 3 * t**2 * (1 - t), t**3]).T def deviation(k): """the distance from the center along the quarter, relative to r, less 1""" return np.hypot(*(bernstein @ quarter(k)).T) / r - 1 d = deviation(k) print("middle", d[10000], "| largest", d.max(), "at t", t[d.argmax()], "| smallest", d.min()) ``` ## A better handle The largest deviation is smallest when the curve runs as far inside the circle as outside. SciPy's `minimize_scalar` finds that handle. It brings the largest deviation down by a quarter and the area error by more than two thirds (Figure 2): ```python best = minimize_scalar(lambda k: abs(deviation(k)).max(), bounds=(0.54, 0.56), method="bounded", options={"xatol": 1e-12}) d_best = deviation(best.x) print("k", best.x, "| largest", d_best.max(), "| smallest", d_best.min()) print("area relative", circle(best.x).mass / (math.pi * r**2) - 1) # plt.plot(t, 100 * d, t, 100 * d_best); plt.show() ``` ```python nbsphinx="hide-input" Chart("t along the quarter", "Distance from the circle in %", lines={f"k = {k:.4f}, the middle on the circle": (t, 100 * d), f"k = {best.x:.4f}, the smallest largest distance": (t, 100 * d_best)}, title="How far a cubic quarter runs from the circle") ``` ## The exact quarter A rational B-spline divides by weights. The quadratic quarter with the control points $(r, 0)$, $(r, r)$, $(0, r)$ and the weights $1$, $\sqrt{2}/2$, $1$ is exactly the circle. planex2d measures its length and the area inside four of them from the exact curves: ```python w = math.sqrt(2) / 2 exact = [px.BSpline(turned([(r, 0), (r, r), (0, r)], j), [0, 0, 0, 1, 1, 1], 2, weights=[1, w, 1]) for j in range(4)] print("quarter length", exact[0].mass, "| π r / 2", math.pi * r / 2) print("area", px.Face(exact).mass, "| π r²", math.pi * r**2) ``` See [Splines](../core/basics/splines.md) for `px.BSpline` and rational splines. ```python nbsphinx="hidden" import sys import matplotlib.pyplot as plt sys.path.insert(0, "..") from docs_figures import FACE, INK, PLANEX, SOFT from gallery import save_figure theta = np.linspace(0, 2 * np.pi, 2000) fig, ax = plt.subplots(figsize=(6, 4)) ax.plot(r * np.cos(theta), r * np.sin(theta), color=SOFT, lw=1.0, ls=(0, (4, 3))) for j in range(4): scaled = (bernstein @ quarter(k)) * (1 + 300 * d)[:, None] xs, ys = np.array(turned(scaled, j)).T ax.fill(np.r_[xs, 0], np.r_[ys, 0], color=FACE, lw=0) ax.plot(xs, ys, color=PLANEX, lw=2.2) poles = np.array(turned(quarter(k), j)) ax.plot(*poles.T, color=INK, lw=0.9, ls=(0, (3, 2))) ax.scatter(*poles.T, s=22, color=INK, zorder=5) ax.set_xlim(-18.6, 18.6) ax.set_ylim(-12.4, 12.4) ax.set_aspect("equal") ax.axis("off") save_figure(fig, "bezier-circle") plt.close(fig) ```