Skip to content

Commit ba0fce9

Browse files
author
Sepand Parhami
committed
Initial commit of code.
1 parent 6c36629 commit ba0fce9

13 files changed

+1772
-0
lines changed

src/animation-test-controller.ts

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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+
* Sets all animations in a DOM subtree to a certain time into the animation.
19+
* This is done by adding CSS rules to target the Elements and their pseudo
20+
* Elements to apply a negative animation-delay. Note: if you depend on any
21+
* events that trigger off of an animation ending, you must allow the JavaScript
22+
* execution loop to end (i.e. by using setTimeout) before they trigger.
23+
* @param offset The animation offset, in milliseconds.
24+
* @param root The optional root to set animation offsets for.
25+
*/
26+
export function offset(offset: number, root: ParentNode|undefined = document) {
27+
const elements = getAllElements(root);
28+
29+
elements.forEach(resetDelay);
30+
elements.forEach(el => applyOffsets(el, offset));
31+
}
32+
33+
let active: boolean = false;
34+
35+
/**
36+
* Initializes, stopping all animations and allowing them to be offset.
37+
*/
38+
export function setup() {
39+
active = true;
40+
41+
getAllDocs()
42+
.filter(doc => !styleMap.get(doc))
43+
.forEach(attachStyleElement);
44+
}
45+
46+
/**
47+
* Tears down, resuming all animations.
48+
*/
49+
export function tearDown() {
50+
active = false;
51+
52+
const allDocs = getAllDocs();
53+
54+
allDocs
55+
.map(doc => styleMap.get(doc))
56+
.filter(style => style!.parentNode)
57+
.forEach(style => {
58+
style!.parentNode!.removeChild(style!);
59+
});
60+
allDocs.forEach(doc => styleMap.delete(doc));
61+
}
62+
63+
/**
64+
* Gets all the Documents or ShadowRoots present.
65+
*/
66+
function getAllDocs(): Array<Document|ShadowRoot> {
67+
const allShadowRoots = getAllElements(document)
68+
.map(getShadowRoot)
69+
.filter(sr => sr);
70+
71+
return (<Array<Document|ShadowRoot>>allShadowRoots).concat(document);
72+
}
73+
74+
/**
75+
* Capture all ShadowRoots as they are created and attach a style stopper
76+
* to them if we are active.
77+
*/
78+
function captureShadowRoots() {
79+
after(Element.prototype, 'createShadowRoot', attachStyleElement);
80+
after(Element.prototype, 'attachShadow', attachStyleElement);
81+
}
82+
83+
/**
84+
* Used to get ShadowRoots for Elements, even those created in closed mode.
85+
*/
86+
const shadowRootMap: WeakMap<Element, ShadowRoot> = new WeakMap();
87+
88+
/**
89+
* Keeps track of the <style> element associated with the given Document or
90+
* ShadowRoot.
91+
*/
92+
const styleMap: WeakMap<Node, Element> = new WeakMap();
93+
94+
/**
95+
* Keeps track of which CSS rules have already been added to a style element.
96+
*/
97+
const existingRulesMap: WeakMap<Element, Object> = new WeakMap();
98+
99+
/**
100+
* The element selectors. Note: although IE does not support ::backdrop, this
101+
* does not cause any problems.
102+
*/
103+
const elementSelectors = ['', '::after', '::before', '::backdrop'];
104+
105+
/**
106+
* The intial style text, creates rules to stop animations on all elements and
107+
* their pseudo elements.
108+
*/
109+
const styleText = elementSelectors
110+
.map(sel => `*${sel} { animation-play-state: paused !important; }\n`)
111+
.join('');
112+
113+
/**
114+
* Creates a style tag that pauses all animations in the page. Note: for IE
115+
* this must be re-created each time, using cloneNode(true) will not work.
116+
* @return An HTMLStyleElement that stops all animations on all Elements.
117+
*/
118+
const createStopperStyle = function(): HTMLStyleElement {
119+
const stopperStyle = document.createElement('style');
120+
stopperStyle.appendChild(document.createTextNode(styleText));
121+
return stopperStyle;
122+
}
123+
124+
/**
125+
* After function calls to obj[prop], calls the callback with the return value.
126+
* @param {!Object} obj
127+
* @param {string} prop
128+
* @param {function(*)} callback
129+
*/
130+
function after(obj: Object, prop: string, callback: Function) {
131+
if (!(prop in obj)) {
132+
return;
133+
}
134+
135+
const old = obj[prop];
136+
obj[prop] = function() {
137+
const retn = old.apply(this, arguments);
138+
callback(retn);
139+
return retn;
140+
}
141+
}
142+
143+
/**
144+
* Gets the root of a DOM element, which is either the Document or ShadowRoot
145+
* that contains it, if the element is attached. Otherwise, the element itself
146+
* is returned.
147+
* @param el The element to get the root for.
148+
* @return The root.
149+
*/
150+
function getRoot(el: Node): Node {
151+
return !el.parentNode ? el : getRoot(el.parentNode);
152+
}
153+
154+
/**
155+
* Attaches a style for the root of a document that is used to stop and offset
156+
* animations.
157+
* @param root The root to attach the style Element to.
158+
*/
159+
function attachStyleElement(root: Document|ShadowRoot) {
160+
if ('host' in root) {
161+
shadowRootMap.set(root.host, root);
162+
}
163+
164+
if (!active) {
165+
return;
166+
}
167+
168+
const style = createStopperStyle();
169+
170+
if (root == document) {
171+
root.head!.appendChild(style);
172+
} else {
173+
root.appendChild(style);
174+
}
175+
176+
styleMap.set(root, style);
177+
existingRulesMap.set(style, {});
178+
}
179+
180+
/**
181+
* @param el The Element to check for a ShadowRoot.
182+
* @return The ShadowRoot, if present.
183+
*/
184+
function getShadowRoot(el: Element): ShadowRoot|undefined {
185+
return shadowRootMap.get(el);
186+
}
187+
188+
/**
189+
* Gets all the elements within the root, including those in shadow DOM.
190+
* @param root
191+
* @return All the Elements within the root, including those in ShadowRoots.
192+
*/
193+
function getAllElements(root: ParentNode): Array<Element> {
194+
const elements = Array.from(root.querySelectorAll('*'));
195+
196+
return elements
197+
.map(getShadowRoot)
198+
.filter(s => s)
199+
.map(s => getAllElements(s!))
200+
.reduce((p, c) => p.concat(c), elements);
201+
}
202+
203+
/**
204+
* Given a pseudo element selector, returns a string to refer to a rule
205+
* matching the pseudo element. The string is safe for use as an attribute name.
206+
* @param pseudoElt The pseudo element selector.
207+
* @return he attribute name to use for the element.
208+
*/
209+
function getAttrName(pseudoElt: string): string {
210+
return 'offset-animation-delay' + pseudoElt.replace('::', '-');
211+
}
212+
213+
/**
214+
* Resets an Element's animation delay property back to its initial value.
215+
* @param el
216+
*/
217+
function resetDelay(el: Element) {
218+
elementSelectors.map(getAttrName)
219+
.forEach((attrName) => el.removeAttribute(attrName));
220+
}
221+
222+
/**
223+
* @param el The element to get the delay string for.
224+
* @param pseudoElt The pseudo element selector.
225+
* @param offset The animation offset, in milliseconds.
226+
* @return The animation delay string for the given offset.
227+
*/
228+
function getDelayString(
229+
el: Element, pseudoElt: string, offset: number): string {
230+
return getComputedStyle(el, pseudoElt)!['animationDelay']!
231+
.split(',')
232+
.map(part => parseFloat(part) * 1000)
233+
.map(delay => `${delay - offset}ms`)
234+
.join(', ');
235+
}
236+
237+
/**
238+
* Applies the animation delay for a given offset. Adds an attribute and
239+
* matching CSS rule to target the Element (or one of the Element's pseudo
240+
* elements).
241+
*
242+
* @param el The element to set the animation offset for.
243+
* @param offset The animation offset, in milliseconds.
244+
* @param style The style element associated with el.
245+
* @param pseudoElt The pseudo element selector.
246+
*/
247+
function applyDelay(
248+
el: Element, offset: number, style: Element, pseudoElt: string) {
249+
const delay = getDelayString(el, pseudoElt, offset);
250+
const attrName = getAttrName(pseudoElt);
251+
const selector = `[${attrName}="${delay}"]${pseudoElt}`;
252+
const existingRules = existingRulesMap.get(style)!;
253+
254+
if (!existingRules[selector]) {
255+
existingRules[selector] = true;
256+
style.textContent +=
257+
`${selector} { animation-delay: ${delay} !important }\n`;
258+
}
259+
260+
el.setAttribute(attrName, delay);
261+
}
262+
263+
/**
264+
* Sets the offset of the animation for an Element and any psuedo elements,
265+
* taking into account any animation delays specified via CSS.
266+
*
267+
* @param el The element to set the animation offset for.
268+
* @param offset The animation offset, in milliseconds.
269+
*/
270+
function applyOffsets(el: Element, offset: number) {
271+
const documentRoot = getRoot(el);
272+
const stylesheet = styleMap.get(documentRoot)!;
273+
274+
elementSelectors.forEach((pseudoElt) => {
275+
applyDelay(el, offset, stylesheet, pseudoElt);
276+
});
277+
}
278+
279+
// Always capture ShadowRoots so that we get them even if they were already
280+
// created.
281+
captureShadowRoots();

src/bezier-curve-utils.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
export interface Curve {
18+
x1: number,
19+
y1: number,
20+
x2: number,
21+
y2: number,
22+
}
23+
24+
/**
25+
* A string representation of the curve that can be used as an
26+
* `animation-timing-function`.
27+
*/
28+
export function curveToString(curve: Curve): string {
29+
return `cubic-bezier(${curve.x1}, ${curve.y1}, ${curve.x2}, ${curve.y2})`;
30+
}
31+
32+
/**
33+
* Gets the x/y value for the given control points for a given value of t.
34+
*/
35+
export function getBezierCurveValue(c1: number, c2: number, t: number): number {
36+
const t_2 = t * t;
37+
const t_3 = t_2 * t;
38+
// Formula for 4 point bezier curve with c0 = 0 and c3 = 1.
39+
return (3 * (t - 2 * t_2 + t_3) * c1) +
40+
(3 * (t_2 - t_3) * c2) + (t_3);
41+
}
42+

0 commit comments

Comments
 (0)