#!/usr/bin/env dub
/+ dub.sdl:
name "manim_bezier_eval"
targetPath "build"
+/
/**
* Quadratic (3-point) vs cubic (4-point) Bézier curves: de Casteljau
* evaluation, exact quadratic→cubic degree elevation, and the one-way
* conversion cost that a single quadratic pays approximating a cubic.
*
* The *object & scene model* axis of the analysis spine, exercised in code.
* The two Manim geometry camps disagree on the Bézier basis: ManimGL and
* community's OpenGL classes store curves as **quadratic** triples
* `(anchor, handle, anchor)`; community's Cairo classes store **cubic**
* quads `(anchor, handle, handle, anchor)`. The choice is load-bearing for a
* reimplementation because the conversion between the two bases is *not*
* symmetric:
*
* 1. A quadratic Bézier `Q(P0,P1,P2)` elevates to a cubic `C(C0..C3)`
* *exactly* — `C0=P0`, `C1=P0+2/3(P1-P0)`, `C2=P2+2/3(P1-P2)`, `C3=P2` —
* so the two curves are pointwise identical. This probe elevates a
* quadratic and prints the max sample deviation (0 within fp epsilon):
* quadratics are a strict subset of cubics.
* 2. The reverse is lossy: a general cubic has an inflection a single
* quadratic cannot follow. This probe fits the "best" single quadratic
* (sharing the cubic's endpoints, handle at the cubic control average)
* and prints the residual max deviation — the per-curve error the
* quadratic-canonical engines eat, and the reason a cubic-canonical
* store only makes the *GPU* backend lower curves (§ cubic-canonical).
*
* This is the concrete evidence behind the `manim-community/scene-graph.md`
* and `manimgl.md` geometry sections, and behind the proposal's decision to
* standardise on a cubic interchange basis.
*
* Companion to docs/research/manim/manim-community/scene-graph.md
* § "Bézier basis: cubic vs quadratic" and docs/research/manim/manimgl.md
* § "Quadratic curves in a structured array".
* Run with: dub run --single bezier-eval.d
*
* Portability: pure computation, no external dependencies or host
* capabilities — compiles and runs identically everywhere.
*/
module (module) manim_bezier_evalQuadratic (3-point) vs cubic (4-point) Bézier curves: de Casteljau
evaluation, exact quadratic→cubic degree elevation, and the one-way
conversion cost that a single quadratic pays approximating a cubic.
The object & scene model axis of the analysis spine, exercised in code.
The two Manim geometry camps disagree on the Bézier basis: ManimGL and
community's OpenGL classes store curves as quadratic triples
(anchor, handle, anchor); community's Cairo classes store cubic
quads (anchor, handle, handle, anchor). The choice is load-bearing for a
reimplementation because the conversion between the two bases is not
symmetric:
A quadratic Bézier Q(P0,P1,P2) elevates to a cubic C(C0..C3)
exactly — C0=P0, C1=P0+2/3(P1-P0), C2=P2+2/3(P1-P2), C3=P2 —
so the two curves are pointwise identical. This probe elevates a
quadratic and prints the max sample deviation (0 within fp epsilon):
quadratics are a strict subset of cubics.
The reverse is lossy: a general cubic has an inflection a single
quadratic cannot follow. This probe fits the "best" single quadratic
(sharing the cubic's endpoints, handle at the cubic control average)
and prints the residual max deviation — the per-curve error the
quadratic-canonical engines eat, and the reason a cubic-canonical
store only makes the GPU backend lower curves (§ cubic-canonical).
This is the concrete evidence behind the manim-community/scene-graph.md
and manimgl.md geometry sections, and behind the proposal's decision to
standardise on a cubic interchange basis.
Companion to docs/research/manim/manim-community/scene-graph.md
§ "Bézier basis: cubic vs quadratic" and docs/research/manim/manimgl.md
§ "Quadratic curves in a structured array".
Run with: dub run --single bezier-eval.d
Portability
pure computation, no external dependencies or host
capabilities — compiles and runs identically everywhere.
manim_bezier_eval;
import (package) stdstd.(module) std.mathContains the elementary mathematical functions (powers, roots,
and trigonometric functions), and low-level floating-point operations.
Mathematical special functions are available in std.mathspecial.
Category Members Constants E PI PI_2 PI4 M1_PI M2_PI M2_SQRTPI LN10 LN2 LOG2 LOG2E LOG2T LOG10E SQRT2 SQRT1_2 Algebraic abs fabs sqrt cbrt hypot poly nextPow2 truncPow2 Trigonometry sin cos tan asin acos atan atan2 sinh cosh tanh asinh acosh atanh Rounding ceil floor round lround trunc rint lrint nearbyint rndtol quantize Exponentiation & Logarithms pow powmod exp exp2 expm1 ldexp frexp log log2 log10 logb ilogb log1p scalbn Remainder fmod modf remainder remquo Floating-point operations approxEqual feqrel fdim fmax fmin fma isClose nextDown nextUp nextafter NaN getNaNPayload cmp Introspection isFinite isIdentical isInfinity isNaN isNormal isSubnormal signbit sgn copysign isPowerOf2 Hardware Control IeeeFlags ieeeFlags resetIeeeFlags FloatingPointControl
The functionality closely follows the IEEE754-2008 standard for
floating-point arithmetic, including the use of camelCase names rather
than C99-style lower case names. All of these functions behave correctly
when presented with an infinity or NaN.
The following IEEE 'real' formats are currently supported:
64 bit Big-endian 'double' (eg PowerPC)
128 bit Big-endian 'quadruple' (eg SPARC)
64 bit Little-endian 'double' (eg x86-SSE2)
80 bit Little-endian, with implied bit 'real80' (eg x87, Itanium)
128 bit Little-endian 'quadruple' (not implemented on any known processor!)
Non-IEEE 128 bit Big-endian 'doubledouble' (eg PowerPC) has partial support
Unlike C, there is no global 'errno' variable. Consequently, almost all of
these functions are pure nothrow.
Source
std/math/package.d
math : (alias) manim_bezier_eval.fabs = real std.math.algebraic.fabs(real x) pure nothrow @nogc @safeReturns |x|
x fabs(x) 0.0 +0.0 +
fabs, (alias) manim_bezier_eval.sqrt = float std.math.algebraic.sqrt(float x) pure nothrow @nogc @safeCompute square root of x.
x sqrt(x) invalid? -0.0 -0.0 no <0.0 yes + + no
sqrt;
import (package) stdstd.(module) std.stdioCategory Symbols File handles _popen File isFileHandle openNetwork stderr stdin stdout Reading chunks lines readf readfln readln Writing toFile write writef writefln writeln Misc KeepTerminator LockType StdioException
Standard I/O functions that extend core.stdc.stdio. core.stdc.stdio
is publically imported when importing std.stdio.
There are three layers of I/O:
The lowest layer is the operating system layer. The two main schemes are Windows and Posix.
C's stdio.h which unifies the two operating system schemes.
std.stdio, this module, unifies the various stdio.h implementations into
a high level package for D programs.
Source
std/stdio.d
stdio : (alias template) manim_bezier_eval.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))Equivalent to writef(fmt, args, '\n').
writefln, (alias template) manim_bezier_eval.writeln = std.stdio.writeln(T...)(T args)Equivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln;
alias (alias) manim_bezier_eval.P = double[2]P = double[2];
(alias) manim_bezier_eval.P = double[2]P double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp(in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) aa, in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) bb, double (parameter) double tt) @safe pure nothrow @nogc
=> [(parameter) const(double[2]) aa[0] + ((parameter) const(double[2]) bb[0] - (parameter) const(double[2]) aa[0]) * (parameter) double tt, (parameter) const(double[2]) aa[1] + ((parameter) const(double[2]) bb[1] - (parameter) const(double[2]) aa[1]) * (parameter) double tt];
/// de Casteljau for a quadratic (3 control points).
(alias) manim_bezier_eval.P = double[2]P double[2] manim_bezier_eval.quad(in double[2] p0, in double[2] p1, in double[2] p2, double t) pure nothrow @nogc @safede Casteljau for a quadratic (3 control points).
quad(in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) p0p0, in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) p1p1, in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) p2p2, double (parameter) double tt) @safe pure nothrow @nogc
{
const (local variable) const(double[2]) aa = double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp((parameter) const(double[2]) p0p0, (parameter) const(double[2]) p1p1, (parameter) double tt);
const (local variable) const(double[2]) bb = double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp((parameter) const(double[2]) p1p1, (parameter) const(double[2]) p2p2, (parameter) double tt);
return double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp((local variable) const(double[2]) aa, (local variable) const(double[2]) bb, (parameter) double tt);
}
/// de Casteljau for a cubic (4 control points).
(alias) manim_bezier_eval.P = double[2]P double[2] manim_bezier_eval.cubic(in double[2] p0, in double[2] p1, in double[2] p2, in double[2] p3, double t) pure nothrow @nogc @safede Casteljau for a cubic (4 control points).
cubic(in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) p0p0, in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) p1p1, in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) p2p2, in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) p3p3, double (parameter) double tt) @safe pure nothrow @nogc
{
const (local variable) const(double[2]) aa = double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp((parameter) const(double[2]) p0p0, (parameter) const(double[2]) p1p1, (parameter) double tt);
const (local variable) const(double[2]) bb = double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp((parameter) const(double[2]) p1p1, (parameter) const(double[2]) p2p2, (parameter) double tt);
const (local variable) const(double[2]) cc = double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp((parameter) const(double[2]) p2p2, (parameter) const(double[2]) p3p3, (parameter) double tt);
const (local variable) const(double[2]) dd = double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp((local variable) const(double[2]) aa, (local variable) const(double[2]) bb, (parameter) double tt);
const (local variable) const(double[2]) ee = double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp((local variable) const(double[2]) bb, (local variable) const(double[2]) cc, (parameter) double tt);
return double[2] manim_bezier_eval.lerp(in double[2] a, in double[2] b, double t) pure nothrow @nogc @safelerp((local variable) const(double[2]) dd, (local variable) const(double[2]) ee, (parameter) double tt);
}
double double manim_bezier_eval.dist(in double[2] a, in double[2] b) pure nothrow @nogc @safedist(in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) aa, in (alias) manim_bezier_eval.P = double[2]P (parameter) const(double[2]) bb) @safe pure nothrow @nogc
=> double std.math.algebraic.sqrt(double x) pure nothrow @nogc @safeCompute square root of x.
x sqrt(x) invalid? -0.0 -0.0 no <0.0 yes + + no
sqrt(((parameter) const(double[2]) aa[0] - (parameter) const(double[2]) bb[0]) ^^ 2 + ((parameter) const(double[2]) aa[1] - (parameter) const(double[2]) bb[1]) ^^ 2);
/// Polyline arc-length estimate over `n` samples.
double double manim_bezier_eval.arcLength(scope double[2] delegate(double) @safe f, ulong n = 256LU) @safePolyline arc-length estimate over n samples.
arcLength(scope (alias) manim_bezier_eval.P = double[2]P delegate(double) @safe (parameter) double[2] delegate(double) @safe ff, (alias) object.size_t = ulongsize_t (parameter) ulong nn = 256) @safe
{
double (local variable) double lenlen = 0;
(alias) manim_bezier_eval.P = double[2]P (local variable) double[2] prevprev = (parameter) double[2] delegate(double) @safe ff(0);
foreach ((local variable) ulong ii; 1 .. (parameter) ulong nn + 1)
{
const (local variable) const(double[2]) curcur = (parameter) double[2] delegate(double) @safe ff(cast(double) (local variable) ulong ii / (parameter) ulong nn);
(local variable) double lenlen += double manim_bezier_eval.dist(in double[2] a, in double[2] b) pure nothrow @nogc @safedist((local variable) double[2] prevprev, (local variable) const(double[2]) curcur);
(local variable) double[2] prevprev = (local variable) const(double[2]) curcur;
}
return (local variable) double lenlen;
}
int int D main() @safemain() @safe
{
// A quadratic and its EXACT cubic elevation.
const (alias) manim_bezier_eval.P = double[2]P (local variable) const(double[2]) q0q0 = [0.0, 0.0], (local variable) const(double[2]) q1q1 = [1.0, 2.0], (local variable) const(double[2]) q2q2 = [3.0, 0.0];
const (alias) manim_bezier_eval.P = double[2]P (local variable) const(double[2]) c0c0 = (local variable) const(double[2]) q0q0;
const (alias) manim_bezier_eval.P = double[2]P (local variable) const(double[2]) c1c1 = [(local variable) const(double[2]) q0q0[0] + 2.0 / 3 * ((local variable) const(double[2]) q1q1[0] - (local variable) const(double[2]) q0q0[0]), (local variable) const(double[2]) q0q0[1] + 2.0 / 3 * ((local variable) const(double[2]) q1q1[1] - (local variable) const(double[2]) q0q0[1])];
const (alias) manim_bezier_eval.P = double[2]P (local variable) const(double[2]) c2c2 = [(local variable) const(double[2]) q2q2[0] + 2.0 / 3 * ((local variable) const(double[2]) q1q1[0] - (local variable) const(double[2]) q2q2[0]), (local variable) const(double[2]) q2q2[1] + 2.0 / 3 * ((local variable) const(double[2]) q1q1[1] - (local variable) const(double[2]) q2q2[1])];
const (alias) manim_bezier_eval.P = double[2]P (local variable) const(double[2]) c3c3 = (local variable) const(double[2]) q2q2;
void std.stdio.writeln!string(string __param_0) @safeEquivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln("== quadratic (3-pt) vs its exact cubic (4-pt) elevation ==");
void std.stdio.writefln!(char, const(double[2]), const(double[2]), const(double[2]))(in char[] fmt, const(double[2]) __param_1, const(double[2]) __param_2, const(double[2]) __param_3) @safeEquivalent to writef(fmt, args, '\n').
writefln(" quadratic control: %s %s %s", (local variable) const(double[2]) q0q0, (local variable) const(double[2]) q1q1, (local variable) const(double[2]) q2q2);
void std.stdio.writefln!(char, const(double[2]), const(double[2]), const(double[2]), const(double[2]))(in char[] fmt, const(double[2]) __param_1, const(double[2]) __param_2, const(double[2]) __param_3, const(double[2]) __param_4) @safeEquivalent to writef(fmt, args, '\n').
writefln(" elevated cubic : %s %s %s %s", (local variable) const(double[2]) c0c0, (local variable) const(double[2]) c1c1, (local variable) const(double[2]) c2c2, (local variable) const(double[2]) c3c3);
void std.stdio.writefln!char(in char[] fmt) @safeEquivalent to writef(fmt, args, '\n').
writefln(" t quad(t) cubic(t) |Δ|");
double (local variable) double elevMaxelevMax = 0;
foreach ((local variable) int ii; 0 .. 11)
{
const (local variable) const(double) tt = (local variable) int ii / 10.0;
const (local variable) const(double[2]) aa = double[2] manim_bezier_eval.quad(in double[2] p0, in double[2] p1, in double[2] p2, double t) pure nothrow @nogc @safede Casteljau for a quadratic (3 control points).
quad((local variable) const(double[2]) q0q0, (local variable) const(double[2]) q1q1, (local variable) const(double[2]) q2q2, (local variable) const(double) tt);
const (local variable) const(double[2]) bb = double[2] manim_bezier_eval.cubic(in double[2] p0, in double[2] p1, in double[2] p2, in double[2] p3, double t) pure nothrow @nogc @safede Casteljau for a cubic (4 control points).
cubic((local variable) const(double[2]) c0c0, (local variable) const(double[2]) c1c1, (local variable) const(double[2]) c2c2, (local variable) const(double[2]) c3c3, (local variable) const(double) tt);
const (local variable) const(double) dd = double manim_bezier_eval.dist(in double[2] a, in double[2] b) pure nothrow @nogc @safedist((local variable) const(double[2]) aa, (local variable) const(double[2]) bb);
if ((local variable) const(double) dd > (local variable) double elevMaxelevMax)
(local variable) double elevMaxelevMax = (local variable) const(double) dd;
void std.stdio.writefln!(char, const(double), const(double), const(double), const(double), const(double), const(double))(in char[] fmt, const(double) __param_1, const(double) __param_2, const(double) __param_3, const(double) __param_4, const(double) __param_5, const(double) __param_6) @safeEquivalent to writef(fmt, args, '\n').
writefln(" %4.1f (%7.4f, %7.4f) (%7.4f, %7.4f) %.2e", (local variable) const(double) tt, (local variable) const(double[2]) aa[0], (local variable) const(double[2]) aa[1], (local variable) const(double[2]) bb[0], (local variable) const(double[2]) bb[1], (local variable) const(double) dd);
}
void std.stdio.writefln!(char, double)(in char[] fmt, double __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln(" max elevation deviation: %.2e (quadratics are a strict subset of cubics)", (local variable) double elevMaxelevMax);
// A general cubic with an inflection, and the best single quadratic
// through its endpoints (handle at the average of the two cubic handles).
const (alias) manim_bezier_eval.P = double[2]P (local variable) const(double[2]) g0g0 = [0.0, 0.0], (local variable) const(double[2]) g1g1 = [1.0, 3.0], (local variable) const(double[2]) g2g2 = [2.0, -3.0], (local variable) const(double[2]) g3g3 = [3.0, 0.0];
const (alias) manim_bezier_eval.P = double[2]P (local variable) const(double[2]) hh = [((local variable) const(double[2]) g1g1[0] + (local variable) const(double[2]) g2g2[0]) / 2, ((local variable) const(double[2]) g1g1[1] + (local variable) const(double[2]) g2g2[1]) / 2];
void std.stdio.writeln!string(string __param_0) @safeEquivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln("\n== a cubic with an inflection, approximated by ONE quadratic ==");
void std.stdio.writefln!(char, const(double[2]), const(double[2]), const(double[2]), const(double[2]))(in char[] fmt, const(double[2]) __param_1, const(double[2]) __param_2, const(double[2]) __param_3, const(double[2]) __param_4) @safeEquivalent to writef(fmt, args, '\n').
writefln(" cubic control : %s %s %s %s", (local variable) const(double[2]) g0g0, (local variable) const(double[2]) g1g1, (local variable) const(double[2]) g2g2, (local variable) const(double[2]) g3g3);
void std.stdio.writefln!(char, const(double[2]))(in char[] fmt, const(double[2]) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln(" quad handle (avg of cubic handles): %s", (local variable) const(double[2]) hh);
double (local variable) double fitMaxfitMax = 0;
foreach ((local variable) int ii; 0 .. 11)
{
const (local variable) const(double) tt = (local variable) int ii / 10.0;
const (local variable) const(double) dd = double manim_bezier_eval.dist(in double[2] a, in double[2] b) pure nothrow @nogc @safedist(double[2] manim_bezier_eval.cubic(in double[2] p0, in double[2] p1, in double[2] p2, in double[2] p3, double t) pure nothrow @nogc @safede Casteljau for a cubic (4 control points).
cubic((local variable) const(double[2]) g0g0, (local variable) const(double[2]) g1g1, (local variable) const(double[2]) g2g2, (local variable) const(double[2]) g3g3, (local variable) const(double) tt), double[2] manim_bezier_eval.quad(in double[2] p0, in double[2] p1, in double[2] p2, double t) pure nothrow @nogc @safede Casteljau for a quadratic (3 control points).
quad((local variable) const(double[2]) g0g0, (local variable) const(double[2]) hh, (local variable) const(double[2]) g3g3, (local variable) const(double) tt));
if ((local variable) const(double) dd > (local variable) double fitMaxfitMax)
(local variable) double fitMaxfitMax = (local variable) const(double) dd;
}
void std.stdio.writefln!(char, double)(in char[] fmt, double __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln(" max approximation error: %.4f (the per-curve cost a quadratic-only", (local variable) double fitMaxfitMax);
void std.stdio.writefln!char(in char[] fmt) @safeEquivalent to writef(fmt, args, '\n').
writefln(" store pays; a cubic store lowers to quads only on the GPU backend)");
void std.stdio.writefln!(char, double, double)(in char[] fmt, double __param_1, double __param_2) @safeEquivalent to writef(fmt, args, '\n').
writefln("\n arc length: quadratic %.4f, inflected cubic %.4f",
double manim_bezier_eval.arcLength(scope double[2] delegate(double) @safe f, ulong n = 256LU) @safePolyline arc-length estimate over n samples.
arcLength((double (parameter) double tt) @safe => double[2] manim_bezier_eval.quad(in double[2] p0, in double[2] p1, in double[2] p2, double t) pure nothrow @nogc @safede Casteljau for a quadratic (3 control points).
quad((local variable) const(double[2]) q0q0, (local variable) const(double[2]) q1q1, (local variable) const(double[2]) q2q2, (parameter) double tt)),
double manim_bezier_eval.arcLength(scope double[2] delegate(double) @safe f, ulong n = 256LU) @safePolyline arc-length estimate over n samples.
arcLength((double (parameter) double tt) @safe => double[2] manim_bezier_eval.cubic(in double[2] p0, in double[2] p1, in double[2] p2, in double[2] p3, double t) pure nothrow @nogc @safede Casteljau for a cubic (4 control points).
cubic((local variable) const(double[2]) g0g0, (local variable) const(double[2]) g1g1, (local variable) const(double[2]) g2g2, (local variable) const(double[2]) g3g3, (parameter) double tt)));
return 0;
}