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