Spot the bug - #125: Color Blender

Why is my potion mixing color mixer returning completely invalid rgb values?

function parseHexToRgb(hex) {
  const cleanHex = hex.replace('#', '');
  const bigint = parseInt(cleanHex, 16);
  return {
    r: (bigint >> 16) & 255,
    g: (bigint >> 8) & 255,
    b: bigint & 255
  };
}

function rgbToHsl({ r, g, b }) {
  const rNorm = r / 255;
  const gNorm = g / 255;
  const bNorm = b / 255;
  const max = Math.max(rNorm, gNorm, bNorm);
  const min = Math.min(rNorm, gNorm, bNorm);
  const d = max - min;
  let h = 0;
  let s = 0;
  const l = (max + min) / 2;

  if (max !== min) {
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case rNorm:
        h = (gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0);
        break;
      case gNorm:
        h = (bNorm - rNorm) / d + 2;
        break;
      case bNorm:
        h = (rNorm - gNorm) / d + 4;
        break;
    }
    h = h / 6;
  }
  return { h, s, l };
}

function blendPotionColors(potionA, potionB, ratio = 0.5) {
  const rgbA = parseHexToRgb(potionA);
  const rgbB = parseHexToRgb(potionB);
  const hslA = rgbToHsl(rgbA);
  const hslB = rgbToHsl(rgbB);

  const mixedH = hslA.h + (hslB.h - hslA.h) * ratio;
  const mixedS = hslA.s + (hslB.s - hslA.s) * ratio;
  const mixedL = hslA.l + (hslB.l - hslA.l) * ratio;

  const hue2rgb = (p, q, t) => {
    let adjustedT = t;
    if (adjustedT < 0) adjustedT += 1;
    if (adjustedT > 1) adjustedT -= 1;
    if (adjustedT < 1 / 6) return p + (q - p) * 6 * adjustedT;
    if (adjustedT < 1 / 2) return q;
    if (adjustedT < 2 / 3) return p + (q - p) * (2 / 3 - adjustedT) * 6;
    return p;
  };

  const q = mixedL < 0.5 ? mixedL * (1 + mixedS) : mixedL + mixedS - mixedL * mixedS;
  const p = 2 * mixedL - q;

  const finalR = Math.round(hue2rgb(p, q, mixedH + 1 / 3) * 255);
  const finalG = Math.round(hue2rgb(p, q, mixedH) * 255);
  const finalB = Math.round(hue2rgb(p, q, mixedH - 1 / 3) * 255);

  return `rgb(${finalR}, ${finalG}, ${finalB})`;
}

Reply with what is broken and how you would fix it.

The main problem is that the hue is being blended directly:

const mixedH = hslA.h + (hslB.h - hslA.h) * ratio;

Hue is circular, so this doesn’t always take the shortest path around the color wheel. For example, blending a hue near 0 with one near 1 can produce a completely different color.

I’d handle the hue as a circular value and interpolate the shortest direction between the two hues. The RGB parsing part looks fine.

Also, I’d clamp ratio between 0 and 1 if it comes from user input, so unexpected values don’t produce invalid results.

That’s a good spot with the hue blending. The circular nature can definitely throw things off if you’re not careful. We’ll see if that’s the whole story when the solution goes up later.

The hue blending always reminds me of trying to mix paint as a kid and ending up with brown every time. It’s easy to forget it wraps around.