feat: improve team colors with LCH color space (#3146)

## Summary

Refactor `generateTeamColors()` to use LCH (Lightness-Chroma-Hue) color
space instead of HSL for perceptually uniform team color variations.

## Changes

- **`Colors.ts`**: Rewrite `generateTeamColors()` to use LCH color space
- Golden angle hue distribution clamped to ±12° to preserve team
identity
- Chroma oscillates ±10% around the base to add variety without washing
out
- Lightness alternates ±18 around the base to keep teammates
recognizable

## Why LCH?

LCH is a perceptually uniform color space, meaning equal numeric
differences correspond to equal perceived differences. This produces
team color variations that look more consistent and distinguishable
compared to HSL-based generation.

## Notes

- The "skip ally attack confirmation" feature that was previously in
this PR has been split into a separate PR as requested by @evanpelle.
This commit is contained in:
rubenperezrial
2026-02-08 04:56:06 +01:00
committed by GitHub
parent e97f4650b7
commit 1ef0cf28a1
+16 -9
View File
@@ -24,20 +24,27 @@ export const greenTeamColors: Colord[] = generateTeamColors(green);
export const botTeamColors: Colord[] = [botColor];
function generateTeamColors(baseColor: Colord): Colord[] {
const hsl = baseColor.toHsl();
const lch = baseColor.toLch();
const colorCount = 64;
const goldenAngle = 137.508;
return Array.from({ length: colorCount }, (_, index) => {
const progression = index / (colorCount - 1);
if (index === 0) return baseColor;
const saturation = hsl.s * (1.0 - 0.3 * progression);
const lightness = Math.min(100, hsl.l + progression * 30);
// Spread hues evenly across ±12° band using golden angle within that range
const hueShift = ((index * goldenAngle) % 24) - 12;
const h = (lch.h + hueShift + 360) % 360;
return colord({
h: hsl.h,
s: saturation,
l: lightness,
});
// Chroma oscillates ±10% around the base to add variety without washing out
const chromaFactor = 1.0 + 0.1 * Math.sin(index * 0.7);
const c = Math.max(10, Math.min(130, lch.c * chromaFactor));
// Lightness alternates above/below the base using golden angle spacing
// Tighter range (±18) keeps teammates recognizable as the same team
const lightOffset = 18 * Math.sin(index * goldenAngle * (Math.PI / 180));
const l = Math.max(25, Math.min(80, lch.l + lightOffset));
return colord({ l, c, h });
});
}