feat(sim): pure altitude-scrub math module + node unit suite

Per plan Task 1. position->segment, frac->currentTime, audio gains, and
integer-crossing detection; 9 node --test cases green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-27 15:37:27 -07:00
parent 937461e3ba
commit 3dc3e3491f
3 changed files with 113 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
// Pure altitude-scrub math — no DOM. A continuous `pos` (float) is the knob:
// integers are altitudes/detents, fractions are mid-morph blend. UMD so the
// browser gets a `HEFScrub` global and `node --test` can require() it.
(function (root, factory) {
const api = factory();
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.HEFScrub = api;
})(typeof self !== "undefined" ? self : this, function () {
"use strict";
const clamp01 = (x) => Math.max(0, Math.min(1, x));
const wrapIndex = (i, n) => ((i % n) + n) % n;
function segmentOf(pos) {
const lo = Math.floor(pos);
return { lo, frac: pos - lo };
}
function accumToPos(restPos, accumDeg, stepDeg) {
return restPos + accumDeg / stepDeg;
}
function fracToTime(frac, duration) {
return clamp01(frac) * (duration || 0);
}
function crossfadeGains(frac) {
const f = clamp01(frac);
return { from: 1 - f, to: f };
}
// Integers strictly crossed moving prevPos -> pos, in travel order. Landing
// exactly on an integer counts as crossing it (it commits that altitude).
function integerCrossings(prevPos, pos) {
const out = [];
if (pos > prevPos) {
for (let k = Math.floor(prevPos) + 1; k <= pos + 1e-9; k++) out.push({ index: k, dir: 1 });
} else if (pos < prevPos) {
for (let k = Math.ceil(prevPos) - 1; k >= pos - 1e-9; k--) out.push({ index: k, dir: -1 });
}
return out;
}
return { clamp01, wrapIndex, segmentOf, accumToPos, fracToTime, crossfadeGains, integerCrossings };
});