Skip to content
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

✨ amp-animation: add x(), y(), rescale by scope element's transform #28146

Merged
merged 14 commits into from
May 7, 2020
50 changes: 50 additions & 0 deletions examples/amp-story/amp-story-animation.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
font-family: sans-serif;
font-size: 18px;
}
.box {
background: blue;
width: 80px;
height: 100px;
margin: 0 0 20px;
}
</style>
</head>
<body>
Expand Down Expand Up @@ -68,6 +74,50 @@
</amp-story-animation>
</amp-story-page>

<amp-story-page id="page2">
<amp-story-grid-layer template="vertical">
<div class="box animate-bottom-right"></div>
<div class="box animate-bottom"></div>
<div class="box animate-right"></div>
</amp-story-grid-layer>
<amp-story-animation layout="nodisplay">
<script type="application/json">
[
{
"selector": ".animate-bottom",
"duration": "2s",
"iterations": "infinite",
"keyframes": [
{"transform": "none"},
{"transform": "translate(0, calc(100vh - y() - height()))"},
{"transform": "none"}
]
},
{
"selector": ".animate-right",
"duration": "2s",
"iterations": "infinite",
"keyframes": [
{"transform": "none"},
{"transform": "translate(calc(100vw - x() - width()), 0)"},
{"transform": "none"}
]
},
{
"selector": ".animate-bottom-right",
"duration": "2s",
"iterations": "infinite",
"keyframes": [
{"transform": "none"},
{"transform": "translate(calc(100vw - x() - width()), calc(100vh - y() - height()))"},
{"transform": "none"}
]
}
]
</script>
</amp-story-animation>
</amp-story-page>

<amp-story-bookend
src="bookendv1.json"
layout="nodisplay"
Expand Down
54 changes: 53 additions & 1 deletion extensions/amp-animation/0.1/parsers/css-expr-ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
const FINAL_URL_RE = /^(data|https)\:/i;
const DEG_TO_RAD = (2 * Math.PI) / 360;
const GRAD_TO_RAD = Math.PI / 200;
const VAR_CSS_RE = /(calc|min|max|clamp|var|url|rand|index|width|height|num|length)\(/i;
const VAR_CSS_RE = /(calc|min|max|clamp|var|url|rand|index|width|height|num|length|([^a-z]|^)x|([^a-z]|^)y)\(/i;
alanorozco marked this conversation as resolved.
Show resolved Hide resolved
const NORM_CSS_RE = /\d(%|em|rem|vw|vh|vmin|vmax|s|deg|grad)/i;
const INFINITY_RE = /^(infinity|infinite)$/i;
const BOX_DIMENSIONS = ['h', 'w', 'h', 'w'];
Expand Down Expand Up @@ -100,6 +100,20 @@ export class CssContext {
*/
getElementSize(unusedSelector, unusedSelectionMethod) {}

/**
* Returns the current element's position.
* @return {!{x: number, y: number}}
*/
getCurrentElementPosition() {}
alanorozco marked this conversation as resolved.
Show resolved Hide resolved

/**
* Returns the specified element's position.
* @param {string} unusedSelector
* @param {?string} unusedSelectionMethod
* @return {!{x: number, y: number}}
*/
getElementPosition(unusedSelector, unusedSelectionMethod) {}

/**
* Returns the dimension: "w" for width or "h" for height.
* @return {?string}
Expand Down Expand Up @@ -882,6 +896,44 @@ export class CssDimSizeNode extends CssNode {
}
}

/**
* AMP-specific `x()` and `y()` functions.
*/
export class CssDimPosNode extends CssNode {
/**
* @param {string} dim
* @param {?string=} opt_selector
* @param {?string=} opt_selectionMethod Either `undefined` or "closest".
*/
constructor(dim, opt_selector, opt_selectionMethod) {
super();
/** @const @private */
this.dim_ = dim;
/** @const @private */
this.selector_ = opt_selector || null;
/** @const @private */
this.selectionMethod_ = opt_selectionMethod || null;
}

/** @override */
css() {
throw noCss();
}

/** @override */
isConst() {
return false;
}

/** @override */
calc(context) {
const position = this.selector_
? context.getElementPosition(this.selector_, this.selectionMethod_)
: context.getCurrentElementPosition();
return new CssLengthNode(position[this.dim_], 'px');
alanorozco marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* AMP-specific `num()` function. Format is `num(value)`. Returns a numeric
* representation of the value. E.g. `11px` -> 11, `12em` -> 12, `10s` -> 10.
Expand Down
27 changes: 27 additions & 0 deletions extensions/amp-animation/0.1/parsers/css-expr-impl.jison
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ ident \-?[a-zA-Z_][\-a-zA-Z0-9_]*
{C}{I}{R}{C}{L}{E}\( return 'CIRCLE_START'
{E}{L}{L}{I}{P}{S}{E}\( return 'ELLIPSE_START'
{P}{O}{L}{Y}{G}{O}{N}\( return 'POLYGON_START'
{X}\( return 'X_START'
{Y}\( return 'Y_START'
{ident}\( return 'FUNCTION_START'
{ident} return 'IDENT'
\-\-{ident} return 'VAR_NAME';
Expand Down Expand Up @@ -262,6 +264,8 @@ function:
{$$ = $1;}
| dim_function
{$$ = $1;}
| pos_function
alanorozco marked this conversation as resolved.
Show resolved Hide resolved
{$$ = $1;}
| num_function
{$$ = $1;}
| rand_function
Expand Down Expand Up @@ -479,6 +483,29 @@ dim_function:
;


/**
* AMP-specific `x()` and `y()` functions:
* - `x(".selector")`
* - `y(".selector")`
* - `x(closest(".selector"))`
* - `y(closest(".selector"))`
*/
pos_function:
alanorozco marked this conversation as resolved.
Show resolved Hide resolved
X_START ')'
{$$ = new ast.CssDimPosNode('x');}
| Y_START ')'
{$$ = new ast.CssDimPosNode('y');}
| X_START STRING ')'
{$$ = new ast.CssDimPosNode('x', $2.slice(1, -1));}
| Y_START STRING ')'
{$$ = new ast.CssDimPosNode('y', $2.slice(1, -1));}
| X_START CLOSEST_START STRING ')' ')'
{$$ = new ast.CssDimPosNode('x', $3.slice(1, -1), 'closest');}
| Y_START CLOSEST_START STRING ')' ')'
{$$ = new ast.CssDimPosNode('y', $3.slice(1, -1), 'closest');}
;


/**
* AMP-specific `num()` function:
* - `num(10px)`
Expand Down
28 changes: 28 additions & 0 deletions extensions/amp-animation/0.1/test/test-css-expr-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ describes.sandboxed('CSS parse', {}, () => {
`, ${n.selectionMethod_}>`
);
}
if (n instanceof ast.CssDimPosNode) {
return (
`POS<${n.dim_}` +
`, ${n.selector_ ? '"' + n.selector_ + '"' : null}` +
`, ${n.selectionMethod_}>`
);
}
if (n instanceof ast.CssNumConvertNode) {
return `NUMC<${n.value_ ? pseudo(n.value_) : null}>`;
}
Expand Down Expand Up @@ -322,6 +329,27 @@ describes.sandboxed('CSS parse', {}, () => {
);
});

it('should parse a position function', () => {
// Current.
expect(parsePseudo('x()')).to.equal('POS<x, null, null>');
expect(parsePseudo('y()')).to.equal('POS<y, null, null>');

// Query.
expect(parsePseudo('x(".sel")')).to.equal('POS<x, ".sel", null>');
expect(parsePseudo('x(".sel > div")')).to.equal(
'POS<x, ".sel > div", null>'
);
expect(parsePseudo('y(".sel")')).to.equal('POS<y, ".sel", null>');

// Closest.
expect(parsePseudo('x(closest(".sel"))')).to.equal(
'POS<x, ".sel", closest>'
);
expect(parsePseudo('y(closest(".sel"))')).to.equal(
'POS<y, ".sel", closest>'
);
});

it('should parse a num-convert function', () => {
expect(parsePseudo('num(10)')).to.equal('NUMC<NUM<10>>');
expect(parsePseudo('num(10px)')).to.equal('NUMC<LEN<10 PX>>');
Expand Down
58 changes: 58 additions & 0 deletions extensions/amp-animation/0.1/test/test-css-expr-resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,64 @@ describes.sandboxed('CSS resolve', {}, (env) => {
});
});

describe('position', () => {
let positions;

beforeEach(() => {
positions = {
'null(.class)': {x: 111, y: 222},
'closest(.class > div)': {x: 112, y: 224},
};
context.getCurrentElementPosition = () => ({x: 110, y: 220});

context.getElementPosition = (selector, selectionMethod) =>
positions[`${selectionMethod}(${selector})`];
});

it('should always consider as non-const', () => {
expect(ast.isVarCss('x()', false)).to.be.true;
expect(ast.isVarCss('y()', false)).to.be.true;
expect(ast.isVarCss('x("")')).to.be.true;
expect(ast.isVarCss('y("")')).to.be.true;
});

it('should be always a non-const and no css', () => {
const node = new ast.CssDimPosNode('?');
expect(node.isConst()).to.be.false;
expect(() => node.css()).to.throw(/no css/);
});

it('should resolve x coord on the current node', () => {
const node = new ast.CssDimPosNode('x');
expect(node.calc(context).css()).to.equal('110px');
});

it('should resolve y coord on the current node', () => {
const node = new ast.CssDimPosNode('y');
expect(node.calc(context).css()).to.equal('220px');
});

it('should resolve x coord on the selected node', () => {
const node = new ast.CssDimPosNode('x', '.class');
expect(node.calc(context).css()).to.equal('111px');
});

it('should resolve y coord on the selected node', () => {
const node = new ast.CssDimPosNode('y', '.class');
expect(node.calc(context).css()).to.equal('222px');
});

it('should resolve x coord on the selected closest node', () => {
const node = new ast.CssDimPosNode('x', '.class > div', 'closest');
expect(node.calc(context).css()).to.equal('112px');
});

it('should resolve y coord on the selected closest node', () => {
const node = new ast.CssDimPosNode('y', '.class > div', 'closest');
expect(node.calc(context).css()).to.equal('224px');
});
});

describe('num-convert', () => {
it('should always consider as non-const', () => {
expect(ast.isVarCss('num(10px)')).to.be.true;
Expand Down