Skip to content

[css-color-hdr] How should checking for percentages summing to 0 work in dynamic-range-limit-mix() and color-mix() when calc() is used? #11678

Closed

Description

@weinig

The text for dynamic-range-color-mix() contains this statement:

If the sum of all percentages is 0%, then the function is invalid.

If the intent is that this is checked at parse time, which is when things are usually marked invalid, what should happen for calc() values that aren't resolvable until style building?

Activity

  1. weinig commented on Feb 9, 2025

    @weinig
    ContributorAuthor
  2. ccameron-chromium commented on Feb 10, 2025

    @ccameron-chromium
    Contributor

    This should be the same as with color-mix. I vaguely remember there being some words like "if it can be determined to be 0% then it is invalid". But maybe we should change both of those to say "if it sums to 0% then distribute all parameters evenly". That would keep things simpler.

  3. weinig commented on Feb 10, 2025

    @weinig
    ContributorAuthor

    Ah! Looks like I had the same concern with color-mix when implementing that, https://github.com/WebKit/WebKit/blob/main/Source/WebCore/css/parser/CSSPropertyParserConsumer%2BColor.cpp#L617.

    I think your suggestion makes sense.

  4. svgeesus commented on Feb 10, 2025

    @svgeesus
    Collaborator

    I see that we only have two WPT tests (14 subtests in total), none of which tests

    If the percentages sum to zero, the function is invalid.

  5. ccameron-chromium commented on Feb 10, 2025

    @ccameron-chromium
    Contributor

    For dynamic-range-limit, this test is testing the behavior. But I'm totally fine with changing it.

  6. squelart commented on Feb 10, 2025

    @squelart
    Contributor

    I think there could be some ambiguity and/or inconsistency with accepting a sum of 0%:
    The proposed solution would compute:

    • mix(a 0%, b 0%) to be mix(a 50%, b 50%)
    • mix(a 0%, b 0%, c 0%) to be mix(a 33%, b 33%, c 33%)

    However, since we have the other kind of equivalence mix(a 50%, b 50%) == mix(a 50%, b 50%, c 0%), it would be consistent that mix(a 0%, b 0%) == mix(a 0%, b 0%, c 0%), and now we have a conflict as they result in something different!
    (Or did I miss some detail?)

  7. css-meeting-bot commented on Feb 26, 2025

    @css-meeting-bot
    Member

    The CSS Working Group just discussed [css-color-hdr] How should checking for percentages summing to 0 work in dynamic-range-limit-mix() when calc() is used?, and agreed to the following:

    • RESOLVED: remove the sentence in spec that says if sum is zero, treat as invalid; possibly add new prose about splitting the percentage, and update tests
    The full IRC log of that discussion <jcraig> weinig: issue is that the spec for dynamic color mix has rules applied at parse time that check if value is zero
    <jcraig> if all percentages add up to zero, they are invalid, but calc() may not know if it's invalid yet
    <jcraig> options: either define at calc time what do in that scenario, mark as invalid at computed style time, or have it behave as if all percentages are the same; evenly divided
    <jcraig> someone brought up the a concern with 50 high 50 low example, nothing constrained, you'd get 33/33/33 (not sure if I scribed that correctly)
    <jcraig> currently we even split it, but worth considering when animating the ramps to zero
    <jcraig> ChrisL: people will ramp it for animation so we should ensure it's not jarring
    <jcraig> weinig: no current test for that.... but yes, should fix that
    <jcraig> propose if all zero, treat as all at 33%
    <jcraig> ChrisL: sounds reasonable
    <jcraig> proposal: remove the sentence in spec that says if sum is zero, treat as invalid
    <jcraig> weining: i can take on an item to work up the correct prose for that change to splitting the percentage
    <jcraig> s/treat as invalid/treat as invalid; possibly add new prose about splitting the percentage, and update tests/
    <jcraig> astearns: no objections resolved...
    <jcraig> rrsagent, make minutes
    <RRSAgent> I have made the request to generate https://www.w3.org/2025/02/26-css-minutes.html jcraig
    <jcraig> RESOLVED: remove the sentence in spec that says if sum is zero, treat as invalid; possibly add new prose about splitting the percentage, and update tests
  8. self-assigned this
    on Feb 26, 2025
  9. tabatkins commented on Feb 26, 2025

    @tabatkins
    Member

    However, since we have the other kind of equivalence mix(a 50%, b 50%) == mix(a 50%, b 50%, c 0%), it would be consistent that mix(a 0%, b 0%) == mix(a 0%, b 0%, c 0%), and now we have a conflict as they result in something different!
    (Or did I miss some detail?)

    Yeah, I think the right answer is to just make the output transparent black when the sum is zero.

    The existing behavior can also be conceptualized as adding an extra argument of transparent N%, where N is the leftover % needed to sum to 100%, and in a premultiplied color space that's exactly identical to the current behavior and is well-defined all the way down to 0%. In a non-premultiplied space you have to handle things slightly more manually (thus the current text), but continuing to treat this as the preferred mental model, the limit behavior at 0% should still result in transparent.

  10. svgeesus commented on Mar 12, 2025

    @svgeesus
    Collaborator

    Yeah, I think the right answer is to just make the output transparent black when the sum is zero.

    @tabatkins That work for color-mix(). It isn't clear what the equivalent conclusion would be for dynamic-range-limit-mix().

  11. svgeesus commented on Mar 13, 2025

    @svgeesus
    Collaborator

    Yeah, I think the right answer is to just make the output transparent black when the sum is zero.

    Currently, for color-mix(), browsers mark the property value as invalid (which was correct until 2 weeks ago). However we can see what they would do otherwise by making the percentages very close to zero (like 0.01%) and then using RCS to make the alpha 1 again.

    background-color: oklab(from color-mix(in oklab, red 0.01%, green 0.01%) l a b / 1);

    Live test shows that they do not produce transparent black; they produce the actual mixed color. So we would have a discontinuity at 0%. Which may be the best we can do.

    The current spec text needs to be updated, because with percentages summing to zero there is no defined color produced.

  12. svgeesus commented on Mar 13, 2025

    @svgeesus
    Collaborator

    @tabatkins wrote

    in a premultiplied color space that's exactly identical to the current behavior and is well-defined all the way down to 0%. In a non-premultiplied space you have to handle things slightly more manually (thus the current text),

    I don't understand why you state that the current text is non-premultiplied. Color 5 3.2. Calculating the Result of color-mix references Color 4 12. Color Interpolation in which step 5 is "changing the color components to premultiplied form".

  13. tabatkins commented on Mar 13, 2025

    @tabatkins
    Member

    Ah, my mistake, I didn't realize the text had behavior for premultiplying polar spaces. Cool. I assumed the manual handling of the bonus alpha was a required step, not just a convenience to avoid invoking the full interpolation machinery just to do a trivial alpha adjustment to the result.

    [example with using tiny equal %] So we would have a discontinuity at 0%. Which may be the best we can do.

    Yeah, I think it's the best generic behavior to do - just have some reasonably neutral default value that gets triggered by this corner case.

    After all, you can't rely on limit arguments for this - if the initial %s are 20% and 80%, and they gradually scale down to 0%, they'll also arrive at 0% at the same time, but with a totally different interpolated hue. So any assumption you make about mixing weights will be wrong for some examples.

    "Might as well make them all equally wrong" isn't always the best thing to do, of course, but when talking about it generically, like with dynamic-range-limit-mix(), it's not clear there's any better answer. Just let the "default" value fill in any missing %, and if all the specified %s are 0, then the default value is what you get, regardless of any fancier mixing mechanics you'd get with small non-zero %s. I presume for dynamic-range-limit-mix() we'd say the default value is the initial value of dynamic-range-limit.

    (Note #6245 (comment) where we're defining the *-mix() family more generically, and explicitly talk about a "neutral" element taking up the leftover %. We didn't mention this corner case, but it follows naturally, I think.)

  14. changed the title [-][css-color-hdr] How should checking for percentages summing to 0 work in dynamic-range-limit-mix() when calc() is used?[/-] [+][css-color-hdr] How should checking for percentages summing to 0 work in `dynamic-range-limit-mix()` and `color-mix()` when calc() is used?[/+] on Mar 26, 2025
  15. svgeesus commented on Mar 26, 2025

    @svgeesus
    Collaborator

    I updated the issue title because a lot of the discussion has been about color-mix() and it is hard to disentangle into a separate issue.

    So, per WG resolution, the sentence in Color 5 about zero being invalid has been removed. That means we continue on to step 6:

    Otherwise, if both are provided and add up to less than 100%, the sum is saved as an alpha multiplier. They are then scaled accordingly so that they add up to 100%.

    That is going to need to be altered because, while we now have an alpha multiplier (zero), no possible scaling will make p1 and p2 sum to 100. Which is fine because we can just return the result, without any interpolation, in that corner case.

  16. svgeesus commented on Mar 26, 2025

    @svgeesus
    Collaborator

    @weinig @tabatkins For color-mix(), since we removed the validity check, it doesn't matter whether the zero percentages are specified with calc() or directly as 0%. So I think the spec is now self-consistent and WPT that check for zero-sum being invalid can be edited to check instead for transparent.

    Now on to dynamic-range-limit-mix(). We removed the invalidity check, so we arrive at step 6:

    Otherwise, the computed value is dynamic-range-limit-mix(), with parameters standard, constrained-high, and no-limit, in that order, and percentages p_standard, p_constrained_high, and p_no_limit, omitting parameters with a percentage equal to 0%.

    If they all sum to zero then all parameters are omitted, which we can't really have

    dynamic-range-limit-mix()

    so I guess we need an all-zero check that returns the initial value of ``dynamic-range-limit`.

  17. ccameron-chromium commented on May 22, 2025

    @ccameron-chromium
    Contributor

    The proposed change sounds good to me.

    One other issue on the topic of calc(). If the result of calc() is outside of the range [0,100], should the value be clamped to that range (otherwise we start to have to worry about negative numbers, etc).

  18. svgeesus commented on May 22, 2025

    @svgeesus
    Collaborator

    Yes, I believe that calc() result should be clamped.

  19. tabatkins commented on May 22, 2025

    @tabatkins
    Member

    Yup, all math functions clamp their output to the allowed range for their context, automatically.

  20. svgeesus commented on Jun 19, 2025

    @svgeesus
    Collaborator

    Since @tabatkins said that calc() always clamps, I didn't mention it explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions