Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functions to get min/max component values for all color types, alpha #173

Merged
merged 1 commit into from Mar 23, 2020

Conversation

okaneco
Copy link
Contributor

@okaneco okaneco commented Mar 19, 2020

Add functions to retrieve the maximum and minimum component values for all color types and alpha.
There are no functions for min/max values of Hue components since they are periodic and range from 0 to 360 degrees, 360 degrees not inclusive.
Lch and HWB are special cases:

  • Lch chroma is fully saturated starting at 128 but continues to 181, so two max chroma functions are implemented.
  • Hwb's whiteness and blackness components both have a max of 1 but they cannot simultaneously be 1. The sum of whiteness and blackness must be less than or equal to 1.

@okaneco
Copy link
Contributor Author

okaneco commented Mar 19, 2020

I'm working on a project where I want to generate random Lab colors but possibly other spaces in the future. Limited seemed like a good candidate to include these additions under. Of the 10 types, most were straightforward but value judgements had to be made for Hwb and Lch.

Hwb

Hwb is a special case due to its validity check:

fn is_valid(&self) -> bool {
    self.blackness >= T::zero() && self.blackness <= T::one() &&
    self.whiteness >= T::zero() && self.whiteness <= T::one() &&
    self.whiteness + self.blackness <= T::one()
}

Technically Hwb(360, 1, 1) is invalid but seems like the right choice for a maximum value. I don't use the space or see a reason I'll ever use it but it feels wrong to specify the max as an invalid color. It's a strange model compared to every other color in the crate though so probably the best that can be done. If the user is using it they should know then that it'd have to be Hwb(360, 1, 0), Hwb(360, 0, 1), or Hwb(360, 0.5, 0.5) and half the values are grayscale.

Lch

For Lch, chroma is a bit special. From the docs:

C* is the colorfulness of the color. It's similar to saturation. 0.0 gives gray scale colors, and numbers around 128-181 gives fully saturated colors. The upper limit of 128 should include the whole Lab* space and some more.

I chose 128 as the maximum value to be returned even though it can actually be up to ~181. However, the values in the fully saturated range are well out of space for Srgb.

@Ogeon
Copy link
Owner

Ogeon commented Mar 19, 2020

Yeah, it's hard to define what's really the lower and upper bounds in some cases. It's a questionable concept for hues, as they are periodic, and even stranger for HWB, as you noticed. Most of the spaces have tricky detail, really, but it's usually fine to ignore them and/or clamp the final output.

Considering that this is based on your project's use case, would you mind posting the code for generating the colors, for the discussion? I'm wondering if having associated constants like {MIN,MAX}_RED and maybe even implementing rand support would be a less ambiguous option.

Also, for Lch, I'm not sure what to do for the chroma. In case you haven't looked into the details, Lab is basically a cube, while Lch is a cylinder. With chroma=128, the cylinder will reach the sides of the Lab cube, but not its corners.

Issues like with Lab and Hwb is why I'm leaning towards looking into options. If random generation was built in, we could just suggest using Lab when Lch doesn't generate the desired results, without defining a definite chroma limit. Besides, generating cylindrical colors without taking the clumping in the center into consideration may generate too many grayish samples. And Hwb could be generated in a correct and uniform way from the start.

I don't know if I'm over thinking it, but I think random generation is something that could be provided as an optional feature.

@okaneco
Copy link
Contributor Author

okaneco commented Mar 19, 2020

Yes, the main point is that there are a lot of details to take into account across the various models and I wanted to reduce some of the cognitive load for users (and myself). I think there are some places for improvement in reducing possible confusion or at least providing direction. The documentation is there but it's not obvious what the implication of each unit or scaling is for color type values.

Here's a simple example of a color I'm generating from a range using rand. It's uniform by default.

let lab = Lab::new(
    rng.gen_range(0.0, 100.0),
    rng.gen_range(-128.0, 127.0),
    rng.gen_range(-128.0, 127.0),
);

// With the PR, I imagined making random colors like this but still feels clunky
let min = RGB.get_lower_bounds();
let max = RGB.get_upper_bounds();
let rgb = Rgb::new(
    rng.gen_range(min.red, max.red),
    rng.gen_range(min.green, max.green),
    rng.gen_range(min.blue, max.blue),
);

Something like a MAX_L, the associated constants you mentioned, would be helpful in a lot of places. One thing I was working on was generating initial values for k-means clustering centroids which relies on random generation of their positions. I debated only implementing it for the most common types like Lab and Rgb. Hues being periodic made me somewhat hesitant as well but I tend to assume people are operating in a range of 0-2π/0-360.

Another case recently of using the bounds was adjusting the luma of two colors until they had an adequate contrast ratio. I was able to keep the colors close enough to their original values while getting something that had good contrast for text and a background color by adjusting each proportionally 1/4 of the way toward black/white.

I'm open to any options and also the idea of rand support sounds like a good idea, I don't think you're overthinking it. By the nature of being cylindrical I wouldn't want to pick Lch as a space for random generation as you mentioned. Hwb as well despite being advertised as "intuitive" is something that implementers have to handle with care. The concept for this PR works for some of these types but definitely not all of them.

@Ogeon
Copy link
Owner

Ogeon commented Mar 19, 2020

Sounds like the associated constants would fulfill your immediate needs. It's effectively the same thing, but without the additional implications of being valid in combination. But I'm not sure how well they work with type parameters, now that I think about it. They may need to be static methods instead (i.e. Lch::min_luma()) if it becomes too complicated. The hues are probably trivial enough to maybe not need them, but I guess it doesn't hurt. Just funny how min and max are the same hues...

The rand support can be added in a separate stage. Given how tricky it is to get uniform samples in some cases (especially for HSL/HWB), I think it could be a good addition. But behind a feature flag to keep it small. I think there will be more design considerations there, though, so let's wait with that. Generating Lab values is straight forward enough without it (just as you did it).

I'll create an issue, but feel free to add any use case you stumble upon. It's always useful.

@Ogeon Ogeon mentioned this pull request Mar 19, 2020
22 tasks
@okaneco
Copy link
Contributor Author

okaneco commented Mar 20, 2020

This is a more conservative approach. I didn't provide any min or max for Hues or Hwb at all.

I couldn't originally figure out Xyz but then I did. Wp::get_xyz() is called to get the reference values of a whitepoint for Xyz. For Xyz min/max, it almost makes sense to return an Xyz<Wp, T> instead of T since an Xyz has to be created each time a component min/max is requested. This wouldn't be consistent with how the other methods work though.

I tried to mark the functions as const but that's not supported yet. Is marking them static worth it or wait until const is stabilized in the future?


The build failed on the no_std_test initially. For some reason the Float trait is dropped or ambiguous? If I add a use statement, it'll turn into a warning about an unused trait. I don't understand why this happens.

error[E0599]: no method named `sqrt` found for type `f64` in the current scope
   --> palette/src/:53
    |
133 |         from_f64((127.0f64 * 127.0 + 128.0 * 128.0).sqrt())
    |                                                     ^^^^ method not found in `f64`
    |
    = help: items from traits can only be used if the trait is in scope
    = note: the following traits are implemented but not in scope; perhaps add a `use` for one of them:
            candidate #1: `use num_traits::float::Float;`
            candidate #2: `use num_traits::real::Real;`
error: aborting due to previous error

I fixed it with

// fails
from_f64((127.0f64 * 127.0 + 128.0 * 128.0).sqrt())

// passes
from_f64(crate::float::Float::sqrt(127.0f64 * 127.0 + 128.0 * 128.0))

Copy link
Owner

@Ogeon Ogeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I noticed HWB and Alpha disappeared, but I think they can be added back in for completeness' sake.

palette/src/hsv.rs Outdated Show resolved Hide resolved
palette/src/lch.rs Outdated Show resolved Hide resolved
@okaneco
Copy link
Contributor Author

okaneco commented Mar 20, 2020

I readded HWB and Alpha.

I removed HWB because I wasn't sure about it in general. You need whiteness to get the max blackness and you need blackness to get the max whiteness. The straight answer is that they both max out at 1 but it's the only model with dependent components in the library.

I wasn't sure whether to implement for Alpha now that it wasn't part of a trait. I also wasn't sure whether the max should be T::max_intensity or T::one. Similarly, should Yxy stay T::one or be switched to max_intensity?

@okaneco
Copy link
Contributor Author

okaneco commented Mar 20, 2020

5 builds passed but the Mac stable errored out as unresponsive. They were having issues recently with Mac builds https://www.traviscistatus.com/incidents/9861dc7dtsb0?u=nfsz8t7c7m1q

I noticed many errors in the past few commits with 5b11613 a few hours ago being the last one where all Mac branches built.

@okaneco okaneco changed the title Add get_lower_bound and get_upper_bound to Limited Add functions to get min/max component values for all color types, alpha Mar 20, 2020
@Ogeon
Copy link
Owner

Ogeon commented Mar 20, 2020

I restarted the Mac build. Looks like it's working this time.

I removed HWB because I wasn't sure about it in general.

Understandable. I think it's less weird now when they are not part of a complete color value. The value on its own doesn't have any other meaning than "this is the maximum value of this particular channel".

Similarly, should Yxy stay T::one or be switched to max_intensity?

Yes, it should be max_intensity wherever it's on a 0-100% like scale. Floats with 0-1 scales and integers with 0-MAX scales are basically rescaled percentages and max_intensity is just 100% in whichever data representation we happen to use. So it depends on if it's used for describing the number 1 specifically or just the max intensity of a color channel in general.

Comment on lines 131 to 143
/// Return the `chroma` value maximum. This value will suffice for most
/// use cases.
pub fn max_chroma() -> T {
from_f64(128.0)
}

/// Return the `chroma` extended maximum value.
pub fn max_extended_chroma() -> T {
from_f64(crate::float::Float::sqrt(127.0f64 * 127.0 + 128.0 * 128.0))
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation should probably describe the geometric reason for picking one or the other. I don't have any good suggestion off the top of my head right now, but something about how the first one doesn't cover the full space, while still covering enough to be useful, and the second one covering the full space, but also overshoothing.

Oh, and I forgot before, but the square root thingy should maybe use 128 all the way. That way it should reach the negative corner. even if it extends outside all other corners. Once again, this format is weird, but it's the expected one.

@Ogeon
Copy link
Owner

Ogeon commented Mar 20, 2020

Never mind, the Mac build gave up. We can try again later.

Add functions for Alpha, Hsl, Hsv, Hwb, Lab, Lch, Luma, Rgb, Yxy, Xyz
Add max_extended_chroma for Lch in addition to max_chroma
@okaneco
Copy link
Contributor Author

okaneco commented Mar 21, 2020

I've updated the documentation for max_chroma/extended, hopefully it will suffice but I can change with any feedback.

Travis is in maintenance but should be back up soon. https://www.traviscistatus.com/incidents/blv0v9hd1flk?u=f362jrtv8qb6

@Ogeon
Copy link
Owner

Ogeon commented Mar 23, 2020

Looks like they may have fixed the issues now, so time to merge. Thanks for this addition!

bors r+

@bors
Copy link
Contributor

bors bot commented Mar 23, 2020

Build succeeded

@bors bors bot merged commit acecaee into Ogeon:master Mar 23, 2020
@okaneco okaneco deleted the bounds branch March 23, 2020 21:18
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.

None yet

2 participants