w3c / csswg-drafts

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

[css-animations-2] Declarative syntax for GroupEffects #9554

Open ydaniv opened 1 year ago

ydaniv commented 1 year ago

This is a proposal for a declarative syntax of Web Animations 2's GroupEffects. The behavior of these suggested properties is already defined in Web Animations 2, and here I only suggest how to align it with CSS Animations. This follows a proposal in a comment on a previous issue.

Description:

Add a new property, name to be bike-shedded, like group-effect, to declare a group effect that can be later referenced in another new property, like animation-group, that takes a list of idents to add an animation to a group, and optionally an integer for the position in the group.

The group-effect can be just specified on a parent, which all animation-group's just seek upwards on the tree. The group-effect property can be a shorthand of all properties: name, parent, and rest of timing properties. name can be a dashed/custom-ident.

An example usage could be something along the lines of:

@keyframes slide { ... }
@keyframes blink { ... }

#container {
  group-effect-TBB: --gorup-1 sequence 2.5s 0.5s linear 2;
}

#target {
  animation:
    slide 1s ease-out both,
    blink 1.5s ease-in-out infinite;
  animation-group: --group-1 1, --group-1 2;
}

Once the common parent with the group-effect is matched, that animation becomes active and starts its progress, and then the same goes for its children accordingly.

group-effect will be a shorthand of the following longhands:

New property group-effect-align

This property will control synchronization of child effects inside the group. The name of course is TBB. It behaves same as described here, with the addition of splitting the parallel behavior of "GroupEffect" to start and end, which aligns the child effects to either start or end together respectively. It has the following possible values:

(+) Need to discuss adding iterationStart and endDelay to the syntax. (++) Aligning group-effect-align with current spec of GroupEffect and SequenceEffect in Web Animations 2 will be discussed in a separate issue.


Proposed syntax

animation-group

animation-group-name: <dashed-ident>
animation-group-ordinal: <number>

animation-group: [<animation-group-name> <animation-group-ordinal>?]#

group-effect

group-effect-name: <dashed-ident>
group-effect-parent: <dashed-ident>

group-effect-duration: ... // all effect timing options are 

group-effect-align: start | end | sequence | sequence-reverse

group-effect: [ <group-effect-name> [ / <group-effect-parent>]? [<group-effect-align> || ...]? ]# // ... for rest of timing properties

I probably did some syntax mistakes, but I think the gist of it is clear.

ping: @birtles @flackr @bramus

birtles commented 1 year ago

Looks good! Thanks for doing this! There are two areas I'm particularly interested in.

1. animation-group-ordinal

I'm curious to know more about how you see animation-group-ordinal working and if there are steps we should take to make it more ergonomic.

I think the order property is a useful precedent for this sort of property. However it's a bit different because for layout normally the source order and the layout order match so you only need to specify order occasionally.

It's probably less common for source order and temporal order to match, however? Sometimes they will match (e.g. when you're animating a list in or out), and sometimes the order of animations in the animation-name property could provide the correct group ordering, but when you're animating the elements of a screen layout, for example, neither the source order not the animation-name order is likely to provide a suitable group ordering and the animation-group-ordinal property will be needed.

I guess we need some example content to play with to see what makes most sense? We might discover that a relative ordering could be helpful, e.g. before <animation-name>?

2. Bottom-up vs top-down

Forgive me if you've covered this elsewhere, but I'm curious if there was a particular reason for taking a bottom-up approach vs a top-down one such as defining the group children via an @ rule instead?

I think what you have is probably the more CSS-y way to go, but from an authoring point of view, I imagine it might be simpler to define all the children at once like you would do in JS, e.g.

.curtain {
  animation: slide-open 1s;
  animation-ref: --curtain;
}

.character {
  animation: fade-in 1s --character;
}

@animation-group intro {
  /* Group delay... build the suspense */
  animation-delay: 1s;
  /* Specify child animations or groups */
  animation-group-items: --curtain, --character;
}

Just an idea for comparison. Apart from being easier to reason about from an authoring point of view, one advantage is it provides an unambiguous order for children. I'm sure there are plenty of disadvantages, however.

ydaniv commented 12 months ago

I think the order property is a useful precedent for this sort of property. However it's a bit different because for layout normally the source order and the layout order match so you only need to specify order occasionally.

Yes, that was my intention. To have some sensible defaults that we carefully need to work out because it's not as simple as DOM order.

It's probably less common for source order and temporal order to match, however? Sometimes they will match (e.g. when you're animating a list in or out), and sometimes the order of animations in the animation-name property could provide the correct group ordering, but when you're animating the elements of a screen layout, for example, neither the source order not the animation-name order is likely to provide a suitable group ordering and the animation-group-ordinal property will be needed.

I can currently think of 4 use-cases, there are probably more. You mentioned 3 of them above:

  1. synching different effects on same element
  2. synching same effect on a list of siblings
  3. synching same effect on a grid - siblings but order is not trivial
  4. synching separate effects on non-sibling elements(not 2 or 3)

For 1 we could default to the order of effects in animation-name. For 2 we could default to DOM order. For 3 we could still default to DOM order, but an override might be required. For 4 we could also default to DOM order but very likely it will require an override.

It's also possible to see a mixture of case 1 together with one of the cases 2-4. So maybe you'll have an index per element, plus extra indices per animation on each element.


Forgive me if you've covered this elsewhere, but I'm curious if there was a particular reason for taking a bottom-up approach vs a top-down one such as defining the group children via an @ rule instead?

I followed the design we already have for view/scroll-timeline and timeline-scope. The goal here is to have a property on the element with the animation property that opts it out of being active automatically, and instead adds it into a group. I guess doing that with a property like animation-ref could have the same effect as an animation-group property.

If we consider again the use-cases above, in some having a bottom-up is more convenient, and in others rather the top-down. For case 1 the top-down one could be more handy, where you have a small number of specific effects and you could name each effect and then reference them in a group. Same could be for case 4. In cases 2 and 3, however, you could have a single effect (or a single group effect) and a large number of elements with that effect. So naming the effect doesn't provide much benefit here, but rather opting-in to a group is more straight forward.

Having an @ rule could be handy for declaring a reusable group. However, you're still left with defining the activation of the group. You still need to specify it somehow on an element for it to become active. I can't find a proper way to do that for multiple elements, other than the way suggested above.

In your suggestion above using animation-ref and @animation-group the activation is implicitly spread across the group children, rather than have a single point of activation. Although, I think have a an alternative form of naming the child effects could be very comfortable in the case 1 above.

Perhaps we could have sort of an anonymous group definition for that case with single element and multiple effects? And have a named group for the single effect with multiple elements case?

birtles commented 12 months ago

In cases 2 and 3, however, you could have a single effect (or a single group effect) and a large number of elements with that effect. So naming the effect doesn't provide much benefit here, but rather opting-in to a group is more straight forward.

I think this (along with the view/scroll-timeline precedent) is a really good reason for taking the bottom-up approach. It does feel more CSS-y too.

Your point about activation is really important too.

Supposing we can add a top-down approach later, I think that would be fine to consider a the needs arises and pursue the bottom-up approach initially.

ydaniv commented 12 months ago

Supposing we can add a top-down approach later, I think that would be fine to consider a the needs arises and pursue the bottom-up approach initially.

Yep, that's one option, although case 1 is something I really, and quite commonly, need today for stuff I'm working on. Basically it's something any animation library could benefit from. Perhaps it's something that can more easily be done first via WAAPI, and later we can solve the declarative form.

ydaniv commented 9 months ago

@birtles I've been working lately a lot on the use-case you mentioned, synching multiple animations on a single target, and I think it's imperative we address that as well. Also, if we consider this use-case it also makes sense to be able to create reusable GroupEffects, like you mentioned.

I think we can generalize the use-cases as:

  1. One animation to one target: solved.
  2. One animation to multiple targets: can be addressed with the above proposal.
  3. Multiple animations to a single target: needs addressing with what you described above.
  4. Multiple animations to multiple targets: can be addressed with 2 + 3.

I have a raw proposal for use-case 3, it's still has some open questions, but I think it's a good start.

The gist of it:

Add a new at-rule for declaring a GroupEffect, e.g. @groupeffect:

Here's a rough example of how the syntax will look like for declaring a reusable GroupEffect:

@keyframes slide {
  ...
}

@keyframes jump {
  ...
}

@keyframes recolor {
  ...
}

@groupeffect crazy {
  slide {
    duration: 1.2s;
    easing: ease-in;
  }

  jump {
    duration: 0.8s;
    easing: ease-out;
  }

  recolor {
    duration: 1s;
    easing: linear;
    delay: -1s;
  }
}

#target {
  group-effect: crazy 2s sequence;
}

I think if we combine this proposal and the one above we can get both the bottom-up and top-down options managed.

I'm not sure that using same property for both applying an effect and scoping nested child animations is a good idea, but the values for these are similar, so I'm not sure how to solve that part yet.

WDYT?

birtles commented 9 months ago

@ydaniv Thanks for working on this. That looks promising. I'm having a little trouble remembering how all the parts fit together so perhaps we could work it out in the context of a specific application of case (4).

For example, suppose we have:

<dialog>
  <div class="popup">
    <h1>Popup heading</h1>
    <ul>
      <li>List item 1
      <li>List item 2
      <li>List item 3
    </ul>
  </div>
</dialog>

Then suppose we want to implement the following animation when the dialog:modal selector matches:

  1. Does an opacity fade on the dialog::backdrop pseudo element
  2. Simultaneously instantly animates .popup's translate property to -100% or somesuch so that it is initially offscreen (i.e. vertically off the top of the screen)
  3. Slightly before the opacity fade ends, starts to animate .popup's translate property to none.
  4. After .popup arrives at its resting place, does an opacity fade on h1 and each of the li elements in turn, i.e. "Popup heading", then "List item 1", then "List item 2" etc.. The fade animations should slightly overlap and ideally the amount of overlap should steadily increase so that the start of "List item 2" and "List item 3" is nearly the same.

I wonder how we could express that so that it retains the benefits of group effects like being able to pause/seek the whole animation and adjust the duration globally?

ydaniv commented 9 months ago

Great, that's good test, although in your example there's no case of multiple animations on the same element. So it's a case 4 but not really requiring the @groupeffect part.

There's a fade on the ::backdrop, a slide on the .popup, and the fade on different parts of the contents. If I try to translate this example into code it should like as below:

@keyframes slide {
  from {
    translate: -100%;
  }
}

@keyframes fade {
  from {
    opacity: 0;
  }
}

dialog {
  group-effect:
    --showModal 2s sequence,
    --showContent / --showModal 2 1s start calc(200ms * log(effect-index() + 1));

  &::backdrop {
    animation: fade 0.5s ease-out;
    animation-group: --showModal 0;
  }

  .popup {
    animation: slide 0.6s -0.1s ease-out;
    animation-group: --showModal 1;
  }

  h1 {
    animation: fade 0.2s;
    animation-group: --showContent 0;
  }

  li {
    animation: fade 0.2s;
    animation-group: --showContent calc(sibling-index() + 1);
  }
}

Some highlights

This part:

 group-effect:
    --showModal 2s sequence,

Groups the entire timeline.

While the second GroupEffect here:

    --showContent / --showModal 2 1s start calc(200ms * log(effect-index() + 1));

Groups together the effects on the content. Specifically this part / showModal 2 stands for some syntax that should say that the parent group of --showContent is --showModal and its ordinal in that group is 2.

Notice I also used negative delay to slightly overlap the face of the ::backdrop and the slide of .popup as requeted, here:

animation: slide 0.6s -0.1s ease-out;

I guess if we wanted to test case 4 with also multiple animations on a single element, let's assume the fade and slide effects are both on the dialog itself. Then it would look like the following:

@groupeffect --showModal {
  fade {
    duration: 0.5s;
    easing: ease-out;
  }

  slide {
    duration: 0.6s;
    delay: -0.1s;
    easing: ease-in;
  }
}

dialog {
  group-effect:
    --showModal 2s sequence,
    --showContent / --showModal 2 1s start calc(200ms * log(effect-index() + 1));

  ... 
}

And with the assumption above that we define the effects defined inside --showModal get ordinals that start from 0.

birtles commented 9 months ago

Great, that's good test, although in your example there's no case of multiple animations on the same element. So it's a case 4 but not really requiring the @groupeffect part.

Oh, you're right. I think I originally intended that the dialog would both translate and scale a little but I forgot to add the scale part.

Overall it looks very promising. It's great to see the negative delay for overlap and the decreasingly staggered start times too.

One thing I didn't understand was in the following:

 group-effect:
    --showModal 2s sequence,

Why do we need to define the length of the group here? For groups in Web Animations 2, by default they calculate their length from their children (see 1 and 2). This can be overridden to make a group clip its children or be extended but the typical usage would be to use the intrinsic duration.

I know that in the context of finite timelines we've also talked about allowing groups to behave like temporal flexbox containers where you'd set the group duration to 2s and let children specify their duration as a fraction of that. I can find the issues where we discussed that if that's helpful. That would certainly be great to have in future but I don't think that's the case in this example is it?

I guess what I'm really saying is that I didn't follow the second half of this line:

--showContent / --showModal 2 1s start calc(200ms * log(effect-index() + 1));

It looks like we're defining some basic timing properties that apply to each child in the group but I wonder if that's the right place to do it. Given that groups and children can have their duration defined separately, maybe the child timing properties should only be defined on the child.

ydaniv commented 8 months ago

Why do we need to define the length of the group here? For groups in Web Animations 2, by default they calculate their length from their children.

By "length" you mean duration? We don't need, I wasn't 100% sure whether the Group defines timing for its children or vice versa. So if it's defined by the children's duration then it's fine and we don't need to specify it again. So, IIUC, it's either we have "intrinsic" duration or "extrinsic" one.

I know that in the context if finite timelines we've also talked about allowing groups to behave like temporal flexbox containers where you'd set the group duration to 2s and let children specify their duration as a fraction of that. I can find the issues where we discussed that if that's helpful. That would certainly be great to have in future but I don't think that's the case in this example is it?

That's super interesting on its own, but not necessary for this issue.

It looks like we're defining some basic timing properties that apply to each child in the group but I wonder if that's the right place to do it. Given that groups and children can have their duration defined separately, maybe the child timing properties should only be defined on the child.

In your test-case you asked for a decaying stagger effect on the children:

The fade animations should slightly overlap and ideally the amount of overlap should steadily increase so that the start of "List item 2" and "List item 3" is nearly the same.

So either we define specific delay on the children or group-effect-align on the parent. This is the syntax I proposed on #9561. It's like place-items and place-self, if you use -align on the parent you specify alignment for all the children in one property. If you specify delay you set that animation/group's alignment individually.

birtles commented 8 months ago

Why do we need to define the length of the group here? For groups in Web Animations 2, by default they calculate their length from their children.

By "length" you mean duration? We don't need, I wasn't 100% sure whether the Group defines timing for its children or vice versa. So if it's defined by the children's duration then it's fine and we don't need to specify it again. So, IIUC, it's either we have "intrinsic" duration or "extrinsic" one.

Yes, duration. I think we ultimately want to get to a model similar to what we have for layout where by default children define the height of their parent, but parents can override it and children can be made to have a percentage of their parent's height.

That is, by default, the duration of the children defines the duration of the parent but we introduce means in future for expressing a child's duration as a proportion of its parent's.

It looks like we're defining some basic timing properties that apply to each child in the group but I wonder if that's the right place to do it. Given that groups and children can have their duration defined separately, maybe the child timing properties should only be defined on the child.

In your test-case you asked for a decaying stagger effect on the children:

The fade animations should slightly overlap and ideally the amount of overlap should steadily increase so that the start of "List item 2" and "List item 3" is nearly the same.

So either we define specific delay on the children or group-effect-align on the parent. This is the syntax I proposed on #9561. It's like place-items and place-self, if you use -align on the parent you specify alignment for all the children in one property. If you specify delay you set that animation/group's alignment individually.

Oh, I see, sorry I'd forgotten about that. I'm still trying to process this but one question comes to mind:

Currently we have a situation where CSS animation/transition properties map pretty closely to Web animation models. If you get a CSSAnimation or CSSTransition object using getAnimations() and mutate part of the the object, we break the link with the corresponding CSS property (e.g. see this section https://drafts.csswg.org/css-animations-2/#animations).

If we have a CSS animation in a group with these group-effect-align properties etc. and we fetch the corresponding CSSAnimation object then mutate its start delay, will the other objects in the group continue to behave sensibly?

I guess my question is really, if we're introducing this alignment concept only in the CSS syntax, are there any cases where that causes problems when trying to interact with animations via the API?

ydaniv commented 8 months ago

I guess my question is really, if we're introducing this alignment concept only in the CSS syntax, are there any cases where that causes problems when trying to interact with animations via the API?

I also opened #9557 to propose adding align to WAAPI. So these properties need to follow same rules as above.

I think we only need to specify whether start/endDelay override delays set by the parent's align or whether they add up together.

yisibl commented 4 months ago

Framer Motion has a similar API: https://www.framer.com/motion/animate-function/##animate-sequences

I would like to know how a sequential increase in Delay time, like in the example, should be represented in CSS in the current proposal?

https://github.com/w3c/csswg-drafts/assets/2784308/be270efc-3a5b-43bb-8e51-616c4cd1bf81

ydaniv commented 4 months ago

Yes, also GSAP's timeline.

But while we're at it, I was considering renaming GroupEffect to just Sequence, but I guess bikesheding should probably be done on a separate isssue.