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

Side by Side Diff: pkg/polymer/lib/elements/web-animations-js/web-animations.js

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

Powered by Google App Engine
This is Rietveld 408576698