-
Notifications
You must be signed in to change notification settings - Fork 711
[web-animations-1] Alternative to FillAnimation: replace events #3689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
/cc @graouts @flackr @stephenmcgruer This proposal sounds similar to a couple of things you have mentioned in the past. Let me know what you think. |
Thinking about this a bit further, automatically canceling is a bit weird. Under this proposal two animations might start at the same time but one ends up in the Perhaps we need some orthogonal bit of state for reflecting replaced-ness. e.g. enum ReplaceState {
no, // Not yet replaced
replaced,
persisted
}
partial interface Animation {
attribute EventHandler onreplace;
readonly attribute ReplaceState replaced;
void persist(); // Throws if replaced === 'no'?
} (I also looked at putting this onto the effect instead so that in a |
Could you clarify, in the summary you state that only completely "replaced" animations are canceled, but in step 3 of the proposal you state that an animation is replaced if all of its properties are specified by any subsequent fill: forwards animation, without mention that they need to be composite: replace. Was this intentional or did you mean to require composite replace? It seems like if we limit this to |
No it doesn't require
I think that wouldn't help with the example though. In that example, we would continue to leak memory. |
Thanks, this makes sense now. I worry about requiring developer cooperation with the animation system when the animation's effects are replaced. It sounds like forgetting to do this could go unnoticed and result in occasional unpredictable behavior. If the developer does know to handle this, they feasibly could also handle applying their animation's final output to the inline style on animationend and canceling the animations. The missing piece (I think) would be how to compute the result of adding the final keyframes' styles, which may not be trivial. It would be nice to have more predictable behavior that "just works", whether that's replacing completed |
Refining this further, I have two particular questions that might help clarify how this should behave. One stems from something @danwilson mentioned on the Animation at Work slack (#waapi channel), and one is based on @flackr's question above. 1) Do we replace even if there are underlying animations? e.g.
(Assume all animations target the same set of properties on the same element and all are additive.) Do we replace If we say "Yes" → whether or not the
which is clearly different. In this case script would need to know that there is an underlying effect so it can call If we say "No" → Then script can naively just clobber specified style with whatever is passed in and generally the result will not change since, once That's simpler but it will mean if you have a never-ending underlying animation, we'd never dispatch any 2) Do we replace only once all properties are covered? I initially assumed so but the following case has me concerned:
So far, so good.
That's probably ok, but it might be a bit surprising that the output changes suddenly due to two unrelated animations. An alternative might be to replace on a property-by-property basis but I think that would leak to a much more complex API and it's probably ok to expect authors to call |
Right, especially if the animation is filling in between two keyframes. We may want to add an API for querying intermediate animation values in future anyway, but I think in this context the advantage of putting it on the events is that we snapshot the values. That's particularly important because the
I think that's essentially the
I'm a little averse to this because I'm afraid it will be surprising to the author if we silently update specified style or their (I believe we actually tried to implement updating specified style in a previous iteration of this discussion and it didn't work but I don't recall what the specific issue was.) |
I'm going to try and document the alternative proposal we discussed at the meeting yesterday, but first, one thing I recalled after our meeting was that even if we change the order of operation for the effect stack so that interpolation happens before addition, I don't think it will drastically simplify implementation or improve performance of any Consider lengths and percentages. For some properties it is possible to interpolate from The simplest way to do that is simply to preserve the interpolation interval endpoint(s) (there will be two endpoints if the animation fills mid-interval, one otherwise). For a stack using addition, this simple approach involves preserving the interval endpoint(s) for the whole additive part of the stack. If that proves undesirable, a UA can introduce various optimizations. For example, in the case where the units are all the same, e.g. all If the UA wants to optimize further still, it could perhaps build up buckets for each unit type and add appropriate portions to each bucket. The final values of each bucket could be stuck in a single Implementing the above example optimizations is quite involved and the end result still does not account for variables and only applies to length/percentages. My experience working on |
Yesterday at our sync up we discussed yet another alternative approach which is somewhat of a middle-ground between this proposal and the The idea is something like the following:
That's a fairly high-level overview. I haven't thought through the specifics of it yet. Advantages:
Disadvantages:
Personally, my main concern is that I'm still not sure that this alternative offers a net reduction in complexity. Although it reduces complexity surrounding existing If it turns out that this proposal is comparable to the |
Worth mentioning on this topic is the It's basically equivalent to the |
I'm not clear what the relevance of On the interpolation topic, I believe @flackr was working on an idea that hopefully addresses the "doesn't simplify things" concern. |
Yeah, like I said, it's just terminology.
Great. I'd love to hear it as soon as possible. |
Sorry for taking so long to review this. Just to make sure I don't misunderstand, this proposal suggests that an Are this proposal and the |
Yes that's right. I'd prefer to either do this proposal or the |
I wonder if we could have API directly to commit an animation. I worry that a lot of authors would write something like this: function commitStyles(replaceEvent)
{
const animation = replaceEvent.target;
const element = animation.effect.target;
for (let property of Object.getOwnPropertyNames(animation.getKeyframes())) {
if (property == "offset" || property == "computedOffset" || property == "easing" || property == "composite")
continue;
element.style[property] = replaceEvent.computedStyle[property];
}
}
target.animate(…).onreplace = commitStyles; I think it'd be awesome if we could provide some built-in function on an effect to apply its animated values directly to the target, with potentially a list of properties to commit. // Commit all animated properties to inline style upon completion.
target.animate(…).onreplace = evt => {
evt.target.effect.commitAnimatedProperties();
};
// Commit select animated properties to inline style upon completion.
target.animate(…).onreplace = evt => {
evt.target.effect.commitAnimatedProperties("transform", "opacity");
}; Naming certainly could improve, there likely is a catchier and shorter name for the function of committing some animated properties to inline style. We could take it one step further and forgo the use of the // Commit all animated properties to inline style upon completion.
target.animate(
{ transform: 'translateX(-80px)', composite: 'add' },
{ duration: 1000, easing: 'ease', commit: 'all' }
);
// Commit select animated properties to inline style upon completion.
target.animate(
{ transform: 'translateX(-80px)', composite: 'add' },
{ duration: 1000, easing: 'ease', commit: ['transform', 'opacity'] }
); |
I also suggest we add informative text that explains that this |
If we can switch the composition and interpolation order as proposed in #2204 I believe this will be a complete solution as we can always replace with the equivalent transform matrix.
I think the compatibility risk is reduced over this proposal. The only time existing sites would break is if they tried to use completed animations, whereas in this proposal animations will silently be dropped and change output value.
I disagree, I think that this proposal describes a hard to predict set of circumstances for dropping animations where an always replace model has very predictable behavior. We could however make it only replace at the point in time where there was more than one filling animation.
I believe that resolving #2204 would mean that the keyframes of a replaced animation could simply have the transform matrix which greatly reduces the complexity.
Agreed |
I definitely agree we want to make this easier. A few comments:
|
I'm not sure it does since the problem there is not just combining matrices, but also dealing with context-sensitive values. |
Incorporating some of the suggestions that have come up on this thread, the proposed IDL might be something like: enum RemoveState {
"no", // Not yet removed
"removed",
"persisted"
};
partial interface Animation {
attribute EventHandler onremove;
readonly attribute RemoveState removed;
void commitComputedStyles(optional sequence<DOMString> propertiesToCommit);
void persist();
};
[Exposed=Window,
Constructor (DOMString type, optional AnimationRemoveEvent eventInitDict)]
interface AnimationRemoveEvent : Event {
readonly attribute double? timelineTime;
readonly attribute CSSStyleDeclaration computedStyle;
readonly attribute StylePropertyMapReadOnly computedStyleMap;
};
dictionary AnimationRemoveEventInit : EventInit {
double? timelineTime = null;
CSSStyleDeclaration computedStyle;
StylePropertyMapReadOnly computedStyleMap;
}; Or, alternatively: enum CommitKeyword = { "all", "none" };
partial dictionary KeyframeEffectOptions {
commit: CommitKeyword or sequence<string> = "none";
};
partial interface KeyframeEffect {
readonly attribute (CommitKeyword or FrozenArray<string>) commit;
}; Unfortunately, I don't think the latter alone covers the case where the author wants to persist the full effect so I suspect we would need most of the first part too (but probably minus the |
Discussed this with @stephenmcgruer @flackr @graouts @majido and Olga yesterday. Summarizing some of the points:
That last point highlights a problem with the proposal in this issue: specifically the proposed replace/remove event includes a single There are a few possibilities for addressing this last point:
|
Actually, I think it would have to accept shorthand (and probably logical) properties too. Not only is that easier for authors but it also means that if any longhand gets promoted to a shorthand (as they tend to do) code keeps working. |
I've implemented this alternative proposal in Firefox including pretty thoroughgoing web-platform-tests. I plan to prepare a spec PR tomorrow. The IDL as it currently stands looks like: enum AnimationReplaceState {
"ok",
"removed",
"persisted"
};
partial interface Animation {
readonly attribute AnimationReplaceState replaceState;
attribute EventHandler onreplace;
void persist ();
void commitStyles (optional sequence<DOMString> propertiesToCommit);
}; Most of it is quite straight forward. The key part is that the check for replacement happens after updating the time of all timelines but before running any animation event handlers or promise callbacks (that is it happens as part of the update animations and send events procedure). That seems to work well, making the replacement predictable and producing a consistent state when script runs. I've made it possible to call I've also made it possible to call Most of the outstanding questions concern the 1. What do we call it?
I've gone with 2. Should
|
From my experience implementing this alternative approach:
|
There is a PR for this over in #3833 but I've encountered one issue regarding Initially I argued that In that PR I also specified that However, that means that simple test cases like the following don't work: const div = createDiv(t);
const animation = div.animate({ opacity: 0 }, { duration: 1, fill: 'forwards' });
animation.finish();
animation.commitStyles(); At the point where we call I think it might be better to make It's not great for performance, but at least UAs only need to flush style, not layout. I'd be keen to know what others think about this. I imagine @emilio might have some ideas. (The only other alternative I can think of to this API that avoids this issue and accommodates |
I'm not particularly excited about the need to flush in there... Is the computed style only needed for logical properties? I wonder if we should be preserving them later, or deferring the
Note that last time I checked Blink and WebKit don't have that difference between "just update styles" and "update style and layout". |
The computed style is only needed for logical properties, but the flush is needed to ensure we commit the up-to-date state. For example: div.style.fontSize = '10px';
const anim = div.animate(
{ width: '100em' },
{ duration: 1000, fill: 'forwards' }
);
anim.ready.then(() => {
anim.finish();
div.style.fontSize = '20px';
anim.commitStyles();
anim.cancel();
console.log(getComputedStyle(div).width);
}); Without the flush, this will produce A more likely case is something like: container.style.fontSize = '10px';
const containerAnim = container.animate(
{ fontSize: '20px' },
{
duration: 1000,
fill: 'forwards',
easing: 'step-end',
}
);
const childAnimA = child.animate(
{ width: '100em' },
{ duration: 1000, fill: 'forwards' }
);
const childAnimB = child.animate(
{ width: '100em' },
{ duration: 1000, fill: 'forwards' }
);
childAnimA.onremove = () => {
childAnimA.commitStyles();
childAnimB.cancel();
console.log(getComputedStyle(div).width);
}; This too will produce
Deferring the |
https://drafts.csswg.org/web-animations/#replacing-animations Animations with fill modes are a potential source of memory leaks. To overcome this problem, replaceable animations were introduced to the spec. If a finished animation is active purely by virtue of having a fill mode and if all of the properties have been replaced by an animation higher in the composite ordering, then the animation can be removed (somewhat over simplified). Animations can be forced to remain active via the persist() method. This patch introduces Animation.replaceState and Animation.persist() as well as support for detection and removal of replaced animations. The logic for handling CSS transitions and CSS animations has been oversimplified in this patch to avoid over-aggressive pruning. Special handling for animations with owning elements will be adding in a later patch. CommitStyle will also be added in a later patch. Note that removal of replaced animations can introduce a visual change if the composite mode is not replace. This is intentional (w3c/csswg-drafts#3689) as the memory leak was seen as the more serious issue, and can be addressed via use of persist and commitStyle. https://www.chromestatus.com/feature/5127767286874112 https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/H5sz_dg6fKc/1X7K7U4XCgAJ Bug: 981905 Change-Id: I704cb3964aae0336266984ad3da8e37578b3bcb9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1994014 Reviewed-by: Robert Flack <flackr@chromium.org> Reviewed-by: Yi Gu <yigu@chromium.org> Commit-Queue: Kevin Ellis <kevers@chromium.org> Cr-Commit-Position: refs/heads/master@{#734454}
In #3210 and #2054 I have been working on a proposal to allow filling animations to not leak memory. I have prepared spec text for that and implemented (but not yet landed) it in Firefox but it is quite complex. This is an alternative approach that might be simpler.
Summary
Proposal: automatically cancel any completely replaced filling script animations and fire a 'replace' event that includes their animated style so the author can manually set that on the element if they wish to preserve the animation's effect:
e.g. instead of
(Live example)
The author would be required to write:
When additive animations are not in use, the authors would typically be free to ignore the event unless they planned to cancel the overriding animation.
Proposal
Define a forever-filling animation as:
That is, it is not a CSS animation with an owning element, nor a CSS transition with an owning element.
Any time a forever-filling animation finishes, run the following steps:
Look up the effect stack for the target element of the animation that finished.
(Once we introduce
GroupEffect
s, this will involve visiting all target elements.)Let replaced animations be an empty set.
Proceeding from the bottom of the effect stack[1], for each forever-filling animation, animation, where all the target properties (once resolved to physical longhand properties) are also specified by one or more forever-filling animations that have a greater composite order:
Calculate the computed value of each of animation's target effect's target properties at its current position in the effect stack.
Add (animation, computed values) to replaced animations.
For each pair (animation, computed values) in replaced animations:
Cancel animation (but don't dispatch a 'cancel' event? See below).
Create an
AnimationReplaceEvent
, replaceEvent.(I'm not sure if we should re-use
AnimationPlaybackEvent
or not here.)Set replaceEvent’s
type
attribute to 'replace'.Set replaceEvent’s
computedStyle
attribute to computed values.(For implementations that implement CSS Typed OM, there is also
a
computedStyleMap
attribute.)Set replaceEvent’s
timelineTime
attribute to the current time of the timeline with which animation is associated. If animation is not associated with a timeline, or the timeline is inactive, let timelineTime be null.If animation has a document for timing, then append replaceEvent to its document for timing's pending animation event queue along with its target, animation. For the scheduled event time, use the result of converting the most recently finished overriding animation’s target effect end to an origin-relative time.
Otherwise, queue a task to dispatch replaceEvent at animation. The task source for this task is the DOM manipulation task source.
[1] Obviously no one would actually implement this as proceeding from the bottom of the stack. You'd almost certainly want to proceed from the top and go down. The point here is just to ensure that the replaced animations and hence the replace events are dispatched in order from lower in stack to higher.
Since replace events are dispatched in composite order, script can blindly just set the computed style on the target element and wait for subsequent events to override it.
Given that replace events are typically queued as part of updating timing and run before updating the rendering, there should be no visual glitch as a result of updating style in this manner.
In order to generate the replace events an additional style flush will typically be required to compute the correct computed style. Implementations might want to avoid doing this when there are no replace event listeners registered. Such an optimization could possibly produce Web-observable changes with regards to dispatching transitions so the behavior with regard to style change events might need to be specified.
IDL
Relationship to finish events
Finish events sort before replace events. It is the finishing of a higher-priority animation that triggers the replacing.
For an animation that is replaced as soon as it finishes, the finish event still fires first since logically it finishes before it is replaced.
Relationship to the
finished
PromiseSince finishing happens before replacing, the
finished
promise will be resolved and hence will not be rejected when an animation is subsequently canceled by being replaced.Relationship to cancel events
I suspect cancel events should not be fired even though we effectively cancel these animations. Dispatching cancel events will likely confuse authors and might break existing content. Rather, we should consider replacing a special kind of canceling; neutering perhaps.
Side effects
Replace events give the author the computed animated style for the effect that is being canceled so that they can apply it directly. However, since the returned value will be comprised of computed values for longhand (and probably physical-only) properties, any context-sensitive values (
em
units, CSS variables(?) etc.) will be lost. Furthermore, since CSS cannot yet represent additive values in static contexts, any additive behavior will also be lost.These side effects should at least be somewhat apparent from the naming (
computedStyle
) and steps taken to achieve the effect.Compatibility
Currently only effects that fully replace the underlying value are shipped so the only situation where this proposal would produce an observable change is if an overriding forever-filling animation is canceled while there is an underlying forever-filling animation. Under this proposal, the underlying animation would have been canceled so the result would differ.
I don't expect this is common but usage of the cancel() method is surprisingly high (3% of page loads!)
Issues
Ideally it would be good to offer a way for authors to opt-out of the replacing behavior.
For example, if, in the replace event callback, the author could re-instate the animation to its filling state, they could avoid the side effects mentioned above. This might produce a situation that effectively leaks memory, but it would be at the author's explicit direction much as if they created new Elements ad infinitum (and unlike the case where they fire off filling animations without necessarily being aware the animations will never disappear).
I don't have any idea for what a suitable API for reinstating such an Animation would be, however.
The text was updated successfully, but these errors were encountered: