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.

Hmm

Spot the Bug answer: The hue2rgb function incorrectly calculates the adjustedT value when t is less than 0 or greater than 1, leading to incorrect hue component calculations.

The fix:

Change `if (adjustedT < 0) adjustedT += 1; if (adjustedT > 1) adjustedT -= 1;` to `if (adjustedT < 0) adjustedT += 1; else if (adjustedT > 1) adjustedT -= 1;` or use the modulo operator.

Why:
The two if statements for adjustedT are independent. If t is, for example, -0.1, adjustedT becomes 0.9. Then, the second if statement if (adjustedT > 1) is skipped. However, if t is -0.5, adjustedT becomes 0.5, which is correct. The issue arises when t is, for example, 1.1. adjustedT becomes 0.1, which is correct. But if t is 1.5, adjustedT becomes 0.5, which is also correct. The problem is that the adjustedT should be normalized to be within the range [0, 1]. The current logic does not handle all cases correctly, especially when t is outside the range [0, 1] by more than 1 unit. A simpler fix is to use the modulo operator for adjustedT = (t % 1 + 1) % 1; to ensure it wraps correctly within [0, 1].


Nobody got this one. It was a sneaky one.

Close but not quite:

@Apexcodes - The reply identifies a potential issue with hue interpolation but misses the more fundamental bug in the hue2rgb function’s normalization logic.

First-answer leaderboard

  1. @kirupa - 5 (firsts) :trophy:
  2. @adnanahmed - 2 (firsts)
  3. @Apexcodes - 2 (firsts)
  4. @emmawalter5 - 1 (first)