Skip to content

Suggestion to specify target gamut for clampChroma() #213

@danburzo

Description

@danburzo

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions