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

Side by Side Diff: tools/perf/page_sets/key_silk_cases/ink-button_files/web-animations.js

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

Powered by Google App Engine
This is Rietveld 408576698