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).
| Type | Components | Also named | Default |
|---|---|---|---|
math::vec2 | x y (32-bit float) | width height | (0.0, 0.0) |
math::vec3 | x y z (32-bit float) | width height depth | (0.0, 0.0, 0.0) |
math::vec4 | x y z w (32-bit float) | width height depth | (0.0, 0.0, 0.0, 0.0) |
math::ivec2 | x y (32-bit int) | width height | (0, 0) |
math::ivec3 | x y z (32-bit int) | width height depth | (0, 0, 0) |
math::color | r 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.
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));(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:
using math::vec3, math::ivec2;
vec3 up = vec3(0, 1, 0);
ivec2 tile = ivec2(5, 9);
print(up, " ", tile, " ", type_of(up));(0.0, 1.0, 0.0) (5, 9) math::vec3Components
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:
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(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 falseA 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:
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);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.
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));(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):
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); }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:
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);(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:
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);(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.
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); }(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::colorThe 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.
| Function | Does |
|---|---|
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::round | per 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 |
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); }(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 dimensionsResults 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
| Function | Method | Result |
|---|---|---|
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) |
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); }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 vectorsThe 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.
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); }(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 + operatorArrays 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.
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); }(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.
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));(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::ivec2What 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:
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();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.