avocado-old/packages/math/vector/index.js
2020-06-23 11:19:59 -05:00

429 lines
9.1 KiB
JavaScript

import {Range as MathRange} from '../range';
import {randomNumber} from '../math';
export {VectorMixin as Mixin} from './mixin';
export const SQRT_2_2 = Math.sqrt(2) / 2;
export const EIGHTH_PI = Math.PI * 0.125;
export const QUARTER_PI = EIGHTH_PI * 2;
export const HALF_PI = QUARTER_PI * 2;
export const TWO_PI = Math.PI * 2;
// export function {VectorMixin as Mixin} from './mixin'
// Scale a vector. This multiplies *x* and *y* by **k**.
//
// avocado> Vector.scale [.5, 1.5], 2
// [1, 3]
export function scale(v, k) {
return [v[0] * k, v[1] * k];
}
// Add two vectors.
//
// avocado> Vector.add [1, 2], [1, 1]
// [2, 3]
export function add(l, r) {
return [l[0] + r[0], l[1] + r[1]];
}
// Subtract two vectors.
//
// avocado> Vector.sub [9, 5], [5, 2]
// [4, 3]
export function sub(l, r) {
return [l[0] - r[0], l[1] - r[1]];
}
// Multiply two vectors.
//
// avocado> Vector.mul [3, 5], [5, 5]
// [15, 25]
export function mul(l, r) {
return [l[0] * r[0], l[1] * r[1]];
}
export function distance(l, r) {
const xd = l[0] - r[0];
const yd = l[1] - r[1];
return Math.sqrt(xd * xd + yd * yd);
}
// Divide two vectors.
//
// avocado> Vector.div [15, 5], [5, 5]
// [3, 1]
export function div(l, r) {
return [l[0] / r[0], l[1] / r[1]];
}
// Modulo divide two vectors.
//
// avocado> Vector.mod [13, 6], [5, 5]
// [3, 1]
export function mod(l, r) {
return [l[0] % r[0], l[1] % r[1]];
}
// Get the magnitude between two point vectors.
//
// avocado> Vector.magnitude [0, 0], [1, 1]
// 1.4142135623730951
export function magnitude(l, r) {
return Math.sqrt(dot(sub(l, r)));
}
// Get the minimum values from two vectors.
//
// avocado> Vector.min [-10, 10], [0, 0]
// [-10, 0]
export function min(l, r) {
return [Math.min(l[0], r[0]), Math.min(l[1], r[1])];
}
// Get the maximum values from two vectors.
//
// avocado> Vector.max [-10, 10], [0, 0]
// [0, 10]
export function max(l, r) {
return [Math.max(l[0], r[0]), Math.max(l[1], r[1])];
}
// Clamp a vector's axes using a min vector and a max vector.
//
// avocado> Vector.clamp [-10, 10], [0, 0], [5, 5]
// [0, 5]
export function clamp(v, min_, max_) {
return min(max_, max(min_, v));
}
// Returns a deep copy of the vector.
//
// avocado> vector = [0, 0]
// avocado> otherVectory Vector.copy vector
// avocado> vector is otherVector
// false
export function copy(v) {
return [v[0], v[1]];
}
// Check whether a vector equals another vector.
//
// avocado> Vector.equals [4, 4], [5, 4]
// false
//
// avocado> Vector.equals [4, 4], [4, 4]
// true
export function equals(l, r) {
return l[0] === r[0] && l[1] === r[1];
}
export function equalsClose(l, r, epsilon = 0.000001) {
return (
(Math.abs(l[0] - r[0]) < epsilon)
&& (Math.abs(l[1] - r[1]) < epsilon)
);
}
// Checks whether a vector is [0, 0].
//
// avocado> Vector.zero [0, 0]
// true
//
// avocado> Vector.zero [0, 1]
// false
export function isZero(v) {
return v[0] === 0 && v[1] === 0;
}
// Round both axes of a vector.
//
// avocado> Vector.round [3.14, 4.70]
// [3, 5]
export function round(v) {
return [Math.round(v[0]), Math.round(v[1])];
}
// Get the dot product of two vectors. If only one gievn, dupe it.
//
// avocado> Vector.dot [2, 3], [4, 5]
// 23
export function dot(l, r) {
if (!r) {
r = l;
}
return l[0] * r[0] + l[1] * r[1];
}
// Get a unit vector.
//
// avocado> Vector.normalize [.5, .7]
// [0.5812381937190965, 0.813733471206735]
export function normalize(v) {
const dp = dot(v);
if (0 === dp) {
return [0, 0];
}
const normalized = scale(v, 1 / Math.sqrt(dp));
// Don't let NaN poison our equations.
return [
NaN === normalized[0] ? 0 : normalized[0],
NaN === normalized[1] ? 0 : normalized[1],
];
}
// Get the absolute values of the axes of a vector.
//
// avocado> Vector.abs [23, -5.20]
// [23, 5.20]
export function abs(v) {
return [Math.abs(v[0]), Math.abs(v[1])];
}
// Floor both axes of a vector.
//
// avocado> Vector.floor [3.14, 4.70]
// [3, 4]
export function floor(v) {
return [Math.floor(v[0]), Math.floor(v[1])];
}
// Ceiling both axes of a vector.
//
// avocado> Vector.floor [3.14, 4.70]
// [3, 4]
export function ceil(v) {
return [Math.ceil(v[0]), Math.ceil(v[1])];
}
// Get the area a vector.
//
// avocado> Vector.area [3, 6]
// 18
export function area(v) {
return v[0] * v[1];
}
// Checks whether a vector is null. A vector is null if either axis is 0.
// The algorithm prefers horizontal directions to vertical; if you move
// up-right or down-right you'll face right.
//
// avocado> Vector.isNull [1, 0]
// true
//
// avocado> Vector.isNull [1, 1]
// false
export function isNull(v) {
if (!v) {
return true;
}
if (2 !== v.length) {
return true;
}
return 0 === v[0] || 0 === v[1];
}
export function overshot(position, normal, destination) {
const overshot = [false, false];
for (let i = 0; i < 2; ++i) {
if (normal[i] < 0) {
if (position[i] < destination[i]) {
overshot[i] = true;
}
}
else if (normal[i] > 0) {
if (position[i] > destination[i]) {
overshot[i] = true;
}
}
}
return overshot;
}
// Convert a vector to a 4-direction. A 4-direction is:
//
// * 0: Up
// * 1: Right
// * 2: Down
// * 3: Left
//
// avocado> Vector.toDirection4 [0, 1]
// 2
//
// avocado> Vector.toDirection4 [1, 0]
// 1
export function toDirection4(vector) {
vector = normalize(vector);
const x = Math.abs(vector[0]) - SQRT_2_2;
if (x > 0 && x < SQRT_2_2) {
return vector[0] > 0 ? 1 : 3;
}
else {
return vector[1] > 0 ? 2 : 0;
}
}
export function toAngle(v) {
return (Math.atan2(-v[1], v[0]) + Math.PI * 2) % (Math.PI * 2);
}
export function fromAngle(angle) {
return [Math.cos(angle), -Math.sin(angle)];
}
// Convert a vector to an 8-direction. An 8-direction is:
//
// * 0: Up
// * 1: Right
// * 2: Down
// * 3: Left
// * 4: Up-Right
// * 5: Down-Right
// * 6: Down-Left
// * 7: Up-Left
//
// avocado> Vector.toDirection8 [1, 1]
// 5
//
// avocado> Vector.toDirection8 [1, 0]
// 1
export function toDirection8(v) {
v = normalize(v);
// Orient radians.
let rad = (TWO_PI + angle(v)) % TWO_PI;
rad = (rad + HALF_PI) % TWO_PI;
// Truncate.
rad = Math.floor(rad * 100000) / 100000;
const stepStart = TWO_PI - EIGHTH_PI;
// Clockwise direction map.
const directions = [0, 4, 1, 5, 2, 6, 3, 7];
for (const direction of directions) {
let stepEnd = (stepStart + QUARTER_PI) % TWO_PI
stepEnd = Math.floor(stepEnd * 100000) / 100000;
if (rad >= stepStart && rad < stepEnd) {
return direction;
}
stepStart = stepEnd;
}
return 0;
}
// Convert a vector to a *directionCount*-direction.
//
// avocado> Vector.toDirection [0, 1], 4
// 2
export function toDirection(vector, directionCount) {
switch (directionCount) {
case 1: return 0;
case 4: return toDirection4(vector);
case 8: return toDirection8(vector);
default:
throw new Error `Unsupported conversion of vector to ${
directionCount
}-direction.`;
}
}
export function fromDirection(direction) {
switch (direction) {
case 0: return [0, -1];
case 1: return [1, 0];
case 2: return [0, 1];
case 3: return [-1, 0];
case 4: return normalize([1, -1]);
case 5: return normalize([1, 1]);
case 6: return normalize([-1, 1]);
case 7: return normalize([-1, -1]);
}
}
export function directionalProjection(direction, vector) {
switch (direction) {
case 0:
return [vector[0], -vector[1]];
case 1:
return [vector[1], vector[0]];
case 2:
return [vector[0], vector[1]];
case 3:
return [-vector[1], vector[0]];
}
}
export function interpolate(ultimate, actual, easing = 0) {
if (0 === easing) {
return ultimate;
}
const magnitude_ = magnitude(ultimate, actual);
if (0 === magnitude_) {
return ultimate;
}
return add(
actual,
scale(
normalize(sub(ultimate, actual)),
magnitude_ / easing
)
);
}
export function fromObject(object) {
return [object.x, object.y];
}
// Convert a vector to an object.
//
// avocado> Vector.toObject [3, 4]
// {x: 3, y: 4}
export function toObject(v) {
return {x: v[0], y: v[1]};
}
export function unpackFromUint32(uint32) {
return [
uint32 & 0xFFFF,
uint32 >> 16,
];
}
export function packToUint32(v) {
const x = Math.min(65535, Math.max(0, v[0]));
const y = Math.min(65535, Math.max(0, v[1]));
return (y << 16) | x;
}
export class Range extends MathRange {
value() {
if ('undefined' === typeof this.max) {
return this.min;
}
else {
return [
randomNumber(this.min[0], this.max[0], false),
randomNumber(this.min[1], this.max[1], false),
];
}
}
}
export function behaviorTypes() {
return {
type: 'Vector',
children: {
fromDirection: {
type: 'vector',
label: 'Vector from direction $1.',
args: [
['direction', {
type: 'number',
}],
],
},
},
};
}