Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(304)

Side by Side Diff: bower_components/web-animations-js/web-animations.js

Issue 786953007: npm_modules: Fork bower_components into Polymer 0.4.0 and 0.5.0 versions (Closed) Base URL: https://chromium.googlesource.com/infra/third_party/npm_modules.git@master
Patch Set: Created 5 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 /**
2 * Copyright 2012 Google Inc. 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 (function() {
18 'use strict';
19
20 var ASSERT_ENABLED = false;
21 var SVG_NS = 'http://www.w3.org/2000/svg';
22
23 function assert(check, message) {
24 console.assert(ASSERT_ENABLED,
25 'assert should not be called when ASSERT_ENABLED is false');
26 console.assert(check, message);
27 // Some implementations of console.assert don't actually throw
28 if (!check) { throw message; }
29 }
30
31 function detectFeatures() {
32 var el = createDummyElement();
33 el.style.cssText = 'width: calc(0px);' +
34 'width: -webkit-calc(0px);';
35 var calcFunction = el.style.width.split('(')[0];
36 function detectProperty(candidateProperties) {
37 return [].filter.call(candidateProperties, function(property) {
38 return property in el.style;
39 })[0];
40 }
41 var transformProperty = detectProperty([
42 'transform',
43 'webkitTransform',
44 'msTransform']);
45 var perspectiveProperty = detectProperty([
46 'perspective',
47 'webkitPerspective',
48 'msPerspective']);
49 return {
50 calcFunction: calcFunction,
51 transformProperty: transformProperty,
52 transformOriginProperty: transformProperty + 'Origin',
53 perspectiveProperty: perspectiveProperty,
54 perspectiveOriginProperty: perspectiveProperty + 'Origin'
55 };
56 }
57 var features = detectFeatures();
58
59 function prefixProperty(property) {
60 switch (property) {
61 case 'transform':
62 return features.transformProperty;
63 case 'transformOrigin':
64 return features.transformOriginProperty;
65 case 'perspective':
66 return features.perspectiveProperty;
67 case 'perspectiveOrigin':
68 return features.perspectiveOriginProperty;
69 default:
70 return property;
71 }
72 }
73
74 function createDummyElement() {
75 return document.documentElement.namespaceURI == SVG_NS ?
76 document.createElementNS(SVG_NS, 'g') :
77 document.createElement('div');
78 }
79
80 var constructorToken = {};
81 var deprecationsSilenced = {};
82
83 var createObject = function(proto, obj) {
84 var newObject = Object.create(proto);
85 Object.getOwnPropertyNames(obj).forEach(function(name) {
86 Object.defineProperty(
87 newObject, name, Object.getOwnPropertyDescriptor(obj, name));
88 });
89 return newObject;
90 };
91
92 var abstractMethod = function() {
93 throw 'Abstract method not implemented.';
94 };
95
96 var deprecated = function(name, deprecationDate, advice, plural) {
97 if (deprecationsSilenced[name]) {
98 return;
99 }
100 var auxVerb = plural ? 'are' : 'is';
101 var today = new Date();
102 var cutoffDate = new Date(deprecationDate);
103 cutoffDate.setMonth(cutoffDate.getMonth() + 3); // 3 months grace period
104
105 if (today < cutoffDate) {
106 console.warn('Web Animations: ' + name +
107 ' ' + auxVerb + ' deprecated and will stop working on ' +
108 cutoffDate.toDateString() + '. ' + advice);
109 deprecationsSilenced[name] = true;
110 } else {
111 throw new Error(name + ' ' + auxVerb + ' no longer supported. ' + advice);
112 }
113 };
114
115 var defineDeprecatedProperty = function(object, property, getFunc, setFunc) {
116 var descriptor = {
117 get: getFunc,
118 configurable: true
119 };
120 if (setFunc) {
121 descriptor.set = setFunc;
122 }
123 Object.defineProperty(object, property, descriptor);
124 };
125
126 var IndexSizeError = function(message) {
127 Error.call(this);
128 this.name = 'IndexSizeError';
129 this.message = message;
130 };
131
132 IndexSizeError.prototype = Object.create(Error.prototype);
133
134
135
136 /** @constructor */
137 var TimingDict = function(timingInput) {
138 if (typeof timingInput === 'object') {
139 for (var k in timingInput) {
140 if (k in TimingDict.prototype) {
141 this[k] = timingInput[k];
142 }
143 }
144 } else if (isDefinedAndNotNull(timingInput)) {
145 this.duration = Number(timingInput);
146 }
147 };
148
149 TimingDict.prototype = {
150 delay: 0,
151 endDelay: 0,
152 fill: 'auto',
153 iterationStart: 0,
154 iterations: 1,
155 duration: 'auto',
156 playbackRate: 1,
157 direction: 'normal',
158 easing: 'linear'
159 };
160
161
162
163 /** @constructor */
164 var Timing = function(token, timingInput, changeHandler) {
165 if (token !== constructorToken) {
166 throw new TypeError('Illegal constructor');
167 }
168 this._dict = new TimingDict(timingInput);
169 this._changeHandler = changeHandler;
170 };
171
172 Timing.prototype = {
173 _timingFunction: function(timedItem) {
174 var timingFunction = TimingFunction.createFromString(
175 this.easing, timedItem);
176 this._timingFunction = function() {
177 return timingFunction;
178 };
179 return timingFunction;
180 },
181 _invalidateTimingFunction: function() {
182 delete this._timingFunction;
183 },
184 _iterations: function() {
185 var value = this._dict.iterations;
186 return value < 0 ? 1 : value;
187 },
188 _duration: function() {
189 var value = this._dict.duration;
190 return typeof value === 'number' ? value : 'auto';
191 },
192 _clone: function() {
193 return new Timing(
194 constructorToken, this._dict, this._updateInternalState.bind(this));
195 }
196 };
197
198 // Configures an accessor descriptor for use with Object.defineProperty() to
199 // allow the property to be changed and enumerated, to match __defineGetter__()
200 // and __defineSetter__().
201 var configureDescriptor = function(descriptor) {
202 descriptor.configurable = true;
203 descriptor.enumerable = true;
204 return descriptor;
205 };
206
207 Timing._defineProperty = function(prop) {
208 Object.defineProperty(Timing.prototype, prop, configureDescriptor({
209 get: function() {
210 return this._dict[prop];
211 },
212 set: function(value) {
213 if (isDefinedAndNotNull(value)) {
214 if (prop == 'duration' && value == 'auto') {
215 // duration is not always a number
216 } else if (['delay', 'endDelay', 'iterationStart', 'iterations',
217 'duration', 'playbackRate'].indexOf(prop) >= 0) {
218 value = Number(value);
219 }
220 this._dict[prop] = value;
221 } else {
222 delete this._dict[prop];
223 }
224 // FIXME: probably need to implement specialized handling parsing
225 // for each property
226 if (prop === 'easing') {
227 // Cached timing function may be invalid now.
228 this._invalidateTimingFunction();
229 }
230 this._changeHandler();
231 }
232 }));
233 };
234
235 for (var prop in TimingDict.prototype) {
236 Timing._defineProperty(prop);
237 }
238
239 var isDefined = function(val) {
240 return typeof val !== 'undefined';
241 };
242
243 var isDefinedAndNotNull = function(val) {
244 return isDefined(val) && (val !== null);
245 };
246
247
248
249 /** @constructor */
250 var AnimationTimeline = function(token) {
251 if (token !== constructorToken) {
252 throw new TypeError('Illegal constructor');
253 }
254 // TODO: This will probably need to change.
255 this._startTime = documentTimeZeroAsClockTime;
256 };
257
258 AnimationTimeline.prototype = {
259 get currentTime() {
260 if (this._startTime === undefined) {
261 this._startTime = documentTimeZeroAsClockTime;
262 if (this._startTime === undefined) {
263 return null;
264 }
265 }
266 return relativeTime(cachedClockTime(), this._startTime);
267 },
268 get effectiveCurrentTime() {
269 return this.currentTime || 0;
270 },
271 play: function(source) {
272 return new AnimationPlayer(constructorToken, source, this);
273 },
274 getCurrentPlayers: function() {
275 return PLAYERS.filter(function(player) {
276 return !player._isPastEndOfActiveInterval();
277 });
278 },
279 toTimelineTime: function(otherTime, other) {
280 if ((this.currentTime === null) || (other.currentTime === null)) {
281 return null;
282 } else {
283 return otherTime + other._startTime - this._startTime;
284 }
285 },
286 _pauseAnimationsForTesting: function(pauseAt) {
287 PLAYERS.forEach(function(player) {
288 player.pause();
289 player.currentTime = pauseAt;
290 });
291 }
292 };
293
294 // TODO: Remove dead players from here?
295 var PLAYERS = [];
296 var playersAreSorted = false;
297 var playerSequenceNumber = 0;
298
299 // Methods for event target objects.
300 var initializeEventTarget = function(eventTarget) {
301 eventTarget._handlers = {};
302 eventTarget._onHandlers = {};
303 };
304 var setOnEventHandler = function(eventTarget, type, handler) {
305 if (typeof handler === 'function') {
306 eventTarget._onHandlers[type] = {
307 callback: handler,
308 index: (eventTarget._handlers[type] || []).length
309 };
310 } else {
311 eventTarget._onHandlers[type] = null;
312 }
313 };
314 var getOnEventHandler = function(eventTarget, type) {
315 if (isDefinedAndNotNull(eventTarget._onHandlers[type])) {
316 return eventTarget._onHandlers[type].callback;
317 }
318 return null;
319 };
320 var addEventHandler = function(eventTarget, type, handler) {
321 if (typeof handler !== 'function') {
322 return;
323 }
324 if (!isDefinedAndNotNull(eventTarget._handlers[type])) {
325 eventTarget._handlers[type] = [];
326 } else if (eventTarget._handlers[type].indexOf(handler) !== -1) {
327 return;
328 }
329 eventTarget._handlers[type].push(handler);
330 };
331 var removeEventHandler = function(eventTarget, type, handler) {
332 if (!eventTarget._handlers[type]) {
333 return;
334 }
335 var index = eventTarget._handlers[type].indexOf(handler);
336 if (index === -1) {
337 return;
338 }
339 eventTarget._handlers[type].splice(index, 1);
340 if (isDefinedAndNotNull(eventTarget._onHandlers[type]) &&
341 (index < eventTarget._onHandlers[type].index)) {
342 eventTarget._onHandlers[type].index -= 1;
343 }
344 };
345 var hasEventHandlersForEvent = function(eventTarget, type) {
346 return (isDefinedAndNotNull(eventTarget._handlers[type]) &&
347 eventTarget._handlers[type].length > 0) ||
348 isDefinedAndNotNull(eventTarget._onHandlers[type]);
349 };
350 var callEventHandlers = function(eventTarget, type, event) {
351 var callbackList;
352 if (isDefinedAndNotNull(eventTarget._handlers[type])) {
353 callbackList = eventTarget._handlers[type].slice();
354 } else {
355 callbackList = [];
356 }
357 if (isDefinedAndNotNull(eventTarget._onHandlers[type])) {
358 callbackList.splice(eventTarget._onHandlers[type].index, 0,
359 eventTarget._onHandlers[type].callback);
360 }
361 setTimeout(function() {
362 for (var i = 0; i < callbackList.length; i++) {
363 callbackList[i].call(eventTarget, event);
364 }
365 }, 0);
366 };
367 var createEventPrototype = function() {
368 var prototype = Object.create(window.Event.prototype, {
369 type: { get: function() { return this._type; } },
370 target: { get: function() { return this._target; } },
371 currentTarget: { get: function() { return this._target; } },
372 eventPhase: { get: function() { return this._eventPhase; } },
373 bubbles: { get: function() { return false; } },
374 cancelable: { get: function() { return false; } },
375 timeStamp: { get: function() { return this._timeStamp; } },
376 defaultPrevented: { get: function() { return false; } }
377 });
378 prototype._type = '';
379 prototype._target = null;
380 prototype._eventPhase = Event.NONE;
381 prototype._timeStamp = 0;
382 prototype._initialize = function(target) {
383 this._target = target;
384 this._eventPhase = Event.AT_TARGET;
385 this._timeStamp = cachedClockTime();
386 };
387 return prototype;
388 };
389
390
391
392 /** @constructor */
393 var AnimationPlayer = function(token, source, timeline) {
394 if (token !== constructorToken) {
395 throw new TypeError('Illegal constructor');
396 }
397 enterModifyCurrentAnimationState();
398 try {
399 this._registeredOnTimeline = false;
400 this._sequenceNumber = playerSequenceNumber++;
401 this._timeline = timeline;
402 this._startTime =
403 this.timeline.currentTime === null ? 0 : this.timeline.currentTime;
404 this._storedTimeLag = 0.0;
405 this._pausedState = false;
406 this._holdTime = null;
407 this._previousCurrentTime = null;
408 this._playbackRate = 1.0;
409 this._hasTicked = false;
410
411 this.source = source;
412 this._lastCurrentTime = undefined;
413 this._finishedFlag = false;
414 initializeEventTarget(this);
415
416 playersAreSorted = false;
417 maybeRestartAnimation();
418 } finally {
419 exitModifyCurrentAnimationState(ensureRetickBeforeGetComputedStyle);
420 }
421 };
422
423 AnimationPlayer.prototype = {
424 set source(source) {
425 enterModifyCurrentAnimationState();
426 try {
427 if (isDefinedAndNotNull(this.source)) {
428 // To prevent infinite recursion.
429 var oldTimedItem = this.source;
430 this._source = null;
431 oldTimedItem._attach(null);
432 }
433 this._source = source;
434 if (isDefinedAndNotNull(this.source)) {
435 this.source._attach(this);
436 this._update();
437 maybeRestartAnimation();
438 }
439 } finally {
440 exitModifyCurrentAnimationState(repeatLastTick);
441 }
442 },
443 get source() {
444 return this._source;
445 },
446 // This is the effective current time.
447 set currentTime(currentTime) {
448 enterModifyCurrentAnimationState();
449 try {
450 this._currentTime = currentTime;
451 } finally {
452 exitModifyCurrentAnimationState(repeatLastTick);
453 }
454 },
455 get currentTime() {
456 return this._currentTime;
457 },
458 set _currentTime(seekTime) {
459 // If we are paused or seeking to a time where limiting applies (i.e. beyond
460 // the end in the current direction), update the hold time.
461 var sourceContentEnd = this.source ? this.source.endTime : 0;
462 if (this.paused ||
463 (this.playbackRate > 0 && seekTime >= sourceContentEnd) ||
464 (this.playbackRate < 0 && seekTime <= 0)) {
465 this._holdTime = seekTime;
466 // Otherwise, clear the hold time (it may been set by previously seeking to
467 // a limited time) and update the time lag.
468 } else {
469 this._holdTime = null;
470 this._storedTimeLag = (this.timeline.effectiveCurrentTime -
471 this.startTime) * this.playbackRate - seekTime;
472 }
473 this._update();
474 maybeRestartAnimation();
475 },
476 get _currentTime() {
477 this._previousCurrentTime = (this.timeline.effectiveCurrentTime -
478 this.startTime) * this.playbackRate - this.timeLag;
479 return this._previousCurrentTime;
480 },
481 get _unlimitedCurrentTime() {
482 return (this.timeline.effectiveCurrentTime - this.startTime) *
483 this.playbackRate - this._storedTimeLag;
484 },
485 get timeLag() {
486 if (this.paused) {
487 return this._pauseTimeLag;
488 }
489
490 // Apply limiting at start of interval when playing in reverse
491 if (this.playbackRate < 0 && this._unlimitedCurrentTime <= 0) {
492 if (this._holdTime === null) {
493 this._holdTime = Math.min(this._previousCurrentTime, 0);
494 }
495 return this._pauseTimeLag;
496 }
497
498 // Apply limiting at end of interval when playing forwards
499 var sourceContentEnd = this.source ? this.source.endTime : 0;
500 if (this.playbackRate > 0 &&
501 this._unlimitedCurrentTime >= sourceContentEnd) {
502 if (this._holdTime === null) {
503 this._holdTime = Math.max(this._previousCurrentTime, sourceContentEnd);
504 }
505 return this._pauseTimeLag;
506 }
507
508 // Finished limiting so store pause time lag
509 if (this._holdTime !== null) {
510 this._storedTimeLag = this._pauseTimeLag;
511 this._holdTime = null;
512 }
513
514 return this._storedTimeLag;
515 },
516 get _pauseTimeLag() {
517 return ((this.timeline.currentTime || 0) - this.startTime) *
518 this.playbackRate - this._holdTime;
519 },
520 set startTime(startTime) {
521 enterModifyCurrentAnimationState();
522 try {
523 // This seeks by updating _startTime and hence the currentTime. It does
524 // not affect _storedTimeLag.
525 this._startTime = startTime;
526 this._holdTime = null;
527 playersAreSorted = false;
528 this._update();
529 maybeRestartAnimation();
530 } finally {
531 exitModifyCurrentAnimationState(repeatLastTick);
532 }
533 },
534 get startTime() {
535 return this._startTime;
536 },
537 set _paused(isPaused) {
538 if (isPaused === this._pausedState) {
539 return;
540 }
541 if (this._pausedState) {
542 this._storedTimeLag = this.timeLag;
543 this._holdTime = null;
544 maybeRestartAnimation();
545 } else {
546 this._holdTime = this.currentTime;
547 }
548 this._pausedState = isPaused;
549 },
550 get paused() {
551 return this._pausedState;
552 },
553 get timeline() {
554 return this._timeline;
555 },
556 set playbackRate(playbackRate) {
557 enterModifyCurrentAnimationState();
558 try {
559 var cachedCurrentTime = this.currentTime;
560 // This will impact currentTime, so perform a compensatory seek.
561 this._playbackRate = playbackRate;
562 this.currentTime = cachedCurrentTime;
563 } finally {
564 exitModifyCurrentAnimationState(repeatLastTick);
565 }
566 },
567 get playbackRate() {
568 return this._playbackRate;
569 },
570 get finished() {
571 return this._isLimited;
572 },
573 get _isLimited() {
574 var sourceEnd = this.source ? this.source.endTime : 0;
575 return ((this.playbackRate > 0 && this.currentTime >= sourceEnd) ||
576 (this.playbackRate < 0 && this.currentTime <= 0));
577 },
578 cancel: function() {
579 this.source = null;
580 },
581 finish: function() {
582 if (this.playbackRate < 0) {
583 this.currentTime = 0;
584 } else if (this.playbackRate > 0) {
585 var sourceEndTime = this.source ? this.source.endTime : 0;
586 if (sourceEndTime === Infinity) {
587 throw new Error('InvalidStateError');
588 }
589 this.currentTime = sourceEndTime;
590 }
591 },
592 play: function() {
593 this._paused = false;
594 if (!this.source) {
595 return;
596 }
597 if (this.playbackRate > 0 &&
598 (this.currentTime < 0 ||
599 this.currentTime >= this.source.endTime)) {
600 this.currentTime = 0;
601 } else if (this.playbackRate < 0 &&
602 (this.currentTime <= 0 ||
603 this.currentTime > this.source.endTime)) {
604 this.currentTime = this.source.endTime;
605 }
606 },
607 pause: function() {
608 this._paused = true;
609 },
610 reverse: function() {
611 if (this.playbackRate === 0) {
612 return;
613 }
614 if (this.source) {
615 if (this.playbackRate > 0 && this.currentTime >= this.source.endTime) {
616 this.currentTime = this.source.endTime;
617 } else if (this.playbackRate < 0 && this.currentTime < 0) {
618 this.currentTime = 0;
619 }
620 }
621 this.playbackRate = -this.playbackRate;
622 this._paused = false;
623 },
624 _update: function() {
625 if (this.source !== null) {
626 this.source._updateInheritedTime(
627 this.timeline.currentTime === null ? null : this._currentTime);
628 this._registerOnTimeline();
629 }
630 },
631 _hasFutureAnimation: function() {
632 return this.source === null || this.playbackRate === 0 ||
633 this.source._hasFutureAnimation(this.playbackRate > 0);
634 },
635 _isPastEndOfActiveInterval: function() {
636 return this.source === null ||
637 this.source._isPastEndOfActiveInterval();
638 },
639 _isCurrent: function() {
640 return this.source && this.source._isCurrent();
641 },
642 _hasFutureEffect: function() {
643 return this.source && this.source._hasFutureEffect();
644 },
645 _getLeafItemsInEffect: function(items) {
646 if (this.source) {
647 this.source._getLeafItemsInEffect(items);
648 }
649 },
650 _isTargetingElement: function(element) {
651 return this.source && this.source._isTargetingElement(element);
652 },
653 _getAnimationsTargetingElement: function(element, animations) {
654 if (this.source) {
655 this.source._getAnimationsTargetingElement(element, animations);
656 }
657 },
658 set onfinish(handler) {
659 return setOnEventHandler(this, 'finish', handler);
660 },
661 get onfinish() {
662 return getOnEventHandler(this, 'finish');
663 },
664 addEventListener: function(type, handler) {
665 if (type === 'finish') {
666 addEventHandler(this, type, handler);
667 }
668 },
669 removeEventListener: function(type, handler) {
670 if (type === 'finish') {
671 removeEventHandler(this, type, handler);
672 }
673 },
674 _generateEvents: function() {
675 if (!this._finishedFlag && this.finished &&
676 hasEventHandlersForEvent(this, 'finish')) {
677 var event = new AnimationPlayerEvent('finish', {
678 currentTime: this.currentTime,
679 timelineTime: this.timeline.currentTime
680 });
681 event._initialize(this);
682 callEventHandlers(this, 'finish', event);
683 }
684 this._finishedFlag = this.finished;
685
686 // The following code is for deprecated TimedItem event handling and should
687 // be removed once we stop supporting it.
688 if (!isDefinedAndNotNull(this._lastCurrentTime)) {
689 this._lastCurrentTime = 0;
690 }
691
692 this._lastCurrentTime = this._unlimitedCurrentTime;
693 },
694 _registerOnTimeline: function() {
695 if (!this._registeredOnTimeline) {
696 PLAYERS.push(this);
697 this._registeredOnTimeline = true;
698 }
699 },
700 _deregisterFromTimeline: function() {
701 PLAYERS.splice(PLAYERS.indexOf(this), 1);
702 this._registeredOnTimeline = false;
703 }
704 };
705
706
707
708 /** @constructor */
709 var AnimationPlayerEvent = function(type, eventInit) {
710 this._type = type;
711 this.currentTime = eventInit.currentTime;
712 this.timelineTime = eventInit.timelineTime;
713 };
714
715 AnimationPlayerEvent.prototype = createEventPrototype();
716
717
718
719 /** @constructor */
720 var TimedItem = function(token, timingInput) {
721 if (token !== constructorToken) {
722 throw new TypeError('Illegal constructor');
723 }
724 this.timing = new Timing(
725 constructorToken, timingInput,
726 this._specifiedTimingModified.bind(this));
727 this._inheritedTime = null;
728 this.currentIteration = null;
729 this._iterationTime = null;
730 this._animationTime = null;
731 this._startTime = 0.0;
732 this._player = null;
733 this._parent = null;
734 this._updateInternalState();
735 this._fill = this._resolveFillMode(this.timing.fill);
736 initializeEventTarget(this);
737 };
738
739 TimedItem.prototype = {
740 // TODO: It would be good to avoid the need for this. We would need to modify
741 // call sites to instead rely on a call from the parent.
742 get _effectiveParentTime() {
743 return this.parent !== null && this.parent._iterationTime !== null ?
744 this.parent._iterationTime : 0;
745 },
746 get localTime() {
747 return this._inheritedTime === null ?
748 null : this._inheritedTime - this._startTime;
749 },
750 get startTime() {
751 return this._startTime;
752 },
753 get duration() {
754 var result = this.timing._duration();
755 if (result === 'auto') {
756 result = this._intrinsicDuration();
757 }
758 return result;
759 },
760 get activeDuration() {
761 var repeatedDuration = this.duration * this.timing._iterations();
762 return repeatedDuration / Math.abs(this.timing.playbackRate);
763 },
764 get endTime() {
765 return this._startTime + this.activeDuration + this.timing.delay +
766 this.timing.endDelay;
767 },
768 get parent() {
769 return this._parent;
770 },
771 get previousSibling() {
772 if (!this.parent) {
773 return null;
774 }
775 var siblingIndex = this.parent.indexOf(this) - 1;
776 if (siblingIndex < 0) {
777 return null;
778 }
779 return this.parent.children[siblingIndex];
780 },
781 get nextSibling() {
782 if (!this.parent) {
783 return null;
784 }
785 var siblingIndex = this.parent.indexOf(this) + 1;
786 if (siblingIndex >= this.parent.children.length) {
787 return null;
788 }
789 return this.parent.children[siblingIndex];
790 },
791 _attach: function(player) {
792 // Remove ourselves from our parent, if we have one. This also removes any
793 // exsisting player.
794 this._reparent(null);
795 this._player = player;
796 },
797 // Takes care of updating the outgoing parent. This is called with a non-null
798 // parent only from TimingGroup.splice(), which takes care of calling
799 // TimingGroup._childrenStateModified() for the new parent.
800 _reparent: function(parent) {
801 if (parent === this) {
802 throw new Error('parent can not be set to self!');
803 }
804 enterModifyCurrentAnimationState();
805 try {
806 if (this._player !== null) {
807 this._player.source = null;
808 this._player = null;
809 }
810 if (this.parent !== null) {
811 this.remove();
812 }
813 this._parent = parent;
814 // In the case of a AnimationSequence parent, _startTime will be updated
815 // by TimingGroup.splice().
816 if (this.parent === null || this.parent.type !== 'seq') {
817 this._startTime =
818 this._stashedStartTime === undefined ? 0.0 : this._stashedStartTime;
819 this._stashedStartTime = undefined;
820 }
821 // In the case of the parent being non-null, _childrenStateModified() will
822 // call this via _updateChildInheritedTimes().
823 // TODO: Consider optimising this case by skipping this call.
824 this._updateTimeMarkers();
825 } finally {
826 exitModifyCurrentAnimationState(
827 Boolean(this.player) ? repeatLastTick : null);
828 }
829 },
830 _intrinsicDuration: function() {
831 return 0.0;
832 },
833 _resolveFillMode: abstractMethod,
834 _updateInternalState: function() {
835 this._fill = this._resolveFillMode(this.timing.fill);
836 if (this.parent) {
837 this.parent._childrenStateModified();
838 } else if (this._player) {
839 this._player._registerOnTimeline();
840 }
841 this._updateTimeMarkers();
842 },
843 _specifiedTimingModified: function() {
844 enterModifyCurrentAnimationState();
845 try {
846 this._updateInternalState();
847 } finally {
848 exitModifyCurrentAnimationState(
849 Boolean(this.player) ? repeatLastTick : null);
850 }
851 },
852 // We push time down to children. We could instead have children pull from
853 // above, but this is tricky because a TimedItem may use either a parent
854 // TimedItem or an AnimationPlayer. This requires either logic in
855 // TimedItem, or for TimedItem and AnimationPlayer to implement Timeline
856 // (or an equivalent), both of which are ugly.
857 _updateInheritedTime: function(inheritedTime) {
858 this._inheritedTime = inheritedTime;
859 this._updateTimeMarkers();
860 },
861 _updateAnimationTime: function() {
862 if (this.localTime < this.timing.delay) {
863 if (this._fill === 'backwards' ||
864 this._fill === 'both') {
865 this._animationTime = 0;
866 } else {
867 this._animationTime = null;
868 }
869 } else if (this.localTime <
870 this.timing.delay + this.activeDuration) {
871 this._animationTime = this.localTime - this.timing.delay;
872 } else {
873 if (this._fill === 'forwards' ||
874 this._fill === 'both') {
875 this._animationTime = this.activeDuration;
876 } else {
877 this._animationTime = null;
878 }
879 }
880 },
881 _updateIterationParamsZeroDuration: function() {
882 this._iterationTime = 0;
883 var isAtEndOfIterations = this.timing._iterations() !== 0 &&
884 this.localTime >= this.timing.delay;
885 this.currentIteration = (
886 isAtEndOfIterations ?
887 this._floorWithOpenClosedRange(
888 this.timing.iterationStart + this.timing._iterations(),
889 1.0) :
890 this._floorWithClosedOpenRange(this.timing.iterationStart, 1.0));
891 // Equivalent to unscaledIterationTime below.
892 var unscaledFraction = (
893 isAtEndOfIterations ?
894 this._modulusWithOpenClosedRange(
895 this.timing.iterationStart + this.timing._iterations(),
896 1.0) :
897 this._modulusWithClosedOpenRange(this.timing.iterationStart, 1.0));
898 var timingFunction = this.timing._timingFunction(this);
899 this._timeFraction = (
900 this._isCurrentDirectionForwards() ?
901 unscaledFraction :
902 1.0 - unscaledFraction);
903 ASSERT_ENABLED && assert(
904 this._timeFraction >= 0.0 && this._timeFraction <= 1.0,
905 'Time fraction should be in the range [0, 1]');
906 if (timingFunction) {
907 this._timeFraction = timingFunction.scaleTime(this._timeFraction);
908 }
909 },
910 _getAdjustedAnimationTime: function(animationTime) {
911 var startOffset =
912 multiplyZeroGivesZero(this.timing.iterationStart, this.duration);
913 return (this.timing.playbackRate < 0 ?
914 (animationTime - this.activeDuration) : animationTime) *
915 this.timing.playbackRate + startOffset;
916 },
917 _scaleIterationTime: function(unscaledIterationTime) {
918 return this._isCurrentDirectionForwards() ?
919 unscaledIterationTime :
920 this.duration - unscaledIterationTime;
921 },
922 _updateIterationParams: function() {
923 var adjustedAnimationTime =
924 this._getAdjustedAnimationTime(this._animationTime);
925 var repeatedDuration = this.duration * this.timing._iterations();
926 var startOffset = this.timing.iterationStart * this.duration;
927 var isAtEndOfIterations = (this.timing._iterations() !== 0) &&
928 (adjustedAnimationTime - startOffset === repeatedDuration);
929 this.currentIteration = isAtEndOfIterations ?
930 this._floorWithOpenClosedRange(
931 adjustedAnimationTime, this.duration) :
932 this._floorWithClosedOpenRange(
933 adjustedAnimationTime, this.duration);
934 var unscaledIterationTime = isAtEndOfIterations ?
935 this._modulusWithOpenClosedRange(
936 adjustedAnimationTime, this.duration) :
937 this._modulusWithClosedOpenRange(
938 adjustedAnimationTime, this.duration);
939 this._iterationTime = this._scaleIterationTime(unscaledIterationTime);
940 if (this.duration == Infinity) {
941 this._timeFraction = 0;
942 return;
943 }
944 this._timeFraction = this._iterationTime / this.duration;
945 ASSERT_ENABLED && assert(
946 this._timeFraction >= 0.0 && this._timeFraction <= 1.0,
947 'Time fraction should be in the range [0, 1], got ' +
948 this._timeFraction + ' ' + this._iterationTime + ' ' +
949 this.duration + ' ' + isAtEndOfIterations + ' ' +
950 unscaledIterationTime);
951 var timingFunction = this.timing._timingFunction(this);
952 if (timingFunction) {
953 this._timeFraction = timingFunction.scaleTime(this._timeFraction);
954 }
955 this._iterationTime = this._timeFraction * this.duration;
956 },
957 _updateTimeMarkers: function() {
958 if (this.localTime === null) {
959 this._animationTime = null;
960 this._iterationTime = null;
961 this.currentIteration = null;
962 this._timeFraction = null;
963 return false;
964 }
965 this._updateAnimationTime();
966 if (this._animationTime === null) {
967 this._iterationTime = null;
968 this.currentIteration = null;
969 this._timeFraction = null;
970 } else if (this.duration === 0) {
971 this._updateIterationParamsZeroDuration();
972 } else {
973 this._updateIterationParams();
974 }
975 maybeRestartAnimation();
976 },
977 _floorWithClosedOpenRange: function(x, range) {
978 return Math.floor(x / range);
979 },
980 _floorWithOpenClosedRange: function(x, range) {
981 return Math.ceil(x / range) - 1;
982 },
983 _modulusWithClosedOpenRange: function(x, range) {
984 ASSERT_ENABLED && assert(
985 range > 0, 'Range must be strictly positive');
986 var modulus = x % range;
987 var result = modulus < 0 ? modulus + range : modulus;
988 ASSERT_ENABLED && assert(
989 result >= 0.0 && result < range,
990 'Result should be in the range [0, range)');
991 return result;
992 },
993 _modulusWithOpenClosedRange: function(x, range) {
994 var modulus = this._modulusWithClosedOpenRange(x, range);
995 var result = modulus === 0 ? range : modulus;
996 ASSERT_ENABLED && assert(
997 result > 0.0 && result <= range,
998 'Result should be in the range (0, range]');
999 return result;
1000 },
1001 _isCurrentDirectionForwards: function() {
1002 if (this.timing.direction === 'normal') {
1003 return true;
1004 }
1005 if (this.timing.direction === 'reverse') {
1006 return false;
1007 }
1008 var d = this.currentIteration;
1009 if (this.timing.direction === 'alternate-reverse') {
1010 d += 1;
1011 }
1012 // TODO: 6.13.3 step 3. wtf?
1013 return d % 2 === 0;
1014 },
1015 clone: abstractMethod,
1016 before: function() {
1017 var newItems = [];
1018 for (var i = 0; i < arguments.length; i++) {
1019 newItems.push(arguments[i]);
1020 }
1021 this.parent._splice(this.parent.indexOf(this), 0, newItems);
1022 },
1023 after: function() {
1024 var newItems = [];
1025 for (var i = 0; i < arguments.length; i++) {
1026 newItems.push(arguments[i]);
1027 }
1028 this.parent._splice(this.parent.indexOf(this) + 1, 0, newItems);
1029 },
1030 replace: function() {
1031 var newItems = [];
1032 for (var i = 0; i < arguments.length; i++) {
1033 newItems.push(arguments[i]);
1034 }
1035 this.parent._splice(this.parent.indexOf(this), 1, newItems);
1036 },
1037 remove: function() {
1038 this.parent._splice(this.parent.indexOf(this), 1);
1039 },
1040 // Gets the leaf TimedItems currently in effect. Note that this is a superset
1041 // of the leaf TimedItems in their active interval, as a TimedItem can have an
1042 // effect outside its active interval due to fill.
1043 _getLeafItemsInEffect: function(items) {
1044 if (this._timeFraction !== null) {
1045 this._getLeafItemsInEffectImpl(items);
1046 }
1047 },
1048 _getLeafItemsInEffectImpl: abstractMethod,
1049 _hasFutureAnimation: function(timeDirectionForwards) {
1050 return timeDirectionForwards ? this._inheritedTime < this.endTime :
1051 this._inheritedTime > this.startTime;
1052 },
1053 _isPastEndOfActiveInterval: function() {
1054 return this._inheritedTime >= this.endTime;
1055 },
1056 get player() {
1057 return this.parent === null ?
1058 this._player : this.parent.player;
1059 },
1060 _isCurrent: function() {
1061 return !this._isPastEndOfActiveInterval() ||
1062 (this.parent !== null && this.parent._isCurrent());
1063 },
1064 _isTargetingElement: abstractMethod,
1065 _getAnimationsTargetingElement: abstractMethod,
1066 _netEffectivePlaybackRate: function() {
1067 var effectivePlaybackRate = this._isCurrentDirectionForwards() ?
1068 this.timing.playbackRate : -this.timing.playbackRate;
1069 return this.parent === null ? effectivePlaybackRate :
1070 effectivePlaybackRate * this.parent._netEffectivePlaybackRate();
1071 },
1072 // Note that this restriction is currently incomplete - for example,
1073 // Animations which are playing forwards and have a fill of backwards
1074 // are not in effect unless current.
1075 // TODO: Complete this restriction.
1076 _hasFutureEffect: function() {
1077 return this._isCurrent() || this._fill !== 'none';
1078 },
1079 _toSubRanges: function(fromTime, toTime, iterationTimes) {
1080 if (fromTime > toTime) {
1081 var revRanges = this._toSubRanges(toTime, fromTime, iterationTimes);
1082 revRanges.ranges.forEach(function(a) { a.reverse(); });
1083 revRanges.ranges.reverse();
1084 revRanges.start = iterationTimes.length - revRanges.start - 1;
1085 revRanges.delta = -1;
1086 return revRanges;
1087 }
1088 var skipped = 0;
1089 // TODO: this should be calculatable. This would be more efficient
1090 // than searching through the list.
1091 while (iterationTimes[skipped] < fromTime) {
1092 skipped++;
1093 }
1094 var currentStart = fromTime;
1095 var ranges = [];
1096 for (var i = skipped; i < iterationTimes.length; i++) {
1097 if (iterationTimes[i] < toTime) {
1098 ranges.push([currentStart, iterationTimes[i]]);
1099 currentStart = iterationTimes[i];
1100 } else {
1101 ranges.push([currentStart, toTime]);
1102 return {start: skipped, delta: 1, ranges: ranges};
1103 }
1104 }
1105 ranges.push([currentStart, toTime]);
1106 return {start: skipped, delta: 1, ranges: ranges};
1107 }
1108 };
1109
1110 var TimingEvent = function(
1111 token, target, type, localTime, timelineTime, iterationIndex, seeked) {
1112 if (token !== constructorToken) {
1113 throw new TypeError('Illegal constructor');
1114 }
1115 this._initialize(target);
1116 this._type = type;
1117 this.localTime = localTime;
1118 this.timelineTime = timelineTime;
1119 this.iterationIndex = iterationIndex;
1120 this.seeked = seeked ? true : false;
1121 };
1122
1123 TimingEvent.prototype = createEventPrototype();
1124
1125 var isEffectCallback = function(animationEffect) {
1126 return typeof animationEffect === 'function';
1127 };
1128
1129 var interpretAnimationEffect = function(animationEffect) {
1130 if (animationEffect instanceof AnimationEffect ||
1131 isEffectCallback(animationEffect)) {
1132 return animationEffect;
1133 } else if (isDefinedAndNotNull(animationEffect) &&
1134 typeof animationEffect === 'object') {
1135 // The spec requires animationEffect to be an instance of
1136 // OneOrMoreKeyframes, but this type is just a dictionary or a list of
1137 // dictionaries, so the best we can do is test for an object.
1138 return new KeyframeEffect(animationEffect);
1139 }
1140 return null;
1141 };
1142
1143 var cloneAnimationEffect = function(animationEffect) {
1144 if (animationEffect instanceof AnimationEffect) {
1145 return animationEffect.clone();
1146 } else if (isEffectCallback(animationEffect)) {
1147 return animationEffect;
1148 } else {
1149 return null;
1150 }
1151 };
1152
1153
1154
1155 /** @constructor */
1156 var Animation = function(target, animationEffect, timingInput) {
1157 enterModifyCurrentAnimationState();
1158 try {
1159 TimedItem.call(this, constructorToken, timingInput);
1160 this.effect = interpretAnimationEffect(animationEffect);
1161 this._target = target;
1162 } finally {
1163 exitModifyCurrentAnimationState(null);
1164 }
1165 };
1166
1167 Animation.prototype = createObject(TimedItem.prototype, {
1168 _resolveFillMode: function(fillMode) {
1169 return fillMode === 'auto' ? 'none' : fillMode;
1170 },
1171 _sample: function() {
1172 if (isDefinedAndNotNull(this.effect) &&
1173 !(this.target instanceof PseudoElementReference)) {
1174 if (isEffectCallback(this.effect)) {
1175 this.effect(this._timeFraction, this.target, this);
1176 } else {
1177 this.effect._sample(this._timeFraction, this.currentIteration,
1178 this.target, this.underlyingValue);
1179 }
1180 }
1181 },
1182 _getLeafItemsInEffectImpl: function(items) {
1183 items.push(this);
1184 },
1185 _isTargetingElement: function(element) {
1186 return element === this.target;
1187 },
1188 _getAnimationsTargetingElement: function(element, animations) {
1189 if (this._isTargetingElement(element)) {
1190 animations.push(this);
1191 }
1192 },
1193 get target() {
1194 return this._target;
1195 },
1196 set effect(effect) {
1197 enterModifyCurrentAnimationState();
1198 try {
1199 this._effect = effect;
1200 this.timing._invalidateTimingFunction();
1201 } finally {
1202 exitModifyCurrentAnimationState(
1203 Boolean(this.player) ? repeatLastTick : null);
1204 }
1205 },
1206 get effect() {
1207 return this._effect;
1208 },
1209 clone: function() {
1210 return new Animation(this.target,
1211 cloneAnimationEffect(this.effect), this.timing._dict);
1212 },
1213 toString: function() {
1214 var effectString = '<none>';
1215 if (this.effect instanceof AnimationEffect) {
1216 effectString = this.effect.toString();
1217 } else if (isEffectCallback(this.effect)) {
1218 effectString = 'Effect callback';
1219 }
1220 return 'Animation ' + this.startTime + '-' + this.endTime + ' (' +
1221 this.localTime + ') ' + effectString;
1222 }
1223 });
1224
1225 function throwNewHierarchyRequestError() {
1226 var element = document.createElement('span');
1227 element.appendChild(element);
1228 }
1229
1230
1231
1232 /** @constructor */
1233 var TimedItemList = function(token, children) {
1234 if (token !== constructorToken) {
1235 throw new TypeError('Illegal constructor');
1236 }
1237 this._children = children;
1238 this._getters = 0;
1239 this._ensureGetters();
1240 };
1241
1242 TimedItemList.prototype = {
1243 get length() {
1244 return this._children.length;
1245 },
1246 _ensureGetters: function() {
1247 while (this._getters < this._children.length) {
1248 this._ensureGetter(this._getters++);
1249 }
1250 },
1251 _ensureGetter: function(i) {
1252 Object.defineProperty(this, i, {
1253 get: function() {
1254 return this._children[i];
1255 }
1256 });
1257 }
1258 };
1259
1260
1261
1262 /** @constructor */
1263 var TimingGroup = function(token, type, children, timing) {
1264 if (token !== constructorToken) {
1265 throw new TypeError('Illegal constructor');
1266 }
1267 // Take a copy of the children array, as it could be modified as a side-effect
1268 // of creating this object. See
1269 // https://github.com/web-animations/web-animations-js/issues/65 for details.
1270 var childrenCopy = (children && Array.isArray(children)) ?
1271 children.slice() : [];
1272 // used by TimedItem via _intrinsicDuration(), so needs to be set before
1273 // initializing super.
1274 this.type = type || 'par';
1275 this._children = [];
1276 this._cachedTimedItemList = null;
1277 this._cachedIntrinsicDuration = null;
1278 TimedItem.call(this, constructorToken, timing);
1279 // We add children after setting the parent. This means that if an ancestor
1280 // (including the parent) is specified as a child, it will be removed from our
1281 // ancestors and used as a child,
1282 this.append.apply(this, childrenCopy);
1283 };
1284
1285 TimingGroup.prototype = createObject(TimedItem.prototype, {
1286 _resolveFillMode: function(fillMode) {
1287 return fillMode === 'auto' ? 'both' : fillMode;
1288 },
1289 _childrenStateModified: function() {
1290 // See _updateChildStartTimes().
1291 this._isInChildrenStateModified = true;
1292 if (this._cachedTimedItemList) {
1293 this._cachedTimedItemList._ensureGetters();
1294 }
1295 this._cachedIntrinsicDuration = null;
1296
1297 // We need to walk up and down the tree to re-layout. endTime and the
1298 // various durations (which are all calculated lazily) are the only
1299 // properties of a TimedItem which can affect the layout of its ancestors.
1300 // So it should be sufficient to simply update start times and time markers
1301 // on the way down.
1302
1303 // This calls up to our parent, then calls _updateTimeMarkers().
1304 this._updateInternalState();
1305 this._updateChildInheritedTimes();
1306
1307 // Update child start times before walking down.
1308 this._updateChildStartTimes();
1309
1310 this._isInChildrenStateModified = false;
1311 },
1312 _updateInheritedTime: function(inheritedTime) {
1313 this._inheritedTime = inheritedTime;
1314 this._updateTimeMarkers();
1315 this._updateChildInheritedTimes();
1316 },
1317 _updateChildInheritedTimes: function() {
1318 for (var i = 0; i < this._children.length; i++) {
1319 var child = this._children[i];
1320 child._updateInheritedTime(this._iterationTime);
1321 }
1322 },
1323 _updateChildStartTimes: function() {
1324 if (this.type === 'seq') {
1325 var cumulativeStartTime = 0;
1326 for (var i = 0; i < this._children.length; i++) {
1327 var child = this._children[i];
1328 if (child._stashedStartTime === undefined) {
1329 child._stashedStartTime = child._startTime;
1330 }
1331 child._startTime = cumulativeStartTime;
1332 // Avoid updating the child's inherited time and time markers if this is
1333 // about to be done in the down phase of _childrenStateModified().
1334 if (!child._isInChildrenStateModified) {
1335 // This calls _updateTimeMarkers() on the child.
1336 child._updateInheritedTime(this._iterationTime);
1337 }
1338 cumulativeStartTime += Math.max(0, child.timing.delay +
1339 child.activeDuration + child.timing.endDelay);
1340 }
1341 }
1342 },
1343 get children() {
1344 if (!this._cachedTimedItemList) {
1345 this._cachedTimedItemList = new TimedItemList(
1346 constructorToken, this._children);
1347 }
1348 return this._cachedTimedItemList;
1349 },
1350 get firstChild() {
1351 return this._children[0];
1352 },
1353 get lastChild() {
1354 return this._children[this.children.length - 1];
1355 },
1356 _intrinsicDuration: function() {
1357 if (!isDefinedAndNotNull(this._cachedIntrinsicDuration)) {
1358 if (this.type === 'par') {
1359 var dur = Math.max.apply(undefined, this._children.map(function(a) {
1360 return a.endTime;
1361 }));
1362 this._cachedIntrinsicDuration = Math.max(0, dur);
1363 } else if (this.type === 'seq') {
1364 var result = 0;
1365 this._children.forEach(function(a) {
1366 result += a.activeDuration + a.timing.delay + a.timing.endDelay;
1367 });
1368 this._cachedIntrinsicDuration = result;
1369 } else {
1370 throw 'Unsupported type ' + this.type;
1371 }
1372 }
1373 return this._cachedIntrinsicDuration;
1374 },
1375 _getLeafItemsInEffectImpl: function(items) {
1376 for (var i = 0; i < this._children.length; i++) {
1377 this._children[i]._getLeafItemsInEffect(items);
1378 }
1379 },
1380 clone: function() {
1381 var children = [];
1382 this._children.forEach(function(child) {
1383 children.push(child.clone());
1384 });
1385 return this.type === 'par' ?
1386 new AnimationGroup(children, this.timing._dict) :
1387 new AnimationSequence(children, this.timing._dict);
1388 },
1389 clear: function() {
1390 this._splice(0, this._children.length);
1391 },
1392 append: function() {
1393 var newItems = [];
1394 for (var i = 0; i < arguments.length; i++) {
1395 newItems.push(arguments[i]);
1396 }
1397 this._splice(this._children.length, 0, newItems);
1398 },
1399 prepend: function() {
1400 var newItems = [];
1401 for (var i = 0; i < arguments.length; i++) {
1402 newItems.push(arguments[i]);
1403 }
1404 this._splice(0, 0, newItems);
1405 },
1406 _addInternal: function(child) {
1407 this._children.push(child);
1408 this._childrenStateModified();
1409 },
1410 indexOf: function(item) {
1411 return this._children.indexOf(item);
1412 },
1413 _splice: function(start, deleteCount, newItems) {
1414 enterModifyCurrentAnimationState();
1415 try {
1416 var args = arguments;
1417 if (args.length === 3) {
1418 args = [start, deleteCount].concat(newItems);
1419 }
1420 for (var i = 2; i < args.length; i++) {
1421 var newChild = args[i];
1422 if (this._isInclusiveAncestor(newChild)) {
1423 throwNewHierarchyRequestError();
1424 }
1425 newChild._reparent(this);
1426 }
1427 var result = Array.prototype.splice.apply(this._children, args);
1428 for (var i = 0; i < result.length; i++) {
1429 result[i]._parent = null;
1430 }
1431 this._childrenStateModified();
1432 return result;
1433 } finally {
1434 exitModifyCurrentAnimationState(
1435 Boolean(this.player) ? repeatLastTick : null);
1436 }
1437 },
1438 _isInclusiveAncestor: function(item) {
1439 for (var ancestor = this; ancestor !== null; ancestor = ancestor.parent) {
1440 if (ancestor === item) {
1441 return true;
1442 }
1443 }
1444 return false;
1445 },
1446 _isTargetingElement: function(element) {
1447 return this._children.some(function(child) {
1448 return child._isTargetingElement(element);
1449 });
1450 },
1451 _getAnimationsTargetingElement: function(element, animations) {
1452 this._children.map(function(child) {
1453 return child._getAnimationsTargetingElement(element, animations);
1454 });
1455 },
1456 toString: function() {
1457 return this.type + ' ' + this.startTime + '-' + this.endTime + ' (' +
1458 this.localTime + ') ' + ' [' +
1459 this._children.map(function(a) { return a.toString(); }) + ']';
1460 }
1461 });
1462
1463
1464
1465 /** @constructor */
1466 var AnimationGroup = function(children, timing, parent) {
1467 TimingGroup.call(this, constructorToken, 'par', children, timing, parent);
1468 };
1469
1470 AnimationGroup.prototype = Object.create(TimingGroup.prototype);
1471
1472
1473
1474 /** @constructor */
1475 var AnimationSequence = function(children, timing, parent) {
1476 TimingGroup.call(this, constructorToken, 'seq', children, timing, parent);
1477 };
1478
1479 AnimationSequence.prototype = Object.create(TimingGroup.prototype);
1480
1481
1482
1483 /** @constructor */
1484 var PseudoElementReference = function(element, pseudoElement) {
1485 this.element = element;
1486 this.pseudoElement = pseudoElement;
1487 console.warn('PseudoElementReference is not supported.');
1488 };
1489
1490
1491
1492 /** @constructor */
1493 var MediaReference = function(mediaElement, timing, parent, delta) {
1494 TimedItem.call(this, constructorToken, timing, parent);
1495 this._media = mediaElement;
1496
1497 // We can never be sure when _updateInheritedTime() is going to be called
1498 // next, due to skipped frames or the player being seeked. Plus the media
1499 // element's currentTime may drift from our iterationTime. So if a media
1500 // element has loop set, we can't be sure that we'll stop it before it wraps.
1501 // For this reason, we simply disable looping.
1502 // TODO: Maybe we should let it loop if our duration exceeds it's
1503 // length?
1504 this._media.loop = false;
1505
1506 // If the media element has a media controller, we detach it. This mirrors the
1507 // behaviour when re-parenting a TimedItem, or attaching one to an
1508 // AnimationPlayer.
1509 // TODO: It would be neater to assign to MediaElement.controller, but this was
1510 // broken in Chrome until recently. See crbug.com/226270.
1511 this._media.mediaGroup = '';
1512
1513 this._delta = delta;
1514 };
1515
1516 MediaReference.prototype = createObject(TimedItem.prototype, {
1517 _resolveFillMode: function(fillMode) {
1518 // TODO: Fill modes for MediaReferences are still undecided. The spec is not
1519 // clear what 'auto' should mean for TimedItems other than Animations and
1520 // groups.
1521 return fillMode === 'auto' ? 'none' : fillMode;
1522 },
1523 _intrinsicDuration: function() {
1524 // TODO: This should probably default to zero. But doing so means that as
1525 // soon as our inheritedTime is zero, the polyfill deems the animation to be
1526 // done and stops ticking, so we don't get any further calls to
1527 // _updateInheritedTime(). One way around this would be to modify
1528 // TimedItem._isPastEndOfActiveInterval() to recurse down the tree, then we
1529 // could override it here.
1530 return isNaN(this._media.duration) ?
1531 Infinity : this._media.duration / this._media.defaultPlaybackRate;
1532 },
1533 _unscaledMediaCurrentTime: function() {
1534 return this._media.currentTime / this._media.defaultPlaybackRate;
1535 },
1536 _getLeafItemsInEffectImpl: function(items) {
1537 items.push(this);
1538 },
1539 _ensurePlaying: function() {
1540 // The media element is paused when created.
1541 if (this._media.paused) {
1542 this._media.play();
1543 }
1544 },
1545 _ensurePaused: function() {
1546 if (!this._media.paused) {
1547 this._media.pause();
1548 }
1549 },
1550 _isSeekableUnscaledTime: function(time) {
1551 var seekTime = time * this._media.defaultPlaybackRate;
1552 var ranges = this._media.seekable;
1553 for (var i = 0; i < ranges.length; i++) {
1554 if (seekTime >= ranges.start(i) && seekTime <= ranges.end(i)) {
1555 return true;
1556 }
1557 }
1558 return false;
1559 },
1560 // Note that a media element's timeline may not start at zero, although its
1561 // duration is always the timeline time at the end point. This means that an
1562 // element's duration isn't always it's length and not all values of the
1563 // timline are seekable. Furthermore, some types of media further limit the
1564 // range of seekable timeline times. For this reason, we always map an
1565 // iteration to the range [0, duration] and simply seek to the nearest
1566 // seekable time.
1567 _ensureIsAtUnscaledTime: function(time) {
1568 if (this._unscaledMediaCurrentTime() !== time) {
1569 this._media.currentTime = time * this._media.defaultPlaybackRate;
1570 }
1571 },
1572 // This is called by the polyfill on each tick when our AnimationPlayer's tree
1573 // is active.
1574 _updateInheritedTime: function(inheritedTime) {
1575 this._inheritedTime = inheritedTime;
1576 this._updateTimeMarkers();
1577
1578 // The polyfill uses a sampling model whereby time values are propagated
1579 // down the tree at each sample. However, for the media item, we need to use
1580 // play() and pause().
1581
1582 // Handle the case of being outside our effect interval.
1583 if (this._iterationTime === null) {
1584 this._ensureIsAtUnscaledTime(0);
1585 this._ensurePaused();
1586 return;
1587 }
1588
1589 if (this._iterationTime >= this._intrinsicDuration()) {
1590 // Our iteration time exceeds the media element's duration, so just make
1591 // sure the media element is at the end. It will stop automatically, but
1592 // that could take some time if the seek below is significant, so force
1593 // it.
1594 this._ensureIsAtUnscaledTime(this._intrinsicDuration());
1595 this._ensurePaused();
1596 return;
1597 }
1598
1599 var finalIteration = this._floorWithOpenClosedRange(
1600 this.timing.iterationStart + this.timing._iterations(), 1.0);
1601 var endTimeFraction = this._modulusWithOpenClosedRange(
1602 this.timing.iterationStart + this.timing._iterations(), 1.0);
1603 if (this.currentIteration === finalIteration &&
1604 this._timeFraction === endTimeFraction &&
1605 this._intrinsicDuration() >= this.duration) {
1606 // We have reached the end of our final iteration, but the media element
1607 // is not done.
1608 this._ensureIsAtUnscaledTime(this.duration * endTimeFraction);
1609 this._ensurePaused();
1610 return;
1611 }
1612
1613 // Set the appropriate playback rate.
1614 var playbackRate =
1615 this._media.defaultPlaybackRate * this._netEffectivePlaybackRate();
1616 if (this._media.playbackRate !== playbackRate) {
1617 this._media.playbackRate = playbackRate;
1618 }
1619
1620 // Set the appropriate play/pause state. Note that we may not be able to
1621 // seek to the desired time. In this case, the media element's seek
1622 // algorithm repositions the seek to the nearest seekable time. This is OK,
1623 // but in this case, we don't want to play the media element, as it prevents
1624 // us from synchronising properly.
1625 if (this.player.paused ||
1626 !this._isSeekableUnscaledTime(this._iterationTime)) {
1627 this._ensurePaused();
1628 } else {
1629 this._ensurePlaying();
1630 }
1631
1632 // Seek if required. This could be due to our AnimationPlayer being seeked,
1633 // or video slippage. We need to handle the fact that the video may not play
1634 // at exactly the right speed. There's also a variable delay when the video
1635 // is first played.
1636 // TODO: What's the right value for this delta?
1637 var delta = isDefinedAndNotNull(this._delta) ? this._delta :
1638 0.2 * Math.abs(this._media.playbackRate);
1639 if (Math.abs(this._iterationTime - this._unscaledMediaCurrentTime()) >
1640 delta) {
1641 this._ensureIsAtUnscaledTime(this._iterationTime);
1642 }
1643 },
1644 _isTargetingElement: function(element) {
1645 return this._media === element;
1646 },
1647 _getAnimationsTargetingElement: function() { },
1648 _attach: function(player) {
1649 this._ensurePaused();
1650 TimedItem.prototype._attach.call(this, player);
1651 }
1652 });
1653
1654
1655
1656 /** @constructor */
1657 var AnimationEffect = function(token) {
1658 if (token !== constructorToken) {
1659 throw new TypeError('Illegal constructor');
1660 }
1661 };
1662
1663 AnimationEffect.prototype = {
1664 _sample: abstractMethod,
1665 clone: abstractMethod,
1666 toString: abstractMethod
1667 };
1668
1669 var clamp = function(x, min, max) {
1670 return Math.max(Math.min(x, max), min);
1671 };
1672
1673
1674
1675 /** @constructor */
1676 var MotionPathEffect = function(path, autoRotate, angle, composite) {
1677 var iterationComposite = undefined;
1678 var options = autoRotate;
1679 if (typeof options == 'string' || options instanceof String ||
1680 angle || composite) {
1681 // FIXME: add deprecation warning - please pass an options dictionary to
1682 // MotionPathEffect constructor
1683 } else if (options) {
1684 autoRotate = options.autoRotate;
1685 angle = options.angle;
1686 composite = options.composite;
1687 iterationComposite = options.iterationComposite;
1688 }
1689
1690 enterModifyCurrentAnimationState();
1691 try {
1692 AnimationEffect.call(this, constructorToken);
1693
1694 this.composite = composite;
1695 this.iterationComposite = iterationComposite;
1696
1697 // TODO: path argument is not in the spec -- seems useful since
1698 // SVGPathSegList doesn't have a constructor.
1699 this.autoRotate = isDefined(autoRotate) ? autoRotate : 'none';
1700 this.angle = isDefined(angle) ? angle : 0;
1701 this._path = document.createElementNS(SVG_NS, 'path');
1702 if (path instanceof SVGPathSegList) {
1703 this.segments = path;
1704 } else {
1705 var tempPath = document.createElementNS(SVG_NS, 'path');
1706 tempPath.setAttribute('d', String(path));
1707 this.segments = tempPath.pathSegList;
1708 }
1709 } finally {
1710 exitModifyCurrentAnimationState(null);
1711 }
1712 };
1713
1714 MotionPathEffect.prototype = createObject(AnimationEffect.prototype, {
1715 get composite() {
1716 return this._composite;
1717 },
1718 set composite(value) {
1719 enterModifyCurrentAnimationState();
1720 try {
1721 // Use the default value if an invalid string is specified.
1722 this._composite = value === 'add' ? 'add' : 'replace';
1723 } finally {
1724 exitModifyCurrentAnimationState(repeatLastTick);
1725 }
1726 },
1727 get iterationComposite() {
1728 return this._iterationComposite;
1729 },
1730 set iterationComposite(value) {
1731 enterModifyCurrentAnimationState();
1732 try {
1733 // Use the default value if an invalid string is specified.
1734 this._iterationComposite =
1735 value === 'accumulate' ? 'accumulate' : 'replace';
1736 this._updateOffsetPerIteration();
1737 } finally {
1738 exitModifyCurrentAnimationState(repeatLastTick);
1739 }
1740 },
1741 _sample: function(timeFraction, currentIteration, target) {
1742 // TODO: Handle accumulation.
1743 var lengthAtTimeFraction = this._lengthAtTimeFraction(timeFraction);
1744 var point = this._path.getPointAtLength(lengthAtTimeFraction);
1745 var x = point.x - target.offsetWidth / 2;
1746 var y = point.y - target.offsetHeight / 2;
1747 if (currentIteration !== 0 && this._offsetPerIteration) {
1748 x += this._offsetPerIteration.x * currentIteration;
1749 y += this._offsetPerIteration.y * currentIteration;
1750 }
1751 // TODO: calc(point.x - 50%) doesn't work?
1752 var value = [{t: 'translate', d: [{px: x}, {px: y}]}];
1753 var angle = this.angle;
1754 if (this._autoRotate === 'auto-rotate') {
1755 // Super hacks
1756 var lastPoint = this._path.getPointAtLength(lengthAtTimeFraction - 0.01);
1757 var dx = point.x - lastPoint.x;
1758 var dy = point.y - lastPoint.y;
1759 var rotation = Math.atan2(dy, dx);
1760 angle += rotation / 2 / Math.PI * 360;
1761 }
1762 value.push({t: 'rotate', d: [angle]});
1763 compositor.setAnimatedValue(target, 'transform',
1764 new AddReplaceCompositableValue(value, this.composite));
1765 },
1766 _lengthAtTimeFraction: function(timeFraction) {
1767 var segmentCount = this._cumulativeLengths.length - 1;
1768 if (!segmentCount) {
1769 return 0;
1770 }
1771 var scaledFraction = timeFraction * segmentCount;
1772 var index = clamp(Math.floor(scaledFraction), 0, segmentCount);
1773 return this._cumulativeLengths[index] + ((scaledFraction % 1) * (
1774 this._cumulativeLengths[index + 1] - this._cumulativeLengths[index]));
1775 },
1776 _updateOffsetPerIteration: function() {
1777 if (this.iterationComposite === 'accumulate' &&
1778 this._cumulativeLengths &&
1779 this._cumulativeLengths.length > 0) {
1780 this._offsetPerIteration = this._path.getPointAtLength(
1781 this._cumulativeLengths[this._cumulativeLengths.length - 1]);
1782 } else {
1783 this._offsetPerIteration = null;
1784 }
1785 },
1786 clone: function() {
1787 return new MotionPathEffect(this._path.getAttribute('d'));
1788 },
1789 toString: function() {
1790 return '<MotionPathEffect>';
1791 },
1792 set autoRotate(autoRotate) {
1793 enterModifyCurrentAnimationState();
1794 try {
1795 this._autoRotate = String(autoRotate);
1796 } finally {
1797 exitModifyCurrentAnimationState(repeatLastTick);
1798 }
1799 },
1800 get autoRotate() {
1801 return this._autoRotate;
1802 },
1803 set angle(angle) {
1804 enterModifyCurrentAnimationState();
1805 try {
1806 // TODO: This should probably be a string with a unit, but the spec
1807 // says it's a double.
1808 this._angle = Number(angle);
1809 } finally {
1810 exitModifyCurrentAnimationState(repeatLastTick);
1811 }
1812 },
1813 get angle() {
1814 return this._angle;
1815 },
1816 set segments(segments) {
1817 enterModifyCurrentAnimationState();
1818 try {
1819 var targetSegments = this.segments;
1820 targetSegments.clear();
1821 var cumulativeLengths = [0];
1822 // TODO: *moving* the path segments is not correct, but pathSegList
1823 // is read only
1824 var items = segments.numberOfItems;
1825 while (targetSegments.numberOfItems < items) {
1826 var segment = segments.removeItem(0);
1827 targetSegments.appendItem(segment);
1828 if (segment.pathSegType !== SVGPathSeg.PATHSEG_MOVETO_REL &&
1829 segment.pathSegType !== SVGPathSeg.PATHSEG_MOVETO_ABS) {
1830 cumulativeLengths.push(this._path.getTotalLength());
1831 }
1832 }
1833 this._cumulativeLengths = cumulativeLengths;
1834 this._updateOffsetPerIteration();
1835 } finally {
1836 exitModifyCurrentAnimationState(repeatLastTick);
1837 }
1838 },
1839 get segments() {
1840 return this._path.pathSegList;
1841 }
1842 });
1843
1844 var shorthandToLonghand = {
1845 background: [
1846 'backgroundImage',
1847 'backgroundPosition',
1848 'backgroundSize',
1849 'backgroundRepeat',
1850 'backgroundAttachment',
1851 'backgroundOrigin',
1852 'backgroundClip',
1853 'backgroundColor'
1854 ],
1855 border: [
1856 'borderTopColor',
1857 'borderTopStyle',
1858 'borderTopWidth',
1859 'borderRightColor',
1860 'borderRightStyle',
1861 'borderRightWidth',
1862 'borderBottomColor',
1863 'borderBottomStyle',
1864 'borderBottomWidth',
1865 'borderLeftColor',
1866 'borderLeftStyle',
1867 'borderLeftWidth'
1868 ],
1869 borderBottom: [
1870 'borderBottomWidth',
1871 'borderBottomStyle',
1872 'borderBottomColor'
1873 ],
1874 borderColor: [
1875 'borderTopColor',
1876 'borderRightColor',
1877 'borderBottomColor',
1878 'borderLeftColor'
1879 ],
1880 borderLeft: [
1881 'borderLeftWidth',
1882 'borderLeftStyle',
1883 'borderLeftColor'
1884 ],
1885 borderRadius: [
1886 'borderTopLeftRadius',
1887 'borderTopRightRadius',
1888 'borderBottomRightRadius',
1889 'borderBottomLeftRadius'
1890 ],
1891 borderRight: [
1892 'borderRightWidth',
1893 'borderRightStyle',
1894 'borderRightColor'
1895 ],
1896 borderTop: [
1897 'borderTopWidth',
1898 'borderTopStyle',
1899 'borderTopColor'
1900 ],
1901 borderWidth: [
1902 'borderTopWidth',
1903 'borderRightWidth',
1904 'borderBottomWidth',
1905 'borderLeftWidth'
1906 ],
1907 font: [
1908 'fontFamily',
1909 'fontSize',
1910 'fontStyle',
1911 'fontVariant',
1912 'fontWeight',
1913 'lineHeight'
1914 ],
1915 margin: [
1916 'marginTop',
1917 'marginRight',
1918 'marginBottom',
1919 'marginLeft'
1920 ],
1921 outline: [
1922 'outlineColor',
1923 'outlineStyle',
1924 'outlineWidth'
1925 ],
1926 padding: [
1927 'paddingTop',
1928 'paddingRight',
1929 'paddingBottom',
1930 'paddingLeft'
1931 ]
1932 };
1933
1934 // This delegates parsing shorthand value syntax to the browser.
1935 var shorthandExpanderElem = createDummyElement();
1936 var expandShorthand = function(property, value, result) {
1937 shorthandExpanderElem.style[property] = value;
1938 var longProperties = shorthandToLonghand[property];
1939 for (var i in longProperties) {
1940 var longProperty = longProperties[i];
1941 var longhandValue = shorthandExpanderElem.style[longProperty];
1942 result[longProperty] = longhandValue;
1943 }
1944 };
1945
1946 var normalizeKeyframeDictionary = function(properties) {
1947 var result = {
1948 offset: null,
1949 composite: null,
1950 easing: presetTimingFunctions.linear
1951 };
1952 var animationProperties = [];
1953 for (var property in properties) {
1954 // TODO: Apply the CSS property to IDL attribute algorithm.
1955 if (property === 'offset') {
1956 if (typeof properties.offset === 'number') {
1957 result.offset = properties.offset;
1958 }
1959 } else if (property === 'composite') {
1960 if (properties.composite === 'add' ||
1961 properties.composite === 'replace') {
1962 result.composite = properties.composite;
1963 }
1964 } else if (property === 'easing') {
1965 result.easing = TimingFunction.createFromString(properties.easing);
1966 } else {
1967 // TODO: Check whether this is a supported property.
1968 animationProperties.push(property);
1969 }
1970 }
1971 // TODO: Remove prefixed properties if the unprefixed version is also
1972 // supported and present.
1973 animationProperties = animationProperties.sort(playerSortFunction);
1974 for (var i = 0; i < animationProperties.length; i++) {
1975 // TODO: Apply the IDL attribute to CSS property algorithm.
1976 var property = animationProperties[i];
1977 // TODO: The spec does not specify how to handle null values.
1978 // See https://www.w3.org/Bugs/Public/show_bug.cgi?id=22572
1979 var value = isDefinedAndNotNull(properties[property]) ?
1980 properties[property].toString() : '';
1981 if (property in shorthandToLonghand) {
1982 expandShorthand(property, value, result);
1983 } else {
1984 result[property] = value;
1985 }
1986 }
1987 return result;
1988 };
1989
1990
1991
1992 /** @constructor */
1993 var KeyframeEffect = function(oneOrMoreKeyframeDictionaries,
1994 composite) {
1995 enterModifyCurrentAnimationState();
1996 try {
1997 AnimationEffect.call(this, constructorToken);
1998
1999 this.composite = composite;
2000
2001 this.setFrames(oneOrMoreKeyframeDictionaries);
2002 } finally {
2003 exitModifyCurrentAnimationState(null);
2004 }
2005 };
2006
2007 KeyframeEffect.prototype = createObject(AnimationEffect.prototype, {
2008 get composite() {
2009 return this._composite;
2010 },
2011 set composite(value) {
2012 enterModifyCurrentAnimationState();
2013 try {
2014 // Use the default value if an invalid string is specified.
2015 this._composite = value === 'add' ? 'add' : 'replace';
2016 } finally {
2017 exitModifyCurrentAnimationState(repeatLastTick);
2018 }
2019 },
2020 getFrames: function() {
2021 return this._keyframeDictionaries.slice(0);
2022 },
2023 setFrames: function(oneOrMoreKeyframeDictionaries) {
2024 enterModifyCurrentAnimationState();
2025 try {
2026 if (!Array.isArray(oneOrMoreKeyframeDictionaries)) {
2027 oneOrMoreKeyframeDictionaries = [oneOrMoreKeyframeDictionaries];
2028 }
2029 this._keyframeDictionaries =
2030 oneOrMoreKeyframeDictionaries.map(normalizeKeyframeDictionary);
2031 // Set lazily
2032 this._cachedPropertySpecificKeyframes = null;
2033 } finally {
2034 exitModifyCurrentAnimationState(repeatLastTick);
2035 }
2036 },
2037 _sample: function(timeFraction, currentIteration, target) {
2038 var frames = this._propertySpecificKeyframes();
2039 for (var property in frames) {
2040 compositor.setAnimatedValue(target, property,
2041 this._sampleForProperty(
2042 frames[property], timeFraction, currentIteration));
2043 }
2044 },
2045 _sampleForProperty: function(frames, timeFraction, currentIteration) {
2046 ASSERT_ENABLED && assert(
2047 frames.length >= 2,
2048 'Interpolation requires at least two keyframes');
2049
2050 var startKeyframeIndex;
2051 var length = frames.length;
2052 // We extrapolate differently depending on whether or not there are multiple
2053 // keyframes at offsets of 0 and 1.
2054 if (timeFraction < 0.0) {
2055 if (frames[1].offset === 0.0) {
2056 return new AddReplaceCompositableValue(frames[0].rawValue(),
2057 this._compositeForKeyframe(frames[0]));
2058 } else {
2059 startKeyframeIndex = 0;
2060 }
2061 } else if (timeFraction >= 1.0) {
2062 if (frames[length - 2].offset === 1.0) {
2063 return new AddReplaceCompositableValue(frames[length - 1].rawValue(),
2064 this._compositeForKeyframe(frames[length - 1]));
2065 } else {
2066 startKeyframeIndex = length - 2;
2067 }
2068 } else {
2069 for (var i = length - 1; i >= 0; i--) {
2070 if (frames[i].offset <= timeFraction) {
2071 ASSERT_ENABLED && assert(frames[i].offset !== 1.0);
2072 startKeyframeIndex = i;
2073 break;
2074 }
2075 }
2076 }
2077 var startKeyframe = frames[startKeyframeIndex];
2078 var endKeyframe = frames[startKeyframeIndex + 1];
2079 if (startKeyframe.offset === timeFraction) {
2080 return new AddReplaceCompositableValue(startKeyframe.rawValue(),
2081 this._compositeForKeyframe(startKeyframe));
2082 }
2083 if (endKeyframe.offset === timeFraction) {
2084 return new AddReplaceCompositableValue(endKeyframe.rawValue(),
2085 this._compositeForKeyframe(endKeyframe));
2086 }
2087 var intervalDistance = (timeFraction - startKeyframe.offset) /
2088 (endKeyframe.offset - startKeyframe.offset);
2089 if (startKeyframe.easing) {
2090 intervalDistance = startKeyframe.easing.scaleTime(intervalDistance);
2091 }
2092 return new BlendedCompositableValue(
2093 new AddReplaceCompositableValue(startKeyframe.rawValue(),
2094 this._compositeForKeyframe(startKeyframe)),
2095 new AddReplaceCompositableValue(endKeyframe.rawValue(),
2096 this._compositeForKeyframe(endKeyframe)),
2097 intervalDistance);
2098 },
2099 _propertySpecificKeyframes: function() {
2100 if (isDefinedAndNotNull(this._cachedPropertySpecificKeyframes)) {
2101 return this._cachedPropertySpecificKeyframes;
2102 }
2103
2104 this._cachedPropertySpecificKeyframes = {};
2105 var distributedFrames = this._getDistributedKeyframes();
2106 for (var i = 0; i < distributedFrames.length; i++) {
2107 for (var property in distributedFrames[i].cssValues) {
2108 if (!(property in this._cachedPropertySpecificKeyframes)) {
2109 this._cachedPropertySpecificKeyframes[property] = [];
2110 }
2111 var frame = distributedFrames[i];
2112 this._cachedPropertySpecificKeyframes[property].push(
2113 new PropertySpecificKeyframe(frame.offset, frame.composite,
2114 frame.easing, property, frame.cssValues[property]));
2115 }
2116 }
2117
2118 for (var property in this._cachedPropertySpecificKeyframes) {
2119 var frames = this._cachedPropertySpecificKeyframes[property];
2120 ASSERT_ENABLED && assert(
2121 frames.length > 0,
2122 'There should always be keyframes for each property');
2123
2124 // Add synthetic keyframes at offsets of 0 and 1 if required.
2125 if (frames[0].offset !== 0.0) {
2126 var keyframe = new PropertySpecificKeyframe(0.0, 'add',
2127 presetTimingFunctions.linear, property, cssNeutralValue);
2128 frames.unshift(keyframe);
2129 }
2130 if (frames[frames.length - 1].offset !== 1.0) {
2131 var keyframe = new PropertySpecificKeyframe(1.0, 'add',
2132 presetTimingFunctions.linear, property, cssNeutralValue);
2133 frames.push(keyframe);
2134 }
2135 ASSERT_ENABLED && assert(
2136 frames.length >= 2,
2137 'There should be at least two keyframes including' +
2138 ' synthetic keyframes');
2139 }
2140
2141 return this._cachedPropertySpecificKeyframes;
2142 },
2143 clone: function() {
2144 var result = new KeyframeEffect([], this.composite);
2145 result._keyframeDictionaries = this._keyframeDictionaries.slice(0);
2146 return result;
2147 },
2148 toString: function() {
2149 return '<KeyframeEffect>';
2150 },
2151 _compositeForKeyframe: function(keyframe) {
2152 return isDefinedAndNotNull(keyframe.composite) ?
2153 keyframe.composite : this.composite;
2154 },
2155 _allKeyframesUseSameCompositeOperation: function(keyframes) {
2156 ASSERT_ENABLED && assert(
2157 keyframes.length >= 1, 'This requires at least one keyframe');
2158 var composite = this._compositeForKeyframe(keyframes[0]);
2159 for (var i = 1; i < keyframes.length; i++) {
2160 if (this._compositeForKeyframe(keyframes[i]) !== composite) {
2161 return false;
2162 }
2163 }
2164 return true;
2165 },
2166 _areKeyframeDictionariesLooselySorted: function() {
2167 var previousOffset = -Infinity;
2168 for (var i = 0; i < this._keyframeDictionaries.length; i++) {
2169 if (isDefinedAndNotNull(this._keyframeDictionaries[i].offset)) {
2170 if (this._keyframeDictionaries[i].offset < previousOffset) {
2171 return false;
2172 }
2173 previousOffset = this._keyframeDictionaries[i].offset;
2174 }
2175 }
2176 return true;
2177 },
2178 // The spec describes both this process and the process for interpretting the
2179 // properties of a keyframe dictionary as 'normalizing'. Here we use the term
2180 // 'distributing' to avoid confusion with normalizeKeyframeDictionary().
2181 _getDistributedKeyframes: function() {
2182 if (!this._areKeyframeDictionariesLooselySorted()) {
2183 return [];
2184 }
2185
2186 var distributedKeyframes = this._keyframeDictionaries.map(
2187 KeyframeInternal.createFromNormalizedProperties);
2188
2189 // Remove keyframes with offsets out of bounds.
2190 var length = distributedKeyframes.length;
2191 var count = 0;
2192 for (var i = 0; i < length; i++) {
2193 var offset = distributedKeyframes[i].offset;
2194 if (isDefinedAndNotNull(offset)) {
2195 if (offset >= 0) {
2196 break;
2197 } else {
2198 count = i;
2199 }
2200 }
2201 }
2202 distributedKeyframes.splice(0, count);
2203
2204 length = distributedKeyframes.length;
2205 count = 0;
2206 for (var i = length - 1; i >= 0; i--) {
2207 var offset = distributedKeyframes[i].offset;
2208 if (isDefinedAndNotNull(offset)) {
2209 if (offset <= 1) {
2210 break;
2211 } else {
2212 count = length - i;
2213 }
2214 }
2215 }
2216 distributedKeyframes.splice(length - count, count);
2217
2218 // Distribute offsets.
2219 length = distributedKeyframes.length;
2220 if (length > 1 && !isDefinedAndNotNull(distributedKeyframes[0].offset)) {
2221 distributedKeyframes[0].offset = 0;
2222 }
2223 if (length > 0 &&
2224 !isDefinedAndNotNull(distributedKeyframes[length - 1].offset)) {
2225 distributedKeyframes[length - 1].offset = 1;
2226 }
2227 var lastOffsetIndex = 0;
2228 var nextOffsetIndex = 0;
2229 for (var i = 1; i < distributedKeyframes.length - 1; i++) {
2230 var keyframe = distributedKeyframes[i];
2231 if (isDefinedAndNotNull(keyframe.offset)) {
2232 lastOffsetIndex = i;
2233 continue;
2234 }
2235 if (i > nextOffsetIndex) {
2236 nextOffsetIndex = i;
2237 while (!isDefinedAndNotNull(
2238 distributedKeyframes[nextOffsetIndex].offset)) {
2239 nextOffsetIndex++;
2240 }
2241 }
2242 var lastOffset = distributedKeyframes[lastOffsetIndex].offset;
2243 var nextOffset = distributedKeyframes[nextOffsetIndex].offset;
2244 var unspecifiedKeyframes = nextOffsetIndex - lastOffsetIndex - 1;
2245 ASSERT_ENABLED && assert(unspecifiedKeyframes > 0);
2246 var localIndex = i - lastOffsetIndex;
2247 ASSERT_ENABLED && assert(localIndex > 0);
2248 distributedKeyframes[i].offset = lastOffset +
2249 (nextOffset - lastOffset) * localIndex / (unspecifiedKeyframes + 1);
2250 }
2251
2252 // Remove invalid property values.
2253 for (var i = distributedKeyframes.length - 1; i >= 0; i--) {
2254 var keyframe = distributedKeyframes[i];
2255 for (var property in keyframe.cssValues) {
2256 if (!KeyframeInternal.isSupportedPropertyValue(
2257 keyframe.cssValues[property])) {
2258 delete(keyframe.cssValues[property]);
2259 }
2260 }
2261 if (Object.keys(keyframe).length === 0) {
2262 distributedKeyframes.splice(i, 1);
2263 }
2264 }
2265
2266 return distributedKeyframes;
2267 }
2268 });
2269
2270
2271
2272 /**
2273 * An internal representation of a keyframe. The Keyframe type from the spec is
2274 * just a dictionary and is not exposed.
2275 *
2276 * @constructor
2277 */
2278 var KeyframeInternal = function(offset, composite, easing) {
2279 ASSERT_ENABLED && assert(
2280 typeof offset === 'number' || offset === null,
2281 'Invalid offset value');
2282 ASSERT_ENABLED && assert(
2283 composite === 'add' || composite === 'replace' || composite === null,
2284 'Invalid composite value');
2285 this.offset = offset;
2286 this.composite = composite;
2287 this.easing = easing;
2288 this.cssValues = {};
2289 };
2290
2291 KeyframeInternal.prototype = {
2292 addPropertyValuePair: function(property, value) {
2293 ASSERT_ENABLED && assert(!this.cssValues.hasOwnProperty(property));
2294 this.cssValues[property] = value;
2295 },
2296 hasValueForProperty: function(property) {
2297 return property in this.cssValues;
2298 }
2299 };
2300
2301 KeyframeInternal.isSupportedPropertyValue = function(value) {
2302 ASSERT_ENABLED && assert(
2303 typeof value === 'string' || value === cssNeutralValue);
2304 // TODO: Check this properly!
2305 return value !== '';
2306 };
2307
2308 KeyframeInternal.createFromNormalizedProperties = function(properties) {
2309 ASSERT_ENABLED && assert(
2310 isDefinedAndNotNull(properties) && typeof properties === 'object',
2311 'Properties must be an object');
2312 var keyframe = new KeyframeInternal(properties.offset, properties.composite,
2313 properties.easing);
2314 for (var candidate in properties) {
2315 if (candidate !== 'offset' &&
2316 candidate !== 'composite' &&
2317 candidate !== 'easing') {
2318 keyframe.addPropertyValuePair(candidate, properties[candidate]);
2319 }
2320 }
2321 return keyframe;
2322 };
2323
2324
2325
2326 /** @constructor */
2327 var PropertySpecificKeyframe = function(offset, composite, easing, property,
2328 cssValue) {
2329 this.offset = offset;
2330 this.composite = composite;
2331 this.easing = easing;
2332 this.property = property;
2333 this.cssValue = cssValue;
2334 // Calculated lazily
2335 this.cachedRawValue = null;
2336 };
2337
2338 PropertySpecificKeyframe.prototype = {
2339 rawValue: function() {
2340 if (!isDefinedAndNotNull(this.cachedRawValue)) {
2341 this.cachedRawValue = fromCssValue(this.property, this.cssValue);
2342 }
2343 return this.cachedRawValue;
2344 }
2345 };
2346
2347
2348
2349 /** @constructor */
2350 var TimingFunction = function() {
2351 throw new TypeError('Illegal constructor');
2352 };
2353
2354 TimingFunction.prototype.scaleTime = abstractMethod;
2355
2356 TimingFunction.createFromString = function(spec, timedItem) {
2357 var preset = presetTimingFunctions[spec];
2358 if (preset) {
2359 return preset;
2360 }
2361 if (spec === 'paced') {
2362 if (timedItem instanceof Animation &&
2363 timedItem.effect instanceof MotionPathEffect) {
2364 return new PacedTimingFunction(timedItem.effect);
2365 }
2366 return presetTimingFunctions.linear;
2367 }
2368 var stepMatch = /steps\(\s*(\d+)\s*,\s*(start|end|middle)\s*\)/.exec(spec);
2369 if (stepMatch) {
2370 return new StepTimingFunction(Number(stepMatch[1]), stepMatch[2]);
2371 }
2372 var bezierMatch =
2373 /cubic-bezier\(([^,]*),([^,]*),([^,]*),([^)]*)\)/.exec(spec);
2374 if (bezierMatch) {
2375 return new CubicBezierTimingFunction([
2376 Number(bezierMatch[1]),
2377 Number(bezierMatch[2]),
2378 Number(bezierMatch[3]),
2379 Number(bezierMatch[4])
2380 ]);
2381 }
2382 return presetTimingFunctions.linear;
2383 };
2384
2385
2386
2387 /** @constructor */
2388 var CubicBezierTimingFunction = function(spec) {
2389 this.params = spec;
2390 this.map = [];
2391 for (var ii = 0; ii <= 100; ii += 1) {
2392 var i = ii / 100;
2393 this.map.push([
2394 3 * i * (1 - i) * (1 - i) * this.params[0] +
2395 3 * i * i * (1 - i) * this.params[2] + i * i * i,
2396 3 * i * (1 - i) * (1 - i) * this.params[1] +
2397 3 * i * i * (1 - i) * this.params[3] + i * i * i
2398 ]);
2399 }
2400 };
2401
2402 CubicBezierTimingFunction.prototype = createObject(TimingFunction.prototype, {
2403 scaleTime: function(fraction) {
2404 var fst = 0;
2405 while (fst !== 100 && fraction > this.map[fst][0]) {
2406 fst += 1;
2407 }
2408 if (fraction === this.map[fst][0] || fst === 0) {
2409 return this.map[fst][1];
2410 }
2411 var yDiff = this.map[fst][1] - this.map[fst - 1][1];
2412 var xDiff = this.map[fst][0] - this.map[fst - 1][0];
2413 var p = (fraction - this.map[fst - 1][0]) / xDiff;
2414 return this.map[fst - 1][1] + p * yDiff;
2415 }
2416 });
2417
2418
2419
2420 /** @constructor */
2421 var StepTimingFunction = function(numSteps, position) {
2422 this.numSteps = numSteps;
2423 this.position = position || 'end';
2424 };
2425
2426 StepTimingFunction.prototype = createObject(TimingFunction.prototype, {
2427 scaleTime: function(fraction) {
2428 if (fraction >= 1) {
2429 return 1;
2430 }
2431 var stepSize = 1 / this.numSteps;
2432 if (this.position === 'start') {
2433 fraction += stepSize;
2434 } else if (this.position === 'middle') {
2435 fraction += stepSize / 2;
2436 }
2437 return fraction - fraction % stepSize;
2438 }
2439 });
2440
2441 var presetTimingFunctions = {
2442 'linear': null,
2443 'ease': new CubicBezierTimingFunction([0.25, 0.1, 0.25, 1.0]),
2444 'ease-in': new CubicBezierTimingFunction([0.42, 0, 1.0, 1.0]),
2445 'ease-out': new CubicBezierTimingFunction([0, 0, 0.58, 1.0]),
2446 'ease-in-out': new CubicBezierTimingFunction([0.42, 0, 0.58, 1.0]),
2447 'step-start': new StepTimingFunction(1, 'start'),
2448 'step-middle': new StepTimingFunction(1, 'middle'),
2449 'step-end': new StepTimingFunction(1, 'end')
2450 };
2451
2452
2453
2454 /** @constructor */
2455 var PacedTimingFunction = function(pathEffect) {
2456 ASSERT_ENABLED && assert(pathEffect instanceof MotionPathEffect);
2457 this._pathEffect = pathEffect;
2458 // Range is the portion of the effect over which we pace, normalized to
2459 // [0, 1].
2460 this._range = {min: 0, max: 1};
2461 };
2462
2463 PacedTimingFunction.prototype = createObject(TimingFunction.prototype, {
2464 setRange: function(range) {
2465 ASSERT_ENABLED && assert(range.min >= 0 && range.min <= 1);
2466 ASSERT_ENABLED && assert(range.max >= 0 && range.max <= 1);
2467 ASSERT_ENABLED && assert(range.min < range.max);
2468 this._range = range;
2469 },
2470 scaleTime: function(fraction) {
2471 var cumulativeLengths = this._pathEffect._cumulativeLengths;
2472 var numSegments = cumulativeLengths.length - 1;
2473 if (!cumulativeLengths[numSegments] || fraction <= 0) {
2474 return this._range.min;
2475 }
2476 if (fraction >= 1) {
2477 return this._range.max;
2478 }
2479 var minLength = this.lengthAtIndex(this._range.min * numSegments);
2480 var maxLength = this.lengthAtIndex(this._range.max * numSegments);
2481 var length = interp(minLength, maxLength, fraction);
2482 var leftIndex = this.findLeftIndex(cumulativeLengths, length);
2483 var leftLength = cumulativeLengths[leftIndex];
2484 var segmentLength = cumulativeLengths[leftIndex + 1] - leftLength;
2485 if (segmentLength > 0) {
2486 return (leftIndex + (length - leftLength) / segmentLength) / numSegments;
2487 }
2488 return leftLength / cumulativeLengths.length;
2489 },
2490 findLeftIndex: function(array, value) {
2491 var leftIndex = 0;
2492 var rightIndex = array.length;
2493 while (rightIndex - leftIndex > 1) {
2494 var midIndex = (leftIndex + rightIndex) >> 1;
2495 if (array[midIndex] <= value) {
2496 leftIndex = midIndex;
2497 } else {
2498 rightIndex = midIndex;
2499 }
2500 }
2501 return leftIndex;
2502 },
2503 lengthAtIndex: function(i) {
2504 ASSERT_ENABLED &&
2505 console.assert(i >= 0 && i <= cumulativeLengths.length - 1);
2506 var leftIndex = Math.floor(i);
2507 var startLength = this._pathEffect._cumulativeLengths[leftIndex];
2508 var endLength = this._pathEffect._cumulativeLengths[leftIndex + 1];
2509 var indexFraction = i % 1;
2510 return interp(startLength, endLength, indexFraction);
2511 }
2512 });
2513
2514 var interp = function(from, to, f, type) {
2515 if (Array.isArray(from) || Array.isArray(to)) {
2516 return interpArray(from, to, f, type);
2517 }
2518 var zero = (type && type.indexOf('scale') === 0) ? 1 : 0;
2519 to = isDefinedAndNotNull(to) ? to : zero;
2520 from = isDefinedAndNotNull(from) ? from : zero;
2521
2522 return to * f + from * (1 - f);
2523 };
2524
2525 var interpArray = function(from, to, f, type) {
2526 ASSERT_ENABLED && assert(
2527 Array.isArray(from) || from === null,
2528 'From is not an array or null');
2529 ASSERT_ENABLED && assert(
2530 Array.isArray(to) || to === null,
2531 'To is not an array or null');
2532 ASSERT_ENABLED && assert(
2533 from === null || to === null || from.length === to.length,
2534 'Arrays differ in length ' + from + ' : ' + to);
2535 var length = from ? from.length : to.length;
2536
2537 var result = [];
2538 for (var i = 0; i < length; i++) {
2539 result[i] = interp(from ? from[i] : null, to ? to[i] : null, f, type);
2540 }
2541 return result;
2542 };
2543
2544 var typeWithKeywords = function(keywords, type) {
2545 var isKeyword;
2546 if (keywords.length === 1) {
2547 var keyword = keywords[0];
2548 isKeyword = function(value) {
2549 return value === keyword;
2550 };
2551 } else {
2552 isKeyword = function(value) {
2553 return keywords.indexOf(value) >= 0;
2554 };
2555 }
2556 return createObject(type, {
2557 add: function(base, delta) {
2558 if (isKeyword(base) || isKeyword(delta)) {
2559 return delta;
2560 }
2561 return type.add(base, delta);
2562 },
2563 interpolate: function(from, to, f) {
2564 if (isKeyword(from) || isKeyword(to)) {
2565 return nonNumericType.interpolate(from, to, f);
2566 }
2567 return type.interpolate(from, to, f);
2568 },
2569 toCssValue: function(value, svgMode) {
2570 return isKeyword(value) ? value : type.toCssValue(value, svgMode);
2571 },
2572 fromCssValue: function(value) {
2573 return isKeyword(value) ? value : type.fromCssValue(value);
2574 }
2575 });
2576 };
2577
2578 var numberType = {
2579 add: function(base, delta) {
2580 // If base or delta are 'auto', we fall back to replacement.
2581 if (base === 'auto' || delta === 'auto') {
2582 return nonNumericType.add(base, delta);
2583 }
2584 return base + delta;
2585 },
2586 interpolate: function(from, to, f) {
2587 // If from or to are 'auto', we fall back to step interpolation.
2588 if (from === 'auto' || to === 'auto') {
2589 return nonNumericType.interpolate(from, to);
2590 }
2591 return interp(from, to, f);
2592 },
2593 toCssValue: function(value) { return value + ''; },
2594 fromCssValue: function(value) {
2595 if (value === 'auto') {
2596 return 'auto';
2597 }
2598 var result = Number(value);
2599 return isNaN(result) ? undefined : result;
2600 }
2601 };
2602
2603 var integerType = createObject(numberType, {
2604 interpolate: function(from, to, f) {
2605 // If from or to are 'auto', we fall back to step interpolation.
2606 if (from === 'auto' || to === 'auto') {
2607 return nonNumericType.interpolate(from, to);
2608 }
2609 return Math.floor(interp(from, to, f));
2610 }
2611 });
2612
2613 var fontWeightType = {
2614 add: function(base, delta) { return base + delta; },
2615 interpolate: function(from, to, f) {
2616 return interp(from, to, f);
2617 },
2618 toCssValue: function(value) {
2619 value = Math.round(value / 100) * 100;
2620 value = clamp(value, 100, 900);
2621 if (value === 400) {
2622 return 'normal';
2623 }
2624 if (value === 700) {
2625 return 'bold';
2626 }
2627 return String(value);
2628 },
2629 fromCssValue: function(value) {
2630 // TODO: support lighter / darker ?
2631 var out = Number(value);
2632 if (isNaN(out) || out < 100 || out > 900 || out % 100 !== 0) {
2633 return undefined;
2634 }
2635 return out;
2636 }
2637 };
2638
2639 // This regular expression is intentionally permissive, so that
2640 // platform-prefixed versions of calc will still be accepted as
2641 // input. While we are restrictive with the transform property
2642 // name, we need to be able to read underlying calc values from
2643 // computedStyle so can't easily restrict the input here.
2644 var outerCalcRE = /^\s*(-webkit-)?calc\s*\(\s*([^)]*)\)/;
2645 var valueRE = /^\s*(-?[0-9]+(\.[0-9])?[0-9]*)([a-zA-Z%]*)/;
2646 var operatorRE = /^\s*([+-])/;
2647 var autoRE = /^\s*auto/i;
2648 var percentLengthType = {
2649 zero: function() { return {}; },
2650 add: function(base, delta) {
2651 var out = {};
2652 for (var value in base) {
2653 out[value] = base[value] + (delta[value] || 0);
2654 }
2655 for (value in delta) {
2656 if (value in base) {
2657 continue;
2658 }
2659 out[value] = delta[value];
2660 }
2661 return out;
2662 },
2663 interpolate: function(from, to, f) {
2664 var out = {};
2665 for (var value in from) {
2666 out[value] = interp(from[value], to[value], f);
2667 }
2668 for (var value in to) {
2669 if (value in out) {
2670 continue;
2671 }
2672 out[value] = interp(0, to[value], f);
2673 }
2674 return out;
2675 },
2676 toCssValue: function(value) {
2677 var s = '';
2678 var singleValue = true;
2679 for (var item in value) {
2680 if (s === '') {
2681 s = value[item] + item;
2682 } else if (singleValue) {
2683 if (value[item] !== 0) {
2684 s = features.calcFunction +
2685 '(' + s + ' + ' + value[item] + item + ')';
2686 singleValue = false;
2687 }
2688 } else if (value[item] !== 0) {
2689 s = s.substring(0, s.length - 1) + ' + ' + value[item] + item + ')';
2690 }
2691 }
2692 return s;
2693 },
2694 fromCssValue: function(value) {
2695 var result = percentLengthType.consumeValueFromString(value);
2696 if (result) {
2697 return result.value;
2698 }
2699 return undefined;
2700 },
2701 consumeValueFromString: function(value) {
2702 if (!isDefinedAndNotNull(value)) {
2703 return undefined;
2704 }
2705 var autoMatch = autoRE.exec(value);
2706 if (autoMatch) {
2707 return {
2708 value: { auto: true },
2709 remaining: value.substring(autoMatch[0].length)
2710 };
2711 }
2712 var out = {};
2713 var calcMatch = outerCalcRE.exec(value);
2714 if (!calcMatch) {
2715 var singleValue = valueRE.exec(value);
2716 if (singleValue && (singleValue.length === 4)) {
2717 out[singleValue[3]] = Number(singleValue[1]);
2718 return {
2719 value: out,
2720 remaining: value.substring(singleValue[0].length)
2721 };
2722 }
2723 return undefined;
2724 }
2725 var remaining = value.substring(calcMatch[0].length);
2726 var calcInnards = calcMatch[2];
2727 var firstTime = true;
2728 while (true) {
2729 var reversed = false;
2730 if (firstTime) {
2731 firstTime = false;
2732 } else {
2733 var op = operatorRE.exec(calcInnards);
2734 if (!op) {
2735 return undefined;
2736 }
2737 if (op[1] === '-') {
2738 reversed = true;
2739 }
2740 calcInnards = calcInnards.substring(op[0].length);
2741 }
2742 value = valueRE.exec(calcInnards);
2743 if (!value) {
2744 return undefined;
2745 }
2746 var valueUnit = value[3];
2747 var valueNumber = Number(value[1]);
2748 if (!isDefinedAndNotNull(out[valueUnit])) {
2749 out[valueUnit] = 0;
2750 }
2751 if (reversed) {
2752 out[valueUnit] -= valueNumber;
2753 } else {
2754 out[valueUnit] += valueNumber;
2755 }
2756 calcInnards = calcInnards.substring(value[0].length);
2757 if (/\s*/.exec(calcInnards)[0].length === calcInnards.length) {
2758 return {
2759 value: out,
2760 remaining: remaining
2761 };
2762 }
2763 }
2764 },
2765 negate: function(value) {
2766 var out = {};
2767 for (var unit in value) {
2768 out[unit] = -value[unit];
2769 }
2770 return out;
2771 }
2772 };
2773
2774 var percentLengthAutoType = typeWithKeywords(['auto'], percentLengthType);
2775
2776 var positionKeywordRE = /^\s*left|^\s*center|^\s*right|^\s*top|^\s*bottom/i;
2777 var positionType = {
2778 zero: function() { return [{ px: 0 }, { px: 0 }]; },
2779 add: function(base, delta) {
2780 return [
2781 percentLengthType.add(base[0], delta[0]),
2782 percentLengthType.add(base[1], delta[1])
2783 ];
2784 },
2785 interpolate: function(from, to, f) {
2786 return [
2787 percentLengthType.interpolate(from[0], to[0], f),
2788 percentLengthType.interpolate(from[1], to[1], f)
2789 ];
2790 },
2791 toCssValue: function(value) {
2792 return value.map(percentLengthType.toCssValue).join(' ');
2793 },
2794 fromCssValue: function(value) {
2795 var tokens = positionType.consumeAllTokensFromString(value);
2796 if (!tokens || tokens.length > 4) {
2797 return undefined;
2798 }
2799
2800 if (tokens.length === 1) {
2801 var token = tokens[0];
2802 return (positionType.isHorizontalToken(token) ?
2803 [token, 'center'] : ['center', token]).map(positionType.resolveToken);
2804 }
2805
2806 if (tokens.length === 2 &&
2807 positionType.isHorizontalToken(tokens[0]) &&
2808 positionType.isVerticalToken(tokens[1])) {
2809 return tokens.map(positionType.resolveToken);
2810 }
2811
2812 if (tokens.filter(positionType.isKeyword).length !== 2) {
2813 return undefined;
2814 }
2815
2816 var out = [undefined, undefined];
2817 var center = false;
2818 for (var i = 0; i < tokens.length; i++) {
2819 var token = tokens[i];
2820 if (!positionType.isKeyword(token)) {
2821 return undefined;
2822 }
2823 if (token === 'center') {
2824 if (center) {
2825 return undefined;
2826 }
2827 center = true;
2828 continue;
2829 }
2830 var axis = Number(positionType.isVerticalToken(token));
2831 if (out[axis]) {
2832 return undefined;
2833 }
2834 if (i === tokens.length - 1 || positionType.isKeyword(tokens[i + 1])) {
2835 out[axis] = positionType.resolveToken(token);
2836 continue;
2837 }
2838 var percentLength = tokens[++i];
2839 if (token === 'bottom' || token === 'right') {
2840 percentLength = percentLengthType.negate(percentLength);
2841 percentLength['%'] = (percentLength['%'] || 0) + 100;
2842 }
2843 out[axis] = percentLength;
2844 }
2845 if (center) {
2846 if (!out[0]) {
2847 out[0] = positionType.resolveToken('center');
2848 } else if (!out[1]) {
2849 out[1] = positionType.resolveToken('center');
2850 } else {
2851 return undefined;
2852 }
2853 }
2854 return out.every(isDefinedAndNotNull) ? out : undefined;
2855 },
2856 consumeAllTokensFromString: function(remaining) {
2857 var tokens = [];
2858 while (remaining.trim()) {
2859 var result = positionType.consumeTokenFromString(remaining);
2860 if (!result) {
2861 return undefined;
2862 }
2863 tokens.push(result.value);
2864 remaining = result.remaining;
2865 }
2866 return tokens;
2867 },
2868 consumeTokenFromString: function(value) {
2869 var keywordMatch = positionKeywordRE.exec(value);
2870 if (keywordMatch) {
2871 return {
2872 value: keywordMatch[0].trim().toLowerCase(),
2873 remaining: value.substring(keywordMatch[0].length)
2874 };
2875 }
2876 return percentLengthType.consumeValueFromString(value);
2877 },
2878 resolveToken: function(token) {
2879 if (typeof token === 'string') {
2880 return percentLengthType.fromCssValue({
2881 left: '0%',
2882 center: '50%',
2883 right: '100%',
2884 top: '0%',
2885 bottom: '100%'
2886 }[token]);
2887 }
2888 return token;
2889 },
2890 isHorizontalToken: function(token) {
2891 if (typeof token === 'string') {
2892 return token in { left: true, center: true, right: true };
2893 }
2894 return true;
2895 },
2896 isVerticalToken: function(token) {
2897 if (typeof token === 'string') {
2898 return token in { top: true, center: true, bottom: true };
2899 }
2900 return true;
2901 },
2902 isKeyword: function(token) {
2903 return typeof token === 'string';
2904 }
2905 };
2906
2907 // Spec: http://dev.w3.org/csswg/css-backgrounds/#background-position
2908 var positionListType = {
2909 zero: function() { return [positionType.zero()]; },
2910 add: function(base, delta) {
2911 var out = [];
2912 var maxLength = Math.max(base.length, delta.length);
2913 for (var i = 0; i < maxLength; i++) {
2914 var basePosition = base[i] ? base[i] : positionType.zero();
2915 var deltaPosition = delta[i] ? delta[i] : positionType.zero();
2916 out.push(positionType.add(basePosition, deltaPosition));
2917 }
2918 return out;
2919 },
2920 interpolate: function(from, to, f) {
2921 var out = [];
2922 var maxLength = Math.max(from.length, to.length);
2923 for (var i = 0; i < maxLength; i++) {
2924 var fromPosition = from[i] ? from[i] : positionType.zero();
2925 var toPosition = to[i] ? to[i] : positionType.zero();
2926 out.push(positionType.interpolate(fromPosition, toPosition, f));
2927 }
2928 return out;
2929 },
2930 toCssValue: function(value) {
2931 return value.map(positionType.toCssValue).join(', ');
2932 },
2933 fromCssValue: function(value) {
2934 if (!isDefinedAndNotNull(value)) {
2935 return undefined;
2936 }
2937 if (!value.trim()) {
2938 return [positionType.fromCssValue('0% 0%')];
2939 }
2940 var positionValues = value.split(',');
2941 var out = positionValues.map(positionType.fromCssValue);
2942 return out.every(isDefinedAndNotNull) ? out : undefined;
2943 }
2944 };
2945
2946 var rectangleRE = /rect\(([^,]+),([^,]+),([^,]+),([^)]+)\)/;
2947 var rectangleType = {
2948 add: function(base, delta) {
2949 return {
2950 top: percentLengthType.add(base.top, delta.top),
2951 right: percentLengthType.add(base.right, delta.right),
2952 bottom: percentLengthType.add(base.bottom, delta.bottom),
2953 left: percentLengthType.add(base.left, delta.left)
2954 };
2955 },
2956 interpolate: function(from, to, f) {
2957 return {
2958 top: percentLengthType.interpolate(from.top, to.top, f),
2959 right: percentLengthType.interpolate(from.right, to.right, f),
2960 bottom: percentLengthType.interpolate(from.bottom, to.bottom, f),
2961 left: percentLengthType.interpolate(from.left, to.left, f)
2962 };
2963 },
2964 toCssValue: function(value) {
2965 return 'rect(' +
2966 percentLengthType.toCssValue(value.top) + ',' +
2967 percentLengthType.toCssValue(value.right) + ',' +
2968 percentLengthType.toCssValue(value.bottom) + ',' +
2969 percentLengthType.toCssValue(value.left) + ')';
2970 },
2971 fromCssValue: function(value) {
2972 var match = rectangleRE.exec(value);
2973 if (!match) {
2974 return undefined;
2975 }
2976 var out = {
2977 top: percentLengthType.fromCssValue(match[1]),
2978 right: percentLengthType.fromCssValue(match[2]),
2979 bottom: percentLengthType.fromCssValue(match[3]),
2980 left: percentLengthType.fromCssValue(match[4])
2981 };
2982 if (out.top && out.right && out.bottom && out.left) {
2983 return out;
2984 }
2985 return undefined;
2986 }
2987 };
2988
2989 var originType = {
2990 zero: function() { return [{'%': 0}, {'%': 0}, {px: 0}]; },
2991 add: function(base, delta) {
2992 return [
2993 percentLengthType.add(base[0], delta[0]),
2994 percentLengthType.add(base[1], delta[1]),
2995 percentLengthType.add(base[2], delta[2])
2996 ];
2997 },
2998 interpolate: function(from, to, f) {
2999 return [
3000 percentLengthType.interpolate(from[0], to[0], f),
3001 percentLengthType.interpolate(from[1], to[1], f),
3002 percentLengthType.interpolate(from[2], to[2], f)
3003 ];
3004 },
3005 toCssValue: function(value) {
3006 var result = percentLengthType.toCssValue(value[0]) + ' ' +
3007 percentLengthType.toCssValue(value[1]);
3008 // Return the third value if it is non-zero.
3009 for (var unit in value[2]) {
3010 if (value[2][unit] !== 0) {
3011 return result + ' ' + percentLengthType.toCssValue(value[2]);
3012 }
3013 }
3014 return result;
3015 },
3016 fromCssValue: function(value) {
3017 var tokens = positionType.consumeAllTokensFromString(value);
3018 if (!tokens) {
3019 return undefined;
3020 }
3021 var out = ['center', 'center', {px: 0}];
3022 switch (tokens.length) {
3023 case 0:
3024 return originType.zero();
3025 case 1:
3026 if (positionType.isHorizontalToken(tokens[0])) {
3027 out[0] = tokens[0];
3028 } else if (positionType.isVerticalToken(tokens[0])) {
3029 out[1] = tokens[0];
3030 } else {
3031 return undefined;
3032 }
3033 return out.map(positionType.resolveToken);
3034 case 3:
3035 if (positionType.isKeyword(tokens[2])) {
3036 return undefined;
3037 }
3038 out[2] = tokens[2];
3039 case 2:
3040 if (positionType.isHorizontalToken(tokens[0]) &&
3041 positionType.isVerticalToken(tokens[1])) {
3042 out[0] = tokens[0];
3043 out[1] = tokens[1];
3044 } else if (positionType.isVerticalToken(tokens[0]) &&
3045 positionType.isHorizontalToken(tokens[1])) {
3046 out[0] = tokens[1];
3047 out[1] = tokens[0];
3048 } else {
3049 return undefined;
3050 }
3051 return out.map(positionType.resolveToken);
3052 default:
3053 return undefined;
3054 }
3055 }
3056 };
3057
3058 var shadowType = {
3059 zero: function() {
3060 return {
3061 hOffset: lengthType.zero(),
3062 vOffset: lengthType.zero()
3063 };
3064 },
3065 _addSingle: function(base, delta) {
3066 if (base && delta && base.inset !== delta.inset) {
3067 return delta;
3068 }
3069 var result = {
3070 inset: base ? base.inset : delta.inset,
3071 hOffset: lengthType.add(
3072 base ? base.hOffset : lengthType.zero(),
3073 delta ? delta.hOffset : lengthType.zero()),
3074 vOffset: lengthType.add(
3075 base ? base.vOffset : lengthType.zero(),
3076 delta ? delta.vOffset : lengthType.zero()),
3077 blur: lengthType.add(
3078 base && base.blur || lengthType.zero(),
3079 delta && delta.blur || lengthType.zero())
3080 };
3081 if (base && base.spread || delta && delta.spread) {
3082 result.spread = lengthType.add(
3083 base && base.spread || lengthType.zero(),
3084 delta && delta.spread || lengthType.zero());
3085 }
3086 if (base && base.color || delta && delta.color) {
3087 result.color = colorType.add(
3088 base && base.color || colorType.zero(),
3089 delta && delta.color || colorType.zero());
3090 }
3091 return result;
3092 },
3093 add: function(base, delta) {
3094 var result = [];
3095 for (var i = 0; i < base.length || i < delta.length; i++) {
3096 result.push(this._addSingle(base[i], delta[i]));
3097 }
3098 return result;
3099 },
3100 _interpolateSingle: function(from, to, f) {
3101 if (from && to && from.inset !== to.inset) {
3102 return f < 0.5 ? from : to;
3103 }
3104 var result = {
3105 inset: from ? from.inset : to.inset,
3106 hOffset: lengthType.interpolate(
3107 from ? from.hOffset : lengthType.zero(),
3108 to ? to.hOffset : lengthType.zero(), f),
3109 vOffset: lengthType.interpolate(
3110 from ? from.vOffset : lengthType.zero(),
3111 to ? to.vOffset : lengthType.zero(), f),
3112 blur: lengthType.interpolate(
3113 from && from.blur || lengthType.zero(),
3114 to && to.blur || lengthType.zero(), f)
3115 };
3116 if (from && from.spread || to && to.spread) {
3117 result.spread = lengthType.interpolate(
3118 from && from.spread || lengthType.zero(),
3119 to && to.spread || lengthType.zero(), f);
3120 }
3121 if (from && from.color || to && to.color) {
3122 result.color = colorType.interpolate(
3123 from && from.color || colorType.zero(),
3124 to && to.color || colorType.zero(), f);
3125 }
3126 return result;
3127 },
3128 interpolate: function(from, to, f) {
3129 var result = [];
3130 for (var i = 0; i < from.length || i < to.length; i++) {
3131 result.push(this._interpolateSingle(from[i], to[i], f));
3132 }
3133 return result;
3134 },
3135 _toCssValueSingle: function(value) {
3136 return (value.inset ? 'inset ' : '') +
3137 lengthType.toCssValue(value.hOffset) + ' ' +
3138 lengthType.toCssValue(value.vOffset) + ' ' +
3139 lengthType.toCssValue(value.blur) +
3140 (value.spread ? ' ' + lengthType.toCssValue(value.spread) : '') +
3141 (value.color ? ' ' + colorType.toCssValue(value.color) : '');
3142 },
3143 toCssValue: function(value) {
3144 return value.map(this._toCssValueSingle).join(', ');
3145 },
3146 fromCssValue: function(value) {
3147 var shadowRE = /(([^(,]+(\([^)]*\))?)+)/g;
3148 var match;
3149 var shadows = [];
3150 while ((match = shadowRE.exec(value)) !== null) {
3151 shadows.push(match[0]);
3152 }
3153
3154 var result = shadows.map(function(value) {
3155 if (value === 'none') {
3156 return shadowType.zero();
3157 }
3158 value = value.replace(/^\s+|\s+$/g, '');
3159
3160 var partsRE = /([^ (]+(\([^)]*\))?)/g;
3161 var parts = [];
3162 while ((match = partsRE.exec(value)) !== null) {
3163 parts.push(match[0]);
3164 }
3165
3166 if (parts.length < 2 || parts.length > 7) {
3167 return undefined;
3168 }
3169 var result = {
3170 inset: false
3171 };
3172
3173 var lengths = [];
3174 while (parts.length) {
3175 var part = parts.shift();
3176
3177 var length = lengthType.fromCssValue(part);
3178 if (length) {
3179 lengths.push(length);
3180 continue;
3181 }
3182
3183 var color = colorType.fromCssValue(part);
3184 if (color) {
3185 result.color = color;
3186 }
3187
3188 if (part === 'inset') {
3189 result.inset = true;
3190 }
3191 }
3192
3193 if (lengths.length < 2 || lengths.length > 4) {
3194 return undefined;
3195 }
3196 result.hOffset = lengths[0];
3197 result.vOffset = lengths[1];
3198 if (lengths.length > 2) {
3199 result.blur = lengths[2];
3200 }
3201 if (lengths.length > 3) {
3202 result.spread = lengths[3];
3203 }
3204 return result;
3205 });
3206
3207 return result.every(isDefined) ? result : undefined;
3208 }
3209 };
3210
3211 var nonNumericType = {
3212 add: function(base, delta) {
3213 return isDefined(delta) ? delta : base;
3214 },
3215 interpolate: function(from, to, f) {
3216 return f < 0.5 ? from : to;
3217 },
3218 toCssValue: function(value) {
3219 return value;
3220 },
3221 fromCssValue: function(value) {
3222 return value;
3223 }
3224 };
3225
3226 var visibilityType = createObject(nonNumericType, {
3227 interpolate: function(from, to, f) {
3228 if (from !== 'visible' && to !== 'visible') {
3229 return nonNumericType.interpolate(from, to, f);
3230 }
3231 if (f <= 0) {
3232 return from;
3233 }
3234 if (f >= 1) {
3235 return to;
3236 }
3237 return 'visible';
3238 },
3239 fromCssValue: function(value) {
3240 if (['visible', 'hidden', 'collapse'].indexOf(value) !== -1) {
3241 return value;
3242 }
3243 return undefined;
3244 }
3245 });
3246
3247 var lengthType = percentLengthType;
3248 var lengthAutoType = typeWithKeywords(['auto'], lengthType);
3249
3250 var colorRE = new RegExp(
3251 '(hsla?|rgba?)\\(' +
3252 '([\\-0-9]+%?),?\\s*' +
3253 '([\\-0-9]+%?),?\\s*' +
3254 '([\\-0-9]+%?)(?:,?\\s*([\\-0-9\\.]+%?))?' +
3255 '\\)');
3256 var colorHashRE = new RegExp(
3257 '#([0-9A-Fa-f][0-9A-Fa-f]?)' +
3258 '([0-9A-Fa-f][0-9A-Fa-f]?)' +
3259 '([0-9A-Fa-f][0-9A-Fa-f]?)');
3260
3261 function hsl2rgb(h, s, l) {
3262 // Cribbed from http://dev.w3.org/csswg/css-color/#hsl-color
3263 // Wrap to 0->360 degrees (IE -10 === 350) then normalize
3264 h = (((h % 360) + 360) % 360) / 360;
3265 s = s / 100;
3266 l = l / 100;
3267 function hue2rgb(m1, m2, h) {
3268 if (h < 0) {
3269 h += 1;
3270 }
3271 if (h > 1) {
3272 h -= 1;
3273 }
3274 if (h * 6 < 1) {
3275 return m1 + (m2 - m1) * h * 6;
3276 }
3277 if (h * 2 < 1) {
3278 return m2;
3279 }
3280 if (h * 3 < 2) {
3281 return m1 + (m2 - m1) * (2 / 3 - h) * 6;
3282 }
3283 return m1;
3284 }
3285 var m2;
3286 if (l <= 0.5) {
3287 m2 = l * (s + 1);
3288 } else {
3289 m2 = l + s - l * s;
3290 }
3291
3292 var m1 = l * 2 - m2;
3293 var r = Math.ceil(hue2rgb(m1, m2, h + 1 / 3) * 255);
3294 var g = Math.ceil(hue2rgb(m1, m2, h) * 255);
3295 var b = Math.ceil(hue2rgb(m1, m2, h - 1 / 3) * 255);
3296 return [r, g, b];
3297 }
3298
3299 var namedColors = {
3300 aliceblue: [240, 248, 255, 1],
3301 antiquewhite: [250, 235, 215, 1],
3302 aqua: [0, 255, 255, 1],
3303 aquamarine: [127, 255, 212, 1],
3304 azure: [240, 255, 255, 1],
3305 beige: [245, 245, 220, 1],
3306 bisque: [255, 228, 196, 1],
3307 black: [0, 0, 0, 1],
3308 blanchedalmond: [255, 235, 205, 1],
3309 blue: [0, 0, 255, 1],
3310 blueviolet: [138, 43, 226, 1],
3311 brown: [165, 42, 42, 1],
3312 burlywood: [222, 184, 135, 1],
3313 cadetblue: [95, 158, 160, 1],
3314 chartreuse: [127, 255, 0, 1],
3315 chocolate: [210, 105, 30, 1],
3316 coral: [255, 127, 80, 1],
3317 cornflowerblue: [100, 149, 237, 1],
3318 cornsilk: [255, 248, 220, 1],
3319 crimson: [220, 20, 60, 1],
3320 cyan: [0, 255, 255, 1],
3321 darkblue: [0, 0, 139, 1],
3322 darkcyan: [0, 139, 139, 1],
3323 darkgoldenrod: [184, 134, 11, 1],
3324 darkgray: [169, 169, 169, 1],
3325 darkgreen: [0, 100, 0, 1],
3326 darkgrey: [169, 169, 169, 1],
3327 darkkhaki: [189, 183, 107, 1],
3328 darkmagenta: [139, 0, 139, 1],
3329 darkolivegreen: [85, 107, 47, 1],
3330 darkorange: [255, 140, 0, 1],
3331 darkorchid: [153, 50, 204, 1],
3332 darkred: [139, 0, 0, 1],
3333 darksalmon: [233, 150, 122, 1],
3334 darkseagreen: [143, 188, 143, 1],
3335 darkslateblue: [72, 61, 139, 1],
3336 darkslategray: [47, 79, 79, 1],
3337 darkslategrey: [47, 79, 79, 1],
3338 darkturquoise: [0, 206, 209, 1],
3339 darkviolet: [148, 0, 211, 1],
3340 deeppink: [255, 20, 147, 1],
3341 deepskyblue: [0, 191, 255, 1],
3342 dimgray: [105, 105, 105, 1],
3343 dimgrey: [105, 105, 105, 1],
3344 dodgerblue: [30, 144, 255, 1],
3345 firebrick: [178, 34, 34, 1],
3346 floralwhite: [255, 250, 240, 1],
3347 forestgreen: [34, 139, 34, 1],
3348 fuchsia: [255, 0, 255, 1],
3349 gainsboro: [220, 220, 220, 1],
3350 ghostwhite: [248, 248, 255, 1],
3351 gold: [255, 215, 0, 1],
3352 goldenrod: [218, 165, 32, 1],
3353 gray: [128, 128, 128, 1],
3354 green: [0, 128, 0, 1],
3355 greenyellow: [173, 255, 47, 1],
3356 grey: [128, 128, 128, 1],
3357 honeydew: [240, 255, 240, 1],
3358 hotpink: [255, 105, 180, 1],
3359 indianred: [205, 92, 92, 1],
3360 indigo: [75, 0, 130, 1],
3361 ivory: [255, 255, 240, 1],
3362 khaki: [240, 230, 140, 1],
3363 lavender: [230, 230, 250, 1],
3364 lavenderblush: [255, 240, 245, 1],
3365 lawngreen: [124, 252, 0, 1],
3366 lemonchiffon: [255, 250, 205, 1],
3367 lightblue: [173, 216, 230, 1],
3368 lightcoral: [240, 128, 128, 1],
3369 lightcyan: [224, 255, 255, 1],
3370 lightgoldenrodyellow: [250, 250, 210, 1],
3371 lightgray: [211, 211, 211, 1],
3372 lightgreen: [144, 238, 144, 1],
3373 lightgrey: [211, 211, 211, 1],
3374 lightpink: [255, 182, 193, 1],
3375 lightsalmon: [255, 160, 122, 1],
3376 lightseagreen: [32, 178, 170, 1],
3377 lightskyblue: [135, 206, 250, 1],
3378 lightslategray: [119, 136, 153, 1],
3379 lightslategrey: [119, 136, 153, 1],
3380 lightsteelblue: [176, 196, 222, 1],
3381 lightyellow: [255, 255, 224, 1],
3382 lime: [0, 255, 0, 1],
3383 limegreen: [50, 205, 50, 1],
3384 linen: [250, 240, 230, 1],
3385 magenta: [255, 0, 255, 1],
3386 maroon: [128, 0, 0, 1],
3387 mediumaquamarine: [102, 205, 170, 1],
3388 mediumblue: [0, 0, 205, 1],
3389 mediumorchid: [186, 85, 211, 1],
3390 mediumpurple: [147, 112, 219, 1],
3391 mediumseagreen: [60, 179, 113, 1],
3392 mediumslateblue: [123, 104, 238, 1],
3393 mediumspringgreen: [0, 250, 154, 1],
3394 mediumturquoise: [72, 209, 204, 1],
3395 mediumvioletred: [199, 21, 133, 1],
3396 midnightblue: [25, 25, 112, 1],
3397 mintcream: [245, 255, 250, 1],
3398 mistyrose: [255, 228, 225, 1],
3399 moccasin: [255, 228, 181, 1],
3400 navajowhite: [255, 222, 173, 1],
3401 navy: [0, 0, 128, 1],
3402 oldlace: [253, 245, 230, 1],
3403 olive: [128, 128, 0, 1],
3404 olivedrab: [107, 142, 35, 1],
3405 orange: [255, 165, 0, 1],
3406 orangered: [255, 69, 0, 1],
3407 orchid: [218, 112, 214, 1],
3408 palegoldenrod: [238, 232, 170, 1],
3409 palegreen: [152, 251, 152, 1],
3410 paleturquoise: [175, 238, 238, 1],
3411 palevioletred: [219, 112, 147, 1],
3412 papayawhip: [255, 239, 213, 1],
3413 peachpuff: [255, 218, 185, 1],
3414 peru: [205, 133, 63, 1],
3415 pink: [255, 192, 203, 1],
3416 plum: [221, 160, 221, 1],
3417 powderblue: [176, 224, 230, 1],
3418 purple: [128, 0, 128, 1],
3419 red: [255, 0, 0, 1],
3420 rosybrown: [188, 143, 143, 1],
3421 royalblue: [65, 105, 225, 1],
3422 saddlebrown: [139, 69, 19, 1],
3423 salmon: [250, 128, 114, 1],
3424 sandybrown: [244, 164, 96, 1],
3425 seagreen: [46, 139, 87, 1],
3426 seashell: [255, 245, 238, 1],
3427 sienna: [160, 82, 45, 1],
3428 silver: [192, 192, 192, 1],
3429 skyblue: [135, 206, 235, 1],
3430 slateblue: [106, 90, 205, 1],
3431 slategray: [112, 128, 144, 1],
3432 slategrey: [112, 128, 144, 1],
3433 snow: [255, 250, 250, 1],
3434 springgreen: [0, 255, 127, 1],
3435 steelblue: [70, 130, 180, 1],
3436 tan: [210, 180, 140, 1],
3437 teal: [0, 128, 128, 1],
3438 thistle: [216, 191, 216, 1],
3439 tomato: [255, 99, 71, 1],
3440 transparent: [0, 0, 0, 0],
3441 turquoise: [64, 224, 208, 1],
3442 violet: [238, 130, 238, 1],
3443 wheat: [245, 222, 179, 1],
3444 white: [255, 255, 255, 1],
3445 whitesmoke: [245, 245, 245, 1],
3446 yellow: [255, 255, 0, 1],
3447 yellowgreen: [154, 205, 50, 1]
3448 };
3449
3450 var colorType = typeWithKeywords(['currentColor'], {
3451 zero: function() { return [0, 0, 0, 0]; },
3452 _premultiply: function(value) {
3453 var alpha = value[3];
3454 return [value[0] * alpha, value[1] * alpha, value[2] * alpha];
3455 },
3456 add: function(base, delta) {
3457 var alpha = Math.min(base[3] + delta[3], 1);
3458 if (alpha === 0) {
3459 return [0, 0, 0, 0];
3460 }
3461 base = this._premultiply(base);
3462 delta = this._premultiply(delta);
3463 return [(base[0] + delta[0]) / alpha, (base[1] + delta[1]) / alpha,
3464 (base[2] + delta[2]) / alpha, alpha];
3465 },
3466 interpolate: function(from, to, f) {
3467 var alpha = clamp(interp(from[3], to[3], f), 0, 1);
3468 if (alpha === 0) {
3469 return [0, 0, 0, 0];
3470 }
3471 from = this._premultiply(from);
3472 to = this._premultiply(to);
3473 return [interp(from[0], to[0], f) / alpha,
3474 interp(from[1], to[1], f) / alpha,
3475 interp(from[2], to[2], f) / alpha, alpha];
3476 },
3477 toCssValue: function(value) {
3478 return 'rgba(' + Math.round(value[0]) + ', ' + Math.round(value[1]) +
3479 ', ' + Math.round(value[2]) + ', ' + value[3] + ')';
3480 },
3481 fromCssValue: function(value) {
3482 // http://dev.w3.org/csswg/css-color/#color
3483 var out = [];
3484
3485 var regexResult = colorHashRE.exec(value);
3486 if (regexResult) {
3487 if (value.length !== 4 && value.length !== 7) {
3488 return undefined;
3489 }
3490
3491 var out = [];
3492 regexResult.shift();
3493 for (var i = 0; i < 3; i++) {
3494 if (regexResult[i].length === 1) {
3495 regexResult[i] = regexResult[i] + regexResult[i];
3496 }
3497 var v = Math.max(Math.min(parseInt(regexResult[i], 16), 255), 0);
3498 out[i] = v;
3499 }
3500 out.push(1.0);
3501 }
3502
3503 var regexResult = colorRE.exec(value);
3504 if (regexResult) {
3505 regexResult.shift();
3506 var type = regexResult.shift().substr(0, 3);
3507 for (var i = 0; i < 3; i++) {
3508 var m = 1;
3509 if (regexResult[i][regexResult[i].length - 1] === '%') {
3510 regexResult[i] = regexResult[i].substr(0, regexResult[i].length - 1);
3511 m = 255.0 / 100.0;
3512 }
3513 if (type === 'rgb') {
3514 out[i] = clamp(Math.round(parseInt(regexResult[i], 10) * m), 0, 255);
3515 } else {
3516 out[i] = parseInt(regexResult[i], 10);
3517 }
3518 }
3519
3520 // Convert hsl values to rgb value
3521 if (type === 'hsl') {
3522 out = hsl2rgb.apply(null, out);
3523 }
3524
3525 if (typeof regexResult[3] !== 'undefined') {
3526 out[3] = Math.max(Math.min(parseFloat(regexResult[3]), 1.0), 0.0);
3527 } else {
3528 out.push(1.0);
3529 }
3530 }
3531
3532 if (out.some(isNaN)) {
3533 return undefined;
3534 }
3535 if (out.length > 0) {
3536 return out;
3537 }
3538 return namedColors[value];
3539 }
3540 });
3541
3542 var convertToDeg = function(num, type) {
3543 switch (type) {
3544 case 'grad':
3545 return num / 400 * 360;
3546 case 'rad':
3547 return num / 2 / Math.PI * 360;
3548 case 'turn':
3549 return num * 360;
3550 default:
3551 return num;
3552 }
3553 };
3554
3555 var extractValue = function(values, pos, hasUnits) {
3556 var value = Number(values[pos]);
3557 if (!hasUnits) {
3558 return value;
3559 }
3560 var type = values[pos + 1];
3561 if (type === '') { type = 'px'; }
3562 var result = {};
3563 result[type] = value;
3564 return result;
3565 };
3566
3567 var extractValues = function(values, numValues, hasOptionalValue,
3568 hasUnits) {
3569 var result = [];
3570 for (var i = 0; i < numValues; i++) {
3571 result.push(extractValue(values, 1 + 2 * i, hasUnits));
3572 }
3573 if (hasOptionalValue && values[1 + 2 * numValues]) {
3574 result.push(extractValue(values, 1 + 2 * numValues, hasUnits));
3575 }
3576 return result;
3577 };
3578
3579 var SPACES = '\\s*';
3580 var NUMBER = '[+-]?(?:\\d+|\\d*\\.\\d+)';
3581 var RAW_OPEN_BRACKET = '\\(';
3582 var RAW_CLOSE_BRACKET = '\\)';
3583 var RAW_COMMA = ',';
3584 var UNIT = '[a-zA-Z%]*';
3585 var START = '^';
3586
3587 function capture(x) { return '(' + x + ')'; }
3588 function optional(x) { return '(?:' + x + ')?'; }
3589
3590 var OPEN_BRACKET = [SPACES, RAW_OPEN_BRACKET, SPACES].join('');
3591 var CLOSE_BRACKET = [SPACES, RAW_CLOSE_BRACKET, SPACES].join('');
3592 var COMMA = [SPACES, RAW_COMMA, SPACES].join('');
3593 var UNIT_NUMBER = [capture(NUMBER), capture(UNIT)].join('');
3594
3595 function transformRE(name, numParms, hasOptionalParm) {
3596 var tokenList = [START, SPACES, name, OPEN_BRACKET];
3597 for (var i = 0; i < numParms - 1; i++) {
3598 tokenList.push(UNIT_NUMBER);
3599 tokenList.push(COMMA);
3600 }
3601 tokenList.push(UNIT_NUMBER);
3602 if (hasOptionalParm) {
3603 tokenList.push(optional([COMMA, UNIT_NUMBER].join('')));
3604 }
3605 tokenList.push(CLOSE_BRACKET);
3606 return new RegExp(tokenList.join(''));
3607 }
3608
3609 function buildMatcher(name, numValues, hasOptionalValue, hasUnits,
3610 baseValue) {
3611 var baseName = name;
3612 if (baseValue) {
3613 if (name[name.length - 1] === 'X' || name[name.length - 1] === 'Y') {
3614 baseName = name.substring(0, name.length - 1);
3615 } else if (name[name.length - 1] === 'Z') {
3616 baseName = name.substring(0, name.length - 1) + '3d';
3617 }
3618 }
3619
3620 var f = function(x) {
3621 var r = extractValues(x, numValues, hasOptionalValue, hasUnits);
3622 if (baseValue !== undefined) {
3623 if (name[name.length - 1] === 'X') {
3624 r.push(baseValue);
3625 } else if (name[name.length - 1] === 'Y') {
3626 r = [baseValue].concat(r);
3627 } else if (name[name.length - 1] === 'Z') {
3628 r = [baseValue, baseValue].concat(r);
3629 } else if (hasOptionalValue) {
3630 while (r.length < 2) {
3631 if (baseValue === 'copy') {
3632 r.push(r[0]);
3633 } else {
3634 r.push(baseValue);
3635 }
3636 }
3637 }
3638 }
3639 return r;
3640 };
3641 return [transformRE(name, numValues, hasOptionalValue), f, baseName];
3642 }
3643
3644 function buildRotationMatcher(name, numValues, hasOptionalValue,
3645 baseValue) {
3646 var m = buildMatcher(name, numValues, hasOptionalValue, true, baseValue);
3647
3648 var f = function(x) {
3649 var r = m[1](x);
3650 return r.map(function(v) {
3651 var result = 0;
3652 for (var type in v) {
3653 result += convertToDeg(v[type], type);
3654 }
3655 return result;
3656 });
3657 };
3658 return [m[0], f, m[2]];
3659 }
3660
3661 function build3DRotationMatcher() {
3662 var m = buildMatcher('rotate3d', 4, false, true);
3663 var f = function(x) {
3664 var r = m[1](x);
3665 var out = [];
3666 for (var i = 0; i < 3; i++) {
3667 out.push(r[i].px);
3668 }
3669 var angle = 0;
3670 for (var unit in r[3]) {
3671 angle += convertToDeg(r[3][unit], unit);
3672 }
3673 out.push(angle);
3674 return out;
3675 };
3676 return [m[0], f, m[2]];
3677 }
3678
3679 var transformREs = [
3680 buildRotationMatcher('rotate', 1, false),
3681 buildRotationMatcher('rotateX', 1, false),
3682 buildRotationMatcher('rotateY', 1, false),
3683 buildRotationMatcher('rotateZ', 1, false),
3684 build3DRotationMatcher(),
3685 buildRotationMatcher('skew', 1, true, 0),
3686 buildRotationMatcher('skewX', 1, false),
3687 buildRotationMatcher('skewY', 1, false),
3688 buildMatcher('translateX', 1, false, true, {px: 0}),
3689 buildMatcher('translateY', 1, false, true, {px: 0}),
3690 buildMatcher('translateZ', 1, false, true, {px: 0}),
3691 buildMatcher('translate', 1, true, true, {px: 0}),
3692 buildMatcher('translate3d', 3, false, true),
3693 buildMatcher('scale', 1, true, false, 'copy'),
3694 buildMatcher('scaleX', 1, false, false, 1),
3695 buildMatcher('scaleY', 1, false, false, 1),
3696 buildMatcher('scaleZ', 1, false, false, 1),
3697 buildMatcher('scale3d', 3, false, false),
3698 buildMatcher('perspective', 1, false, true),
3699 buildMatcher('matrix', 6, false, false),
3700 buildMatcher('matrix3d', 16, false, false)
3701 ];
3702
3703 var decomposeMatrix = (function() {
3704 // this is only ever used on the perspective matrix, which has 0, 0, 0, 1 as
3705 // last column
3706 function determinant(m) {
3707 return m[0][0] * m[1][1] * m[2][2] +
3708 m[1][0] * m[2][1] * m[0][2] +
3709 m[2][0] * m[0][1] * m[1][2] -
3710 m[0][2] * m[1][1] * m[2][0] -
3711 m[1][2] * m[2][1] * m[0][0] -
3712 m[2][2] * m[0][1] * m[1][0];
3713 }
3714
3715 // from Wikipedia:
3716 //
3717 // [A B]^-1 = [A^-1 + A^-1B(D - CA^-1B)^-1CA^-1 -A^-1B(D - CA^-1B)^-1]
3718 // [C D] [-(D - CA^-1B)^-1CA^-1 (D - CA^-1B)^-1 ]
3719 //
3720 // Therefore
3721 //
3722 // [A [0]]^-1 = [A^-1 [0]]
3723 // [C 1 ] [ -CA^-1 1 ]
3724 function inverse(m) {
3725 var iDet = 1 / determinant(m);
3726 var a = m[0][0], b = m[0][1], c = m[0][2];
3727 var d = m[1][0], e = m[1][1], f = m[1][2];
3728 var g = m[2][0], h = m[2][1], k = m[2][2];
3729 var Ainv = [
3730 [(e * k - f * h) * iDet, (c * h - b * k) * iDet,
3731 (b * f - c * e) * iDet, 0],
3732 [(f * g - d * k) * iDet, (a * k - c * g) * iDet,
3733 (c * d - a * f) * iDet, 0],
3734 [(d * h - e * g) * iDet, (g * b - a * h) * iDet,
3735 (a * e - b * d) * iDet, 0]
3736 ];
3737 var lastRow = [];
3738 for (var i = 0; i < 3; i++) {
3739 var val = 0;
3740 for (var j = 0; j < 3; j++) {
3741 val += m[3][j] * Ainv[j][i];
3742 }
3743 lastRow.push(val);
3744 }
3745 lastRow.push(1);
3746 Ainv.push(lastRow);
3747 return Ainv;
3748 }
3749
3750 function transposeMatrix4(m) {
3751 return [[m[0][0], m[1][0], m[2][0], m[3][0]],
3752 [m[0][1], m[1][1], m[2][1], m[3][1]],
3753 [m[0][2], m[1][2], m[2][2], m[3][2]],
3754 [m[0][3], m[1][3], m[2][3], m[3][3]]];
3755 }
3756
3757 function multVecMatrix(v, m) {
3758 var result = [];
3759 for (var i = 0; i < 4; i++) {
3760 var val = 0;
3761 for (var j = 0; j < 4; j++) {
3762 val += v[j] * m[j][i];
3763 }
3764 result.push(val);
3765 }
3766 return result;
3767 }
3768
3769 function normalize(v) {
3770 var len = length(v);
3771 return [v[0] / len, v[1] / len, v[2] / len];
3772 }
3773
3774 function length(v) {
3775 return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
3776 }
3777
3778 function combine(v1, v2, v1s, v2s) {
3779 return [v1s * v1[0] + v2s * v2[0], v1s * v1[1] + v2s * v2[1],
3780 v1s * v1[2] + v2s * v2[2]];
3781 }
3782
3783 function cross(v1, v2) {
3784 return [v1[1] * v2[2] - v1[2] * v2[1],
3785 v1[2] * v2[0] - v1[0] * v2[2],
3786 v1[0] * v2[1] - v1[1] * v2[0]];
3787 }
3788
3789 // TODO: Implement 2D matrix decomposition.
3790 // http://dev.w3.org/csswg/css-transforms/#decomposing-a-2d-matrix
3791 function decomposeMatrix(matrix) {
3792 var m3d = [
3793 matrix.slice(0, 4),
3794 matrix.slice(4, 8),
3795 matrix.slice(8, 12),
3796 matrix.slice(12, 16)
3797 ];
3798
3799 // skip normalization step as m3d[3][3] should always be 1
3800 if (m3d[3][3] !== 1) {
3801 throw 'attempt to decompose non-normalized matrix';
3802 }
3803
3804 var perspectiveMatrix = m3d.concat(); // copy m3d
3805 for (var i = 0; i < 3; i++) {
3806 perspectiveMatrix[i][3] = 0;
3807 }
3808
3809 if (determinant(perspectiveMatrix) === 0) {
3810 return false;
3811 }
3812
3813 var rhs = [];
3814
3815 var perspective;
3816 if (m3d[0][3] !== 0 || m3d[1][3] !== 0 || m3d[2][3] !== 0) {
3817 rhs.push(m3d[0][3]);
3818 rhs.push(m3d[1][3]);
3819 rhs.push(m3d[2][3]);
3820 rhs.push(m3d[3][3]);
3821
3822 var inversePerspectiveMatrix = inverse(perspectiveMatrix);
3823 var transposedInversePerspectiveMatrix =
3824 transposeMatrix4(inversePerspectiveMatrix);
3825 perspective = multVecMatrix(rhs, transposedInversePerspectiveMatrix);
3826 } else {
3827 perspective = [0, 0, 0, 1];
3828 }
3829
3830 var translate = m3d[3].slice(0, 3);
3831
3832 var row = [];
3833 row.push(m3d[0].slice(0, 3));
3834 var scale = [];
3835 scale.push(length(row[0]));
3836 row[0] = normalize(row[0]);
3837
3838 var skew = [];
3839 row.push(m3d[1].slice(0, 3));
3840 skew.push(dot(row[0], row[1]));
3841 row[1] = combine(row[1], row[0], 1.0, -skew[0]);
3842
3843 scale.push(length(row[1]));
3844 row[1] = normalize(row[1]);
3845 skew[0] /= scale[1];
3846
3847 row.push(m3d[2].slice(0, 3));
3848 skew.push(dot(row[0], row[2]));
3849 row[2] = combine(row[2], row[0], 1.0, -skew[1]);
3850 skew.push(dot(row[1], row[2]));
3851 row[2] = combine(row[2], row[1], 1.0, -skew[2]);
3852
3853 scale.push(length(row[2]));
3854 row[2] = normalize(row[2]);
3855 skew[1] /= scale[2];
3856 skew[2] /= scale[2];
3857
3858 var pdum3 = cross(row[1], row[2]);
3859 if (dot(row[0], pdum3) < 0) {
3860 for (var i = 0; i < 3; i++) {
3861 scale[i] *= -1;
3862 row[i][0] *= -1;
3863 row[i][1] *= -1;
3864 row[i][2] *= -1;
3865 }
3866 }
3867
3868 var t = row[0][0] + row[1][1] + row[2][2] + 1;
3869 var s;
3870 var quaternion;
3871
3872 if (t > 1e-4) {
3873 s = 0.5 / Math.sqrt(t);
3874 quaternion = [
3875 (row[2][1] - row[1][2]) * s,
3876 (row[0][2] - row[2][0]) * s,
3877 (row[1][0] - row[0][1]) * s,
3878 0.25 / s
3879 ];
3880 } else if (row[0][0] > row[1][1] && row[0][0] > row[2][2]) {
3881 s = Math.sqrt(1 + row[0][0] - row[1][1] - row[2][2]) * 2.0;
3882 quaternion = [
3883 0.25 * s,
3884 (row[0][1] + row[1][0]) / s,
3885 (row[0][2] + row[2][0]) / s,
3886 (row[2][1] - row[1][2]) / s
3887 ];
3888 } else if (row[1][1] > row[2][2]) {
3889 s = Math.sqrt(1.0 + row[1][1] - row[0][0] - row[2][2]) * 2.0;
3890 quaternion = [
3891 (row[0][1] + row[1][0]) / s,
3892 0.25 * s,
3893 (row[1][2] + row[2][1]) / s,
3894 (row[0][2] - row[2][0]) / s
3895 ];
3896 } else {
3897 s = Math.sqrt(1.0 + row[2][2] - row[0][0] - row[1][1]) * 2.0;
3898 quaternion = [
3899 (row[0][2] + row[2][0]) / s,
3900 (row[1][2] + row[2][1]) / s,
3901 0.25 * s,
3902 (row[1][0] - row[0][1]) / s
3903 ];
3904 }
3905
3906 return {
3907 translate: translate, scale: scale, skew: skew,
3908 quaternion: quaternion, perspective: perspective
3909 };
3910 }
3911 return decomposeMatrix;
3912 })();
3913
3914 function dot(v1, v2) {
3915 var result = 0;
3916 for (var i = 0; i < v1.length; i++) {
3917 result += v1[i] * v2[i];
3918 }
3919 return result;
3920 }
3921
3922 function multiplyMatrices(a, b) {
3923 return [
3924 a[0] * b[0] + a[4] * b[1] + a[8] * b[2] + a[12] * b[3],
3925 a[1] * b[0] + a[5] * b[1] + a[9] * b[2] + a[13] * b[3],
3926 a[2] * b[0] + a[6] * b[1] + a[10] * b[2] + a[14] * b[3],
3927 a[3] * b[0] + a[7] * b[1] + a[11] * b[2] + a[15] * b[3],
3928
3929 a[0] * b[4] + a[4] * b[5] + a[8] * b[6] + a[12] * b[7],
3930 a[1] * b[4] + a[5] * b[5] + a[9] * b[6] + a[13] * b[7],
3931 a[2] * b[4] + a[6] * b[5] + a[10] * b[6] + a[14] * b[7],
3932 a[3] * b[4] + a[7] * b[5] + a[11] * b[6] + a[15] * b[7],
3933
3934 a[0] * b[8] + a[4] * b[9] + a[8] * b[10] + a[12] * b[11],
3935 a[1] * b[8] + a[5] * b[9] + a[9] * b[10] + a[13] * b[11],
3936 a[2] * b[8] + a[6] * b[9] + a[10] * b[10] + a[14] * b[11],
3937 a[3] * b[8] + a[7] * b[9] + a[11] * b[10] + a[15] * b[11],
3938
3939 a[0] * b[12] + a[4] * b[13] + a[8] * b[14] + a[12] * b[15],
3940 a[1] * b[12] + a[5] * b[13] + a[9] * b[14] + a[13] * b[15],
3941 a[2] * b[12] + a[6] * b[13] + a[10] * b[14] + a[14] * b[15],
3942 a[3] * b[12] + a[7] * b[13] + a[11] * b[14] + a[15] * b[15]
3943 ];
3944 }
3945
3946 function convertItemToMatrix(item) {
3947 switch (item.t) {
3948 case 'rotateX':
3949 var angle = item.d * Math.PI / 180;
3950 return [1, 0, 0, 0,
3951 0, Math.cos(angle), Math.sin(angle), 0,
3952 0, -Math.sin(angle), Math.cos(angle), 0,
3953 0, 0, 0, 1];
3954 case 'rotateY':
3955 var angle = item.d * Math.PI / 180;
3956 return [Math.cos(angle), 0, -Math.sin(angle), 0,
3957 0, 1, 0, 0,
3958 Math.sin(angle), 0, Math.cos(angle), 0,
3959 0, 0, 0, 1];
3960 case 'rotate':
3961 case 'rotateZ':
3962 var angle = item.d * Math.PI / 180;
3963 return [Math.cos(angle), Math.sin(angle), 0, 0,
3964 -Math.sin(angle), Math.cos(angle), 0, 0,
3965 0, 0, 1, 0,
3966 0, 0, 0, 1];
3967 case 'rotate3d':
3968 var x = item.d[0];
3969 var y = item.d[1];
3970 var z = item.d[2];
3971 var sqrLength = x * x + y * y + z * z;
3972 if (sqrLength === 0) {
3973 x = 1;
3974 y = 0;
3975 z = 0;
3976 } else if (sqrLength !== 1) {
3977 var length = Math.sqrt(sqrLength);
3978 x /= length;
3979 y /= length;
3980 z /= length;
3981 }
3982 var s = Math.sin(item.d[3] * Math.PI / 360);
3983 var sc = s * Math.cos(item.d[3] * Math.PI / 360);
3984 var sq = s * s;
3985 return [
3986 1 - 2 * (y * y + z * z) * sq,
3987 2 * (x * y * sq + z * sc),
3988 2 * (x * z * sq - y * sc),
3989 0,
3990
3991 2 * (x * y * sq - z * sc),
3992 1 - 2 * (x * x + z * z) * sq,
3993 2 * (y * z * sq + x * sc),
3994 0,
3995
3996 2 * (x * z * sq + y * sc),
3997 2 * (y * z * sq - x * sc),
3998 1 - 2 * (x * x + y * y) * sq,
3999 0,
4000
4001 0, 0, 0, 1
4002 ];
4003 case 'scale':
4004 return [item.d[0], 0, 0, 0,
4005 0, item.d[1], 0, 0,
4006 0, 0, 1, 0,
4007 0, 0, 0, 1];
4008 case 'scale3d':
4009 return [item.d[0], 0, 0, 0,
4010 0, item.d[1], 0, 0,
4011 0, 0, item.d[2], 0,
4012 0, 0, 0, 1];
4013 case 'skew':
4014 return [1, Math.tan(item.d[1] * Math.PI / 180), 0, 0,
4015 Math.tan(item.d[0] * Math.PI / 180), 1, 0, 0,
4016 0, 0, 1, 0,
4017 0, 0, 0, 1];
4018 case 'skewX':
4019 return [1, 0, 0, 0,
4020 Math.tan(item.d * Math.PI / 180), 1, 0, 0,
4021 0, 0, 1, 0,
4022 0, 0, 0, 1];
4023 case 'skewY':
4024 return [1, Math.tan(item.d * Math.PI / 180), 0, 0,
4025 0, 1, 0, 0,
4026 0, 0, 1, 0,
4027 0, 0, 0, 1];
4028 // TODO: Work out what to do with non-px values.
4029 case 'translate':
4030 return [1, 0, 0, 0,
4031 0, 1, 0, 0,
4032 0, 0, 1, 0,
4033 item.d[0].px, item.d[1].px, 0, 1];
4034 case 'translate3d':
4035 return [1, 0, 0, 0,
4036 0, 1, 0, 0,
4037 0, 0, 1, 0,
4038 item.d[0].px, item.d[1].px, item.d[2].px, 1];
4039 case 'perspective':
4040 return [
4041 1, 0, 0, 0,
4042 0, 1, 0, 0,
4043 0, 0, 1, -1 / item.d.px,
4044 0, 0, 0, 1];
4045 case 'matrix':
4046 return [item.d[0], item.d[1], 0, 0,
4047 item.d[2], item.d[3], 0, 0,
4048 0, 0, 1, 0,
4049 item.d[4], item.d[5], 0, 1];
4050 case 'matrix3d':
4051 return item.d;
4052 default:
4053 ASSERT_ENABLED && assert(false, 'Transform item type ' + item.t +
4054 ' conversion to matrix not yet implemented.');
4055 }
4056 }
4057
4058 function convertToMatrix(transformList) {
4059 if (transformList.length === 0) {
4060 return [1, 0, 0, 0,
4061 0, 1, 0, 0,
4062 0, 0, 1, 0,
4063 0, 0, 0, 1];
4064 }
4065 return transformList.map(convertItemToMatrix).reduce(multiplyMatrices);
4066 }
4067
4068 var composeMatrix = (function() {
4069 function multiply(a, b) {
4070 var result = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]];
4071 for (var i = 0; i < 4; i++) {
4072 for (var j = 0; j < 4; j++) {
4073 for (var k = 0; k < 4; k++) {
4074 result[i][j] += b[i][k] * a[k][j];
4075 }
4076 }
4077 }
4078 return result;
4079 }
4080
4081 function is2D(m) {
4082 return (
4083 m[0][2] == 0 &&
4084 m[0][3] == 0 &&
4085 m[1][2] == 0 &&
4086 m[1][3] == 0 &&
4087 m[2][0] == 0 &&
4088 m[2][1] == 0 &&
4089 m[2][2] == 1 &&
4090 m[2][3] == 0 &&
4091 m[3][2] == 0 &&
4092 m[3][3] == 1);
4093 }
4094
4095 function composeMatrix(translate, scale, skew, quat, perspective) {
4096 var matrix = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]];
4097
4098 for (var i = 0; i < 4; i++) {
4099 matrix[i][3] = perspective[i];
4100 }
4101
4102 for (var i = 0; i < 3; i++) {
4103 for (var j = 0; j < 3; j++) {
4104 matrix[3][i] += translate[j] * matrix[j][i];
4105 }
4106 }
4107
4108 var x = quat[0], y = quat[1], z = quat[2], w = quat[3];
4109
4110 var rotMatrix = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]];
4111
4112 rotMatrix[0][0] = 1 - 2 * (y * y + z * z);
4113 rotMatrix[0][1] = 2 * (x * y - z * w);
4114 rotMatrix[0][2] = 2 * (x * z + y * w);
4115 rotMatrix[1][0] = 2 * (x * y + z * w);
4116 rotMatrix[1][1] = 1 - 2 * (x * x + z * z);
4117 rotMatrix[1][2] = 2 * (y * z - x * w);
4118 rotMatrix[2][0] = 2 * (x * z - y * w);
4119 rotMatrix[2][1] = 2 * (y * z + x * w);
4120 rotMatrix[2][2] = 1 - 2 * (x * x + y * y);
4121
4122 matrix = multiply(matrix, rotMatrix);
4123
4124 var temp = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]];
4125 if (skew[2]) {
4126 temp[2][1] = skew[2];
4127 matrix = multiply(matrix, temp);
4128 }
4129
4130 if (skew[1]) {
4131 temp[2][1] = 0;
4132 temp[2][0] = skew[0];
4133 matrix = multiply(matrix, temp);
4134 }
4135
4136 if (skew[0]) {
4137 temp[2][0] = 0;
4138 temp[1][0] = skew[0];
4139 matrix = multiply(matrix, temp);
4140 }
4141
4142 for (var i = 0; i < 3; i++) {
4143 for (var j = 0; j < 3; j++) {
4144 matrix[i][j] *= scale[i];
4145 }
4146 }
4147
4148 if (is2D(matrix)) {
4149 return {
4150 t: 'matrix',
4151 d: [matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1],
4152 matrix[3][0], matrix[3][1]]
4153 };
4154 }
4155 return {
4156 t: 'matrix3d',
4157 d: matrix[0].concat(matrix[1], matrix[2], matrix[3])
4158 };
4159 }
4160 return composeMatrix;
4161 })();
4162
4163 function interpolateDecomposedTransformsWithMatrices(fromM, toM, f) {
4164 var product = dot(fromM.quaternion, toM.quaternion);
4165 product = clamp(product, -1.0, 1.0);
4166
4167 var quat = [];
4168 if (product === 1.0) {
4169 quat = fromM.quaternion;
4170 } else {
4171 var theta = Math.acos(product);
4172 var w = Math.sin(f * theta) * 1 / Math.sqrt(1 - product * product);
4173
4174 for (var i = 0; i < 4; i++) {
4175 quat.push(fromM.quaternion[i] * (Math.cos(f * theta) - product * w) +
4176 toM.quaternion[i] * w);
4177 }
4178 }
4179
4180 var translate = interp(fromM.translate, toM.translate, f);
4181 var scale = interp(fromM.scale, toM.scale, f);
4182 var skew = interp(fromM.skew, toM.skew, f);
4183 var perspective = interp(fromM.perspective, toM.perspective, f);
4184
4185 return composeMatrix(translate, scale, skew, quat, perspective);
4186 }
4187
4188 function interpTransformValue(from, to, f) {
4189 var type = from.t ? from.t : to.t;
4190 switch (type) {
4191 case 'matrix':
4192 case 'matrix3d':
4193 ASSERT_ENABLED && assert(false,
4194 'Must use matrix decomposition when interpolating raw matrices');
4195 // Transforms with unitless parameters.
4196 case 'rotate':
4197 case 'rotateX':
4198 case 'rotateY':
4199 case 'rotateZ':
4200 case 'rotate3d':
4201 case 'scale':
4202 case 'scaleX':
4203 case 'scaleY':
4204 case 'scaleZ':
4205 case 'scale3d':
4206 case 'skew':
4207 case 'skewX':
4208 case 'skewY':
4209 return {t: type, d: interp(from.d, to.d, f, type)};
4210 default:
4211 // Transforms with lengthType parameters.
4212 var result = [];
4213 var maxVal;
4214 if (from.d && to.d) {
4215 maxVal = Math.max(from.d.length, to.d.length);
4216 } else if (from.d) {
4217 maxVal = from.d.length;
4218 } else {
4219 maxVal = to.d.length;
4220 }
4221 for (var j = 0; j < maxVal; j++) {
4222 var fromVal = from.d ? from.d[j] : {};
4223 var toVal = to.d ? to.d[j] : {};
4224 result.push(lengthType.interpolate(fromVal, toVal, f));
4225 }
4226 return {t: type, d: result};
4227 }
4228 }
4229
4230 function isMatrix(item) {
4231 return item.t[0] === 'm';
4232 }
4233
4234 // The CSSWG decided to disallow scientific notation in CSS property strings
4235 // (see http://lists.w3.org/Archives/Public/www-style/2010Feb/0050.html).
4236 // We need this function to hakonitize all numbers before adding them to
4237 // property strings.
4238 // TODO: Apply this function to all property strings
4239 function n(num) {
4240 return Number(num).toFixed(4);
4241 }
4242
4243 var transformType = {
4244 add: function(base, delta) { return base.concat(delta); },
4245 interpolate: function(from, to, f) {
4246 var out = [];
4247 for (var i = 0; i < Math.min(from.length, to.length); i++) {
4248 if (from[i].t !== to[i].t || isMatrix(from[i])) {
4249 break;
4250 }
4251 out.push(interpTransformValue(from[i], to[i], f));
4252 }
4253
4254 if (i < Math.min(from.length, to.length) ||
4255 from.some(isMatrix) || to.some(isMatrix)) {
4256 if (from.decompositionPair !== to) {
4257 from.decompositionPair = to;
4258 from.decomposition = decomposeMatrix(convertToMatrix(from.slice(i)));
4259 }
4260 if (to.decompositionPair !== from) {
4261 to.decompositionPair = from;
4262 to.decomposition = decomposeMatrix(convertToMatrix(to.slice(i)));
4263 }
4264 out.push(interpolateDecomposedTransformsWithMatrices(
4265 from.decomposition, to.decomposition, f));
4266 return out;
4267 }
4268
4269 for (; i < from.length; i++) {
4270 out.push(interpTransformValue(from[i], {t: null, d: null}, f));
4271 }
4272 for (; i < to.length; i++) {
4273 out.push(interpTransformValue({t: null, d: null}, to[i], f));
4274 }
4275 return out;
4276 },
4277 toCssValue: function(value, svgMode) {
4278 // TODO: fix this :)
4279 var out = '';
4280 for (var i = 0; i < value.length; i++) {
4281 ASSERT_ENABLED && assert(
4282 value[i].t, 'transform type should be resolved by now');
4283 switch (value[i].t) {
4284 case 'rotate':
4285 case 'rotateX':
4286 case 'rotateY':
4287 case 'rotateZ':
4288 case 'skewX':
4289 case 'skewY':
4290 var unit = svgMode ? '' : 'deg';
4291 out += value[i].t + '(' + value[i].d + unit + ') ';
4292 break;
4293 case 'skew':
4294 var unit = svgMode ? '' : 'deg';
4295 out += value[i].t + '(' + value[i].d[0] + unit;
4296 if (value[i].d[1] === 0) {
4297 out += ') ';
4298 } else {
4299 out += ', ' + value[i].d[1] + unit + ') ';
4300 }
4301 break;
4302 case 'rotate3d':
4303 var unit = svgMode ? '' : 'deg';
4304 out += value[i].t + '(' + value[i].d[0] + ', ' + value[i].d[1] +
4305 ', ' + value[i].d[2] + ', ' + value[i].d[3] + unit + ') ';
4306 break;
4307 case 'translateX':
4308 case 'translateY':
4309 case 'translateZ':
4310 case 'perspective':
4311 out += value[i].t + '(' + lengthType.toCssValue(value[i].d[0]) +
4312 ') ';
4313 break;
4314 case 'translate':
4315 if (svgMode) {
4316 if (value[i].d[1] === undefined) {
4317 out += value[i].t + '(' + value[i].d[0].px + ') ';
4318 } else {
4319 out += (
4320 value[i].t + '(' + value[i].d[0].px + ', ' +
4321 value[i].d[1].px + ') ');
4322 }
4323 break;
4324 }
4325 if (value[i].d[1] === undefined) {
4326 out += value[i].t + '(' + lengthType.toCssValue(value[i].d[0]) +
4327 ') ';
4328 } else {
4329 out += value[i].t + '(' + lengthType.toCssValue(value[i].d[0]) +
4330 ', ' + lengthType.toCssValue(value[i].d[1]) + ') ';
4331 }
4332 break;
4333 case 'translate3d':
4334 var values = value[i].d.map(lengthType.toCssValue);
4335 out += value[i].t + '(' + values[0] + ', ' + values[1] +
4336 ', ' + values[2] + ') ';
4337 break;
4338 case 'scale':
4339 if (value[i].d[0] === value[i].d[1]) {
4340 out += value[i].t + '(' + value[i].d[0] + ') ';
4341 } else {
4342 out += value[i].t + '(' + value[i].d[0] + ', ' + value[i].d[1] +
4343 ') ';
4344 }
4345 break;
4346 case 'scaleX':
4347 case 'scaleY':
4348 case 'scaleZ':
4349 out += value[i].t + '(' + value[i].d[0] + ') ';
4350 break;
4351 case 'scale3d':
4352 out += value[i].t + '(' + value[i].d[0] + ', ' +
4353 value[i].d[1] + ', ' + value[i].d[2] + ') ';
4354 break;
4355 case 'matrix':
4356 case 'matrix3d':
4357 out += value[i].t + '(' + value[i].d.map(n).join(', ') + ') ';
4358 break;
4359 }
4360 }
4361 return out.substring(0, out.length - 1);
4362 },
4363 fromCssValue: function(value) {
4364 // TODO: fix this :)
4365 if (value === undefined) {
4366 return undefined;
4367 }
4368 var result = [];
4369 while (value.length > 0) {
4370 var r;
4371 for (var i = 0; i < transformREs.length; i++) {
4372 var reSpec = transformREs[i];
4373 r = reSpec[0].exec(value);
4374 if (r) {
4375 result.push({t: reSpec[2], d: reSpec[1](r)});
4376 value = value.substring(r[0].length);
4377 break;
4378 }
4379 }
4380 if (!isDefinedAndNotNull(r)) {
4381 return result;
4382 }
4383 }
4384 return result;
4385 }
4386 };
4387
4388 var pathType = {
4389 // Properties ...
4390 // - path: The target path element
4391 // - points: The absolute points to set on the path
4392 // - cachedCumulativeLengths: The lengths at the end of each segment
4393 add: function() { throw 'Addition not supported for path attribute' },
4394 cumulativeLengths: function(value) {
4395 if (isDefinedAndNotNull(value.cachedCumulativeLengths))
4396 return value.cachedCumulativeLengths;
4397 var path = value.path.cloneNode(true);
4398 var cumulativeLengths = [];
4399 while (path.pathSegList.numberOfItems > 0) {
4400 // TODO: It would be good to skip moves here and when generating points.
4401 cumulativeLengths.unshift(path.getTotalLength());
4402 path.pathSegList.removeItem(path.pathSegList.numberOfItems - 1);
4403 }
4404 value.cachedCumulativeLengths = cumulativeLengths;
4405 return value.cachedCumulativeLengths;
4406 },
4407 appendFractions: function(fractions, cumulativeLengths) {
4408 ASSERT_ENABLED && assert(cumulativeLengths[0] === 0);
4409 var totalLength = cumulativeLengths[cumulativeLengths.length - 1];
4410 for (var i = 1; i < cumulativeLengths.length - 1; ++i)
4411 fractions.push(cumulativeLengths[i] / totalLength);
4412 },
4413 interpolate: function(from, to, f) {
4414 // FIXME: Handle non-linear path segments.
4415 // Get the fractions at which we need to sample.
4416 var sampleFractions = [0, 1];
4417 pathType.appendFractions(sampleFractions, pathType.cumulativeLengths(from));
4418 pathType.appendFractions(sampleFractions, pathType.cumulativeLengths(to));
4419 sampleFractions.sort();
4420 ASSERT_ENABLED && assert(sampleFractions[0] === 0);
4421 ASSERT_ENABLED && assert(sampleFractions[sampleFractions.length - 1] === 1);
4422
4423 // FIXME: Cache the 'from' and 'to' points.
4424 var fromTotalLength = from.path.getTotalLength();
4425 var toTotalLength = to.path.getTotalLength();
4426 var points = [];
4427 for (var i = 0; i < sampleFractions.length; ++i) {
4428 var fromPoint = from.path.getPointAtLength(
4429 fromTotalLength * sampleFractions[i]);
4430 var toPoint = to.path.getPointAtLength(
4431 toTotalLength * sampleFractions[i]);
4432 points.push({
4433 x: interp(fromPoint.x, toPoint.x, f),
4434 y: interp(fromPoint.y, toPoint.y, f)
4435 });
4436 }
4437 return {points: points};
4438 },
4439 pointToString: function(point) {
4440 return point.x + ',' + point.y;
4441 },
4442 toCssValue: function(value, svgMode) {
4443 // FIXME: It would be good to use PathSegList API on the target directly,
4444 // rather than generating this string, but that would require a hack to
4445 // setValue().
4446 ASSERT_ENABLED && assert(svgMode,
4447 'Path type should only be used with SVG \'d\' attribute');
4448 if (value.path)
4449 return value.path.getAttribute('d');
4450 var ret = 'M' + pathType.pointToString(value.points[0]);
4451 for (var i = 1; i < value.points.length; ++i)
4452 ret += 'L' + pathType.pointToString(value.points[i]);
4453 return ret;
4454 },
4455 fromCssValue: function(value) {
4456 var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
4457 if (value)
4458 path.setAttribute('d', value);
4459 return {path: path};
4460 }
4461 };
4462
4463 var propertyTypes = {
4464 backgroundColor: colorType,
4465 backgroundPosition: positionListType,
4466 borderBottomColor: colorType,
4467 borderBottomLeftRadius: percentLengthType,
4468 borderBottomRightRadius: percentLengthType,
4469 borderBottomWidth: lengthType,
4470 borderLeftColor: colorType,
4471 borderLeftWidth: lengthType,
4472 borderRightColor: colorType,
4473 borderRightWidth: lengthType,
4474 borderSpacing: lengthType,
4475 borderTopColor: colorType,
4476 borderTopLeftRadius: percentLengthType,
4477 borderTopRightRadius: percentLengthType,
4478 borderTopWidth: lengthType,
4479 bottom: percentLengthAutoType,
4480 boxShadow: shadowType,
4481 clip: typeWithKeywords(['auto'], rectangleType),
4482 color: colorType,
4483 cx: lengthType,
4484 cy: lengthType,
4485 d: pathType,
4486 dx: lengthType,
4487 dy: lengthType,
4488 fill: colorType,
4489 floodColor: colorType,
4490
4491 // TODO: Handle these keywords properly.
4492 fontSize: typeWithKeywords(['smaller', 'larger'], percentLengthType),
4493 fontWeight: typeWithKeywords(['lighter', 'bolder'], fontWeightType),
4494
4495 height: percentLengthAutoType,
4496 left: percentLengthAutoType,
4497 letterSpacing: typeWithKeywords(['normal'], lengthType),
4498 lightingColor: colorType,
4499 lineHeight: percentLengthType, // TODO: Should support numberType as well.
4500 marginBottom: lengthAutoType,
4501 marginLeft: lengthAutoType,
4502 marginRight: lengthAutoType,
4503 marginTop: lengthAutoType,
4504 maxHeight: typeWithKeywords(
4505 ['none', 'max-content', 'min-content', 'fill-available', 'fit-content'],
4506 percentLengthType),
4507 maxWidth: typeWithKeywords(
4508 ['none', 'max-content', 'min-content', 'fill-available', 'fit-content'],
4509 percentLengthType),
4510 minHeight: typeWithKeywords(
4511 ['max-content', 'min-content', 'fill-available', 'fit-content'],
4512 percentLengthType),
4513 minWidth: typeWithKeywords(
4514 ['max-content', 'min-content', 'fill-available', 'fit-content'],
4515 percentLengthType),
4516 opacity: numberType,
4517 outlineColor: typeWithKeywords(['invert'], colorType),
4518 outlineOffset: lengthType,
4519 outlineWidth: lengthType,
4520 paddingBottom: lengthType,
4521 paddingLeft: lengthType,
4522 paddingRight: lengthType,
4523 paddingTop: lengthType,
4524 perspective: typeWithKeywords(['none'], lengthType),
4525 perspectiveOrigin: originType,
4526 r: lengthType,
4527 right: percentLengthAutoType,
4528 stopColor: colorType,
4529 stroke: colorType,
4530 textIndent: typeWithKeywords(['each-line', 'hanging'], percentLengthType),
4531 textShadow: shadowType,
4532 top: percentLengthAutoType,
4533 transform: transformType,
4534 transformOrigin: originType,
4535 verticalAlign: typeWithKeywords([
4536 'baseline',
4537 'sub',
4538 'super',
4539 'text-top',
4540 'text-bottom',
4541 'middle',
4542 'top',
4543 'bottom'
4544 ], percentLengthType),
4545 visibility: visibilityType,
4546 width: typeWithKeywords([
4547 'border-box',
4548 'content-box',
4549 'auto',
4550 'max-content',
4551 'min-content',
4552 'available',
4553 'fit-content'
4554 ], percentLengthType),
4555 wordSpacing: typeWithKeywords(['normal'], percentLengthType),
4556 x: lengthType,
4557 y: lengthType,
4558 zIndex: typeWithKeywords(['auto'], integerType)
4559 };
4560
4561 var svgProperties = {
4562 'cx': 1,
4563 'cy': 1,
4564 'd': 1,
4565 'dx': 1,
4566 'dy': 1,
4567 'fill': 1,
4568 'floodColor': 1,
4569 'height': 1,
4570 'lightingColor': 1,
4571 'r': 1,
4572 'stopColor': 1,
4573 'stroke': 1,
4574 'width': 1,
4575 'x': 1,
4576 'y': 1
4577 };
4578
4579 var borderWidthAliases = {
4580 initial: '3px',
4581 thin: '1px',
4582 medium: '3px',
4583 thick: '5px'
4584 };
4585
4586 var propertyValueAliases = {
4587 backgroundColor: { initial: 'transparent' },
4588 backgroundPosition: { initial: '0% 0%' },
4589 borderBottomColor: { initial: 'currentColor' },
4590 borderBottomLeftRadius: { initial: '0px' },
4591 borderBottomRightRadius: { initial: '0px' },
4592 borderBottomWidth: borderWidthAliases,
4593 borderLeftColor: { initial: 'currentColor' },
4594 borderLeftWidth: borderWidthAliases,
4595 borderRightColor: { initial: 'currentColor' },
4596 borderRightWidth: borderWidthAliases,
4597 // Spec says this should be 0 but in practise it is 2px.
4598 borderSpacing: { initial: '2px' },
4599 borderTopColor: { initial: 'currentColor' },
4600 borderTopLeftRadius: { initial: '0px' },
4601 borderTopRightRadius: { initial: '0px' },
4602 borderTopWidth: borderWidthAliases,
4603 bottom: { initial: 'auto' },
4604 clip: { initial: 'rect(0px, 0px, 0px, 0px)' },
4605 color: { initial: 'black' }, // Depends on user agent.
4606 fontSize: {
4607 initial: '100%',
4608 'xx-small': '60%',
4609 'x-small': '75%',
4610 'small': '89%',
4611 'medium': '100%',
4612 'large': '120%',
4613 'x-large': '150%',
4614 'xx-large': '200%'
4615 },
4616 fontWeight: {
4617 initial: '400',
4618 normal: '400',
4619 bold: '700'
4620 },
4621 height: { initial: 'auto' },
4622 left: { initial: 'auto' },
4623 letterSpacing: { initial: 'normal' },
4624 lineHeight: {
4625 initial: '120%',
4626 normal: '120%'
4627 },
4628 marginBottom: { initial: '0px' },
4629 marginLeft: { initial: '0px' },
4630 marginRight: { initial: '0px' },
4631 marginTop: { initial: '0px' },
4632 maxHeight: { initial: 'none' },
4633 maxWidth: { initial: 'none' },
4634 minHeight: { initial: '0px' },
4635 minWidth: { initial: '0px' },
4636 opacity: { initial: '1.0' },
4637 outlineColor: { initial: 'invert' },
4638 outlineOffset: { initial: '0px' },
4639 outlineWidth: borderWidthAliases,
4640 paddingBottom: { initial: '0px' },
4641 paddingLeft: { initial: '0px' },
4642 paddingRight: { initial: '0px' },
4643 paddingTop: { initial: '0px' },
4644 right: { initial: 'auto' },
4645 textIndent: { initial: '0px' },
4646 textShadow: {
4647 initial: '0px 0px 0px transparent',
4648 none: '0px 0px 0px transparent'
4649 },
4650 top: { initial: 'auto' },
4651 transform: {
4652 initial: '',
4653 none: ''
4654 },
4655 verticalAlign: { initial: '0px' },
4656 visibility: { initial: 'visible' },
4657 width: { initial: 'auto' },
4658 wordSpacing: { initial: 'normal' },
4659 zIndex: { initial: 'auto' }
4660 };
4661
4662 var propertyIsSVGAttrib = function(property, target) {
4663 return target.namespaceURI === 'http://www.w3.org/2000/svg' &&
4664 property in svgProperties;
4665 };
4666
4667 var getType = function(property) {
4668 return propertyTypes[property] || nonNumericType;
4669 };
4670
4671 var add = function(property, base, delta) {
4672 if (delta === rawNeutralValue) {
4673 return base;
4674 }
4675 if (base === 'inherit' || delta === 'inherit') {
4676 return nonNumericType.add(base, delta);
4677 }
4678 return getType(property).add(base, delta);
4679 };
4680
4681
4682 /**
4683 * Interpolate the given property name (f*100)% of the way from 'from' to 'to'.
4684 * 'from' and 'to' are both raw values already converted from CSS value
4685 * strings. Requires the target element to be able to determine whether the
4686 * given property is an SVG attribute or not, as this impacts the conversion of
4687 * the interpolated value back into a CSS value string for transform
4688 * translations.
4689 *
4690 * e.g. interpolate('transform', elem, 'rotate(40deg)', 'rotate(50deg)', 0.3);
4691 * will return 'rotate(43deg)'.
4692 */
4693 var interpolate = function(property, from, to, f) {
4694 ASSERT_ENABLED && assert(
4695 isDefinedAndNotNull(from) && isDefinedAndNotNull(to),
4696 'Both to and from values should be specified for interpolation');
4697 if (from === 'inherit' || to === 'inherit') {
4698 return nonNumericType.interpolate(from, to, f);
4699 }
4700 if (f === 0) {
4701 return from;
4702 }
4703 if (f === 1) {
4704 return to;
4705 }
4706 return getType(property).interpolate(from, to, f);
4707 };
4708
4709
4710 /**
4711 * Convert the provided interpolable value for the provided property to a CSS
4712 * value string. Note that SVG transforms do not require units for translate
4713 * or rotate values while CSS properties require 'px' or 'deg' units.
4714 */
4715 var toCssValue = function(property, value, svgMode) {
4716 if (value === 'inherit') {
4717 return value;
4718 }
4719 return getType(property).toCssValue(value, svgMode);
4720 };
4721
4722 var fromCssValue = function(property, value) {
4723 if (value === cssNeutralValue) {
4724 return rawNeutralValue;
4725 }
4726 if (value === 'inherit') {
4727 return value;
4728 }
4729 if (property in propertyValueAliases &&
4730 value in propertyValueAliases[property]) {
4731 value = propertyValueAliases[property][value];
4732 }
4733 var result = getType(property).fromCssValue(value);
4734 // Currently we'll hit this assert if input to the API is bad. To avoid this,
4735 // we should eliminate invalid values when normalizing the list of keyframes.
4736 // See the TODO in isSupportedPropertyValue().
4737 ASSERT_ENABLED && assert(isDefinedAndNotNull(result),
4738 'Invalid property value "' + value + '" for property "' + property + '"');
4739 return result;
4740 };
4741
4742 // Sentinel values
4743 var cssNeutralValue = {};
4744 var rawNeutralValue = {};
4745
4746
4747
4748 /** @constructor */
4749 var CompositableValue = function() {
4750 };
4751
4752 CompositableValue.prototype = {
4753 compositeOnto: abstractMethod,
4754 // This is purely an optimization.
4755 dependsOnUnderlyingValue: function() {
4756 return true;
4757 }
4758 };
4759
4760
4761
4762 /** @constructor */
4763 var AddReplaceCompositableValue = function(value, composite) {
4764 this.value = value;
4765 this.composite = composite;
4766 ASSERT_ENABLED && assert(
4767 !(this.value === cssNeutralValue && this.composite === 'replace'),
4768 'Should never replace-composite the neutral value');
4769 };
4770
4771 AddReplaceCompositableValue.prototype = createObject(
4772 CompositableValue.prototype, {
4773 compositeOnto: function(property, underlyingValue) {
4774 switch (this.composite) {
4775 case 'replace':
4776 return this.value;
4777 case 'add':
4778 return add(property, underlyingValue, this.value);
4779 default:
4780 ASSERT_ENABLED && assert(
4781 false, 'Invalid composite operation ' + this.composite);
4782 }
4783 },
4784 dependsOnUnderlyingValue: function() {
4785 return this.composite === 'add';
4786 }
4787 });
4788
4789
4790
4791 /** @constructor */
4792 var BlendedCompositableValue = function(startValue, endValue, fraction) {
4793 this.startValue = startValue;
4794 this.endValue = endValue;
4795 this.fraction = fraction;
4796 };
4797
4798 BlendedCompositableValue.prototype = createObject(
4799 CompositableValue.prototype, {
4800 compositeOnto: function(property, underlyingValue) {
4801 return interpolate(property,
4802 this.startValue.compositeOnto(property, underlyingValue),
4803 this.endValue.compositeOnto(property, underlyingValue),
4804 this.fraction);
4805 },
4806 dependsOnUnderlyingValue: function() {
4807 // Travis crashes here randomly in Chrome beta and unstable,
4808 // this try catch is to help debug the problem.
4809 try {
4810 return this.startValue.dependsOnUnderlyingValue() ||
4811 this.endValue.dependsOnUnderlyingValue();
4812 }
4813 catch (error) {
4814 throw new Error(
4815 error + '\n JSON.stringify(this) = ' + JSON.stringify(this));
4816 }
4817 }
4818 });
4819
4820 /** @constructor */
4821 var CompositedPropertyMap = function(target) {
4822 this.properties = {};
4823 this.baseValues = {};
4824 this.target = target;
4825 };
4826
4827 CompositedPropertyMap.prototype = {
4828 addValue: function(property, animValue) {
4829 if (!(property in this.properties)) {
4830 this.properties[property] = [];
4831 }
4832 if (!(animValue instanceof CompositableValue)) {
4833 throw new TypeError('expected CompositableValue');
4834 }
4835 this.properties[property].push(animValue);
4836 },
4837 stackDependsOnUnderlyingValue: function(stack) {
4838 for (var i = 0; i < stack.length; i++) {
4839 if (!stack[i].dependsOnUnderlyingValue()) {
4840 return false;
4841 }
4842 }
4843 return true;
4844 },
4845 clear: function() {
4846 for (var property in this.properties) {
4847 if (this.stackDependsOnUnderlyingValue(this.properties[property])) {
4848 clearValue(this.target, property);
4849 }
4850 }
4851 },
4852 captureBaseValues: function() {
4853 for (var property in this.properties) {
4854 var stack = this.properties[property];
4855 if (stack.length > 0 && this.stackDependsOnUnderlyingValue(stack)) {
4856 var baseValue = fromCssValue(property, getValue(this.target, property));
4857 // TODO: Decide what to do with elements not in the DOM.
4858 ASSERT_ENABLED && assert(
4859 isDefinedAndNotNull(baseValue) && baseValue !== '',
4860 'Base value should always be set. ' +
4861 'Is the target element in the DOM?');
4862 this.baseValues[property] = baseValue;
4863 } else {
4864 this.baseValues[property] = undefined;
4865 }
4866 }
4867 },
4868 applyAnimatedValues: function() {
4869 for (var property in this.properties) {
4870 var valuesToComposite = this.properties[property];
4871 if (valuesToComposite.length === 0) {
4872 continue;
4873 }
4874 var baseValue = this.baseValues[property];
4875 var i = valuesToComposite.length - 1;
4876 while (i > 0 && valuesToComposite[i].dependsOnUnderlyingValue()) {
4877 i--;
4878 }
4879 for (; i < valuesToComposite.length; i++) {
4880 baseValue = valuesToComposite[i].compositeOnto(property, baseValue);
4881 }
4882 ASSERT_ENABLED && assert(
4883 isDefinedAndNotNull(baseValue) && baseValue !== '',
4884 'Value should always be set after compositing');
4885 var isSvgMode = propertyIsSVGAttrib(property, this.target);
4886 setValue(this.target, property, toCssValue(property, baseValue,
4887 isSvgMode));
4888 this.properties[property] = [];
4889 }
4890 }
4891 };
4892
4893
4894 var cssStyleDeclarationAttribute = {
4895 cssText: true,
4896 length: true,
4897 parentRule: true,
4898 'var': true
4899 };
4900
4901 var cssStyleDeclarationMethodModifiesStyle = {
4902 getPropertyValue: false,
4903 getPropertyCSSValue: false,
4904 removeProperty: true,
4905 getPropertyPriority: false,
4906 setProperty: true,
4907 item: false
4908 };
4909
4910 var copyInlineStyle = function(sourceStyle, destinationStyle) {
4911 for (var i = 0; i < sourceStyle.length; i++) {
4912 var property = sourceStyle[i];
4913 destinationStyle[property] = sourceStyle[property];
4914 }
4915 };
4916
4917 var retickThenGetComputedStyle = function() {
4918 repeatLastTick();
4919 ensureOriginalGetComputedStyle();
4920 return window.getComputedStyle.apply(this, arguments);
4921 };
4922
4923 // This redundant flag is to support Safari which has trouble determining
4924 // function object equality during an animation.
4925 var isGetComputedStylePatched = false;
4926 var originalGetComputedStyle = window.getComputedStyle;
4927
4928 var ensureRetickBeforeGetComputedStyle = function() {
4929 if (!isGetComputedStylePatched) {
4930 Object.defineProperty(window, 'getComputedStyle', configureDescriptor({
4931 value: retickThenGetComputedStyle
4932 }));
4933 isGetComputedStylePatched = true;
4934 }
4935 };
4936
4937 var ensureOriginalGetComputedStyle = function() {
4938 if (isGetComputedStylePatched) {
4939 Object.defineProperty(window, 'getComputedStyle', configureDescriptor({
4940 value: originalGetComputedStyle
4941 }));
4942 isGetComputedStylePatched = false;
4943 }
4944 };
4945
4946 // Changing the inline style of an element under animation may require the
4947 // animation to be recomputed ontop of the new inline style if
4948 // getComputedStyle() is called inbetween setting the style and the next
4949 // animation frame.
4950 // We modify getComputedStyle() to re-evaluate the animations only if it is
4951 // called instead of re-evaluating them here potentially unnecessarily.
4952 var animatedInlineStyleChanged = function() {
4953 maybeRestartAnimation();
4954 ensureRetickBeforeGetComputedStyle();
4955 };
4956
4957
4958
4959 /** @constructor */
4960 var AnimatedCSSStyleDeclaration = function(element) {
4961 ASSERT_ENABLED && assert(
4962 !(element.style instanceof AnimatedCSSStyleDeclaration),
4963 'Element must not already have an animated style attached.');
4964
4965 // Stores the inline style of the element on its behalf while the
4966 // polyfill uses the element's inline style to simulate web animations.
4967 // This is needed to fake regular inline style CSSOM access on the element.
4968 this._surrogateElement = createDummyElement();
4969 this._style = element.style;
4970 this._length = 0;
4971 this._isAnimatedProperty = {};
4972
4973 // Populate the surrogate element's inline style.
4974 copyInlineStyle(this._style, this._surrogateElement.style);
4975 this._updateIndices();
4976 };
4977
4978 AnimatedCSSStyleDeclaration.prototype = {
4979 get cssText() {
4980 return this._surrogateElement.style.cssText;
4981 },
4982 set cssText(text) {
4983 var isAffectedProperty = {};
4984 for (var i = 0; i < this._surrogateElement.style.length; i++) {
4985 isAffectedProperty[this._surrogateElement.style[i]] = true;
4986 }
4987 this._surrogateElement.style.cssText = text;
4988 this._updateIndices();
4989 for (var i = 0; i < this._surrogateElement.style.length; i++) {
4990 isAffectedProperty[this._surrogateElement.style[i]] = true;
4991 }
4992 for (var property in isAffectedProperty) {
4993 if (!this._isAnimatedProperty[property]) {
4994 this._style.setProperty(property,
4995 this._surrogateElement.style.getPropertyValue(property));
4996 }
4997 }
4998 animatedInlineStyleChanged();
4999 },
5000 get length() {
5001 return this._surrogateElement.style.length;
5002 },
5003 get parentRule() {
5004 return this._style.parentRule;
5005 },
5006 get 'var'() {
5007 return this._style.var;
5008 },
5009 _updateIndices: function() {
5010 while (this._length < this._surrogateElement.style.length) {
5011 Object.defineProperty(this, this._length, {
5012 configurable: true,
5013 enumerable: false,
5014 get: (function(index) {
5015 return function() {
5016 return this._surrogateElement.style[index];
5017 };
5018 })(this._length)
5019 });
5020 this._length++;
5021 }
5022 while (this._length > this._surrogateElement.style.length) {
5023 this._length--;
5024 Object.defineProperty(this, this._length, {
5025 configurable: true,
5026 enumerable: false,
5027 value: undefined
5028 });
5029 }
5030 },
5031 _clearAnimatedProperty: function(property) {
5032 this._style[property] = this._surrogateElement.style[property];
5033 this._isAnimatedProperty[property] = false;
5034 },
5035 _setAnimatedProperty: function(property, value) {
5036 this._style[property] = value;
5037 this._isAnimatedProperty[property] = true;
5038 }
5039 };
5040
5041 for (var method in cssStyleDeclarationMethodModifiesStyle) {
5042 AnimatedCSSStyleDeclaration.prototype[method] =
5043 (function(method, modifiesStyle) {
5044 return function() {
5045 var result = this._surrogateElement.style[method].apply(
5046 this._surrogateElement.style, arguments);
5047 if (modifiesStyle) {
5048 if (!this._isAnimatedProperty[arguments[0]]) {
5049 this._style[method].apply(this._style, arguments);
5050 }
5051 this._updateIndices();
5052 animatedInlineStyleChanged();
5053 }
5054 return result;
5055 }
5056 })(method, cssStyleDeclarationMethodModifiesStyle[method]);
5057 }
5058
5059 for (var property in document.documentElement.style) {
5060 if (cssStyleDeclarationAttribute[property] ||
5061 property in cssStyleDeclarationMethodModifiesStyle) {
5062 continue;
5063 }
5064 (function(property) {
5065 Object.defineProperty(AnimatedCSSStyleDeclaration.prototype, property,
5066 configureDescriptor({
5067 get: function() {
5068 return this._surrogateElement.style[property];
5069 },
5070 set: function(value) {
5071 this._surrogateElement.style[property] = value;
5072 this._updateIndices();
5073 if (!this._isAnimatedProperty[property]) {
5074 this._style[property] = value;
5075 }
5076 animatedInlineStyleChanged();
5077 }
5078 }));
5079 })(property);
5080 }
5081
5082 // This function is a fallback for when we can't replace an element's style with
5083 // AnimatatedCSSStyleDeclaration and must patch the existing style to behave
5084 // in a similar way.
5085 // Only the methods listed in cssStyleDeclarationMethodModifiesStyle will
5086 // be patched to behave in the same manner as a native implementation,
5087 // getter properties like style.left or style[0] will be tainted by the
5088 // polyfill's animation engine.
5089 var patchInlineStyleForAnimation = function(style) {
5090 var surrogateElement = document.createElement('div');
5091 copyInlineStyle(style, surrogateElement.style);
5092 var isAnimatedProperty = {};
5093 for (var method in cssStyleDeclarationMethodModifiesStyle) {
5094 if (!(method in style)) {
5095 continue;
5096 }
5097 Object.defineProperty(style, method, configureDescriptor({
5098 value: (function(method, originalMethod, modifiesStyle) {
5099 return function() {
5100 var result = surrogateElement.style[method].apply(
5101 surrogateElement.style, arguments);
5102 if (modifiesStyle) {
5103 if (!isAnimatedProperty[arguments[0]]) {
5104 originalMethod.apply(style, arguments);
5105 }
5106 animatedInlineStyleChanged();
5107 }
5108 return result;
5109 }
5110 })(method, style[method], cssStyleDeclarationMethodModifiesStyle[method])
5111 }));
5112 }
5113
5114 style._clearAnimatedProperty = function(property) {
5115 this[property] = surrogateElement.style[property];
5116 isAnimatedProperty[property] = false;
5117 };
5118
5119 style._setAnimatedProperty = function(property, value) {
5120 this[property] = value;
5121 isAnimatedProperty[property] = true;
5122 };
5123 };
5124
5125
5126
5127 /** @constructor */
5128 var Compositor = function() {
5129 this.targets = [];
5130 };
5131
5132 Compositor.prototype = {
5133 setAnimatedValue: function(target, property, animValue) {
5134 if (target !== null) {
5135 if (target._animProperties === undefined) {
5136 target._animProperties = new CompositedPropertyMap(target);
5137 this.targets.push(target);
5138 }
5139 target._animProperties.addValue(property, animValue);
5140 }
5141 },
5142 applyAnimatedValues: function() {
5143 for (var i = 0; i < this.targets.length; i++) {
5144 this.targets[i]._animProperties.clear();
5145 }
5146 for (var i = 0; i < this.targets.length; i++) {
5147 this.targets[i]._animProperties.captureBaseValues();
5148 }
5149 for (var i = 0; i < this.targets.length; i++) {
5150 this.targets[i]._animProperties.applyAnimatedValues();
5151 }
5152 }
5153 };
5154
5155 var ensureTargetInitialised = function(property, target) {
5156 if (propertyIsSVGAttrib(property, target)) {
5157 ensureTargetSVGInitialised(property, target);
5158 } else {
5159 ensureTargetCSSInitialised(target);
5160 }
5161 };
5162
5163 var ensureTargetSVGInitialised = function(property, target) {
5164 if (!isDefinedAndNotNull(target._actuals)) {
5165 target._actuals = {};
5166 target._bases = {};
5167 target.actuals = {};
5168 target._getAttribute = target.getAttribute;
5169 target._setAttribute = target.setAttribute;
5170 target.getAttribute = function(name) {
5171 if (isDefinedAndNotNull(target._bases[name])) {
5172 return target._bases[name];
5173 }
5174 return target._getAttribute(name);
5175 };
5176 target.setAttribute = function(name, value) {
5177 if (isDefinedAndNotNull(target._actuals[name])) {
5178 target._bases[name] = value;
5179 } else {
5180 target._setAttribute(name, value);
5181 }
5182 };
5183 }
5184 if (!isDefinedAndNotNull(target._actuals[property])) {
5185 var baseVal = target.getAttribute(property);
5186 target._actuals[property] = 0;
5187 target._bases[property] = baseVal;
5188
5189 Object.defineProperty(target.actuals, property, configureDescriptor({
5190 set: function(value) {
5191 if (value === null) {
5192 target._actuals[property] = target._bases[property];
5193 target._setAttribute(property, target._bases[property]);
5194 } else {
5195 target._actuals[property] = value;
5196 target._setAttribute(property, value);
5197 }
5198 },
5199 get: function() {
5200 return target._actuals[property];
5201 }
5202 }));
5203 }
5204 };
5205
5206 var ensureTargetCSSInitialised = function(target) {
5207 if (target.style._webAnimationsStyleInitialised) {
5208 return;
5209 }
5210 try {
5211 var animatedStyle = new AnimatedCSSStyleDeclaration(target);
5212 Object.defineProperty(target, 'style', configureDescriptor({
5213 get: function() { return animatedStyle; }
5214 }));
5215 } catch (error) {
5216 patchInlineStyleForAnimation(target.style);
5217 }
5218 target.style._webAnimationsStyleInitialised = true;
5219 };
5220
5221 var setValue = function(target, property, value) {
5222 ensureTargetInitialised(property, target);
5223 property = prefixProperty(property);
5224 if (propertyIsSVGAttrib(property, target)) {
5225 target.actuals[property] = value;
5226 } else {
5227 target.style._setAnimatedProperty(property, value);
5228 }
5229 };
5230
5231 var clearValue = function(target, property) {
5232 ensureTargetInitialised(property, target);
5233 property = prefixProperty(property);
5234 if (propertyIsSVGAttrib(property, target)) {
5235 target.actuals[property] = null;
5236 } else {
5237 target.style._clearAnimatedProperty(property);
5238 }
5239 };
5240
5241 var getValue = function(target, property) {
5242 ensureTargetInitialised(property, target);
5243 property = prefixProperty(property);
5244 if (propertyIsSVGAttrib(property, target)) {
5245 return target.actuals[property];
5246 } else {
5247 return getComputedStyle(target)[property];
5248 }
5249 };
5250
5251 var rafScheduled = false;
5252
5253 var compositor = new Compositor();
5254
5255 var usePerformanceTiming =
5256 typeof window.performance === 'object' &&
5257 typeof window.performance.timing === 'object' &&
5258 typeof window.performance.now === 'function';
5259
5260 // Don't use a local named requestAnimationFrame, to avoid potential problems
5261 // with hoisting.
5262 var nativeRaf = window.requestAnimationFrame ||
5263 window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
5264 var raf;
5265 if (nativeRaf) {
5266 raf = function(callback) {
5267 nativeRaf(function() {
5268 callback(clockMillis());
5269 });
5270 };
5271 } else {
5272 raf = function(callback) {
5273 setTimeout(function() {
5274 callback(clockMillis());
5275 }, 1000 / 60);
5276 };
5277 }
5278
5279 var clockMillis = function() {
5280 return usePerformanceTiming ? window.performance.now() : Date.now();
5281 };
5282 // Set up the zero times for document time. Document time is relative to the
5283 // document load event.
5284 var documentTimeZeroAsRafTime;
5285 var documentTimeZeroAsClockTime;
5286 var load;
5287 if (usePerformanceTiming) {
5288 load = function() {
5289 // RAF time is relative to the navigationStart event.
5290 documentTimeZeroAsRafTime =
5291 window.performance.timing.loadEventStart -
5292 window.performance.timing.navigationStart;
5293 // performance.now() uses the same origin as RAF time.
5294 documentTimeZeroAsClockTime = documentTimeZeroAsRafTime;
5295 };
5296 } else {
5297 // The best approximation we have for the relevant clock and RAF times is to
5298 // listen to the load event.
5299 load = function() {
5300 raf(function(rafTime) {
5301 documentTimeZeroAsRafTime = rafTime;
5302 });
5303 documentTimeZeroAsClockTime = Date.now();
5304 };
5305 }
5306 // Start timing when load event fires or if this script is processed when
5307 // document loading is already complete.
5308 if (document.readyState === 'complete') {
5309 // When performance timing is unavailable and this script is loaded
5310 // dynamically, document zero time is incorrect.
5311 // Warn the user in this case.
5312 if (!usePerformanceTiming) {
5313 console.warn(
5314 'Web animations can\'t discover document zero time when ' +
5315 'asynchronously loaded in the absence of performance timing.');
5316 }
5317 load();
5318 } else {
5319 addEventListener('load', function() {
5320 load();
5321 if (usePerformanceTiming) {
5322 // We use setTimeout() to clear cachedClockTimeMillis at the end of a
5323 // frame, but this will not run until after other load handlers. We need
5324 // those handlers to pick up the new value of clockMillis(), so we must
5325 // clear the cached value.
5326 cachedClockTimeMillis = undefined;
5327 }
5328 });
5329 }
5330
5331 // A cached document time for use during the current callstack.
5332 var cachedClockTimeMillis;
5333 // Calculates one time relative to another, returning null if the zero time is
5334 // undefined.
5335 var relativeTime = function(time, zeroTime) {
5336 return isDefined(zeroTime) ? time - zeroTime : null;
5337 };
5338
5339 var lastClockTimeMillis;
5340
5341 var cachedClockTime = function() {
5342 // Cache a document time for the remainder of this callstack.
5343 if (!isDefined(cachedClockTimeMillis)) {
5344 cachedClockTimeMillis = clockMillis();
5345 lastClockTimeMillis = cachedClockTimeMillis;
5346 setTimeout(function() { cachedClockTimeMillis = undefined; }, 0);
5347 }
5348 return cachedClockTimeMillis;
5349 };
5350
5351
5352 // These functions should be called in every stack that could possibly modify
5353 // the effect results that have already been calculated for the current tick.
5354 var modifyCurrentAnimationStateDepth = 0;
5355 var enterModifyCurrentAnimationState = function() {
5356 modifyCurrentAnimationStateDepth++;
5357 };
5358 var exitModifyCurrentAnimationState = function(updateCallback) {
5359 modifyCurrentAnimationStateDepth--;
5360 // updateCallback is set to null when we know we can't possibly affect the
5361 // current state (eg. a TimedItem which is not attached to a player). We track
5362 // the depth of recursive calls trigger just one repeat per entry. Only the
5363 // updateCallback from the outermost call is considered, this allows certain
5364 // locatations (eg. constructors) to override nested calls that would
5365 // otherwise set updateCallback unconditionally.
5366 if (modifyCurrentAnimationStateDepth === 0 && updateCallback) {
5367 updateCallback();
5368 }
5369 };
5370
5371 var repeatLastTick = function() {
5372 if (isDefined(lastTickTime)) {
5373 ticker(lastTickTime, true);
5374 }
5375 };
5376
5377 var playerSortFunction = function(a, b) {
5378 var result = a.startTime - b.startTime;
5379 return result !== 0 ? result : a._sequenceNumber - b._sequenceNumber;
5380 };
5381
5382 var lastTickTime;
5383 var ticker = function(rafTime, isRepeat) {
5384 // Don't tick till the page is loaded....
5385 if (!isDefined(documentTimeZeroAsRafTime)) {
5386 raf(ticker);
5387 return;
5388 }
5389
5390 if (!isRepeat) {
5391 if (rafTime < lastClockTimeMillis) {
5392 rafTime = lastClockTimeMillis;
5393 }
5394 lastTickTime = rafTime;
5395 cachedClockTimeMillis = rafTime;
5396 }
5397
5398 // Clear any modifications to getComputedStyle.
5399 ensureOriginalGetComputedStyle();
5400
5401 // Get animations for this sample. We order by AnimationPlayer then by DFS
5402 // order within each AnimationPlayer's tree.
5403 if (!playersAreSorted) {
5404 PLAYERS.sort(playerSortFunction);
5405 playersAreSorted = true;
5406 }
5407 var finished = true;
5408 var paused = true;
5409 var animations = [];
5410 var finishedPlayers = [];
5411 PLAYERS.forEach(function(player) {
5412 player._update();
5413 finished = finished && !player._hasFutureAnimation();
5414 if (!player._hasFutureEffect()) {
5415 finishedPlayers.push(player);
5416 }
5417 paused = paused && player.paused;
5418 player._getLeafItemsInEffect(animations);
5419 });
5420
5421 // Apply animations in order
5422 for (var i = 0; i < animations.length; i++) {
5423 if (animations[i] instanceof Animation) {
5424 animations[i]._sample();
5425 }
5426 }
5427
5428 // Generate events
5429 PLAYERS.forEach(function(player) {
5430 player._generateEvents();
5431 });
5432
5433 // Remove finished players. Warning: _deregisterFromTimeline modifies
5434 // the PLAYER list. It should not be called from within a PLAYERS.forEach
5435 // loop directly.
5436 finishedPlayers.forEach(function(player) {
5437 player._deregisterFromTimeline();
5438 playersAreSorted = false;
5439 });
5440
5441 // Composite animated values into element styles
5442 compositor.applyAnimatedValues();
5443
5444 if (!isRepeat) {
5445 if (finished || paused) {
5446 rafScheduled = false;
5447 } else {
5448 raf(ticker);
5449 }
5450 cachedClockTimeMillis = undefined;
5451 }
5452 };
5453
5454 // Multiplication where zero multiplied by any value (including infinity)
5455 // gives zero.
5456 var multiplyZeroGivesZero = function(a, b) {
5457 return (a === 0 || b === 0) ? 0 : a * b;
5458 };
5459
5460 var maybeRestartAnimation = function() {
5461 if (rafScheduled) {
5462 return;
5463 }
5464 raf(ticker);
5465 rafScheduled = true;
5466 };
5467
5468 var DOCUMENT_TIMELINE = new AnimationTimeline(constructorToken);
5469 // attempt to override native implementation
5470 try {
5471 Object.defineProperty(document, 'timeline', {
5472 configurable: true,
5473 get: function() { return DOCUMENT_TIMELINE }
5474 });
5475 } catch (e) { }
5476 // maintain support for Safari
5477 try {
5478 document.timeline = DOCUMENT_TIMELINE;
5479 } catch (e) { }
5480
5481 window.Element.prototype.animate = function(effect, timing) {
5482 var anim = new Animation(this, effect, timing);
5483 DOCUMENT_TIMELINE.play(anim);
5484 return anim.player;
5485 };
5486 window.Element.prototype.getCurrentPlayers = function() {
5487 return PLAYERS.filter((function(player) {
5488 return player._isCurrent() && player._isTargetingElement(this);
5489 }).bind(this));
5490 };
5491 window.Element.prototype.getCurrentAnimations = function() {
5492 var animations = [];
5493 PLAYERS.forEach((function(player) {
5494 if (player._isCurrent()) {
5495 player._getAnimationsTargetingElement(this, animations);
5496 }
5497 }).bind(this));
5498 return animations;
5499 };
5500
5501 window.Animation = Animation;
5502 window.AnimationEffect = AnimationEffect;
5503 window.AnimationGroup = AnimationGroup;
5504 window.AnimationPlayer = AnimationPlayer;
5505 window.AnimationSequence = AnimationSequence;
5506 window.AnimationTimeline = AnimationTimeline;
5507 window.KeyframeEffect = KeyframeEffect;
5508 window.MediaReference = MediaReference;
5509 window.MotionPathEffect = MotionPathEffect;
5510 window.PseudoElementReference = PseudoElementReference;
5511 window.TimedItem = TimedItem;
5512 window.TimedItemList = TimedItemList;
5513 window.Timing = Timing;
5514 window.TimingEvent = TimingEvent;
5515 window.TimingGroup = TimingGroup;
5516
5517 window._WebAnimationsTestingUtilities = {
5518 _constructorToken: constructorToken,
5519 _deprecated: deprecated,
5520 _positionListType: positionListType,
5521 _hsl2rgb: hsl2rgb,
5522 _types: propertyTypes,
5523 _knownPlayers: PLAYERS,
5524 _pacedTimingFunction: PacedTimingFunction,
5525 _prefixProperty: prefixProperty,
5526 _propertyIsSVGAttrib: propertyIsSVGAttrib
5527 };
5528
5529 })();
OLDNEW
« no previous file with comments | « bower_components/web-animations-js/tutorial/tutorial-testing.js ('k') | polymer_0.4.0/bower.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698