Skip to content

Commit

Permalink
Implement amp-story dynamic page scaling
Browse files Browse the repository at this point in the history
  • Loading branch information
alanorozco committed Jan 18, 2018
1 parent d21a8f9 commit 26488aa
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 8 deletions.
13 changes: 9 additions & 4 deletions extensions/amp-story/0.1/amp-story-desktop.css
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,21 @@ amp-story[standalone][desktop] {
}

[desktop] > amp-story-page,
.i-amphtml-story-page-sentinel {
left: 50%!important;
right: auto !important;
margin: auto !important;
[desktop] > .i-amphtml-story-page-sizer,
[desktop] .i-amphtml-story-page-sentinel {
max-height: 75vh !important;
max-width: calc(3/5 * 75vh) !important;
min-width: 320px !important;
min-height: 533px !important;
}

[desktop] > amp-story-page,
[desktop] .i-amphtml-story-page-sentinel {
left: 50%!important;
right: auto !important;
margin: auto !important;
}

[desktop] > amp-story-page {
box-shadow: 0 0 15px rgba(0, 0, 0, .4)!important;
}
Expand Down
14 changes: 12 additions & 2 deletions extensions/amp-story/0.1/amp-story-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import {upgradeBackgroundAudio} from './audio';
import {EventType, dispatch, dispatchCustom} from './events';
import {AdvancementConfig} from './page-advancement';
import {matches, scopedQuerySelectorAll} from '../../../src/dom';
import {dev} from '../../../src/log';
import {getLogEntries} from './logging';
import {getMode} from '../../../src/mode';
import {CommonSignals} from '../../../src/common-signals';
import {PageScalingService} from './page-scaling';


/**
Expand Down Expand Up @@ -179,7 +181,7 @@ export class AmpStoryPage extends AMP.BaseElement {
/** @return {!Promise} */
beforeVisible() {
this.rewindAllMediaToBeginning_();
return this.maybeApplyFirstAnimationFrame();
return this.scale_().then(() => this.maybeApplyFirstAnimationFrame());
}


Expand Down Expand Up @@ -327,6 +329,14 @@ export class AmpStoryPage extends AMP.BaseElement {
return this.animationManager_.applyFirstFrame();
}

/**
* @return {!Promise}
* @private
*/
scale_() {
const storyEl = dev().assertElement(this.element.parentNode);
return PageScalingService.for(this.win, storyEl).scale(this.element);
}

/**
* @param {boolean} isActive
Expand Down Expand Up @@ -356,10 +366,10 @@ export class AmpStoryPage extends AMP.BaseElement {
*/
setDistance(distance) {
this.element.setAttribute('distance', distance);

this.registerAllMedia_();
if (distance > 0 && distance <= 2) {
this.preloadAllMedia_();
this.scale_();
}
}

Expand Down
14 changes: 12 additions & 2 deletions extensions/amp-story/0.1/amp-story.css
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,24 @@ amp-story .amp-video-eq {
}

/** Page level */
amp-story-page {
amp-story-page,
.i-amphtml-story-page-sizer {
bottom: 0 !important;
display: none !important;
height: auto !important;
left: 0 !important;
position: absolute !important;
right: 0 !important;
top: 0 !important;
}

.i-amphtml-story-page-sizer {
opacity: 0 !important;
pointer-events: none !important;
contain: strict !important;
}

amp-story-page {
display: none !important;
transition: none !important;
}

Expand Down
5 changes: 5 additions & 0 deletions extensions/amp-story/0.1/amp-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ export class AmpStory extends AMP.BaseElement {
buildCallback() {
this.assertAmpStoryExperiment_();

if (this.isDesktop_()) {
this.element.setAttribute('desktop','');
}

if (this.element.hasAttribute(AMP_STORY_STANDALONE_ATTRIBUTE)) {
this.getAmpDoc().win.document.documentElement.classList
.add('i-amphtml-story-standalone');
Expand Down Expand Up @@ -921,6 +925,7 @@ export class AmpStory extends AMP.BaseElement {
this.bookend_.hide();
}


/**
* Toggle content when bookend is opened/closed.
* @param {boolean} display
Expand Down
252 changes: 252 additions & 0 deletions extensions/amp-story/0.1/page-scaling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/**
* Copyright 2018 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 {Services} from '../../../src/services';
import {dev, user} from '../../../src/log';
import {dict} from '../../../src/utils/object';
import {isExperimentOn} from '../../../src/experiments';
import {childElementsByTag, matches} from '../../../src/dom';
import {px, setImportantStyles} from '../../../src/style';
import {renderAsElement} from './simple-template';
import {throttle} from '../../../src/utils/rate-limit';
import {toArray} from '../../../src/types';


/** @private @const {number} */
const MIN_LAYER_WIDTH_PX = 380;


/** @private @const {number} */
const MAX_LAYER_WIDTH_PX = 520;


/** @private @const {string} */
const SCALING_APPLIED_CLASSNAME = 'i-amphtml-story-scaled';


/** @private @const {!./simple-template.ElementDef} */
const SIZER_TEMPLATE = {
tag: 'div',
attrs: dict({'class': 'i-amphtml-story-page-sizer'}),
};


/** @struct @typedef {{factor: number, width: number, height: number}} */
let DimensionsDef;


/** @typedef {{scale: function(!Element):!Promise}} */
let PageScalingServiceInterface;


/**
* @param {!Element} sizer
* @return {!DimensionsDef}
*/
function targetDimensionsFor(sizer) {
const {width, height} = sizer./*OK*/getBoundingClientRect();

const ratio = width / height;

const targetWidth = Math.min(MAX_LAYER_WIDTH_PX,
Math.max(width, Math.max(1, ratio) * MIN_LAYER_WIDTH_PX));

const targetHeight = (targetWidth / ratio);

const factor = width / targetWidth;

return {width: targetWidth, height: targetHeight, factor};
}


/**
* @param {!Element} page
* @param {!DimensionsDef} dimensions
*/
function applyScaling(page, dimensions) {
const {width, height, factor} = dimensions;
// TODO(alanorozco): Calculate relative layer dimensions for cases where
// they won't fill the entire page.
const style = {
'width': px(width),
'height': px(height),
'zoom': `${factor * 100}%`,
'box-sizing': 'border-box',
};
scalableChildren(page).forEach(el => {
setImportantStyles(el, style);
});
markScalingApplied(page);
}


/**
* @param {!Element} page
* @return {!Array<!Element>}
*/
function scalableChildren(page) {
return toArray(childElementsByTag(page, 'amp-story-grid-layer'));
}


/**
* @param {!Element} page
* @return {boolean}
*/
function isScalingEnabled(page) {
return page.getAttribute('scaling') != 'disabled';
}


/**
* @param {!Element} page
* @return {boolean}
*/
function isScalingApplied(page) {
return page.classList.contains(SCALING_APPLIED_CLASSNAME);
}


/**
* @param {!Element} page
* @param {boolean} isApplied
*/
function markScalingApplied(page, isApplied = true) {
page.classList.toggle(SCALING_APPLIED_CLASSNAME, isApplied);
}


/**
* Required for lazy evaluation after resize.
* @param {!Element} page
* @return {boolean}
*/
function withinRange(page) {
return matches(page, '[active], [distance="1"], [desktop] > [distance="2"]');
}


/**
* @param {!Element} page
* @return {boolean}
*/
function needsScaling(page) {
return isScalingEnabled(page) && !isScalingApplied(page) && withinRange(page);
}


/**
* @param {!Document} doc
* @param {!Element} container
*/
function createSizer(doc, container) {
const element = renderAsElement(doc, SIZER_TEMPLATE);
container.appendChild(element);
return element;
}


/** @private @const {!PageScalingServiceInterface} */
const MOCK_PAGE_SCALING_SERVICE = {
scale(unusedPage) {
return Promise.resolve();
},
};


/** @private {?PageScalingServiceInterface} */
let pageScalingService = null;


/**
* Service for scaling pages dynamically so their layers will be sized within a
* certain pixel range independent of visual dimensions.
*/
// TODO(alanorozco): Make this part of the runtime layout system to prevent
// FOUC-like jump and allow for SSR.
export class PageScalingService {
constructor(win, story) {
/** @private @const {!Element} */
this.story_ = story;

/** @private @const */
this.vsync_ = Services.vsyncFor(win);

/** @private @const {!Element} */
this.sizer_ = createSizer(win.document, story);

/** @private {?DimensionsDef} */
this.dimensions_ = null;

Services.viewportForDoc(story).onResize(
throttle(win, () => this.onViewportResize_(), 100));
}

/**
* @param {!Window} win
* @param {!Element} story
* @return {!PageScalingServiceInterface}
*/
static for(win, story) {
// Implemented as singleton for now, should be mapped to story element.
// TODO(alanorozco): Implement mapping to support multiple <amp-story>
// instances in one doc.
if (!pageScalingService) {
if (!isExperimentOn(win, 'amp-story-scaling')) {
pageScalingService = MOCK_PAGE_SCALING_SERVICE;
} else if (Services.platformFor(win).isFirefox()) {
// Firefox does not support `zoom`.
// TODO(alanorozco): Use `scale` on Firefox.
user().warn('`amp-story-scaling` ignored: Firefox is not supported.');
pageScalingService = MOCK_PAGE_SCALING_SERVICE;
} else {
pageScalingService = new PageScalingService(win, story);
}
}
return pageScalingService;
}

/**
* @param {!Element} page
* @return {!Promise}
*/
scale(page) {
if (!needsScaling(page)) {
return Promise.resolve();
}
return this.vsync_.runPromise({
measure: () => {
if (!this.dimensions_) {
this.dimensions_ = targetDimensionsFor(this.sizer_);
}
},
mutate: () => {
const dimensions =
/** @type {!DimensionsDef} */ (dev().assert(this.dimensions_));
applyScaling(page, dimensions);
},
});
}

/** @private */
onViewportResize_() {
const pages = toArray(childElementsByTag(this.story_, 'amp-story-page'));
this.dimensions_ = null;
pages.forEach(page => {
markScalingApplied(page, false);
this.scale(page);
});
}
}

0 comments on commit 26488aa

Please sign in to comment.