diff --git a/examples/animations.amp.html b/examples/animations.amp.html
index 5805bd308909..7a9b301a8bf0 100644
--- a/examples/animations.amp.html
+++ b/examples/animations.amp.html
@@ -122,6 +122,17 @@
"--delay": "rand(0.1s, var(--max-delay))",
"delay": "var(--delay)",
"direction": "normal",
+ "subtargets": [
+ {
+ "index": 0,
+ "direction": "reverse"
+ },
+ {
+ "selector": ".antigrav",
+ "direction": "reverse",
+ "--delay": "0s"
+ }
+ ],
"keyframes": {"transform": "translateY(120vh)"}
}
@@ -130,7 +141,7 @@
-
+
diff --git a/extensions/amp-animation/0.1/test/test-web-animations.js b/extensions/amp-animation/0.1/test/test-web-animations.js
index 942fd8dca51f..f52bf020e604 100644
--- a/extensions/amp-animation/0.1/test/test-web-animations.js
+++ b/extensions/amp-animation/0.1/test/test-web-animations.js
@@ -325,6 +325,77 @@ describes.realWin('MeasureScanner', {amp: 1}, env => {
.to.equal('translate(11px,22px)');
});
+ it('should override vars in subtargets with index', () => {
+ const requests = scan({
+ '--parent1': '11px',
+ '--parent2': '12px',
+ animations: [{
+ selector: '.target',
+ '--child1': '21px',
+ '--parent2': '22px', // Override parent.
+ '--child2': 'var(--child1)',
+ '--child3': 'var(--parent1)',
+ '--child4': 'var(--parent2)',
+ '--child5': 'var(--child6)', // Reverse order dependency.
+ '--child6': '23px',
+ subtargets: [
+ // By index.
+ {
+ index: 0,
+ '--child6': '31px',
+ },
+ {
+ index: 1,
+ '--child6': '32px',
+ },
+ // By selector.
+ {
+ selector: '#target1',
+ '--child1': '33px',
+ },
+ {
+ selector: '#target2',
+ '--child1': '34px',
+ },
+ {
+ selector: 'div',
+ '--child2': '35px',
+ },
+ ],
+ keyframes: {
+ transform: 'translate(var(--child6), var(--child1))',
+ },
+ }],
+ });
+ expect(requests).to.have.length(2);
+
+ // `#target1`
+ expect(requests[0].vars).to.jsonEqual({
+ '--parent1': '11px',
+ '--parent2': '22px',
+ '--child1': '33px', // Overriden via `#target1`
+ '--child2': '35px', // Overriden via `div`
+ '--child3': '11px',
+ '--child4': '22px',
+ '--child5': '31px', // Overriden via `index: 0`
+ '--child6': '31px', // Overriden via `index: 0`
+ });
+ expect(requests[0].keyframes.transform[1]).to.equal('translate(31px,33px)');
+
+ // `#target2`
+ expect(requests[1].vars).to.jsonEqual({
+ '--parent1': '11px',
+ '--parent2': '22px',
+ '--child1': '34px', // Overriden via `#target2`
+ '--child2': '35px', // Overriden via `div`
+ '--child3': '11px',
+ '--child4': '22px',
+ '--child5': '32px', // Overriden via `index: 1`
+ '--child6': '32px', // Overriden via `index: 1`
+ });
+ expect(requests[1].keyframes.transform[1]).to.equal('translate(32px,34px)');
+ });
+
it('should accept keyframe animation', () => {
const requests = scan({
target: target1,
diff --git a/extensions/amp-animation/0.1/web-animation-types.js b/extensions/amp-animation/0.1/web-animation-types.js
index 49dae0f1f61e..a41b00213cc0 100644
--- a/extensions/amp-animation/0.1/web-animation-types.js
+++ b/extensions/amp-animation/0.1/web-animation-types.js
@@ -24,6 +24,7 @@ export let WebAnimationDef;
/**
* @mixes WebAnimationSelectorDef
* @mixes WebAnimationTimingDef
+ * @mixes WebAnimationVarsDef
* @mixes WebAnimationMediaDef
* @typedef {{
* animations: !Array,
@@ -35,6 +36,7 @@ export let WebMultiAnimationDef;
/**
* @mixes WebAnimationSelectorDef
* @mixes WebAnimationTimingDef
+ * @mixes WebAnimationVarsDef
* @mixes WebAnimationMediaDef
* @typedef {{
* animation: string,
@@ -46,6 +48,7 @@ export let WebCompAnimationDef;
/**
* @mixes WebAnimationSelectorDef
* @mixes WebAnimationTimingDef
+ * @mixes WebAnimationVarsDef
* @mixes WebAnimationMediaDef
* @typedef {{
* keyframes: (string|!WebKeyframesDef),
@@ -79,6 +82,16 @@ export let WebKeyframesDef;
export let WebAnimationTimingDef;
+/**
+ * Indicates an extension to a type that allows specifying vars. Vars are
+ * specified as properties with the name in the format of `--varName`.
+ *
+ * @mixin
+ * @typedef {Object}
+ */
+export let WebAnimationVarsDef;
+
+
/**
* Defines media parameters for an animation.
*
@@ -94,11 +107,24 @@ export let WebAnimationMediaDef;
* @typedef {{
* target: (!Element|undefined),
* selector: (string|undefined),
+ * subtargets: (!Array|undefined),
* }}
*/
export let WebAnimationSelectorDef;
+/**
+ * @mixes WebAnimationTimingDef
+ * @mixes WebAnimationVarsDef
+ * @typedef {{
+ * matcher: (function(!Element, number):boolean|undefined),
+ * index: (number|undefined),
+ * selector: (string|undefined),
+ * }}
+ */
+export let WebAnimationSubtargetDef;
+
+
/**
* See https://developer.mozilla.org/en-US/docs/Web/API/Animation/playState
* @enum {string}
diff --git a/extensions/amp-animation/0.1/web-animations.js b/extensions/amp-animation/0.1/web-animations.js
index bd02cbe1ac95..10f9f39e2c51 100644
--- a/extensions/amp-animation/0.1/web-animations.js
+++ b/extensions/amp-animation/0.1/web-animations.js
@@ -18,7 +18,7 @@ import {CssNumberNode, CssTimeNode, isVarCss} from './css-expr-ast';
import {Observable} from '../../../src/observable';
import {ScrollboundPlayer} from './scrollbound-player';
import {assertHttpsUrl, resolveRelativeUrl} from '../../../src/url';
-import {closestBySelector} from '../../../src/dom';
+import {closestBySelector, matches} from '../../../src/dom';
import {dev, user} from '../../../src/log';
import {extractKeyframes} from './keyframes-extractor';
import {getMode} from '../../../src/mode';
@@ -29,6 +29,8 @@ import {parseCss} from './css-expr';
import {
WebAnimationDef,
WebAnimationPlayState,
+ WebAnimationSelectorDef,
+ WebAnimationSubtargetDef,
WebAnimationTimingDef,
WebAnimationTimingDirection,
WebAnimationTimingFill,
@@ -690,12 +692,16 @@ export class MeasureScanner extends Scanner {
(spec.target || spec.selector) ?
this.resolveTargets_(spec) :
[null];
- targets.forEach(target => {
+ targets.forEach((target, index) => {
this.target_ = target || prevTarget;
this.css_.withTarget(this.target_, () => {
- this.vars_ = this.mergeVars_(spec, prevVars);
+ const subtargetSpec =
+ this.target_ ?
+ this.matchSubtargets_(this.target_, index, spec) :
+ spec;
+ this.vars_ = this.mergeVars_(subtargetSpec, prevVars);
this.css_.withVars(this.vars_, () => {
- this.timing_ = this.mergeTiming_(spec, prevTiming);
+ this.timing_ = this.mergeTiming_(subtargetSpec, prevTiming);
callback();
});
});
@@ -739,6 +745,59 @@ export class MeasureScanner extends Scanner {
return targets;
}
+ /**
+ * @param {!Element} target
+ * @param {number} index
+ * @param {!WebAnimationSelectorDef} spec
+ * @return {!WebAnimationSelectorDef}
+ */
+ matchSubtargets_(target, index, spec) {
+ if (!spec.subtargets || spec.subtargets.length == 0) {
+ return spec;
+ }
+ const result = map(spec);
+ spec.subtargets.forEach(subtargetSpec => {
+ const matcher = this.getMatcher_(subtargetSpec);
+ if (matcher(target, index)) {
+ Object.assign(result, subtargetSpec);
+ }
+ });
+ return result;
+ }
+
+ /**
+ * @param {!WebAnimationSubtargetDef} spec
+ * @return {function(!Element, number):boolean}
+ */
+ getMatcher_(spec) {
+ if (spec.matcher) {
+ return spec.matcher;
+ }
+ user().assert(
+ (spec.index !== undefined || spec.selector !== undefined) &&
+ (spec.index === undefined || spec.selector === undefined),
+ 'Only one "index" or "selector" must be specified');
+
+ let matcher;
+ if (spec.index !== undefined) {
+ // Match by index, e.g. `index: 0`.
+ const specIndex = Number(spec.index);
+ matcher = (target, index) => index === specIndex;
+ } else {
+ // Match by selector, e.g. `:nth-child(2n+1)`.
+ const specSelector = /** @type {string} */ (spec.selector);
+ matcher = target => {
+ try {
+ return matches(target, specSelector);
+ } catch (e) {
+ throw user().createError(
+ `Bad subtarget selector: "${specSelector}"`, e);
+ }
+ };
+ }
+ return spec.matcher = matcher;
+ }
+
/**
* Merges vars by defaulting values from the previous vars.
* @param {!Object
} newVars
diff --git a/extensions/amp-animation/amp-animation.md b/extensions/amp-animation/amp-animation.md
index 360f00837d77..3d2abb86d1d3 100644
--- a/extensions/amp-animation/amp-animation.md
+++ b/extensions/amp-animation/amp-animation.md
@@ -88,6 +88,7 @@ and is comprised of:
"media": "(min-width:300px)",
// Variables
// Timing properties
+ // Subtargets
...
"keyframes": []
}
@@ -214,6 +215,35 @@ An example of timing properties in JSON:
Animation components inherit timing properties specified for the top-level animation.
+
+### Subtargets
+
+Everywhere where `selector` can be specified, it's possible to also specify `subtargets: []`. Subtargets can override timing properties or variables defined in the animation for specific subtargets indicated via either an index or a CSS selector.
+
+For instance:
+```text
+{
+ "selector": ".target",
+ "delay": 100,
+ "--y": "100px",
+ "subtargets": [
+ {
+ "index": 0,
+ "delay": 200,
+ },
+ {
+ "selector": ":nth-child(2n+1)",
+ "--y": "200px"
+ }
+ ]
+}
+```
+
+In this example, by default all targets matched by the ".target" have delay of 100ms and "--y" of 100px. However, the first target (`index: 0`) is overriden to have delay of 200ms; and odd targets are overriden to have "--y" of 200px.
+
+Notice, that multiple subtargets can match one target element.
+
+
### 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 or as a string refering to the `@keyframes` name in the CSS.