w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.51k stars 669 forks source link

Proposal: Add an incremental syntax to CSS Gradients #8616

Open Afif13 opened 1 year ago

Afif13 commented 1 year ago

I think it's good to have a kind of "relative values" to express a relation between two consecutive color stops.

Let's say I have linear-gradient(red 20%,blue 40%,green 60%) where each time I am adding 20% to the color stops. It would be good if I can, instead, write linear-gradient(red 20%,blue +20%,green +20%). The plus sign means "take the last color stop and increment it". This way we can easily express the 20% as a variable and reuse it.

Here are a few more examples:

/* Instead of */
background: linear-gradient(red 50%, blue calc(50% + 1px));
/* We write */
background: linear-gradient(red 50%, blue +1px);

/* Instead of */
background: linear-gradient(red 20%, blue calc(20% + 20px), green calc(20% + 40px));
/* We write */
background: linear-gradient(red 20%, blue +20px, green +20px);

/* Instead of */
background: conic-gradient(red 60deg, blue 120deg, green 180deg, yellow 240deg);
/* We write */
background: conic-gradient(red 60deg, blue +60deg, green +60deg, yellow +60deg);
/* We can also write */
background: conic-gradient(red +60deg, blue +60deg, green +60deg, yellow +60deg);
/* the first +60deg will be relative to 0deg so the same as 60deg */

/* Instead of */
background: repeating-linear-gradient(red 0 10px, blue 0 20px);
/* we write */
background: repeating-linear-gradient(red 0 10px, blue 0 +10px);
/* the browser will transform the 0 after blue to 10px then will use that value as a reference for the next one to have 10px+10px = 20px
/* we can also write it like below */
background: repeating-linear-gradient(red 0 +10px, blue 0 +10px);
/* the "0 +10px" can be a variable */

I think the + won't create any confusion in the syntax. It can be an optional character before the value to indicate if the value is static or relative.

The calculation should also be done after the Color stop "Fixup" like I did in the last example. When doing linear-gradient(red 150px, blue 70px +50px) we should have 150px + 50px and not 70px + 50px.

romainmenke commented 1 year ago

+60deg is just 60deg with an explicit sign, so that alone won't work. But + 60deg with a space between + and a dimension token could be a way to express incremental values.

Edit : ++60deg might be more readable. As CSS tokens this is a delim token + and a dimension token with explicit sign.

Afif13 commented 1 year ago

+60deg is just 60deg with an explicit sign. But + 60deg with a space between + and a dimension token could be a way to express incremental values.

Edit : ++60deg might be more readable. As CSS tokens this is a delim token + and a dimension token with explicit sign.

Yes, but I thought that since the + is rarely used to indicate positive values we can do an exception inside gradients and use it as increment. Maybe we should consider another character to avoid conflict?

romainmenke commented 1 year ago

++ has the benefit of being well established in other languages and the intent is the same as it would be here : an implicit read and increment with whatever is on the right of ++.

The only awkward thing about a syntax like this is that there really isn't a meaningful difference between ++60deg, +-60deg --60deg, -+60deg, +calc(30deg * 2).

++ and -- aren't real operators in this syntax. One symbol is a delim and the other is the sign of the dimension token.


If this is something you want to prototype as a PostCSS plugin, please let me know, happy to help out. I really like the work you do with CSS gradients!

Afif13 commented 1 year ago

I won't say no to a prototype 😊 if it's something easy to do why not. It can help showing real examples and make the proposal even better! Thank you :+1:

brandonmcconnell commented 1 year ago

@Afif13 I really like this idea!

To make the syntax clear, are you thinking it would always either be a double-plus/minus, so either ++60px or --60px for example?

Another potential idea re the syntax that might be clearer—just thinking out loud here—is to introduce a function that only works in this context, like shift(), which could be used like this:

.selector {
  background: linear-gradient(red 20%, blue shift(20px), green shift(20px));
  background: conic-gradient(red 60deg, blue shift(60deg), green shift(60deg), yellow shift(60deg));
  background: conic-gradient(red shift(60deg), blue shift(60deg), green shift(60deg), yellow shift(60deg));
                                /* ┗━ when using shift without a previous value to shift from,
                                      it would shift from 0, so `shift()` would essentially do
                                      nothing as a first value, but still be valid syntax */
  background: repeating-linear-gradient(red 0 10px, blue 0 shift(10px));
  background: repeating-linear-gradient(red 0 shift(10px), blue 0 shift(10px));
}

Outside of these whitelisted contexts, shift() would have no function. Obviously, this isn't as abbreviated as ++ and --, but as I've been told with my own proposals, it's a lot easier to add a new function to accomplish something than a new syntax or operator, so I'm trying to be constructive here and find a solution that might work to introduce this.

In either case, whether doing ++/--, it would be neat, if not necessary, to make this work with nested functions/variables, like this:

.selector {
  --default-shift: 10px;

  /* ++ example */
  background: linear-gradient(red 20%, blue ++var(--default-shift), green ++calc(var(--default-shift) * 2));

  /* shift() example */
  background: linear-gradient(red 20%, blue shift(var(--default-shift)), green shift(calc(var(--default-shift) * 2)));
}

cc @romainmenke

Afif13 commented 1 year ago

@brandonmcconnell the idea is not using ++ as the operator. As explained by @romainmenke, one plus will the positive sign while the other is the incremental operator so in your last example we would use

background: linear-gradient(red 20%, blue +var(--default-shift), green +calc(var(--default-shift) * 2));

Same logic with -- but I won't focus on this part because incrementing is the only useful operation inside gradient since we always need to have bigger values. Smaller values are not useful that's why my proposal is only about "incremental syntax".

It can also be another character like ~ to avoid confusion but in all the cases, the idea is to have an optional character to indicate if it's a static value or a relative one.

The shift() function is good but .. a bit verbose 😅 I prefer a reduced syntax but if a function is the only way, I would take it.

cdoublev commented 1 year ago

You might be interested in taking inspiration from the relative color syntax, since the idea seems to express relative (to the previous color stop) vs. absolute color stop positions.

For example, linear-gradient(red, orange calc(prevStop + 50%), green).

brandonmcconnell commented 1 year ago

@Afif13 Yeah, either syntax would be good in my opinion— + or shift().

Also, I just did some testing, and it looks like either ++50% or +(50%) could be acceptable since neither are currently valid.

I thought +(50%) might be valid syntax already, but it appears to break the style as far as I can see. I'm not suggesting we strictly stick to one of these but rather than either of these could be acceptable uses.

This is pretty similar to how we can use properties on numbers in JS, for example:

(5).toFixed(2)
5..toFixed(2)

Either of the above are valid, but 5.toFixed(2) is not since a single dot without parentheses could be mistaken for a decimal delimiter.

Similarly in your proposal, a single sign could be valid outside parentheses, or two symbols adjacent without parentheses, which could be validly any combination:

And they each do different things, where the first sign dictates whether to add or remove the amount, and the second is the polarity of the actual number.

romainmenke commented 1 year ago

+(50%) might be a bit less convenient because you can not drop the leading + to toggle between absolute and relative. You also need to remove the parenthesis.


Have looked at this a bit more and there is one case that hasn't been called out in the examples explicitly.

When dealing with mixed units it is unknown which values are larger.

This won't work at all :

.foo {
    background: linear-gradient(red 20px, blue 10vi, green 10vw, yellow 5rem, orange 10ex);
}

To express a progression with mixed units you currently need to wrap every preceding value with max(). After a few stops this escalates dramatically.

This will work :

.foo {
    background: repeating-linear-gradient(red 20px, blue 0 10vi, green 0 ++10vw, yellow 5rem, orange ++10ex);
}

And is equivalent to :

.foo {
    background: repeating-linear-gradient(red 20px, blue 0 10vi, green 0 calc(max(20px, 10vi) + +10vw), yellow 5rem, orange calc(max(calc(max(20px, 10vi) + +10vw), 5rem) + +10ex));
}

This is not an argument in favor of any proposed syntax but it does show that this proposal tries to solve something that is really difficult today.

brandonmcconnell commented 1 year ago

@romainmenke That looks great! Would it be possible to adjust the syntax so that +var() or +calc() assumes its content will be a length, dictated by the leading +? That would make the feature much more flexible.

Also, re the max() you made, I was going to mention it may prevent descending color stops (e.g. 100% ➞ 50% ➞ 0%), but upon testing, that may not be supported anywhere anyway.

romainmenke commented 1 year ago

Would it be possible to adjust the syntax so that +var() or +calc() assumes its content will be a length, dictated by the leading +? That would make the feature much more flexible.

That already works ;) I just didn't bother writing a test for it.

.calc-3 {
    background: repeating-linear-gradient(red 10px blue +calc(5px * 3));

    /* becomes */
    background: repeating-linear-gradient(red 10px blue 25px);
}

.var-1 {
    background: repeating-linear-gradient(red 10px blue +var(--foo));

    /* becomes */
    background: repeating-linear-gradient(red 10px blue calc(10px + var(--foo)));

}
brandonmcconnell commented 1 year ago

@romainmenke Great! Yeah, I only made that comment re your "This won't work with var() because we can not statically determine if var() will resolve to a color or a length." note on https://github.com/csstools/postcss-plugins/pull/895, but I see you clarified "+var() is fine, we know that that must be a length".

I really like this syntax per Temani's proposal and your impl. I'll play with it later. 👏🏼

brandonmcconnell commented 1 year ago

For anyone interested, I ported @romainmenke's plugin to a CodePen instance for quicker tinkering, here:

https://codepen.io/brandonmcconnell/pen/YzOOBVj/66c65611ba8bf3133f1cf87ccfe09c44?editors=0100

Really enjoying this syntax, @Afif13. Great suggestion 💯


& TIL you can't mix % and deg units in conic-gradients, even using pure CSS 🤷🏻‍♂️ e.g. conic-gradient(red, blue +40%, blue 60%, green calc(60% + +10deg))

romainmenke commented 1 year ago

How does this work when color stop positions are omitted?

Does it first fill in the color stops as if the relative value was also missing? And then afterwards combine the interpolated position from the previous color stop with the relative value?

Or does it first apply the relative value as if the previous color stop had a value of zero?


linear-gradient(red, white ++10%, blue);

Is that :


linear-gradient(red, orange, white, pink ++10%, blue);

Is that :


linear-gradient(red, orange ++10%, white, pink, blue);

Is that :

Afif13 commented 1 year ago

@romainmenke

Actually, I never thought about this but now I can see a lot of complex cases to consider 😅

The issue I can see is an example like linear-gradient(red, blue ++50%, pink, green). If we first calculate all the color stops (including the pink one) we can easily find ourselves with the final value of ++50% bigger that the value of pink and we will need another Fixup step which is not good.

I think the suitable solution is to consider a relative color as static if there is no reference for it (no color stop is defined before it). The previous example will become linear-gradient(red, blue 50%, pink, green) then we do the color Fixup to get linear-gradient(red 0%, blue 50%, pink 75%, green 100%)

So like you said "it first apply the relative value as if the previous color stop had a value of zero" sounds good to me.

Afif13 commented 1 year ago

pinging the Spec Editors so they can give us their feedback 🙏

@fantasai @LeaVerou @tabatkins