Math: Vectors & Colors​

The language has six built-in math value types for positions, directions, grid cells and colors: math::vec2, math::vec3, math::vec4, math::ivec2, math::ivec3 and math::color. They are values like int and string: a copy is a separate vector, every arithmetic operator works component by component, and the math:: library has the interpolation and geometry functions a game needs. A host can make its own C++ vector types cross the boundary as these types (Math Adapters).

TypeComponentsAlso namedDefault
math::vec2x y (32-bit float)width height(0.0, 0.0)
math::vec3x y z (32-bit float)width height depth(0.0, 0.0, 0.0)
math::vec4x y z w (32-bit float)width height depth(0.0, 0.0, 0.0, 0.0)
math::ivec2x y (32-bit int)width height(0, 0)
math::ivec3x y z (32-bit int)width height depth(0, 0, 0)
math::colorr g b a (32-bit float, straight alpha)(1.0, 1.0, 1.0, 1.0), opaque white

Scalar math​

All standard math operations and constants live in math::, on every engine. Use math::abs, math::min, math::max, math::clamp, rounding, powers, roots, logarithms, trigonometry and interpolation here; the full signatures list every operation. math::PI, math::E, math::TAU, math::INF and math::NAN_VALUE are namespaced constants. Scalar helpers accept int/float numbers; bool and objects require an explicit numeric conversion.

Function values such as auto root = math::sqrt; call the same math kernel. Random numbers are not here: they come from a random object you own (Random Numbers & Crypto).

Making vectors​

Call the type with up to one argument per component; missing components are 0. A declaration without an initializer holds the default, and the C++ initializer spellings work: T v(args) and T v{args}. A narrower vector followed by scalars that complete the count builds a wider one, and a vector of the same dimensions converts between the float and int families (float to int truncates toward zero). A color takes (r, g, b) with alpha 1, (r, g, b, a), nothing (opaque white) or another color.

jaiscript
math::vec3 a = math::vec3(1, 2.5, 3);
math::vec3 b = math::vec3(1, 2);              // missing components are 0
math::vec3 zero;                              // a declaration default-constructs
math::ivec2 cell(3, 4);                       // direct initialization
math::vec4 q{1, 2, 3, 4};                     // brace initialization
print(a, " ", b, " ", zero, " ", cell, " ", q);

math::vec3 lifted = math::vec3(math::vec2(1, 2), 3);   // a narrower vector, completed by scalars
math::ivec3 snapped = math::ivec3(math::vec3(1.9, -1.9, 2));  // float to int truncates toward zero
print(lifted, " ", snapped, " ", type_of(snapped));

print(math::color(), " ", math::color(1, 0, 0), " ", math::color(0.2, 0.4, 0.6, 0.5));
output
(1.0, 2.5, 3.0) (1.0, 2.0, 0.0) (0.0, 0.0, 0.0) (3, 4) (1.0, 2.0, 3.0, 4.0)
(1.0, 2.0, 3.0) (1, -1, 2) math::ivec3
(1.0, 1.0, 1.0, 1.0) (1.0, 0.0, 0.0, 1.0) (0.2, 0.4, 0.6, 0.5)

One scalar is not a vector: math::vec3(5) raises math::vec3 expects one vector of 3 components, as does a vector of another dimension on its own (a color's lone argument is a color: math::color expects one color); a narrower vector whose scalars do not complete the count raises math::vec4 expects a narrower vector followed by scalars that complete 4 components, a component that is no number math::vec2 expects numeric components, and too many arguments raise math::vec3 expects 0 to 3 arguments. An int component outside the 32-bit range raises Integer overflow: the value does not fit an int32 component.

using brings the names into a file so they read without the prefix (Namespaces); type_of still reports the full name:

jaiscript
using math::vec3, math::ivec2;

vec3 up = vec3(0, 1, 0);
ivec2 tile = ivec2(5, 9);
print(up, " ", tile, " ", type_of(up));
output
(0.0, 1.0, 0.0) (5, 9) math::vec3

Components​

Components read and store by name, with compound assignment and ++/--. A vector's components are float (an ivec's are int); width, height and depth are second names for x, y and z. A color's channels are r g b a only. The components are stored as 32-bit floats, so a component reads back the nearest 32-bit value, widened exactly:

jaiscript
math::vec3 v = math::vec3(1, 2, 3);
v.x = 5;
v.y += 2.5;
v.z *= 2;
print(v, " ", v.width, " ", v.height, " ", v.depth);

math::ivec2 p;
p.x++;
++p.y;
print(p, " ", type_of(p.x), " ", type_of(v.x));

math::color c = math::color(0.25, 0.5, 0.75);
c.a = 0.5;
print(c, " ", c.r + c.g + c.b);

math::vec2 tiny = math::vec2(0.1, 0);
print(tiny.x, " ", tiny.x == 0.1);            // components are 32-bit floats
output
(5.0, 4.5, 6.0) 5.0 4.5 6.0
(1, 1) int float
(0.25, 0.5, 0.75, 0.5) 1.5
0.10000000149011612 false

A store into an int component checks the 32-bit range, a store into a float component rounds to 32 bits (a value beyond that range becomes infinity, as in C++), and a name the type does not have, or a value that is not a number, raises:

jaiscript
math::ivec2 p;
try { p.x = 3000000000; } catch (e) { print(e); }
try { var w = math::vec2(); w.z = 1; } catch (e) { print(e); }
try { var v = math::vec3(); var s = "a"; v.x = s; } catch (e) { print(e); }
math::vec2 big;
big.x = 1e300;
print(big);
output
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
(inf, 0.0)

Operators​

+ - * / 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 (the dot product is math::dot). Unary - negates every component. An ivec divides like an int (truncating) and has %; float vectors have no %. Compound assignment works on any vector lvalue, and a vector concatenates into a string as its text.

jaiscript
math::vec3 a = math::vec3(1, 2, 3);
math::vec3 b = math::vec3(0.5, 0.5, 0.5);
print(a + b, " ", a - b, " ", -a);
print(a * 2, " ", 2 * a, " ", a / 2, " ", 6 / math::vec2(3, 6));
print(math::vec2(1, 2) * math::vec2(3, 4));   // component-wise product
print(math::ivec2(7, -7) / 2, " ", math::ivec2(7, -7) % 4);
a += b;
a *= 2;
print(a);
print("at " + math::ivec2(1, 2));
output
(1.5, 2.5, 3.5) (0.5, 1.5, 2.5) (-1.0, -2.0, -3.0)
(2.0, 4.0, 6.0) (2.0, 4.0, 6.0) (0.5, 1.0, 1.5) (2.0, 1.0)
(3.0, 8.0)
(3, -3) (3, -3)
(3.0, 5.0, 7.0)
at (1, 2)

== and != compare every component; a vec equals an ivec holding the same values, and vectors of different dimensions, or a color and a vec4, are never equal. A vector is true when any component is nonzero. Vectors and colors compare lexicographically by component with <=> and relational operators; colors compare only with colors. Mixing kinds or dimensions in arithmetic, a float scalar with an ivec, and a zero divisor component raise the scalar operators' errors, and ivec arithmetic follows the integer overflow policy (Operators):

jaiscript
print(math::vec3(1, 2, 3) == math::vec3(1, 2, 3), " ", math::vec3(1, 2, 3) == math::ivec3(1, 2, 3), " ", math::vec2(1, 2) == math::vec3(1, 2, 0));
print(math::color() == math::color(1, 1, 1, 1), " ", math::color() == math::vec4(1, 1, 1, 1));
if (math::vec2(0, 0)) { print("zero is true"); } else { print("zero is false"); }
if (math::ivec2(0, 1)) { print("any nonzero component is true"); }

try { var q = math::vec3(1, 2, 3) + math::ivec3(1, 2, 3); } catch (e) { print(e); }
try { var q = math::vec2(1, 2) + math::vec3(1, 2, 3); } catch (e) { print(e); }
try { var q = math::ivec2(1, 2) * 1.5; } catch (e) { print(e); }
try { var q = math::vec2(1, 1) < math::vec2(2, 2); } catch (e) { print(e); }
try { var q = math::vec2(1, 1) / 0; } catch (e) { print(e); }
try { var q = math::ivec2(1, 1) % math::ivec2(1, 0); } catch (e) { print(e); }
try { var q = math::ivec2(2147483647, 0) + 1; } catch (e) { print(e); }
output
true true false
true false
zero is false
any nonzero component is true
Invalid operands for + operator
Invalid operands for + operator
Invalid operands for * operator
Division by zero
Modulo by zero
Integer overflow in '+'

Values and references​

A vector is a value everywhere: assignment, a by-value parameter, an array element and a field each hold their own copy, exactly like a C++ struct. A & parameter is the caller's vector. A field's components store through the object:

jaiscript
math::vec3 a = math::vec3(1, 2, 3);
math::vec3 b = a;                      // a copy
b.x = 9;
array<math::vec3> pts = [a, a];
pts[1].y = 7;
print(a, " ", b, " ", pts[0], " ", pts[1]);

void push_up(math::vec3 p) { p.y += 100; }        // by value: the caller's vector is untouched
void lift(math::vec3& p) { p.y += 100; }          // by reference: it is the caller's vector
push_up(a);
print(a);
lift(a);
print(a);

class Body { math::vec3 pos; }
var body = Body();
body.pos.x = 4;
body.pos += math::vec3(1, 1, 1);
print(body.pos);
output
(1.0, 2.0, 3.0) (9.0, 2.0, 3.0) (1.0, 2.0, 3.0) (1.0, 7.0, 3.0)
(1.0, 2.0, 3.0)
(1.0, 102.0, 3.0)
(5.0, 1.0, 1.0)

A component is an lvalue wherever a reference binds, as a struct member is in C++: 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 even after the whole vector is assigned again:

jaiscript
void scale(float& c) { c *= 10; }

math::vec3 v = math::vec3(1, 2, 3);
scale(v.y);                            // the component itself binds float&
auto& r = v.x;
r = 5;
r += 1;
auto& z = cast<float&>(v.z);
z = 0.5;
print(v);

class Ship {
    math::vec2 pos = math::vec2(1, 2);
    float& px() { return pos.x; }      // a component returned by reference
}
var ship = Ship();
ship.px() = 40;
scale(ship.pos.y);
print(ship.pos);

math::color tint = math::color(1, 1, 1);
scale(tint.a);
print(tint);
output
(6.0, 20.0, 0.5)
(40.0, 20.0)
(1.0, 1.0, 1.0, 10.0)

Typed slots​

A declared vector slot (a variable, a parameter, a return, a field, an array element) converts between the float and int families of the same dimensions; a string slot takes the vector's text; anything else is rejected with the typed-slot text. A color never converts to or from a vec4.

jaiscript
math::vec3 v = math::ivec3(1, 2, 3);            // an ivec into a vec slot
math::ivec2 p = math::vec2(1.5, -2.5);          // a vec into an ivec slot truncates
string s = math::vec2(1, 2);                    // a string slot takes the text
math::ivec2 snap(math::vec2 at) { return at; }  // returns convert too
print(v, " ", p, " ", s, " ", snap(math::vec2(2.9, 1.1)));

var w = math::vec2(1, 2);
try { math::vec3 bad = w; } catch (e) { print(e); }
var c = math::color();
try { math::vec4 bad = c; } catch (e) { print(e); }
output
(1.0, 2.0, 3.0) (1, -2) (1.0, 2.0) (2, 1)
Cannot initialize 'bad' (declared math::vec3) with math::vec2
Cannot initialize 'bad' (declared math::vec4) with math::color

The component-wise library​

The math:: functions that work on numbers work on vectors and colors too, component by component. A parameter that takes a number takes a vector of the call's dimensions, or one number for every component.

FunctionDoes
math::mix(a, b, t), math::lerp(a, b, t)a + (b - a) * t; t a float or a vector
math::unmix(a, b, v)where v lies between a and b (0 over an empty range)
math::remap(v, in_lo, in_hi, out_lo, out_hi)maps v from one range onto another
math::clamp(v, lo, hi), math::min(a, b), math::max(a, b)per component
math::abs, math::floor, math::ceil, math::roundper component; math::round rounds half away from zero
math::mix_in, math::mix_out, math::mix_in_out, math::mix_out_in (a, b, t, strength)eased mixes
math::unmix_in, math::unmix_out, math::unmix_in_out, math::unmix_out_in (a, b, v, strength)their inverses
jaiscript
math::vec2 a = math::vec2(0, 10);
math::vec2 b = math::vec2(10, 20);
print(math::mix(a, b, 0.25), " ", math::lerp(a, b, math::vec2(0.5, 1)));   // a float or a per-component t
print(math::unmix(a, b, math::vec2(5, 20)));                              // where a value sits between a and b
print(math::remap(math::vec2(5, 5), 0.0, 10.0, a, b));                    // value, in_lo, in_hi, out_lo, out_hi
print(math::clamp(math::vec3(-1, 0.5, 2), 0.0, 1.0), " ", math::clamp(math::ivec2(-5, 9), math::ivec2(0, 0), math::ivec2(4, 4)));
print(math::min(math::vec2(1, 5), math::vec2(3, 2)), " ", math::max(math::ivec2(1, 5), 3));
print(math::abs(math::vec2(-1.5, 2)), " ", math::floor(math::vec2(1.5, -1.5)), " ", math::ceil(math::vec2(1.2, -1.2)), " ", math::round(math::vec2(1.5, -2.5)));
print(math::mix_in(a, b, 0.5, 2.0), " ", math::mix_out(a, b, 0.5, 2.0), " ", math::mix_in_out(a, b, 0.25, 2.0));

var m = math::mix(math::ivec2(0, 0), math::ivec2(1, 3), 0.5);
print(m, " ", type_of(m), " ", type_of(math::abs(math::ivec2(-3, 4))));
try { var q = math::mix(math::vec2(0, 0), math::vec3(1, 1, 1), 0.5); } catch (e) { print(e); }
output
(2.5, 12.5) (5.0, 20.0)
(0.5, 1.0)
(5.0, 15.0)
(0.0, 0.5, 1.0) (0, 4)
(1.0, 2.0) (3, 5)
(1.5, 2.0) (1.0, -2.0) (2.0, -1.0) (2.0, -3.0)
(2.5, 12.5) (7.5, 17.5) (1.25, 11.25)
(0.5, 1.5) math::vec2 math::ivec2
math::mix expects vectors of the same dimensions

Results stay exact where they can: math::abs, math::min, math::max and math::clamp with int bounds keep an ivec an ivec, while the interpolations, math::floor/math::ceil/math::round and a float bound give a float vector. Functions with no component-wise meaning (math::sqrt, math::sin) stay scalar and reject a vector.

Geometry​

FunctionMethodResult
math::dot(a, b)a.dot(b)float (int for two ivecs)
math::cross(a, b)a.cross(b)a vec3 for two vec3 (an ivec3 for two ivec3), the scalar z for two vec2
math::length(v), math::length_squared(v)v.length(), v.length_squared()float (length_squared of an ivec is an exact int)
math::normalize(v)v.normalized()a unit float vector; the zero vector stays zero
math::distance(a, b), math::distance_squared(a, b)a.distance_to(b)float (distance_squared of ivecs is an int)
math::reflect(v, n)v reflected about the normal n
math::project(v, onto)v projected onto onto (onto zero is zero)
math::angle_between(a, b)radians; 0 when either is zero
math::perpendicular(v)a vec2 or ivec2 turned a quarter counterclockwise
math::rotate(v, angle), math::rotate(v, axis, angle)v.rotated(angle), v.rotated(axis, angle)a vec2 by an angle, a vec3 about an axis (radians)
jaiscript
math::vec3 v = math::vec3(3, 4, 0);
math::vec3 x = math::vec3(1, 0, 0);
math::vec3 y = math::vec3(0, 1, 0);
print(math::dot(v, x), " ", v.dot(y), " ", math::cross(x, y), " ", x.cross(y));
print(v.length(), " ", math::length_squared(v), " ", v.normalized(), " ", math::normalize(math::vec3()));
print(math::distance(math::vec2(0, 0), math::vec2(3, 4)), " ", math::vec2(1, 1).distance_to(math::vec2(4, 5)), " ", math::distance_squared(math::vec2(1, 1), math::vec2(4, 5)));
print(math::reflect(math::vec2(1, -1), math::vec2(0, 1)), " ", math::project(math::vec2(2, 3), math::vec2(4, 0)));
print(math::angle_between(math::vec2(1, 0), math::vec2(0, 1)), " ", math::perpendicular(math::vec2(1, 2)));
print(math::round(math::rotate(math::vec2(2, 0), math::PI / 2)), " ", math::round(x.rotated(math::vec3(0, 0, 1), math::PI / 2)));

var d = math::dot(math::ivec2(2, 3), math::ivec2(4, 5));
print(d, " ", type_of(d), " ", math::ivec2(3, 4).length_squared(), " ", math::ivec2(3, 4).length(), " ", math::cross(math::vec2(1, 2), math::vec2(3, 4)));
try { var q = math::cross(math::vec4(), math::vec4()); } catch (e) { print(e); }
output
3.0 4.0 (0.0, 0.0, 1.0) (0.0, 0.0, 1.0)
5.0 25.0 (0.6, 0.8, 0.0) (0.0, 0.0, 0.0)
5.0 5.0 25.0
(1.0, 1.0) (2.0, 0.0)
1.5707963267948966 (-2.0, 1.0)
(0.0, 2.0) (0.0, 1.0, 0.0)
23 int 25 5.0 -2.0
math::cross expects two vec2 or two vec3 vectors

The methods are the free functions with the receiver first; a color has none of them. Dimensions must agree, and the errors name the shape they expected (math::dot expects two vectors of the same dimensions, math::rotate expects (vec2, angle) or (vec3, axis, angle)).

Colors​

A color's channels are straight (not premultiplied) alpha. Arithmetic, math::clamp, math::min, math::max and the rounding functions work on all four channels; a color combines only with colors and scalars, never with a vec4. math::mix and math::lerp blend colors in the OKLab perceptual space, so a blend keeps its brightness instead of dipping through gray; math::mix_rgb blends each channel directly and math::mix_hsv travels the shorter way around the hue wheel. Alpha always blends linearly, and the eased mixes ease the same perceptual blend.

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));         // per channel
print(math::mix_hsv(red, blue, 0.5));         // around the hue wheel
print(math::mix(math::color(0, 0, 0, 0), math::color(0, 0, 0, 1), 0.25).a);   // alpha blends linearly
print(math::color(0.25, 0.5, 0.5, 1) * 2, " ", math::clamp(math::color(2, -1, 0.5), 0.0, 1.0));
try { var q = math::color() + 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)
0.25
(0.5, 1.0, 1.0, 2.0) (1.0, 0.0, 0.5, 1.0)
Invalid operands for + operator

Arrays and maps of vectors​

array<math::vec3> (and the same for every math type) is a packed typed buffer: the components sit side by side, an element reads as a copy, element components store in place, and the element boundary converts the other family of the same dimensions. Converting a whole array between the families copies element by element. An ivec makes a natural map key.

jaiscript
array<math::vec3> pts = [math::vec3(1, 2, 3), math::vec3(4, 5, 6)];
pts.push(math::ivec3(7, 8, 9));                 // the element boundary converts
pts[0].x += 10;
for (auto& p : pts) { p.z = 0; }
print(pts.join(" "), " ", pts.size());

array<math::ivec3> cells = pts;                 // converts by value, element by element
print(cells.join(" "));

map<math::ivec2, string> tiles;
tiles[math::ivec2(1, 2)] = "door";
print(tiles[math::ivec2(1, 2)]);

try { pts.push(math::vec2(1, 2)); } catch (e) { print(e); }
try { pts.sort(); } catch (e) { print(e); }
output
(11.0, 2.0, 0.0) (4.0, 5.0, 0.0) (7.0, 8.0, 0.0) 3
(11, 2, 0) (4, 5, 0) (7, 8, 0)
door
Cannot push 'math::vec2' to array<math::vec3>

Text and JSON​

to_string, print and string concatenation write a vector as its components in parentheses, floats in the shortest text that reads back the same 32-bit value. An array of vectors prints its elements, [(1.0, 2.0), (3.0, 4.0)]. 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
math::vec3 v = math::vec3(1, 0.5, -2);
print(to_string(v), " ", "v = " + v, " ", math::vec2(0.1, 1e30));
string j = to_json(v);
print(j);
var back = from_json(j);
print(back, " ", type_of(back));
print(to_json(math::color(1, 0.5, 0, 1)));
var restored = from_base64(to_base64(math::ivec2(-7, 3)));
print(restored, " ", type_of(restored));
output
(1.0, 0.5, -2.0) v = (1.0, 0.5, -2.0) (0.1, 1e+30)
{"$type":"math::vec3","$val":{"x":1.0,"y":0.5,"z":-2.0}}
(1.0, 0.5, -2.0) math::vec3
{"$type":"math::color","$val":{"r":1.0,"g":0.5,"b":0.0,"a":1.0}}
(-7, 3) math::ivec2

What the checker catches​

Under strict checking (Types That Fail Early) the checker knows every math type, its components and methods and the library's result types, so these fail the load before anything runs:

jaiscript
math::vec3 v = 5;
math::vec2 flat;
flat.z = 1;
math::color tint;
float red = tint.x;
float len = math::floor(v);
var n = v.dot();
output
jaiscript: static check failed:
<script>:1:12: error: cannot initialize 'v' declared 'math::vec3' with 'int'
    math::vec3 v = 5;
               ^
<script>:3:6: error: math::vec2 has no member 'z'
    flat.z = 1;
         ^
<script>:5:18: error: math::color has no member 'x'
    float red = tint.x;
                     ^
<script>:6:7: error: cannot initialize 'len' declared 'float' with 'math::vec3'
    float len = math::floor(v);
          ^
<script>:7:15: error: 'dot' expects 1 argument
    var n = v.dot();
                  ^

Mixed kinds and dimensions in arithmetic, a zero divisor and an int32 overflow depend on values, so they are run-time errors, catchable like any other.

Where to next​

That completes the language tour. From here: Embedding covers the C++ side, including Math Adapters, which make a host's own vector and color types arrive in script as these types; Examples walks complete programs; and the Reference is the one-page lookup.