Skip to content

Instantly share code, notes, and snippets.

@ColinMoldenhauer
Last active February 27, 2025 21:29
Show Gist options
  • Save ColinMoldenhauer/485c3496effb351af8a07ed4a278e8ac to your computer and use it in GitHub Desktop.
Save ColinMoldenhauer/485c3496effb351af8a07ed4a278e8ac to your computer and use it in GitHub Desktop.
LaTeX TikZ Bézier curves of arbitrary degree
import string
from math import comb
def bernstein(n, i, var="t"):
"""Return the Bernstein polynomial term as a string."""
return f"{comb(n, i)}*(1-{var})^({n-i})*{var}^({i})"
def generate_bezier_latex(
nodes,
samples=100,
domain=(0, 1),
width="10cm",
height="10cm",
anchors={},
use_commands=True,
var="t"
):
r"""
Generates LaTeX TikZ code for an arbitrary Bézier curve given an array of control points.
TikZ natively supports Bézier curves until 3rd degree (start, end, 2 control points):
```
\draw(p0)..controls (p1) and (p2)..(p3);
```
Any higher order Bézier curve needs to be computed and drawn manually.
$$B(t) = \sum_{i=0}^{n} B_i^n(t) P_i$$
with
$$B_i^n(t) = \binom{n}{i} (1 - t)^{n-i} t^i$$
Parameters
----------
nodes : np.ndarray or list
- if `np.ndarray` array of shape (2, N), as used as input for python package `bezier`
- if `list` list of coordinate tuples, i.e. `[(x0, y0), ..., (xN, yN)]`
samples : int
At how many points to evaluate the Bézier curve
domain : tuple
2-tuple indicating start and end value of curve parameter `t`
width : str
Width of plot
height : str
Height of plot
anchors : dict, optional
Dictionary mapping index of control point `i` to the anchor position for the point label
use_commands : bool, optional
- if `True`
generate a macro for each control point coordinate and insert the macro at coordinate locations. Useful to manually change the coordinates without rerunning this function
- if `False`
insert the node coordinate values directly at coordinate locations
var : str
Variable name for Bézier parameter
Returns
-------
str
LaTeX TikZ code drawing an arbitrary Bézier curve
"""
if isinstance(nodes, np.ndarray):
coords = [(x_, y_) for x_, y_ in nodes.T]
else:
coords = nodes
n = len(coords) - 1 # degree of the Bézier curve
indent = "\t"
def get_id(idx, alphabet=string.ascii_uppercase):
"""Get a unique coordinate ID by cycling `alphabet`."""
result = ""
idx += 1
while idx > 0:
idx, remainder = divmod(idx - 1, len(alphabet))
result += alphabet[remainder]
return result[::-1]
def format_coordinate(i, x=None, y=None):
"""
Format a coordinate either by inserting its value `x` or `y` or
by inserting the coordinate's macro.
"""
assert (x is None) ^ (y is None)
if use_commands:
coord = "X" if x is not None else "Y"
return f"\\bezier{coord}{get_id(i)}"
else:
return x or y
# define control point values and coordinates as macros
if use_commands:
s_def = f"{indent}% Define control point coordinates as commands\n"
for i, (x, y) in enumerate(coords):
namex = format_coordinate(i, x=x)
namey = format_coordinate(i, y=y)
s_def += f"{indent}\\def{namex}{{{x}}}\n"
s_def += f"{indent}\\def{namey}{{{y}}}\n"
else:
s_def = ""
# Build the expressions for the x(t) and y(t) components:
terms_x = []
terms_y = []
for i, (x, y) in enumerate(coords):
terms_x.append(f"({bernstein(n, i, var)})*({format_coordinate(i, x=x)})")
terms_y.append(f"({bernstein(n, i, var)})*({format_coordinate(i, y=y)})")
bezier_x = " + ".join(terms_x)
bezier_y = " + ".join(terms_y)
# Generate code for plotting the control points:
control_points_code = "\n".join(
f"{indent}{indent}\\addplot[only marks, mark=*, red] coordinates {{({format_coordinate(i, x=x)},{format_coordinate(i, y=y)})}};"
for i, (x, y) in enumerate(coords)
)
# Generate code for labeling the control points:
control_labels_code = "\n".join(
f"{indent}{indent}\\node[anchor={anchors.get(i, 'north east')}] at ({format_coordinate(i, x=x)},{format_coordinate(i, y=y)}) {{\\texttt{{P{i}}} ({format_coordinate(i, x=x)},{format_coordinate(i, y=y)})}};"
for i, (x, y) in enumerate(coords)
)
# Build the full LaTeX code using pgfplots
latex_code = f"""\\documentclass{{article}}
\\usepackage{{pgfplots}}
\\pgfplotsset{{compat=1.17}}
\\begin{{document}}
\\begin{{tikzpicture}}
{s_def}
\\begin{{axis}}[
axis lines = middle,
xlabel = {{\\(x\\)}},
ylabel = {{\\(y\\)}},
width={width},
height={height},
samples={samples},
domain={domain[0]}:{domain[1]},
variable={var}
]
% Plot the Bézier curve
\\addplot[
thick, blue
] (
{{ {bezier_x} }},
{{ {bezier_y} }}
);
% Plot control points
{control_points_code}
% Label control points
{control_labels_code}
\\end{{axis}}
\\end{{tikzpicture}}
\\end{{document}}
"""
return latex_code
# Example usage:
control_points = np.round(np.random.rand(2, 10), 2)
latex_code = generate_bezier_latex(control_points)
@ColinMoldenhauer
Copy link
Author

ColinMoldenhauer commented Feb 19, 2025

Output:

summary
\documentclass{article}
\usepackage{pgfplots}
\pgfplotsset{compat=1.17}
\begin{document}
\begin{tikzpicture}
	% Define control point coordinates as commands
	\def\bezierXA{0.94}
	\def\bezierYA{0.96}
	\def\bezierXB{0.27}
	\def\bezierYB{0.89}
	\def\bezierXC{0.75}
	\def\bezierYC{0.53}
	\def\bezierXD{0.73}
	\def\bezierYD{0.03}
	\def\bezierXE{0.46}
	\def\bezierYE{0.09}
	\def\bezierXF{0.82}
	\def\bezierYF{0.04}
	\def\bezierXG{0.38}
	\def\bezierYG{0.98}
	\def\bezierXH{0.53}
	\def\bezierYH{0.34}
	\def\bezierXI{0.76}
	\def\bezierYI{0.88}
	\def\bezierXJ{0.3}
	\def\bezierYJ{0.26}

    \begin{axis}[
        axis lines = middle,
        xlabel = {\(x\)},
        ylabel = {\(y\)},
        xmin=0, xmax=1.,
        ymin=0, ymax=1.,
        width=10cm,
        height=10cm,
        samples=100,
        domain=0:1,
        variable=t
    ]
        % Plot the Bézier curve
        \addplot[
            thick, blue
        ] (
            { (1*(1-t)^(9)*t^(0))*(\bezierXA) + (9*(1-t)^(8)*t^(1))*(\bezierXB) + (36*(1-t)^(7)*t^(2))*(\bezierXC) + (84*(1-t)^(6)*t^(3))*(\bezierXD) + (126*(1-t)^(5)*t^(4))*(\bezierXE) + (126*(1-t)^(4)*t^(5))*(\bezierXF) + (84*(1-t)^(3)*t^(6))*(\bezierXG) + (36*(1-t)^(2)*t^(7))*(\bezierXH) + (9*(1-t)^(1)*t^(8))*(\bezierXI) + (1*(1-t)^(0)*t^(9))*(\bezierXJ) },
            { (1*(1-t)^(9)*t^(0))*(\bezierYA) + (9*(1-t)^(8)*t^(1))*(\bezierYB) + (36*(1-t)^(7)*t^(2))*(\bezierYC) + (84*(1-t)^(6)*t^(3))*(\bezierYD) + (126*(1-t)^(5)*t^(4))*(\bezierYE) + (126*(1-t)^(4)*t^(5))*(\bezierYF) + (84*(1-t)^(3)*t^(6))*(\bezierYG) + (36*(1-t)^(2)*t^(7))*(\bezierYH) + (9*(1-t)^(1)*t^(8))*(\bezierYI) + (1*(1-t)^(0)*t^(9))*(\bezierYJ) }
        );

        % Plot control points
		\addplot[only marks, mark=*, red] coordinates {(\bezierXA,\bezierYA)};
		\addplot[only marks, mark=*, red] coordinates {(\bezierXB,\bezierYB)};
		\addplot[only marks, mark=*, red] coordinates {(\bezierXC,\bezierYC)};
		\addplot[only marks, mark=*, red] coordinates {(\bezierXD,\bezierYD)};
		\addplot[only marks, mark=*, red] coordinates {(\bezierXE,\bezierYE)};
		\addplot[only marks, mark=*, red] coordinates {(\bezierXF,\bezierYF)};
		\addplot[only marks, mark=*, red] coordinates {(\bezierXG,\bezierYG)};
		\addplot[only marks, mark=*, red] coordinates {(\bezierXH,\bezierYH)};
		\addplot[only marks, mark=*, red] coordinates {(\bezierXI,\bezierYI)};
		\addplot[only marks, mark=*, red] coordinates {(\bezierXJ,\bezierYJ)};

        % Label control points
		\node[anchor=north east] at (\bezierXA,\bezierYA) {\texttt{P0} (\bezierXA,\bezierYA)};
		\node[anchor=north east] at (\bezierXB,\bezierYB) {\texttt{P1} (\bezierXB,\bezierYB)};
		\node[anchor=north east] at (\bezierXC,\bezierYC) {\texttt{P2} (\bezierXC,\bezierYC)};
		\node[anchor=north east] at (\bezierXD,\bezierYD) {\texttt{P3} (\bezierXD,\bezierYD)};
		\node[anchor=north east] at (\bezierXE,\bezierYE) {\texttt{P4} (\bezierXE,\bezierYE)};
		\node[anchor=north east] at (\bezierXF,\bezierYF) {\texttt{P5} (\bezierXF,\bezierYF)};
		\node[anchor=north east] at (\bezierXG,\bezierYG) {\texttt{P6} (\bezierXG,\bezierYG)};
		\node[anchor=north east] at (\bezierXH,\bezierYH) {\texttt{P7} (\bezierXH,\bezierYH)};
		\node[anchor=north east] at (\bezierXI,\bezierYI) {\texttt{P8} (\bezierXI,\bezierYI)};
		\node[anchor=north east] at (\bezierXJ,\bezierYJ) {\texttt{P9} (\bezierXJ,\bezierYJ)};

    \end{axis}
\end{tikzpicture}
\end{document}

@ColinMoldenhauer
Copy link
Author

For random points, generates:
grafik

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