toosoon-utils
Version:
Utility functions & classes
331 lines (330 loc) • 12.2 kB
JavaScript
import { EPSILON, PI, TWO_PI } from './constants';
import { lerp } from './maths';
/**
* Convert a radians value into degrees
*
* @param {number} radians Angle in radians
* @returns {number} Angle in degrees
*/
export function toDegrees(radians) {
return (radians * 180) / PI;
}
/**
* Convert a degrees value into radians
*
* @param {number} degrees Angle in degrees
* @returns {number} Angle in radians
*/
export function toRadians(degrees) {
return (degrees * PI) / 180;
}
/**
* Calculate the angle from a point to another
*
* @param {number} x1 X-axis coordinate of the start point
* @param {number} y1 Y-axis coordinate of the start point
* @param {number} x2 X-axis coordinate of the end point
* @param {number} y2 Y-axis coordinate of the end point
* @returns {number} Angle
*/
export function angle(x1, y1, x2, y2) {
return Math.atan2(y2 - y1, x2 - x1);
}
/**
* Find the closest angle between to angles
*
* @param {number} source Source angle (in radians)
* @param {number} target Target angle (in radians)
* @returns {number} Closest angle
*/
export function closestAngle(source, target) {
const delta = target - source;
return delta > PI ? target - 2 * PI : target < -PI ? delta + 2 * PI : target;
}
/**
* Calculate the distance between two points
*
* @param {number} x1 X-axis coordinate of the first point
* @param {number} y1 Y-axis coordinate of the first point
* @param {number} x2 X-axis coordinate of the second point
* @param {number} y2 Y-axis coordinate of the second point
* @returns {number} Computed distance
*/
export function distance(x1, y1, x2, y2) {
const dx = x1 - x2;
const dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* Calculate the dot product of two vectors
*
* @param {number} x1 X-axis coordinate of the first vector
* @param {number} y1 Y-axis coordinate of the first vector
* @param {number} x2 X-axis coordinate of the second vector
* @param {number} y2 Y-axis coordinate of the second vector
* @returns {number} Computed dot product
*/
export function dot(x1, y1, x2, y2) {
return x1 * x2 + y1 * y2;
}
/**
* Calculate the cross product of two vectors
*
* @param {number} x1 X-axis coordinate of the first vector
* @param {number} y1 Y-axis coordinate of the first vector
* @param {number} x2 X-axis coordinate of the second vector
* @param {number} y2 Y-axis coordinate of the second vector
* @returns {number} Computed cross product
*/
export function cross(x1, y1, x2, y2) {
return x1 * x2 - y1 * y2;
}
/**
* Calculate the length of the diagonal of a rectangle
*
* @param {number} width Width of the rectangle
* @param {number} height Height of the rectangle
* @returns {number} Diagonal length
*/
export function diagonal(width, height) {
return Math.sqrt(width * width + height * height);
}
/**
* Convert radians to a 3D point on the surface of a unit sphere
*
* @param {number} radius Radius of the sphere
* @param {number} phi Polar angle from the y (up) axis : [0, PI]
* @param {number} theta Equator angle around the y (up) axis : [0, 2*PI]
* @param {Point3} target Target 3D point
* @returns {Point3}
*/
export function radToSphere(radius, phi, theta, target = [0, 0, 0]) {
target[0] = radius * Math.sin(phi) * Math.sin(theta);
target[1] = radius * Math.cos(phi);
target[2] = radius * Math.sin(phi) * Math.cos(theta);
return target;
}
// *********************
// Curves
// *********************
/**
* Interpolate a point on a line
*
* @param {number} t Normalized time value to interpolate
* @param {number} x1 X-axis coordinate of the start point
* @param {number} y1 Y-axis coordinate of the start point
* @param {number} x2 X-axis coordinate of the end point
* @param {number} y2 Y-axis coordinate of the end point
* @returns {Point} Interpolated coordinates on the line
*/
export function line(t, x1, y1, x2, y2) {
const x = lerp(t, x1, x2);
const y = lerp(t, y1, y2);
return [x, y];
}
/**
* Interpolate a point on a Quadratic Bézier curve
*
* @param {number} t Normalized time value to interpolate
* @param {number} x1 X-axis coordinate of the start point
* @param {number} y1 Y-axis coordinate of the start point
* @param {number} cpx X-axis coordinate of the control point
* @param {number} cpy Y-axis coordinate of the control point
* @param {number} x2 X-axis coordinate of the end point
* @param {number} y2 Y-axis coordinate of the end point
* @returns {Point} Interpolated coordinates on the curve
*/
export function quadraticBezier(t, x1, y1, cpx, cpy, x2, y2) {
const t2 = t * t;
const k = 1 - t;
const k2 = k * k;
const x = k2 * x1 + 2 * k * t * cpx + t2 * x2;
const y = k2 * y1 + 2 * k * t * cpy + t2 * y2;
return [x, y];
}
/**
* Interpolate a point on a Cubic Bézier curve
*
* @param {number} t Normalized time value to interpolate
* @param {number} x1 X-axis coordinate of the start point
* @param {number} y1 Y-axis coordinate of the start point
* @param {number} cp1x X-axis coordinate of the first control point
* @param {number} cp1y Y-axis coordinate of the first control point
* @param {number} cp2x X-axis coordinate of the second control point
* @param {number} cp2y Y-axis coordinate of the second control point
* @param {number} x2 X-axis coordinate of the end point
* @param {number} y2 Y-axis coordinate of the end point
* @returns {Point} Interpolated coordinates on the curve
*/
export function cubicBezier(t, x1, y1, cp1x, cp1y, cp2x, cp2y, x2, y2) {
const t2 = t * t;
const t3 = t2 * t;
const k = 1 - t;
const k2 = k * k;
const k3 = k2 * k;
const x = k3 * x1 + 3 * k2 * t * cp1x + 3 * k * t2 * cp2x + t3 * x2;
const y = k3 * y1 + 3 * k2 * t * cp1y + 3 * k * t2 * cp2y + t3 * y2;
return [x, y];
}
/**
* Interpolate a point on a Catmull-Rom spline
*
* @param {number} t Normalized time value to interpolate
* @param {number} x1 X-axis coordinate of the start point
* @param {number} y1 Y-axis coordinate of the start point
* @param {number} cp1x X-axis coordinate of the first control point
* @param {number} cp1y Y-axis coordinate of the first control point
* @param {number} cp2x X-axis coordinate of the second control point
* @param {number} cp2y Y-axis coordinate of the second control point
* @param {number} x2 X-axis coordinate of the end point
* @param {number} y2 Y-axis coordinate of the end point
* @returns {Point} Interpolated coordinates on the spline
*/
export function catmullRom(t, x1, y1, cp1x, cp1y, cp2x, cp2y, x2, y2) {
const t2 = t * t;
const t3 = t2 * t;
const vx1 = (cp2x - x1) * 0.5;
const vy1 = (cp2y - y1) * 0.5;
const vx2 = (x2 - cp1x) * 0.5;
const vy2 = (y2 - cp1y) * 0.5;
const x = (2 * cp1x - 2 * cp2x + vx1 + vx2) * t3 + (-3 * cp1x + 3 * cp2x - 2 * vx1 - vx2) * t2 + vx1 * t + cp1x;
const y = (2 * cp1y - 2 * cp2y + vy1 + vy2) * t3 + (-3 * cp1y + 3 * cp2y - 2 * vy1 - vy2) * t2 + vy1 * t + cp1y;
return [x, y];
}
/**
* Interpolate a point on an elliptical arc
*
* @param {number} t Normalized time value to interpolate
* @param {number} cx X-axis coordinate of the center of the ellipse
* @param {number} cy Y-axis coordinate of the center of the ellipse
* @param {number} rx X-radius of the ellipse
* @param {number} ry Y-radius of the ellipse
* @param {number} [rotation=0] Rotation angle of the ellipse (in radians), counterclockwise from the positive X-axis
* @param {number} [startAngle=0] Start angle of the arc (in radians)
* @param {number} [endAngle=2*PI] End angle of the arc (in radians)
* @param {boolean} [counterclockwise=false] Flag indicating the direction of the arc
* @returns {Point} Interpolated coordinates on the arc
*/
export function ellipse(t, cx, cy, rx, ry, rotation = 0, startAngle = 0, endAngle = TWO_PI, counterclockwise = false) {
let deltaAngle = endAngle - startAngle;
const isEmpty = Math.abs(deltaAngle) <= EPSILON;
while (deltaAngle < 0)
deltaAngle += TWO_PI;
while (deltaAngle > TWO_PI)
deltaAngle -= TWO_PI;
if (deltaAngle <= EPSILON) {
if (isEmpty) {
deltaAngle = 0;
}
else {
deltaAngle = TWO_PI;
}
}
if (counterclockwise === true && !isEmpty) {
if (deltaAngle === TWO_PI) {
deltaAngle = -TWO_PI;
}
else {
deltaAngle = deltaAngle - TWO_PI;
}
}
const angle = startAngle + t * deltaAngle;
let x = cx + rx * Math.cos(angle);
let y = cy + ry * Math.sin(angle);
if (rotation !== 0) {
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
const deltaX = x - cx;
const deltaY = y - cy;
x = deltaX * cos - deltaY * sin + cx;
y = deltaX * sin + deltaY * cos + cy;
}
return [x, y];
}
/**
* Interpolate a point on a circular arc
*
* @param {number} t Normalized time value to interpolate
* @param {number} cx X-axis coordinate of the center of the circle
* @param {number} cy Y-axis coordinate of the center of the circle
* @param {number} radius Radius of the circle
* @param {number} [startAngle] Start angle of the arc (in radians)
* @param {number} [endAngle] End angle of the arc (in radians)
* @param {boolean} [counterclockwise=false] Flag indicating the direction of the arc
* @returns {Point} Interpolated coordinates on the arc
*/
export function arc(t, cx, cy, radius, startAngle, endAngle, counterclockwise) {
return ellipse(t, cx, cy, radius, radius, 0, startAngle, endAngle, counterclockwise);
}
/**
* Check if two points are coincident
*
* @param {number} x1 X-axis coordinate of the first point
* @param {number} y1 Y-axis coordinate of the first point
* @param {number} x2 X-axis coordinate of the second point
* @param {number} y2 Y-axis coordinate of the second point
* @returns {boolean} True if the two are coincident, false otherwise
*/
export function isCoincident(x1, y1, x2, y2) {
return distance(x1, y1, x2, y2) <= EPSILON;
}
/**
* Check if three points are collinear (aligned on the same line)
*
* @param {number} x1 X-axis coordinate of the first point
* @param {number} y1 Y-axis coordinate of the first point
* @param {number} x2 X-axis coordinate of the second point
* @param {number} y2 Y-axis coordinate of the second point
* @param {number} x3 X-axis coordinate of the third point
* @param {number} y3 Y-axis coordinate of the third point
* @returns {boolean} True if the three points are collinear, false otherwise
*/
export function isCollinear(x1, y1, x2, y2, x3, y3) {
return Math.abs((x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2)) <= EPSILON;
}
/**
* Make a target fit a container
*
* @param {object} target Dimensions of the target
* @param {object} container Dimensions of the container
* @param {'contain'|'cover'} mode Can be 'contain' | 'cover'
* @returns {object}
*/
function fit(target, container, mode) {
const ratioWidth = container.width / target.width;
const ratioHeight = container.height / target.height;
let scale;
if (mode === 'contain') {
scale = ratioWidth < ratioHeight ? ratioWidth : ratioHeight;
}
else {
scale = ratioWidth > ratioHeight ? ratioWidth : ratioHeight;
}
return {
left: (container.width - target.width * scale) >> 1,
top: (container.height - target.height * scale) >> 1,
width: target.width * scale,
height: target.height * scale,
scale
};
}
/**
* Make a target fit a container (cover mode)
*
* @param {FitInput} target Dimensions of the target
* @param {FitInput} container Dimensions of the container
* @returns {FitOutput}
*/
export function cover(target, container) {
return fit(target, container, 'cover');
}
/**
* Make a target fit a container (contain mode)
*
* @param {FitInput} target Dimensions of the target
* @param {FitInput} container Dimensions of the container
* @returns {FitOutput}
*/
export function contain(target, container) {
return fit(target, container, 'contain');
}