#!/usr/bin/env dub
/+ dub.sdl:
name "manim_affine_transform"
targetPath "build"
+/
/**
* 2D affine transforms as 3×3 homogeneous matrices: compose translate ·
* rotate · scale into ONE matrix, apply it to a Bézier control polygon, and
* verify that the single composed matrix equals applying the three transforms
* sequentially.
*
* The *object & scene model* axis (coordinate space) of the analysis spine.
* Every mobject carries a model transform; `Transform`/`ApplyMatrix` and the
* camera all reduce to affine maps on control points. The load-bearing fact a
* reimplementation must respect is composition order: `M = T·R·S` applied once
* is identical to scaling, then rotating, then translating each point in turn
* — matrix multiply *is* function composition, right-to-left. This probe
* proves it numerically (max deviation 0 within fp epsilon), which is why the
* proposal builds a single `Affine2` compose primitive rather than mutating
* points per operation.
*
* It also exercises exactly the `Matrix`/`Affine2` primitive M1 of the
* proposal adds to `libs/math` on top of the existing `Vector` type.
*
* Companion to docs/research/manim/concepts.md § "Affine transform" and
* docs/research/manim/manim-community/scene-graph.md § "Coordinate space".
* Run with: dub run --single affine-transform.d
*
* Portability: pure computation, no external dependencies — runs everywhere.
*/
module (module) manim_affine_transform2D affine transforms as 3×3 homogeneous matrices: compose translate ·
rotate · scale into ONE matrix, apply it to a Bézier control polygon, and
verify that the single composed matrix equals applying the three transforms
sequentially.
The object & scene model axis (coordinate space) of the analysis spine.
Every mobject carries a model transform; Transform/ApplyMatrix and the
camera all reduce to affine maps on control points. The load-bearing fact a
reimplementation must respect is composition order: M = T·R·S applied once
is identical to scaling, then rotating, then translating each point in turn
— matrix multiply is function composition, right-to-left. This probe
proves it numerically (max deviation 0 within fp epsilon), which is why the
proposal builds a single Affine2 compose primitive rather than mutating
points per operation.
It also exercises exactly the Matrix/Affine2 primitive M1 of the
proposal adds to libs/math on top of the existing Vector type.
Companion to docs/research/manim/concepts.md § "Affine transform" and
docs/research/manim/manim-community/scene-graph.md § "Coordinate space".
Run with: dub run --single affine-transform.d
Portability
pure computation, no external dependencies — runs everywhere.
manim_affine_transform;
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_affine_transform.cos = real std.math.trigonometry.cos(real x) pure nothrow @nogc @safeReturns cosine of x. x is in radians.
x cos(x) invalid? yes yes
cos, (alias constant) manim_affine_transform.PI = real std.math.constants.PI = 3.14159Lπ = 3.141592...
PI, (alias) manim_affine_transform.sin = real std.math.trigonometry.sin(real x) pure nothrow @nogc @safesin, (alias) manim_affine_transform.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_affine_transform.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))Equivalent to writef(fmt, args, '\n').
writefln, (alias template) manim_affine_transform.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;
/// Row-major 3×3 affine matrix.
struct (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3
{
double[3][3] (field) double[3][3] manim_affine_transform.Mat3.mm;
static (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3 manim_affine_transform.Mat3 manim_affine_transform.Mat3.identity() pure nothrow @nogc @safeidentity() @safe pure nothrow @nogc
=> (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3([[1.0, 0, 0], [0.0, 1, 0], [0.0, 0, 1]]);
static (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3 manim_affine_transform.Mat3 manim_affine_transform.Mat3.translate(double tx, double ty) pure nothrow @nogc @safetranslate(double (parameter) double txtx, double (parameter) double tyty) @safe pure nothrow @nogc
=> (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3([[1.0, 0, (parameter) double txtx], [0.0, 1, (parameter) double tyty], [0.0, 0, 1]]);
static (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3 manim_affine_transform.Mat3 manim_affine_transform.Mat3.scale(double sx, double sy) pure nothrow @nogc @safescale(double (parameter) double sxsx, double (parameter) double sysy) @safe pure nothrow @nogc
=> (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3([[(parameter) double sxsx, 0, 0], [0.0, (parameter) double sysy, 0], [0.0, 0, 1]]);
static (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3 manim_affine_transform.Mat3 manim_affine_transform.Mat3.rotate(double rad) pure nothrow @nogc @saferotate(double (parameter) double radrad) @safe pure nothrow @nogc
{
const (local variable) const(double) cc = double std.math.trigonometry.cos(double x) pure nothrow @nogc @safeReturns cosine of x. x is in radians.
x cos(x) invalid? yes yes
cos((parameter) double radrad), (local variable) const(double) ss = double std.math.trigonometry.sin(double x) pure nothrow @nogc @safesin((parameter) double radrad);
return (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3([[(local variable) const(double) cc, -(local variable) const(double) ss, 0], [(local variable) const(double) ss, (local variable) const(double) cc, 0], [0.0, 0, 1]]);
}
/// Matrix product `this · rhs`.
(struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3 manim_affine_transform.Mat3 manim_affine_transform.Mat3.mul(in manim_affine_transform.Mat3 rhs) const pure nothrow @nogc @safeMatrix product this · rhs``.
mul(in (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3 (parameter) const(manim_affine_transform.Mat3) rhsrhs) const @safe pure nothrow @nogc
{
(struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3 (local variable) manim_affine_transform.Mat3 rr;
foreach ((local variable) int ii; 0 .. 3)
foreach ((local variable) int jj; 0 .. 3)
{
double (local variable) double accacc = 0;
foreach ((local variable) int kk; 0 .. 3)
(local variable) double accacc += (field) double[3][3] manim_affine_transform.Mat3.mm[(local variable) int ii][(local variable) int kk] * (parameter) const(manim_affine_transform.Mat3) rhsrhs.(field) double[3][3] manim_affine_transform.Mat3.mm[(local variable) int kk][(local variable) int jj];
(local variable) manim_affine_transform.Mat3 rr.(field) double[3][3] manim_affine_transform.Mat3.mm[(local variable) int ii][(local variable) int jj] = (local variable) double accacc;
}
return (local variable) manim_affine_transform.Mat3 rr;
}
/// Apply to a 2D point (homogeneous w = 1).
double[2] double[2] manim_affine_transform.Mat3.apply(in double[2] p) const pure nothrow @nogc @safeApply to a 2D point (homogeneous w = 1).
apply(in double[2] (parameter) const(double[2]) pp) const @safe pure nothrow @nogc
=> [(field) double[3][3] manim_affine_transform.Mat3.mm[0][0] * (parameter) const(double[2]) pp[0] + (field) double[3][3] manim_affine_transform.Mat3.mm[0][1] * (parameter) const(double[2]) pp[1] + (field) double[3][3] manim_affine_transform.Mat3.mm[0][2],
(field) double[3][3] manim_affine_transform.Mat3.mm[1][0] * (parameter) const(double[2]) pp[0] + (field) double[3][3] manim_affine_transform.Mat3.mm[1][1] * (parameter) const(double[2]) pp[1] + (field) double[3][3] manim_affine_transform.Mat3.mm[1][2]];
}
double double manim_affine_transform.dist(in double[2] a, in double[2] b) pure nothrow @nogc @safedist(in double[2] (parameter) const(double[2]) aa, in double[2] (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);
int int D main() @safemain() @safe
{
const (local variable) const(manim_affine_transform.Mat3) TT = (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3.manim_affine_transform.Mat3 manim_affine_transform.Mat3.translate(double tx, double ty) pure nothrow @nogc @safetranslate(2.0, -1.0);
const (local variable) const(manim_affine_transform.Mat3) RR = (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3.manim_affine_transform.Mat3 manim_affine_transform.Mat3.rotate(double rad) pure nothrow @nogc @saferotate((constant) real std.math.constants.PI = 3.14159Lπ = 3.141592...
PI / 6); // 30°
const (local variable) const(manim_affine_transform.Mat3) SS = (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3.manim_affine_transform.Mat3 manim_affine_transform.Mat3.scale(double sx, double sy) pure nothrow @nogc @safescale(1.5, 0.5);
const (local variable) const(manim_affine_transform.Mat3) MM = (local variable) const(manim_affine_transform.Mat3) TT.manim_affine_transform.Mat3 manim_affine_transform.Mat3.mul(in manim_affine_transform.Mat3 rhs) const pure nothrow @nogc @safeMatrix product this · rhs``.
mul((local variable) const(manim_affine_transform.Mat3) RR).manim_affine_transform.Mat3 manim_affine_transform.Mat3.mul(in manim_affine_transform.Mat3 rhs) const pure nothrow @nogc @safeMatrix product this · rhs``.
mul((local variable) const(manim_affine_transform.Mat3) SS); // T·R·S — scale first, then rotate, then translate
// A cubic control polygon.
const double[2][4] (local variable) const(double[2][4]) polypoly = [[0.0, 0], [1.0, 2], [2.0, -1], [3.0, 0]];
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("== M = T·R·S applied once vs S→R→T applied in sequence ==");
void std.stdio.writefln!char(in char[] fmt) @safeEquivalent to writef(fmt, args, '\n').
writefln(" control point via M via sequence |Δ|");
double (local variable) double maxDevmaxDev = 0;
foreach ((parameter) const(double[2]) pp; (local variable) const(double[2][4]) polypoly)
{
const (local variable) const(double[2]) viaMviaM = (local variable) const(manim_affine_transform.Mat3) MM.double[2] manim_affine_transform.Mat3.apply(in double[2] p) const pure nothrow @nogc @safeApply to a 2D point (homogeneous w = 1).
apply((local variable) const(double[2]) pp);
const (local variable) const(double[2]) viaSeqviaSeq = (local variable) const(manim_affine_transform.Mat3) TT.double[2] manim_affine_transform.Mat3.apply(in double[2] p) const pure nothrow @nogc @safeApply to a 2D point (homogeneous w = 1).
apply((local variable) const(manim_affine_transform.Mat3) RR.double[2] manim_affine_transform.Mat3.apply(in double[2] p) const pure nothrow @nogc @safeApply to a 2D point (homogeneous w = 1).
apply((local variable) const(manim_affine_transform.Mat3) SS.double[2] manim_affine_transform.Mat3.apply(in double[2] p) const pure nothrow @nogc @safeApply to a 2D point (homogeneous w = 1).
apply((local variable) const(double[2]) pp)));
const (local variable) const(double) dd = double manim_affine_transform.dist(in double[2] a, in double[2] b) pure nothrow @nogc @safedist((local variable) const(double[2]) viaMviaM, (local variable) const(double[2]) viaSeqviaSeq);
if ((local variable) const(double) dd > (local variable) double maxDevmaxDev)
(local variable) double maxDevmaxDev = (local variable) const(double) dd;
void std.stdio.writefln!(char, const(double), 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, const(double) __param_7) @safeEquivalent to writef(fmt, args, '\n').
writefln(" (%4.1f,%4.1f) (%7.4f,%7.4f) (%7.4f,%7.4f) %.2e",
(local variable) const(double[2]) pp[0], (local variable) const(double[2]) pp[1], (local variable) const(double[2]) viaMviaM[0], (local variable) const(double[2]) viaMviaM[1], (local variable) const(double[2]) viaSeqviaSeq[0], (local variable) const(double[2]) viaSeqviaSeq[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 deviation: %.2e (matrix product = right-to-left composition)", (local variable) double maxDevmaxDev);
// Order matters: T·S != S·T.
const (local variable) const(double[2]) tsts = (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3.manim_affine_transform.Mat3 manim_affine_transform.Mat3.translate(double tx, double ty) pure nothrow @nogc @safetranslate(2, 0).manim_affine_transform.Mat3 manim_affine_transform.Mat3.mul(in manim_affine_transform.Mat3 rhs) const pure nothrow @nogc @safeMatrix product this · rhs``.
mul((struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3.manim_affine_transform.Mat3 manim_affine_transform.Mat3.scale(double sx, double sy) pure nothrow @nogc @safescale(3, 3)).double[2] manim_affine_transform.Mat3.apply(in double[2] p) const pure nothrow @nogc @safeApply to a 2D point (homogeneous w = 1).
apply([1.0, 0]);
const (local variable) const(double[2]) stst = (struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3.manim_affine_transform.Mat3 manim_affine_transform.Mat3.scale(double sx, double sy) pure nothrow @nogc @safescale(3, 3).manim_affine_transform.Mat3 manim_affine_transform.Mat3.mul(in manim_affine_transform.Mat3 rhs) const pure nothrow @nogc @safeMatrix product this · rhs``.
mul((struct) manim_affine_transform.Mat3Row-major 3×3 affine matrix.
Mat3.manim_affine_transform.Mat3 manim_affine_transform.Mat3.translate(double tx, double ty) pure nothrow @nogc @safetranslate(2, 0)).double[2] manim_affine_transform.Mat3.apply(in double[2] p) const pure nothrow @nogc @safeApply to a 2D point (homogeneous w = 1).
apply([1.0, 0]);
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== non-commutativity: translate·scale vs scale·translate on (1,0) ==");
void std.stdio.writefln!(char, const(double), const(double))(in char[] fmt, const(double) __param_1, const(double) __param_2) @safeEquivalent to writef(fmt, args, '\n').
writefln(" T·S (scale then translate): (%.1f, %.1f)", (local variable) const(double[2]) tsts[0], (local variable) const(double[2]) tsts[1]);
void std.stdio.writefln!(char, const(double), const(double))(in char[] fmt, const(double) __param_1, const(double) __param_2) @safeEquivalent to writef(fmt, args, '\n').
writefln(" S·T (translate then scale): (%.1f, %.1f)", (local variable) const(double[2]) stst[0], (local variable) const(double[2]) stst[1]);
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(" (they differ — order is part of the transform, not incidental)");
return 0;
}