orbit/js/utils/Color.js

74 lines
2.7 KiB
JavaScript

import Phaser from '../vendor/phaser.js';
/**
* Converts a config color (number or CSS string like '#1b2540')
* into the integer format Phaser expects.
*/
export function toColor(value, fallback = 0xffffff) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value >>> 0;
}
if (typeof value === 'string' && value.length > 0) {
const color = Phaser.Display.Color.ValueToColor(value.trim());
if (color) return color.color;
}
return fallback;
}
/**
* Converts a color (number or CSS string) to a '#rrggbb' CSS string.
*
* Phaser v4 (4.2.1 Giedi) quirk: Text styles are written straight to the
* canvas — `fillStyle = style.color` — so a numeric color (e.g. 0xeaf6ff)
* is an *invalid* fillStyle and the text silently renders black. Canvas-
* backed text (add.text / TextStyle) must therefore always get CSS strings;
* keep toColor for Graphics/shape APIs that want numbers.
*/
export function toCss(value, fallback = '#ffffff') {
const n = toColor(value, toColor(fallback));
return '#' + (n >>> 0).toString(16).padStart(6, '0');
}
/**
* Rotate a color's HUE (degrees; + is clockwise on the hue wheel) and
* rescale its saturation and lightness (1 = unchanged). Returns a 0xRRGGBB
* number (ready for setTint()).
*
* Used by the nebula FRONT gas layer: the accent tint is the system's base
* color rotated a little and lifted — two gas species of one nebula
* (see data/game.json → nebula.front.accent). Pure math (no Phaser), so it
* is testable in bare Node; grays stay grays (s=0 ⇒ hue is irrelevant),
* and black/white are preserved.
*/
export function shiftHue(color, { hueShift = 0, satScale = 1, lightScale = 1 } = {}) {
const n = toColor(color, 0xffffff) >>> 0;
const r = ((n >> 16) & 255) / 255;
const g = ((n >> 8) & 255) / 255;
const b = (n & 255) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const l = (max + min) / 2;
let h = 0;
const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
if (d !== 0) {
if (max === r) h = ((g - b) / d) % 6;
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
h *= 60;
if (h < 0) h += 360;
}
h = (h + hueShift) % 360;
const s2 = Math.min(1, Math.max(0, s * satScale));
const l2 = Math.min(1, Math.max(0, l * lightScale));
const c = (1 - Math.abs(2 * l2 - 1)) * s2;
const hp = h / 60;
const x = c * (1 - Math.abs((hp % 2) - 1));
const [r2, g2, b2] =
hp < 1 ? [c, x, 0] : hp < 2 ? [x, c, 0] : hp < 3 ? [0, c, x] :
hp < 4 ? [0, x, c] : hp < 5 ? [x, 0, c] : [c, 0, x];
const m = l2 - c / 2;
const to255 = (v) => Math.min(255, Math.max(0, Math.round((v + m) * 255)));
return ((to255(r2) << 16) | (to255(g2) << 8) | to255(b2)) >>> 0;
}