Math: Vectors & Colors
The language has six built-in math value types and a math:: library of scalar, interpolation and geometry functions; the reference lists every signature. A host can make its own C++ vector types cross the boundary as these types (Math Adapters).
| Type | Components | Also named | Default |
|---|---|---|---|
math::vec2, math::vec3, math::vec4 | x y, x y z, x y z w (32-bit float) | width height depth | all 0.0 |
math::ivec2, math::ivec3 | x y, x y z (32-bit int) | width height depth | all 0 |
math::color | r g b a (32-bit float, straight alpha) | opaque white, (1.0, 1.0, 1.0, 1.0) |
- They are values, like
int: assignment, a by-value parameter, an array element and a field each hold their own copy. - An ivec widens into the vec of its dimensions wherever one is expected, as an
intbecomes afloat. A vec enters an ivec slot only throughcast<math::ivec2>(v)(orivec3), which truncates each component toward zero. A color never converts to or from avec4. - Nothing mutates in place. Every function and method returns a new value; a vector changes only by assignment to it or to a component.
Scalar math
math:: holds the standard scalar operations on every engine and the constants math::PI, math::E, math::TAU, math::INF and math::NAN_VALUE. They take ints and floats; a bool or an object needs an explicit numeric conversion. A function is a value too (auto root = math::sqrt;). Random numbers come from a random object (Random Numbers & Crypto).
math::min, math::max and math::clamp return the winning operand unchanged, so with an int and a float the result's type depends on the values:
var a = math::max(1, 0.5);
var b = math::max(1, 2.5);
print(a, " ", type_of(a), " / ", b, " ", type_of(b), " / ", type_of(math::clamp(5, 0.5, 2.5)));1 int / 2.5 float / floatMaking vectors
- Call the type with up to one argument per component; missing components are 0.
T v(args)andT v{args}work, and a bare declaration holds the default. - A narrower vector followed by scalars that complete the count builds a wider one.
- One scalar is not a vector:
math::vec3(5)fails the load (argument 1 of 'math::vec3': cannot pass 'int' where a vector or color is expected), and through avarraisesmath::vec3 expects one vector of 3 components. The other argument mistakes are in the run-time errors table. - A constructor names its type, so it converts what it is handed:
math::ivec2(v)andmath::ivec2(2.7, 1.2)truncate as the cast does. - A color takes
(r, g, b)with alpha 1,(r, g, b, a), nothing or another color. - Every declared vector slot (a variable, a parameter, a return, a field, an array element, a store through a reference) widens an ivec and refuses a vec for an ivec: a load error where both types are visible (
cannot initialize 'cell' declared 'math::ivec2' with 'math::vec2': cast<math::ivec2>(v) truncates each component toward zero), the typed-slot error through avar. Astringslot takes text only, so give itto_string(v); anything else is rejected with the typed-slot text. usingbrings the names into a file (Namespaces);type_ofstill reports the full name.
using math::vec3, math::ivec2;
vec3 b = vec3(1, 2.5); // missing components are 0
math::vec4 q{1, 2, 3, 4};
print(b, " ", q, " ", type_of(b), " ", vec3(math::vec2(1, 2), 3), " ", math::color(1, 0, 0));
ivec2 cell = cast<ivec2>(math::vec2(3.9, -4.2)); // a vec enters an ivec slot through the cast
math::vec2 center(math::vec2 at) { return at + 0.5; }
print(cell, " ", center(cell)); // the parameter widens the ivec
var loose = math::vec2(1.5, 2.5);
try { ivec2 bad = loose; } catch (e) { print(e); }
var c = math::color();
try { math::vec4 bad = c; } catch (e) { print(e); }(1.0, 2.5, 0.0) (1.0, 2.0, 3.0, 4.0) math::vec3 (1.0, 2.0, 3.0) (1.0, 0.0, 0.0, 1.0)
(3, -4) (3.5, -3.5)
Cannot initialize 'bad' (declared math::ivec2) with math::vec2
Cannot initialize 'bad' (declared math::vec4) with math::colorComponents
Components read and store by name, with compound assignment and ++/--, as float (an ivec's as int). Storage is 32-bit: a float store rounds to 32 bits (beyond that range it becomes infinity, as in C++) and reads back that value widened exactly; an int store checks the 32-bit range.
math::vec3 v = math::vec3(1, 2, 3);
v.x = 5;
v.depth *= 2; // depth is z
math::ivec2 p;
p.x++;
math::vec2 tiny = math::vec2(0.1, 1e300);
print(v, " ", p, " ", type_of(v.x), " ", tiny, " ", tiny.x == 0.1);
try { p.x = 3000000000; } catch (e) { print(e); }
try { var w = math::vec2(); w.z = 1; } catch (e) { print(e); }
try { var s = "a"; v.x = s; } catch (e) { print(e); }(5.0, 2.0, 6.0) (1, 0) float (0.1, inf) false
Integer overflow: the value does not fit an int32 component
math::vec2 has no member 'z'
Cannot store a non-numeric value in a math::vec3 componentOperators
+ - * /and their compound assignments work component by component between two vectors of the same type, and between a vector and a scalar on either side.*of two vectors is the component-wise product, notmath::dot.- Unary
-negates every component; unary+returns a copy;~is refused. - An ivec divides like an int (truncating), has
%, and follows the integer overflow policy (Operators). Float vectors have no%. - A vector concatenates into a string as its text, and is true when any component is nonzero.
==and!=compare every component: a vec equals an ivec holding the same values; vectors of different dimensions, or a color and avec4, are never equal.<,<=,>,>=and<=>order vectors lexicographically, the ordersort()uses; across dimensions the shared components decide first, then the shorter vector is less. A color orders only against a color.- Mixed kinds or dimensions in arithmetic, a float scalar with an ivec and an ivec's zero divisor component raise the scalar operators' errors; a float vector's zero divisor is IEEE.
using math::vec2, math::vec3, math::ivec2, math::color;
vec3 a = vec3(1, 2, 3);
print(a + vec3(1, 1, 1), " ", 6 / vec2(3, 6), " ", vec2(1, 2) * vec2(3, 4), " ", ivec2(7, -7) / 2, " ", ivec2(7, -7) % 4);
print(a == math::ivec3(1, 2, 3), " ", vec2(1, 2) == vec3(1, 2, 0), " ", color() == math::vec4(1, 1, 1, 1));
print(vec2(1, 9) < vec2(2, 0), " ", vec2(1, 2) <=> vec2(1, 3), " ", vec2(1, 2) < vec3(1, 2, -5), " ", color(0, 0, 0) < color());
try { print(a + math::ivec3(1, 2, 3)); } catch (e) { print(e); }
try { print(color() < math::vec4()); } catch (e) { print(e); }(2.0, 3.0, 4.0) (2.0, 1.0) (3.0, 8.0) (3, -3) (3, -3)
true false false
true -1 true true
Invalid operands for + operator
math::color has no orderingValues and references
A & parameter is the caller's vector, and a field's components store through the object. A component is an lvalue wherever a reference binds: it passes to a float& parameter, binds auto& and cast<float&>, and returns from a float& function or method. A reference to a component keeps naming that component of the variable after the whole vector is assigned again. A parallel_for over vectors, or over objects holding them, writes each element in place, and a captured vector reads as a value (Parallel Loops).
void lift(math::vec3& p) { p.y += 100; }
void scale(float& c) { c *= 10; }
class Ship {
math::vec2 pos = math::vec2(1, 2);
float& px() { return pos.x; }
}
math::vec3 a = math::vec3(1, 2, 3);
math::vec3 copy = a;
lift(a);
scale(a.x);
auto& z = a.z;
a = a + math::vec3(0, 0, 1); // z still names a's z
z *= 2;
var ship = Ship();
ship.px() = 40;
print(a, " ", copy, " ", ship.pos);(10.0, 102.0, 8.0) (1.0, 2.0, 3.0) (40.0, 2.0)The component-wise library
These functions work on numbers, vectors and colors. A parameter that takes a number takes a vector of the call's dimensions, or one number for every component. Functions with no component-wise meaning (math::sqrt, math::sin) stay scalar and reject a vector, at load where its type is visible.
math::mix(a, b, t), also spelledmath::lerp, isa + (b - a) * t.math::unmix(a, b, v)is wherevlies betweenaandb(0 over an empty range).math::remap(v, in_lo, in_hi, out_lo, out_hi)maps one range onto another.math::mix_in,math::mix_out,math::mix_in_outandmath::mix_out_in(a, b, t, strength)are eased mixes; themath::unmix_infamily inverts them.- Interpolations,
math::floor,math::ceilandmath::round(half away from zero) give a float vector.math::abs,math::min,math::maxandmath::clampkeep an ivec an ivec unless a bound is a float. Withloabovehi,clampgiveslowhenv < lo, elsehi.
using math::vec2, math::ivec2;
vec2 a = vec2(0, 10);
vec2 b = vec2(10, 20);
print(math::mix(a, b, 0.25), " ", math::lerp(a, b, vec2(0.5, 1)), " ", math::unmix(a, b, vec2(5, 20)), " ", math::remap(vec2(5, 5), 0.0, 10.0, a, b));
print(math::mix_in(a, b, 0.5, 2.0), " ", math::round(vec2(1.5, -2.5)), " ", math::clamp(ivec2(-5, 9), 0, 4), " ", math::clamp(ivec2(-5, 9), 0.0, 4.5));
try { print(math::mix(a, math::vec3(1, 1, 1), 0.5)); } catch (e) { print(e); }
var loose = a; // math::sqrt(a) itself fails the load
try { print(math::sqrt(loose)); } catch (e) { print(e); }(2.5, 12.5) (5.0, 20.0) (0.5, 1.0) (5.0, 15.0)
(2.5, 12.5) (2.0, -3.0) (0, 4) (0.0, 4.5)
math::mix expects vectors of the same dimensions
math::sqrt expects a numeric argumentGeometry
A method is the free function with the receiver first, but four names differ. Both spellings return a new value: there is no in-place form, so normalizing in place is v = v.normalized();. A color has none of the methods.
| Free function | Method (also returns a copy) |
|---|---|
math::dot(a, b), math::cross(a, b), math::length(v), math::length_squared(v), math::angle(v) | the same names: a.dot(b), a.cross(b), v.length(), v.length_squared(), v.angle() |
math::normalize(v) | v.normalized() |
math::distance(a, b) | a.distance_to(b) |
math::rotate(v, angle), math::rotate(v, axis, angle) | v.rotated(angle), v.rotated(axis, angle) |
math::rotate_around(v, pivot, angle) | v.rotated_around(pivot, angle) |
math::distance_squared(a, b), math::reflect(v, n), math::project(v, onto), math::angle_between(a, b), math::perpendicular(v), math::from_angle(angle), math::from_angle(angle, length) | none |
- Two ivecs keep exact ints:
dot,length_squaredanddistance_squaredgive an int,crossan ivec3.crossof two vec2 is the scalar z. - Angles are radians, and a positive angle turns x toward y.
math::radians(deg)converts a number of degrees andmath::degrees(rad)converts back. rotateturns a vec2, or a vec3 about an axis of any length (a zero axis leaves the vector as it is).rotate_aroundturns a vec2 point about a vec2 pivot. Each gives a float vector, from an ivec too.angleis a vec2's direction,atan2(y, x)in (-π, π], and 0 for the zero vector.from_angleis the vec2(cos, sin)of a direction times the length (1 when omitted).perpendicularturns a vec2 or ivec2 a quarter counterclockwise.- Normalizing the zero vector gives zero, projecting onto zero gives zero, and
angle_betweenis 0 when either vector is zero. - Dimensions must agree, and an error names the shape it expected:
math::dot expects two vectors of the same dimensions,math::cross expects two vec2 or two vec3 vectors,math::rotate expects (vec2, angle) or (vec3, axis, angle),math::rotate_around expects (vec2, pivot, angle),math::angle expects a vec2.
using math::vec2, math::vec3, math::ivec2;
vec3 v = vec3(3, 4, 0);
vec3 x = vec3(1, 0, 0);
print(v.dot(x), " ", x.cross(vec3(0, 1, 0)), " ", math::cross(vec2(1, 2), vec2(3, 4)), " ", type_of(math::dot(ivec2(2, 3), ivec2(4, 5))));
print(math::normalize(v), " ", v.normalized(), " ", v); // v is unchanged
print(math::distance(vec2(0, 0), vec2(3, 4)), " ", vec2(0, 0).distance_to(vec2(3, 4)));
print(math::round(math::rotate(vec2(2, 0), math::PI / 2)), " ", math::round(x.rotated(vec3(0, 0, 1), math::PI / 2)));
print(math::radians(90), " ", math::degrees(math::PI), " ", math::degrees(vec2(0, 2).angle()));
print(math::round(vec2(2, 1).rotated_around(vec2(1, 1), math::radians(90))), " ", math::from_angle(0.0, 3));
try { var w = vec2(3, 4); w.normalize(); } catch (e) { print(e); }3.0 (0.0, 0.0, 1.0) -2.0 int
(0.6, 0.8, 0.0) (0.6, 0.8, 0.0) (3.0, 4.0, 0.0)
5.0 5.0
(0.0, 2.0) (0.0, 1.0, 0.0)
1.5707963267948966 180.0 90.0
(1.0, 2.0) (3.0, 0.0)
math::vec2 has no member 'normalize'Colors
Arithmetic, math::clamp, math::min, math::max and the rounding functions work on all four channels, and a color combines only with colors and scalars. math::mix and math::lerp blend colors in the OKLab perceptual space, so a blend keeps its brightness instead of dipping through gray; the eased mixes ease the same blend. math::mix_rgb blends each channel directly and math::mix_hsv travels the shorter way around the hue wheel. Alpha always blends linearly.
math::color red = math::color(1, 0, 0);
math::color blue = math::color(0, 0, 1);
print(math::mix(red, blue, 0.5)); // perceptual (OKLab)
print(math::mix_rgb(red, blue, 0.5), " ", math::mix_hsv(red, blue, 0.5), " ", math::clamp(red * 2, 0.0, 1.0));
try { print(red + math::vec4(1, 1, 1, 1)); } catch (e) { print(e); }(0.5504411, 0.32562068, 0.63650066, 1.0)
(0.5, 0.0, 0.5, 1.0) (1.0, 0.0, 1.0, 1.0) (1.0, 0.0, 0.0, 1.0)
Invalid operands for + operatorArrays, maps, text and JSON
array<math::vec3> (and the same for every math type) is a packed typed buffer. An element reads as a copy, element components store in place, and the element boundary widens an ivec as a slot does; an array<math::ivec3> into an array<math::vec3> copies element by element. An ivec makes a natural map key.
Text is the components in parentheses, floats in the shortest form that reads back the same 32-bit value. to_json writes the type and the named components and from_json rebuilds the same type; the binary archive (to_base64 / from_base64) keeps the exact bits.
array<math::vec3> pts = [math::vec3(4, 5, 6), math::vec3(1, 2, 3)];
pts.push(math::ivec3(1, 0, 9)); // the element boundary widens
pts[0].x += 10;
try { pts.push(math::vec2(1, 2)); } catch (e) { print(e); }
pts.sort();
array<math::ivec3> cells = [cast<math::ivec3>(pts[0])];
map<math::ivec2, string> tiles;
tiles[math::ivec2(1, 2)] = "door";
print(pts, " ", cells[0], " ", tiles[math::ivec2(1, 2)]);
string j = to_json(math::vec2(0.1, 1e30));
print(j, " ", type_of(from_json(j)), " ", from_base64(to_base64(math::color(1, 0.5, 0))));Cannot push 'math::vec2' to array<math::vec3>
[(1.0, 0.0, 9.0), (1.0, 2.0, 3.0), (14.0, 5.0, 6.0)] (1, 0, 9) door
{"$type":"math::vec2","$val":{"x":0.1,"y":1e+30}} math::vec2 (1.0, 0.5, 0.0, 1.0)What the checker catches
The strict checker (Types That Fail Early) knows every math type, its members and the library's result types, so a wrong type (math::vec3 v = 5;), member (math::color has no member 'x'), argument count ('dot' expects 1 argument) or argument shape (argument 1 of 'math::sqrt': cannot pass 'string' where a number is expected) fails the load. Errors that depend on values (mixed kinds, an ivec's zero divisor, an int32 overflow) stay catchable at run time.