w3c / csswg-drafts

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

[css-values] Native interpolation function in CSS #581

Closed MadeByMike closed 3 years ago

MadeByMike commented 8 years ago

Note: I originally suggested a name of map rather than interpolate for this function. I thought it might be clearer for some people. I was wrong about this. So I've updated the issue to be clearer and incorporate other comments received.

I think CSS is in need of a native interpolation function. Break-points don't necessarily match the intentions of designers and interpolation will become a more significant feature of web design with the introduction of variable fonts, and an increasing adoption of viewport units and more dynamic layouts.

A interpolation function could be the glue that brings it all together.

In many cases it makes good sense to adjust font properties directly in relation to the viewport width. We can currently do this for some properties with calc(). i.e.

font-size: calc(16px + 8 * ((100vw - 400px) / 400));

The above example will interpolate a font-size between 18px and 24px when the viewport is between 400 and 800px. This has already proven to be very useful but it has limitations. It's linear, not interoperable, it only works with some unit-types, it doesn't work with real numbers or percentages and it's also somewhat difficult to understand.

I'm concerned that many cases axis of variation in variable fonts, won't be able to be transitioned in the same way (because they are unitless values) which for me is one of the main use-cases for variable fonts.

A interpolate() function could be used as component value. Similar to calc() it could be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed. But equally it could work for <color> values and any other values that can be interpolated or animated.

One possible syntax of a interpolate() function is:

interpolate(<initial-value>, <target-value>, <percentage-completion>, [<timing-function>])

The interpolate() function interpolates between the min-value and the max-value, across the specified viewport-width range and according to the easing function. The default timing function should be linear, and accepts CSS transition-timing-function values.

Here are some examples of how a interpolate() could be used with font-weight:

.example {
  font-weight: interpolate(400, 600, 0.5)
}
.example {
  font-weight: interpolate(400, 600, 0.7, ease-in);
}
.example {
  font-weight: interpolate(400, 800, 0.5, cubic-bezier(0.250, 0.250, 0.750, 0.750) );
}

Whilst we could change the percentage completion with media queries, smooth interpolation requires setting a value relative to something else such as the view-port. It was suggested that unit algebra might be a solution for this, but I think the result is at least as complicated and with calc():

--max-viewport: 500px;
--min-viewport: 1000px;
--range: var(--max-viewport) - var(--min-viewport);
--percentage-completion: calc( (100vw - var(--min-viewport)) / var(--range) );
.thing {
  width: interpolate(0px, 500px, var(--percentage-completion), ease-in);
}

I think a percentage could be a better outcome:

--percentage-completion: percentage(500px, 1000px, 100vw);

.thing {
  width: interpolate(0px, 500px, var(--percentage-completion), ease-in);
}

If container queries or new features provide additional unit types percentage function could work with these also.

More detailed thoughts: https://madebymike.com.au/writing/interpolation-without-animation/More

keithjgrant commented 8 years ago

Intriguing idea. The name map might be confusing, as "maps" are a totally different concept in SASS (more like an object or hash map).

MadeByMike commented 8 years ago

@keithjgrant Yes, that's exactly why I added (Not like Sass maps). This type of function is often called map, although not always. In D3 for example it is called a scale but it's described as a function that "maps" an input domain to an output range.

davidkpiano commented 8 years ago

Bikeshedding here, but I'm also against calling this 'map', I feel it should be something like interpolate or modulate, as map can also be confused with the generic programming term map, as in Array.map.

keithjgrant commented 8 years ago

I feel like this is doing two different things. If we break this down into its component parts, it:

  1. Determines where the viewport lies between two bounds
  2. Uses that result to map to value between two other bounds

It would seem more generally useful to me if we could split those two concerns apart. For the second part, what if it just took a number from 0 to 1.0 & did the calculation based on that. Then, assuming a syntax something like map(<min-value>, <max-value>, <scale-value>, [<easing>]), it could help out more generally in all sorts of use cases:

--value = 0.3;
color: map(red, blue, var(--value), ease-in);

...At this point, it's not all that different from a keyframe animation, but the value acts as a way to "scrub" through the animation, or pause it at a specific point. (Is this sort of thing already possible w/ keyframes, in pure CSS, or would it require JS?)

The other piece of the puzzle would be constructing that value from the viewport. Something that asks, “where are we between the bounds of 400px and 800px” and returns a result from 0 to 1.0. As you said, this can be done with calc(), but some sort of shorthand wouldn't hurt.

MadeByMike commented 8 years ago

@keithjgrant “where are we between the bounds of 400px and 800px” and returns a result from 0 to 1.0. As you said, this can be done with calc()

-- The reason I'm proposing this is it can't universally be done with calc() or in a way that can be applied to anything other than a length value.

jpamental commented 8 years ago

Important point here is that font-variation-settings isn't what we should be using in general practice, but rather the goal is to 'clamp' to existing (and in some cases new) higher-level attributes, like font-weight (wght) and font-stretch (wdth) (see description in the spec here: https://drafts.csswg.org/css-fonts-4/#low-level-font-variation-settings-control-the-font-variation-settings-property)

I'm not sure if it makes a difference in this specific situation, but it may be more in line with existing techniques to be trying to solve for specific 'mapping' of width and viewport with a single existing attribute rather than a broader-spectrum low level one like font-variation-settings

keithjgrant commented 8 years ago

@MadeByMike To clarify: I’m not disagreeing with the need/utility this provides. What I mean is it’s tied to one particular use-case: fluid responsive typography. I’m trying to see if there’s a more general use case/solution to tease out that would be beneficial for a broader spectrum of problems.

You did mention other things like color, but making a color respond to viewport width seems... not very useful. It would be more flexible, I think, to break the problem apart: generate a multiplier value based on X (e.g. viewport size between a min/max), and apply a multiplier to Y (e.g. font-size between min/max).

Then again, I’m having a hard time coming up with other use cases anyway, so maybe the more general solution isn’t needed.

MadeByMike commented 8 years ago

@keithjgrant Yes, I can't see why you would want to tie color to the viewport width either. Maybe once just for fun :)

If there is a good way to specify something other than the viewport width, that would be great. However I'm not sure that can be easily done. Not without another function or feature that resolves the "where are we between the bounds of x and y", and I guess it should do this regardless of whether x and y are: 400px and 800px, red and blue, or 1 and 25. I don't know how this might work or if it is a good idea, but I'm happy to explore it.

There would be a correct mathematical term for this but I'm not sure what it is. I'll call it scale and maybe I can update the example later: scale(<value>, <domain-min>, <domain-max>). Returns 0-1.

To use your example:

--value = scale(green, red, blue);
color: map(red, blue, var(--value), ease-in);

or the font-size + vw example:

--value = scale(100vw, 600px, 800px);
font-size: map(1rem, 2rem, var(--value), ease-in);

I'm not sure this is necessarily better than assuming the map() is related to the viewport. In the majority of cases I think it will be, but I'm open to ideas like this.

MadeByMike commented 8 years ago

@jpamental Thank you, I updated the example to reflect your comment.

jpamental commented 8 years ago

Thanks @MadeByMike ! They'll likely be somewhat interchangeable, but I only learned recently that the reason I've had to use font-feature-settings for so long is that not all the browsers have implemented the font-variant-* settings, and didn't want us to end up in the same pickle here of using a hammer instead of 'just the right tool' (and focus on pushing for proper implementation too)

liamquin commented 8 years ago

Sounds like you really want easing-function(minvalue, maxvalue, actualvalue [, curve-name]) returning a number between 0 and 1.

It's hard for me to see wanting fonts to get bolder based on the device or window size, but font-optical-sizing being a continuous function of the viewport size would make a lot of sense.

MadeByMike commented 8 years ago

So like this?

--value = easing(green, red, blue, ease-in);
color: map(red, blue, var(--value));

And:

--value = easing(100vw, 600px, 800px, ease-in);
font-size: map(1rem, 2rem, var(--value));

That is also a valid option.

tabatkins commented 8 years ago

I agree with others - the primitive you want here is a naked interpolate() function. We have this, specialized for images, in cross-fade() - it's reasonable to want something that'll interpolate anything interpolable.

The viewport-responding stuff you're talking about is, I presume, the concept referred to as "CSS Locks"? It's pretty cool (especially with Variable Fonts on the horizon), and I agree that it's way more difficult/limited than it needs to be right now. I think the proper solution for this is something we already have plans to do - being able to do unit algebra. That way, you can express the fraction you want in terms of viewport sizes. For example, your first example (interpolate between 18px and 24px as the viewport goes from 400px to 800px wide) could be done as:

calc(18px + 6px * ((100vw - 400px) / 400px) )

That (100vw - 400px) / 400px part resolves to a number between 0 and 1 when 100vw is between 400px and 800px. Combined with a min() and max() function (or, for some properties, the min-* and max-* properties), it can ensure that the value doesn't go outside that range either. (Or probably more readably, a clamp() function that does both limits at once.)

The calc() expression above actually does the full linear interpolation for you, because lengths are calc-able. It's possible more readable to use the interpolate() function direclty, tho; for other values that aren't directly calc-able, you need interpolate() to make it work at all:

interpolate(18px, 24px, calc((100vw - 400px) / 400px));
interpolate(red, blue, calc((100vw - 400px) / 400px));

While this isn't quite as direct as your original proposal, it has exactly the same power (particularly if interpolate() takes an easing argument), and is based on two very reasonable primitives rather than a single special-case function. If this type of interpolation becomes popular afterwards, we can look at making a built-in function for it.

MadeByMike commented 8 years ago

@tabatkins I thought that because of how calc works calc((100vw - 400px) / 400px) resolves to a length value, so we'd be passing a length value to the interpolate() function not a whole number?

Also using animation-timing functions would be super nice. It opens a world of non-linear options.

tabatkins commented 8 years ago

No, right now calc((100vw - 400px) / 400px) is simply invalid - you can't divide by a unitted value. But allowing unit algebra has been on our roadmap for a while; when we finally add it, that expression will divide a length by a length, resulting in a number.

katerlouis commented 8 years ago

Sorry if this may be trivial; but where in this formula is the 800px limitation? Wouldn't you need a media query surrounding that calc-function to ensure that it doesn't get applied to vws greater than 800?

The way I see it the formula alone won't handle it: ((1200px - 400px) / 400) = 2 .. 18 + 6*2 = 30, not the desired max of 24.

Please, enlighten me :)

MadeByMike commented 8 years ago

@katerlouis In this example the formula is simplified. Expanded it would be: calc( (100vw - 400px) / (800px - 400px ) ) If you are interested how it works I wrote about it in detail here: https://madebymike.com.au/writing/precise-control-responsive-typography/

@tabatkins ^^ I like what you are suggesting, what happens with the case of a viewport with of 400px, i.e. (400px - 400px) / 400 this would result in a divide by 0 error? Would we also need to clamp() the 100vw?

:| If so we're getting back to a point where this is probably not simplifying things for authors as much as we might like.

LeaVerou commented 8 years ago

@tabatkins ^^ I like what you are suggesting, what happens with the case of a viewport with of 400px, i.e. (400px - 400px) / 400 this would result in a divide by 0 error? Would we also need to clamp() the 100vw?

Your example is dividing 0 with 400, i.e. not a divide by 0 error. It just returns 0. If it was dividing by 0, it would render the declaration invalid at computed value time, i.e. the same as if initial was specified. Perhaps we need a fallback mechanism (like in var()) if this is common.

tabatkins commented 8 years ago

Nah, fallback in divide-by-zero doesn't work well; it means we're dealing with an open range, which CSS correctly avoids in designing things. Switching to a fallback means that whatever the behavior was as it got really close to zero, it'll suddenly ignore and switch to your fallback, which isn't guaranteed to do the same thing.

Handling divide-by-zero properly means letting calc() handle infinities (via standard float rules), and defining what happens when they escape the calc() (probably just "largest value the implementation can support for that property").

MadeByMike commented 7 years ago

I tried to emulate this in Sass: https://www.sassmeister.com/gist/beac01f68da4f9ef3007c0d17f72d8c6 Obviously it still only works with calc() and therefore length values. But I think it demonstrates the idea and potential syntax fairly well.

This could also be related to a recent issue to add easing function to colour stops https://github.com/w3c/csswg-drafts/issues/1332 it has some similar ideas about exposing the easing function in a different context to animation.

geelen commented 7 years ago

Coming super late to this discussion I know but I really like Tab's variation on the original proposal. I think you can do pretty much anything you need with it, correct?

interpolate(18px, 24px, calc((100vw - 400px) / 400px), ease-in-out);
interpolate(red, blue, var(--slider-position));

The form would be interpolate(from, to, fraction, easing) and fraction would be automatically clamped between 0 and 1. You'd need unit algebra to use calc in the first example but I'd be suggesting using style.setProperty('--slider-position', mouseX / elWidth) or something in JS for the second.

Manuel87 commented 7 years ago

I just got directed here and thought it might be helpful to share two functions I did in SASS a while ago. (which do translate to plain css calc syntax)

@function interpol($min_window, $min_value, $max_window, $max_value)

@mixin interpolm($prop, $min_window, $min_value, $min_limit, $max_window, $max_value, $max_limit ) 

the actual example with the whole code: https://codepen.io/Manuel87/pen/GqzXdV

PS: @geelen .. the slider stuff looks interesting! :)

tabatkins commented 6 years ago

In https://github.com/w3c/csswg-drafts/issues/2854 we resolved to add this, as it's currently impossible to correctly represent the intermediate value of some 'transform' interpolations.

Closing this, then, as #2854 has the actual WG resolution and this is otherwise a dupe.

fantasai commented 6 years ago

I think that one is the one that should be duped to this, it's a different topic originally, it's got way less discussion and information, and isn't even filed against css-values.

fantasai commented 3 years ago

@mirisuzanne and I propose the following syntax for this “generic interpolation function” that the CSSWG decided to add but didn't decide on syntax for:

  mix( [ <percentage> && [ by <easing-function> ]? ] ; <start-value> ; <end-value>)

<percentage> represents the percent of progress between <start-value> and <end-value>, and <easing-function> is used to calculate the mix ratio represented by that amount of progress. The default easing function is linear.

  opacity: mix( 70% by ease ; 0% ; 100% )

Note: As @tabatkins noted at one point, a generic interpolation function for CSS needs to use semicolons instead of commas, because commas can be part of the value space of various properties. (Since one key use case for the generic interpolation function is used to represent intermediate states in the OM, it has to be able to represent the interpolation of any two values on any two properties. See #2854.)

Note: This is part of rethinking various features for animation timelines and interpolation and how to fit them all together. See particularly our proposal for extensions to mix() for query-linked interpolation in #6245.

LeaVerou commented 3 years ago

I generally love it. It's short, simple, and supports easing functions. Great. My only objection is semicolons. Introducing semicolons in CSS is going to break a lot of highlighters, naive parsers etc, even scanning of single-line CSS by humans and we should try to avoid it. What about introducing an entirely new character? Or using parentheses around values that may include commas?

Loirooriol commented 3 years ago

The semicolon has also been proposed in #5055, so if we use something else it would be good to cover other cases too. I think it should also work in var(), because var(--foo, bar, baz) seems to have 3 independent arguments, var(--foo; bar, baz) makes it clearer that bar, baz is a single thing.

tabatkins commented 3 years ago

It might have been worthwhile to use the semicolon in var() originally; I didn't because it wasn't strictly necessary, and I don't think it's useful to introduce it now since var() has been stable and well-supported for years.

But yeah, there's a very limited set of characters that are suitable for separating values, and we're already using several of them in CSS. Whatever we choose is then locked out from ever being used in CSS as a top-level token in a property value. Semicolon is one of those, and it's already guaranteed to never be used as a top-level CSS value, so it's 100% future-safe.

Crissov commented 3 years ago

To avoid the semicolon, how about at-keywords as in opacity: mix( 70% by ease @from 0% @to 100% )?

tabatkins commented 3 years ago

That should work from a parsing standpoint, but it feels a lot worse than semicolons, to me.

fantasai commented 3 years ago

CSSWG RESOLVED to accept proposal in https://github.com/w3c/csswg-drafts/issues/581#issuecomment-926353789 at today's vF2F.

fantasai commented 3 years ago

This is now published in CSS Values and Units Level 4: https://www.w3.org/TR/css-values-4/#mix

I've opened two follow-up issues:

Closing this one out, please open a new issue for refining details or anything not covered here!