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