Home Reference Source Test

test/functions.spec.js

import expect from 'expect';
import {
  vec,
  add,
  addAll,
  addScaled,
  sub,
  subAll,
  subScaled,
  sumsqr,
  scalar,
  norm,
  scale,
  dist,
  dir,
  orth2,
  descr,
  angle2,
  angle,
  dot,
  cross,
  cross2,
  triple,
  clamp,
  step,
  smoothstep,
  mix,
  round,
  lerp,
  deg,
  rad,
  nrad,
  seq,
  seqI,
  isNullVec,
} from '../src/functions';
import { PI, TAU, SPI, QPI } from '../src/const';
import { Vector, vec2, vec3, vec4 } from '../src/vector';
import { Matrix, mat2 } from '../src/matrix';

describe('functions.js', () => {
  it('should be able to create a vector from two points', () => {
    const p1 = [-1, 0, 2];
    const p2 = [2, -3, 1];

    expect(vec(p1, p2)).toEqual([3, -3, -1]);
    // supply empty array as target
    expect(vec(p1, p2, [])).toEqual([3, -3, -1]);
    // supply array with a specific length
    expect(vec(p1, p2, new Array(2))).toEqual([3, -3]);
    // supply vector as target
    expect(vec(p1, p2, new Vector(3))).toBeInstanceOf(Vector);
  });

  it('should be able to add and subtract vectors/arrays', () => {
    /* single */
    const v1 = vec3(1, 2, 3);
    const v2 = vec3(2, -2, -1);

    const a1 = [1, 2, 3];
    const a2 = [2, -2, -1];

    let res = add(v1, v2, new Vector());
    expect(res).toEqual([3, 0, 2]);
    expect(res).toBeInstanceOf(Vector);

    res = add(a1, a2, new Vector());
    expect(res).toEqual([3, 0, 2]);
    expect(res).toBeInstanceOf(Vector);

    // only add first two components into a 2d vector
    res = add(v1, v2, new Vector(2));
    expect(res).toEqual([3, 0]);

    res = add(a1, a2, new Vector(2));
    expect(res).toEqual([3, 0]);

    // add scaled vector
    res = addScaled(v1, v2, 2, new Vector(3));
    expect(res).toEqual([5, -2, 1]);

    // sub scaled vector
    res = subScaled(v1, v2, 2, new Vector(3));
    expect(res).toEqual([-3, 6, 5]);

    // mutate first argument by not specifying a target
    res = add(v1, v2);
    expect(res).toBe(v1);
    expect(v1).toEqual([3, 0, 2]);

    // mutate scaled
    v1.set(1, 2, 3);
    res = addScaled(v1, v2, 2);
    expect(res).toEqual([5, -2, 1]);
    expect(res).toBe(v1);
    expect(v2).toEqual([2, -2, -1]); // should not be mutated

    v1.set(1, 2, 3);
    res = subScaled(v1, v2, 2);
    expect(res).toEqual([-3, 6, 5]);
    expect(res).toBe(v1);
    expect(v2).toEqual([2, -2, -1]); // should not be mutated

    res = sub(a1, v2, new Vector());
    expect(res).toEqual([-1, 4, 4]);
    expect(res).toBeInstanceOf(Vector);

    // target will be overwritten and not part of the sum
    sub(a1, v2, new Vector(10, 10, 10));
    expect(res).toEqual([-1, 4, 4]);
    expect(res).toBeInstanceOf(Vector);

    /* multiple */
    const vectors = [
      vec2(1, -1),
      vec2(3, 2),
      vec2(-6, 0.5),
      vec2(-3.7, 4.5),
    ];

    const arrays = [
      [1, -1],
      [3, 2],
      [-6, 0.5],
      [-3.7, 4.5],
    ];

    res = addAll(vectors, vec2());
    expect(res).toBeInstanceOf(Vector);
    expect(res).toEqual([-5.7, 6]);

    expect(vectors[0]).toEqual([1, -1]);

    // we're adding a target to avoid mutation of the first element in vectors
    res = addAll(vectors, vec2());
    expect(res).toEqual([-5.7, 6]);
    expect(vectors[0]).toEqual([1, -1]);

    // this will mutate the first vector in vectors
    res = addAll(vectors);
    expect(vectors[0]).toEqual([-5.7, 6]);

    // vectors can also be plain arrays.
    res = addAll(arrays, vec2());
    expect(res).toBeInstanceOf(Vector);
    expect(res).toEqual([-5.7, 6]);

    // return result as plain array
    res = addAll(arrays, [0, 0]);
    expect(res).toBeInstanceOf(Array);
    expect(res).toEqual([-5.7, 6]);

    // or allow the first array to be mutated
    const from = vec2(5, 5);
    res = subAll(from, arrays);
    expect(res).toBeInstanceOf(Array);
    expect(res).toBe(from);
    expect(res).toEqual([10.7, -1]);

    arrays[0] = [1, -1]; // reset to original value

    // subtracts all following vectors/arrays from the first parameter
    res = subAll([10, 10], [[5, 6], [3, -1], [0, 5]]);
    expect(res).toBeInstanceOf(Array);
    expect(res).toEqual([2, 0]);

    // we're adding a target to avoid mutation of the first element in vectors
    res = subAll(arrays[0], arrays.slice(1), []);
    expect(arrays[0]).toEqual([1, -1]);
    expect(res).toEqual([7.7, -8]);

    // target will NOT be part of sum when subtracting a list
    res = subAll(vec2(5, 6), [[3, -1], [0, 5]], [100, 100]);
    expect(res).toEqual([2, 2]);

    res = subAll(vec2(5, 6), [[3, -1], [0, 5]]);
    expect(res).toBeInstanceOf(Vector);
    expect(res).toEqual([2, 2]);
  });

  it('can calculate the sum of squares', () => {
    expect(sumsqr([1, 3, 4])).toBe(26);
    expect(sumsqr(vec3(-3, -3, 2))).toBe(22);
  });

  it('can calculate the scalar of a vector/array', () => {
    expect(scalar([1, 3, 4])).toBeCloseTo(Math.sqrt(26));
    expect(scalar(vec3(-3, -3, 2))).toBeCloseTo(Math.sqrt(22));
  });

  it('can normalize a vector/array', () => {
    const vector = [2, 3.4, -2];
    const expected = [0.4522156, 0.76876657, -0.4522156];
    let actual = norm(vector, new Vector()); // immutable

    expect(actual).not.toBe(vector);
    expect(actual).toBeInstanceOf(Vector);
    actual.forEach((d, i) => expect(d).toBeCloseTo(expected[i]));

    actual = norm(vector); // mutate vector

    expect(actual).toBe(vector);
    expect(actual).toBeInstanceOf(Array);
    actual.forEach((d, i) => expect(d).toBeCloseTo(expected[i]));
  });

  it('can scale a vector/array by a factor', () => {
    expect(scale([1, 2, 3], 3)).toEqual([3, 6, 9]); // mutating
    expect(scale([1, 2, 3], -1, [])).toEqual([-1, -2, -3]); // immutable
  });

  it('should find the distance between two points', () => {
    expect(dist([10, 0], [-10, 0])).toBe(20);
    expect(dist(vec3(10, 2, -5), [-3, 0, 2])).toBeCloseTo(14.899664);
  });

  it('should find the directional unit vector between two points', () => {
    expect(dir([10, 0], [-10, 0])).toEqual([-1, 0]);

    let expected = [-0.872503, -0.134231, 0.469809];
    dir(vec3(10, 2, -5), [-3, 0, 2]).forEach((v, i) => expect(v).toBeCloseTo(expected[i]));

    expected = [0.707107, -0.707107];
    dir([0, 3], [3, 0]).forEach((v, i) => expect(v).toBeCloseTo(expected[i]));

    // immutable
    dir([0, 3], [3, 0], []).forEach((v, i) => expect(v).toBeCloseTo(expected[i]));
  });

  it('should describe relationships between two points', () => {
    let res = descr([10, -3], [8, 2]);
    expect(res.vector).toBeInstanceOf(Array);
    expect(res.unit).toBeInstanceOf(Array);
    expect(res.vector).toEqual([-2, 5]);
    expect(res.sqr).toBe(29);
    expect(res.dist).toBeCloseTo(5.3851648);
    const expected = [-0.37139068, 0.9284767];
    res.unit.forEach((d, i) => expect(d).toBeCloseTo(expected[i]));

    res = descr([10, -3, 1], [8, 2, 0], new Vector());
    expect(res.vector).toBeInstanceOf(Vector);
    expect(res.unit).toBeInstanceOf(Vector);
    expect(res.vector.x).toBe(-2);
    expect(res.vector.y).toBe(5);
    expect(res.vector.z).toBe(-1);
    expect(res.sqr).toBe(30);
    expect(res.dist).toBeCloseTo(5.4772256);
  });

  it('should find the an orthogonal unit vector', () => {
    expect(orth2([0, 1])).toEqual([-1, 0]);
    expect(orth2([1, 0])).toEqual([-0, 1]);

    let expected = [-1 / Math.sqrt(2), 1 / Math.sqrt(2)];
    orth2([1, 1]).forEach((v, i) => expect(v).toBeCloseTo(expected[i]));

    expected = [-2 / Math.sqrt(20), 4 / Math.sqrt(20)];
    orth2([4, 2]).forEach((v, i) => expect(v).toBeCloseTo(expected[i]));

    expected = [0.049938, -0.99875];

    const vector = vec([10, 2], [-10, 1]);
    orth2(vector).forEach((v, i) => expect(v).toBeCloseTo(expected[i]));
  });

  it('should be able to calculate the vector dot product', () => {
    expect(dot([2, -2, 1], [-3.5, 1.5, -3])).toBe(-13);
    expect(dot([1, 2, 2, 0], [3, -4, 0, -1])).toBe(-5);
    expect(dot([0.1, 2, -1.4, 2], [2, 0.2, 2, 2.83])).toBeCloseTo(3.46, 5);
  });

  it('should be able to calculate the psudo cross product of 2d vectors', () => {
    expect(cross2(vec2(1, 2), vec2(1, 0))).toBe(-2);
    expect(cross2(vec2(1, 2), [-5, 2])).toBe(12);
    expect(cross2([1, 0], vec2(0, 1))).toBe(1);
    expect(cross2([2, 0], [1, 0])).toBe(0);
  });

  it('should be able to find the cross and triple product of 3d vectors', () => {
    expect(cross([1, 0, 0], [0, 1, 0])).toEqual([0, 0, 1]);

    const i = vec3(1, 0, 0);
    const j = vec3(0, 1, 0);
    const k = vec3(0, 0, 1);

    const res = vec3();
    expect(cross(i, j, res)).toEqual(k);
    expect(cross(k, i, res)).toEqual(j);
    expect(cross(j, k, res)).toEqual(i);

    expect(triple(i, j, k)).toBe(1);
    expect(triple(k, i, j)).toBe(k.dot(i.cross(j)));
  });

  it('Can find angles of 2d vectors', () => {
    expect(angle2(vec2(1, 0))).toBe(0);
    expect(angle2(vec2(1, 1))).toBe(Math.PI / 4);
    expect(angle2(vec2(0, 1))).toBe(Math.PI / 2);
    expect(angle2(vec2(-1, 0))).toBe(Math.PI);
    expect(angle2(vec2(-1, -1))).toBe(-0.75 * Math.PI);
    expect(angle2(vec2(0, -1))).toBe(-0.5 * Math.PI);
    expect(angle2(vec2(0, 0))).toBe(0);
  });

  it('Can find angles of 3d vectors', () => {
    const v = new Vector(2, 1, 2, 0);

    expect(angle(v)).toBe(angle(v, 0));
    expect(angle(v)).toBeCloseTo(0.841069, 5);
    expect(angle(v, 1)).toBeCloseTo(1.23096, 5);
    expect(angle(v, 2)).toBeCloseTo(0.84107, 5);
    expect(angle(v, 3)).toBeUndefined();
    expect(angle(v, -1)).toBeUndefined();
  });

  it('should be able to clamp values and vectors/arrays', () => {
    expect(clamp(-1, 0, 1)).toBe(0);
    expect(clamp(2, 0, 1)).toBe(1);
    expect(clamp(2, 0, 3)).toBe(2);

    expect(clamp([0.32, 6.2, 3.7], 0.4, 5.8)).toEqual([0.4, 5.8, 3.7]);
    expect(clamp(vec3(0.32, 6.2, 3.7), 0.2, 6)).toEqual([0.32, 6, 3.7]);

    const v1 = vec4(1, 2, 3, 4);
    const v2 = vec4(6, -4, 0, 2);

    expect(clamp(v1, 1.25, 3.5)).toEqual(vec4(1.25, 2, 3, 3.5));
    expect(clamp(v2, 1.25, 3.5)).toEqual(vec4(3.5, 1.25, 1.25, 2));
    expect(clamp(
      [
        1, 2, 3,
        4, 5, 6,
        7, 8, 9,
      ],
      2.5,
      8.2,
    )).toEqual([
      2.5, 2.5, 3,
      4, 5, 6,
      7, 8, 8.2,
    ]);
    expect(clamp(-1)).toBe(0);
    expect(clamp(-1, 1)).toBe(1);
    expect(clamp(1.25)).toBe(1);
    expect(clamp(1.25, 0, 0.9)).toBe(0.9);
  });

  it('Can generate threshold values with step and smoothstep functions (as in GLSL)', () => {
    expect(step(0.5, 0.4)).toBe(0);
    expect(step(0.5, 0.5)).toBe(1);
    expect(step(0.5, 0.6)).toBe(1);

    expect(step(vec3(1, 0.5, 2), vec3(2, 0.2, 1))).toEqual(vec3(1, 0, 0));

    expect(smoothstep(0.2, 0.6, 0.1)).toBe(0);
    expect(smoothstep(0.2, 0.6, 0.2)).toBe(0);
    expect(smoothstep(0.2, 0.6, 0.21)).toBeGreaterThan(0);
    expect(smoothstep(0.2, 0.6, 0.4)).toBeCloseTo(0.5);
    expect(smoothstep(0.2, 0.6, 0.6)).toBe(1);
    expect(smoothstep(0.2, 0.6, 0.7)).toBe(1);
  });

  it('Can mix vectors, matrices and values', () => {
    // mix works on arrays, meaning also vectors and matrices
    const v1 = vec4(1, 2, 3, 4);
    const v2 = vec4(6, -4, 0, 2);
    const res = new Vector(4);
    expect(mix(v1, v2, 0, res)).toEqual(v1);
    expect(mix(v1, v2, 1, res)).toEqual(v2);
    expect(mix(v1, v2, 0.25, res)).toEqual(vec4(2.25, 0.5, 2.25, 3.5));
    expect(mix(v1, v2, 0.5, res)).toEqual(vec4(3.5, -1, 1.5, 3));
    expect(mix(v1, v2, 0.75, res)).toEqual(vec4(4.75, -2.5, 0.75, 2.5));
    expect(mix(v1, v2, vec4(0, 0.25, 0.5, 0.75), res)).toEqual(vec4(1, 0.5, 1.5, 2.5));

    const m = mat2(-7, 2, 0, 2);
    expect(mix(m, Matrix.identity(2), 0.5)).toEqual(mat2(-3, 1, 0, 1.5));

    // lerp is for mixing primitive numbers
    expect(lerp(10, 20, 0.5)).toBe(15);
  });

  it('Can round vectors and values', () => {
    expect(round(1.2253455, 2)).toBe(1.23);
    expect(round(1.2253455, 1)).toBe(1.2);
    expect(round(1.2253455, 6)).toBe(1.225346);
    const v = vec4(1.12345, 2.23321, 3.321, 4.033);
    expect(round(v, 1)).toEqual(vec4(1.1, 2.2, 3.3, 4));
  });

  it('Can convert between degrees and radians', () => {
    expect(deg(0)).toBe(0);
    expect(deg(PI)).toBe(180);
    expect(deg(-PI)).toBe(-180);
    expect(deg(SPI)).toBe(90);
    expect(deg(-SPI)).toBe(-90);
    expect(deg(QPI)).toBe(45);
    expect(deg(-QPI)).toBe(-45);
    expect(deg(TAU)).toBe(360);
    expect(deg(-TAU)).toBe(-360);

    expect(rad(0)).toBe(0);
    expect(rad(180)).toBe(PI);
    expect(rad(-180)).toBe(-PI);
    expect(rad(90)).toBe(SPI);
    expect(rad(-90)).toBe(-SPI);
    expect(rad(45)).toBe(QPI);
    expect(rad(-45)).toBe(-QPI);
    expect(rad(360)).toBe(TAU);
    expect(rad(-360)).toBe(-TAU);

    expect(nrad(-QPI)).toBe(TAU - QPI);
    expect(nrad(-SPI)).toBe(TAU - SPI);
    expect(nrad(-TAU)).toBe(-0);
    expect(nrad(-TAU - SPI)).toBe(TAU - SPI);
  });

  it('can create a sequence of interpolated values', () => {
    let a = 100;
    let b = 450;

    let expected = [100, 450];

    seq(a, b, 2).forEach((v, i) => expect(v).toBe(expected[i]));

    expected = [100, 150, 200, 250, 300, 350, 400, 450];
    seq(a, b, 8).forEach((v, i) => expect(v).toBeCloseTo(expected[i]));

    // start at t=0.5 to t=0.75, same amount of steps
    expected = [275, 287.5, 300, 312.5, 325, 337.5, 350, 362.5];
    seq(a, b, 8, 0.5, 0.75).forEach((v, i) => expect(v).toBeCloseTo(expected[i]));

    a = 0;
    b = 1;
    expected = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
    seq(a, b, 11).forEach((v, i) => expect(v).toBeCloseTo(expected[i]));

    // array
    a = [0, 0, 0];
    b = [10, -20, 100];
    expected = [
      [0, 0, 0],
      [2.5, -5, 25],
      [5, -10, 50],
      [7.5, -15, 75],
      [10, -20, 100],
    ];
    seq(a, b, 5).forEach((v, i) => expect(v).toEqual(expected[i]));

    expected = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
    // optimized for 0..1 sequences
    seqI(11).forEach((v, i) => expect(v).toBeCloseTo(expected[i], 10));

  });

  it('should allow easy check for null vectors', () => {
    expect(isNullVec([0, 0, 0])).toBeTruthy();
    expect(isNullVec([-0, -0, -0])).toBeTruthy();
    expect(isNullVec([-1, -2, -3])).toBeFalsy();
    expect(isNullVec([0.00001, 0, 0])).toBeFalsy();
    expect(isNullVec([-0.00001, -0, -0], 0.001)).toBeTruthy();
  });
});