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

Open
MadeByMike opened this Issue Oct 8, 2016 · 19 comments

Projects

None yet

8 participants

@MadeByMike
MadeByMike commented Oct 8, 2016 edited

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
Contributor

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
MadeByMike commented Oct 10, 2016 edited

@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

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
Contributor
keithjgrant commented Oct 10, 2016 edited

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
MadeByMike commented Oct 10, 2016 edited

@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

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
Contributor

@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

@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
MadeByMike commented Oct 10, 2016 edited

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

@jpamental

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

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

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
Member

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.

@tabatkins tabatkins self-assigned this Oct 10, 2016
@MadeByMike
MadeByMike commented Oct 10, 2016 edited

@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
Member

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

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
MadeByMike commented Oct 11, 2016 edited

@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
Contributor
LeaVerou commented Oct 14, 2016 edited

@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
Member

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 MadeByMike changed the title from [css-values] [css-fonts-4] Native map() function in CSS to [css-values] [css-fonts-4] Native interpolation function in CSS Dec 8, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment