Vec2 & Vec3
The SDK's vector types. Fields are plain mutable numbers (v.x = 3 works), but every method is
pure — a.add(b) returns a NEW vector and never touches a. The only mutators are set and
copy. Both types share the same API; Vec3 adds 3D-only operations (cross product as a vector,
quaternion/matrix transforms). See math value semantics.
Everywhere a vector is accepted, a raw tuple works too (Vec2Like / Vec3Like): a wrapper, a
[x, y] / [x, y, z] tuple, a number[], or a Float32Array. Vectors are iterable, so they
spread and destructure like tuples.
At a glance
const toTarget = new Vec3(target.position).sub(pos)
const dir = toTarget.normalize() // pure — toTarget is untouched
pos = pos.add(dir.scale(speed * dt)) // reassign; add() returns a new vector
node.position = [0, 1, 0] // raw tuples work wherever a Vec3 is accepted
const [x, y, z] = node.position // vectors destructure like tuples
if (toTarget.lengthSq() < 4) attack() // cheaper than length() for range checksCreating
new Vec2(x?, y?) // components; default (0, 0)
new Vec2([3, 4]) // or copy any Vec2Like
new Vec3(x?, y?, z?) // default (0, 0, 0)
Vec3.from(v: Vec3Like): Vec3
Vec2.zero / .one / .up / .down / .left / .right
Vec3.zero / .one / .up / .down / .left / .right / .forward / .backthe static constants are getters — each access returns a fresh instance, so mutating
one is safe. Vec3.forward is −Z (cameras look down −Z), Vec3.back is +Z. Vec2.up is
+Y (2D world space is Y-up).
Mutation — the only two methods that change this
v.set(x, y): this // Vec3: set(x, y, z)
v.copy(other: Vec2Like): thisArithmetic (pure — each returns a new vector)
a.add(b) a.sub(b) a.mul(b) a.div(b) // component-wise; b may be a tuple
a.scale(s) // multiply by a scalar
a.scaleAndAdd(b, s) // a + b·s in one call
a.negate()
a.withX(x) a.withY(y) // copy with one component replaced (Vec3: withZ)Products & measures
a.dot(b): number
a.length(): number a.lengthSq(): number
a.distanceTo(b): number a.distanceSqTo(b): number
a.angle(b): number // angle between the vectors, RADIANS (0 if either is zero)
a.normalize(): Vec // unit vector; a zero vector normalizes to zeroPrefer the Sq variants for comparisons — they skip the square root.
Interpolation & clamping
a.lerp(b, t) // component-wise blend, t usually 0..1
a.clamp(min, max) // component-wise clamp between two vectors
a.min(b) a.max(b) // component-wise min / maxVec2 only
a.cross(b): number // scalar z of the 3D cross — signed parallelogram area
a.perp(): Vec2 // (-y, x): 90° CCW perpendicular
a.rotate(rad, origin?) // rotate CCW by RADIANS around origin (default [0, 0])
a.heading(): number // atan2(y, x) — the vector's angle in RADIANSVec3 only
a.cross(b): Vec3
a.reflect(normal) // mirror about a UNIT normal: v − 2(v·n)n
a.project(onto) // vector projection of a onto `onto` (zero `onto` → zero)
a.rotateX(rad, origin?) // rotate the point around an axis through origin, RADIANS
a.rotateY(rad, origin?)
a.rotateZ(rad, origin?)
a.rotate(q) // rotate by a Quat or raw [x, y, z, w]
a.transform(m) // transform as a POINT by a Mat4 / raw column-major length-16 array
// — applies translation and the perspective divideFor a direction (no translation), use Mat4.transformDirection instead of
transform.
Interop
a.equals(b, eps? /* 1e-6 */): boolean // relative-epsilon compare
a.clone(): Vec
a.toArray(): [number, number] // Vec3: [number, number, number]
const [x, y] = a; [...a] // iterablePitfalls
// ✗ expecting methods to mutate — they never do
dir.normalize() // discards the result; dir unchanged
dir = dir.normalize() // ✓ reassign
// ✗ mutating a node transform copy — getters return fresh values
node.position.x = 3 // silent no-op (see conventions)
node.x = 3 // ✓See also
- Mathf — scalar helpers (
clamp,damp,smoothDampon arrays) - Quat — rotations;
Vec3.rotate(q)isQuat.rotateVec3from the other side - Mat4 — full transforms;
transformPoint/transformDirection - Conventions — value semantics, tuple interop