/** * host-dev/engine-math.js — 引擎数学库门面构造(纯函数,零引擎 import) * owner:T1b-β A1 | 消费方:host-dev/host.js(real 通道工厂 makeEngineCaps 调它)+ A1 node 单测 * * ════════════════════════════════════════════════════════════════════════════ * 【为什么单列本模块(不埋进 host.js)】 * A1 须在 real 通道把 EngineCapabilities.math(lerp/smoothStep + easing 门面)真活包装引擎。 * 把门面构造抽为「吃 ljs 参数」的纯函数,且放在**零引擎 import 的独立模块**,是 testability 刚需: * · F7(node 实跑亲验):`import * as LJS from 'littlejsengine'` 在 node 顶层抛 `window is not defined` * → host.js 因顶层 import 引擎,node 根本 import 不了 host.js(实测同样抛 window)。 * · 故门面纯函数必须住在**不 import 引擎**的模块里,node 测才能 import 它、注入「内联引擎 Ease 曲线的 * mock ljs」(逐字节复制 esm.js:15118)验门面映射正确性——全程不碰真引擎。 * blast radius:仅 host.js 内部组织(host.js 改为从本模块 import clampUnit/buildEngineMath); * 不改契约形状(api.d.ts)、不改 plugin.js、不改任何插件 impl。 * * 【引擎↔插件 边界(创始人 2026-06-13 拍)】 * 纯通用数学族特例(lerp/smoothStep/easing 曲线):引擎与门面公式逐字节/ULP 等价、无状态、无语义冲突 * → math façade=getEngine().math。本模块只构造门面(包装传入的 ljs),不持引擎引用、不做降级 sim。 * ════════════════════════════════════════════════════════════════════════════ */ 'use strict'; /** * 把 t 钳到 [0,1](与 gamefeel clamp01 同口径)。 * 为何必须钳:gamefeel 内置每条 easing 都以 clamp01(t) 打头(gamefeel/impl.js:59 等),且其测断越界钳制 * (gamefeel.test.mjs 断 fn(-0.5)===fn(0) / fn(1.5)===fn(1));门面是「替换 gamefeel easing 的等价件」, * 须与被替换者越界行为一致。而引擎 Ease.POWER/LINEAR 等**不自钳**(POWER:(n)=>(x)=>x**n,越界 x 直接幂运算) * → 门面不钳则对越界 t 行为 ≠ 内置 fallback → A4 改门面后行为回归。 * @param {number} t * @returns {number} 钳制到 [0,1] 的值 */ export function clampUnit(t) { return t < 0 ? 0 : t > 1 ? 1 : t; } /** * 【A1】构造引擎数学库门面(EngineMath:lerp/smoothStep + easing 11 条等价曲线)。 * 纯函数:仅依赖传入的 ljs(引擎名字空间,含 lerp/smoothStep/Ease)。host real 通道传真 LJS;node 测传 mock。 * * ── math.lerp / math.smoothStep ──逐字节包装引擎(口径已对齐,引擎 lerp 自带 percent 钳制 esm.js:1444)。 * ── math.easing ──门面包装引擎 LJS.Ease 曲线族(esm.js:15118),**仅暴已 node 逐点亲验等价的 11 条曲线**: * quadIn=POWER(2)(逐字节恒等)/ quadOut=OUT(POWER(2))(逐字节恒等)/ quadInOut=IN_OUT(POWER(2))(ULP)/ * cubicIn=POWER(3) / cubicOut=OUT(POWER(3)) / cubicInOut=IN_OUT(POWER(3))(ULP)/ * backIn=BACK / backOut=OUT(BACK) / elasticIn=ELASTIC / elasticOut=OUT(ELASTIC)(均 ULP <1e-15)。 * 每条调引擎曲线前先 clampUnit(t)(引擎 Ease 不自钳,须钳齐保端点恒等+越界一致)。 * **刻意不暴 backInOut/elasticInOut**:引擎 IN_OUT(BACK/ELASTIC) 经 PIECEWISE 拼接是**另一条曲线** * (back 差 6.6%、elastic 差 17%,非 ULP)→ gamefeel 对这二者恒走内置(引擎无此精确曲线=补层,非 fallback)。 * * ── 性能:POWER(2)/OUT(POWER(2))/IN_OUT(...) 每次调用会新建闭包;easing 门面在 juice 热路径每帧调, * 故在此**预构造一次**这些曲线函数(_p2/_p3/_qOut/... 缓存),各 easing 方法只做 clampUnit + 调缓存曲线。 * * @param {any} ljs 引擎名字空间(须含 lerp/smoothStep/Ease;real=LJS,测=内联引擎曲线的 mock)。 * @returns {import('../src/core/api.d.ts').EngineMath} 引擎数学门面(lerp/smoothStep/easing)。 */ export function buildEngineMath(ljs) { const Ease = ljs.Ease; // 预构造曲线一次(避免每次 easing 调用重建 POWER/OUT/IN_OUT 闭包;功能等价,纯性能优化)。 const _p2 = Ease.POWER(2); // 二次基曲线 x**2 const _p3 = Ease.POWER(3); // 三次基曲线 x**3 const _qOut = Ease.OUT(_p2); // 二次缓出 1-(1-x)**2 const _qInOut = Ease.IN_OUT(_p2); // 二次缓入缓出(PIECEWISE 拼接) const _cOut = Ease.OUT(_p3); // 三次缓出 const _cInOut = Ease.IN_OUT(_p3); // 三次缓入缓出 const _backOut = Ease.OUT(Ease.BACK); // 回弹缓出 const _elasticOut = Ease.OUT(Ease.ELASTIC); // 弹性缓出 return { // ── lerp/smoothStep:逐字节包装引擎(F4/F5)── lerp: (a, b, p) => ljs.lerp(a, b, p), smoothStep: (p) => ljs.smoothStep(p), // ── easing 门面:11 条等价曲线,每条钳 t∈[0,1] 后调预构造的引擎曲线 ── easing: { linear: (t) => Ease.LINEAR(clampUnit(t)), quadIn: (t) => _p2(clampUnit(t)), quadOut: (t) => _qOut(clampUnit(t)), quadInOut: (t) => _qInOut(clampUnit(t)), cubicIn: (t) => _p3(clampUnit(t)), cubicOut: (t) => _cOut(clampUnit(t)), cubicInOut: (t) => _cInOut(clampUnit(t)), backIn: (t) => Ease.BACK(clampUnit(t)), backOut: (t) => _backOut(clampUnit(t)), elasticIn: (t) => Ease.ELASTIC(clampUnit(t)), elasticOut: (t) => _elasticOut(clampUnit(t)), // backInOut/elasticInOut 刻意不暴(引擎 IN_OUT 拼接≠gamefeel 闭式,真分歧 6.6%/17%);gamefeel 对其恒走内置。 }, }; }