UNPKG

ixfx

Version:

Bundle of ixfx libraries

1,657 lines (1,643 loc) 103 kB
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