/** * http://seer.ufrgs.br/rita/article/viewFile/rita_v16_n2_p141/7428 * * @module color-space/hsm */ import rgb from './rgb.js'; var hsm = { name: 'hsm', min: [0, 0, 0], max: [1, 1, 1], channel: ['hue', 'saturation', 'mixture'] }; export default hsm /** * HSM to RGB * * @param {Array} hsm Channel values * * @return {Array} RGB channel values */ hsm.rgb = function ([h, s, m]) { // This implementation uses an alternate derivation (with help of GPTs), since the one in paper is incorrect. // The idea is that the original RGB is recovered as: // [r, g, b] = m + d, // where d = s * D(m) * [cos(omega)*u + sin(omega)*v], // with omega = 2π * h. // We choose reference vector u as the normalized version of (3, -4, -4) (this is // consistent with the forward computation of hue in the paper) and then take v to be // an orthonormal vector in the same plane. The plane is defined by the constraint // 4*(r-m) + 2*(g-m) + (b-m) = 0. // 1. Compute normalization denominator D(m) (FIXED: piecewise terms from forward transform) let D; if (m >= 0 && m <= 1 / 7) D = Math.sqrt((0 - m) ** 2 + (0 - m) ** 2 + (7 - m) ** 2); else if (m > 1 / 7 && m <= 3 / 7) D = Math.sqrt((0 - m) ** 2 + ((7 * m - 1) / 2 - m) ** 2 + (1 - m) ** 2); else if (m > 3 / 7 && m <= 0.5) D = Math.sqrt(((7 * m - 3) / 2 - m) ** 2 + (1 - m) ** 2 + (1 - m) ** 2); else if (m > 0.5 && m <= 4 / 7) D = Math.sqrt((7 * m / 4 - m) ** 2 + (0 - m) ** 2 + (0 - m) ** 2); else if (m > 4 / 7 && m <= 6 / 7) D = Math.sqrt((1 - m) ** 2 + ((7 * m - 4) / 2 - m) ** 2 + (0 - m) ** 2); else if (m > 6 / 7 && m <= 1) D = Math.sqrt((1 - m) ** 2 + (1 - m) ** 2 + (7 * m - 6 - m) ** 2); else D = 1; // 2. Compute chromatic magnitude (FIXED: include D(m)) const R = s * D; // 3. Precompute coefficients (FIXED: orthonormal basis vectors) const cosTheta = Math.cos(2 * Math.PI * h); const sinTheta = Math.sin(2 * Math.PI * h); const u_r = 3 / Math.sqrt(41), v_r = -4 / Math.sqrt(861); // FIXED: correct u/v components const u_g = -4 / Math.sqrt(41), v_g = 19 / Math.sqrt(861); const u_b = -4 / Math.sqrt(41), v_b = -22 / Math.sqrt(861); // 4. Compute deviations (FIXED: use u/v basis) const dr = R * (u_r * cosTheta + v_r * sinTheta); const dg = R * (u_g * cosTheta + v_g * sinTheta); const db = R * (u_b * cosTheta + v_b * sinTheta); // 5. Reconstruct RGB (FIXED: add m directly, clamp values) const r = Math.max(0, Math.min(1, m + dr)) * 255; const g = Math.max(0, Math.min(1, m + dg)) * 255; const b = Math.max(0, Math.min(1, m + db)) * 255; return [r, g, b]; }; /** * RGB to HSM * * @param {Array} rgb Channel values * * @return {Array} HSM channel values */ rgb.hsm = function ([r, g, b]) { r /= 255, g /= 255, b /= 255; let m = (4 * r + 2 * g + b) / 7; // distance in the deviation space let dr = r - m, dg = g - m, db = b - m; let d = Math.sqrt(dr * dr + dg * dg + db * db); // calc hue based on the angle in the deviation plane. let theta = Math.acos(((3 * dr - 4 * dg - 4 * db) / Math.sqrt(41 * (dr * dr + dg * dg + db * db))) || 0); let h = (b <= g) ? theta / (2 * Math.PI) : 1 - theta / (2 * Math.PI); // Calculate saturation (s) based on the value of m let s; if (0 <= m && m <= 1 / 7) s = d / Math.sqrt((0 - m) ** 2 + (0 - m) ** 2 + (7 - m) ** 2); else if (1 / 7 < m && m <= 3 / 7) s = d / Math.sqrt((0 - m) ** 2 + ((7 * m - 1) / 2 - m) ** 2 + (1 - m) ** 2); else if (3 / 7 < m && m <= 1 / 2) s = d / Math.sqrt(((7 * m - 3) / 2 - m) ** 2 + (1 - m) ** 2 + (1 - m) ** 2); else if (1 / 2 < m && m <= 4 / 7) s = d / Math.sqrt(((7 * m) / 4 - m) ** 2 + (0 - m) ** 2 + (0 - m) ** 2); else if (4 / 7 < m && m <= 6 / 7) s = d / Math.sqrt((1 - m) ** 2 + ((7 * m - 4) / 2 - m) ** 2 + (0 - m) ** 2); else if (6 / 7 < m && m < 1) s = d / Math.sqrt((1 - m) ** 2 + (1 - m) ** 2 + ((7 * m - 6) - m) ** 2); else s = 0; // s = Math.max(0, Math.min(1, s)) return [h, s, m] };