148 lines
3.8 KiB
TypeScript
148 lines
3.8 KiB
TypeScript
// Star Map palette — ported from apps/desktop/src/app/starmap/color.ts so the
|
|
// TUI overlay derives the same memory ink (complement of the theme primary) and
|
|
// the same age fade (rgba(ink, alpha) over the background) as the desktop panel.
|
|
|
|
interface Rgb {
|
|
b: number
|
|
g: number
|
|
r: number
|
|
}
|
|
|
|
function hexToRgb(hex: string): Rgb {
|
|
let s = hex.trim().replace(/^#/, '')
|
|
|
|
if (s.length === 3) {
|
|
s = s
|
|
.split('')
|
|
.map(c => c + c)
|
|
.join('')
|
|
}
|
|
|
|
const n = parseInt(s, 16)
|
|
|
|
if (Number.isNaN(n) || s.length < 6) {
|
|
return { b: 0, g: 215, r: 255 }
|
|
}
|
|
|
|
return { b: n & 255, g: (n >> 8) & 255, r: (n >> 16) & 255 }
|
|
}
|
|
|
|
function rgbToHex(c: Rgb): string {
|
|
const h = (v: number) =>
|
|
Math.max(0, Math.min(255, Math.round(v)))
|
|
.toString(16)
|
|
.padStart(2, '0')
|
|
|
|
return `#${h(c.r)}${h(c.g)}${h(c.b)}`
|
|
}
|
|
|
|
function mix(a: Rgb, b: Rgb, t: number): Rgb {
|
|
const p = Math.max(0, Math.min(1, t))
|
|
|
|
return { b: a.b + (b.b - a.b) * p, g: a.g + (b.g - a.g) * p, r: a.r + (b.r - a.r) * p }
|
|
}
|
|
|
|
function luminance(c: Rgb): number {
|
|
return (0.2126 * c.r + 0.7152 * c.g + 0.114 * c.b) / 255
|
|
}
|
|
|
|
function rgbToHsl(c: Rgb): [number, number, number] {
|
|
const r = c.r / 255
|
|
const g = c.g / 255
|
|
const b = c.b / 255
|
|
const max = Math.max(r, g, b)
|
|
const min = Math.min(r, g, b)
|
|
const l = (max + min) / 2
|
|
const d = max - min
|
|
|
|
if (!d) {
|
|
return [0, 0, l]
|
|
}
|
|
|
|
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
const h = (max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4) * 60
|
|
|
|
return [h, s, l]
|
|
}
|
|
|
|
function hslToRgb(h: number, s: number, l: number): Rgb {
|
|
const hue = ((h % 360) + 360) % 360
|
|
const c = (1 - Math.abs(2 * l - 1)) * s
|
|
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1))
|
|
const m = l - c / 2
|
|
|
|
const [r, g, b] =
|
|
hue < 60
|
|
? [c, x, 0]
|
|
: hue < 120
|
|
? [x, c, 0]
|
|
: hue < 180
|
|
? [0, c, x]
|
|
: hue < 240
|
|
? [0, x, c]
|
|
: hue < 300
|
|
? [x, 0, c]
|
|
: [c, 0, x]
|
|
|
|
return { b: (b + m) * 255, g: (g + m) * 255, r: (r + m) * 255 }
|
|
}
|
|
|
|
function complementaryInk(c: Rgb): Rgb {
|
|
const [h, s, l] = rgbToHsl(c)
|
|
|
|
return hslToRgb(h + 165, Math.max(s, 0.5), Math.max(0.5, Math.min(0.7, l)))
|
|
}
|
|
|
|
export interface StarmapPalette {
|
|
bg: Rgb
|
|
dim: Rgb
|
|
label: Rgb
|
|
memory: Rgb
|
|
skill: Rgb
|
|
}
|
|
|
|
/** Derive the Star Map inks from the theme primary + foreground color. */
|
|
export function deriveStarmapPalette(primaryHex: string, fgHex: string): StarmapPalette {
|
|
const primary = hexToRgb(primaryHex)
|
|
const dark = luminance(hexToRgb(fgHex)) > 0.55
|
|
const base: Rgb = dark ? { b: 255, g: 255, r: 255 } : { b: 0, g: 0, r: 0 }
|
|
const bg: Rgb = dark ? { b: 12, g: 8, r: 8 } : { b: 250, g: 250, r: 250 }
|
|
|
|
return {
|
|
bg,
|
|
dim: mix(base, bg, 0.7),
|
|
label: mix(base, bg, 0.35),
|
|
// Memories are drillable, so they wear the primary "clickable" ink; skills
|
|
// are dead-ends and get the muted complement.
|
|
memory: mix(primary, base, dark ? 0.12 : 0.18),
|
|
skill: mix(complementaryInk(primary), bg, 0.45)
|
|
}
|
|
}
|
|
|
|
/** Fade an explicit hex ink toward the background by alpha (for category bars). */
|
|
export function fadeHex(palette: StarmapPalette, hex: string, alpha: number): string {
|
|
const base = hexToRgb(hex)
|
|
|
|
return rgbToHex(alpha >= 0.999 ? base : mix(palette.bg, base, alpha))
|
|
}
|
|
|
|
/** Fade a base ink toward the background by alpha (rgba-over-bg), as a hex. */
|
|
export function fadeInk(palette: StarmapPalette, style: string, alpha: number): string | undefined {
|
|
const base =
|
|
style === 'skill'
|
|
? palette.skill
|
|
: style === 'memory'
|
|
? palette.memory
|
|
: style === 'label'
|
|
? palette.label
|
|
: style === 'dim'
|
|
? palette.dim
|
|
: null
|
|
|
|
if (!base) {
|
|
return undefined
|
|
}
|
|
|
|
return rgbToHex(alpha >= 0.999 ? base : mix(palette.bg, base, alpha))
|
|
}
|