Skip to content

Commit 09f6188

Browse files
author
Sepand Parhami
authored
Perform animation using absolute positioning. (#5)
This prevents the animation from looking off if you scroll the page while the animation is in progress.
1 parent 39f1e30 commit 09f6188

File tree

5 files changed

+191
-4
lines changed

5 files changed

+191
-4
lines changed

src/positioned-container.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Copyright 2018 The AMP HTML Authors. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS-IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Finds the positioned container (i.e. one that has a computed position that
19+
* is not static). If the element itself is positioned, then we return it,
20+
* otherwise, we find the first positioned parent. If there are no positioned
21+
* elements, this will return the root element.
22+
* @param element The element to get the positioned container for.
23+
* @return The positioned container.
24+
*/
25+
export function getPositionedContainer(element: Element): Element {
26+
const { position } = getComputedStyle(element);
27+
// Element is positioned, we are done.
28+
if (position != 'static') {
29+
return element;
30+
}
31+
// We can skip to the offsetParent if present, no need to check all elements
32+
// in between. If we have an offsetParent, we still need to check that it is
33+
// positioned, as it will return `document.body`, even if it is not
34+
// positioned.
35+
const parent = (<HTMLElement>element).offsetParent || element.parentElement;
36+
return parent ? getPositionedContainer(parent) : element;
37+
}

src/test-positioned-container.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright 2018 The AMP HTML Authors. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS-IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {getPositionedContainer} from './positioned-container.js';
18+
19+
const {expect} = chai;
20+
21+
describe('getPositionedParent', () => {
22+
let child;
23+
let grandchild;
24+
25+
beforeEach(() => {
26+
child = document.createElement('div');
27+
grandchild = document.createElement('div');
28+
child.appendChild(grandchild);
29+
document.body.appendChild(child);
30+
});
31+
32+
afterEach(() => {
33+
document.body.style.cssText = '';
34+
document.body.removeChild(child);
35+
});
36+
37+
describe('for a positioned body', () => {
38+
beforeEach(() => {
39+
document.body.style.position = 'relative';
40+
});
41+
42+
it('should return the correct element for body', () => {
43+
const container = getPositionedContainer(document.body);
44+
expect(container).to.equal(document.body);
45+
});
46+
47+
it('should return the correct element for a child', () => {
48+
const container = getPositionedContainer(child);
49+
expect(container).to.equal(document.body);
50+
});
51+
52+
it('should return the correct element for a grandchild', () => {
53+
const container = getPositionedContainer(grandchild);
54+
expect(container).to.equal(document.body);
55+
});
56+
57+
it('should return the correct element for a grandchild with positioned parent', () => {
58+
child.style.position = 'relative';
59+
60+
const container = getPositionedContainer(grandchild);
61+
expect(container).to.equal(child);
62+
});
63+
});
64+
65+
describe('for a non positioned body', () => {
66+
it('should return the correct element for body', () => {
67+
const container = getPositionedContainer(document.body);
68+
expect(container).to.equal(document.documentElement);
69+
});
70+
71+
it('should return the correct element for a child', () => {
72+
const container = getPositionedContainer(child);
73+
expect(container).to.equal(document.documentElement);
74+
});
75+
76+
it('should return the correct element for a grandchild', () => {
77+
const container = getPositionedContainer(grandchild);
78+
expect(container).to.equal(document.documentElement);
79+
});
80+
81+
it('should return the correct element for a grandchild with positioned parent', () => {
82+
child.style.position = 'relative';
83+
84+
const container = getPositionedContainer(grandchild);
85+
expect(container).to.equal(child);
86+
});
87+
});
88+
});

src/transform-img/test-transform-img.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,57 @@ describe('prepareImageAnimation', () => {
281281
expect(imgHeight).to.be.closeTo(12, COMPARISON_EPSILON);
282282
});
283283
});
284+
285+
describe('scrolling', () => {
286+
let sizer;
287+
288+
beforeEach(async () => {
289+
sizer = document.createElement('div');
290+
sizer.style.height = `${window.innerHeight * 2}px`;
291+
sizer.style.width = `${window.innerWidth * 2}px`;
292+
document.body.appendChild(sizer);
293+
294+
await updateImg(smallerImg, 'contain', threeByFourUri);
295+
smallerImg.style.width = '12px';
296+
smallerImg.style.height = '12px';
297+
smallerImg.style.top = '500px';
298+
smallerImg.style.left = '100px';
299+
await updateImg(largerImg, 'contain', threeByFourUri);
300+
largerImg.style.width = '24px';
301+
largerImg.style.height = '32px';
302+
largerImg.style.top = '10px';
303+
largerImg.style.left = '10px';
304+
});
305+
306+
afterEach(() => {
307+
document.body.removeChild(sizer);
308+
window.scrollTo(0, 0);
309+
});
310+
311+
it('should have the correct position 200ms in', async () => {
312+
startAnimation(largerImg, smallerImg);
313+
offset(200);
314+
window.scrollTo(50, 75);
315+
316+
const replacement = getIntermediateImg();
317+
const {top, left} = replacement.getBoundingClientRect();
318+
// 20% of the animation, starting from 10, going to 500 - 75 from scroll
319+
expect(top).to.be.closeTo(-51.5, COMPARISON_EPSILON);
320+
// 20% of the animation, starting from 10, going to 100 - 50 from scroll
321+
expect(left).to.be.closeTo(-37.5, COMPARISON_EPSILON);
322+
});
323+
324+
it('should end with the correct position', async () => {
325+
startAnimation(largerImg, smallerImg);
326+
offset(1000);
327+
window.scrollTo(50, 75);
328+
329+
const replacement = getIntermediateImg();
330+
const {top, left} = replacement.getBoundingClientRect();
331+
// smallerImg.style.top - 75 from scroll
332+
expect(top).to.be.closeTo(425, COMPARISON_EPSILON);
333+
// smallerImg.style.left - 50 from scroll
334+
expect(left).to.be.closeTo(50, COMPARISON_EPSILON);
335+
});
336+
});
284337
});

src/transform-img/transform-img.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import {Curve} from '../bezier-curve-utils.js';
18+
import {getPositionedContainer} from '../positioned-container';
1819
import {getRenderedDimensions} from '../img-dimensions.js';
1920
import {createItermediateImg} from '../intermdediate-img.js';
2021
import {prepareCropAnimation} from './crop-animation.js';
@@ -86,7 +87,7 @@ export function prepareImageAnimation({
8687
styles,
8788
keyframesNamespace = 'img-transform',
8889
} : {
89-
transitionContainer: Element|Document|DocumentFragment,
90+
transitionContainer: HTMLElement,
9091
styleContainer: Element|Document|DocumentFragment,
9192
srcImg: HTMLImageElement,
9293
targetImg: HTMLImageElement,
@@ -117,6 +118,8 @@ export function prepareImageAnimation({
117118
counterScaleElement,
118119
img,
119120
} = createItermediateImg(largerImg, largerRect, largerImageDimensions);
121+
const positionedParent = getPositionedContainer(transitionContainer);
122+
const positionedParentRect = positionedParent.getBoundingClientRect();
120123

121124
const cropStyleText = prepareCropAnimation({
122125
scaleElement,
@@ -130,6 +133,7 @@ export function prepareImageAnimation({
130133
});
131134
const translateStyleText = prepareTranslateAnimation({
132135
element: translateElement,
136+
positionedParentRect,
133137
largerRect,
134138
smallerRect,
135139
curve,

src/transform-img/translate-animation.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import {Curve, curveToString} from '../bezier-curve-utils.js';
2424
* inserted for the animation to run.
2525
* @param options
2626
* @param options.element The element to apply the scaling to.
27+
* @param options.positionedParentRect The rect for the positioned parent.
28+
* We need to account for the difference of the target's top/left and where
29+
* we will position absolutely to.
2730
* @param options.largerRect The larger of the start/end scaling rects.
2831
* @param options.smallerRect The smaller of the start/end scaling rects.
2932
* @param options.curve The timing curve for the scaling.
@@ -36,6 +39,7 @@ import {Curve, curveToString} from '../bezier-curve-utils.js';
3639
*/
3740
export function prepareTranslateAnimation({
3841
element,
42+
positionedParentRect,
3943
largerRect,
4044
smallerRect,
4145
curve,
@@ -44,6 +48,7 @@ export function prepareTranslateAnimation({
4448
toLarger,
4549
} : {
4650
element: HTMLElement,
51+
positionedParentRect: ClientRect,
4752
largerRect: ClientRect,
4853
smallerRect: ClientRect,
4954
curve: Curve,
@@ -70,9 +75,9 @@ export function prepareTranslateAnimation({
7075
const deltaTop = startTop - endTop;
7176

7277
Object.assign(element.style, styles, {
73-
'position': 'fixed',
74-
'top': `${endTop}px`,
75-
'left': `${endLeft}px`,
78+
'position': 'absolute',
79+
'top': `${endTop - positionedParentRect.top}px`,
80+
'left': `${endLeft - positionedParentRect.left}px`,
7681
'willChange': 'transform',
7782
'animationName': keyframesName,
7883
'animationTimingFunction': curveToString(curve),

0 commit comments

Comments
 (0)