orbit/js/visuals/SystemEffectsMath.js

71 lines
2.5 KiB
JavaScript

/**
* Pure math for the per-system visual effects — NO Phaser import, so the
* dev harness (dev/system-effects.test.mjs) can run it in bare Node.
*
* The system effect is a full-screen composite over the WORLD camera
* (see SystemEffects.js): a fragment shader displaces the camera's
* screen-space UVs around the system's star. The star is never rendered
* (it is invisible flavor — data/systems.json), but it sits at the
* system origin (world 0,0), so the ripple center is "where world (0,0)
* projects to, in the camera's UV space".
*
* A camera's `matrixCombined` is the world->screen affine transform
* (a·x + c·y + tx, b·x + d·y + ty). Dividing the screen point by the
* camera's viewport size gives the UV: (0,0) = the camera's top-left,
* (1,1) = its bottom-right — exactly the space the shader's
* `outTexCoord` lives in.
*/
/**
* @typedef {{ a: number, b: number, c: number, d: number, tx: number, ty: number }} Affine2D
*
* Affine2D — the shape of a Phaser camera's matrixCombined (the
* world->screen transform). Duck-typed on purpose: the tests pass plain
* objects, the game passes the real matrix.
*/
/**
* Screen-space point of a world point under an affine transform.
*
* @param {Affine2D} m world->screen transform
* @param {number} [wx=0] world x
* @param {number} [wy=0] world y
* @returns {{ x: number, y: number }} screen point (camera viewport px)
*/
export function worldToScreen(m, wx = 0, wy = 0) {
return {
x: m.a * wx + m.c * wy + m.tx,
y: m.b * wx + m.d * wy + m.ty,
};
}
/**
* UV coordinate of a world point for a camera of the given viewport size
* (0,0 top-left -> 1,1 bottom-right).
*
* @param {Affine2D} m world->screen transform
* @param {number} width camera viewport width (px)
* @param {number} height camera viewport height (px)
* @param {number} [wx=0] world x
* @param {number} [wy=0] world y
* @returns {{ x: number, y: number }} UV
*/
export function worldToUV(m, width, height, wx = 0, wy = 0) {
const p = worldToScreen(m, wx, wy);
return { x: p.x / width, y: p.y / height };
}
/**
* The wave's phase (radians) at a moment — the shader's `time` uniform.
* Monotonic in the game clock, scaled by the configured speed, so the
* ripple keeps its pace across system entries without any per-frame
* accumulation to drift.
*
* @param {number} nowMs game-loop time (ms, monotonic)
* @param {number} [speed=1] angular speed multiplier (radians/second)
* @returns {number} phase, radians
*/
export function ripplePhase(nowMs, speed = 1) {
return (nowMs / 1000) * (Number(speed) || 0);
}