w3c / csswg-drafts

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

[css-values] Proposal for a 'progress' function to calculate progress between two <length> values #7268

Open johannesodland opened 2 years ago

johannesodland commented 2 years ago

The proposal is to add a new function to css-values-4 that calculates the progress between two <length> values in percent. The new function could be used together with the new mix function to improve readability of fluid sizes.

The syntax of the progress functions could be as follows:

progress(<input-length> ','  <min-length> ',' <max-length>)

All arguments are <length> values, while the return value would be a <percentage> value representing the progress of the input between the min and max values. Progress would be clamped between 0% and 100%.

The function could then be used together with mix() to calculate fluid sizes:

--progress: progress(100vw, 375px, 1920px);
font-size: mix(var(--progress), 24px, 32px);

This would work well with container query units as well

--progress: progress(100qw, 200px, 800px);
font-size: mix(--progress, 18px, 22px);

The proposed function is not new functionality, but would be syntactic sugar for

clamp(0%, 100% * (<input-length> - <min-length>) / (<max-length> - <min-length>), 100%)

Name to be bikeshedded.

Motivation

Fluid typography has been around for years, but the css necessary to achieve it is hard to create and read.

The clamp() function makes it possible to create fluid typography with less bloat: Example copied from article above:

font-size: clamp(2.25rem, 2vw + 1.5rem, 3.25rem);

It is still complicated to calculate the slope and intercept values necessary to create a fluid type that scales from a minimum to a maximum viewport size. It is not possible understand the min and max viewport sizes from reading the notation.

Preprocessors or other tools can be used to calculate the values, but this does not improve the readability.

The new mix funcion could let us interpolate between two font-sizes, as long as we have a progress argument:

--progress: progress(100vw, 375px, 1920px);
font-size: mix(var(--progress), 24px, 32px);
Loirooriol commented 2 years ago

mix() doesn't clamp (#6701), so this probably shouldn't either.

And could also work with calculation values other than lengths.

johannesodland commented 2 years ago

I guess this issue would be solved if we could have custom mathematical functions

tabatkins commented 2 years ago

Having the ability to reproduce it by hand doesn't necessarily preclude us adding it in natively, if it's sufficiently useful and the manual reproduction is sufficiently annoying. I think this qualifies.

LeaVerou commented 2 years ago

I agree this should be easier.

Some thoughts that might help with naming/syntax:

Both mix() and the proposed progress() are basically linear range mapping operations. To take the example above:

--progress: progress(100vw, 375px, 1920px);
font-size: mix(var(--progress), 24px, 32px);

progress(100vw, 375px, 1920px) maps the [375px, 1920px] range to the [0, 1] range, and mix(var(--progress), 24px, 32px) maps the [0, 1] range to the [24px, 32px] range.

If we have ad hoc functions like that, I think it should somehow be obvious from naming that one is the inverse of the other: mix() interpolates (takes a percentage and gets you the corresponding value), and progress() does the opposite (takes a value and gets you the corresponding percentage). While I like progress() as a name, with the proposed naming they seem like entirely separate things. Perhaps mix-reverse() or mix-inv() or something? (tempted to suggest xim() and duck 🤣)

But what if we exposed the actual range mapping operation? We could have a map() function that takes two ranges and a value and maps the value from one range to the other. This would basically give us the result above in one fell swoop, without the intermediate percentage:

font-size: map(100vw in 375px to 1920px into 24px to 32px);

or

font-size: map(100vw in [375px, 1920px] to [24px, 32px]);

or something (syntax TBB). Perhaps we could even have shortcuts to facilitate the common case where you want percentages as input or output.

Note that these could also be stored in variables to be reused for every conversion:

:root {
    --page-widths: [375px, 1920px];
}
/* ... */
font-size: map(100vw in var(--page-widths) to [24px, 32px]);

or even:

:root {
    --page-width-progress: 100vw in [375px, 1920px];
}
/* ... */
font-size: map(var(--page-width-progress) to [24px, 32px]);
johannesodland commented 2 years ago

While I like progress() as a name, with the proposed naming they seem like entirely separate things. Perhaps mix-reverse() or mix-inv() or something?

I agree that the name should reflect that this function is the inverse of mix(), like asin() is the inverse of sin(). mix-inv() or inverse-mix()or something similar would be great 👍

(tempted to suggest xim() and duck 🤣)

🤣

But what if we exposed the actual range mapping operation? We could have a map() function that takes two ranges and a value and maps the value from one range to the other. This would basically give us the result above in one fell swoop, without the intermediate percentage:

...

font-size: map(100vw in [375px, 1920px] to [24px, 32px]);

A range mapping function would be great!

At the moment mix() does linear interpolation, and map() would do linear range mapping. But; there's an open issue on adding easing functions to calc (https://github.com/w3c/csswg-drafts/issues/6697).

Easing could let us do something like

--progress: inverse-mix(100vw, 375px, 1920px);
font-size: mix(var(--progress) ease-in-out, 1rem, 1.25rem)

It would be great to be able to use easing functions in map as well, so the syntax should have room for extending in the future:

map(100vw in [375px, 1920px] to [24px, 32px] ease-in-out)

Also, while a map() function would be great, separate mix() and inverse-mix() functions would be composable with other functions.

johannesodland commented 2 years ago
--progress: inverse-mix(100vw, 375px, 1920px);
font-size: mix(var(--progress) ease-in-out, 1rem, 1.25rem)

I seem to have provided an example where inverse-mix would not necessarily be the inverse of mix() 🤦😅

Loirooriol commented 2 years ago

Just some notes:

tabatkins commented 2 years ago

Yes, just determining the interpolation progress within a range, in a linear fashion, seems sufficient, and avoids all the complexities and incongruities of trying to invert an easing function, which as you note is not in general an invertable function!

dbaron commented 2 years ago

It might be useful to provide a way to invert the easily-invertible easing functions that we have. I believe the inverse of a cubic-bezier(x1, y1, x2, y2), where 0≤y1≤1 and 0≤y2≤1, is cubic-bezier(y1, x1, y2, x2).

Loirooriol commented 2 years ago

Note that cubic-bezier(1,0,0,1) in only invertible in [0,1]. I think y1 > 0 and y2 < 1 are also needed for the general case.

johannesodland commented 1 year ago

There are discussions in https://github.com/w3c/csswg-drafts/issues/6245 for alternative ways of interpolating values between breakpoints. I still think a ‘progress’ or ‘position-of’ function would be useful for individual properties and more advanced calculations.

johannesodland commented 1 year ago

Seems like progress() is in the css-values-5 ED now: https://drafts.csswg.org/css-values-5/#progress-func

yisibl commented 10 months ago

In range input, progress has different calculation modes. In the following example, the right edge of progress is always in the middle of the thumb.

This prevents the rounded corner radius from changing and overflowing when dragged to the far left.

https://github.com/w3c/csswg-drafts/assets/2784308/f366da0f-346a-49df-a9de-57a32759c7df

With clamp(), you can see that the width of progress is not growing evenly, and I want the tooltips above to always be centered horizontally with the thumb.

.slider-progress {
  --p1: calc((var(--range) - var(--range-min)) / (var(--range-max) - var(--range-min)));
  --w1: clamp(28px, calc(100cqi * var(--p1)), calc(100cqi - 28px));
}

https://github.com/w3c/csswg-drafts/assets/2784308/5b865be7-e6ad-420c-9cbf-d87ea8e0f2a5

My question is, how can I implement such an algorithm using clamp() or progress()? I think this is the scenario that progress() should consider.

johannesodland commented 4 months ago

@fantasai @mirisuzanne Could this issue be closed now that progress() is already specced in css-values-5?

mirisuzanne commented 4 months ago

While it's in the Editor's Draft, I don't see any official resolution from the WG. We would need to get that approval before we publish a new Working Draft (and close the issue). Seems the last time we brought it up (in #6245) was just to introduce the proposal, without asking for a resolution. If we think we're ready for a resolution, we could bring one of these issues back to the group.

@andruud or @danielsakhapov - I think you were exploring a prototype at one point, and had some questions that we might want to address? Are those documented as issues anywhere?

danielsakhapov commented 4 months ago

@mirisuzanne I don't think we've found anything bad, apart from the fact that browsers will have a lot of work to implement it for non-length accepting properties, since none of them (my estimation) are ready to deal with cases like - number-producing math function with relative units dependency (I guess you can see it now with sign() function here). But that's not a spec problem, just a delay for ship. I've already started fixing it for Chrome.

andruud commented 4 months ago

@mirisuzanne I think we should probably change this part:

The computed value of a value specified with <'animation-timeline'> is the computed <'animation-timeline'> and <easing-function> (if any).

and have it compute to the actual (numeric) progress of the timeline instead (post-easing). We generally resolve things at the earliest point they can be resolved, and I'm not sure what we gain from delaying the computation here except increased implementation difficulty.