Evercoder/culori

Suggestion to specify target gamut for clampChroma()

danburzo opened this issue · 1 comments

Discussed in #212

Originally posted by dokozero November 20, 2023
Hello @danburzo,

I use Culori for the OkColor Figma plugin and it's quite useful, however, regarding gamut clipping by the chroma only, when I saw clampGamut() for the first time, I thought that it has an option to specify the target gamut to clamp.

As it was not the case, I ended up making a local modified version that uses inGamut() instead of displayable(), as I read from doc that it was equivalent to inGamut('rgb'). However, I had to duplicate culori's main js file into my project.

With this update, I'm able to easily clamp to sRGB or P3 gamut. I don't use toGamut() as I need to keep the same hue and lightness.

I saw previous issue regarding this topic and my suggestion would be to add a new optional target gamut param to clampChroma(), default to 'rgb' for retro-compatibility:

function clampChroma(color, mode = 'lch', targetGamut = 'rgb') {
  const isInTargetGamut = inGamut(targetGamut)

  color = prepare_default(color)
  if (color === void 0 || isInTargetGamut(color)) return color
  let conv = converter_default(color.mode)
  color = converter_default(mode)(color)
  let clamped = { ...color, c: 0 }
  if (!isInTargetGamut(clamped)) {
    return conv(fixup_rgb(rgb(clamped)))
  }
  let start = 0
  let end = color.c
  let range = getMode(mode).ranges.c
  let resolution = (range[1] - range[0]) / Math.pow(2, 13)
  let _last_good_c
  while (end - start > resolution) {
    clamped.c = start + (end - start) * 0.5
    if (isInTargetGamut(clamped)) {
      _last_good_c = clamped.c
      start = clamped.c
    } else {
      end = clamped.c
    }
  }
  return conv(isInTargetGamut(clamped) ? clamped : { ...clamped, c: _last_good_c })
}

For comparison, current code:

function clampChroma(color, mode = 'lch') {
  color = prepare_default(color)
  if (color === void 0 || displayable(color)) return color
  let conv = converter_default(color.mode)
  color = converter_default(mode)(color)
  let clamped = { ...color, c: 0 }
  if (!displayable(clamped)) {
    return conv(fixup_rgb(rgb(clamped)))
  }
  let start = 0
  let end = color.c
  let range = getMode(mode).ranges.c
  let resolution = (range[1] - range[0]) / Math.pow(2, 13)
  let _last_good_c
  while (end - start > resolution) {
    clamped.c = start + (end - start) * 0.5
    if (displayable(clamped)) {
      _last_good_c = clamped.c
      start = clamped.c
    } else {
      end = clamped.c
    }
  }
  return conv(displayable(clamped) ? clamped : { ...clamped, c: _last_good_c })
}

I saw your answer from #211, with:

oklch(toGamut('p3', 'oklch', differenceEuclidean('oklch'), 0)("oklch(70% 0.4 200)")) 

But personally, I think the new param on clampChroma() would be nice.

Thanks for your time.

Fixed in culori@3.3.0