create.math¶
Maths: Point2D, Vector2D and Matrix, Random and Noise, easing and Tween, scalar helpers, PI and TAU, and the common std.math functions.
lerp¶
def lerp(a: Float64, b: Float64, t: Float64) -> Float64
Blend from a to b, t running 0 to 1. Not clamped: t outside that range extrapolates past either end.
map¶
def map(
value: Float64,
in_low: Float64,
in_high: Float64,
out_low: Float64,
out_high: Float64,
) -> Float64
Re-scale value from one range onto another — a slider position to a world coordinate, say. Linear and unclamped.
inverse_lerp¶
def inverse_lerp(value: Float64, low: Float64, high: Float64) -> Float64
Where value sits in [low, high] as a 0-to-1 fraction. The inverse of lerp, and map with a 0..1 output range.
smoothstep¶
def smoothstep(edge0: Float64, edge1: Float64, x: Float64) -> Float64
Like inverse_lerp, but clamped to 0..1 and eased at both ends — the usual choice for a transition that should not start or stop abruptly.
sign¶
def sign(x: Float64) -> Float64
-1.0, 0.0 or 1.0 by the sign of x. Zero is its own case, not positive.
fractional_part¶
def fractional_part(x: Float64) -> Float64
The part after the decimal point, always positive: fractional_part(-0.25) is 0.75, since it floors rather than truncating.
wrap¶
def wrap(value: Float64, period: Float64) -> Float64
value wrapped into [0, period). Floored, so a negative value wraps forward instead of staying negative — what an angle or a scrolling offset wants.
degrees¶
def degrees(radians: Float64) -> Float64
radians in degrees. Rotations take radians, so this is for display.
radians¶
def radians(degrees: Float64) -> Float64
degrees in radians, for feeding rotate an angle written in degrees.
Easing¶
struct Easing
Which curve ease applies. A Tween stores one as a field.
A wrapped Int rather than a bare one so a stray number cannot be passed
where a curve is expected.
Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.
Constants¶
LINEAR = Easing(Int(0)): No easing — the fraction passes through unchanged.IN_QUAD = Easing(Int(1))OUT_QUAD = Easing(Int(2))IN_OUT_QUAD = Easing(Int(3))IN_CUBIC = Easing(Int(4))OUT_CUBIC = Easing(Int(5))IN_OUT_CUBIC = Easing(Int(6))IN_SINE = Easing(Int(7))OUT_SINE = Easing(Int(8))IN_OUT_SINE = Easing(Int(9))IN_EXPO = Easing(Int(10))OUT_EXPO = Easing(Int(11))IN_OUT_EXPO = Easing(Int(12))IN_BACK = Easing(Int(13))OUT_BACK = Easing(Int(14))IN_OUT_BACK = Easing(Int(15))IN_ELASTIC = Easing(Int(16))OUT_ELASTIC = Easing(Int(17))IN_OUT_ELASTIC = Easing(Int(18))IN_BOUNCE = Easing(Int(19))OUT_BOUNCE = Easing(Int(20))IN_OUT_BOUNCE = Easing(Int(21))
Fields¶
value: Int
Constructors¶
@staticmethod
def __init__(out self, value: Int)
__eq__¶
def __eq__(self, other: Self) -> Bool
__ne__¶
def __ne__(self, other: Self) -> Bool
ease¶
def ease(curve: Easing, t: Float64) -> Float64
Reshape the fraction t by curve.
t is clamped to 0..1 first — unlike lerp, easing does not extrapolate.
The curves are not linear, so an out-of-range input does not continue the
motion sensibly: IN_EXPO at t = 2.0 would return 1024.
The BACK and ELASTIC curves deliberately leave 0..1 in the middle of
their run, overshooting the target before settling. Anything consuming the
result as a colour channel or an index has to clamp it.
Tween¶
struct Tween
A value moving from one number to another over a fixed duration.
A tween is a playhead, in the same sense as Animator: the program
owns one per thing being animated and ticks it once per frame with the
frame delta.
@staticmethod
def create(mut context: Context, mut canvas: Canvas) raises -> Self:
return Self(Tween(0.0, 1.0, 0.4, Easing.OUT_CUBIC))
def update(mut self, mut context: Context, mut canvas: Canvas) raises:
self.fade.update(context.time.delta)
if context.keyboard.pressed(Key.SPACE):
self.fade.play()
canvas.fill(Color(255, 255, 255, Int(255 * self.fade.value())))
Unlike an animation, a tween carries no shared artwork -- its whole
definition is four numbers -- so there is no asset to hold by ArcPointer
and every entity simply owns its own.
Tweening something that is not a number. Tween(duration) runs 0 to 1,
which is exactly what Vector2D.lerp and Color.lerp take, so one scalar
tween drives a position or a colour without a second type:
self.slide = Tween(0.3, Easing.OUT_CUBIC) # in create
...
self.position = self.from_position.lerp(self.to_position, self.slide.value())
Constructed stopped at start -- nothing moves until play, loop or
ping_pong. The verbs mean what they mean on Animator: play
always restarts from the beginning, pause freezes where it is and
resume continues from there.
Implements Copyable, Deinitable, ImplicitlyCopyable, Movable, Writable.
Fields¶
start: Float64: The value atprogress0.end: Float64: The value atprogress1.duration: Float64: Seconds for a full run, start to end.curve: Easing: The easing curve applied toprogressto getvalue.
Constructors¶
@staticmethod
def __init__(
out self,
start: Float64,
end: Float64,
duration: Float64,
curve: Easing = Easing.LINEAR,
)
A tween from start to end over duration seconds.
@staticmethod
def __init__(out self, duration: Float64, curve: Easing = Easing.LINEAR)
A tween from 0 to 1 -- the fraction Vector2D.lerp and Color.lerp take, and the form to reach for when animating anything that is not a plain number.
value¶
def value(self) -> Float64
The eased value, between start and end. Read this after update.
progress¶
def progress(self) -> Float64
How far through the duration, 0 to 1, before easing.
The raw time fraction, so it is what a progress bar wants to render --
and what value is derived from. While ping-ponging it runs back down
to 0.
play¶
def play(mut self)
Run once from the start, holding end at the finish.
loop¶
def loop(mut self)
Run from the start, snapping back and repeating forever.
The snap is visible whenever start and end differ, which is what
ping_pong exists to avoid -- reach for this only when the value wraps
naturally, like a rotation through a full turn.
ping_pong¶
def ping_pong(mut self)
Run to end, back to start, forever -- the usual way to loop a tween, since it never snaps.
progress runs back down on the return leg, so the easing curve is
travelled backwards too and a Easing.OUT_CUBIC pulse decelerates into
both ends rather than into one.
pause¶
def pause(mut self)
Freeze at the current value. resume continues from here.
resume¶
def resume(mut self)
Continue a paused tween without rewinding.
stop¶
def stop(mut self)
Halt and rewind, putting value back to start.
update¶
def update(mut self, dt: Float64)
Advance the playhead by dt seconds. Call once per frame.
Nothing else moves a tween, so a tween never ticked is a tween stuck at
start -- the same failure mode as an Animator that is never
updated.
is_playing¶
def is_playing(self) -> Bool
True while the playhead is advancing -- false when stopped, paused or finished.
is_finished¶
def is_finished(self) -> Bool
True once a play reached end. Never true for loop or ping_pong, which have no end to reach.
Point2D¶
struct Point2D
A location in world space: distance, lerp, the affine operators, and an explicit constructor from a Vector2D for crossing between the two.
Every position in the library is one of these —
canvas.circle(position, r), Rectangle.center(), context.mouse.position — and no
signature takes a location as a loose x/y pair instead. The tuple constructors are @implicit, so
canvas.circle((0, 0), 20) works without naming the type and a program
only spells Point2D when it is storing one. It is the only type here
that converts from a bare tuple, which is what keeps that unambiguous.
The surface is strictly affine, which is the whole point of the type
existing beside Vector2D: subtracting two positions gives the
displacement between them (Vector2D), and adding a displacement to a
position gives another position. magnitude, normalize, dot, scalar *,
unary - and Point2D + Point2D are absent on purpose — none of them
means anything for a location, and position.normalize() compiling was the
mistake this split exists to catch. A program that genuinely wants a
position's components as a direction says so: Vector2D(p).
A displacement is always named: p + Vector2D(1, 2) moves a position.
Mind p - (1, 2): the tuple can only become a Point2D, so it is the
displacement from (1, 2) to p -- a Vector2D, not a moved position.
Moving backwards is p - Vector2D(1, 2).
There are no zero()/one() factories either. (0, 0) through the
implicit constructor is shorter than any name for the origin, and one()
has no meaning for a position at all.
Crossing to another type is an explicit constructor, never an implicit
one: Vector2D(p) is the displacement from the origin to p,
and Point2D(v) the origin moved by v. Naming
the target type is what keeps the conversion visible; an @implicit one
would undo the split. xy returns the components as a plain tuple, to
destructure: var (x, y) = p.xy().
Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.
Fields¶
x: Float64y: Float64
Constructors¶
@staticmethod
def __init__(out self, x: Float64, y: Float64)
@staticmethod
def __init__(out self, x: Int, y: Int)
@staticmethod
def __init__(out self, t: Tuple[Float64, Float64])
@staticmethod
def __init__(out self, t: Tuple[Int, Int])
@staticmethod
def __init__(out self, t: Tuple[Int, Float64])
@staticmethod
def __init__(out self, t: Tuple[Float64, Int])
@staticmethod
def __init__(out self, v: Vector2D)
The origin moved by v: the position v points to.
__eq__¶
def __eq__(self, other: Self) -> Bool
__ne__¶
def __ne__(self, other: Self) -> Bool
__add__¶
def __add__(self, v: Vector2D) -> Self
__sub__¶
def __sub__(self, other: Self) -> Vector2D
The displacement from other to self.
def __sub__(self, v: Vector2D) -> Self
__iadd__¶
def __iadd__(mut self, v: Vector2D)
__isub__¶
def __isub__(mut self, v: Vector2D)
distance¶
def distance(self, other: Self) -> Float64
distance_squared¶
def distance_squared(self, other: Self) -> Float64
lerp¶
def lerp(self, other: Self, t: Float64) -> Self
A new point t of the way from this one to other; this one is unchanged. Not clamped: t outside 0..1 carries on past either end.
xy¶
def xy(self) -> Tuple[Float64, Float64]
Vector2D¶
struct Vector2D
A 2D displacement or direction: arithmetic operators, magnitude, normalize, dot, distance, lerp, the angle helpers from_angle, angle and rotate, the length helpers with_length and limit, and an explicit constructor from a Point2D.
Unlike Point2D, a bare tuple does not convert to one implicitly: a
displacement literal names its type, rect.translate(Vector2D(3, 4)).
Only one type taking a bare tuple is what keeps canvas.circle((0, 0), 20)
unambiguous, and locations are the literals a program writes most;
displacements mostly arrive from arithmetic (b - a, vel * dt), which
already yields a Vector2D.
Crossing to another type is an explicit constructor: Vector2D(p) is
the displacement from the origin to the position p, and Point2D(v)
the reverse. Naming the target type is what
keeps the conversion visible. xy returns the components as a plain
tuple, to destructure: var (x, y) = v.xy().
Angles are radians counter-clockwise from +x, as everywhere in the
library (y is up): Vector2D.from_angle(a) * speed aims, v.angle()
reads the heading back, v.rotate(a) turns. Like normalize, every
helper returns a new vector and leaves self alone.
The direction constants follow the screen's axes, y up: UP is
Vector2D(0, 1) and DOWN is Vector2D(0, -1), so gravity is
Vector2D.DOWN * 9.81.
Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.
Constants¶
ZERO = Vector2D(SIMD(0), SIMD(0))ONE = Vector2D(SIMD(1), SIMD(1))UP = Vector2D(SIMD(0), SIMD(1))DOWN = Vector2D(SIMD(0), SIMD(-1))LEFT = Vector2D(SIMD(-1), SIMD(0))RIGHT = Vector2D(SIMD(1), SIMD(0))
Fields¶
x: Float64y: Float64
Constructors¶
@staticmethod
def __init__(out self, x: Float64, y: Float64)
@staticmethod
def __init__(out self, x: Int, y: Int)
@staticmethod
def __init__(out self, t: Tuple[Float64, Float64])
@staticmethod
def __init__(out self, p: Point2D)
The displacement from the origin to p.
__neg__¶
def __neg__(self) -> Self
__eq__¶
def __eq__(self, other: Self) -> Bool
__ne__¶
def __ne__(self, other: Self) -> Bool
__add__¶
def __add__(self, other: Self) -> Self
__sub__¶
def __sub__(self, other: Self) -> Self
__mul__¶
def __mul__(self, s: Float64) -> Self
__truediv__¶
def __truediv__(self, s: Float64) -> Self
__rmul__¶
def __rmul__(self, s: Float64) -> Self
s * v, the same as v * s, so a formula reads as written.
__iadd__¶
def __iadd__(mut self, other: Self)
__isub__¶
def __isub__(mut self, other: Self)
__imul__¶
def __imul__(mut self, s: Float64)
__itruediv__¶
def __itruediv__(mut self, s: Float64)
magnitude¶
def magnitude(self) -> Float64
magnitude_squared¶
def magnitude_squared(self) -> Float64
normalize¶
def normalize(self) -> Self
dot¶
def dot(self, other: Self) -> Float64
distance¶
def distance(self, other: Self) -> Float64
distance_squared¶
def distance_squared(self, other: Self) -> Float64
lerp¶
def lerp(self, other: Self, t: Float64) -> Self
A new vector t of the way from this one to other; this one is unchanged. Not clamped: t outside 0..1 carries on past either end.
from_angle¶
@staticmethod
def from_angle(angle: Float64, length: Float64 = 1) -> Self
The vector of length pointing at angle radians, counter-clockwise from +x: from_angle(0) is RIGHT.
angle¶
def angle(self) -> Float64
The heading in radians, counter-clockwise from +x, in [-PI, PI]. 0 for the zero vector.
rotate¶
def rotate(self, angle: Float64) -> Self
This vector turned by angle radians, counter-clockwise; its length is unchanged.
with_length¶
def with_length(self, length: Float64) -> Self
This direction at length. Like normalize, the zero vector has no direction and gives NaN.
limit¶
def limit(self, max_length: Float64) -> Self
This vector, shortened to max_length if it is longer — a speed cap. Shorter vectors, the zero vector among them, are unchanged.
xy¶
def xy(self) -> Tuple[Float64, Float64]
Matrix¶
struct Matrix[rows: Int, cols: Int]
A fixed-size matrix, row-major, sized at compile time.
Transforms are homogeneous, so the library's 2D transforms are
Matrix[3, 3] — the extra row and column is what lets a translation be a
multiplication like any other, and so compose into a single matrix.
Build them with the free functions below rather than by hand:
identity, translate, rotate, scale. Compose with
@, which applies right to left — translate(x, y) @ rotate(a) rotates
first, then moves the rotated result. apply(m, point) maps a Point2D
through it, translation included; apply(m, vector) maps a Vector2D,
which a translation does not move.
Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.
Fields¶
data: Array[Float64, (rows * cols)]
Constructors¶
@staticmethod
def __init__(out self)
All zeros.
@staticmethod
def __init__(out self, *values: Float64)
The entries, row by row: Matrix[2, 2](1.0, 2.0, 3.0, 4.0) has 1.0, 2.0 on top. Takes exactly rows * cols values.
@staticmethod
def __init__(out self, *, copy: Self)
__getitem__¶
def __getitem__(self, r: Int, c: Int) -> Float64
__setitem__¶
def __setitem__(mut self, r: Int, c: Int, val: Float64)
__eq__¶
def __eq__(self, other: Self) -> Bool
__ne__¶
def __ne__(self, other: Self) -> Bool
__matmul__¶
def __matmul__[P: Int](self, rhs: Matrix[cols, P]) -> Matrix[rows, P]
transpose¶
def transpose(self) -> Matrix[cols, rows]
Rows and columns swapped, as a new matrix.
identity¶
def identity[N: Int]() -> Matrix[N, N]
The N x N transform that changes nothing — the start of a composition.
inverse¶
def inverse[N: Int](m: Matrix[N, N]) -> Matrix[N, N]
The transform that undoes m — Gauss-Jordan with partial pivoting.
Mapping a point back out of a transformed frame is what this is for, which
is how canvas.to_local maps a mouse position into the current transform.
A singular matrix (a zero scale, say) has no inverse and the result is
meaningless rather than an error.
apply¶
def apply(m: Matrix[Int(3), Int(3)], point: Point2D) -> Point2D
point mapped through m, translation included.
def apply(m: Matrix[Int(3), Int(3)], vector: Vector2D) -> Vector2D
vector mapped through m's rotation, scale and shear, but not its translation: a displacement, unlike a position, is the same wherever it starts. So a velocity turns with a rotate and stretches with a scale, and a translate leaves it as it is. Meant for the affine matrices translate, rotate and scale build; a projective bottom row is ignored.
translate¶
def translate(dx: Float64, dy: Float64) -> Matrix[Int(3), Int(3)]
Move by (dx, dy) in world units. dy is up, since y grows upward.
def translate(delta: Vector2D) -> Matrix[Int(3), Int(3)]
Move by delta in world units, as shape.translate(delta) does.
rotate¶
def rotate(angle: Float64) -> Matrix[Int(3), Int(3)]
Turn by angle radians about the origin, counter-clockwise — the mathematical convention, which holds here because y grows upward. Use radians(d) for an angle written in degrees.
scale¶
def scale(sx: Float64, sy: Float64) -> Matrix[Int(3), Int(3)]
Scale each axis about the origin. Negative values mirror.
def scale(s: Float64) -> Matrix[Int(3), Int(3)]
Scale both axes by the same factor.
Random¶
struct Random
A seeded pseudo-random generator: .float(), .int(), .bool().
Random() seeds itself from the clock and Random(seed) takes one, which
is the difference that matters: a seeded generator replays the same
sequence every run, so a procedurally generated level or a particle burst
can be reproduced exactly while it is being tuned.
xoroshiro128++ — fast and well-distributed, not cryptographic. Don't render keys or tokens from it.
Implements Deinitable, Movable.
Constructors¶
@staticmethod
def __init__(out self)
@staticmethod
def __init__(out self, seed: UInt64)
float¶
def float(mut self) -> Float64
A number in [0.0, 1.0) — 1.0 itself is never returned.
def float(mut self, low: Float64, high: Float64) -> Float64
A number in [low, high).
int¶
def int(mut self, low: Int, high: Int) -> Int
A whole number in [low, high) — high is excluded, so r.int(0, len(items)) indexes a list without going off the end.
bool¶
def bool(mut self) -> Bool
A coin flip — True half the time.
Noise¶
struct Noise
Smooth pseudo-random values: .at(x), .at(position) and .at(position, time), each in [0.0, 1.0].
Where Random gives independent samples that jump from one call to the
next, Noise is a field: nearby inputs give nearby values, so a terrain
height, a wandering position or a flow-field angle drifts instead of
jittering. The range is Random.float()'s, so it swaps in wherever the
jitter should be smooth; lerp(-a, a, n) makes it signed.
Noise() seeds itself from the clock and Noise(seed) takes one; the same
seed always gives the same field. Nothing changes after construction, so
.at reads rather than advances, and a copy is the same field.
feature_size is the distance, in the units passed to .at, over which
the field goes from one value to an unrelated one: hills about 120 units
across are Noise(feature_size=120). It divides x and position but
not time, so an animation's rate is set at the call,
noise.at(position, time=t * 0.3), independently of the spatial scale.
octaves layers the field over itself at doubling frequencies, each
layer's weight falloff times the last: one octave is plain, blobby
Perlin; the default four at 0.5 add the finer detail of a coastline or a
cloud edge. The layers are averaged by weight, so the range stays
[0, 1].
Improved Perlin noise (2002). Its value at a whole-number lattice point,
after dividing by feature_size, is exactly 0.5.
Implements Copyable, Deinitable, Movable, Writable.
Constructors¶
@staticmethod
def __init__(
out self,
*,
octaves: Int = Int(4),
falloff: Float64 = 0.5,
feature_size: Float64 = 1,
)
@staticmethod
def __init__(
out self,
seed: UInt64,
*,
octaves: Int = Int(4),
falloff: Float64 = 0.5,
feature_size: Float64 = 1,
)
at¶
def at(self, x: Float64) -> Float64
The field along a line: a value that wanders smoothly as x (a time, say) moves.
def at(self, position: Point2D) -> Float64
The field over the plane: terrain, texture, a flow-field angle.
def at(self, position: Point2D, time: Float64) -> Float64
The plane field, animated: it evolves smoothly as time moves. time is not divided by feature_size; scale it at the call.
PI¶
comptime PI = pi
Half a turn in radians.
TAU¶
comptime TAU = tau
A whole turn in radians: 2 * PI.
From the standard library¶
Re-exported from std.math, so a program has them without a second import: clamp, sqrt, floor, ceil, sin, cos, tan, asin, acos, atan2, exp, log, log2, isnan, isinf, isfinite.