w3c / csswg-drafts

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

Interpolate values between breakpoints #6245

Open scottkellum opened 3 years ago

scottkellum commented 3 years ago

Allow interpolation between both viewport and element breakpoints.

The problems with clamp(), min(), and max() is that you can only interpolate length values on a single property between two points. You may want to interpolate rulesets across multiple breakpoints. You may also want to interpolate things like variable font settings, color, etc. Additionally it would be nice to be able to ease how breakpoints are interpolated as rates at which things scale across different screen or element sizes can often be variable.

Update Dec. 7, 2022

I’ve created an explainer on this issue with a more detailed proposal: https://css.typetura.com/ruleset-interpolation/explainer/

This includes a more specific and detailed proposal as well as a companion proposal to expand scroll timeline. Expanding scroll timeline is likely the easiest solution at the moment but it would make it more user friendly to allow length values as keyframes. This can always be added at a later date though.

mirisuzanne commented 3 years ago

Yeah, this would be real useful for something like "intrinsic typography" - but I can imagine other use-cases as well. It's interesting to think about media/container "breakpoints" as keyframes in an animation, which we can then interpolate (with easing).

My immediate association is scroll-timeline. I wonder if there would be a way to adapt something similar to get, basically, a container-timeline.

fantasai commented 3 years ago

@mirisuzanne and I put together this proposal for defining and using query-linked timelines as part of rethinking various features for animation timelines and interpolation and how to fit them all together.

Query-linked Timelines

Query-linked interpolation uses a set of keyframes (minimally, two) to interpolate values along an easing curve based on the value of a query (such as a media query or container query). The timeline is therefore defined by the value of the query, and can be referenced by an interpolation function in individual property declarations.

Defining the Query Timeline

The @timeline rule defines a named timeline. It can be expanded later to define other types of timelines, but here we're defining only two types: media query timelines and container query timelines.

@timeline NAME {
  type: media | container;
  feature: <media-feature-name> | <container-feature-name>;
  from: <value>; /* 0% of the timeline */
  to: <value>; /* 100% of the timeline */
  container: <'container'>; /* only applies to container query timelines,
                               same seeking function as container queries */
}

A typical example might look like:

@timeline font-size-timeline {
  type: media;
  feature: width;
  from: 20em;
  to: 60em;
}

While query-linked timelines can be referenced in animation-timeline, it's not recommended to use this method in most cases because it would cause cascading problem: anyone using query-based interpolation via animation properties would override all affected properties at levels of the cascade.

They can, however, be referenced by an interpolation function within the affected property declarations, which allows the interpolated value to cascade the same as any other declared value.

Value Interpolation

Value interpolation uses a percentage value to indicate how close or far from the start/end points to calculate the interpolated value. Interpolation is interpreted through an easing curve, and the input percentage can be selected based on the current position on a timeline such as a query timeline.

Timeline-based Value Interpolation

This extends the generic interpolation function adopted (but as yet unnamed ;) in https://github.com/w3c/csswg-drafts/issues/581

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

By naming a timeline instead of giving a percentage directly (see percentage mixes in https://github.com/w3c/csswg-drafts/issues/581#issuecomment-926353789), the author can use progress along a timeline as the progress percentage. Any value valid for animation-timeline or any timeline name defined via @timeline is valid, which allows the mix() to respond to query-linked timelines and scroll-linked timelines.

Value Interpolation with Keyframes

For more complex interpolation curves, the <start-value> and <end-value> can be replaced by a reference to a named set of keyframes.

  mix( [ <timeline> && [ by <easing-function> ]? && of <animation-name> ] )

Note: Using keyword markers (as in gradients) allows the arguments to be reordered, so that authors don't have to memorize positions of arguments.

bramus commented 3 years ago

A typical example might look like:

@timeline font-size-timeline {
  type: media;
  feature: width;
  from: 20em;
  to: 60em;
}

As an addendum: for CSS @scroll-timeline — as specced in the Scroll-Linked Animations spec — the authors explicitly moved away from using start (here named from) and end (there named to) as descriptors.

They replaced it with one descriptor named scroll-offsets (which here could be named offsets), that accepts an array of values.

As per spec:

Scroll timeline offsets determine the effective scroll offsets in the direction specified by orientation that constitute the equally-distanced in progress intervals in which the timeline is active.

That way they allow more than two offset to be used.

Relevant Issue: https://github.com/w3c/csswg-drafts/issues/4912, specifically this comment.

mirisuzanne commented 3 years ago

@bramus If I understand right, we're thinking about timelines a bit differently here. They seem to be establishing the number and placement of keyframes in the timeline description, and that seems to me like the wrong location for that information.

In our proposal, the timeline doesn't establish the available keyframes, just the distance that we travel from 0% complete to 100% complete. Then authors can attach that to an animation or interpolation function with as many keyframes as they need. The individual offset concerns of each animation ("reveal, "unreveal", etc) are handled in keyframes, rather than in the timeline itself. So you have a single timeline (x to y), and then the ability to offset keyframes within that timeline. "Reveal" might happen between 20%-40% of the timeline, and "unreveal" happens from 60%-80%, each one using as many keyframes as it wants.

The number and placement of keyframes is controlled by the animation/interpolation rather than by the timeline.

But maybe I'm misunderstanding something there?

bramus commented 3 years ago

@mirisuzanne You understand correctly there. Simply wanted to point out that “this move in the other direction” was made before, and that it perhaps could be relevant to take into account ;)

css-meeting-bot commented 3 years ago

The CSS Working Group just discussed interpolating values between breakpoints, and agreed to the following:

The full IRC log of that discussion <TabAtkins> Topic: interpolating values between breakpoints
<TabAtkins> github: https://github.com/w3c/csswg-drafts/issues/6245
<fantasai> https://github.com/w3c/csswg-drafts/issues/6245#issuecomment-926351855
<TabAtkins> miriam: This is building on that same idea, but creating timelines out of MQ/CQs
<TabAtkins> miriam: In this caes you're more often doing interpolated values based on the timeline, not animations specifically
<flackr> q+
<TabAtkins> miriam: We want to be able to create timelines off the size of the container
<TabAtkins> miriam: So for defining the query timeline, we have an @timeline syntax.
<smfr> q+
<TabAtkins> miriam: Give it a name, say what we're querying, what feature we're querying
<TabAtkins> miriam: And give it a from/to value to offset that range
<TabAtkins> miriam: So interp between a container being 100px and 40em to define the timeline
<TabAtkins> miriam: If it's a CQ we give the name of the container
<TabAtkins> miriam: If there are multiple CQs with that name this'll apply to all of them
<TabAtkins> miriam: Kids will look at their appropriate ancestor container
<TabAtkins> miriam: And we can use the timeline name in an animation-timeline
<TabAtkins> miriam: But more often we'll want a value that interps in the cascade instead, so we can override it if we need to
<TabAtkins> miriam: A generic interpolate() function has been discussed for a long time
<TabAtkins> miriam: We called in mix() here, named TBD
<TabAtkins> miriam: Idea is it could be generic, taking a %, or take a timeline which resolves to a %. Could invoke scroll timelines too, etc.
<TabAtkins> miriam: And then it takes an easing function and two values to interp between
<TabAtkins> miriam: In some cases this'll get more complex with multiple values, maybe get quite long
<TabAtkins> miriam: Wondered if mix() could ref keyframes
<TabAtkins> miriam: So you could pull out the value details into keyframes for more detailed control
<TabAtkins> fantasai: I wanted to point out the cascade effects
<chris> q+ to wonder about once more doing piecewise functions with no continuity
<TabAtkins> fantasai: We considered putting query-based timelines as a value of aniamtion-timeline
<TabAtkins> fantasai: That ends up applyin all the props at once, and at an overriding level of the cascade
<TabAtkins> fantasai: Usually you don't want that, you just want to specify a value at a normal cascade spot, but *based on* a timeline
<TabAtkins> fantasai: So my font-size timeline can just spec an interpolated normal font size, and then have an overriding rule setting the font-size to a specific value as normal.
<Rossen_> ack fantasai
<Zakim> fantasai, you wanted to react to flackr to point out cascade effects
<Rossen_> ack flackr
<fantasai> s/as normal/as usual in the cascade/
<TabAtkins> flackr: I think what fantasai just said might change my q...
<TabAtkins> flackr: So this isn't an animation timeline, it only exists for the mix() function?
<TabAtkins> fantasai: We were debating that.
<TabAtkins> fantasai: We definitely want it for mix(). Whether it's available for animation-timeline is an open question
<TabAtkins> fantasai: We've asked brian for feedback and he pointed out there were a lot of complexities, so we might not want to do it
<TabAtkins> fantasai: Not the most important; mix() is the primary case
<TabAtkins> flackr: Yeah was gonna raise the same complexities; if it's animation, we have to have the animation progress update in the middle of the cascade.
<TabAtkins> flackr: Anders said it would be a huge technical burden to have anims update as part of the cascade due to the cascade
<TabAtkins> smfr: This feels like calc() to me
<Rossen_> ack smfr
<TabAtkins> smfr: We could have one that interps with easing funcs
<TabAtkins> smfr: Missing piece is input from media features, could come in as env()
<TabAtkins> smfr: And so with a calc easing function thing
<fantasai> TabAtkins: not quite, implies only doing calc()-able things
<fantasai> TabAtkins: not all things that can be interpolated
<fantasai> TabAtkins: which includes colors, etc.
<fantasai> smfr: Can we make calc() accept these things?
<fantasai> TabAtkins: I don't want to but we can talk about it?
<Rossen_> ack chris
<Zakim> chris, you wanted to wonder about once more doing piecewise functions with no continuity
<TabAtkins> chris: So this is unpopular
<TabAtkins> chris: We start by lerping two values
<TabAtkins> chris: Then we add more values and lerp them
<TabAtkins> chris: And if you draw that it's jaggy on a graph because slopes are different
<TabAtkins> chris: And then we add easings, and you can maybe fake it to look continuous
<TabAtkins> chris: But we never get to a thing that smoothly interpolates thru N values
<TabAtkins> chris: Is that something we want to do or just continue keeping it pairwise?
<TabAtkins> flackr: Is this not having easing on the mix function?
<TabAtkins> chris: That requires the author to figure out C1 continuity on their own
<TabAtkins> fantasai: This seems compatible with what keyframes do right now, we could default to smooth interp
<TabAtkins> TabAtkins: So chris's request is for the abilty to spec an animation with N values and have it automatically smoothly interp, rather than only having pairwise interp control that needs manual adjustment
<TabAtkins> chris: yes
<TabAtkins> TabAtkins: c1 continuity, to be specific
<fantasai> fantasai: We specced multi-stop animations using @keyframes, see last section of proposal
<Rossen_> q?
<fantasai> TabAtkins: I suspect that's something we can handle at a higher level
<fantasai> TabAtkins: we have a default for pairwise interpolation, default to ?
<fantasai> TabAtkins: could do smarter things in animations
<fantasai> TabAtkins: fits within existing syntax structure of animations
<fantasai> flackr: It will be challenging, though
<fantasai> flackr: easing function per keyframe is just between those endpoints
<fantasai> flackr: easin function on animation is just input time to output time
<fantasai> TabAtkins: animation-easing-function is the default between frames
<fantasai> flackr: that's correct for CSS. Web Animations also adds an easing curve to the timeline
<fantasai> TabAtkins: you're easing time into massaged version, that's separate from this
<Rossen_> ack fantasai
<TabAtkins> fantasai: i think we could easily have a "tweak the time"-based version, we could add that into the rule as well
<fantasai> s/rule/@timeline rule/
<fantasai> fantasai: Intention of mix() argument was the default easing between frames
<fantasai> fantasai: If we want to default to doing continuous magic, or adding a keyword to opt into it, that's fine
<TabAtkins> flackr: Yeah it would be like combining adjacent pairs that have the same value into one continuous timing function
<TabAtkins> fantasai: I want to point out we dont' ahve a resolution on the form of the generic interp function
<TabAtkins> fantasai: So our proposal is to have it accept %s and two values
<fantasai> https://github.com/w3c/csswg-drafts/issues/581#issuecomment-926353789
<TabAtkins> fantasai: So this would be a function that replaces the % with a timeline that computes to a %
<TabAtkins> fantasai: We have a resolution to *add* a mix() function but didn't settle on the syntax
<TabAtkins> Rossen_: So what can we resolve on?
<fantasai> s/this would be/this proposal is/
<TabAtkins> fantasai: resolution the first: generic interpolate function is called mix(). Takes %, then start value, then end value. Values are separated with semicolons to avoid ambiguity with comma-containing values
<TabAtkins> (you can interp a comma-separated list, for example)
<TabAtkins> TabAtkins: Simon had some thoughts about this in calc(), do you want to continue talkinga bout this?
<TabAtkins> smfr: I'm not quite sold on @timeline yet, but I don't want to stall this
<fantasai> https://github.com/w3c/csswg-drafts/issues/581
<TabAtkins> fantasai: Right now it's just mix()
<TabAtkins> smfr: Would this be like a calc()?
<TabAtkins> fantasai: Like, but wider.
<fantasai> fantasai: It has to be able to interpolate every possible computed value in the entire space of CSS
<TabAtkins> smfr: It requires UAs to have a parallel version of calc trees, for every possible value
<TabAtkins> fantasai: You kinda already have that since everything can interp
<TabAtkins> fantasai: Like, how do you interp between currentcolor and blue? No way to represent that right now. (color-mix() is coming, but this is a wider issue)
<TabAtkins> fantasai: So we have lots of places where we want to interp things that don't have intermediate values
<TabAtkins> smfr: That makes sense, we also invented cross-fade() to hit the image case
<TabAtkins> smfr: I'd like to hear from other impls about their thoughts on impl complexity, and whether it makes sense to think of it in terms of calc()
<fantasai> TabAtkins: I don't have problem of thinking about it in terms of calc(), can re-use machinery there
<fantasai> TabAtkins: but I think that's an internal detail
<TabAtkins> fantasai: Note that we *resolved* to add the function years ago but didn't resolve on the syntax
<fantasai> see also https://github.com/w3c/csswg-drafts/issues/2854
<TabAtkins> RESOLVED: Accept mix() function into Values 5
<Rossen_> s/Accept mix() function into Values 5/Accept mix() function into Values 4/
<TabAtkins> fantasai: So next is do we want mix() to accept a timeline+easing function instead of a %
<TabAtkins> fantasai: If no, I don't need to go into details. If yes, we'd use the @timeline rule discussed previously.
<TabAtkins> TabAtkins: This just got proposed last week, it's a little big. I'd like more time to review on it.
<TabAtkins> fantasai: And this would def go into level 5
<fantasai> ScribeNick: fantasai
scottkellum commented 1 year ago

I have updated this issue with a more detailed explainer containing a proposal. Here is the explainer.

danielsakhapov commented 1 year ago

Hello, everyone!

Since we now have scroll- and view-timelines, maybe it makes sense to do something similar for this problem? Like, container-timelines? They will allow to drive a regular CSS/Web animation using the size of a query container’s content-box.

A container timeline is created similarly to how a scroll/view-timeline is created:

#container {
  container: mycontainer inline-size;
  container-timeline: mytimeline inline-size;
}

And then the animation is set up as you would set up a scroll/view-timeline:

#target {
  animation: anim auto;
  animation-timeline: mytimeline;
}

And keyframe offsets will accept \<length> values. The following defines an animation that takes place between 40em and 800px:

@keyframes anim {
  40em {
    color: green;
  }
  800px {
    color: red;
  }
}

So, overall:

container-timeline-name: #<dashed-ident>
container-timeline-axis: #[ block | inline | x | y ]
container-timeline-range: #[ <length> <length> ]
container-timeline: #[ <container-timeline-name> [ <container-timeline-axis>? || <container-timeline-range> ] ]

With container-timeline-range being min and max lengths for the progress of the animation.

bramus commented 1 year ago

I’m very much sold on the idea.

Two remarks though:

  1. For ScrollTimeline/ViewTimeline the range is not part of the Timeline; there are no view-timeline-range or scroll-timeline-range properties. Instead, the range part of the animation, using the animation-range property. That way one can re-use one timeline instance for multiple animations with different ranges.

    I would suggest to do the same for container timelines.

  2. ScrollTimeline and ViewTimeline also have anonymous timelines, via the scroll() and view() functional notations. Ideally there should also be an anonymous container timeline.

    Strawperson proposal is to name it container(), with this syntax:

    <container()> = container( <axis>? )
    <axis> = block | inline | x | y

    An anonymous container timeline would always look up the nearest container in the ancestor tree. The default value for <axis> is block.

andruud commented 1 year ago

For ScrollTimeline/ViewTimeline the range is not part of the Timeline;

It very much is, it's just that it's set automatically from min/max scroll (etc) for those. The animation attachment range is not the same thing as the timeline range itself. Notice how animation-range-start/end:normal refers to the start/end of the timeline. Without container-timeline-range, we'd need another way of understanding the start/end.

bramus commented 1 year ago

Ah yes, I see now why one would need to define the container-timeline-range, because unlike scroll-timeline and view-timeline it cannot be automatically determined for containers. Thanks, @andruud.

danielsakhapov commented 1 year ago

Sorry, I forgot about the functional notation. Container timeline can be created directly with a functional notation. The following creates a timeline from the nearest inline-size container:

#target {
  animation: anim auto;
  animation-timeline: container(inline-size);
}
andruud commented 1 year ago

Agenda+ to see if we can add container-timeline-* to css-contain. (cc @mirisuzanne @fantasai)

bramus commented 1 year ago

Ah yes, I see now why one would need to define the container-timeline-range, because unlike scroll-timeline and view-timeline it cannot be automatically determined for containers. Thanks, @andruud.

Or maybe a Container Timeline always span from 0 to (theoretically) +Infinity and there is no normal that it can resolve? In that case, authors must always set the animation-range in order to use Container Timelines.

scottkellum commented 1 year ago

+1 @bramus. I agree there is no normal that can resolve and authors must always set the animation-range.

To unpack my thinking here: I almost always set values from 0 to anywhere between 600px and 1600px using my JS version of this approach. All default states I can think of (0 or the current container size) would result in the animation at the end of its play state. This would be confusing as an author who probably doesn’t know why the animation styles are registering, but are always stuck at the end of the animation-timeline. It’s more clear to not even register the animation until the animation-range is set.

andruud commented 1 year ago

Or maybe a Container Timeline always span from 0 to (theoretically) +Infinity and there is no normal that it can resolve?

That sounds possible.

But does that mean that a container-timeline-linked animation with animation-range:normal is just effectively frozen?

bramus commented 1 year ago

But does that mean that a container-timeline-linked animation with animation-range:normal is just effectively frozen?

Yes.

mirisuzanne commented 1 year ago

Looking back at the previous discussion, there was some debate over making these timelines available to animations, or only to the mix() function. It seems like we want to encourage the use of mix() for most use-cases, so that the values participate in the cascade. Might be useful to support both, but we would want to ensure browsers ship mix() along with any timeline properties. @andruud does that sound right?

mirisuzanne commented 1 year ago

Discussing with @fantasai - it seems like there are some brevity/consistency advantages to the property approach, but also a potential 'traffic jam' when authors across multiple components/style-sheets all want different timeliness on e.g. the root element. Multiple timelines on a single container would either override each other, or need to be carefully coordinated in a list declaration. If we use an at-rule for defining timelines instead, we avoid that issue – though we're also cluttering a global namespace. It's not clear to us that the property approach is necessarily the better one for this use-case?

Consider also that we're not necessarily limited to size-axis queries on containers, but could potentially respond to style queries as well – the size of a font, etc.

kizu commented 1 year ago

I want to add as a reference, current workaround using scroll-driven animations for this purpose: https://kizu.dev/position-driven-styles/ (though, I've yet to apply this to the intrinsic typography use case; need to find time to do so, should be possible).

Especially the @property section: right now it is possible to use the proportions of size and/or position of an element inside a container (with a potential to query the exact size, but in a bit more complicated way; I have an article in drafts).

Having something like that available natively, with support for querying using actual units would be really helpful.

mirisuzanne commented 1 year ago

@kizu proportional size comparison queries seems like a distinct feature request, and might deserve it's own issue for discussion. It could be used in combination with a query timeline feature like the one discussed here, but I don't think either feature relies on or impacts the other directly?

kizu commented 1 year ago

I meant querying as in “getting the value from the animation” rather than using queries as in media/container-query like.

Here is a quick demo with scroll-driven animations of what I mean: https://codepen.io/kizu/pen/QWzppBd — implementing the first example from the explainer where we can interpolate any values from 10rem to 40rem of the container.

We can do this by having an element that has a width of 40rem, applying an inline view timeline on it, using the timeline-scope to lift this timeline up, and finally using it on our header with the cover range from 10rem to 40rem. Voilà.

https://github.com/w3c/csswg-drafts/assets/177485/398e7661-ae80-40c5-a862-ac83c0c0b22a

What I wanted to say: we already can kinda achieve this with the scroll-driven animations, but in a rather cumbersome way, so it would be really nice to have something native, but we can use scroll-driven animations for prototyping things for now.

I don't yet have a preference over which of the proposed solutions could be the best — would need to think about this separately, mostly wanted to express the support for moving in this direction, as well as provide a workaround that currently works in Chrome.

scottkellum commented 1 year ago

The ability to use mix() on the root element is a major advantage to that direction now that you mentioned it @mirisuzanne. While I appreciate its brevity, the syntax still feels a bit unusual to me. But having the ability to use this on root is huge and something I need on my projects.

andruud commented 1 year ago

It seems like we want to encourage the use of mix()

@mirisuzanne I'm confused. This seems to be the opposite of what this issue originally asked for:

The problems with clamp(), min(), and max() is that you can only interpolate length values on a single property between two points. You may want to interpolate rulesets across multiple breakpoints.

It looks to me that mix() (and new creative forms of mix()) should get their own issues for discussion.

we would want to ensure browsers ship mix() along with any timeline properties. @andruud does that sound right?

If container-linked-animations are useless without mix(), then yes. (Are they?)

traffic jam

This would be solved by additive cascade in the future, right? If yes, this should not be a deciding factor, everything in non-additive CSS is a traffic jam. An at-rule for this seems unnecessarily inconsistent with scroll/view-timeline, but IANAA (I-am-not-an-author), so whatever the group wants.

bramus commented 1 year ago

[…] a potential 'traffic jam' when authors across multiple components/style-sheets all want different timeliness on e.g. the root element. Multiple timelines on a single container would either override each other, or need to be carefully coordinated in a list declaration.

For Scroll-Driven Animations we have a root keyword to hook onto the document scroller. This could also be an allowed value here: animation-timeline: container(root inline-size);

As Anders mentioned, additive CSS could also solve this if authors want a named timeline. Same problem space applies to {view,scroll}-timeline, so I don’t think this is now more urgent than before.

If we use an at-rule for defining timelines instead, we avoid that issue – though we're also cluttering a global namespace. It's not clear to us that the property approach is necessarily the better one for this use-case?

Hmm, back in the day the whole scroll-animations-1 spec go rewritten from using an at-rule @scroll-timeline to properties. Introducing an at-rule for container timelines feels very – very – inconsistent.

flackr commented 1 year ago

Can we remove the container-timeline-range and have the 100% value refer to the container's containing block size. This is the size you would normally get by setting the container's width / height to 100%, right? So it seems like a logical 100% value.

Just as with view timelines it can produce values greater than 100% or less than 0% (demo), the container timeline would produce values greater than 100% if sized larger than its containing block. The default attachment range would be 100% but authors could change this by with animation-range.

mirisuzanne commented 1 year ago

Can we remove the container-timeline-range and have the 100% value refer to the container's containing block size. This is the size you would normally get by setting the container's width / height to 100%, right? So it seems like a logical 100% value.

It makes some sense, but the main use-case here is not %-of-auto interpolation, it's %-between-defined-breakpoints.

additive CSS could also solve this if authors want a named timeline.

I agree, we should work on additive cascade. Can we work on additive cascade? Let's do that. :)

I'm confused. This seems to be the opposite of what this issue originally asked for

Yeah, there are a few overlapping issues here, sometimes in tension… - There are advantages (at least in some cases) to referencing multiple 'keyframes' along the timeline, and multiple properties that move in concert. Both of those would push us towards an animation (or animation-like) model. - The downside is that all interpolated values end up in the animation origin, which is not a great place to manage all typography cascade-wise. Ideally we should _be able to_ keep values in the cascade, as part of the author origin, so it's possible to modify them from other selectors. That's the reason to encourage `mix()` where possible, though it might not be right for all use-cases. I don't know if there's a 'right' way to balance those things. It's not that the animation version is useless without the inline version, but that it might be a bad idea to push authors towards _everything is an animation now_. Setting aside the cascade issue: from a syntax perspective, it's similar to discussing value-level vs at-rule media queries. The grouped at-rule is much simpler when managing multiple properties or breakpoints. The inline version becomes much simpler when managing a single property at a time.

But that's maybe a digression from the primary question here: how do we define a range, and get our current position along that scale? Once we have access to this sort of 'timeline', we can theoretically use it in various interpolation/animation contexts. Both @timeline and timeline-* handle that end of the problem, and should work in both mix() and @keyframe situations.

@emilio informally suggested a slightly different option here at TPAC - a calc-like function for accessing position in a range. Name TBD, but something like position-of(<value>, <range-min>, <range-max>) or position-of(100cqi, 320px, 75ch). The returned value would be a (clamped?) fraction representing where the value 100cqi falls along the range from 320px to 75ch (e.g. 0.5 if the value falls half-way between). Then we could apply that to control interpolation position in various contexts? This is basically 'just' the existing calc() solution, but simplified with a purpose-built function. The advantages are:

I can see tradeoffs with any of the approaches. It might be worth bringing these options and their tradeoffs to the group for a decision?

kizu commented 1 year ago

Just so people could play with the “mix” or “position-of” type of syntax: https://codepen.io/kizu/pen/BavRKBK

It is a fork of my previous pen, but just using the scroll-driven animation to distribute a registered custom property from 0 to 1, which can then be used for “mixing” values in a manner very similar to the mix() and position-of() proposals — obviously, basically the position-of() as we get the fractional value in the end, but also can be seen as an approximation of what mix() can achieve.

I think, as an author, I can see cases where I would want to have the animation-like keyframes definition, as it can be very expressive, but also see the mix() variant be useful as a convenient way to assign something for a single property, but also the position-of() which could be then used as a part of more complicated calculations (see the corresponding section of my article, where I modify the variable using CSS math functions to change the way it is distributed).

Basically, the more I think about it, the more I want to have all 3 ways to get these interpolated values, as I can see various use cases benefiting from each of them.

ydaniv commented 1 year ago

What if we worked around the animation issues by using a new property, e.g. interpolation, and then get all the benefits of this method while avoiding the baggage?

scottkellum commented 1 year ago

As a more generalized problem: we are designing for fluid media when designing for the web, yet we lack a way to control how styles interpolate across that fluid media.

Typography illustrates a specific need here as ensuring optimum readability is a function of changing sizing and spacing with changes in measure (width). Additionally the proportion between headings / body copy increases with measure. The optimal relationship is usually non-linear.

HOWEVER, typography is not the only need. More fluid layout changes and media scaling may be desirable. @lynnandtonic designed her whole website around this interpolation approach in 2021: https://lynnandtonic.com/thoughts/entries/case-study-2021-refresh/

I love the idea of a position-of function (and don't think it should be clamped as you might want to query how far out of bounds something is). I proposed abstracting scroll timelines so timelines could be more general purpose a while back, and they could be bound to scroll, container width, or something else with a custom property. My strong opinion remains with needing more control over interpolation of styles with container inline size, not with the means to this end.

mirisuzanne commented 1 year ago

What if we worked around the animation issues by using a new property, e.g. interpolation, and then get all the benefits of this method while avoiding the baggage?

@ydaniv I don't think that quite works. The animation origin exists because styles from @keyframes don't belong to a selector, but have to go somewhere in the cascade – and have to override the styles that would be applied otherwise (by selectors). Even with a new property name, we still have those issues.

Taking the spirit of that approach, tho – what if we had ways to 'reach into' keyframes from declarations? Something like font-size: from-keyframes(typography, 50%). I'm not sure that's possible or reasonable, but the idea is to grab a keyframe-interpolation value and insert it into the cascade directly?

kizu commented 1 year ago

A wild idea, not sure how viable and possible it is (and maybe something like this was already discussed and dismissed previously?), but… Now that we have our custom cascade @layers, what if we could somehow say “ok, for this animation, instead of its regular behavior, let's put it into this one named layer”. All the current rules for the keyframes would work, but only inside this layer, meaning that any declarations/animations from other layers that are defined to go over this layer would override any properties that were set with these layered animations.

This way any animations, be it scroll-driven, container-based or regular ones could be safely overridden by utilizing the custom cascade layers.

ydaniv commented 1 year ago

@mirisuzanne

The animation origin exists because styles from @keyframes don't belong to a selector, but have to go somewhere in the cascade – and have to override the styles that would be applied otherwise (by selectors). Even with a new property name, we still have those issues.

But that's exactly what I meant. The "baggage" of animation being that we have to use it's origin as a mechanism for applying styles from @keyframes. But if we add a new property, can we decide on a new mechanism without breaking anything, i.e. decide it uses same origin it's defined in? Animations require a special origin because of their nature, but simple interpolation of this sort should just fit into it's surrounding.

andruud commented 1 year ago

What if we worked around the animation issues by using a new property, e.g. interpolation, and then get all the benefits of this method while avoiding the baggage?

(Discussed with @mirisuzanne off-GitHub). A new kind of property that expands to other declarations (pulled from keyframes/animations) cascade-time should be possible.

font-size: from-keyframes(typography, 50%). I'm not sure that's possible or reasonable, but the idea is to grab a keyframe-interpolation value and insert it into the cascade directly?

That sounds approachable as well. As long as from-keyframes() doesn't end up on the computed value.

mirisuzanne commented 1 year ago

But if we add a new property, can we decide on a new mechanism without breaking anything, i.e. decide it uses same origin it's defined in?

Got it. I think this is interesting, and I'm not sure it would need a cascade-layering mechanic to work – though that might be a nice addition at some point. My initial expectation would be something like:

That [font-size: from-keyframes(typography, 50%)] sounds approachable as well. As long as from-keyframes() doesn't end up on the computed value.

There's clearly more work needed to flesh out the details. But in my mind, either of these approaches would address the concerns about how the values are applied in the cascade, and remove the mix() pre-requisite.


On the other end we have how timelines are defined. I do like the idea that this feature could/should play well with scroll timelines etc. And I take the point that this is only one more example for why we need additive cascade – that shouldn't block us. And @flackr, I think I understand now, you were suggesting an interpretation of timeline-range: normal? That makes sense to me, sorry I didn't follow.

I do think it would also help to have a more generic timeline-defining function like the one @emilio proposed? That would somewhat relieve the need for new timeline functions for every specific use-case.

andruud commented 1 year ago

That [font-size: from-keyframes(typography, 50%)] sounds approachable as well. As long as from-keyframes() doesn't end up on the computed value.

There's clearly more work needed to flesh out the details. But in my mind, either of these approaches would address the concerns about how the values are applied in the cascade, and remove the mix() pre-requisite.

More thinking out loud: from-keyframes(typography, 50%) would probably need to take the effect value from a hypothetical animation with that progress. This will produce some interesting complexity, since the effect value depends on the computed keyframes, which in turn can rely on the ("main") computed values of the element. So we may have added a brand new way of creating cycles. Hopefully we can ask IACVT to save us (again).

mirisuzanne commented 1 year ago
Attempting once again to summarize the discussion for TPAC… The goal is a more robust way of describing fluid values based on container features. While calc and clamp provide some basics: - the math is not intuitive - the scaling is always linear - the interpolation is always between two points - and related properties like font metrics can't be grouped We also want to avoid relying on 'animation' for this. The animation origin is needed to ensure smooth motion, but fluid typography belongs in the author origin, cascading where defined. To get where we're going, we need two parts…

Query Timelines:

We need a way to access our current position along a 'timeline' of container attributes (e.g. the inline-size of the container is currently 30% of the way between 30em and 2000px). Ideally we can create both named timelines, and anonymous on-the-fly timelines for various aspects of the container.

For named timelines:

  1. @fantasai and I originally proposed a @timeline rule. Part of the reason we avoided animation-timeline is that we didn't want to rely entirely on animations, for cascade reasons.
  2. @danielsakhapov proposed container-timeline properties to match scroll-timeline properties – without necessarily being tied to animations for usage.

For anonymous timelines:

  1. @bramus points out we would also want a container() function to match scroll() and view().
  2. @emilio has proposed (offline) a generic unit-based syntax, eg timeline(100cqi from 320px to 75ch) (name and details TBD). The advantage is that it's very flexible for mixing and matching a variety of units, as long as they can be expressed with CSS math. That might also be the downside, since it requires a bit more mathematical mental model.

(Depending how those resolve, they could also be captured in custom properties to achieve a named use-case?)

Value Interpolation:

Ideally, we want the ability to define both one-off interpolations between two values, and also more complex grouped properties/keyframes.

Grouped properties and multi-keyframe values could both be handled well by @keyframes definitions, if we have good ways to access those values without relying on the animation origin. To that end…

My hope for this discussion:

In the future… It would still be useful in some cases to get all the properties from a keyframe rule, without using animations. There's a proposal above to do that with a new property that expands to represent multiple interpolated properties. @astearns has pointed out [that sounds like a 'mixin'](https://css.oddbird.net/sasslike/mixins-functions/#keyframe-based-mixins-for-interpolated-values). Maybe something we want to think about as a separate issue, and not necessarily required for a first version of the feature.
bramus commented 1 year ago
  1. @bramus points out we would also want a container() function to match scroll() and view().

Also see the follow-up remark that adds the root keyword to container() to prevent code from breaking when multiple CSS rules set the container-timeline-name on :root.

mirisuzanne commented 1 year ago

The changes to CSS Values Level 5 Editor's Draft reflect our latest proposal, based on the conversation here:

We punted on the question of named timelines for media/container progress, because it seems likely that we can store anonymous progress in custom properties, and name them that way. If we do find that we need an explicit syntax for named timelines, we can revisit that.

We also proposed that the mix() function can reference a @keyframes rule - similar to the from-keyframes() proposal above. That allows setting up complex multi-step interpolation with easing across frames, without using the animation origin of the cascade. Ideally this would also be possible in the typed *-mix() functions, but we're not clear if there's an implementation path for that? Can we access component values from keyframes?

andruud commented 12 months ago

The progress() parts look good, but I'm not sure about the keyframe-connected aspect of mix(). The mix() function would need to "know" the property it's being used in for it to grab the correct would-be effect value from the referenced @keyframes. That is a bit disturbing.

I'm not yet sure how implementable this is, but I wonder if it's more consistent to instead expose this feature as a native mixin-like thing:

@keyframes anim {
  from {
    color: rgb(0, 0, 0);
    top: 0px;
  }
  to {
    color: rgb(100, 100, 100);
    top: 100px;
  }
}

div {
  @mix(50% of anim);
  width: 10px;
}

Here, @mix grabs all the would-be effect values of anim at 50%, and produces styles equivalent to:

div {
  /* @mix */
  color: rgb(50, 50, 50);
  top: 50px;

  width: 10px;
}

And then we just say goodbye to the mix() function. :-)

css-meeting-bot commented 12 months ago

The CSS Working Group just discussed Interpolate values between breakpoints.

The full IRC log of that discussion <bramus> miriam: I can intro this
<bramus> astearns: and outline what we may want to resolve for a future meeting
<bramus> miriam: goal of this is to be able to look at set of MQs or CQs and say that we dont want to just the font size in a linear way but want to do …
<bramus> … not just use viewport/container units
<bramus> … we want to follow an easing curve
<bramus> … similar to an animation in some ways, but are looking at one specific frame
<bramus> … more complex easing … eg font size change in one way while line height … several props that follow some easing path as the container grows
<bramus> … ppl are using hacks for this
<bramus> … would be nice if we have this built into the platform
<bramus> … pieces you need are a way to look at container/media and know where you are
<fantasai> -> https://drafts.csswg.org/css-values-5/#progress
<bramus> … proposal is for a progress function
<fantasai> progress(<calc-sum> from <calc-sum> to <calc-sum>)
<fantasai> media-progress(<media-feature> from <calc-sum> to <calc-sum>)
<bramus> … instead of returning dynamic value at min/max and say “where is between both min/max” and get back the fraction
<fantasai> container-progress(<size-feature> [ of <container-name> ]? from <calc-sum> to <calc-sum>)
<bramus> … so you can have generic progress() ???
<bramus> … also a media and container progress, to look at media/container features
<bramus> … next part is being able to mix values using those progresses
<astearns> s/???/that is like clamp(), using calc()
<bramus> … so we proposed several typed mix functions
<bramus> … color-mix and calc-mix
<bramus> … that take 2 values and a progress and give you an interpolation between the two values
<bramus> … last step is to have a way to set up multiple values in a keyframes way and track across multiple keyframes
<bramus> … ??? but go across values
<bramus> … to do that we had in a mix function that can reference keyframes and look at the property
<bramus> … andruud had some concerns
<bramus> … we proposed a mixin like syntax
<bramus> … might at least be a first step
<bramus> … trying to piece all parts together
<bramus> … hopefully that made sense?
<bramus> astearns: so what are the next steps you think?
<bramus> miriam: I would likely tackle them in the proposed order
<bramus> … ??? gets us part way
<bramus> … then the mix functions give us a lot of power to get interpolated values
<astearns> s/???/progress()
<bramus> … and then keyframe access
<fantasai> Proposal: Add *progress() functions to css-values-5 ED
<bramus> fantasai: we added these all in the ED (?). we have previous resolution to draft a bunch of things, but not specifically these things
<bramus> … proposal is to add progress() and calc-mix next to values-5
<fantasai> Proposal: Add calc-mix(), and prallel type of color-mix(), cross-fade() to css-values-5
<bramus> … ??? and parallel types of color-mix, cross-fade (?)
<TabAtkins> Note also that many of these functions hit the "arguments might contain commas" problem; we'll need to resolve on <https://github.com/w3c/csswg-drafts/issues/9539> as well.
<bramus> … eventually we want to ?? but first we ask if we are in the right direction
<fantasai> s/prallel/parallel/
<bramus> astearns: lets take these up in future meetings
<bramus> … progress() can be async
<bramus> … we’ll have to do this on future meetings
<bramus> … thanks for intro’ing this
<fantasai> calc-mix( <progress>, <calc-sum>, <calc-sum> )
<bramus> … see you next week and hopefully get to the zoom items
<bramus> … thanks!
<fantasai> <progress> = [ <percentage> | <number> | <'animation-timeline'> ]? && by <easing-function>
scottkellum commented 12 months ago

Just to reiterate that interpolation across widths can be useful for layout and other styles as well as typography. @lynnandtonic used this approach extensively for her 2021 redesign: https://lynnandtonic.com/thoughts/entries/case-study-2021-refresh/.

But yes, typography is important here because ideal sizing/spacing rarely fall along a linear path. progress() feels really good to me and has use cases beyond the scope of this issue.

andruud commented 11 months ago

From https://github.com/w3c/csswg-drafts/issues/9343#issuecomment-1832665242:

[@tabatkins]: The "extract a value from particular progress along a keyframe'd animation" idea is completely unrelated to mix(); it was discussed around the same time, but it's not in any way a "mixing" function. I'm annoyed it got folded into the spec; it needs to be a completely different function. I suggest ignoring it; I'm pinging Elika right now about killing it (and potentially reviving it in a dedicated function instead).

@tabatkins Is your objection mostly towards the re-use of the name mix(), or it is something more significant? We're considering a prototype of some of the primitives needed for this use-case, so it would be useful to know how deep this objection goes.

tabatkins commented 11 months ago

Just the name. I have no particular opinion on the functionality - last time it was discussed I acknowledged it as having reasonable use-cases.

kizu commented 11 months ago

I did play a bit with the current prototype implementation in Canary, and stumbled upon an issue: https://bugs.chromium.org/p/chromium/issues/detail?id=1503730#c6

It seems, that the current formula in the spec does contradict the intended purpose of the function:

The value returned by a valid progress() notation is progress value / (end value - start value),

vs

returns a value representing the position of one calculation (the progress value) between two other calculations (the progress start value and progress end value).

With my example being progress(75px from 50px to 100px) — we want to know where the 75px lies between 100px and 50px. Per current formula it ends up being 75 / (100 - 50) === 1.5, but, I think, the intended formula was:

(progress value - start value) / (end value - start value)

Which, for the given values, will be (75 - 50) / (100 - 50) === '0.5', which will mean that 75px is half-way between 50px and 100px.

So, I think, the spec should be updated?

mirisuzanne commented 11 months ago

Yeah, that looks like a typo in CSS Values 5.

(I'm willing to help edit this spec, if that's useful - since I've been working on it anyway. Not sure about the process for that.)

tabatkins commented 11 months ago

Feel free to fix obvious typos regardless ^_^

mirisuzanne commented 11 months ago

Progression calculation fixed in 921e2c01c

tabatkins commented 6 hours ago

@fantasai and I were reviewing this issue to see if the progress()/mix() functions adopted into the CSS Values L5 FPWD actually resolves it, and realized some of the important use-cases @scottkellum brought up still aren't solved by the current spec. Notably, providing multiple (more than two) breakpoints doesn't really work (except possibly via some very verbose/clumsy hacks). And interpolations of multiple properties is still awkwardly redundant.

For example, if you wanted to change 'color' from red to yellow as the container height went from 0px to 100px, and then from yellow to green as it went from 100px to 50em, that's not easily possible.

Currently, this is the best you could do:

/* using the keyframes feature to map progress to multiple stops */
@keyframes stoplight-colors {
    0% { color: red; }
    50% { color: yellow; }
    100% { color: green; }
}
el { 
    --progress: calc(
        clamp(0, container-progress(height, 0px, 100px) * .5), .5) 
        + clamp(0, container-progress(100px, 50em) * .5, .5));
    color: mix(var(--progress) of stoplight-colors);
}

/* using @container conditionals to emulate multiple stops */
el {
  color: green;
  @container (100px <= height < 50em) {
    color: color-mix(container-progress(height, 100px, 50em), yellow, green);
  }
  @container (height < 100px) {
    color: color-mix(container-progress(height, 0px, 100px), red, yellow);
  }
}

Neither of these are great, and the first one doesn't work with any of the *-mix() functions, only mix() itself.

Two areas we can explore further to solve these problems are:

A keyframes reference more straightforwardly packages multiple properties across multiple breakpoints; an inline function allows easier re-use of a given breakpoint-value interpolation mapping across properties. So it's possible we might want to adopt both approaches.