feat(static): client-side alteration engine (port of player.alteration/audio)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-30 07:25:24 -07:00
parent 7ef3e0ccc6
commit be13248bdd
3 changed files with 124 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
// Client-side port of player/alteration.py + player/audio.py (the alteration
// engine). Replaces the live POST /api/alteration in the static build — pure math,
// unity calibration (the client never sends a calibration, so DEFAULT_CALIBRATION
// = all gains 1.0 always applies). UMD so the browser gets `HEFAlteration` and
// `node --test` can require() it. Keep IN SYNC with the Python — the numeric
// contract is guarded by tests/test_alteration_js_parity.py.
(function (root, factory) {
const api = factory();
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.HEFAlteration = api;
})(typeof self !== "undefined" ? self : this, function () {
"use strict";
const KNOB_MAX = 4; // player/alteration.py:25
const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
// Mirror of player/alteration.py::plan_alteration with DEFAULT_CALIBRATION (unity).
function plan(c) {
const tone = clamp((c.light - c.dark) / KNOB_MAX, -1, 1);
const overlayIntensity = clamp(c.left / KNOB_MAX, 0, 1);
const dreamIntensity = clamp(c.right / KNOB_MAX, 0, 1);
const affectIntensity = clamp(c.right / KNOB_MAX, 0, 1); // affect uses overlay_gain too
const is_identity = tone === 0 && c.left === 0 && c.right === 0;
return {
grade: { tone },
overlay: { level: c.left, intensity: overlayIntensity },
affect: { strength: c.right, intensity: affectIntensity },
dream: { strength: c.right, intensity: dreamIntensity },
is_identity,
};
}
// Mirror of player/audio.py::resolve_audio. `scaleAudio` is the current scale's
// `audio` filename (relative to <mediaBase>audio/); off → silence.
function renderAudio(source, scaleAudio, mediaBase) {
if (source === "off") return { source: "off", url: null, altitude_coupled: false };
const url = scaleAudio ? mediaBase + "audio/" + scaleAudio : null;
return { source: "soundtrack", url, altitude_coupled: true };
}
// The full /api/alteration response shape, computed locally.
function alteration(controls, scaleAudio, mediaBase) {
return {
plan: plan(controls),
render: {
video: { shown: controls.visual === "on" }, // player/audio.py::resolve_visual
audio: renderAudio(controls.audio, scaleAudio, mediaBase),
},
};
}
return { plan, renderAudio, alteration, KNOB_MAX };
});