omgovich / colord

👑 A tiny yet powerful tool for high-performance color manipulations and conversions
https://colord.omgovich.ru
MIT License
1.63k stars 48 forks source link

Relative lighten/darken #90

Open michal-kurz opened 2 years ago

michal-kurz commented 2 years ago

I replaced color with colord in my project, but found out I don't get the same results, because they treat lighten() and darken() differently: While color shifts color by a fraction of itself, while colord shifts color by a fraction of whole spectrum.

I prototyped this wrapper to implement this functionality:

import { AnyColor, Colord, HslaColor } from 'colord'
import { colord as nativeColord } from 'colord'

export const clamp = (number: number, min = 0, max = 1): number => {
  return number > max ? max : number > min ? number : min
}

export const lightenRelative = (color: AnyColor | Colord, amount: number): HslaColor => {
  const hsla = nativeColord(color).toHsl()

  return {
    h: hsla.h,
    s: hsla.s,
    l: clamp(hsla.l * (1 + amount), 0, 100),
    a: hsla.a
  }
}

class ExtendedColord extends Colord {
  public lightenRelative(amount: number = 0.1) {
    return extendedColord(lightenRelative(this.rgba, amount))
  }

  public darkenRelative(amount: number = 0.1) {
    return extendedColord(lightenRelative(this.rgba, -amount))
  }
}

export const extendedColord = (input: AnyColor | Colord): ExtendedColord => {
  if (input instanceof ExtendedColord) return input
  return new ExtendedColord(input as any)
}

and I will use it for now, but I'd prefer to merge it into the library itself - are you interested in this functionality? I would love to make a PR if so.

Thank you 🙏

EricRovell commented 2 years ago

@michal-kurz, thank you for the idea, I think this is a valid point. I have implemented it via optional relative: Boolean flag, Now we have to wait for the library owner for code review.

Nantris commented 1 year ago

@EricRovell thanks for the great PR!

@omgovich thanks for your incredible work! Do you have an interest in merging that PR? (#91)? We're also moving from Color to colord and this is a really big sticking point for us - it would be days of work to overhaul our project to work without a relative lighten feature.

Nantris commented 1 year ago

I think a relative saturate feature would also be useful. Perhaps this is not necessarily best practice in color manipulation or something, but converting from Qix's Color library - the outputs of saturate() vary wildly and I'm not sure how we could easily transition to colord as a result.

Qix's library does the math like this:

    saturate(ratio) {
        const hsl = this.hsl();
        hsl.color[1] += hsl.color[1] * ratio;
        return hsl;
    },

Assuming I did my math right, the outputs vary as below:

image

I really want to switch to colord but it didn't really make sense for us to put the time into switching to begin with. Nonetheless I decided to undertake the task, and now I'm hoping I'll be able to complete it via an addition of a relative saturate function, or maybe someone has advice for an alternative method for us to get similar outputs?

I know colord is more accurate in its outputs so some of our colors vary by like 1% from our previous values, but colors affected by saturate aren't even close due to relative vs absolute.

EricRovell commented 1 year ago

@Slapbox the author is quite busy these days, unfortunately. I think if you need this PR you can extend the Colord class and override the functionality for your needs🤔

Nantris commented 1 year ago

@EricRovell makes sense! It would be really great to get your PR merged soon, but short of that, maybe consider releasing a plugin for your PR. If I make a plugin for relative saturate, I'll do the same. Even just a gist of our code would surely be a big help to other devs if PRs can't be merged for an extended time.

Nantris commented 1 year ago

Here's a very basic solution for anyone who needs it. It possibly has some mistake in it, as our app still doesn't look as expected, but perhaps that's some other unrelated issue.

import { Colord } from 'colord';

// Copied from colord utils
export const clamp = (number: number, min = 0, max = 1): number =>
  number > max ? max : number > min ? number : min;

// Copied from https://github.com/Qix-/color/blob/master/index.js
function saturateRelative(hslColor, ratio) {
  const { s: saturation } = hslColor;
  const newSaturation = saturation + saturation * ratio;
  return { ...hslColor, s: newSaturation };
}

// Copied from https://github.com/Qix-/color/blob/master/index.js
function desaturateRelative(hslColor, ratio) {
  const { s: saturation } = hslColor;
  const newSaturation = saturation - saturation * ratio;
  return { ...hslColor, s: newSaturation };
}

const relativeSaturatePlugin = ColordClass => {
  ColordClass.prototype.saturate = function (ratio = 1) {
    const mixture = saturateRelative(this.toHsl(), ratio);
    return new ColordClass(mixture);
  };
  ColordClass.prototype.desaturate = function (ratio = 1) {
    const mixture = desaturateRelative(this.toHsl(), ratio);
    return new ColordClass(mixture);
  };
};

function lightenDarken(hslColor, amount, relative) {
  const l = relative ? hslColor.l * (1 + amount) : hslColor.l + amount * 100;
  return { ...hslColor, l: clamp(l, 0, 100) };
}

const relativeLightenDarkenPlugin = ColordClass => {
  ColordClass.prototype.lightenRel = function (amount, relative = true) {
    const adjusted = lightenDarken(this.toHsl(), amount, relative);
    return new ColordClass(adjusted);
  };

  ColordClass.prototype.darkenRel = function (amount, relative = true) {
    const adjusted = lightenDarken(this.toHsl(), 0 - amount, relative);
    return new ColordClass(adjusted);
  };
};

export { relativeSaturatePlugin, relativeLightenDarkenPlugin };