Bug 1004377 - Dispatch events for CSS Animations with empty keyframes rules; r=dholbert

This patch removes the check that skipped queueing events for animations
without keyframes since the spec indicates such animations should dispatch
events.

There is a further correctness fix here for the case where a keyframes rule
is modified using the CSSOM so that it becomes empty. Previously when we
came to create the new animation rules we would end up setting
ElementAnimations::mNeedRefreshes to false since we check if the keyframes
rule is empty and if it is we would skip all further processing (including
setting mNeedsRefreshes).

That means that:
(a) We may end up unregistering from the refresh observer so we would never
    dispatch the end event for such an animation.
(b) If the animation was running on the compositor we may never remove it from
    the compositor or may not do it in a timely fashion.

To fix both these problems, this patch removes the check for an empty keyframes
rule so that mNeedsRefreshes is set in this case.
This commit is contained in:
Brian Birtles 2014-06-12 13:18:14 +09:00
parent c39677d5f1
commit a3321a71fe
4 changed files with 202 additions and 12 deletions

View File

@ -69,6 +69,7 @@ ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime,
ElementAnimation* anim = mAnimations[animIdx];
if (anim->mProperties.IsEmpty()) {
// Empty @keyframes rule.
continue;
}
@ -112,11 +113,6 @@ ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime,
for (uint32_t animIdx = mAnimations.Length(); animIdx-- != 0; ) {
ElementAnimation* anim = mAnimations[animIdx];
if (anim->mProperties.IsEmpty()) {
// Empty keyframes rule.
continue;
}
// The ElapsedDurationAt() call here handles pausing. But:
// FIXME: avoid recalculating every time when paused.
TimeDuration elapsedDuration = anim->ElapsedDurationAt(aRefreshTime);
@ -131,8 +127,9 @@ ElementAnimations::EnsureStyleRuleFor(TimeStamp aRefreshTime,
// If the time fraction is null, we don't have fill data for the current
// time so we shouldn't animate.
if (computedTiming.mTimeFraction == ComputedTiming::kNullTimeFraction)
if (computedTiming.mTimeFraction == ComputedTiming::kNullTimeFraction) {
continue;
}
NS_ABORT_IF_FALSE(0.0 <= computedTiming.mTimeFraction &&
computedTiming.mTimeFraction <= 1.0,
@ -216,12 +213,6 @@ ElementAnimations::GetEventsAt(TimeStamp aRefreshTime,
for (uint32_t animIdx = mAnimations.Length(); animIdx-- != 0; ) {
ElementAnimation* anim = mAnimations[animIdx];
// We should *not* skip animations with no keyframes (bug 1004377).
if (anim->mProperties.IsEmpty()) {
// Empty keyframes rule.
continue;
}
TimeDuration elapsedDuration = anim->ElapsedDurationAt(aRefreshTime);
ComputedTiming computedTiming =
ElementAnimation::GetComputedTimingAt(elapsedDuration, anim->mTiming);

View File

@ -61,6 +61,19 @@ function is_approx(float1, float2, error, desc) {
desc + ": " + float1 + " and " + float2 + " should be within " + error);
}
function findKeyframesRule(name) {
for (var i = 0; i < document.styleSheets.length; i++) {
var match = [].find.call(document.styleSheets[i].cssRules, function(rule) {
return rule.type == CSSRule.KEYFRAMES_RULE &&
rule.name == name;
});
if (match) {
return match;
}
}
return undefined;
}
// Checks if off-main thread animation (OMTA) is available, and if it is, runs
// the provided callback function. If OMTA is not available or is not
// functioning correctly, the second callback, aOnSkip, is run instead.

View File

@ -136,6 +136,9 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=435442
to { margin-top: 150px !important; /* ignored */
margin-bottom: 50px; }
}
@keyframes empty { }
@keyframes nearlyempty { to { margin-left: 100px; } }
</style>
</head>
<body>
@ -1776,6 +1779,83 @@ check_events([{ type: 'animationstart', target: div,
+ " animation");
done_div();
/*
* Bug 1004377 - Animations with empty keyframes rule
*/
new_div("margin-right: 200px; animation: empty 2s 1s both");
listen();
advance_clock(0);
check_events([], "events during delay");
advance_clock(2000); // Skip to middle of animation
div.clientTop; // Trigger events
check_events([{ type: 'animationstart', target: div,
animationName: 'empty', elapsedTime: 0,
pseudoElement: "" }],
"middle of animation with empty keyframes rule");
advance_clock(1000); // Skip to end of animation
div.clientTop; // Trigger events
check_events([{ type: 'animationend', target: div,
animationName: 'empty', elapsedTime: 2,
pseudoElement: "" }],
"end of animation with empty keyframes rule");
done_div();
// Test with a zero-duration animation and empty @keyframes rule
new_div("margin-right: 200px; animation: empty 0s 1s both");
listen();
advance_clock(0);
advance_clock(1000);
div.clientTop; // Trigger events
check_events([{ type: 'animationstart', target: div,
animationName: 'empty', elapsedTime: 0,
pseudoElement: "" },
{ type: 'animationend', target: div,
animationName: 'empty', elapsedTime: 0,
pseudoElement: "" }],
"end of zero-duration animation with empty keyframes rule");
done_div();
// Test with a keyframes rule that becomes empty
new_div("animation: nearlyempty 1s both linear");
advance_clock(0);
advance_clock(500);
is(cs.getPropertyValue("margin-left"), "50px",
"margin-left for animation that is about to be emptied");
listen();
findKeyframesRule("nearlyempty").deleteRule("to");
is(cs.getPropertyValue("margin-left"), "0px",
"margin-left for animation with (now) empty keyframes rule");
check_events([], "events after emptying keyframes rule");
advance_clock(500);
div.clientTop; // Trigger events
check_events([{ type: 'animationend', target: div,
animationName: 'nearlyempty', elapsedTime: 1,
pseudoElement: "" }],
"events at end of animation with newly " +
"empty keyframes rule");
done_div();
// Test when we update to point to an empty animation
new_div("animation: always_fifty 1s both linear");
advance_clock(0);
advance_clock(500);
is(cs.getPropertyValue("margin-left"), "50px",
"margin-left for animation that will soon point to an empty keyframes rule");
listen();
div.style.animationName = "empty";
is(cs.getPropertyValue("margin-left"), "0px",
"margin-left for animation now points to empty keyframes rule");
advance_clock(500);
div.clientTop; // Trigger events
check_events([{ type: 'animationstart', target: div,
animationName: 'empty', elapsedTime: 0,
pseudoElement: "" }],
"events at start of animation updated to use " +
"empty keyframes rule");
done_div();
SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
</script>

View File

@ -138,6 +138,13 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=964646
transform: translate(50px); }
}
@keyframes empty { }
@keyframes nearlyempty {
to {
transform: translate(100px);
}
}
.target {
/* The animation target needs geometry in order to qualify for OMTA */
width: 100px;
@ -1915,6 +1922,105 @@ addAsyncAnimTest(function *() {
done_div();
});
/*
* Bug 1004377 - Animations with empty keyframes rule
*/
addAsyncAnimTest(function *() {
new_div("margin-right: 200px; animation: empty 2s 1s both");
listen();
advance_clock(0);
yield waitForPaintsFlushed();
check_events([], "events during delay");
advance_clock(2000); // Skip to middle of animation
gDiv.clientTop; // Trigger events
check_events([{ type: 'animationstart', target: gDiv,
animationName: 'empty', elapsedTime: 0,
pseudoElement: "" }],
"events during middle of animation with empty keyframes rule");
advance_clock(1000); // Skip to end of animation
gDiv.clientTop; // Trigger events
check_events([{ type: 'animationend', target: gDiv,
animationName: 'empty', elapsedTime: 2,
pseudoElement: "" }],
"events at end of animation with empty keyframes rule");
done_div();
});
// Test with a zero-duration animation and empty @keyframes rule
addAsyncAnimTest(function *() {
new_div("margin-right: 200px; animation: empty 0s 1s both");
listen();
yield waitForPaintsFlushed();
advance_clock(1000);
gDiv.clientTop; // Trigger events
check_events([{ type: 'animationstart', target: gDiv,
animationName: 'empty', elapsedTime: 0,
pseudoElement: "" },
{ type: 'animationend', target: gDiv,
animationName: 'empty', elapsedTime: 0,
pseudoElement: "" }],
"events at end of zero-duration animation with " +
"empty keyframes rule");
done_div();
});
// Test with a keyframes rule that becomes empty
addAsyncAnimTest(function *() {
new_div("animation: nearlyempty 1s both linear");
yield waitForPaintsFlushed();
advance_clock(500);
omta_is("transform", { tx: 50 }, RunningOn.Compositor,
"Animation is animating on compositor");
// Update keyframes rule and check the result gets removed
listen();
findKeyframesRule("nearlyempty").deleteRule("to");
yield waitForPaintsFlushed();
omta_is("transform", { }, RunningOn.MainThread,
"Animation with (now) empty keyframes rule is cleared " +
"from compositor");
// Check we still dispatch the end event however
advance_clock(500);
gDiv.clientTop; // Trigger events
check_events([{ type: 'animationend', target: gDiv,
animationName: 'nearlyempty', elapsedTime: 1,
pseudoElement: "" }],
"events at end of animation with newly " +
"empty keyframes rule");
done_div();
});
// Test when we update to point to an empty animation
addAsyncAnimTest(function *() {
new_div("animation: always_fifty 1s both linear");
yield waitForPaintsFlushed();
advance_clock(500);
omta_is("transform", { tx: 50 }, RunningOn.Compositor,
"Animation is animating on compositor");
// Update animation name
listen();
gDiv.style.animationName = "empty";
yield waitForPaintsFlushed();
omta_is("transform", { }, RunningOn.MainThread,
"Animation updated to use empty keyframes rule is cleared " +
"from compositor");
// Check events
advance_clock(500);
gDiv.clientTop; // Trigger events
check_events([{ type: 'animationstart', target: gDiv,
animationName: 'empty', elapsedTime: 0,
pseudoElement: "" }],
"events at start of animation updated to use " +
"empty keyframes rule");
done_div();
});
//----------------------------------------------------------------------
//
// Helper functions from test_animations.html