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).

TypeComponentsAlso namedDefault
math::vec2, math::vec3, math::vec4x y, x y z, x y z w (32-bit float)width height depthall 0.0
math::ivec2, math::ivec3x y, x y z (32-bit int)width height depthall 0
math::colorr 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 int becomes a float. A vec enters an ivec slot only through cast<math::ivec2>(v) (or ivec3), which truncates each component toward zero. A color never converts to or from a vec4.
  • 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:

jaiscript
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)));
output
1 int / 2.5 float / float

Making vectors​

  • Call the type with up to one argument per component; missing components are 0. T v(args) and T 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 a var raises math::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) and math::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 a var. A string slot takes text only, so give it to_string(v); anything else is rejected with the typed-slot text.
  • using brings the names into a file (Namespaces); type_of still reports the full name.
jaiscript
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); }
output
(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::color

Components​

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.

jaiscript
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); }
output
(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 component

Operators​

  • + - * / 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, not math::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 a vec4, are never equal.
  • <, <=, >, >= and <=> order vectors lexicographically, the order sort() 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.
jaiscript
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); }
output
(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 ordering

Values 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).

jaiscript
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);
output
(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 spelled math::lerp, is a + (b - a) * t. math::unmix(a, b, v) is where v lies between a and b (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_out and math::mix_out_in (a, b, t, strength) are eased mixes; the math::unmix_in family inverts them.
  • Interpolations, math::floor, math::ceil and math::round (half away from zero) give a float vector. math::abs, math::min, math::max and math::clamp keep an ivec an ivec unless a bound is a float. With lo above hi, clamp gives lo when v < lo, else hi.
jaiscript
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); }
output
(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 argument

Geometry​

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 functionMethod (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_squared and distance_squared give an int, cross an ivec3. cross of 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 and math::degrees(rad) converts back.
  • rotate turns a vec2, or a vec3 about an axis of any length (a zero axis leaves the vector as it is). rotate_around turns a vec2 point about a vec2 pivot. Each gives a float vector, from an ivec too.
  • angle is a vec2's direction, atan2(y, x) in (-π, π], and 0 for the zero vector. from_angle is the vec2 (cos, sin) of a direction times the length (1 when omitted).
  • perpendicular turns a vec2 or ivec2 a quarter counterclockwise.
  • Normalizing the zero vector gives zero, projecting onto zero gives zero, and angle_between is 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.
jaiscript
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); }
output
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.

jaiscript
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); }
output
(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 + operator

Arrays, 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.

jaiscript
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))));
output
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.