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