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.