Files
tiennm99 651b123f05 feat: rebalance KIM bucket via L≥70 'metallic shine' rule
KIM was effectively empty (3 GH / 0 GL) because the old rule required
S<5 AND L≥70 — only near-white grays. Generalize KIM to any color with
L≥70, regardless of hue. Stretches Ngũ Hành orthodoxy slightly (KIM was
canonically white-only) but stays within the spirit: highly-reflective
pastels read as 'metallic shine'.

New distribution:
  GitHub: KIM 3 → 65 (10%)
  GitLab: KIM 0 → 6  (7%)

Sample-language drift: only Kotlin #A97BFF (L=74 light purple) moves
HOẢ → KIM. Updated fixture and plan acceptance criteria to match.

See plans/reports/brainstorm-260427-1046-kim-rebalance.md
2026-04-27 11:50:58 +07:00

58 lines
1.6 KiB
JavaScript

// Algorithm: plans/reports/researcher-260427-0854-nguhanh-color-classifier.md §2
// Pure HSL classifier mapping a hex color to one of the 5 Ngũ Hành elements.
export const ELEMENTS = [
{ key: 'kim', label: 'KIM' },
{ key: 'moc', label: 'MỘC' },
{ key: 'thuy', label: 'THUỶ' },
{ key: 'hoa', label: 'HOẢ' },
{ key: 'tho', label: 'THỔ' },
];
const HEX_RE = /^#[0-9a-fA-F]{6}$/;
export function hexToHsl(hex) {
if (typeof hex !== 'string' || !HEX_RE.test(hex)) {
throw new TypeError(`hexToHsl: expected '#RRGGBB', got ${hex}`);
}
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 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;
let s = 0;
if (d !== 0) {
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d) + (g < b ? 6 : 0); break;
case g: h = ((b - r) / d) + 2; break;
case b: h = ((r - g) / d) + 4; break;
}
h *= 60;
}
return { h, s: s * 100, l: l * 100 };
}
export function classify(hex) {
const { h, s, l } = hexToHsl(hex);
// Step 1: KIM — high lightness ("metallic shine"); catches whites + pastels of any hue
if (l >= 70) return 'kim';
// Step 2: grayscale (very low saturation, lightness already < 70)
if (s < 5) return l < 20 ? 'thuy' : 'tho';
// Step 3: hue ranges
if (h < 20) return 'hoa';
if (h < 40) return (s >= 60 && l >= 50) ? 'hoa' : 'tho';
if (h < 70) return 'tho';
if (h < 200) return 'moc';
if (h < 260) return 'thuy';
return 'hoa';
}