event-horizon / src /util.js
maxdemarzi's picture
Deploy 4ed0390343adb220188e58f95ab1a8e7e1dd995e (manual: Actions blocked on billing) (part 2)
1b9ed71 verified
Raw
History Blame Contribute Delete
2.72 kB
/* Small shared helpers. No three.js, no DOM β€” safe to import anywhere. */
/** HTML-escape for interpolation into innerHTML β€” text AND attribute contexts.
* Escapes the five significant characters incl. both quotes, so a value is safe
* whether it lands in element content or inside a "…"/'…' attribute. Node fields
* (label/table/id/…) are attacker-controlled (any `?graph=` URL/data: URI), so
* every innerHTML sink that interpolates them MUST wrap them in this. */
export const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g,
(c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
/** Deterministic PRNG. Same seed always yields the same galaxy. */
export function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
export const clamp = (x, a, b) => Math.max(a, Math.min(b, x));
export const easeInOut = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);
export const easeOut = (t) => 1 - Math.pow(1 - t, 3);
export const lerp = (a, b, t) => a + (b - a) * t;
/** #rrggbb or [r,g,b] (0..1) -> [r,g,b] floats. */
export function toRGB(c) {
if (Array.isArray(c)) return c;
const h = String(c).replace('#', '');
const n = parseInt(h.length === 3 ? h.split('').map((x) => x + x).join('') : h, 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
}
export function rgbToCss(c, alpha) {
const [r, g, b] = toRGB(c);
const v = (x) => Math.round(x * 255);
return alpha == null
? `rgb(${v(r)},${v(g)},${v(b)})`
: `rgba(${v(r)},${v(g)},${v(b)},${alpha})`;
}
/** Compact magnitude formatter: 1_200_000 -> "$1.2M". Override via config.formatMagnitude. */
export function formatCompact(v, prefix = '$') {
if (v == null || !isFinite(v)) return 'β€”';
const a = Math.abs(v);
if (a >= 1e12) return prefix + (v / 1e12).toFixed(a >= 1e13 ? 0 : 1) + 'T';
if (a >= 1e9) return prefix + (v / 1e9).toFixed(a >= 1e10 ? 0 : 1) + 'B';
if (a >= 1e6) return prefix + (v / 1e6).toFixed(a >= 1e7 ? 0 : 1) + 'M';
if (a >= 1e3) return prefix + Math.round(v / 1e3) + 'K';
return prefix + String(Math.round(v));
}
export function truncate(s, n) {
if (!s) return '';
return s.length > n ? s.slice(0, n) + '…' : s;
}
/** Resolve a config value that may be a literal, a key string, or a function. */
export function accessor(spec, fallback) {
if (spec == null) return fallback;
if (typeof spec === 'function') return spec;
return (d) => d[spec];
}