Skip to content

Commit

Permalink
Animations: support style keyframes (#9647)
Browse files Browse the repository at this point in the history
* Animations: support style keyframes

* docs

* review fixes

* lints
  • Loading branch information
Dima Voytenko committed Jun 1, 2017
1 parent 09d6a8c commit cfd3a87
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 10 deletions.
10 changes: 7 additions & 3 deletions examples/animations.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@
background: rgba(0, 0, 0, 0.25);
border-radius: 50%;
}

@keyframes rotate {
100% {
transform: rotate(calc(var(--angle) * -3));
}
}
</style>
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
Expand Down Expand Up @@ -82,9 +88,7 @@
"selector": ".rot-image",
"delay": "var(--delay)",
"easing": "cubic-bezier(0,0,.21,1)",
"keyframes": {
"transform": "rotate(calc(var(--angle) * -2))"
}
"keyframes": "rotate"
},
{
"animation": "anim2",
Expand Down
2 changes: 1 addition & 1 deletion extensions/amp-animation/0.1/test/test-amp-animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ describes.sandboxed('AmpAnimation', {}, () => {
it('should create runner with args', () => {
const args = {};
anim.triggered_ = true;
createRunnerStub.restore();
createRunnerStub./*OK*/restore();
const stub = sandbox.stub(Builder.prototype, 'createRunner',
() => runner);
return anim.startOrResume_(args).then(() => {
Expand Down
37 changes: 36 additions & 1 deletion extensions/amp-animation/0.1/test/test-web-animations.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
WebAnimationPlayState,
} from '../web-animation-types';
import {isArray, isObject} from '../../../../src/types';
import {poll} from '../../../../testing/iframe';
import {user} from '../../../../src/log';
import * as sinon from 'sinon';

Expand Down Expand Up @@ -305,7 +306,7 @@ describes.realWin('MeasureScanner', {amp: 1}, env => {
'--child5': 'var(--child6)', // Reverse order dependency.
'--child6': '23px',
keyframes: {
transform: 'translate()',
transform: 'translate(var(--child3), var(--child4))',
},
}],
});
Expand All @@ -320,6 +321,8 @@ describes.realWin('MeasureScanner', {amp: 1}, env => {
'--child5': '23px',
'--child6': '23px',
});
expect(requests[0].keyframes.transform[1])
.to.equal('translate(11px,22px)');
});

it('should accept keyframe animation', () => {
Expand Down Expand Up @@ -516,6 +519,38 @@ describes.realWin('MeasureScanner', {amp: 1}, env => {
expect(keyframes.opacity).to.jsonEqual(['0', '0.525']);
});

it('should fail when cannot discover style keyframes', () => {
expect(() => scan({target: target1, keyframes: 'keyframes1'}))
.to.throw(/Keyframes not found/);
});

it('should discover style keyframes', () => {
const name = 'keyframes1';
const css = 'from{opacity: 0} to{opacity: 1}';
const style = doc.createElement('style');
style.setAttribute('amp-custom', '');
style.textContent =
`@-ms-keyframes ${name} {${css}}` +
`@-moz-keyframes ${name} {${css}}` +
`@-webkit-keyframes ${name} {${css}}` +
`@keyframes ${name} {${css}}`;
doc.head.appendChild(style);
return poll('wait for style', () => {
for (let i = 0; i < doc.styleSheets.length; i++) {
if (doc.styleSheets[i].ownerNode == style) {
return true;
}
}
return false;
}).then(() => {
const keyframes = scan({target: target1, keyframes: name})[0].keyframes;
expect(keyframes).to.jsonEqual([
{offset: 0, opacity: '0'},
{offset: 1, opacity: '1'},
]);
});
});

it('should check media in top animation', () => {
const requests = scan({
duration: 500,
Expand Down
2 changes: 1 addition & 1 deletion extensions/amp-animation/0.1/web-animation-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export let WebCompAnimationDef;
* @mixes WebAnimationTimingDef
* @mixes WebAnimationMediaDef
* @typedef {{
* keyframes: !WebKeyframesDef,
* keyframes: (string|!WebKeyframesDef),
* }}
*/
export let WebKeyframeAnimationDef;
Expand Down
13 changes: 11 additions & 2 deletions extensions/amp-animation/0.1/web-animations.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {ScrollboundPlayer} from './scrollbound-player';
import {assertHttpsUrl, resolveRelativeUrl} from '../../../src/url';
import {closestBySelector} from '../../../src/dom';
import {dev, user} from '../../../src/log';
import {extractKeyframes} from './keyframes-extractor';
import {getMode} from '../../../src/mode';
import {getVendorJsPropertyName, computedStyle} from '../../../src/style';
import {isArray, isObject, toArray} from '../../../src/types';
Expand Down Expand Up @@ -354,7 +355,7 @@ class Scanner {
export class Builder {
/**
* @param {!Window} win
* @param {!Node} rootNode
* @param {!Document|!ShadowRoot} rootNode
* @param {string} baseUrl
* @param {!../../../src/service/vsync-impl.Vsync} vsync
* @param {!../../../src/service/resources-impl.Resources} resources
Expand Down Expand Up @@ -581,6 +582,14 @@ export class MeasureScanner extends Scanner {
* @private
*/
createKeyframes_(target, spec) {
if (typeof spec.keyframes == 'string') {
// Keyframes name to be extracted from `<style>`.
const keyframes = extractKeyframes(this.css_.rootNode_, spec.keyframes);
user().assert(keyframes,
`Keyframes not found in stylesheet: "${spec.keyframes}"`);
return /** @type {!WebKeyframesDef} */ (keyframes);
}

if (isObject(spec.keyframes)) {
// Property -> keyframes form.
// The object is cloned, while properties are verified to be
Expand Down Expand Up @@ -852,7 +861,7 @@ export class MeasureScanner extends Scanner {
class CssContextImpl {
/**
* @param {!Window} win
* @param {!Node} rootNode
* @param {!Document|!ShadowRoot} rootNode
* @param {string} baseUrl
*/
constructor(win, rootNode, baseUrl) {
Expand Down
34 changes: 32 additions & 2 deletions extensions/amp-animation/amp-animation.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ Animation components inherit timing properties specified for the top-level anima

### Keyframes

Keyframes can be specified in numerous ways described in the [keyframes section](https://www.w3.org/TR/web-animations/#processing-a-keyframes-argument) of the Web Animations spec.
Keyframes can be specified in numerous ways described in the [keyframes section](https://www.w3.org/TR/web-animations/#processing-a-keyframes-argument) of the Web Animations spec or as a string refering to the `@keyframes` name in the CSS.

Some typical examples of keyframes definitions are below.

Expand Down Expand Up @@ -282,6 +282,36 @@ For additional keyframes formats refer to [Web Animations spec](https://www.w3.o

The property values allow any valid CSS values, including `calc()`, `var()` and other CSS expressions.

#### Keyframes from CSS

Another way to specify keyframes is in the document's stylesheet (`<style>` tag) as `@keyframes` CSS rule. For instance:
```html
<style amp-custom>
@keyframes keyframes1 {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>

<amp-animation layout="nodisplay">
<script type="application/json">
{
"duration": "1s",
"keyframes": "keyframes1"
}
</script>
</amp-animation>
```

CSS `@keyframes` are mostly equivalent to inlining keyframes definition in the JSON per [Web Animations spec](https://www.w3.org/TR/web-animations/#processing-a-keyframes-argument). However, there are some nuances:
- For broad-platform support, vendor prefixes, e.g. `@-ms-keyframes {}` or `-moz-transform` may be needed. Vendor prefixes are not needed and not allowed in the JSON format, but in CSS they could be necessary.
- Platforms that do not support `calc()` and `var()` will not be able to take advantage of `amp-animation` polyfills when keyframes are specified in CSS. It's thus recommended to always include fallback values in CSS.
- CSS extensions such as [`width()`, `height()` and `rand()`](#css-extensions) cannot be used in CSS.


#### Whitelisted properties for keyframes

Expand Down Expand Up @@ -427,7 +457,7 @@ Both `var()` and `calc()` polyfilled on platforms that do not directly support t
</amp-animation>
```

Animation components can specify their own variables as `--var-name` fields. These variables are propagated into nested animations and override variables of target elements specified via `<style>`. `var()` expressions first try to resolve variable values specified in the animations and then by querying target styles.
Animation components can specify their own variables as `--var-name` fields. These variables are propagated into nested animations and override variables of target elements specified via stylesheet (`<style>` tag). `var()` expressions first try to resolve variable values specified in the animations and then by querying target styles.


### CSS extensions
Expand Down

0 comments on commit cfd3a87

Please sign in to comment.