Skip to content

fix(hwb): normalize whiteness + blackness over 100% to gray#138

Open
spokodev wants to merge 1 commit into
omgovich:masterfrom
spokodev:fix-hwb-gray-normalization
Open

fix(hwb): normalize whiteness + blackness over 100% to gray#138
spokodev wants to merge 1 commit into
omgovich:masterfrom
spokodev:fix-hwb-gray-normalization

Conversation

@spokodev

Copy link
Copy Markdown

Problem

When an HWB color's whiteness + blackness is >= 100%, the result should be an achromatic gray, but the HWB plugin produces wrong colors:

colord({ h: 120, w: 70, b: 70 }).toHex(); // "#b34db3" (purple)   expected "#808080"
colord({ h: 200, w: 100, b: 100 }).toHex(); // "#000000" (black)  expected "#808080"
colord("hwb(0 50% 60%)").toHex();          // "#668080"           expected "#747474"

These are valid CSS inputs that every browser renders as gray.

Cause

hwbaToRgba converted via HSV without the w + b >= 100% normalization:

  • For w + b > 100, 100 - (w / (100 - b)) * 100 is negative, feeding a negative saturation into HSV → garbage.
  • The b === 100 special case returned s: 0, v: 0 (black) instead of gray.

Fix

Per CSS Color 4 §8.1, when white + black >= 100% the color is gray with value white / (white + black):

if (hwba.w + hwba.b >= 100) {
  const gray = (hwba.w / (hwba.w + hwba.b)) * 255;
  return { r: gray, g: gray, b: gray, a: hwba.a };
}

The b === 100 special case is removed because every b === 100 input has w + b >= 100 and is now handled by the new branch. At the exact w + b === 100 boundary the new branch yields the same value the old HSV path did, so nothing else changes.

Verification

  • New test in tests/plugins.test.ts; fails before, passes after. Full suite: 78 passing.
  • Differential vs the CSS Color 4 algorithm (pure-hue HSL mix, independent of the HSV path used here) over a 15°×5%×5% grid plus 500,000 random inputs: 0 divergences (>1 LSB). This also confirms the untouched w + b < 100 path already matches the spec.

Per CSS Color 4, when whiteness + blackness reaches 100% an HWB color is
an achromatic gray with value white / (white + black). `hwbaToRgba` was
missing this step, so over-specified inputs produced wrong colors:

  colord({ h: 120, w: 70, b: 70 }).toHex()  // was "#b34db3", now "#808080"

For w + b > 100 the existing path fed a negative saturation into HSV; the
`b === 100` special case also returned black instead of gray. Both are now
covered by the spec normalization, so the special case is removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant