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

Allows amp-img.js to check if it has a blurred placeholder child. #17132

Merged
merged 25 commits into from Aug 13, 2018

Conversation

addieachan
Copy link
Contributor

@addieachan
Copy link
Contributor Author

I know there are some lint errors that I will try and ask @rsimha about. Just wanted to get out the basic implementation

@@ -47,6 +48,12 @@ export class AmpImg extends BaseElement {

/** @private {?UnlistenDef} */
this.unlistenError_ = null;

/** @private @const {boolean} */
this.useBlurryPlaceholder_ = isExperimentOn(this.win, 'blurry-placeholder');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ivar not needed, just inline this call below.

const placeholder = this.getPlaceholder();
if (placeholder) {
const classes = placeholder.getAttribute('class').split(' ');
if (classes.find(function(el) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use classList.contains() instead.

if (classes.find(function(el) {
return el == 'i-amphtml-blur';
})) {
this.hasBlurredPlaceHolder_ = true;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add the code that actually uses this boolean. This PR is still quite small.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So just to clarify, that would mean add the code to trigger the loading fade-in animation when the image is loaded, right?Or is it something else?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

@@ -16,6 +16,8 @@

import {BaseElement} from '../src/base-element';
import {dev} from '../src/log';
import {isExperimentOn} from '../src/experiments';
import {guaranteeSrcForSrcsetUnsupportedBrowsers} from '../src/utils/img';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and other calls to guaranteeSrcForSrcsetUnsupportedBrowsers were not implemented by me, but are in the current version of amp-img.js. It's just a two lines, but idk how important it is to ensure the changes that show up are only yours, so just wanted to point it out if you had advice on fixing it. Otherwise, I will try and solution as well!

Edit: I think it was resolved thanks to @choumx 's help!

impl.buildCallback();
impl.layoutCallback();
expect(impl.element.classList.contains(
'i-amphtml-blur-loading')).to.be.true;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if these expect statements are hard to read -I had to break them up to satisfy the lint rule of being <80 chars on a line. If there is a better place for me to break it/an alternate solution, then please let me know!

@addieachan
Copy link
Contributor Author

@choumx, @gmajoulet , @alanorozco I have the amp-img side of things functional for the image blur, and I feel like this PR is at a stage for a preliminary look so PTAL!

@@ -26,7 +27,7 @@ import {registerElement} from '../src/service/custom-element-registry';
* @type {!Array<string>}
*/
const ATTRIBUTES_TO_PROPAGATE = ['alt', 'title', 'referrerpolicy', 'aria-label',
'aria-describedby', 'aria-labelledby','srcset', 'src', 'sizes'];
'aria-describedby', 'aria-labelledby','srcset', 'src', 'sizes', 'blur'];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert this change.

@@ -146,7 +156,7 @@ export class AmpImg extends BaseElement {
layoutCallback() {
this.initialize_();
const img = dev().assertElement(this.img_);
this.unlistenLoad_ = listen(img, 'load', () => this.hideFallbackImg_());
this.unlistenLoad_ = listen(img, 'load', () => this.renderImg_());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: onImgLoad_

@@ -167,6 +177,31 @@ export class AmpImg extends BaseElement {
return true;
}

/**
* Checks to see if there is a placeholder that is blurred

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be more specific. "If a blurry placeholder exists, hides the child until it's fully loaded."

}

/**
* @private

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Move annotations like @private below the comment.

css/amp.css Outdated
@@ -392,7 +410,7 @@ i-amphtml-sizer {
display: block;
}

.i-amphtml-element > [placeholder].hidden,
.i-amphtml-element:not(.i-amphtml-blur-loaded, .i-amphtml-blur-loading) > [placeholder].hidden,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment here explaining this.

impl.unlayoutCallback();
});

function getImgWithBlur(addPlacholder, addBlurClass) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: addPlaceholder

*/
renderImg_() {
if (this.hasBlurredPlaceHolder_) {
this.element.classList.add('i-amphtml-blur-loaded');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove i-amphtml-blur-loading right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah! I think I thought that the blur would disappear before the fade in would complete, but I realize that wasn't the case. Thanks!

* placeholder if one exists, or hiding the fallback if not
*/
renderImg_() {
if (this.hasBlurredPlaceHolder_) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You won't need this ivar if you just check this.classList.contains('i-amphtml-blur-loading').

const loadEvent = createCustomEvent(iframe.win, 'load');
impl.img_.dispatchEvent(loadEvent);
expect(impl.element.classList.contains(
'i-amphtml-blur-loaded')).to.be.true;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try using expect(el.classList).includes('i-amphtml-blur-loaded'). This will yield a better error message on failure -- currently it'll look like "expected false to be true".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense! As a tangentially related question, is there a stylistic preference between using 'el.classList' vs 'impl.element.classList'?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a reference to the element so just use it. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok cool!

Also btw, when I tried the 'expect(el.classList).includes('i-amphtml-blur-loaded')', the test failed because it said the object being tested was a domtokenlist which wasn't supported by the "includes()" call. Instead, I just did 'expect(el).to.have.class('i-amphtml-blur-loaded')' which seems to give the right error. Please let me know if this is a problem!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even better!

impl = new AmpImg(el);
impl.buildCallback();
impl.layoutCallback();
expect(impl.hasBlurredPlaceHolder_).to.be.false;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check CSS classes instead of ivars.

@addieachan
Copy link
Contributor Author

@choumx, @alanorozco, and @gmajoulet PTAL as I talked to Will on Friday, and over the weekend, I consolidated the blur implementation since we noticed that it was actually being rendered on top of the image so it had to be faded out. I also had to remove one test (that was testing the different classes given to the amp-img regarding hiding/unhiding of the image as it loads since it is now unnecessary to hide it). If there are any new tests that need to be added, then please let me know :)

css/amp.css Outdated
@@ -780,4 +791,4 @@ amp-story .i-amphtml-loader {
*/
[amp-fx^="fly-in"] {
visibility: hidden;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restore trailing newline.

css/amp.css Outdated
transition: opacity 1s 1s !important;
}

.i-amphtml-blur{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Space before {.

css/amp.css Outdated
@@ -23,7 +23,7 @@
* to be computed to `auto` on both the `body` and `html` elements so they both
* potentially get a scrolling box. See #3108 for more details.
*/
html {
html {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert.

css/amp.css Outdated
@@ -167,6 +167,16 @@ html.i-amphtml-singledoc.i-amphtml-ios-embed-sd > body {
display: inline-block;
}

.i-amphtml-fade-out .i-amphtml-blur {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i-amphtml-layout class is added after layoutCallback(), so can we avoid a custom CSS class altogether and do instead:

.i-amphtml-layout > .i-amphtml-blur {
  opacity: 0 !important;
}

.i-amphtml-blur {
  transition: ...;
  filter: ...;
  transform: ...;
}

css/amp.css Outdated
@@ -167,6 +167,16 @@ html.i-amphtml-singledoc.i-amphtml-ios-embed-sd > body {
display: inline-block;
}

.i-amphtml-fade-out .i-amphtml-blur {
opacity: 0 !important;
transition: opacity 1s 1s !important;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have a transition-delay of 1s? Also, let's use ease timing function.

transition: opacity 1s ease !important;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I did it because I thought it looked better on fast connections where the image looked like it flashed since the placeholder wasn't seen. But the 'ease' function solves that problem so thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not aware of other AMP animation curves, but the material guidelines would recommend a "decelerating curve" here, either ease-out or the actual material easing: cubic-bezier(0.0, 0.0, 0.2, 1).

const placeholder = this.getPlaceholder();
if (isExperimentOn(this.win, 'blurry-placeholder') && placeholder &&
placeholder.classList.contains('i-amphtml-blur')) {
this.element.classList.add('i-amphtml-fade-out');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my comment below, we actually might not need any changes here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So when I take out the overriding of fistLayoutCompleted(), the blur doesn't show up since the togglePlaceholder(false) is hiding it quickly not allowing for the fade out. Is it ok to keep the overriding just to make sure images with blurred placeholders don't call togglePlaceholder(false)?

firstLayoutCompleted() {
const placeholder = this.getPlaceholder();
if (isExperimentOn(this.win, 'blurry-placeholder') && placeholder &&
placeholder.classList.contains('i-amphtml-blur')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused, how is i-amphtml-blur set?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The devtool/cache generates it.

/** @override */
firstLayoutCompleted() {
const placeholder = this.getPlaceholder();
if (isExperimentOn(this.win, 'blurry-placeholder') && placeholder &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny improvement: isExperimentOn could read the cookies, which is an operation that's a bit more expensive than checking a class. Maybe it'd be worth moving the isExperimentOn() check last?

css/amp.css Outdated
@@ -167,6 +167,16 @@ html.i-amphtml-singledoc.i-amphtml-ios-embed-sd > body {
display: inline-block;
}

.i-amphtml-fade-out .i-amphtml-blur {
opacity: 0 !important;
transition: opacity 1s 1s !important;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not aware of other AMP animation curves, but the material guidelines would recommend a "decelerating curve" here, either ease-out or the actual material easing: cubic-bezier(0.0, 0.0, 0.2, 1).

firstLayoutCompleted() {
const placeholder = this.getPlaceholder();
if (!(placeholder && placeholder.classList.contains('i-amphtml-blur') &&
isExperimentOn(this.win, 'blurry-placeholder'))) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Flip the conditional to avoid the need for !.

css/amp.css Outdated
@@ -167,6 +167,13 @@ html.i-amphtml-singledoc.i-amphtml-ios-embed-sd > body {
display: inline-block;
}


.i-amphtml-blur{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Space before {.

* @param {boolean} addBlurClass Whether the child should have the
* class that allows it to be a blurred placeholder.
*/
function getImgWithBlur(addPlaceholder, addBlurClass) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test code would be easier to follow if this helper was idempotent. I.e. change it to return the AmpImg instead of modifying existing variables.

it('my test', () => {
  const impl = getImgWithBlur(...);
  const el = impl.element;
});

}
el.appendChild(img);
el.getResources = () => Services.resourcesForDoc(document);
el.getPlaceholder = sandbox.stub();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this return img if addPlaceholder is true? I.e. () => img.

impl.layoutWidth_ = 200;
}

it('should correctly detect if a blurry image child exists', () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test name is out of date. How about "should set blurry placeholder opacity to 0 on image load"?

impl.buildCallback();
impl.layoutCallback();
impl.firstLayoutCompleted();
expect(img.style.opacity).to.be.equal('');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also verify that togglePlaceholder(false) continues to be called in the default case, here and below.

expect(img.style.opacity).to.be.equal('');
const loadEvent = createCustomEvent(iframe.win, 'load');
impl.img_.dispatchEvent(loadEvent);
impl.firstLayoutCompleted();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispatching the event is no longer necessary. You can probably just remove this test.

@addieachan
Copy link
Contributor Author

PTAL whenever you guys are free!

css/amp.css Outdated
filter: blur(20px) !important;
transform: scale(1.1) !important;
transition: opacity 1s ease !important;
transition: opacity 1s cubic-bezier(0.0, 0.0, 0.2, 1) !important;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why cubic-bezier and not ease?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually asked for it in the previous review, that's the material deceleration curve.
However, I believe the animation should not last more than 300ms

if (addPlaceholder) {
img.setAttribute('placeholder', '');
el.getPlaceholder = () => {return img;};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: You can use closure shorthand here. e.getPlaceholder = () => img;

impl.layoutWidth_ = 200;

impl.togglePlaceholder = sandbox.stub();
toggleExperiment(impl.win, 'blurry-placeholder', true, true);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only need to call this once in beforeEach. impl.win == window, so just do:

toggleExperiment(window, 'blurry-placeholder', true, true);

impl.buildCallback();
impl.layoutCallback();
impl.firstLayoutCompleted();
let el = impl.element;
let img = el.firstChild;
expect(img.style.opacity).to.equal('0');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an expectation that togglePlaceholder was not called here.

el = impl.element;
img = el.firstChild;
expect(impl.togglePlaceholder).to.have.been.calledWith(false);
impl.unlayoutCallback();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why call unlayoutCallback here?

el.appendChild(img);
el.getResources = () => Services.resourcesForDoc(document);
const impl = new AmpImg(el);
impl.layoutWidth_ = 200;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stub getLayoutWidth() instead.

sandbox.stub(impl, 'getLayoutWidth').returns(200);

.i-amphtml-blur {
filter: blur(20px) !important;
transform: scale(1.1) !important;
transition: opacity 0.3s cubic-bezier(0.0, 0.0, 0.2, 1) !important;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided to use this cubic-bezier as it is what is suggested for easing elements in per the material design doc:
https://material.io/design/motion/speed.html#easing

@addieachan
Copy link
Contributor Author

addieachan commented Aug 8, 2018

If one of you with merge access can help me merge this, that would be greatly appreciated! (Assuming everything looks ready - otherwise PTAL again)

el = impl.element;
img = el.firstChild;
expect(impl.togglePlaceholder).to.have.been.calledWith(false);
impl.unlayoutCallback();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed comment from earlier: Why is unlayoutCallback called here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry! I just saw it in the aftereach of other tests, so I included it in my aftereach method, and then I took out the afterEach because I only had one test method after deleting the other two, so I included the code here. I'll take it out though!

@dreamofabear dreamofabear merged commit 57058c7 into ampproject:master Aug 13, 2018
Enriqe pushed a commit to Enriqe/amphtml that referenced this pull request Nov 28, 2018
…pproject#17132)

* Redid blur detection changes with updated amp-img file

* Fixed stylistic issues

* Lint errors

* Got the animation of the fading in of the rendered image

* Fixed wrong variable name

* Fix small changes

* fixed lint errors

* Added tests

* Fixed lint errors

* Re-add accidentally deleted line

* Fixed some of the requested changes. Animation changes are a WIP

* Consolidated changes but need to reconfigure tests

* Took out comments and debugging code

* Rewrote tests to reflect implementation change

* Consolidated changes further and redid tests

* Fixed small style changes

* Fixed tests and small fixes

* Fixed javadoc

* Fixed style things

* Changed bundle size

* Took out unnecessary unlayoutCallback() call
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants