@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
347 lines • 11.4 kB
JavaScript
/**
* Math utility class providing common mathematical operations.
* Access via the exported `Mathf` constant.
*
* @example
* ```ts
* import { Mathf } from "@needle-tools/engine";
*
* // Random number between 0 and 10
* const rand = Mathf.random(0, 10);
*
* // Clamp a value
* const clamped = Mathf.clamp(value, 0, 100);
*
* // Smooth interpolation
* const smoothed = Mathf.lerp(start, end, t);
* ```
*/
class MathHelper {
random(arrayOrMin, max) {
if (Array.isArray(arrayOrMin)) {
if (arrayOrMin.length <= 0)
return null;
return arrayOrMin[Math.floor(Math.random() * arrayOrMin.length)];
}
else {
if (arrayOrMin !== undefined && max !== undefined) {
return Math.random() * (max - arrayOrMin) + arrayOrMin;
}
}
return Math.random();
}
/**
* Fills a Vector3 with random values.
* @param target Vector3 to fill with random values
* @param min Minimum value for each component
* @param max Maximum value for each component
*/
randomVector3(target, min = 0, max = 1) {
target.x = this.random(min, max);
target.y = this.random(min, max);
target.z = this.random(min, max);
}
/**
* Clamps a value between min and max.
* @param value Value to clamp
* @param min Minimum bound
* @param max Maximum bound
* @returns Clamped value
*/
clamp(value, min, max) {
if (value < min) {
return min;
}
else if (value > max) {
return max;
}
return value;
}
/**
* Clamps a value between 0 and 1.
* @param value Value to clamp
* @returns Value clamped to [0, 1]
*/
clamp01(value) {
return this.clamp(value, 0, 1);
}
/**
* Linearly interpolates between two values.
* @param value1 Start value (returned when t=0)
* @param value2 End value (returned when t=1)
* @param t Interpolation factor, clamped to [0, 1]
* @returns Interpolated value
*/
lerp(value1, value2, t) {
t = t < 0 ? 0 : t;
t = t > 1 ? 1 : t;
return value1 + (value2 - value1) * t;
}
/**
* Calculates the linear interpolation parameter that produces the given value.
* Inverse of lerp: if `lerp(a, b, t) = v`, then `inverseLerp(a, b, v) = t`
* @param value1 Start value
* @param value2 End value
* @param t The value to find the parameter for
* @returns The interpolation parameter (may be outside [0,1] if t is outside [value1, value2])
*/
inverseLerp(value1, value2, t) {
return (t - value1) / (value2 - value1);
}
/**
* Remaps a value from one range to another.
* @param value The value to remap.
* @param min1 The minimum value of the current range.
* @param max1 The maximum value of the current range.
* @param min2 The minimum value of the target range.
* @param max2 The maximum value of the target range.
*/
remap(value, min1, max1, min2, max2) {
return min2 + (max2 - min2) * (value - min1) / (max1 - min1);
}
/**
* Moves a value towards a target by a maximum step amount.
* Useful for smooth following or gradual value changes.
* @param value1 Current value
* @param value2 Target value
* @param amount Maximum step to move (positive moves toward target)
* @returns New value moved toward target, never overshooting
*/
moveTowards(value1, value2, amount) {
value1 += amount;
if (amount < 0 && value1 < value2)
value1 = value2;
else if (amount > 0 && value1 > value2)
value1 = value2;
return value1;
}
Rad2Deg = 180 / Math.PI;
Deg2Rad = Math.PI / 180;
Epsilon = 0.00001;
/**
* Converts radians to degrees
*/
toDegrees(radians) {
return radians * 180 / Math.PI;
}
/**
* Converts degrees to radians
*/
toRadians(degrees) {
return degrees * Math.PI / 180;
}
tan(radians) {
return Math.tan(radians);
}
gammaToLinear(gamma) {
return Math.pow(gamma, 2.2);
}
linearToGamma(linear) {
return Math.pow(linear, 1 / 2.2);
}
/**
* Checks if two vectors are approximately equal within epsilon tolerance.
* Works with Vector2, Vector3, Vector4, and Quaternion.
* @param v1 First vector
* @param v2 Second vector
* @param epsilon Tolerance for comparison (default: Number.EPSILON)
* @returns True if all components are within epsilon of each other
*/
approximately(v1, v2, epsilon = Number.EPSILON) {
for (const key of vectorKeys) {
const a = v1[key];
const b = v2[key];
if (a === undefined || b === undefined)
break;
const diff = Math.abs(a - b);
if (diff > epsilon) {
return false;
}
}
return true;
}
/**
* Easing function: slow start, fast middle, slow end (cubic).
* @param x Input value from 0 to 1
* @returns Eased value from 0 to 1
*/
easeInOutCubic(x) {
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
}
}
;
const vectorKeys = ["x", "y", "z", "w"];
export const Mathf = new MathHelper();
class LowPassFilter {
y;
s;
alpha = 0;
constructor(alpha) {
this.setAlpha(alpha);
this.y = null;
this.s = null;
}
setAlpha(alpha) {
if (alpha <= 0 || alpha > 1.0) {
throw new Error();
}
this.alpha = alpha;
}
filter(value, alpha) {
if (alpha) {
this.setAlpha(alpha);
}
let s;
if (!this.y) {
s = value;
}
else {
s = this.alpha * value + (1.0 - this.alpha) * this.s;
}
this.y = value;
this.s = s;
return s;
}
lastValue() {
return this.y;
}
reset(value) {
this.y = value;
this.s = value;
}
}
/**
* [OneEuroFilter](https://engine.needle.tools/docs/api/OneEuroFilter) is a low-pass filter designed to reduce jitter in noisy signals while maintaining low latency.
* It's particularly useful for smoothing tracking data from XR controllers, hand tracking, or other input devices where the signal contains noise but responsiveness is important.
*
* The filter automatically adapts its smoothing strength based on the signal's velocity:
* - When the signal moves slowly, it applies strong smoothing to reduce jitter
* - When the signal moves quickly, it reduces smoothing to maintain responsiveness
*
* Based on the research paper: [1€ Filter: A Simple Speed-based Low-pass Filter for Noisy Input](http://cristal.univ-lille.fr/~casiez/1euro/)
*
* @example Basic usage with timestamp
* ```ts
* const filter = new OneEuroFilter(120, 1.0, 0.0);
*
* // In your update loop:
* const smoothedValue = filter.filter(noisyValue, this.context.time.time);
* ```
*
* @example Without timestamps (using frequency estimate)
* ```ts
* // Assuming 60 FPS update rate
* const filter = new OneEuroFilter(60, 1.0, 0.5);
*
* // Call without timestamp - uses the frequency estimate
* const smoothedValue = filter.filter(noisyValue);
* ```
*
* @example Smoothing 3D positions
* ```ts
* const posFilter = new OneEuroFilterXYZ(90, 0.5, 0.0);
*
* posFilter.filter(trackedPosition, smoothedPosition, this.context.time.time);
* ```
*
* @see {@link OneEuroFilterXYZ} for filtering 3D vectors
*/
export class OneEuroFilter {
/**
* An estimate of the frequency in Hz of the signal (> 0), if timestamps are not available.
*/
freq;
/**
* Min cutoff frequency in Hz (> 0). Lower values allow to remove more jitter.
*/
minCutOff;
/**
* Parameter to reduce latency (> 0). Higher values make the filter react faster to changes.
*/
beta;
/**
* Used to filter the derivates. 1 Hz by default. Change this parameter if you know what you are doing.
*/
dCutOff;
/**
* The low-pass filter for the signal.
*/
x;
/**
* The low-pass filter for the derivates.
*/
dx;
/**
* The last time the filter was called.
*/
lasttime;
/** Create a new OneEuroFilter
* @param freq - An estimate of the frequency in Hz of the signal (> 0), if timestamps are not available.
* @param minCutOff - Min cutoff frequency in Hz (> 0). Lower values allow to remove more jitter.
* @param beta - Parameter to reduce latency (> 0). Higher values make the filter react faster to changes.
* @param dCutOff - Used to filter the derivates. 1 Hz by default. Change this parameter if you know what you are doing.
*/
constructor(freq, minCutOff = 1.0, beta = 0.0, dCutOff = 1.0) {
if (freq <= 0 || minCutOff <= 0 || dCutOff <= 0) {
throw new Error();
}
this.freq = freq;
this.minCutOff = minCutOff;
this.beta = beta;
this.dCutOff = dCutOff;
this.x = new LowPassFilter(this.alpha(this.minCutOff));
this.dx = new LowPassFilter(this.alpha(this.dCutOff));
this.lasttime = null;
}
alpha(cutOff) {
const te = 1.0 / this.freq;
const tau = 1.0 / (2 * Math.PI * cutOff);
return 1.0 / (1.0 + tau / te);
}
/** Filter your value: call with your value and the current timestamp (e.g. from this.context.time.time) */
filter(x, time = null) {
if (this.lasttime && time) {
this.freq = 1.0 / (time - this.lasttime);
}
this.lasttime = time;
const prevX = this.x.lastValue();
const dx = !prevX ? 0.0 : (x - prevX) * this.freq;
const edx = this.dx.filter(dx, this.alpha(this.dCutOff));
const cutOff = this.minCutOff + this.beta * Math.abs(edx);
return this.x.filter(x, this.alpha(cutOff));
}
reset(x) {
if (x != undefined)
this.x.reset(x);
this.x.alpha = this.alpha(this.minCutOff);
this.dx.alpha = this.alpha(this.dCutOff);
this.lasttime = null;
}
}
export class OneEuroFilterXYZ {
x;
y;
z;
/** Create a new OneEuroFilter
* @param freq - An estimate of the frequency in Hz of the signal (> 0), if timestamps are not available.
* @param minCutOff - Min cutoff frequency in Hz (> 0). Lower values allow to remove more jitter.
* @param beta - Parameter to reduce latency (> 0). Higher values make the filter react faster to changes.
* @param dCutOff - Used to filter the derivates. 1 Hz by default. Change this parameter if you know what you are doing.
*/
constructor(freq, minCutOff = 1.0, beta = 0.0, dCutOff = 1.0) {
this.x = new OneEuroFilter(freq, minCutOff, beta, dCutOff);
this.y = new OneEuroFilter(freq, minCutOff, beta, dCutOff);
this.z = new OneEuroFilter(freq, minCutOff, beta, dCutOff);
}
filter(value, target, time = null) {
target.x = this.x.filter(value.x, time);
target.y = this.y.filter(value.y, time);
target.z = this.z.filter(value.z, time);
}
reset(value) {
this.x.reset(value?.x);
this.y.reset(value?.y);
this.z.reset(value?.z);
}
}
//# sourceMappingURL=engine_math.js.map