ixfx
Version:
Bundle of ixfx libraries
1,657 lines (1,643 loc) • 103 kB
JavaScript
import { n as __exportAll } from "./chunk-CaR5F9JI.js";
import { Dt as isEqualValueDefault, M as byWord, N as levenshteinOps, h as intervalToMs, j as byCharacter, l as resolveWithFallbackSync } from "./src-BUqDa_u7.js";
import { C as numberTest, M as resultThrow, n as stringTest, s as functionTest, v as integerTest } from "./src-C_hvyftg.js";
import { n as SimpleEventEmitter } from "./src-CRR1VQls.js";
import { F as interpolateAngle, P as interpolate$1, f as scale, rt as clamp } from "./src-Cebc3sfq.js";
import { Bt as sum, C as interpolator, Ct as Empty, Dt as angleRadian, Et as compare, H as angleConvert, Lt as getEdgeX, Ot as abs, Rt as getEdgeY, S as cubic, St as interpolate$2, T as toPath, Tt as divide, U as angleParse, V as toCartesian, Vt as distance, _t as normalise, bt as multiplyScalar, dt as toRadian, et as isAngleTypeConvertible, gt as pipelineApply, ht as pipeline, tt as radianArc, vt as clampMagnitude, w as quadraticSimple, wt as Unit, xt as invert, yt as multiply, zt as subtract } from "./src-DyTd46TV.js";
import { O as repeat, d as elapsedTicksAbsolute, f as frequencyTimer, g as relative, h as ofTotalTicks, m as ofTotal, r as StateMachineWithEvents, u as elapsedMillisecondsAbsolute, y as timerWithFunction } from "./src-C3g81yvt.js";
import { T as floatSource, w as float } from "./src-BH_hkHiA.js";
//#region ../packages/modulation/src/cubic-bezier.ts
/**
* Creates an easing function using a simple cubic bezier defined by two points.
*
* Eg: https://cubic-bezier.com/#0,1.33,1,-1.25
* a:0, b: 1.33, c: 1, d: -1.25
*
* ```js
* import { Easings } from "@ixfx/modulation.js";
* // Time-based easing using bezier
* const e = Easings.time(fromCubicBezier(1.33, -1.25), 1000);
* e.compute();
* ```
* @param b
* @param d
* @returns Value
*/
const cubicBezierShape = (b, d) => (t) => {
const s = 1 - t;
const s2 = s * s;
const t2 = t * t;
const t3 = t2 * t;
return 3 * b * s2 * t + 3 * d * s * t2 + t3;
};
//#endregion
//#region ../packages/modulation/src/drift.ts
/**
* WIP
* Returns a {@link Drifter} that moves a value over time.
*
* It keeps track of how much time has elapsed, accumulating `driftAmtPerMs`.
* The accumulated drift is wrapped on a 0..1 scale.
* ```js
* // Set up the drifer
* const d = drif(0.001);
*
* d.update(1.0);
* // Returns 1.0 + accumulated drift
* ```
* @param driftAmtPerMs
* @returns
*/
const drift = (driftAmtPerMs) => {
let lastChange = performance.now();
const update = (v = 1) => {
const amt = driftAmtPerMs * (performance.now() - lastChange) % 1;
lastChange = performance.now();
return (v + amt) % 1;
};
const reset = () => {
lastChange = performance.now();
};
return {
update,
reset
};
};
//#endregion
//#region ../packages/modulation/src/gaussian.ts
const pow$1 = Math.pow;
const gaussianA = 1 / Math.sqrt(2 * Math.PI);
/**
* Returns a roughly gaussian easing function
* ```js
* const fn = Easings.gaussian();
* ```
*
* Try different positive and negative values for `stdDev` to pinch
* or flatten the bell shape.
* @param standardDeviation
* @returns
*/
const gaussian = (standardDeviation = .4) => {
const mean = .5;
return (t) => {
const f = gaussianA / standardDeviation;
let p = -2.5;
let c = (t - mean) / standardDeviation;
c *= c;
p *= c;
const v = f * pow$1(Math.E, p);
if (v > 1) return 1;
if (v < 0) return 0;
return v;
};
};
//#endregion
//#region ../packages/modulation/src/easing/easings-named.ts
var easings_named_exports = /* @__PURE__ */ __exportAll({
arch: () => arch,
backIn: () => backIn,
backInOut: () => backInOut,
backOut: () => backOut,
bell: () => bell,
bounceIn: () => bounceIn,
bounceInOut: () => bounceInOut,
bounceOut: () => bounceOut,
circIn: () => circIn,
circInOut: () => circInOut,
circOut: () => circOut,
cubicIn: () => cubicIn,
cubicOut: () => cubicOut,
elasticIn: () => elasticIn,
elasticInOut: () => elasticInOut,
elasticOut: () => elasticOut,
expoIn: () => expoIn,
expoInOut: () => expoInOut,
expoOut: () => expoOut,
quadIn: () => quadIn,
quadInOut: () => quadInOut,
quadOut: () => quadOut,
quartIn: () => quartIn,
quartOut: () => quartOut,
quintIn: () => quintIn,
quintInOut: () => quintInOut,
quintOut: () => quintOut,
sineIn: () => sineIn,
sineInOut: () => sineInOut,
sineOut: () => sineOut,
smootherstep: () => smootherstep,
smoothstep: () => smoothstep
});
const sqrt = Math.sqrt;
const pow = Math.pow;
const cos = Math.cos;
const pi = Math.PI;
const sin = Math.sin;
const bounceOut = (x) => {
const n1 = 7.5625;
const d1 = 2.75;
if (x < 1 / d1) return n1 * x * x;
else if (x < 2 / d1) return n1 * (x -= 1.5 / d1) * x + .75;
else if (x < 2.5 / d1) return n1 * (x -= 2.25 / d1) * x + .9375;
else return n1 * (x -= 2.625 / d1) * x + .984375;
};
const quintIn = (x) => x * x * x * x * x;
const quintOut = (x) => 1 - pow(1 - x, 5);
const arch = (x) => x * (1 - x) * 4;
const smoothstep = (x) => x * x * (3 - 2 * x);
const smootherstep = (x) => (x * (x * 6 - 15) + 10) * x * x * x;
const sineIn = (x) => 1 - cos(x * pi / 2);
const sineOut = (x) => sin(x * pi / 2);
const quadIn = (x) => x * x;
const quadOut = (x) => 1 - (1 - x) * (1 - x);
const sineInOut = (x) => -(cos(pi * x) - 1) / 2;
const quadInOut = (x) => x < .5 ? 2 * x * x : 1 - pow(-2 * x + 2, 2) / 2;
const cubicIn = (x) => x * x * x;
const cubicOut = (x) => 1 - pow(1 - x, 3);
const quartIn = (x) => x * x * x * x;
const quartOut = (x) => 1 - pow(1 - x, 4);
const expoIn = (x) => x === 0 ? 0 : pow(2, 10 * x - 10);
const expoOut = (x) => x === 1 ? 1 : 1 - pow(2, -10 * x);
const quintInOut = (x) => x < .5 ? 16 * x * x * x * x * x : 1 - pow(-2 * x + 2, 5) / 2;
const expoInOut = (x) => x === 0 ? 0 : x === 1 ? 1 : x < .5 ? pow(2, 20 * x - 10) / 2 : (2 - pow(2, -20 * x + 10)) / 2;
const circIn = (x) => 1 - sqrt(1 - pow(x, 2));
const circOut = (x) => sqrt(1 - pow(x - 1, 2));
const backIn = (x) => {
const c1 = 1.70158;
return (c1 + 1) * x * x * x - c1 * x * x;
};
const backOut = (x) => {
const c1 = 1.70158;
return 1 + (c1 + 1) * pow(x - 1, 3) + c1 * pow(x - 1, 2);
};
const circInOut = (x) => x < .5 ? (1 - sqrt(1 - pow(2 * x, 2))) / 2 : (sqrt(1 - pow(-2 * x + 2, 2)) + 1) / 2;
const backInOut = (x) => {
const c2 = 1.70158 * 1.525;
return x < .5 ? pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2) / 2 : (pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
};
const elasticIn = (x) => {
const c4 = 2 * pi / 3;
return x === 0 ? 0 : x === 1 ? 1 : -pow(2, 10 * x - 10) * sin((x * 10 - 10.75) * c4);
};
const elasticOut = (x) => {
const c4 = 2 * pi / 3;
return x === 0 ? 0 : x === 1 ? 1 : pow(2, -10 * x) * sin((x * 10 - .75) * c4) + 1;
};
const bounceIn = (x) => 1 - bounceOut(1 - x);
const bell = gaussian();
const elasticInOut = (x) => {
const c5 = 2 * pi / 4.5;
return x === 0 ? 0 : x === 1 ? 1 : x < .5 ? -(pow(2, 20 * x - 10) * sin((20 * x - 11.125) * c5)) / 2 : pow(2, -20 * x + 10) * sin((20 * x - 11.125) * c5) / 2 + 1;
};
const bounceInOut = (x) => x < .5 ? (1 - bounceOut(1 - 2 * x)) / 2 : (1 + bounceOut(2 * x - 1)) / 2;
//#endregion
//#region ../packages/modulation/src/easing/line.ts
/**
* Interpolates points along a line.
* By default it's a straight line, so use `bend` to make a non-linear curve.
* @param bend -1...1. -1 will pull line up, 1 will push it down.
* @returns
*/
const line = (bend = 0, warp = 0) => {
const max = 1;
const cubicB = {
x: scale(bend, -1, 1, 0, max),
y: scale(bend, -1, 1, max, 0)
};
let cubicA = interpolate$2(Math.abs(bend), Empty, cubicB);
if (bend !== 0 && warp > 0) if (bend > 0) cubicA = interpolate$2(warp, cubicA, {
x: 0,
y: cubicB.x * 2
});
else cubicA = interpolate$2(warp, cubicA, {
x: cubicB.y * 2,
y: 0
});
const bzr = cubic(Empty, Unit, cubicA, cubicB);
const inter = interpolator(bzr);
return (value) => inter(value);
};
//#endregion
//#region ../packages/modulation/src/modulator-timed.ts
/**
* Produce values over time. When the modulate function is complete, the final
* value continues to return. Timer starts when return function is first invoked.
*
* ```js
* const fn = (t) => {
* // 't' will be values 0..1 where 1 represents end of time period.
* // Return some computed value based on 't'
* return t*Math.random();
* }
* const e = Modulate.time(fn, 1000);
*
* // Keep calling e() to get the current value
* e();
* ```
* @param fn Modulate function
* @param duration Duration
* @returns
*/
const time$1 = (fn, duration) => {
resultThrow(functionTest(fn, `fn`));
let relative;
return () => {
if (typeof relative === `undefined`) relative = ofTotal(duration, { clampValue: true });
return fn(relative());
};
};
/**
* Creates an modulator based on clock time. Time
* starts being counted when modulate function is created.
*
* `timeModulator` allows you to reset and check for completion.
* Alternatively, use {@link time} which is a simple function that just returns a value.
*
* @example Time based easing
* ```
* import { timeModulator } from "@ixfx/modulation.js";
* const fn = (t) => {
* // 't' will be a value 0..1 representing time elapsed. 1 being end of period.
* return t*Math.random();
* }
* const t = timeModulator(fn, 5*1000); // Will take 5 seconds to complete
* ...
* t.compute(); // Get current value of modulator
* t.reset(); // Reset to 0
* t.isDone; // _True_ if finished
* ```
* @param fn Modulator
* @param duration Duration
* @returns ModulatorTimed
*/
const timeModulator = (fn, duration) => {
resultThrow(functionTest(fn, `fn`));
const timer = elapsedMillisecondsAbsolute();
const durationMs = intervalToMs(duration);
if (durationMs === void 0) throw new Error(`Param 'duration' not provided`);
const relativeTimer = relative(durationMs, {
timer,
clampValue: true
});
return timerWithFunction(fn, relativeTimer);
};
/**
* Produce modulate values with each invocation. When the time is complete, the final
* value continues to return. Timer starts when return function is first invoked.
*
* If you need to check if a modulator is done or reset it, consider {@link tickModulator}.
*
* ```js
* const fn = (t) => {
* // 't' will be values 0..1 representing elapsed ticks toward totwal
* }
* const e = ticks(fn, 100);
*
* // Keep calling e() to get the current value
* e();
* ```
* @param fn Function that produces 0..1 scale
* @param totalTicks Total length of ticks
* @returns
*/
const ticks$2 = (fn, totalTicks) => {
resultThrow(functionTest(fn, `fn`));
let relative;
return () => {
if (typeof relative === `undefined`) relative = ofTotalTicks(totalTicks, { clampValue: true });
return fn(relative());
};
};
/**
* Creates an modulator based on ticks.
*
* `tickModulator` allows you to reset and check for completion.
* Alternatively, use {@link ticks} which is a simple function that just returns a value.
*
* @example Tick-based modulator
* ```
* import { tickModulator } from "@ixfx/modulation.js";
* const fn = (t) => {
* // 't' will be values 0..1 based on completion
* return Math.random() * t;
* }
* const t = tickModulator(fn, 1000); // Will take 1000 ticks to complete
* t.compute(); // Each call to `compute` progresses the tick count
* t.reset(); // Reset to 0
* t.isDone; // _True_ if finished
* ```
* @param fn Modulate function that returns 0..1
* @param durationTicks Duration in ticks
* @returns ModulatorTimed
*/
const tickModulator = (fn, durationTicks) => {
resultThrow(functionTest(fn, `fn`));
const timer = elapsedTicksAbsolute();
const relativeTimer = relative(durationTicks, {
timer,
clampValue: true
});
return timerWithFunction(fn, relativeTimer);
};
//#endregion
//#region ../packages/modulation/src/easing.ts
var easing_exports = /* @__PURE__ */ __exportAll({
Named: () => easings_named_exports,
create: () => create,
get: () => get,
getEasingNames: () => getEasingNames,
line: () => line,
tickEasing: () => tickEasing,
ticks: () => ticks$1,
time: () => time,
timeEasing: () => timeEasing
});
/**
* Creates an easing function
* ```js
* const e = Easings.create({ duration: 1000, name: `quadIn` });
* const e = Easings.create({ ticks: 100, name: `sineOut` });
* const e = Easings.create({
* duration: 1000,
* fn: (v) => {
* // v will be 0..1 based on time
* return Math.random() * v
* }
* });
* ```
* @param options
* @returns
*/
const create = (options) => {
const fn = resolveEasingName(options.name ?? `quintIn`) ?? options.fn;
if (typeof fn === `undefined`) throw new Error(`Either 'name' or 'fn' must be set`);
if (`duration` in options) return time(fn, options.duration);
else if (`ticks` in options) return ticks$1(fn, options.ticks);
else throw new Error(`Expected 'duration' or 'ticks' in options`);
};
/**
* Creates an easing based on clock time. Time
* starts being counted when easing function is created.
*
* `timeEasing` allows you to reset and check for completion.
* Alternatively, use {@link time} which is a simple function that just returns a value.
*
*
* @example Time based easing
* ```
* const t = Easings.timeEasing(`quintIn`, 5*1000); // Will take 5 seconds to complete
* ...
* t.compute(); // Get current value of easing
* t.reset(); // Reset to 0
* t.isDone; // _True_ if finished
* ```
*
* Thisi function is just a wrapper around Modulator.timedModulator.
* @param nameOrFunction Name of easing, or an easing function
* @param duration Duration
* @returns Easing
*/
const timeEasing = (nameOrFunction, duration) => {
const fn = resolveEasingName(nameOrFunction);
return timeModulator(fn, duration);
};
/**
* Produce easing values over time. When the easing is complete, the final
* value continues to return. Timer starts when return function is first invoked.
*
* If you need to check if an easing is done or reset it, consider {@link timeEasing}.
*
* ```js
* // Quad-in easing over one second
* const e = Easings.time(`quadIn`, 1000);
*
* // Keep calling e() to get the current value
* e();
* ```
*
* This function is just a wrapper around Modulate.time
* @param nameOrFunction Easing name or a function that produces 0..1 scale
* @param duration Duration
* @returns
*/
const time = (nameOrFunction, duration) => {
const fn = resolveEasingName(nameOrFunction);
return time$1(fn, duration);
};
/**
* Produce easing values with each invocation. When the easing is complete, the final
* value continues to return. Timer starts when return function is first invoked.
*
* If you need to check if an easing is done or reset it, consider {@link tickEasing}.
*
* ```js
* // Quad-in easing over 100 ticks
* const e = Easings.ticks(`quadIn`, 100);
*
* // Keep calling e() to get the current value
* e();
* ```
*
* This is just a wrapper around Modulator.ticks
* @param nameOrFunction Easing name or a function that produces 0..1 scale
* @param totalTicks Total length of ticks
* @returns
*/
const ticks$1 = (nameOrFunction, totalTicks) => {
const fn = resolveEasingName(nameOrFunction);
return ticks$2(fn, totalTicks);
};
/**
* Creates an easing based on ticks.
*
* `tickEasing` allows you to reset and check for completion.
* Alternatively, use {@link ticks} which is a simple function that just returns a value.
*
* @example Tick-based easing
* ```
* const t = Easings.tickEasing(`sineIn`, 1000); // Will take 1000 ticks to complete
* t.compute(); // Each call to `compute` progresses the tick count
* t.reset(); // Reset to 0
* t.isDone; // _True_ if finished
* ```
* @param nameOrFunction Name of easing, or an easing function
* @param durationTicks Duration in ticks
* @returns Easing
*/
const tickEasing = (nameOrFunction, durationTicks) => {
const fn = resolveEasingName(nameOrFunction);
return tickModulator(fn, durationTicks);
};
const resolveEasingName = (nameOrFunction) => {
const fn = typeof nameOrFunction === `function` ? nameOrFunction : get(nameOrFunction);
if (typeof fn === `undefined`) throw typeof nameOrFunction === `string` ? /* @__PURE__ */ new Error(`Easing function not found: '${nameOrFunction}'`) : /* @__PURE__ */ new Error(`Easing function not found`);
return fn;
};
/**
* Creates a new easing by name
*
* ```js
* const e = Easings.create(`circInOut`, 1000, elapsedMillisecondsAbsolute);
* ```
* @param nameOrFunction Name of easing, or an easing function
* @param duration Duration (meaning depends on timer source)
* @param timerSource Timer source
* @returns
*/
let easingsMap;
/**
* Returns an easing function by name. Throws an error if
* easing is not found.
*
* ```js
* const fn = Easings.get(`sineIn`);
* // Returns 'eased' transformation of 0.5
* fn(0.5);
* ```
* @param easingName eg `sineIn`
* @returns Easing function
*/
const get = function(easingName) {
resultThrow(stringTest(easingName, `non-empty`, `easingName`));
const found = cacheEasings().get(easingName.toLowerCase());
if (found === void 0) throw new Error(`Easing not found: '${easingName}'`);
return found;
};
function cacheEasings() {
if (easingsMap === void 0) {
easingsMap = /* @__PURE__ */ new Map();
for (const [k, v] of Object.entries(easings_named_exports)) easingsMap.set(k.toLowerCase(), v);
return easingsMap;
} else return easingsMap;
}
/**
* Iterate over available easings.
* @private
* @returns Returns list of available easing names
*/
function* getEasingNames() {
yield* cacheEasings().keys();
}
//#endregion
//#region ../packages/modulation/src/envelope/Types.ts
const adsrStateTransitions = Object.freeze({
attack: [`decay`, `release`],
decay: [`sustain`, `release`],
sustain: [`release`],
release: [`complete`],
complete: null
});
//#endregion
//#region ../packages/modulation/src/envelope/AdsrBase.ts
const defaultAdsrTimingOpts = {
attackDuration: 600,
decayDuration: 200,
releaseDuration: 800,
shouldLoop: false
};
/**
* Base class for an ADSR envelope.
*
* It outputs values on a scale of 0..1 corresponding to each phase.
*/
var AdsrBase = class extends SimpleEventEmitter {
#sm;
#timeSource;
#timer;
#holding;
#holdingInitial;
#disposed = false;
#triggered = false;
attackDuration;
decayDuration;
releaseDuration;
decayDurationTotal;
/**
* If _true_ envelope will loop
*/
shouldLoop;
constructor(opts = {}) {
super();
this.attackDuration = opts.attackDuration ?? defaultAdsrTimingOpts.attackDuration;
this.decayDuration = opts.decayDuration ?? defaultAdsrTimingOpts.decayDuration;
this.releaseDuration = opts.releaseDuration ?? defaultAdsrTimingOpts.releaseDuration;
this.shouldLoop = opts.shouldLoop ?? defaultAdsrTimingOpts.shouldLoop;
this.#sm = new StateMachineWithEvents(adsrStateTransitions, { initial: `attack` });
this.#sm.addEventListener(`change`, (event) => {
if (event.newState === `release` && this.#holdingInitial) this.#timer?.reset();
super.fireEvent(`change`, event);
});
this.#sm.addEventListener(`stop`, (event) => {
super.fireEvent(`complete`, event);
});
this.#timeSource = () => elapsedMillisecondsAbsolute();
this.#holding = this.#holdingInitial = false;
this.decayDurationTotal = this.attackDuration + this.decayDuration;
}
dispose() {
if (this.#disposed) return;
this.#sm.dispose();
}
get isDisposed() {
return this.#disposed;
}
/**
* Changes state based on timer status
* @returns _True_ if state was changed
*/
switchStateIfNeeded(allowLooping) {
if (this.#timer === void 0) return false;
let elapsed = this.#timer.elapsed;
const wasHeld = this.#holdingInitial && !this.#holding;
let hasChanged = false;
let state = this.#sm.state;
do {
hasChanged = false;
state = this.#sm.state;
switch (state) {
case `attack`:
if (elapsed > this.attackDuration || wasHeld) {
this.#sm.next();
hasChanged = true;
}
break;
case `decay`:
if (elapsed > this.decayDurationTotal || wasHeld) {
this.#sm.next();
hasChanged = true;
}
break;
case `sustain`:
if (!this.#holding || wasHeld) {
elapsed = 0;
this.#sm.next();
this.#timer.reset();
hasChanged = true;
}
break;
case `release`:
if (elapsed > this.releaseDuration) {
this.#sm.next();
hasChanged = true;
}
break;
case `complete`: if (this.shouldLoop && allowLooping) this.trigger(this.#holdingInitial);
}
} while (hasChanged && state !== `complete`);
return hasChanged;
}
/**
* Computes a stage's progress from 0-1
* @param allowStateChange
* @returns
*/
computeRaw(allowStateChange = true, allowLooping = true) {
if (this.#timer === void 0) return [
void 0,
0,
this.#sm.state
];
if (allowStateChange) this.switchStateIfNeeded(allowLooping);
const previousStage = this.#sm.state;
const elapsed = this.#timer.elapsed;
let relative = 0;
const state = this.#sm.state;
switch (state) {
case `attack`:
relative = elapsed / this.attackDuration;
break;
case `decay`:
relative = (elapsed - this.attackDuration) / this.decayDuration;
break;
case `sustain`:
relative = 1;
break;
case `release`:
relative = Math.min(elapsed / this.releaseDuration, 1);
break;
case `complete`: return [
`complete`,
1,
previousStage
];
default: throw new Error(`State machine in unknown state: ${state}`);
}
return [
state,
relative,
previousStage
];
}
/**
* Returns _true_ if envelope has finished
*/
get isDone() {
return this.#sm.isDone;
}
onTrigger() {}
/**
* Triggers envelope, optionally _holding_ it.
*
* If `hold` is _false_ (default), envelope will run through all stages,
* but sustain stage won't have an affect.
*
* If `hold` is _true_, it will run to, and stay at the sustain stage.
* Use {@link release} to later release the envelope.
*
* If event is already trigged it will be _retriggered_.
* Initial value depends on `opts.retrigger`
* * _false_ (default): envelope continues at current value.
* * _true_: envelope value resets to `opts.initialValue`.
*
* @param hold If _true_ envelope will hold at sustain stage
*/
trigger(hold = false) {
this.onTrigger();
this.#triggered = true;
this.#sm.reset();
this.#timer = this.#timeSource();
this.#holding = hold;
this.#holdingInitial = hold;
}
get hasTriggered() {
return this.#triggered;
}
compute() {}
/**
* Release if 'trigger(true)' was previouslly called.
* Has no effect if not triggered or held.
* @returns
*/
release() {
if (this.isDone || !this.#holdingInitial) return;
this.#holding = false;
this.compute();
}
};
//#endregion
//#region ../packages/modulation/src/envelope/Adsr.ts
const defaultAdsrOpts = {
attackBend: -1,
decayBend: -.3,
releaseBend: -.3,
peakLevel: 1,
initialLevel: 0,
sustainLevel: .6,
releaseLevel: 0,
retrigger: false
};
var AdsrIterator = class {
constructor(adsr) {
this.adsr = adsr;
}
next(...args) {
if (!this.adsr.hasTriggered) this.adsr.trigger();
const c = this.adsr.compute();
return {
value: c[1],
done: c[0] === `complete`
};
}
[Symbol.toStringTag] = `Generator`;
};
/**
* ADSR (Attack Decay Sustain Release) envelope. An envelope is a value that changes over time,
* usually in response to an intial trigger.
*
* [See the ixfx Guide on Envelopes](https://ixfx.fun/modulation/envelopes/introduction/).
*
* @example Setup
* ```js
* const env = new Envelopes.Adsr({
* attackDuration: 1000,
* decayDuration: 200,
* sustainDuration: 100
* });
* ```
*
* Options for envelope are as follows:
*
* ```js
* initialLevel?: number
* attackBend: number
* attackDuration: number
* decayBend: number
* decayDuration:number
* sustainLevel: number
* releaseBend: number
* releaseDuration: number
* releaseLevel?: number
* peakLevel: number
* retrigger?: boolean
* shouldLoop: boolean
* ```
*
* If `retrigger` is _false_ (default), a re-triggered envelope continues at current value
* rather than resetting to `initialLevel`.
*
* If `shouldLoop` is true, envelope loops until `release()` is called.
*
* @example Using
* ```js
* env.trigger(); // Start envelope
* ...
* // Get current value of envelope
* const [state, scaled, raw] = env.compute();
* ```
*
* * `state` is a string, one of the following: 'attack', 'decay', 'sustain', 'release', 'complete'
* * `scaled` is a value scaled according to the stage's _levels_
* * `raw` is the progress from 0 to 1 within a stage. ie. 0.5 means we're halfway through a stage.
*
* Instead of `compute()`, most usage of the envelope is just fetching the `value` property, which returns the same scaled value of `compute()`:
*
* ```js
* const value = env.value; // Get scaled number
* ```
*
* @example Hold & release
* ```js
* env.trigger(true); // Pass in true to hold
* ...envelope will stop at sustain stage...
* env.release(); // Release into decay
* ```
*
* Check if it's done:
*
* ```js
* env.isDone; // True if envelope is completed
* ```
*
* Envelope has events to track activity: 'change' and 'complete':
*
* ```
* env.addEventListener(`change`, ev => {
* console.log(`Old: ${evt.oldState} new: ${ev.newState}`);
* })
* ```
*
* It's also possible to iterate over the values of the envelope:
* ```js
* const env = new Envelopes.Adsr();
* for await (const v of env) {
* // v is the numeric value
* await Flow.sleep(100); // Want to pause a little to give envelope time to run
* }
* // Envelope has finished
* ```
*/
var Adsr = class extends AdsrBase {
attackPath;
decayPath;
releasePath;
initialLevel;
peakLevel;
releaseLevel;
sustainLevel;
attackBend;
decayBend;
releaseBend;
initialLevelOverride;
retrigger;
releasedAt;
constructor(opts = {}) {
super(opts);
this.retrigger = opts.retrigger ?? defaultAdsrOpts.retrigger;
this.initialLevel = opts.initialLevel ?? defaultAdsrOpts.initialLevel;
this.peakLevel = opts.peakLevel ?? defaultAdsrOpts.peakLevel;
this.releaseLevel = opts.releaseLevel ?? defaultAdsrOpts.releaseLevel;
this.sustainLevel = opts.sustainLevel ?? defaultAdsrOpts.sustainLevel;
this.attackBend = opts.attackBend ?? defaultAdsrOpts.attackBend;
this.releaseBend = opts.releaseBend ?? defaultAdsrOpts.releaseBend;
this.decayBend = opts.decayBend ?? defaultAdsrOpts.decayBend;
const max = 1;
this.attackPath = toPath(quadraticSimple({
x: 0,
y: this.initialLevel
}, {
x: max,
y: this.peakLevel
}, -this.attackBend));
this.decayPath = toPath(quadraticSimple({
x: 0,
y: this.peakLevel
}, {
x: max,
y: this.sustainLevel
}, -this.decayBend));
this.releasePath = toPath(quadraticSimple({
x: 0,
y: this.sustainLevel
}, {
x: max,
y: this.releaseLevel
}, -this.releaseBend));
}
onTrigger() {
this.initialLevelOverride = void 0;
if (!this.retrigger) {
const [_stage, scaled, _raw] = this.compute(true, false);
if (!Number.isNaN(scaled) && scaled > 0) this.initialLevelOverride = scaled;
}
}
[Symbol.iterator]() {
return new AdsrIterator(this);
}
/**
* Returns the scaled value
* Same as .compute()[1]
*/
get value() {
return this.compute(true)[1];
}
/**
* Compute value of envelope at this point in time.
*
* Returns an array of [stage, scaled, raw]. Most likely you want to use {@link value} to just get the scaled value.
* @param allowStateChange If true (default) envelope will be allowed to change state if necessary before returning value
*/
compute(allowStateChange = true, allowLooping = true) {
const [stage, amt] = super.computeRaw(allowStateChange, allowLooping);
if (stage === void 0) return [
void 0,
NaN,
NaN
];
let v;
switch (stage) {
case `attack`:
v = this.attackPath.interpolate(amt).y;
if (this.initialLevelOverride !== void 0) v = scale(v, 0, 1, this.initialLevelOverride, 1);
this.releasedAt = v;
break;
case `decay`:
v = this.decayPath.interpolate(amt).y;
this.releasedAt = v;
break;
case `sustain`:
v = this.sustainLevel;
this.releasedAt = v;
break;
case `release`:
v = this.releasePath.interpolate(amt).y;
if (this.releasedAt !== void 0) v = scale(v, 0, this.sustainLevel, 0, this.releasedAt);
break;
case `complete`:
v = this.releaseLevel;
this.releasedAt = void 0;
break;
default: throw new Error(`Unknown state: ${stage}`);
}
return [
stage,
v,
amt
];
}
};
//#endregion
//#region ../packages/modulation/src/envelope.ts
var envelope_exports = /* @__PURE__ */ __exportAll({
Adsr: () => Adsr,
AdsrBase: () => AdsrBase,
AdsrIterator: () => AdsrIterator,
adsr: () => adsr,
adsrIterable: () => adsrIterable,
adsrStateTransitions: () => adsrStateTransitions,
defaultAdsrOpts: () => defaultAdsrOpts,
defaultAdsrTimingOpts: () => defaultAdsrTimingOpts
});
/**
* Returns a function that iterates over an envelope
* ```js
* const e = Envelopes.adsr();
*
* e(); // Yields current value
* ```
*
* Starts the envelope the first time the return function is called.
* When the envelope finishes, it continues to return the `releaseLevel` of the envelope.
*
* Options can be provided to set the shape of the envelope as usual, eg:
* ```js
* const e = Envelopes.adsr({
* attackDuration: 1000,
* releaseDuration: 500
* });
* ```
* @param opts
* @returns
*/
const adsr = (opts = {}) => {
const envelope = new Adsr(opts);
const finalValue = envelope.releaseLevel;
const iterator = envelope[Symbol.iterator]();
return () => resolveWithFallbackSync(iterator, {
overrideWithLast: true,
value: finalValue
});
};
/**
* Creates and runs an envelope, sampling its values at `sampleRateMs`.
* Note that if the envelope loops, iterator never returns.
*
* @example Init
* ```js
* import { Envelopes } from '@ixfx/modulation.js';
* import { IterableAsync } from '@ixfx/iterable.js';
*
* const opts = {
* attackDuration: 1000,
* releaseDuration: 1000,
* sustainLevel: 1,
* attackBend: 1,
* decayBend: -1
* };
* ```
*
* ```js
* // Add data to array
* // Sample an envelope every 20ms into an array
* const data = await IterableAsync.toArray(Envelopes.adsrIterable(opts, 20));
* ```
*
* ```js
* // Iterate with `for await`
* // Work with values as sampled
* for await (const v of Envelopes.adsrIterable(opts, 5)) {
* // Work with envelope value `v`...
* }
* ```
* @param opts Envelope options
* @returns
*/
async function* adsrIterable(opts) {
const envelope = new Adsr(opts.env);
const sampleRateMs = opts.sampleRateMs ?? 100;
envelope.trigger();
const r = repeat(() => envelope.value, {
while: () => !envelope.isDone,
delay: sampleRateMs,
signal: opts.signal
});
for await (const v of r) yield v;
}
//#endregion
//#region ../packages/modulation/src/forces.ts
/**
* Acknowledgements: much of the work here is an adapation from Daniel Shiffman's excellent _The Nature of Code_ website.
*/
var forces_exports = /* @__PURE__ */ __exportAll({
accelerationForce: () => accelerationForce,
angleFromAccelerationForce: () => angleFromAccelerationForce,
angleFromVelocityForce: () => angleFromVelocityForce,
angularForce: () => angularForce,
apply: () => apply,
attractionForce: () => attractionForce,
computeAccelerationToTarget: () => computeAccelerationToTarget,
computeAttractionForce: () => computeAttractionForce,
computePositionFromAngle: () => computePositionFromAngle,
computePositionFromVelocity: () => computePositionFromVelocity,
computeVelocity: () => computeVelocity,
constrainBounce: () => constrainBounce,
guard: () => guard,
magnitudeForce: () => magnitudeForce,
nullForce: () => nullForce,
orientationForce: () => orientationForce,
pendulumForce: () => pendulumForce,
springForce: () => springForce,
targetForce: () => targetForce,
velocityForce: () => velocityForce
});
/**
* Throws an error if `t` is not of the `ForceAffected` shape.
* @param t
* @param name
*/
const guard = (t, name = `t`) => {
if (t === void 0) throw new Error(`Parameter ${name} is undefined. Expected ForceAffected`);
if (t === null) throw new Error(`Parameter ${name} is null. Expected ForceAffected`);
if (typeof t !== `object`) throw new TypeError(`Parameter ${name} is type ${typeof t}. Expected object of shape ForceAffected`);
};
/**
* `constrainBounce` yields a function that affects `t`'s position and velocity such that it
* bounces within bounds.
*
* ```js
* // Setup bounce with area constraints
* // Reduce velocity by 10% with each impact
* const b = constrainBounce({ width:200, height:500 }, 0.9);
*
* // Thing
* const t = {
* position: { x: 50, y: 50 },
* velocity: { x: 0.3, y: 0.01 }
* };
*
* // `b` returns an altereted version of `t`, with the
* // bounce logic applied.
* const bounced = b(t);
* ```
*
* `dampen` parameter allows velocity to be dampened with each bounce. A value
* of 0.9 for example reduces velocity by 10%. A value of 1.1 will increase velocity by
* 10% with each bounce.
* @param bounds Constraints of area
* @param dampen How much to dampen velocity by. Defaults to 1 meaning there is no damping.
* @returns A function that can perform bounce logic
*/
const constrainBounce = (bounds, dampen = 1) => {
if (!bounds) bounds = {
width: 1,
height: 1
};
const minX = getEdgeX(bounds, `left`);
const maxX = getEdgeX(bounds, `right`);
const minY = getEdgeY(bounds, `top`);
const maxY = getEdgeY(bounds, `bottom`);
return (t) => {
const position = computePositionFromVelocity(t.position ?? Empty, t.velocity ?? Empty);
let velocity = t.velocity ?? Empty;
let { x, y } = position;
if (x > maxX) {
x = maxX;
velocity = invert(multiplyScalar(velocity, dampen), `x`);
} else if (x < minX) {
x = minX;
velocity = invert(multiplyScalar(velocity, dampen), `x`);
}
if (y > maxY) {
y = maxY;
velocity = multiplyScalar(invert(velocity, `y`), dampen);
} else if (position.y < minY) {
y = minY;
velocity = invert(multiplyScalar(velocity, dampen), `y`);
}
return Object.freeze({
...t,
position: {
x,
y
},
velocity
});
};
};
/**
* For a given set of attractors, returns a function that a sets acceleration of attractee.
* Keep note though that this bakes-in the values of the attractor, it won't reflect changes to their state. For dynamic
* attractors, it might be easier to use `computeAttractionForce`.
*
* @example Force
* ```js
* const f = Forces.attractionForce(sun, gravity);
* earth = Forces.apply(earth, f);
* ```
*
* @example Everything mutually attracted
* ```js
* // Create a force with all things as attractors.
* const f = Forces.attractionForce(things, gravity);
* // Apply force to all things.
* // The function returned by attractionForce will automatically ignore self-attraction
* things = things.map(a => Forces.apply(a, f));
* ```
* @param attractors
* @param gravity
* @param distanceRange
* @returns
*/
const attractionForce = (attractors, gravity, distanceRange = {}) => (attractee) => {
let accel = attractee.acceleration ?? Empty;
for (const a of attractors) {
if (a === attractee) continue;
const f = computeAttractionForce(a, attractee, gravity, distanceRange);
accel = sum(accel, f);
}
return {
...attractee,
acceleration: accel
};
};
/**
* Computes the attraction force between two things.
* Value for `gravity` will depend on what range is used for `mass`. It's probably a good idea
* to keep mass to mean something relative - ie 1 is 'full' mass, and adjust the `gravity`
* value until it behaves as you like. Keeping mass in 0..1 range makes it easier to apply to
* visual properties later.
*
* @example Attractee and attractor, gravity 0.005
* ```js
* const attractor = { position: { x:0.5, y:0.5 }, mass: 1 };
* const attractee = { position: Points.random(), mass: 0.01 };
* attractee = Forces.apply(attractee, Forces.computeAttractionForce(attractor, attractee, 0.005));
* ```
*
* @example Many attractees for one attractor, gravity 0.005
* ```js
* attractor = { position: { x:0.5, y:0.5 }, mass: 1 };
* attractees = attractees.map(a => Forces.apply(a, Forces.computeAttractionForce(attractor, a, 0.005)));
* ```
*
* @example Everything mutually attracted
* ```js
* // Create a force with all things as attractors.
* const f = Forces.attractionForce(things, gravity);
* // Apply force to all things.
* // The function returned by attractionForce will automatically ignore self-attraction
* things = things.map(a => Forces.apply(a, f));
* ```
*
* `attractor` thing attracting (eg, earth)
* `attractee` thing being attracted (eg. satellite)
*
*
* `gravity` will have to be tweaked to taste.
* `distanceRange` clamps the computed distance. This affects how tightly the particles will orbit and can also determine speed. By default it is 0.001-0.7
* @param attractor Attractor (eg earth)
* @param attractee Attractee (eg satellite)
* @param gravity Gravity constant
* @param distanceRange Min/max that distance is clamped to.
* @returns
*/
const computeAttractionForce = (attractor, attractee, gravity, distanceRange = {}) => {
if (attractor.position === void 0) throw new Error(`attractor.position not set`);
if (attractee.position === void 0) throw new Error(`attractee.position not set`);
const distributionRangeMin = distanceRange.min ?? .01;
const distributionRangeMax = distanceRange.max ?? .7;
const f = normalise(subtract(attractor.position, attractee.position));
const d = clamp(distance(f), distributionRangeMin, distributionRangeMax);
return multiplyScalar(f, gravity * (attractor.mass ?? 1) * (attractee.mass ?? 1) / (d * d));
};
/**
* A force that moves a thing toward `targetPos`.
*
* ```js
* const t = Forces.apply(t, Forces.targetForce(targetPos));
* ```
* @param targetPos
* @param opts
* @returns
*/
const targetForce = (targetPos, opts = {}) => {
const fn = (t) => {
const accel = computeAccelerationToTarget(targetPos, t.position ?? {
x: .5,
y: .5
}, opts);
return {
...t,
acceleration: sum(t.acceleration ?? Empty, accel)
};
};
return fn;
};
/**
* Returns `pt` with x and y set to `setpoint` if either's absolute value is below `v`
* @param pt
* @param v
* @returns
*/
/**
* Apply a series of force functions or forces to `t`. Null/undefined entries are skipped silently.
* It also updates the velocity and position of the returned version of `t`.
*
* ```js
* // Wind adds acceleration. Force is dampened by mass
* const wind = Forces.accelerationForce({ x: 0.00001, y: 0 }, `dampen`);
*
* // Gravity adds acceleration. Force is magnified by mass
* const gravity = Forces.accelerationForce({ x: 0, y: 0.0001 }, `multiply`);
*
* // Friction is calculated based on velocity. Force is magnified by mass
* const friction = Forces.velocityForce(0.00001, `multiply`);
*
* // Flip movement velocity if we hit a wall. And dampen it by 10%
* const bouncer = Forces.constrainBounce({ width: 1, height: 1 }, 0.9);
*
* let t = {
* position: Points.random(),
* mass: 0.1
* };
*
* // Apply list of forces, returning a new version of the thing
* t = Forces.apply(t,
* gravity,
* wind,
* friction,
* bouncer
* );
* ```
*/
const apply = (t, ...accelForces) => {
if (t === void 0) throw new Error(`t parameter is undefined`);
for (const f of accelForces) {
if (f === null || f === void 0) continue;
t = typeof f === `function` ? f(t) : {
...t,
acceleration: sum(t.acceleration ?? Empty, f)
};
}
const velo = computeVelocity(t.acceleration ?? Empty, t.velocity ?? Empty);
const pos = computePositionFromVelocity(t.position ?? Empty, velo);
return {
...t,
position: pos,
velocity: velo,
acceleration: Empty
};
};
/**
* Apples `vector` to acceleration, scaling according to mass, based on the `mass` option.
* It returns a function which can later be applied to a thing.
*
* ```js
* // Acceleration vector of (0.1, 0), ie moving straight on horizontal axis
* const f = Forces.accelerationForce({ x:0.1, y:0 }, `dampen`);
*
* // Thing to move
* let t = { position: ..., acceleration: ... }
*
* // Apply force
* t = f(t);
* ```
* @param vector
* @returns Force function
*/
const accelerationForce = (vector, mass = `ignored`) => (t) => Object.freeze({
...t,
acceleration: massApplyAccel(vector, t, mass)
});
/**
* Returns an acceleration vector with mass either dampening or multiplying it.
* The passed-in `thing` is not modified.
*
* ```js
* // Initial acceleration vector
* const accel = { x: 0.1, y: 0};
*
* // Thing being moved
* const thing = { mass: 0.5, position: ..., acceleration: ... }
*
* // New acceleration vector, affected by mass of `thing`
* const accelWithMass = massApplyAccel(accel, thing, `dampen`);
* ```
* Mass of thing can be factored in, according to `mass` setting. Use `dampen`
* to reduce acceleration with greater mass of thing. Use `multiply` to increase
* the effect of acceleration with a greater mass of thing. `ignored` means
* mass is not taken into account.
*
* If `t` has no mass, the `mass` setting is ignored.
*
* This function is used internally by the predefined forces.
*
* @param vector Vector force
* @param thing Thing being affected
* @param mass How to factor in mass of thing (default ignored)
* @returns Acceleration vector
*/
const massApplyAccel = (vector, thing, mass = `ignored`) => {
let op;
switch (mass) {
case `dampen`:
op = (mass) => divide(vector, mass, mass);
break;
case `multiply`:
op = (mass) => multiply(vector, mass, mass);
break;
case `ignored`:
op = (_mass) => vector;
break;
default: throw new Error(`Unknown 'mass' parameter '${mass}. Expected 'dampen', 'multiply' or 'ignored'`);
}
return sum(thing.acceleration ?? Empty, op(thing.mass ?? 1));
};
/**
* A force based on the square of the thing's velocity.
* It's like {@link velocityForce}, but here the velocity has a bigger impact.
*
* ```js
* const thing = {
* position: { x: 0.5, y:0.5 },
* velocity: { x: 0.001, y:0 }
* };
* const drag = magnitudeForce(0.1);
*
* // Apply drag force to thing, returning result
* const t = Forces.apply(thing, drag);
* ```
* @param force Force value
* @param mass How to factor in mass
* @returns Function that computes force
*/
const magnitudeForce = (force, mass = `ignored`) => (t) => {
if (t.velocity === void 0) return t;
const mag = distance(normalise(t.velocity));
const magSq = force * mag * mag;
const vv = multiplyScalar(invert(t.velocity), magSq);
return Object.freeze({
...t,
acceleration: massApplyAccel(vv, t, mass)
});
};
/**
* Null force does nothing
* @returns A force that does nothing
*/
const nullForce = (t) => t;
/**
* Force calculated from velocity of object. Reads velocity and influences acceleration.
*
* ```js
* let t = { position: Points.random(), mass: 0.1 };
* const friction = velocityForce(0.1, `dampen`);
*
* // Apply force, updating position and velocity
* t = Forces.apply(t, friction);
* ```
* @param force Force
* @param mass How to factor in mass
* @returns Function that computes force
*/
const velocityForce = (force, mass) => {
const pipeline$1 = pipeline(invert, (v) => multiplyScalar(v, force));
return (t) => {
if (t.velocity === void 0) return t;
const v = pipeline$1(t.velocity);
return Object.freeze({
...t,
acceleration: massApplyAccel(v, t, mass)
});
};
};
/**
* Sets angle, angularVelocity and angularAcceleration based on
* angularAcceleration, angularVelocity, angle
* @returns
*/
const angularForce = () => (t) => {
const accumulator = t.angularAcceleration ?? 0;
const vel = t.angularVelocity ?? 0;
const angle = t.angle ?? 0;
const v = vel + accumulator;
const a = angle + v;
return Object.freeze({
...t,
angle: a,
angularVelocity: v,
angularAcceleration: 0
});
};
/**
* Yields a force function that applies the thing's acceleration.x to its angular acceleration.
* @param scaling Use this to scale the accel.x value. Defaults to 20 (ie accel.x*20). Adjust if rotation is too much or too little
* @returns
*/
const angleFromAccelerationForce = (scaling = 20) => (t) => {
const accel = t.acceleration ?? Empty;
return Object.freeze({
...t,
angularAcceleration: accel.x * scaling
});
};
/**
* Yields a force function that applies the thing's velocity to its angle.
* This will mean it points in the direction of travel.
* @param interpolateAmt If provided, the angle will be interpolated toward by this amount. Defaults to 1, no interpolation
* @returns
*/
const angleFromVelocityForce = (interpolateAmt = 1) => (t) => {
const a = angleRadian(t.velocity ?? Empty);
return Object.freeze({
...t,
angle: interpolateAmt < 1 ? interpolateAngle(interpolateAmt, t.angle ?? 0, a) : a
});
};
/**
* Spring force
*
* * ```js
* // End of spring that moves
* let thing = {
* position: { x: 1, y: 0.5 },
* mass: 0.1
* };
*
* // Anchored other end of spring
* const pinnedAt = {x: 0.5, y: 0.5};
*
* // Create force: length of 0.4
* const springForce = Forces.springForce(pinnedAt, 0.4);
*
* continuously(() => {
* // Apply force
* thing = Forces.apply(thing, springForce);
* }).start();
* ```
* [Read more](https://www.joshwcomeau.com/animation/a-friendly-introduction-to-spring-physics/)
*
* @param pinnedAt Anchored end of the spring
* @param restingLength Length of spring-at-rest (default: 0.5)
* @param k Spring stiffness (default: 0.0002)
* @param damping Damping factor to apply, so spring slows over time. (default: 0.995)
* @returns
*/
const springForce = (pinnedAt, restingLength = .5, k = 2e-4, damping = .999) => (t) => {
const direction = subtract(t.position ?? Empty, pinnedAt);
const mag = distance(direction);
const stretch = Math.abs(restingLength - mag);
const velo = computeVelocity(massApplyAccel(pipelineApply(direction, normalise, (p) => multiplyScalar(p, -k * stretch)), t, `dampen`) ?? Empty, t.velocity ?? Empty);
const veloDamped = multiply(velo, damping, damping);
return {
...t,
velocity: veloDamped,
acceleration: Empty
};
};
/**
* The pendulum force swings something back and forth.
*
* ```js
* // Swinger
* let thing = {
* position: { x: 1, y: 0.5 },
* mass: 0.1
* };
*
* // Position thing swings from (middle of screen)
* const pinnedAt = {x: 0.5, y: 0.5};
*
* // Create force: length of 0.4
* const pendulumForce = Forces.pendulumForce(pinnedAt, { length: 0.4 });
*
* continuously(() => {
* // Apply force
* // Returns a new thing with recalculated angularVelocity, angle and position.
* thing = Forces.apply(thing, pendulumForce);
* }).start();
* ```
*
* [Read more](https://natureofcode.com/book/chapter-3-oscillation/)
*
* @param pinnedAt Location to swing from (x:0.5, y:0.5 default)
* @param opts Options
* @returns
*/
const pendulumForce = (pinnedAt, opts = {}) => (t) => {
if (!pinnedAt) pinnedAt = {
x: 0,
y: 0
};
const length = opts.length ?? distance(pinnedAt, t.position ?? Empty);
const speed = opts.speed ?? .001;
const damping = opts.damping ?? .995;
let angle = t.angle;
if (angle === void 0) if (t.position) angle = angleRadian(pinnedAt, t.position) - Math.PI / 2;
else angle = 0;
const accel = -1 * speed / length * Math.sin(angle);
const v = (t.angularVelocity ?? 0) + accel;
angle += v;
return Object.freeze({
angularVelocity: v * damping,
angle,
position: computePositionFromAngle(length, angle + Math.PI / 2, pinnedAt)
});
};
/**
* Compute velocity based on acceleration and current velocity
* @param acceleration Acceleration
* @param velocity Velocity
* @param velocityMax If specified, velocity will be capped at this value
* @returns
*/
const computeVelocity = (acceleration, velocity, velocityMax) => {
const p = sum(velocity, acceleration);
return velocityMax === void 0 ? p : clampMagnitude(p, velocityMax);
};
/**
* Returns the acceleration to get from `currentPos` to `targetPos`.
*
* @example Barebones usage:
* ```js
* const accel = Forces.computeAccelerationToTarget(targetPos, currentPos);
* const vel = Forces.computeVelocity(accel, currentVelocity);
*
* // New position:
* const pos = Points.sum(currentPos, vel);
* ```
*
* @example Implementation:
* ```js
* const direction = Points.subtract(targetPos, currentPos);
* const accel = Points.multiply(direction, diminishBy);
* ```
* @param currentPos Current position
* @param targetPos Target position
* @param opts Options
* @returns
*/
const computeAccelerationToTarget = (targetPos, currentPos, opts = {}) => {
const diminishBy = opts.diminishBy ?? .001;
const direction = subtract(targetPos, currentPos);
if (opts.range && compare(abs(direction), opts.range) === -2) return Empty;
return multiplyScalar(direction, diminishBy);
};
/**
* Compute a new position based on existing position and velocity vector
* @param position Position Current position
* @param velocity Velocity vector
* @returns Point
*/
const computePositionFromVelocity = (position, velocity) => sum(position, velocity);
/**
* Compute a position based on distance and angle from origin
* @param distance Distance from origin
* @param angleRadians Angle, in radians from origin
* @param origin Origin point
* @returns Point
*/
const computePositionFromAngle = (distance, angleRadians, origin) => toCartesian(distance, angleRadians, origin);
const _angularForce = angularForce();
const _angleFromAccelerationForce = angleFromAccelerationForce();
/**
* A force that orients things according to direction of travel.
*
* Under the hood, it applies:
* * angularForce,
* * angleFromAccelerationForce, and
* * angleFromVelocityForce
* @param interpolationAmt
* @returns
*/
const orientationForce = (interpolationAmt = .5) => {
const angleFromVel = angleFromVelocityForce(interpolationAmt);
return (t) => {
t = _angularForce(t);
t = _angleFromAccelerationForce(t);
t = angleFromVel(t);
return t;
};
};
//#endregion
//#region ../packages/modulation/src/util/pi-pi.ts
const piPi$1 = Math.PI * 2;
//#endregion
//#region ../packages/modulation/src/interpolate/angle.ts
/**
* Interpolate between angles `a` and `b` by `amount`. Angles are in degrees unless specified otherwise.
*
* ```js
* // Remmebering 0 is east, 270 is south, clockwise is negative:
* const i = interpolatorAngle(`0deg`, `270deg`); // Defaults to 'short' direction
* i(0.5); // 135 (north-west)
*
* const i = interpolatorAngle(`0deg`, `270deg`, { direction: `long` });
* i(0.5); // 315 (south-east)
* ```
* @param a Start angle (assumed radian if numbers are given)
* @param b End angle (assumed radian if numbers are given)
* @returns Interpolated angle, using same unit as the `b` angle
*/
function interpolatorAngle(amountOrA, aOrB, bOrOptions, options) {
let aa;
let