1// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5//     You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//     See the License for the specific language governing permissions and
13// limitations under the License.
14
15(function(shared, testing) {
16  var shorthandToLonghand = {
17    background: [
18      'backgroundImage',
19      'backgroundPosition',
20      'backgroundSize',
21      'backgroundRepeat',
22      'backgroundAttachment',
23      'backgroundOrigin',
24      'backgroundClip',
25      'backgroundColor'
26    ],
27    border: [
28      'borderTopColor',
29      'borderTopStyle',
30      'borderTopWidth',
31      'borderRightColor',
32      'borderRightStyle',
33      'borderRightWidth',
34      'borderBottomColor',
35      'borderBottomStyle',
36      'borderBottomWidth',
37      'borderLeftColor',
38      'borderLeftStyle',
39      'borderLeftWidth'
40    ],
41    borderBottom: [
42      'borderBottomWidth',
43      'borderBottomStyle',
44      'borderBottomColor'
45    ],
46    borderColor: [
47      'borderTopColor',
48      'borderRightColor',
49      'borderBottomColor',
50      'borderLeftColor'
51    ],
52    borderLeft: [
53      'borderLeftWidth',
54      'borderLeftStyle',
55      'borderLeftColor'
56    ],
57    borderRadius: [
58      'borderTopLeftRadius',
59      'borderTopRightRadius',
60      'borderBottomRightRadius',
61      'borderBottomLeftRadius'
62    ],
63    borderRight: [
64      'borderRightWidth',
65      'borderRightStyle',
66      'borderRightColor'
67    ],
68    borderTop: [
69      'borderTopWidth',
70      'borderTopStyle',
71      'borderTopColor'
72    ],
73    borderWidth: [
74      'borderTopWidth',
75      'borderRightWidth',
76      'borderBottomWidth',
77      'borderLeftWidth'
78    ],
79    flex: [
80      'flexGrow',
81      'flexShrink',
82      'flexBasis'
83    ],
84    font: [
85      'fontFamily',
86      'fontSize',
87      'fontStyle',
88      'fontVariant',
89      'fontWeight',
90      'lineHeight'
91    ],
92    margin: [
93      'marginTop',
94      'marginRight',
95      'marginBottom',
96      'marginLeft'
97    ],
98    outline: [
99      'outlineColor',
100      'outlineStyle',
101      'outlineWidth'
102    ],
103    padding: [
104      'paddingTop',
105      'paddingRight',
106      'paddingBottom',
107      'paddingLeft'
108    ]
109  };
110
111  var shorthandExpanderElem = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
112
113  var borderWidthAliases = {
114    thin: '1px',
115    medium: '3px',
116    thick: '5px'
117  };
118
119  var aliases = {
120    borderBottomWidth: borderWidthAliases,
121    borderLeftWidth: borderWidthAliases,
122    borderRightWidth: borderWidthAliases,
123    borderTopWidth: borderWidthAliases,
124    fontSize: {
125      'xx-small': '60%',
126      'x-small': '75%',
127      'small': '89%',
128      'medium': '100%',
129      'large': '120%',
130      'x-large': '150%',
131      'xx-large': '200%'
132    },
133    fontWeight: {
134      normal: '400',
135      bold: '700'
136    },
137    outlineWidth: borderWidthAliases,
138    textShadow: {
139      none: '0px 0px 0px transparent'
140    },
141    boxShadow: {
142      none: '0px 0px 0px 0px transparent'
143    }
144  };
145
146  function antiAlias(property, value) {
147    if (property in aliases) {
148      return aliases[property][value] || value;
149    }
150    return value;
151  }
152
153  function isNotAnimatable(property) {
154    // https://w3c.github.io/web-animations/#concept-not-animatable
155    return property === 'display' || property.lastIndexOf('animation', 0) === 0 || property.lastIndexOf('transition', 0) === 0;
156  }
157
158  // This delegates parsing shorthand value syntax to the browser.
159  function expandShorthandAndAntiAlias(property, value, result) {
160    if (isNotAnimatable(property)) {
161      return;
162    }
163    var longProperties = shorthandToLonghand[property];
164    if (longProperties) {
165      shorthandExpanderElem.style[property] = value;
166      for (var i in longProperties) {
167        var longProperty = longProperties[i];
168        var longhandValue = shorthandExpanderElem.style[longProperty];
169        result[longProperty] = antiAlias(longProperty, longhandValue);
170      }
171    } else {
172      result[property] = antiAlias(property, value);
173    }
174  };
175
176  function convertToArrayForm(effectInput) {
177    var normalizedEffectInput = [];
178
179    for (var property in effectInput) {
180      if (property in ['easing', 'offset', 'composite']) {
181        continue;
182      }
183
184      var values = effectInput[property];
185      if (!Array.isArray(values)) {
186        values = [values];
187      }
188
189      var keyframe;
190      var numKeyframes = values.length;
191      for (var i = 0; i < numKeyframes; i++) {
192        keyframe = {};
193
194        if ('offset' in effectInput) {
195          keyframe.offset = effectInput.offset;
196        } else if (numKeyframes == 1) {
197          keyframe.offset = 1.0;
198        } else {
199          keyframe.offset = i / (numKeyframes - 1.0);
200        }
201
202        if ('easing' in effectInput) {
203          keyframe.easing = effectInput.easing;
204        }
205
206        if ('composite' in effectInput) {
207          keyframe.composite = effectInput.composite;
208        }
209
210        keyframe[property] = values[i];
211
212        normalizedEffectInput.push(keyframe);
213      }
214    }
215
216    normalizedEffectInput.sort(function(a, b) { return a.offset - b.offset; });
217    return normalizedEffectInput;
218  };
219
220  function normalizeKeyframes(effectInput) {
221    if (effectInput == null) {
222      return [];
223    }
224
225    if (window.Symbol && Symbol.iterator && Array.prototype.from && effectInput[Symbol.iterator]) {
226      // Handle custom iterables in most browsers by converting to an array
227      effectInput = Array.from(effectInput);
228    }
229
230    if (!Array.isArray(effectInput)) {
231      effectInput = convertToArrayForm(effectInput);
232    }
233
234    var keyframes = effectInput.map(function(originalKeyframe) {
235      var keyframe = {};
236      for (var member in originalKeyframe) {
237        var memberValue = originalKeyframe[member];
238        if (member == 'offset') {
239          if (memberValue != null) {
240            memberValue = Number(memberValue);
241            if (!isFinite(memberValue))
242              throw new TypeError('Keyframe offsets must be numbers.');
243            if (memberValue < 0 || memberValue > 1)
244              throw new TypeError('Keyframe offsets must be between 0 and 1.');
245          }
246        } else if (member == 'composite') {
247          if (memberValue == 'add' || memberValue == 'accumulate') {
248            throw {
249              type: DOMException.NOT_SUPPORTED_ERR,
250              name: 'NotSupportedError',
251              message: 'add compositing is not supported'
252            };
253          } else if (memberValue != 'replace') {
254            throw new TypeError('Invalid composite mode ' + memberValue + '.');
255          }
256        } else if (member == 'easing') {
257          memberValue = shared.normalizeEasing(memberValue);
258        } else {
259          memberValue = '' + memberValue;
260        }
261        expandShorthandAndAntiAlias(member, memberValue, keyframe);
262      }
263      if (keyframe.offset == undefined)
264        keyframe.offset = null;
265      if (keyframe.easing == undefined)
266        keyframe.easing = 'linear';
267      return keyframe;
268    });
269
270    var everyFrameHasOffset = true;
271    var looselySortedByOffset = true;
272    var previousOffset = -Infinity;
273    for (var i = 0; i < keyframes.length; i++) {
274      var offset = keyframes[i].offset;
275      if (offset != null) {
276        if (offset < previousOffset) {
277          throw new TypeError('Keyframes are not loosely sorted by offset. Sort or specify offsets.');
278        }
279        previousOffset = offset;
280      } else {
281        everyFrameHasOffset = false;
282      }
283    }
284
285    keyframes = keyframes.filter(function(keyframe) {
286      return keyframe.offset >= 0 && keyframe.offset <= 1;
287    });
288
289    function spaceKeyframes() {
290      var length = keyframes.length;
291      if (keyframes[length - 1].offset == null)
292        keyframes[length - 1].offset = 1;
293      if (length > 1 && keyframes[0].offset == null)
294        keyframes[0].offset = 0;
295
296      var previousIndex = 0;
297      var previousOffset = keyframes[0].offset;
298      for (var i = 1; i < length; i++) {
299        var offset = keyframes[i].offset;
300        if (offset != null) {
301          for (var j = 1; j < i - previousIndex; j++)
302            keyframes[previousIndex + j].offset = previousOffset + (offset - previousOffset) * j / (i - previousIndex);
303          previousIndex = i;
304          previousOffset = offset;
305        }
306      }
307    }
308    if (!everyFrameHasOffset)
309      spaceKeyframes();
310
311    return keyframes;
312  }
313
314  shared.convertToArrayForm = convertToArrayForm;
315  shared.normalizeKeyframes = normalizeKeyframes;
316
317  if (WEB_ANIMATIONS_TESTING) {
318    testing.normalizeKeyframes = normalizeKeyframes;
319  }
320
321})(webAnimationsShared, webAnimationsTesting);
322