Skip to content

Commit

Permalink
Implement parallax effect extension (ampproject#7794)
Browse files Browse the repository at this point in the history
  • Loading branch information
cvializ authored and kmh287 committed Mar 13, 2017
1 parent 9f4082f commit 76ce6fb
Show file tree
Hide file tree
Showing 9 changed files with 615 additions and 0 deletions.
2 changes: 2 additions & 0 deletions build-system/dep-check-config.js
Expand Up @@ -157,6 +157,8 @@ exports.rules = [
'extensions/amp-youtube/0.1/amp-youtube.js->' +
'src/service/video-manager-impl.js',
'extensions/amp-a4a/0.1/amp-a4a.js->src/service/variable-source.js',
'extensions/amp-fx-parallax/0.1/amp-fx-parallax.js->' +
'src/service/parallax-impl.js',
],
},
{
Expand Down
85 changes: 85 additions & 0 deletions examples/article-parallax.amp.html
@@ -0,0 +1,85 @@
<!doctype html>
<html >
<head>
<meta charset="utf-8">
<title>AMP Article with parallax title</title>
<link rel="canonical" href="amps.html">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<script async src="https://cdn.ampproject.org/v0.js"></script>
<style amp-custom>
article, header h1 {
margin: 0px 10px;
}

header h1 {
position: absolute;
top: 25vh;
padding: 5px;
z-index: 1;
max-width: 70vw;
}

header h1 span {
background-color: black;
color: white;
line-height: 1.2em;
}

header amp-img img {
object-fit: cover;
}

/*
* Vertically center the tite within the image.
*/
.vertically-center {
transform: translateY(-50%);
}
</style>
<script async custom-element="amp-fx-parallax" src="https://cdn.ampproject.org/v0/amp-fx-parallax-0.1.js"></script>
<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>
</head>
<body>
<header>
<h1 amp-fx-parallax="1.7">
<div class="vertically-center">
<span>Lorem Ipsum Dolor Sit Amet Consectetur Adipiscing<span>
</div>
</h1>
<amp-img height="50vh" layout="fixed-height" src="img/hero@1x.jpg"></amp-img>
</header>

<article>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus
luctus nunc ut elit cursus, et imperdiet diam vehicula. Duis et nisi sed urna blandit bibendum et sit amet erat. Suspendisse
potenti. Curabitur consequat volutpat arcu nec elementum. Etiam a turpis ac libero varius condimentum. Maecenas sollicitudin
felis aliquam tortor vulputate, ac posuere velit semper.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus
luctus nunc ut elit cursus, et imperdiet diam vehicula. Duis et nisi sed urna blandit bibendum et sit amet erat. Suspendisse
potenti. Curabitur consequat volutpat arcu nec elementum. Etiam a turpis ac libero varius condimentum. Maecenas sollicitudin
felis aliquam tortor vulputate, ac posuere velit semper.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus
luctus nunc ut elit cursus, et imperdiet diam vehicula. Duis et nisi sed urna blandit bibendum et sit amet erat. Suspendisse
potenti. Curabitur consequat volutpat arcu nec elementum. Etiam a turpis ac libero varius condimentum. Maecenas sollicitudin
felis aliquam tortor vulputate, ac posuere velit semper.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus
luctus nunc ut elit cursus, et imperdiet diam vehicula. Duis et nisi sed urna blandit bibendum et sit amet erat. Suspendisse
potenti. Curabitur consequat volutpat arcu nec elementum. Etiam a turpis ac libero varius condimentum. Maecenas sollicitudin
felis aliquam tortor vulputate, ac posuere velit semper.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus
luctus nunc ut elit cursus, et imperdiet diam vehicula. Duis et nisi sed urna blandit bibendum et sit amet erat. Suspendisse
potenti. Curabitur consequat volutpat arcu nec elementum. Etiam a turpis ac libero varius condimentum. Maecenas sollicitudin
felis aliquam tortor vulputate, ac posuere velit semper.
</p>
</article>
</body>
</html>
24 changes: 24 additions & 0 deletions extensions/amp-fx-parallax/0.1/amp-fx-parallax.js
@@ -0,0 +1,24 @@
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {ampdocServiceFor} from '../../../src/ampdoc';
import {installParallaxForDoc} from '../../../src/service/parallax-impl';
import {onDocumentReady} from '../../../src/document-ready';

const ampdoc = ampdocServiceFor(AMP.win).getAmpDoc();
onDocumentReady(ampdoc.win.document, () => {
installParallaxForDoc(ampdoc.getRootNode());
});
202 changes: 202 additions & 0 deletions extensions/amp-fx-parallax/0.1/test/test-amp-fx-parallax.js
@@ -0,0 +1,202 @@
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {createIframePromise} from '../../../../testing/iframe';
import {installParallaxForDoc} from '../../../../src/service/parallax-impl';
import {parallaxForDoc} from '../../../../src/parallax';
import {toggleExperiment} from '../../../../src/experiments';
import {viewportForDoc} from '../../../../src/viewport';
import {vsyncFor} from '../../../../src/vsync';

describes.sandboxed('amp-fx-parallax', {}, () => {
const DEFAULT_FACTOR = 1.7;

function addTextChildren(iframe) {
return [iframe.doc.createTextNode('AMP: Accelerated Mobile Pages')];
}

function getAmpParallaxElement(opt_childrenCallback, opt_factor, opt_top) {
const factor = opt_factor || DEFAULT_FACTOR;
const top = opt_top || 0;
let viewport;
let parallaxElement;

return createIframePromise().then(iframe => {
const bodyResizer = iframe.doc.createElement('div');
bodyResizer.style.height = '4000px';
bodyResizer.style.width = '1px';
iframe.doc.body.appendChild(bodyResizer);

viewport = viewportForDoc(iframe.win.document);
viewport.resize_();

toggleExperiment(iframe.win, 'amp-fx-parallax', true);

parallaxElement = iframe.doc.createElement('div');
parallaxElement.setAttribute('amp-fx-parallax', factor);
if (opt_childrenCallback) {
const children = opt_childrenCallback(iframe, parallaxElement);
children.forEach(child => {
parallaxElement.appendChild(child);
});
}

const parent = iframe.doc.querySelector('#parent');
parent.appendChild(parallaxElement);
installParallaxForDoc(iframe.doc);

return new Promise(resolve => {
vsyncFor(iframe.win).mutate(() => {
resolve({
element: parallaxElement,
iframe,
viewport,
});
});
viewport.setScrollTop(top);
});
}).catch(error => {
return Promise.reject({error, parallaxElement, stack: error.stack});
});
}

it('should move when the user scrolls, if visible', () => {
const scroll = 10;
const expectedParallax = -1 * DEFAULT_FACTOR * scroll;

return getAmpParallaxElement(addTextChildren)
.then(({element, iframe, viewport}) => {
const parallaxService = parallaxForDoc(iframe.doc);
const top = element.getBoundingClientRect().top;
expect(top).to.equal(viewport.getScrollTop());

return new Promise(resolve => {
parallaxService.addScrollListener_(() => {
const top = element.getBoundingClientRect().top;
expect(top).to.equal(expectedParallax);
resolve();
});
viewport.setScrollTop(scroll);
});
});
});

it('should not move after it is outside of the viewport', () => {
const scroll = 100;
const expectedParallax = -1 * DEFAULT_FACTOR * scroll;

return getAmpParallaxElement(addTextChildren, DEFAULT_FACTOR)
.then(({element, iframe, viewport}) => {
const parallaxService = parallaxForDoc(iframe.doc);

return new Promise(resolve => {
parallaxService.addScrollListener_(() => {
const top = element.getBoundingClientRect().top;
expect(top).to.not.equal(expectedParallax);
resolve();
});
viewport.setScrollTop(scroll);
});
});
});

it('should move downward with a negative parallax factor', () => {
const scroll = 10;
const expectedParallax = -1 * DEFAULT_FACTOR * scroll;

return getAmpParallaxElement(addTextChildren, DEFAULT_FACTOR)
.then(({element, iframe, viewport}) => {
const parallaxService = parallaxForDoc(iframe.doc);
return new Promise(resolve => {
parallaxService.addScrollListener_(() => {
const top = element.getBoundingClientRect().top;
expect(top).to.equal(expectedParallax);
resolve();
});
viewport.setScrollTop(scroll);
});
});
});

it('should apply multiple scrolls as if they were one large scroll', () => {
const scroll = 10;
const factor = -1.7; // move downward so it stays in the viewport
const expectedParallax = -1 * factor * 2 * scroll;

return getAmpParallaxElement(addTextChildren, factor)
.then(({element, iframe, viewport}) => {
const parallaxService = parallaxForDoc(iframe.doc);
return new Promise(resolve => {
parallaxService.addScrollListener_(afterFirstScroll);
viewport.setScrollTop(scroll);

function afterFirstScroll() {
parallaxService.removeScrollListener_(afterFirstScroll);
parallaxService.addScrollListener_(afterSecondScroll);
viewport.setScrollTop(2 * scroll);
}

function afterSecondScroll() {
const top = element.getBoundingClientRect().top;
expect(top).to.equal(expectedParallax);
resolve();
}
});
});
});

it('should return to its original position when scrolling back', () => {
const factor = -1.7; // move downward so it stays in the viewport

return getAmpParallaxElement(addTextChildren, factor)
.then(({element, iframe, viewport}) => {
const parallaxService = parallaxForDoc(iframe.doc);
return new Promise(resolve => {
parallaxService.addScrollListener_(afterFirstScroll);
viewport.setScrollTop(10);

function afterFirstScroll() {
parallaxService.removeScrollListener_(afterFirstScroll);
parallaxService.addScrollListener_(afterSecondScroll);
viewport.setScrollTop(200);
}

function afterSecondScroll() {
parallaxService.removeScrollListener_(afterSecondScroll);
parallaxService.addScrollListener_(afterThirdScroll);
viewport.setScrollTop(0);
}

function afterThirdScroll() {
const top = element.getBoundingClientRect().top;
expect(top).to.equal(0);
resolve();
}
});
});
});

it('should render moved if the page loads partially scrolled', () => {
const scroll = 10;
const expectedParallax = -1 * DEFAULT_FACTOR * scroll;

return getAmpParallaxElement(addTextChildren, DEFAULT_FACTOR, scroll)
.then(({element}) => {
const top = element.getBoundingClientRect().top;
expect(top).to.equal(expectedParallax);
});
});
});
53 changes: 53 additions & 0 deletions extensions/amp-fx-parallax/amp-fx-parallax.md
@@ -0,0 +1,53 @@
<!---
Copyright 2017 The AMP HTML Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS-IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# <a name="amp-fx-parallax"></a> `amp-fx-parallax`

<table>
<tr>
<td class="col-fourty"><strong>Description</strong></td>
<td><code>amp-fx-parallax</code> enables a 3D-perspective effect on elements with the attribute.</td>
</tr>
<tr>
<td class="col-fourty" width="40%"><strong>Availability</strong></td>
<td>In development</td>
</tr>
<tr>
<td class="col-fourty"><strong>Required Script</strong></td>
<td><code>&lt;script async custom-element="amp-fx-parallax" src="https://cdn.ampproject.org/v0/amp-fx-parallax-0.1.js">&lt;/script></code></td>
</tr>
<tr>
<td class="col-fourty"><strong>Examples</strong></td>
<td>In development</td>
</tr>
</table>

## Behavior

The `amp-fx-parallax` attribute causes an element to move as if it is nearer or farther relative to the foreground of the page content. As the user scrolls the page, the element scrolls faster or slower depending on the value assigned to the attribute.

Example:

```html
<amp-img amp-fx-parallax="0.5" height="50vh" layout="fixed-height" src="hero.jpg">
</amp-img>
```

## Attributes

**amp-fx-parallax**

The factor to use when scrolling. A value greater than 1 scrolls the element upward when the user scrolls down the page. A value less than 1 scrolls the element downward when the user scrolls downward. A value of 1 behaves normally. A value of 0 effectively makes the element scroll fixed with the page.
1 change: 1 addition & 0 deletions gulpfile.js
Expand Up @@ -78,6 +78,7 @@ declareExtension('amp-font', '0.1', false, 'NO_TYPE_CHECK');
declareExtension('amp-form', '0.1', true);
declareExtension('amp-fresh', '0.1', true);
declareExtension('amp-fx-flying-carpet', '0.1', true);
declareExtension('amp-fx-parallax', '0.1', false);
declareExtension('amp-gfycat', '0.1', false);
declareExtension('amp-hulu', '0.1', false);
declareExtension('amp-iframe', '0.1', false, 'NO_TYPE_CHECK');
Expand Down

0 comments on commit 76ce6fb

Please sign in to comment.