/** * Copyright 2012 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ (function() { 'use strict'; var ASSERT_ENABLED = false; var SVG_NS = 'http://www.w3.org/2000/svg'; function assert(check, message) { console.assert(ASSERT_ENABLED, 'assert should not be called when ASSERT_ENABLED is false'); console.assert(check, message); // Some implementations of console.assert don't actually throw if (!check) { throw message; } } function detectFeatures() { var el = createDummyElement(); el.style.cssText = 'width: calc(0px);' + 'width: -webkit-calc(0px);'; var calcFunction = el.style.width.split('(')[0]; function detectProperty(candidateProperties) { return [].filter.call(candidateProperties, function(property) { return property in el.style; })[0]; } var transformProperty = detectProperty([ 'transform', 'webkitTransform', 'msTransform']); var perspectiveProperty = detectProperty([ 'perspective', 'webkitPerspective', 'msPerspective']); return { calcFunction: calcFunction, transformProperty: transformProperty, transformOriginProperty: transformProperty + 'Origin', perspectiveProperty: perspectiveProperty, perspectiveOriginProperty: perspectiveProperty + 'Origin' }; } var features = detectFeatures(); function prefixProperty(property) { switch (property) { case 'transform': return features.transformProperty; case 'transformOrigin': return features.transformOriginProperty; case 'perspective': return features.perspectiveProperty; case 'perspectiveOrigin': return features.perspectiveOriginProperty; default: return property; } } function createDummyElement() { return document.documentElement.namespaceURI == SVG_NS ? document.createElementNS(SVG_NS, 'g') : document.createElement('div'); } var constructorToken = {}; var deprecationsSilenced = {}; var createObject = function(proto, obj) { var newObject = Object.create(proto); Object.getOwnPropertyNames(obj).forEach(function(name) { Object.defineProperty( newObject, name, Object.getOwnPropertyDescriptor(obj, name)); }); return newObject; }; var abstractMethod = function() { throw 'Abstract method not implemented.'; }; var deprecated = function(name, deprecationDate, advice, plural) { if (deprecationsSilenced[name]) { return; } var auxVerb = plural ? 'are' : 'is'; var today = new Date(); var cutoffDate = new Date(deprecationDate); cutoffDate.setMonth(cutoffDate.getMonth() + 3); // 3 months grace period if (today < cutoffDate) { console.warn('Web Animations: ' + name + ' ' + auxVerb + ' deprecated and will stop working on ' + cutoffDate.toDateString() + '. ' + advice); deprecationsSilenced[name] = true; } else { throw new Error(name + ' ' + auxVerb + ' no longer supported. ' + advice); } }; var defineDeprecatedProperty = function(object, property, getFunc, setFunc) { var descriptor = { get: getFunc, configurable: true }; if (setFunc) { descriptor.set = setFunc; } Object.defineProperty(object, property, descriptor); }; var IndexSizeError = function(message) { Error.call(this); this.name = 'IndexSizeError'; this.message = message; }; IndexSizeError.prototype = Object.create(Error.prototype); /** @constructor */ var TimingDict = function(timingInput) { if (typeof timingInput === 'object') { for (var k in timingInput) { if (k in TimingDict.prototype) { this[k] = timingInput[k]; } } } else if (isDefinedAndNotNull(timingInput)) { this.duration = Number(timingInput); } }; TimingDict.prototype = { delay: 0, endDelay: 0, fill: 'auto', iterationStart: 0, iterations: 1, duration: 'auto', playbackRate: 1, direction: 'normal', easing: 'linear' }; /** @constructor */ var Timing = function(token, timingInput, changeHandler) { if (token !== constructorToken) { throw new TypeError('Illegal constructor'); } this._dict = new TimingDict(timingInput); this._changeHandler = changeHandler; }; Timing.prototype = { _timingFunction: function(timedItem) { var timingFunction = TimingFunction.createFromString( this.easing, timedItem); this._timingFunction = function() { return timingFunction; }; return timingFunction; }, _invalidateTimingFunction: function() { delete this._timingFunction; }, _iterations: function() { var value = this._dict.iterations; return value < 0 ? 1 : value; }, _duration: function() { var value = this._dict.duration; return typeof value === 'number' ? value : 'auto'; }, _clone: function() { return new Timing( constructorToken, this._dict, this._updateInternalState.bind(this)); } }; // Configures an accessor descriptor for use with Object.defineProperty() to // allow the property to be changed and enumerated, to match __defineGetter__() // and __defineSetter__(). var configureDescriptor = function(descriptor) { descriptor.configurable = true; descriptor.enumerable = true; return descriptor; }; Timing._defineProperty = function(prop) { Object.defineProperty(Timing.prototype, prop, configureDescriptor({ get: function() { return this._dict[prop]; }, set: function(value) { if (isDefinedAndNotNull(value)) { if (prop == 'duration' && value == 'auto') { // duration is not always a number } else if (['delay', 'endDelay', 'iterationStart', 'iterations', 'duration', 'playbackRate'].indexOf(prop) >= 0) { value = Number(value); } this._dict[prop] = value; } else { delete this._dict[prop]; } // FIXME: probably need to implement specialized handling parsing // for each property if (prop === 'easing') { // Cached timing function may be invalid now. this._invalidateTimingFunction(); } this._changeHandler(); } })); }; for (var prop in TimingDict.prototype) { Timing._defineProperty(prop); } var isDefined = function(val) { return typeof val !== 'undefined'; }; var isDefinedAndNotNull = function(val) { return isDefined(val) && (val !== null); }; /** @constructor */ var AnimationTimeline = function(token) { if (token !== constructorToken) { throw new TypeError('Illegal constructor'); } // TODO: This will probably need to change. this._startTime = documentTimeZeroAsClockTime; }; AnimationTimeline.prototype = { get currentTime() { if (this._startTime === undefined) { this._startTime = documentTimeZeroAsClockTime; if (this._startTime === undefined) { return null; } } return relativeTime(cachedClockTime(), this._startTime); }, get effectiveCurrentTime() { return this.currentTime || 0; }, play: function(source) { return new AnimationPlayer(constructorToken, source, this); }, getCurrentPlayers: function() { return PLAYERS.filter(function(player) { return !player._isPastEndOfActiveInterval(); }); }, toTimelineTime: function(otherTime, other) { if ((this.currentTime === null) || (other.currentTime === null)) { return null; } else { return otherTime + other._startTime - this._startTime; } }, _pauseAnimationsForTesting: function(pauseAt) { PLAYERS.forEach(function(player) { player.pause(); player.currentTime = pauseAt; }); } }; // TODO: Remove dead players from here? var PLAYERS = []; var playersAreSorted = false; var playerSequenceNumber = 0; // Methods for event target objects. var initializeEventTarget = function(eventTarget) { eventTarget._handlers = {}; eventTarget._onHandlers = {}; }; var setOnEventHandler = function(eventTarget, type, handler) { if (typeof handler === 'function') { eventTarget._onHandlers[type] = { callback: handler, index: (eventTarget._handlers[type] || []).length }; } else { eventTarget._onHandlers[type] = null; } }; var getOnEventHandler = function(eventTarget, type) { if (isDefinedAndNotNull(eventTarget._onHandlers[type])) { return eventTarget._onHandlers[type].callback; } return null; }; var addEventHandler = function(eventTarget, type, handler) { if (typeof handler !== 'function') { return; } if (!isDefinedAndNotNull(eventTarget._handlers[type])) { eventTarget._handlers[type] = []; } else if (eventTarget._handlers[type].indexOf(handler) !== -1) { return; } eventTarget._handlers[type].push(handler); }; var removeEventHandler = function(eventTarget, type, handler) { if (!eventTarget._handlers[type]) { return; } var index = eventTarget._handlers[type].indexOf(handler); if (index === -1) { return; } eventTarget._handlers[type].splice(index, 1); if (isDefinedAndNotNull(eventTarget._onHandlers[type]) && (index < eventTarget._onHandlers[type].index)) { eventTarget._onHandlers[type].index -= 1; } }; var hasEventHandlersForEvent = function(eventTarget, type) { return (isDefinedAndNotNull(eventTarget._handlers[type]) && eventTarget._handlers[type].length > 0) || isDefinedAndNotNull(eventTarget._onHandlers[type]); }; var callEventHandlers = function(eventTarget, type, event) { var callbackList; if (isDefinedAndNotNull(eventTarget._handlers[type])) { callbackList = eventTarget._handlers[type].slice(); } else { callbackList = []; } if (isDefinedAndNotNull(eventTarget._onHandlers[type])) { callbackList.splice(eventTarget._onHandlers[type].index, 0, eventTarget._onHandlers[type].callback); } setTimeout(function() { for (var i = 0; i < callbackList.length; i++) { callbackList[i].call(eventTarget, event); } }, 0); }; var createEventPrototype = function() { var prototype = Object.create(window.Event.prototype, { type: { get: function() { return this._type; } }, target: { get: function() { return this._target; } }, currentTarget: { get: function() { return this._target; } }, eventPhase: { get: function() { return this._eventPhase; } }, bubbles: { get: function() { return false; } }, cancelable: { get: function() { return false; } }, timeStamp: { get: function() { return this._timeStamp; } }, defaultPrevented: { get: function() { return false; } } }); prototype._type = ''; prototype._target = null; prototype._eventPhase = Event.NONE; prototype._timeStamp = 0; prototype._initialize = function(target) { this._target = target; this._eventPhase = Event.AT_TARGET; this._timeStamp = cachedClockTime(); }; return prototype; }; /** @constructor */ var AnimationPlayer = function(token, source, timeline) { if (token !== constructorToken) { throw new TypeError('Illegal constructor'); } enterModifyCurrentAnimationState(); try { this._registeredOnTimeline = false; this._sequenceNumber = playerSequenceNumber++; this._timeline = timeline; this._startTime = this.timeline.currentTime === null ? 0 : this.timeline.currentTime; this._storedTimeLag = 0.0; this._pausedState = false; this._holdTime = null; this._previousCurrentTime = null; this._playbackRate = 1.0; this._hasTicked = false; this.source = source; this._checkForLegacyHandlers(); this._lastCurrentTime = undefined; this._finishedFlag = false; initializeEventTarget(this); playersAreSorted = false; maybeRestartAnimation(); } finally { exitModifyCurrentAnimationState(ensureRetickBeforeGetComputedStyle); } }; AnimationPlayer.prototype = { set source(source) { enterModifyCurrentAnimationState(); try { if (isDefinedAndNotNull(this.source)) { // To prevent infinite recursion. var oldTimedItem = this.source; this._source = null; oldTimedItem._attach(null); } this._source = source; if (isDefinedAndNotNull(this.source)) { this.source._attach(this); this._update(); maybeRestartAnimation(); } this._checkForLegacyHandlers(); } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, get source() { return this._source; }, // This is the effective current time. set currentTime(currentTime) { enterModifyCurrentAnimationState(); try { this._currentTime = currentTime; } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, get currentTime() { return this._currentTime; }, set _currentTime(seekTime) { // If we are paused or seeking to a time where limiting applies (i.e. beyond // the end in the current direction), update the hold time. var sourceContentEnd = this.source ? this.source.endTime : 0; if (this.paused || (this.playbackRate > 0 && seekTime >= sourceContentEnd) || (this.playbackRate < 0 && seekTime <= 0)) { this._holdTime = seekTime; // Otherwise, clear the hold time (it may been set by previously seeking to // a limited time) and update the time lag. } else { this._holdTime = null; this._storedTimeLag = (this.timeline.effectiveCurrentTime - this.startTime) * this.playbackRate - seekTime; } this._update(); maybeRestartAnimation(); }, get _currentTime() { this._previousCurrentTime = (this.timeline.effectiveCurrentTime - this.startTime) * this.playbackRate - this.timeLag; return this._previousCurrentTime; }, get _unlimitedCurrentTime() { return (this.timeline.effectiveCurrentTime - this.startTime) * this.playbackRate - this._storedTimeLag; }, get timeLag() { if (this.paused) { return this._pauseTimeLag; } // Apply limiting at start of interval when playing in reverse if (this.playbackRate < 0 && this._unlimitedCurrentTime <= 0) { if (this._holdTime === null) { this._holdTime = Math.min(this._previousCurrentTime, 0); } return this._pauseTimeLag; } // Apply limiting at end of interval when playing forwards var sourceContentEnd = this.source ? this.source.endTime : 0; if (this.playbackRate > 0 && this._unlimitedCurrentTime >= sourceContentEnd) { if (this._holdTime === null) { this._holdTime = Math.max(this._previousCurrentTime, sourceContentEnd); } return this._pauseTimeLag; } // Finished limiting so store pause time lag if (this._holdTime !== null) { this._storedTimeLag = this._pauseTimeLag; this._holdTime = null; } return this._storedTimeLag; }, get _pauseTimeLag() { return ((this.timeline.currentTime || 0) - this.startTime) * this.playbackRate - this._holdTime; }, set startTime(startTime) { enterModifyCurrentAnimationState(); try { // This seeks by updating _startTime and hence the currentTime. It does // not affect _storedTimeLag. this._startTime = startTime; this._holdTime = null; playersAreSorted = false; this._update(); maybeRestartAnimation(); } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, get startTime() { return this._startTime; }, set _paused(isPaused) { if (isPaused === this._pausedState) { return; } if (this._pausedState) { this._storedTimeLag = this.timeLag; this._holdTime = null; maybeRestartAnimation(); } else { this._holdTime = this.currentTime; } this._pausedState = isPaused; }, get paused() { return this._pausedState; }, get timeline() { return this._timeline; }, set playbackRate(playbackRate) { enterModifyCurrentAnimationState(); try { var cachedCurrentTime = this.currentTime; // This will impact currentTime, so perform a compensatory seek. this._playbackRate = playbackRate; this.currentTime = cachedCurrentTime; } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, get playbackRate() { return this._playbackRate; }, get finished() { return this._isLimited; }, get _isLimited() { var sourceEnd = this.source ? this.source.endTime : 0; return ((this.playbackRate > 0 && this.currentTime >= sourceEnd) || (this.playbackRate < 0 && this.currentTime <= 0)); }, cancel: function() { this.source = null; }, finish: function() { if (this.playbackRate < 0) { this.currentTime = 0; } else if (this.playbackRate > 0) { var sourceEndTime = this.source ? this.source.endTime : 0; if (sourceEndTime === Infinity) { throw new Error('InvalidStateError'); } this.currentTime = sourceEndTime; } }, play: function() { this._paused = false; if (!this.source) { return; } if (this.playbackRate > 0 && (this.currentTime < 0 || this.currentTime >= this.source.endTime)) { this.currentTime = 0; } else if (this.playbackRate < 0 && (this.currentTime <= 0 || this.currentTime > this.source.endTime)) { this.currentTime = this.source.endTime; } }, pause: function() { this._paused = true; }, reverse: function() { if (this.playbackRate === 0) { return; } if (this.source) { if (this.playbackRate > 0 && this.currentTime >= this.source.endTime) { this.currentTime = this.source.endTime; } else if (this.playbackRate < 0 && this.currentTime < 0) { this.currentTime = 0; } } this.playbackRate = -this.playbackRate; this._paused = false; }, _update: function() { if (this.source !== null) { this.source._updateInheritedTime( this.timeline.currentTime === null ? null : this._currentTime); this._registerOnTimeline(); } }, _hasFutureAnimation: function() { return this.source === null || this.playbackRate === 0 || this.source._hasFutureAnimation(this.playbackRate > 0); }, _isPastEndOfActiveInterval: function() { return this.source === null || this.source._isPastEndOfActiveInterval(); }, _isCurrent: function() { return this.source && this.source._isCurrent(); }, _hasFutureEffect: function() { return this.source && this.source._hasFutureEffect(); }, _getLeafItemsInEffect: function(items) { if (this.source) { this.source._getLeafItemsInEffect(items); } }, _isTargetingElement: function(element) { return this.source && this.source._isTargetingElement(element); }, _getAnimationsTargetingElement: function(element, animations) { if (this.source) { this.source._getAnimationsTargetingElement(element, animations); } }, set onfinish(handler) { return setOnEventHandler(this, 'finish', handler); }, get onfinish() { return getOnEventHandler(this, 'finish'); }, addEventListener: function(type, handler) { if (type === 'finish') { addEventHandler(this, type, handler); } }, removeEventListener: function(type, handler) { if (type === 'finish') { removeEventHandler(this, type, handler); } }, _generateEvents: function() { if (!this._finishedFlag && this.finished && hasEventHandlersForEvent(this, 'finish')) { var event = new AnimationPlayerEvent('finish', { currentTime: this.currentTime, timelineTime: this.timeline.currentTime }); event._initialize(this); callEventHandlers(this, 'finish', event); } this._finishedFlag = this.finished; // The following code is for deprecated TimedItem event handling and should // be removed once we stop supporting it. if (!isDefinedAndNotNull(this._lastCurrentTime)) { this._lastCurrentTime = 0; } if (this._needsLegacyHandlerPass) { var timeDelta = this._unlimitedCurrentTime - this._lastCurrentTime; if (timeDelta > 0) { this.source._generateLegacyEvents( this._lastCurrentTime, this._unlimitedCurrentTime, this.timeline.currentTime, 1); } } this._lastCurrentTime = this._unlimitedCurrentTime; }, // These two legacy methods are for deprecated TimedItem event handling and // should be removed once we stop supporting it. _legacyHandlerAdded: function() { this._needsLegacyHandlerPass = true; }, _checkForLegacyHandlers: function() { this._needsLegacyHandlerPass = this.source !== null && this.source._hasLegacyEventHandlers(); }, _registerOnTimeline: function() { if (!this._registeredOnTimeline) { PLAYERS.push(this); this._registeredOnTimeline = true; } }, _deregisterFromTimeline: function() { PLAYERS.splice(PLAYERS.indexOf(this), 1); this._registeredOnTimeline = false; } }; /** @constructor */ var AnimationPlayerEvent = function(type, eventInit) { this._type = type; this.currentTime = eventInit.currentTime; this.timelineTime = eventInit.timelineTime; }; AnimationPlayerEvent.prototype = createEventPrototype(); /** @constructor */ var TimedItem = function(token, timingInput) { if (token !== constructorToken) { throw new TypeError('Illegal constructor'); } this.timing = new Timing( constructorToken, timingInput, this._specifiedTimingModified.bind(this)); this._inheritedTime = null; this.currentIteration = null; this._iterationTime = null; this._animationTime = null; this._startTime = 0.0; this._player = null; this._parent = null; this._updateInternalState(); this._fill = this._resolveFillMode(this.timing.fill); initializeEventTarget(this); }; TimedItem.prototype = { // TODO: It would be good to avoid the need for this. We would need to modify // call sites to instead rely on a call from the parent. get _effectiveParentTime() { return this.parent !== null && this.parent._iterationTime !== null ? this.parent._iterationTime : 0; }, get localTime() { return this._inheritedTime === null ? null : this._inheritedTime - this._startTime; }, get startTime() { return this._startTime; }, get duration() { var result = this.timing._duration(); if (result === 'auto') { result = this._intrinsicDuration(); } return result; }, get activeDuration() { var repeatedDuration = this.duration * this.timing._iterations(); return repeatedDuration / Math.abs(this.timing.playbackRate); }, get endTime() { return this._startTime + this.activeDuration + this.timing.delay + this.timing.endDelay; }, get parent() { return this._parent; }, get previousSibling() { if (!this.parent) { return null; } var siblingIndex = this.parent.indexOf(this) - 1; if (siblingIndex < 0) { return null; } return this.parent.children[siblingIndex]; }, get nextSibling() { if (!this.parent) { return null; } var siblingIndex = this.parent.indexOf(this) + 1; if (siblingIndex >= this.parent.children.length) { return null; } return this.parent.children[siblingIndex]; }, _attach: function(player) { // Remove ourselves from our parent, if we have one. This also removes any // exsisting player. this._reparent(null); this._player = player; }, // Takes care of updating the outgoing parent. This is called with a non-null // parent only from TimingGroup.splice(), which takes care of calling // TimingGroup._childrenStateModified() for the new parent. _reparent: function(parent) { if (parent === this) { throw new Error('parent can not be set to self!'); } enterModifyCurrentAnimationState(); try { if (this._player !== null) { this._player.source = null; this._player = null; } if (this.parent !== null) { this.remove(); } this._parent = parent; // In the case of a AnimationSequence parent, _startTime will be updated // by TimingGroup.splice(). if (this.parent === null || this.parent.type !== 'seq') { this._startTime = this._stashedStartTime === undefined ? 0.0 : this._stashedStartTime; this._stashedStartTime = undefined; } // In the case of the parent being non-null, _childrenStateModified() will // call this via _updateChildInheritedTimes(). // TODO: Consider optimising this case by skipping this call. this._updateTimeMarkers(); } finally { exitModifyCurrentAnimationState( Boolean(this.player) ? repeatLastTick : null); } }, _intrinsicDuration: function() { return 0.0; }, _resolveFillMode: abstractMethod, _updateInternalState: function() { this._fill = this._resolveFillMode(this.timing.fill); if (this.parent) { this.parent._childrenStateModified(); } else if (this._player) { this._player._registerOnTimeline(); } this._updateTimeMarkers(); }, _specifiedTimingModified: function() { enterModifyCurrentAnimationState(); try { this._updateInternalState(); } finally { exitModifyCurrentAnimationState( Boolean(this.player) ? repeatLastTick : null); } }, // We push time down to children. We could instead have children pull from // above, but this is tricky because a TimedItem may use either a parent // TimedItem or an AnimationPlayer. This requires either logic in // TimedItem, or for TimedItem and AnimationPlayer to implement Timeline // (or an equivalent), both of which are ugly. _updateInheritedTime: function(inheritedTime) { this._inheritedTime = inheritedTime; this._updateTimeMarkers(); }, _updateAnimationTime: function() { if (this.localTime < this.timing.delay) { if (this._fill === 'backwards' || this._fill === 'both') { this._animationTime = 0; } else { this._animationTime = null; } } else if (this.localTime < this.timing.delay + this.activeDuration) { this._animationTime = this.localTime - this.timing.delay; } else { if (this._fill === 'forwards' || this._fill === 'both') { this._animationTime = this.activeDuration; } else { this._animationTime = null; } } }, _updateIterationParamsZeroDuration: function() { this._iterationTime = 0; var isAtEndOfIterations = this.timing._iterations() !== 0 && this.localTime >= this.timing.delay; this.currentIteration = ( isAtEndOfIterations ? this._floorWithOpenClosedRange( this.timing.iterationStart + this.timing._iterations(), 1.0) : this._floorWithClosedOpenRange(this.timing.iterationStart, 1.0)); // Equivalent to unscaledIterationTime below. var unscaledFraction = ( isAtEndOfIterations ? this._modulusWithOpenClosedRange( this.timing.iterationStart + this.timing._iterations(), 1.0) : this._modulusWithClosedOpenRange(this.timing.iterationStart, 1.0)); var timingFunction = this.timing._timingFunction(this); this._timeFraction = ( this._isCurrentDirectionForwards() ? unscaledFraction : 1.0 - unscaledFraction); ASSERT_ENABLED && assert( this._timeFraction >= 0.0 && this._timeFraction <= 1.0, 'Time fraction should be in the range [0, 1]'); if (timingFunction) { this._timeFraction = timingFunction.scaleTime(this._timeFraction); } }, _getAdjustedAnimationTime: function(animationTime) { var startOffset = multiplyZeroGivesZero(this.timing.iterationStart, this.duration); return (this.timing.playbackRate < 0 ? (animationTime - this.activeDuration) : animationTime) * this.timing.playbackRate + startOffset; }, _scaleIterationTime: function(unscaledIterationTime) { return this._isCurrentDirectionForwards() ? unscaledIterationTime : this.duration - unscaledIterationTime; }, _updateIterationParams: function() { var adjustedAnimationTime = this._getAdjustedAnimationTime(this._animationTime); var repeatedDuration = this.duration * this.timing._iterations(); var startOffset = this.timing.iterationStart * this.duration; var isAtEndOfIterations = (this.timing._iterations() !== 0) && (adjustedAnimationTime - startOffset === repeatedDuration); this.currentIteration = isAtEndOfIterations ? this._floorWithOpenClosedRange( adjustedAnimationTime, this.duration) : this._floorWithClosedOpenRange( adjustedAnimationTime, this.duration); var unscaledIterationTime = isAtEndOfIterations ? this._modulusWithOpenClosedRange( adjustedAnimationTime, this.duration) : this._modulusWithClosedOpenRange( adjustedAnimationTime, this.duration); this._iterationTime = this._scaleIterationTime(unscaledIterationTime); if (this.duration == Infinity) { this._timeFraction = 0; return; } this._timeFraction = this._iterationTime / this.duration; ASSERT_ENABLED && assert( this._timeFraction >= 0.0 && this._timeFraction <= 1.0, 'Time fraction should be in the range [0, 1], got ' + this._timeFraction + ' ' + this._iterationTime + ' ' + this.duration + ' ' + isAtEndOfIterations + ' ' + unscaledIterationTime); var timingFunction = this.timing._timingFunction(this); if (timingFunction) { this._timeFraction = timingFunction.scaleTime(this._timeFraction); } this._iterationTime = this._timeFraction * this.duration; }, _updateTimeMarkers: function() { if (this.localTime === null) { this._animationTime = null; this._iterationTime = null; this.currentIteration = null; this._timeFraction = null; return false; } this._updateAnimationTime(); if (this._animationTime === null) { this._iterationTime = null; this.currentIteration = null; this._timeFraction = null; } else if (this.duration === 0) { this._updateIterationParamsZeroDuration(); } else { this._updateIterationParams(); } maybeRestartAnimation(); }, _floorWithClosedOpenRange: function(x, range) { return Math.floor(x / range); }, _floorWithOpenClosedRange: function(x, range) { return Math.ceil(x / range) - 1; }, _modulusWithClosedOpenRange: function(x, range) { ASSERT_ENABLED && assert( range > 0, 'Range must be strictly positive'); var modulus = x % range; var result = modulus < 0 ? modulus + range : modulus; ASSERT_ENABLED && assert( result >= 0.0 && result < range, 'Result should be in the range [0, range)'); return result; }, _modulusWithOpenClosedRange: function(x, range) { var modulus = this._modulusWithClosedOpenRange(x, range); var result = modulus === 0 ? range : modulus; ASSERT_ENABLED && assert( result > 0.0 && result <= range, 'Result should be in the range (0, range]'); return result; }, _isCurrentDirectionForwards: function() { if (this.timing.direction === 'normal') { return true; } if (this.timing.direction === 'reverse') { return false; } var d = this.currentIteration; if (this.timing.direction === 'alternate-reverse') { d += 1; } // TODO: 6.13.3 step 3. wtf? return d % 2 === 0; }, clone: abstractMethod, before: function() { var newItems = []; for (var i = 0; i < arguments.length; i++) { newItems.push(arguments[i]); } this.parent._splice(this.parent.indexOf(this), 0, newItems); }, after: function() { var newItems = []; for (var i = 0; i < arguments.length; i++) { newItems.push(arguments[i]); } this.parent._splice(this.parent.indexOf(this) + 1, 0, newItems); }, replace: function() { var newItems = []; for (var i = 0; i < arguments.length; i++) { newItems.push(arguments[i]); } this.parent._splice(this.parent.indexOf(this), 1, newItems); }, remove: function() { this.parent._splice(this.parent.indexOf(this), 1); }, // Gets the leaf TimedItems currently in effect. Note that this is a superset // of the leaf TimedItems in their active interval, as a TimedItem can have an // effect outside its active interval due to fill. _getLeafItemsInEffect: function(items) { if (this._timeFraction !== null) { this._getLeafItemsInEffectImpl(items); } }, _getLeafItemsInEffectImpl: abstractMethod, _hasFutureAnimation: function(timeDirectionForwards) { return timeDirectionForwards ? this._inheritedTime < this.endTime : this._inheritedTime > this.startTime; }, _isPastEndOfActiveInterval: function() { return this._inheritedTime >= this.endTime; }, get player() { return this.parent === null ? this._player : this.parent.player; }, _isCurrent: function() { return !this._isPastEndOfActiveInterval() || (this.parent !== null && this.parent._isCurrent()); }, _isTargetingElement: abstractMethod, _getAnimationsTargetingElement: abstractMethod, _netEffectivePlaybackRate: function() { var effectivePlaybackRate = this._isCurrentDirectionForwards() ? this.timing.playbackRate : -this.timing.playbackRate; return this.parent === null ? effectivePlaybackRate : effectivePlaybackRate * this.parent._netEffectivePlaybackRate(); }, // Note that this restriction is currently incomplete - for example, // Animations which are playing forwards and have a fill of backwards // are not in effect unless current. // TODO: Complete this restriction. _hasFutureEffect: function() { return this._isCurrent() || this._fill !== 'none'; }, _hasLegacyEventHandlers: function() { return hasEventHandlersForEvent(this, 'start') || hasEventHandlersForEvent(this, 'iteration') || hasEventHandlersForEvent(this, 'end') || hasEventHandlersForEvent(this, 'cancel'); }, _generateChildLegacyEventsForRange: function() { }, _toSubRanges: function(fromTime, toTime, iterationTimes) { if (fromTime > toTime) { var revRanges = this._toSubRanges(toTime, fromTime, iterationTimes); revRanges.ranges.forEach(function(a) { a.reverse(); }); revRanges.ranges.reverse(); revRanges.start = iterationTimes.length - revRanges.start - 1; revRanges.delta = -1; return revRanges; } var skipped = 0; // TODO: this should be calculatable. This would be more efficient // than searching through the list. while (iterationTimes[skipped] < fromTime) { skipped++; } var currentStart = fromTime; var ranges = []; for (var i = skipped; i < iterationTimes.length; i++) { if (iterationTimes[i] < toTime) { ranges.push([currentStart, iterationTimes[i]]); currentStart = iterationTimes[i]; } else { ranges.push([currentStart, toTime]); return {start: skipped, delta: 1, ranges: ranges}; } } ranges.push([currentStart, toTime]); return {start: skipped, delta: 1, ranges: ranges}; }, _generateLegacyEvents: function(fromTime, toTime, globalTime, deltaScale) { function toGlobal(time) { return (globalTime - (toTime - (time / deltaScale))); } var firstIteration = Math.floor(this.timing.iterationStart); var lastIteration = Math.floor(this.timing.iterationStart + this.timing.iterations); if (lastIteration === this.timing.iterationStart + this.timing.iterations) { lastIteration -= 1; } var startTime = this.startTime + this.timing.delay; if (hasEventHandlersForEvent(this, 'start')) { // Did we pass the start of this animation in the forward direction? if (fromTime <= startTime && toTime > startTime) { callEventHandlers(this, 'start', new TimingEvent( constructorToken, this, 'start', this.timing.delay, toGlobal(startTime), firstIteration)); // Did we pass the end of this animation in the reverse direction? } else if (fromTime > this.endTime && toTime <= this.endTime) { callEventHandlers(this, 'start', new TimingEvent( constructorToken, this, 'start', this.endTime - this.startTime, toGlobal(this.endTime), lastIteration)); } } // Calculate a list of uneased iteration times. var iterationTimes = []; for (var i = firstIteration + 1; i <= lastIteration; i++) { iterationTimes.push(i - this.timing.iterationStart); } iterationTimes = iterationTimes.map(function(i) { return i * this.duration / this.timing.playbackRate + startTime; }.bind(this)); // Determine the impacted subranges. var clippedFromTime; var clippedToTime; if (fromTime < toTime) { clippedFromTime = Math.max(fromTime, startTime); clippedToTime = Math.min(toTime, this.endTime); } else { clippedFromTime = Math.min(fromTime, this.endTime); clippedToTime = Math.max(toTime, startTime); } var subranges = this._toSubRanges( clippedFromTime, clippedToTime, iterationTimes); for (var i = 0; i < subranges.ranges.length; i++) { var currentIter = subranges.start + i * subranges.delta; if (i > 0 && hasEventHandlersForEvent(this, 'iteration')) { var iterTime = subranges.ranges[i][0]; callEventHandlers(this, 'iteration', new TimingEvent( constructorToken, this, 'iteration', iterTime - this.startTime, toGlobal(iterTime), currentIter)); } var iterFraction; if (subranges.delta > 0) { iterFraction = this.timing.iterationStart % 1; } else { iterFraction = 1 - (this.timing.iterationStart + this.timing.iterations) % 1; } this._generateChildLegacyEventsForRange( subranges.ranges[i][0], subranges.ranges[i][1], fromTime, toTime, currentIter - iterFraction, globalTime, deltaScale * this.timing.playbackRate); } if (hasEventHandlersForEvent(this, 'end')) { // Did we pass the end of this animation in the forward direction? if (fromTime < this.endTime && toTime >= this.endTime) { callEventHandlers(this, 'end', new TimingEvent( constructorToken, this, 'end', this.endTime - this.startTime, toGlobal(this.endTime), lastIteration)); // Did we pass the start of this animation in the reverse direction? } else if (fromTime >= startTime && toTime < startTime) { callEventHandlers(this, 'end', new TimingEvent( constructorToken, this, 'end', this.timing.delay, toGlobal(startTime), firstIteration)); } } } }; defineDeprecatedProperty(TimedItem.prototype, 'specified', function() { deprecated('specified', '2014-04-16', 'Please use timing instead.'); return this.timing; }); var deprecatedTimedItemEvents = function() { deprecated('TimedItem events', '2014-04-22', 'Please use the AnimationPlayer finish event instead.', true); }; ['start', 'iteration', 'end', 'cancel'].forEach(function(eventName) { defineDeprecatedProperty(TimedItem.prototype, 'on' + eventName, function() { deprecatedTimedItemEvents(); return getOnEventHandler(this, eventName); }, function(handler) { deprecatedTimedItemEvents(); setOnEventHandler(this, eventName, handler); if (this.player) { if (typeof func === 'function') { this.player._legacyHandlerAdded(); } else { this.player._checkForLegacyHandlers(); } } }); }); defineDeprecatedProperty(TimedItem.prototype, 'addEventListener', function() { deprecatedTimedItemEvents(); return function(type, handler) { if (type !== 'start' && type !== 'iteration' && type !== 'end' && type !== 'cancel') { return; } addEventHandler(this, type, handler); if (this.player) { this.player._legacyHandlerAdded(); } } }); defineDeprecatedProperty(TimedItem.prototype, 'removeEventListener', function() { deprecatedTimedItemEvents(); return function(type, handler) { removeEventHandler(this, type, handler); if (this.player) { this.player._checkForLegacyHandlers(); } } }); var TimingEvent = function( token, target, type, localTime, timelineTime, iterationIndex, seeked) { if (token !== constructorToken) { throw new TypeError('Illegal constructor'); } this._initialize(target); this._type = type; this.localTime = localTime; this.timelineTime = timelineTime; this.iterationIndex = iterationIndex; this.seeked = seeked ? true : false; }; TimingEvent.prototype = createEventPrototype(); var isEffectCallback = function(animationEffect) { return typeof animationEffect === 'function'; }; var interpretAnimationEffect = function(animationEffect) { if (animationEffect instanceof AnimationEffect || isEffectCallback(animationEffect)) { return animationEffect; } else if (isDefinedAndNotNull(animationEffect) && typeof animationEffect === 'object') { // The spec requires animationEffect to be an instance of // OneOrMoreKeyframes, but this type is just a dictionary or a list of // dictionaries, so the best we can do is test for an object. return new KeyframeEffect(animationEffect); } return null; }; var cloneAnimationEffect = function(animationEffect) { if (animationEffect instanceof AnimationEffect) { return animationEffect.clone(); } else if (isEffectCallback(animationEffect)) { return animationEffect; } else { return null; } }; /** @constructor */ var Animation = function(target, animationEffect, timingInput) { enterModifyCurrentAnimationState(); try { TimedItem.call(this, constructorToken, timingInput); this.effect = interpretAnimationEffect(animationEffect); this._target = target; } finally { exitModifyCurrentAnimationState(null); } }; Animation.prototype = createObject(TimedItem.prototype, { _resolveFillMode: function(fillMode) { return fillMode === 'auto' ? 'none' : fillMode; }, _sample: function() { if (isDefinedAndNotNull(this.effect) && !(this.target instanceof PseudoElementReference)) { if (isEffectCallback(this.effect)) { this.effect(this._timeFraction, this.target, this); } else { this.effect._sample(this._timeFraction, this.currentIteration, this.target, this.underlyingValue); } } }, _getLeafItemsInEffectImpl: function(items) { items.push(this); }, _isTargetingElement: function(element) { return element === this.target; }, _getAnimationsTargetingElement: function(element, animations) { if (this._isTargetingElement(element)) { animations.push(this); } }, get target() { return this._target; }, set effect(effect) { enterModifyCurrentAnimationState(); try { this._effect = effect; this.timing._invalidateTimingFunction(); } finally { exitModifyCurrentAnimationState( Boolean(this.player) ? repeatLastTick : null); } }, get effect() { return this._effect; }, clone: function() { return new Animation(this.target, cloneAnimationEffect(this.effect), this.timing._dict); }, toString: function() { var effectString = ''; if (this.effect instanceof AnimationEffect) { effectString = this.effect.toString(); } else if (isEffectCallback(this.effect)) { effectString = 'Effect callback'; } return 'Animation ' + this.startTime + '-' + this.endTime + ' (' + this.localTime + ') ' + effectString; } }); function throwNewHierarchyRequestError() { var element = document.createElement('span'); element.appendChild(element); } /** @constructor */ var TimedItemList = function(token, children) { if (token !== constructorToken) { throw new TypeError('Illegal constructor'); } this._children = children; this._getters = 0; this._ensureGetters(); }; TimedItemList.prototype = { get length() { return this._children.length; }, _ensureGetters: function() { while (this._getters < this._children.length) { this._ensureGetter(this._getters++); } }, _ensureGetter: function(i) { Object.defineProperty(this, i, { get: function() { return this._children[i]; } }); } }; /** @constructor */ var TimingGroup = function(token, type, children, timing) { if (token !== constructorToken) { throw new TypeError('Illegal constructor'); } // Take a copy of the children array, as it could be modified as a side-effect // of creating this object. See // https://github.com/web-animations/web-animations-js/issues/65 for details. var childrenCopy = (children && Array.isArray(children)) ? children.slice() : []; // used by TimedItem via _intrinsicDuration(), so needs to be set before // initializing super. this.type = type || 'par'; this._children = []; this._cachedTimedItemList = null; this._cachedIntrinsicDuration = null; TimedItem.call(this, constructorToken, timing); // We add children after setting the parent. This means that if an ancestor // (including the parent) is specified as a child, it will be removed from our // ancestors and used as a child, this.append.apply(this, childrenCopy); }; TimingGroup.prototype = createObject(TimedItem.prototype, { _resolveFillMode: function(fillMode) { return fillMode === 'auto' ? 'both' : fillMode; }, _childrenStateModified: function() { // See _updateChildStartTimes(). this._isInChildrenStateModified = true; if (this._cachedTimedItemList) { this._cachedTimedItemList._ensureGetters(); } this._cachedIntrinsicDuration = null; // We need to walk up and down the tree to re-layout. endTime and the // various durations (which are all calculated lazily) are the only // properties of a TimedItem which can affect the layout of its ancestors. // So it should be sufficient to simply update start times and time markers // on the way down. // This calls up to our parent, then calls _updateTimeMarkers(). this._updateInternalState(); this._updateChildInheritedTimes(); // Update child start times before walking down. this._updateChildStartTimes(); if (this.player) { this.player._checkForLegacyHandlers(); } this._isInChildrenStateModified = false; }, _updateInheritedTime: function(inheritedTime) { this._inheritedTime = inheritedTime; this._updateTimeMarkers(); this._updateChildInheritedTimes(); }, _updateChildInheritedTimes: function() { for (var i = 0; i < this._children.length; i++) { var child = this._children[i]; child._updateInheritedTime(this._iterationTime); } }, _updateChildStartTimes: function() { if (this.type === 'seq') { var cumulativeStartTime = 0; for (var i = 0; i < this._children.length; i++) { var child = this._children[i]; if (child._stashedStartTime === undefined) { child._stashedStartTime = child._startTime; } child._startTime = cumulativeStartTime; // Avoid updating the child's inherited time and time markers if this is // about to be done in the down phase of _childrenStateModified(). if (!child._isInChildrenStateModified) { // This calls _updateTimeMarkers() on the child. child._updateInheritedTime(this._iterationTime); } cumulativeStartTime += Math.max(0, child.timing.delay + child.activeDuration + child.timing.endDelay); } } }, get children() { if (!this._cachedTimedItemList) { this._cachedTimedItemList = new TimedItemList( constructorToken, this._children); } return this._cachedTimedItemList; }, get firstChild() { return this._children[0]; }, get lastChild() { return this._children[this.children.length - 1]; }, _intrinsicDuration: function() { if (!isDefinedAndNotNull(this._cachedIntrinsicDuration)) { if (this.type === 'par') { var dur = Math.max.apply(undefined, this._children.map(function(a) { return a.endTime; })); this._cachedIntrinsicDuration = Math.max(0, dur); } else if (this.type === 'seq') { var result = 0; this._children.forEach(function(a) { result += a.activeDuration + a.timing.delay + a.timing.endDelay; }); this._cachedIntrinsicDuration = result; } else { throw 'Unsupported type ' + this.type; } } return this._cachedIntrinsicDuration; }, _getLeafItemsInEffectImpl: function(items) { for (var i = 0; i < this._children.length; i++) { this._children[i]._getLeafItemsInEffect(items); } }, clone: function() { var children = []; this._children.forEach(function(child) { children.push(child.clone()); }); return this.type === 'par' ? new AnimationGroup(children, this.timing._dict) : new AnimationSequence(children, this.timing._dict); }, clear: function() { this._splice(0, this._children.length); }, append: function() { var newItems = []; for (var i = 0; i < arguments.length; i++) { newItems.push(arguments[i]); } this._splice(this._children.length, 0, newItems); }, prepend: function() { var newItems = []; for (var i = 0; i < arguments.length; i++) { newItems.push(arguments[i]); } this._splice(0, 0, newItems); }, _addInternal: function(child) { this._children.push(child); this._childrenStateModified(); }, indexOf: function(item) { return this._children.indexOf(item); }, _splice: function(start, deleteCount, newItems) { enterModifyCurrentAnimationState(); try { var args = arguments; if (args.length === 3) { args = [start, deleteCount].concat(newItems); } for (var i = 2; i < args.length; i++) { var newChild = args[i]; if (this._isInclusiveAncestor(newChild)) { throwNewHierarchyRequestError(); } newChild._reparent(this); } var result = Array.prototype.splice.apply(this._children, args); for (var i = 0; i < result.length; i++) { result[i]._parent = null; } this._childrenStateModified(); return result; } finally { exitModifyCurrentAnimationState( Boolean(this.player) ? repeatLastTick : null); } }, _isInclusiveAncestor: function(item) { for (var ancestor = this; ancestor !== null; ancestor = ancestor.parent) { if (ancestor === item) { return true; } } return false; }, _isTargetingElement: function(element) { return this._children.some(function(child) { return child._isTargetingElement(element); }); }, _getAnimationsTargetingElement: function(element, animations) { this._children.map(function(child) { return child._getAnimationsTargetingElement(element, animations); }); }, toString: function() { return this.type + ' ' + this.startTime + '-' + this.endTime + ' (' + this.localTime + ') ' + ' [' + this._children.map(function(a) { return a.toString(); }) + ']'; }, _hasLegacyEventHandlers: function() { return TimedItem.prototype._hasLegacyEventHandlers.call(this) || ( this._children.length > 0 && this._children.reduce( function(a, b) { return a || b._hasLegacyEventHandlers(); }, false)); }, _generateChildLegacyEventsForRange: function(localStart, localEnd, rangeStart, rangeEnd, iteration, globalTime, deltaScale) { var start; var end; if (localEnd - localStart > 0) { start = Math.max(rangeStart, localStart); end = Math.min(rangeEnd, localEnd); if (start >= end) { return; } } else { start = Math.min(rangeStart, localStart); end = Math.max(rangeEnd, localEnd); if (start <= end) { return; } } var endDelta = rangeEnd - end; start -= iteration * this.duration / deltaScale; end -= iteration * this.duration / deltaScale; for (var i = 0; i < this._children.length; i++) { this._children[i]._generateLegacyEvents( start, end, globalTime - endDelta, deltaScale); } } }); /** @constructor */ var AnimationGroup = function(children, timing, parent) { TimingGroup.call(this, constructorToken, 'par', children, timing, parent); }; AnimationGroup.prototype = Object.create(TimingGroup.prototype); /** @constructor */ var AnimationSequence = function(children, timing, parent) { TimingGroup.call(this, constructorToken, 'seq', children, timing, parent); }; AnimationSequence.prototype = Object.create(TimingGroup.prototype); /** @constructor */ var PseudoElementReference = function(element, pseudoElement) { this.element = element; this.pseudoElement = pseudoElement; console.warn('PseudoElementReference is not supported.'); }; /** @constructor */ var MediaReference = function(mediaElement, timing, parent, delta) { TimedItem.call(this, constructorToken, timing, parent); this._media = mediaElement; // We can never be sure when _updateInheritedTime() is going to be called // next, due to skipped frames or the player being seeked. Plus the media // element's currentTime may drift from our iterationTime. So if a media // element has loop set, we can't be sure that we'll stop it before it wraps. // For this reason, we simply disable looping. // TODO: Maybe we should let it loop if our duration exceeds it's // length? this._media.loop = false; // If the media element has a media controller, we detach it. This mirrors the // behaviour when re-parenting a TimedItem, or attaching one to an // AnimationPlayer. // TODO: It would be neater to assign to MediaElement.controller, but this was // broken in Chrome until recently. See crbug.com/226270. this._media.mediaGroup = ''; this._delta = delta; }; MediaReference.prototype = createObject(TimedItem.prototype, { _resolveFillMode: function(fillMode) { // TODO: Fill modes for MediaReferences are still undecided. The spec is not // clear what 'auto' should mean for TimedItems other than Animations and // groups. return fillMode === 'auto' ? 'none' : fillMode; }, _intrinsicDuration: function() { // TODO: This should probably default to zero. But doing so means that as // soon as our inheritedTime is zero, the polyfill deems the animation to be // done and stops ticking, so we don't get any further calls to // _updateInheritedTime(). One way around this would be to modify // TimedItem._isPastEndOfActiveInterval() to recurse down the tree, then we // could override it here. return isNaN(this._media.duration) ? Infinity : this._media.duration / this._media.defaultPlaybackRate; }, _unscaledMediaCurrentTime: function() { return this._media.currentTime / this._media.defaultPlaybackRate; }, _getLeafItemsInEffectImpl: function(items) { items.push(this); }, _ensurePlaying: function() { // The media element is paused when created. if (this._media.paused) { this._media.play(); } }, _ensurePaused: function() { if (!this._media.paused) { this._media.pause(); } }, _isSeekableUnscaledTime: function(time) { var seekTime = time * this._media.defaultPlaybackRate; var ranges = this._media.seekable; for (var i = 0; i < ranges.length; i++) { if (seekTime >= ranges.start(i) && seekTime <= ranges.end(i)) { return true; } } return false; }, // Note that a media element's timeline may not start at zero, although its // duration is always the timeline time at the end point. This means that an // element's duration isn't always it's length and not all values of the // timline are seekable. Furthermore, some types of media further limit the // range of seekable timeline times. For this reason, we always map an // iteration to the range [0, duration] and simply seek to the nearest // seekable time. _ensureIsAtUnscaledTime: function(time) { if (this._unscaledMediaCurrentTime() !== time) { this._media.currentTime = time * this._media.defaultPlaybackRate; } }, // This is called by the polyfill on each tick when our AnimationPlayer's tree // is active. _updateInheritedTime: function(inheritedTime) { this._inheritedTime = inheritedTime; this._updateTimeMarkers(); // The polyfill uses a sampling model whereby time values are propagated // down the tree at each sample. However, for the media item, we need to use // play() and pause(). // Handle the case of being outside our effect interval. if (this._iterationTime === null) { this._ensureIsAtUnscaledTime(0); this._ensurePaused(); return; } if (this._iterationTime >= this._intrinsicDuration()) { // Our iteration time exceeds the media element's duration, so just make // sure the media element is at the end. It will stop automatically, but // that could take some time if the seek below is significant, so force // it. this._ensureIsAtUnscaledTime(this._intrinsicDuration()); this._ensurePaused(); return; } var finalIteration = this._floorWithOpenClosedRange( this.timing.iterationStart + this.timing._iterations(), 1.0); var endTimeFraction = this._modulusWithOpenClosedRange( this.timing.iterationStart + this.timing._iterations(), 1.0); if (this.currentIteration === finalIteration && this._timeFraction === endTimeFraction && this._intrinsicDuration() >= this.duration) { // We have reached the end of our final iteration, but the media element // is not done. this._ensureIsAtUnscaledTime(this.duration * endTimeFraction); this._ensurePaused(); return; } // Set the appropriate playback rate. var playbackRate = this._media.defaultPlaybackRate * this._netEffectivePlaybackRate(); if (this._media.playbackRate !== playbackRate) { this._media.playbackRate = playbackRate; } // Set the appropriate play/pause state. Note that we may not be able to // seek to the desired time. In this case, the media element's seek // algorithm repositions the seek to the nearest seekable time. This is OK, // but in this case, we don't want to play the media element, as it prevents // us from synchronising properly. if (this.player.paused || !this._isSeekableUnscaledTime(this._iterationTime)) { this._ensurePaused(); } else { this._ensurePlaying(); } // Seek if required. This could be due to our AnimationPlayer being seeked, // or video slippage. We need to handle the fact that the video may not play // at exactly the right speed. There's also a variable delay when the video // is first played. // TODO: What's the right value for this delta? var delta = isDefinedAndNotNull(this._delta) ? this._delta : 0.2 * Math.abs(this._media.playbackRate); if (Math.abs(this._iterationTime - this._unscaledMediaCurrentTime()) > delta) { this._ensureIsAtUnscaledTime(this._iterationTime); } }, _isTargetingElement: function(element) { return this._media === element; }, _getAnimationsTargetingElement: function() { }, _attach: function(player) { this._ensurePaused(); TimedItem.prototype._attach.call(this, player); } }); /** @constructor */ var AnimationEffect = function(token) { if (token !== constructorToken) { throw new TypeError('Illegal constructor'); } }; AnimationEffect.prototype = { _sample: abstractMethod, clone: abstractMethod, toString: abstractMethod }; var clamp = function(x, min, max) { return Math.max(Math.min(x, max), min); }; /** @constructor */ var MotionPathEffect = function(path, autoRotate, angle, composite) { var iterationComposite = undefined; var options = autoRotate; if (typeof options == 'string' || options instanceof String || angle || composite) { // FIXME: add deprecation warning - please pass an options dictionary to // MotionPathEffect constructor } else if (options) { autoRotate = options.autoRotate; angle = options.angle; composite = options.composite; iterationComposite = options.iterationComposite; } enterModifyCurrentAnimationState(); try { AnimationEffect.call(this, constructorToken); this.composite = composite; this.iterationComposite = iterationComposite; // TODO: path argument is not in the spec -- seems useful since // SVGPathSegList doesn't have a constructor. this.autoRotate = isDefined(autoRotate) ? autoRotate : 'none'; this.angle = isDefined(angle) ? angle : 0; this._path = document.createElementNS(SVG_NS, 'path'); if (path instanceof SVGPathSegList) { this.segments = path; } else { var tempPath = document.createElementNS(SVG_NS, 'path'); tempPath.setAttribute('d', String(path)); this.segments = tempPath.pathSegList; } } finally { exitModifyCurrentAnimationState(null); } }; MotionPathEffect.prototype = createObject(AnimationEffect.prototype, { get composite() { return this._composite; }, set composite(value) { enterModifyCurrentAnimationState(); try { // Use the default value if an invalid string is specified. this._composite = value === 'add' ? 'add' : 'replace'; } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, get iterationComposite() { return this._iterationComposite; }, set iterationComposite(value) { enterModifyCurrentAnimationState(); try { // Use the default value if an invalid string is specified. this._iterationComposite = value === 'accumulate' ? 'accumulate' : 'replace'; this._updateOffsetPerIteration(); } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, _sample: function(timeFraction, currentIteration, target) { // TODO: Handle accumulation. var lengthAtTimeFraction = this._lengthAtTimeFraction(timeFraction); var point = this._path.getPointAtLength(lengthAtTimeFraction); var x = point.x - target.offsetWidth / 2; var y = point.y - target.offsetHeight / 2; if (currentIteration !== 0 && this._offsetPerIteration) { x += this._offsetPerIteration.x * currentIteration; y += this._offsetPerIteration.y * currentIteration; } // TODO: calc(point.x - 50%) doesn't work? var value = [{t: 'translate', d: [{px: x}, {px: y}]}]; var angle = this.angle; if (this._autoRotate === 'auto-rotate') { // Super hacks var lastPoint = this._path.getPointAtLength(lengthAtTimeFraction - 0.01); var dx = point.x - lastPoint.x; var dy = point.y - lastPoint.y; var rotation = Math.atan2(dy, dx); angle += rotation / 2 / Math.PI * 360; } value.push({t: 'rotate', d: [angle]}); compositor.setAnimatedValue(target, 'transform', new AddReplaceCompositableValue(value, this.composite)); }, _lengthAtTimeFraction: function(timeFraction) { var segmentCount = this._cumulativeLengths.length - 1; if (!segmentCount) { return 0; } var scaledFraction = timeFraction * segmentCount; var index = clamp(Math.floor(scaledFraction), 0, segmentCount); return this._cumulativeLengths[index] + ((scaledFraction % 1) * ( this._cumulativeLengths[index + 1] - this._cumulativeLengths[index])); }, _updateOffsetPerIteration: function() { if (this.iterationComposite === 'accumulate' && this._cumulativeLengths && this._cumulativeLengths.length > 0) { this._offsetPerIteration = this._path.getPointAtLength( this._cumulativeLengths[this._cumulativeLengths.length - 1]); } else { this._offsetPerIteration = null; } }, clone: function() { return new MotionPathEffect(this._path.getAttribute('d')); }, toString: function() { return ''; }, set autoRotate(autoRotate) { enterModifyCurrentAnimationState(); try { this._autoRotate = String(autoRotate); } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, get autoRotate() { return this._autoRotate; }, set angle(angle) { enterModifyCurrentAnimationState(); try { // TODO: This should probably be a string with a unit, but the spec // says it's a double. this._angle = Number(angle); } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, get angle() { return this._angle; }, set segments(segments) { enterModifyCurrentAnimationState(); try { var targetSegments = this.segments; targetSegments.clear(); var cumulativeLengths = [0]; // TODO: *moving* the path segments is not correct, but pathSegList // is read only var items = segments.numberOfItems; while (targetSegments.numberOfItems < items) { var segment = segments.removeItem(0); targetSegments.appendItem(segment); if (segment.pathSegType !== SVGPathSeg.PATHSEG_MOVETO_REL && segment.pathSegType !== SVGPathSeg.PATHSEG_MOVETO_ABS) { cumulativeLengths.push(this._path.getTotalLength()); } } this._cumulativeLengths = cumulativeLengths; this._updateOffsetPerIteration(); } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, get segments() { return this._path.pathSegList; } }); var shorthandToLonghand = { background: [ 'backgroundImage', 'backgroundPosition', 'backgroundSize', 'backgroundRepeat', 'backgroundAttachment', 'backgroundOrigin', 'backgroundClip', 'backgroundColor' ], border: [ 'borderTopColor', 'borderTopStyle', 'borderTopWidth', 'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderBottomColor', 'borderBottomStyle', 'borderBottomWidth', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth' ], borderBottom: [ 'borderBottomWidth', 'borderBottomStyle', 'borderBottomColor' ], borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ], borderLeft: [ 'borderLeftWidth', 'borderLeftStyle', 'borderLeftColor' ], borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ], borderRight: [ 'borderRightWidth', 'borderRightStyle', 'borderRightColor' ], borderTop: [ 'borderTopWidth', 'borderTopStyle', 'borderTopColor' ], borderWidth: [ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth' ], font: [ 'fontFamily', 'fontSize', 'fontStyle', 'fontVariant', 'fontWeight', 'lineHeight' ], margin: [ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft' ], outline: [ 'outlineColor', 'outlineStyle', 'outlineWidth' ], padding: [ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft' ] }; // This delegates parsing shorthand value syntax to the browser. var shorthandExpanderElem = createDummyElement(); var expandShorthand = function(property, value, result) { shorthandExpanderElem.style[property] = value; var longProperties = shorthandToLonghand[property]; for (var i in longProperties) { var longProperty = longProperties[i]; var longhandValue = shorthandExpanderElem.style[longProperty]; result[longProperty] = longhandValue; } }; var normalizeKeyframeDictionary = function(properties) { var result = { offset: null, composite: null, easing: presetTimingFunctions.linear }; var animationProperties = []; for (var property in properties) { // TODO: Apply the CSS property to IDL attribute algorithm. if (property === 'offset') { if (typeof properties.offset === 'number') { result.offset = properties.offset; } } else if (property === 'composite') { if (properties.composite === 'add' || properties.composite === 'replace') { result.composite = properties.composite; } } else if (property === 'easing') { result.easing = TimingFunction.createFromString(properties.easing); } else { // TODO: Check whether this is a supported property. animationProperties.push(property); } } // TODO: Remove prefixed properties if the unprefixed version is also // supported and present. animationProperties = animationProperties.sort(playerSortFunction); for (var i = 0; i < animationProperties.length; i++) { // TODO: Apply the IDL attribute to CSS property algorithm. var property = animationProperties[i]; // TODO: The spec does not specify how to handle null values. // See https://www.w3.org/Bugs/Public/show_bug.cgi?id=22572 var value = isDefinedAndNotNull(properties[property]) ? properties[property].toString() : ''; if (property in shorthandToLonghand) { expandShorthand(property, value, result); } else { result[property] = value; } } return result; }; /** @constructor */ var KeyframeEffect = function(oneOrMoreKeyframeDictionaries, composite) { enterModifyCurrentAnimationState(); try { AnimationEffect.call(this, constructorToken); this.composite = composite; this.setFrames(oneOrMoreKeyframeDictionaries); } finally { exitModifyCurrentAnimationState(null); } }; KeyframeEffect.prototype = createObject(AnimationEffect.prototype, { get composite() { return this._composite; }, set composite(value) { enterModifyCurrentAnimationState(); try { // Use the default value if an invalid string is specified. this._composite = value === 'add' ? 'add' : 'replace'; } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, getFrames: function() { return this._keyframeDictionaries.slice(0); }, setFrames: function(oneOrMoreKeyframeDictionaries) { enterModifyCurrentAnimationState(); try { if (!Array.isArray(oneOrMoreKeyframeDictionaries)) { oneOrMoreKeyframeDictionaries = [oneOrMoreKeyframeDictionaries]; } this._keyframeDictionaries = oneOrMoreKeyframeDictionaries.map(normalizeKeyframeDictionary); // Set lazily this._cachedPropertySpecificKeyframes = null; } finally { exitModifyCurrentAnimationState(repeatLastTick); } }, _sample: function(timeFraction, currentIteration, target) { var frames = this._propertySpecificKeyframes(); for (var property in frames) { compositor.setAnimatedValue(target, property, this._sampleForProperty( frames[property], timeFraction, currentIteration)); } }, _sampleForProperty: function(frames, timeFraction, currentIteration) { ASSERT_ENABLED && assert( frames.length >= 2, 'Interpolation requires at least two keyframes'); var startKeyframeIndex; var length = frames.length; // We extrapolate differently depending on whether or not there are multiple // keyframes at offsets of 0 and 1. if (timeFraction < 0.0) { if (frames[1].offset === 0.0) { return new AddReplaceCompositableValue(frames[0].rawValue(), this._compositeForKeyframe(frames[0])); } else { startKeyframeIndex = 0; } } else if (timeFraction >= 1.0) { if (frames[length - 2].offset === 1.0) { return new AddReplaceCompositableValue(frames[length - 1].rawValue(), this._compositeForKeyframe(frames[length - 1])); } else { startKeyframeIndex = length - 2; } } else { for (var i = length - 1; i >= 0; i--) { if (frames[i].offset <= timeFraction) { ASSERT_ENABLED && assert(frames[i].offset !== 1.0); startKeyframeIndex = i; break; } } } var startKeyframe = frames[startKeyframeIndex]; var endKeyframe = frames[startKeyframeIndex + 1]; if (startKeyframe.offset === timeFraction) { return new AddReplaceCompositableValue(startKeyframe.rawValue(), this._compositeForKeyframe(startKeyframe)); } if (endKeyframe.offset === timeFraction) { return new AddReplaceCompositableValue(endKeyframe.rawValue(), this._compositeForKeyframe(endKeyframe)); } var intervalDistance = (timeFraction - startKeyframe.offset) / (endKeyframe.offset - startKeyframe.offset); if (startKeyframe.easing) { intervalDistance = startKeyframe.easing.scaleTime(intervalDistance); } return new BlendedCompositableValue( new AddReplaceCompositableValue(startKeyframe.rawValue(), this._compositeForKeyframe(startKeyframe)), new AddReplaceCompositableValue(endKeyframe.rawValue(), this._compositeForKeyframe(endKeyframe)), intervalDistance); }, _propertySpecificKeyframes: function() { if (isDefinedAndNotNull(this._cachedPropertySpecificKeyframes)) { return this._cachedPropertySpecificKeyframes; } this._cachedPropertySpecificKeyframes = {}; var distributedFrames = this._getDistributedKeyframes(); for (var i = 0; i < distributedFrames.length; i++) { for (var property in distributedFrames[i].cssValues) { if (!(property in this._cachedPropertySpecificKeyframes)) { this._cachedPropertySpecificKeyframes[property] = []; } var frame = distributedFrames[i]; this._cachedPropertySpecificKeyframes[property].push( new PropertySpecificKeyframe(frame.offset, frame.composite, frame.easing, property, frame.cssValues[property])); } } for (var property in this._cachedPropertySpecificKeyframes) { var frames = this._cachedPropertySpecificKeyframes[property]; ASSERT_ENABLED && assert( frames.length > 0, 'There should always be keyframes for each property'); // Add synthetic keyframes at offsets of 0 and 1 if required. if (frames[0].offset !== 0.0) { var keyframe = new PropertySpecificKeyframe(0.0, 'add', presetTimingFunctions.linear, property, cssNeutralValue); frames.unshift(keyframe); } if (frames[frames.length - 1].offset !== 1.0) { var keyframe = new PropertySpecificKeyframe(1.0, 'add', presetTimingFunctions.linear, property, cssNeutralValue); frames.push(keyframe); } ASSERT_ENABLED && assert( frames.length >= 2, 'There should be at least two keyframes including' + ' synthetic keyframes'); } return this._cachedPropertySpecificKeyframes; }, clone: function() { var result = new KeyframeEffect([], this.composite); result._keyframeDictionaries = this._keyframeDictionaries.slice(0); return result; }, toString: function() { return ''; }, _compositeForKeyframe: function(keyframe) { return isDefinedAndNotNull(keyframe.composite) ? keyframe.composite : this.composite; }, _allKeyframesUseSameCompositeOperation: function(keyframes) { ASSERT_ENABLED && assert( keyframes.length >= 1, 'This requires at least one keyframe'); var composite = this._compositeForKeyframe(keyframes[0]); for (var i = 1; i < keyframes.length; i++) { if (this._compositeForKeyframe(keyframes[i]) !== composite) { return false; } } return true; }, _areKeyframeDictionariesLooselySorted: function() { var previousOffset = -Infinity; for (var i = 0; i < this._keyframeDictionaries.length; i++) { if (isDefinedAndNotNull(this._keyframeDictionaries[i].offset)) { if (this._keyframeDictionaries[i].offset < previousOffset) { return false; } previousOffset = this._keyframeDictionaries[i].offset; } } return true; }, // The spec describes both this process and the process for interpretting the // properties of a keyframe dictionary as 'normalizing'. Here we use the term // 'distributing' to avoid confusion with normalizeKeyframeDictionary(). _getDistributedKeyframes: function() { if (!this._areKeyframeDictionariesLooselySorted()) { return []; } var distributedKeyframes = this._keyframeDictionaries.map( KeyframeInternal.createFromNormalizedProperties); // Remove keyframes with offsets out of bounds. var length = distributedKeyframes.length; var count = 0; for (var i = 0; i < length; i++) { var offset = distributedKeyframes[i].offset; if (isDefinedAndNotNull(offset)) { if (offset >= 0) { break; } else { count = i; } } } distributedKeyframes.splice(0, count); length = distributedKeyframes.length; count = 0; for (var i = length - 1; i >= 0; i--) { var offset = distributedKeyframes[i].offset; if (isDefinedAndNotNull(offset)) { if (offset <= 1) { break; } else { count = length - i; } } } distributedKeyframes.splice(length - count, count); // Distribute offsets. length = distributedKeyframes.length; if (length > 1 && !isDefinedAndNotNull(distributedKeyframes[0].offset)) { distributedKeyframes[0].offset = 0; } if (length > 0 && !isDefinedAndNotNull(distributedKeyframes[length - 1].offset)) { distributedKeyframes[length - 1].offset = 1; } var lastOffsetIndex = 0; var nextOffsetIndex = 0; for (var i = 1; i < distributedKeyframes.length - 1; i++) { var keyframe = distributedKeyframes[i]; if (isDefinedAndNotNull(keyframe.offset)) { lastOffsetIndex = i; continue; } if (i > nextOffsetIndex) { nextOffsetIndex = i; while (!isDefinedAndNotNull( distributedKeyframes[nextOffsetIndex].offset)) { nextOffsetIndex++; } } var lastOffset = distributedKeyframes[lastOffsetIndex].offset; var nextOffset = distributedKeyframes[nextOffsetIndex].offset; var unspecifiedKeyframes = nextOffsetIndex - lastOffsetIndex - 1; ASSERT_ENABLED && assert(unspecifiedKeyframes > 0); var localIndex = i - lastOffsetIndex; ASSERT_ENABLED && assert(localIndex > 0); distributedKeyframes[i].offset = lastOffset + (nextOffset - lastOffset) * localIndex / (unspecifiedKeyframes + 1); } // Remove invalid property values. for (var i = distributedKeyframes.length - 1; i >= 0; i--) { var keyframe = distributedKeyframes[i]; for (var property in keyframe.cssValues) { if (!KeyframeInternal.isSupportedPropertyValue( keyframe.cssValues[property])) { delete(keyframe.cssValues[property]); } } if (Object.keys(keyframe).length === 0) { distributedKeyframes.splice(i, 1); } } return distributedKeyframes; } }); /** * An internal representation of a keyframe. The Keyframe type from the spec is * just a dictionary and is not exposed. * * @constructor */ var KeyframeInternal = function(offset, composite, easing) { ASSERT_ENABLED && assert( typeof offset === 'number' || offset === null, 'Invalid offset value'); ASSERT_ENABLED && assert( composite === 'add' || composite === 'replace' || composite === null, 'Invalid composite value'); this.offset = offset; this.composite = composite; this.easing = easing; this.cssValues = {}; }; KeyframeInternal.prototype = { addPropertyValuePair: function(property, value) { ASSERT_ENABLED && assert(!this.cssValues.hasOwnProperty(property)); this.cssValues[property] = value; }, hasValueForProperty: function(property) { return property in this.cssValues; } }; KeyframeInternal.isSupportedPropertyValue = function(value) { ASSERT_ENABLED && assert( typeof value === 'string' || value === cssNeutralValue); // TODO: Check this properly! return value !== ''; }; KeyframeInternal.createFromNormalizedProperties = function(properties) { ASSERT_ENABLED && assert( isDefinedAndNotNull(properties) && typeof properties === 'object', 'Properties must be an object'); var keyframe = new KeyframeInternal(properties.offset, properties.composite, properties.easing); for (var candidate in properties) { if (candidate !== 'offset' && candidate !== 'composite' && candidate !== 'easing') { keyframe.addPropertyValuePair(candidate, properties[candidate]); } } return keyframe; }; /** @constructor */ var PropertySpecificKeyframe = function(offset, composite, easing, property, cssValue) { this.offset = offset; this.composite = composite; this.easing = easing; this.property = property; this.cssValue = cssValue; // Calculated lazily this.cachedRawValue = null; }; PropertySpecificKeyframe.prototype = { rawValue: function() { if (!isDefinedAndNotNull(this.cachedRawValue)) { this.cachedRawValue = fromCssValue(this.property, this.cssValue); } return this.cachedRawValue; } }; /** @constructor */ var TimingFunction = function() { throw new TypeError('Illegal constructor'); }; TimingFunction.prototype.scaleTime = abstractMethod; TimingFunction.createFromString = function(spec, timedItem) { var preset = presetTimingFunctions[spec]; if (preset) { return preset; } if (spec === 'paced') { if (timedItem instanceof Animation && timedItem.effect instanceof MotionPathEffect) { return new PacedTimingFunction(timedItem.effect); } return presetTimingFunctions.linear; } var stepMatch = /steps\(\s*(\d+)\s*,\s*(start|end|middle)\s*\)/.exec(spec); if (stepMatch) { return new StepTimingFunction(Number(stepMatch[1]), stepMatch[2]); } var bezierMatch = /cubic-bezier\(([^,]*),([^,]*),([^,]*),([^)]*)\)/.exec(spec); if (bezierMatch) { return new CubicBezierTimingFunction([ Number(bezierMatch[1]), Number(bezierMatch[2]), Number(bezierMatch[3]), Number(bezierMatch[4]) ]); } return presetTimingFunctions.linear; }; /** @constructor */ var CubicBezierTimingFunction = function(spec) { this.params = spec; this.map = []; for (var ii = 0; ii <= 100; ii += 1) { var i = ii / 100; this.map.push([ 3 * i * (1 - i) * (1 - i) * this.params[0] + 3 * i * i * (1 - i) * this.params[2] + i * i * i, 3 * i * (1 - i) * (1 - i) * this.params[1] + 3 * i * i * (1 - i) * this.params[3] + i * i * i ]); } }; CubicBezierTimingFunction.prototype = createObject(TimingFunction.prototype, { scaleTime: function(fraction) { var fst = 0; while (fst !== 100 && fraction > this.map[fst][0]) { fst += 1; } if (fraction === this.map[fst][0] || fst === 0) { return this.map[fst][1]; } var yDiff = this.map[fst][1] - this.map[fst - 1][1]; var xDiff = this.map[fst][0] - this.map[fst - 1][0]; var p = (fraction - this.map[fst - 1][0]) / xDiff; return this.map[fst - 1][1] + p * yDiff; } }); /** @constructor */ var StepTimingFunction = function(numSteps, position) { this.numSteps = numSteps; this.position = position || 'end'; }; StepTimingFunction.prototype = createObject(TimingFunction.prototype, { scaleTime: function(fraction) { if (fraction >= 1) { return 1; } var stepSize = 1 / this.numSteps; if (this.position === 'start') { fraction += stepSize; } else if (this.position === 'middle') { fraction += stepSize / 2; } return fraction - fraction % stepSize; } }); var presetTimingFunctions = { 'linear': null, 'ease': new CubicBezierTimingFunction([0.25, 0.1, 0.25, 1.0]), 'ease-in': new CubicBezierTimingFunction([0.42, 0, 1.0, 1.0]), 'ease-out': new CubicBezierTimingFunction([0, 0, 0.58, 1.0]), 'ease-in-out': new CubicBezierTimingFunction([0.42, 0, 0.58, 1.0]), 'step-start': new StepTimingFunction(1, 'start'), 'step-middle': new StepTimingFunction(1, 'middle'), 'step-end': new StepTimingFunction(1, 'end') }; /** @constructor */ var PacedTimingFunction = function(pathEffect) { ASSERT_ENABLED && assert(pathEffect instanceof MotionPathEffect); this._pathEffect = pathEffect; // Range is the portion of the effect over which we pace, normalized to // [0, 1]. this._range = {min: 0, max: 1}; }; PacedTimingFunction.prototype = createObject(TimingFunction.prototype, { setRange: function(range) { ASSERT_ENABLED && assert(range.min >= 0 && range.min <= 1); ASSERT_ENABLED && assert(range.max >= 0 && range.max <= 1); ASSERT_ENABLED && assert(range.min < range.max); this._range = range; }, scaleTime: function(fraction) { var cumulativeLengths = this._pathEffect._cumulativeLengths; var numSegments = cumulativeLengths.length - 1; if (!cumulativeLengths[numSegments] || fraction <= 0) { return this._range.min; } if (fraction >= 1) { return this._range.max; } var minLength = this.lengthAtIndex(this._range.min * numSegments); var maxLength = this.lengthAtIndex(this._range.max * numSegments); var length = interp(minLength, maxLength, fraction); var leftIndex = this.findLeftIndex(cumulativeLengths, length); var leftLength = cumulativeLengths[leftIndex]; var segmentLength = cumulativeLengths[leftIndex + 1] - leftLength; if (segmentLength > 0) { return (leftIndex + (length - leftLength) / segmentLength) / numSegments; } return leftLength / cumulativeLengths.length; }, findLeftIndex: function(array, value) { var leftIndex = 0; var rightIndex = array.length; while (rightIndex - leftIndex > 1) { var midIndex = (leftIndex + rightIndex) >> 1; if (array[midIndex] <= value) { leftIndex = midIndex; } else { rightIndex = midIndex; } } return leftIndex; }, lengthAtIndex: function(i) { ASSERT_ENABLED && console.assert(i >= 0 && i <= cumulativeLengths.length - 1); var leftIndex = Math.floor(i); var startLength = this._pathEffect._cumulativeLengths[leftIndex]; var endLength = this._pathEffect._cumulativeLengths[leftIndex + 1]; var indexFraction = i % 1; return interp(startLength, endLength, indexFraction); } }); var interp = function(from, to, f, type) { if (Array.isArray(from) || Array.isArray(to)) { return interpArray(from, to, f, type); } var zero = (type && type.indexOf('scale') === 0) ? 1 : 0; to = isDefinedAndNotNull(to) ? to : zero; from = isDefinedAndNotNull(from) ? from : zero; return to * f + from * (1 - f); }; var interpArray = function(from, to, f, type) { ASSERT_ENABLED && assert( Array.isArray(from) || from === null, 'From is not an array or null'); ASSERT_ENABLED && assert( Array.isArray(to) || to === null, 'To is not an array or null'); ASSERT_ENABLED && assert( from === null || to === null || from.length === to.length, 'Arrays differ in length ' + from + ' : ' + to); var length = from ? from.length : to.length; var result = []; for (var i = 0; i < length; i++) { result[i] = interp(from ? from[i] : null, to ? to[i] : null, f, type); } return result; }; var typeWithKeywords = function(keywords, type) { var isKeyword; if (keywords.length === 1) { var keyword = keywords[0]; isKeyword = function(value) { return value === keyword; }; } else { isKeyword = function(value) { return keywords.indexOf(value) >= 0; }; } return createObject(type, { add: function(base, delta) { if (isKeyword(base) || isKeyword(delta)) { return delta; } return type.add(base, delta); }, interpolate: function(from, to, f) { if (isKeyword(from) || isKeyword(to)) { return nonNumericType.interpolate(from, to, f); } return type.interpolate(from, to, f); }, toCssValue: function(value, svgMode) { return isKeyword(value) ? value : type.toCssValue(value, svgMode); }, fromCssValue: function(value) { return isKeyword(value) ? value : type.fromCssValue(value); } }); }; var numberType = { add: function(base, delta) { // If base or delta are 'auto', we fall back to replacement. if (base === 'auto' || delta === 'auto') { return nonNumericType.add(base, delta); } return base + delta; }, interpolate: function(from, to, f) { // If from or to are 'auto', we fall back to step interpolation. if (from === 'auto' || to === 'auto') { return nonNumericType.interpolate(from, to); } return interp(from, to, f); }, toCssValue: function(value) { return value + ''; }, fromCssValue: function(value) { if (value === 'auto') { return 'auto'; } var result = Number(value); return isNaN(result) ? undefined : result; } }; var integerType = createObject(numberType, { interpolate: function(from, to, f) { // If from or to are 'auto', we fall back to step interpolation. if (from === 'auto' || to === 'auto') { return nonNumericType.interpolate(from, to); } return Math.floor(interp(from, to, f)); } }); var fontWeightType = { add: function(base, delta) { return base + delta; }, interpolate: function(from, to, f) { return interp(from, to, f); }, toCssValue: function(value) { value = Math.round(value / 100) * 100; value = clamp(value, 100, 900); if (value === 400) { return 'normal'; } if (value === 700) { return 'bold'; } return String(value); }, fromCssValue: function(value) { // TODO: support lighter / darker ? var out = Number(value); if (isNaN(out) || out < 100 || out > 900 || out % 100 !== 0) { return undefined; } return out; } }; // This regular expression is intentionally permissive, so that // platform-prefixed versions of calc will still be accepted as // input. While we are restrictive with the transform property // name, we need to be able to read underlying calc values from // computedStyle so can't easily restrict the input here. var outerCalcRE = /^\s*(-webkit-)?calc\s*\(\s*([^)]*)\)/; var valueRE = /^\s*(-?[0-9]+(\.[0-9])?[0-9]*)([a-zA-Z%]*)/; var operatorRE = /^\s*([+-])/; var autoRE = /^\s*auto/i; var percentLengthType = { zero: function() { return {}; }, add: function(base, delta) { var out = {}; for (var value in base) { out[value] = base[value] + (delta[value] || 0); } for (value in delta) { if (value in base) { continue; } out[value] = delta[value]; } return out; }, interpolate: function(from, to, f) { var out = {}; for (var value in from) { out[value] = interp(from[value], to[value], f); } for (var value in to) { if (value in out) { continue; } out[value] = interp(0, to[value], f); } return out; }, toCssValue: function(value) { var s = ''; var singleValue = true; for (var item in value) { if (s === '') { s = value[item] + item; } else if (singleValue) { if (value[item] !== 0) { s = features.calcFunction + '(' + s + ' + ' + value[item] + item + ')'; singleValue = false; } } else if (value[item] !== 0) { s = s.substring(0, s.length - 1) + ' + ' + value[item] + item + ')'; } } return s; }, fromCssValue: function(value) { var result = percentLengthType.consumeValueFromString(value); if (result) { return result.value; } return undefined; }, consumeValueFromString: function(value) { if (!isDefinedAndNotNull(value)) { return undefined; } var autoMatch = autoRE.exec(value); if (autoMatch) { return { value: { auto: true }, remaining: value.substring(autoMatch[0].length) }; } var out = {}; var calcMatch = outerCalcRE.exec(value); if (!calcMatch) { var singleValue = valueRE.exec(value); if (singleValue && (singleValue.length === 4)) { out[singleValue[3]] = Number(singleValue[1]); return { value: out, remaining: value.substring(singleValue[0].length) }; } return undefined; } var remaining = value.substring(calcMatch[0].length); var calcInnards = calcMatch[2]; var firstTime = true; while (true) { var reversed = false; if (firstTime) { firstTime = false; } else { var op = operatorRE.exec(calcInnards); if (!op) { return undefined; } if (op[1] === '-') { reversed = true; } calcInnards = calcInnards.substring(op[0].length); } value = valueRE.exec(calcInnards); if (!value) { return undefined; } var valueUnit = value[3]; var valueNumber = Number(value[1]); if (!isDefinedAndNotNull(out[valueUnit])) { out[valueUnit] = 0; } if (reversed) { out[valueUnit] -= valueNumber; } else { out[valueUnit] += valueNumber; } calcInnards = calcInnards.substring(value[0].length); if (/\s*/.exec(calcInnards)[0].length === calcInnards.length) { return { value: out, remaining: remaining }; } } }, negate: function(value) { var out = {}; for (var unit in value) { out[unit] = -value[unit]; } return out; } }; var percentLengthAutoType = typeWithKeywords(['auto'], percentLengthType); var positionKeywordRE = /^\s*left|^\s*center|^\s*right|^\s*top|^\s*bottom/i; var positionType = { zero: function() { return [{ px: 0 }, { px: 0 }]; }, add: function(base, delta) { return [ percentLengthType.add(base[0], delta[0]), percentLengthType.add(base[1], delta[1]) ]; }, interpolate: function(from, to, f) { return [ percentLengthType.interpolate(from[0], to[0], f), percentLengthType.interpolate(from[1], to[1], f) ]; }, toCssValue: function(value) { return value.map(percentLengthType.toCssValue).join(' '); }, fromCssValue: function(value) { var tokens = positionType.consumeAllTokensFromString(value); if (!tokens || tokens.length > 4) { return undefined; } if (tokens.length === 1) { var token = tokens[0]; return (positionType.isHorizontalToken(token) ? [token, 'center'] : ['center', token]).map(positionType.resolveToken); } if (tokens.length === 2 && positionType.isHorizontalToken(tokens[0]) && positionType.isVerticalToken(tokens[1])) { return tokens.map(positionType.resolveToken); } if (tokens.filter(positionType.isKeyword).length !== 2) { return undefined; } var out = [undefined, undefined]; var center = false; for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; if (!positionType.isKeyword(token)) { return undefined; } if (token === 'center') { if (center) { return undefined; } center = true; continue; } var axis = Number(positionType.isVerticalToken(token)); if (out[axis]) { return undefined; } if (i === tokens.length - 1 || positionType.isKeyword(tokens[i + 1])) { out[axis] = positionType.resolveToken(token); continue; } var percentLength = tokens[++i]; if (token === 'bottom' || token === 'right') { percentLength = percentLengthType.negate(percentLength); percentLength['%'] = (percentLength['%'] || 0) + 100; } out[axis] = percentLength; } if (center) { if (!out[0]) { out[0] = positionType.resolveToken('center'); } else if (!out[1]) { out[1] = positionType.resolveToken('center'); } else { return undefined; } } return out.every(isDefinedAndNotNull) ? out : undefined; }, consumeAllTokensFromString: function(remaining) { var tokens = []; while (remaining.trim()) { var result = positionType.consumeTokenFromString(remaining); if (!result) { return undefined; } tokens.push(result.value); remaining = result.remaining; } return tokens; }, consumeTokenFromString: function(value) { var keywordMatch = positionKeywordRE.exec(value); if (keywordMatch) { return { value: keywordMatch[0].trim().toLowerCase(), remaining: value.substring(keywordMatch[0].length) }; } return percentLengthType.consumeValueFromString(value); }, resolveToken: function(token) { if (typeof token === 'string') { return percentLengthType.fromCssValue({ left: '0%', center: '50%', right: '100%', top: '0%', bottom: '100%' }[token]); } return token; }, isHorizontalToken: function(token) { if (typeof token === 'string') { return token in { left: true, center: true, right: true }; } return true; }, isVerticalToken: function(token) { if (typeof token === 'string') { return token in { top: true, center: true, bottom: true }; } return true; }, isKeyword: function(token) { return typeof token === 'string'; } }; // Spec: http://dev.w3.org/csswg/css-backgrounds/#background-position var positionListType = { zero: function() { return [positionType.zero()]; }, add: function(base, delta) { var out = []; var maxLength = Math.max(base.length, delta.length); for (var i = 0; i < maxLength; i++) { var basePosition = base[i] ? base[i] : positionType.zero(); var deltaPosition = delta[i] ? delta[i] : positionType.zero(); out.push(positionType.add(basePosition, deltaPosition)); } return out; }, interpolate: function(from, to, f) { var out = []; var maxLength = Math.max(from.length, to.length); for (var i = 0; i < maxLength; i++) { var fromPosition = from[i] ? from[i] : positionType.zero(); var toPosition = to[i] ? to[i] : positionType.zero(); out.push(positionType.interpolate(fromPosition, toPosition, f)); } return out; }, toCssValue: function(value) { return value.map(positionType.toCssValue).join(', '); }, fromCssValue: function(value) { if (!isDefinedAndNotNull(value)) { return undefined; } if (!value.trim()) { return [positionType.fromCssValue('0% 0%')]; } var positionValues = value.split(','); var out = positionValues.map(positionType.fromCssValue); return out.every(isDefinedAndNotNull) ? out : undefined; } }; var rectangleRE = /rect\(([^,]+),([^,]+),([^,]+),([^)]+)\)/; var rectangleType = { add: function(base, delta) { return { top: percentLengthType.add(base.top, delta.top), right: percentLengthType.add(base.right, delta.right), bottom: percentLengthType.add(base.bottom, delta.bottom), left: percentLengthType.add(base.left, delta.left) }; }, interpolate: function(from, to, f) { return { top: percentLengthType.interpolate(from.top, to.top, f), right: percentLengthType.interpolate(from.right, to.right, f), bottom: percentLengthType.interpolate(from.bottom, to.bottom, f), left: percentLengthType.interpolate(from.left, to.left, f) }; }, toCssValue: function(value) { return 'rect(' + percentLengthType.toCssValue(value.top) + ',' + percentLengthType.toCssValue(value.right) + ',' + percentLengthType.toCssValue(value.bottom) + ',' + percentLengthType.toCssValue(value.left) + ')'; }, fromCssValue: function(value) { var match = rectangleRE.exec(value); if (!match) { return undefined; } var out = { top: percentLengthType.fromCssValue(match[1]), right: percentLengthType.fromCssValue(match[2]), bottom: percentLengthType.fromCssValue(match[3]), left: percentLengthType.fromCssValue(match[4]) }; if (out.top && out.right && out.bottom && out.left) { return out; } return undefined; } }; var originType = { zero: function() { return [{'%': 0}, {'%': 0}, {px: 0}]; }, add: function(base, delta) { return [ percentLengthType.add(base[0], delta[0]), percentLengthType.add(base[1], delta[1]), percentLengthType.add(base[2], delta[2]) ]; }, interpolate: function(from, to, f) { return [ percentLengthType.interpolate(from[0], to[0], f), percentLengthType.interpolate(from[1], to[1], f), percentLengthType.interpolate(from[2], to[2], f) ]; }, toCssValue: function(value) { var result = percentLengthType.toCssValue(value[0]) + ' ' + percentLengthType.toCssValue(value[1]); // Return the third value if it is non-zero. for (var unit in value[2]) { if (value[2][unit] !== 0) { return result + ' ' + percentLengthType.toCssValue(value[2]); } } return result; }, fromCssValue: function(value) { var tokens = positionType.consumeAllTokensFromString(value); if (!tokens) { return undefined; } var out = ['center', 'center', {px: 0}]; switch (tokens.length) { case 0: return originType.zero(); case 1: if (positionType.isHorizontalToken(tokens[0])) { out[0] = tokens[0]; } else if (positionType.isVerticalToken(tokens[0])) { out[1] = tokens[0]; } else { return undefined; } return out.map(positionType.resolveToken); case 3: if (positionType.isKeyword(tokens[2])) { return undefined; } out[2] = tokens[2]; case 2: if (positionType.isHorizontalToken(tokens[0]) && positionType.isVerticalToken(tokens[1])) { out[0] = tokens[0]; out[1] = tokens[1]; } else if (positionType.isVerticalToken(tokens[0]) && positionType.isHorizontalToken(tokens[1])) { out[0] = tokens[1]; out[1] = tokens[0]; } else { return undefined; } return out.map(positionType.resolveToken); default: return undefined; } } }; var shadowType = { zero: function() { return { hOffset: lengthType.zero(), vOffset: lengthType.zero() }; }, _addSingle: function(base, delta) { if (base && delta && base.inset !== delta.inset) { return delta; } var result = { inset: base ? base.inset : delta.inset, hOffset: lengthType.add( base ? base.hOffset : lengthType.zero(), delta ? delta.hOffset : lengthType.zero()), vOffset: lengthType.add( base ? base.vOffset : lengthType.zero(), delta ? delta.vOffset : lengthType.zero()), blur: lengthType.add( base && base.blur || lengthType.zero(), delta && delta.blur || lengthType.zero()) }; if (base && base.spread || delta && delta.spread) { result.spread = lengthType.add( base && base.spread || lengthType.zero(), delta && delta.spread || lengthType.zero()); } if (base && base.color || delta && delta.color) { result.color = colorType.add( base && base.color || colorType.zero(), delta && delta.color || colorType.zero()); } return result; }, add: function(base, delta) { var result = []; for (var i = 0; i < base.length || i < delta.length; i++) { result.push(this._addSingle(base[i], delta[i])); } return result; }, _interpolateSingle: function(from, to, f) { if (from && to && from.inset !== to.inset) { return f < 0.5 ? from : to; } var result = { inset: from ? from.inset : to.inset, hOffset: lengthType.interpolate( from ? from.hOffset : lengthType.zero(), to ? to.hOffset : lengthType.zero(), f), vOffset: lengthType.interpolate( from ? from.vOffset : lengthType.zero(), to ? to.vOffset : lengthType.zero(), f), blur: lengthType.interpolate( from && from.blur || lengthType.zero(), to && to.blur || lengthType.zero(), f) }; if (from && from.spread || to && to.spread) { result.spread = lengthType.interpolate( from && from.spread || lengthType.zero(), to && to.spread || lengthType.zero(), f); } if (from && from.color || to && to.color) { result.color = colorType.interpolate( from && from.color || colorType.zero(), to && to.color || colorType.zero(), f); } return result; }, interpolate: function(from, to, f) { var result = []; for (var i = 0; i < from.length || i < to.length; i++) { result.push(this._interpolateSingle(from[i], to[i], f)); } return result; }, _toCssValueSingle: function(value) { return (value.inset ? 'inset ' : '') + lengthType.toCssValue(value.hOffset) + ' ' + lengthType.toCssValue(value.vOffset) + ' ' + lengthType.toCssValue(value.blur) + (value.spread ? ' ' + lengthType.toCssValue(value.spread) : '') + (value.color ? ' ' + colorType.toCssValue(value.color) : ''); }, toCssValue: function(value) { return value.map(this._toCssValueSingle).join(', '); }, fromCssValue: function(value) { var shadowRE = /(([^(,]+(\([^)]*\))?)+)/g; var match; var shadows = []; while ((match = shadowRE.exec(value)) !== null) { shadows.push(match[0]); } var result = shadows.map(function(value) { if (value === 'none') { return shadowType.zero(); } value = value.replace(/^\s+|\s+$/g, ''); var partsRE = /([^ (]+(\([^)]*\))?)/g; var parts = []; while ((match = partsRE.exec(value)) !== null) { parts.push(match[0]); } if (parts.length < 2 || parts.length > 7) { return undefined; } var result = { inset: false }; var lengths = []; while (parts.length) { var part = parts.shift(); var length = lengthType.fromCssValue(part); if (length) { lengths.push(length); continue; } var color = colorType.fromCssValue(part); if (color) { result.color = color; } if (part === 'inset') { result.inset = true; } } if (lengths.length < 2 || lengths.length > 4) { return undefined; } result.hOffset = lengths[0]; result.vOffset = lengths[1]; if (lengths.length > 2) { result.blur = lengths[2]; } if (lengths.length > 3) { result.spread = lengths[3]; } return result; }); return result.every(isDefined) ? result : undefined; } }; var nonNumericType = { add: function(base, delta) { return isDefined(delta) ? delta : base; }, interpolate: function(from, to, f) { return f < 0.5 ? from : to; }, toCssValue: function(value) { return value; }, fromCssValue: function(value) { return value; } }; var visibilityType = createObject(nonNumericType, { interpolate: function(from, to, f) { if (from !== 'visible' && to !== 'visible') { return nonNumericType.interpolate(from, to, f); } if (f <= 0) { return from; } if (f >= 1) { return to; } return 'visible'; }, fromCssValue: function(value) { if (['visible', 'hidden', 'collapse'].indexOf(value) !== -1) { return value; } return undefined; } }); var lengthType = percentLengthType; var lengthAutoType = typeWithKeywords(['auto'], lengthType); var colorRE = new RegExp( '(hsla?|rgba?)\\(' + '([\\-0-9]+%?),?\\s*' + '([\\-0-9]+%?),?\\s*' + '([\\-0-9]+%?)(?:,?\\s*([\\-0-9\\.]+%?))?' + '\\)'); var colorHashRE = new RegExp( '#([0-9A-Fa-f][0-9A-Fa-f]?)' + '([0-9A-Fa-f][0-9A-Fa-f]?)' + '([0-9A-Fa-f][0-9A-Fa-f]?)'); function hsl2rgb(h, s, l) { // Cribbed from http://dev.w3.org/csswg/css-color/#hsl-color // Wrap to 0->360 degrees (IE -10 === 350) then normalize h = (((h % 360) + 360) % 360) / 360; s = s / 100; l = l / 100; function hue2rgb(m1, m2, h) { if (h < 0) { h += 1; } if (h > 1) { h -= 1; } if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; } if (h * 2 < 1) { return m2; } if (h * 3 < 2) { return m1 + (m2 - m1) * (2 / 3 - h) * 6; } return m1; } var m2; if (l <= 0.5) { m2 = l * (s + 1); } else { m2 = l + s - l * s; } var m1 = l * 2 - m2; var r = Math.ceil(hue2rgb(m1, m2, h + 1 / 3) * 255); var g = Math.ceil(hue2rgb(m1, m2, h) * 255); var b = Math.ceil(hue2rgb(m1, m2, h - 1 / 3) * 255); return [r, g, b]; } var namedColors = { aliceblue: [240, 248, 255, 1], antiquewhite: [250, 235, 215, 1], aqua: [0, 255, 255, 1], aquamarine: [127, 255, 212, 1], azure: [240, 255, 255, 1], beige: [245, 245, 220, 1], bisque: [255, 228, 196, 1], black: [0, 0, 0, 1], blanchedalmond: [255, 235, 205, 1], blue: [0, 0, 255, 1], blueviolet: [138, 43, 226, 1], brown: [165, 42, 42, 1], burlywood: [222, 184, 135, 1], cadetblue: [95, 158, 160, 1], chartreuse: [127, 255, 0, 1], chocolate: [210, 105, 30, 1], coral: [255, 127, 80, 1], cornflowerblue: [100, 149, 237, 1], cornsilk: [255, 248, 220, 1], crimson: [220, 20, 60, 1], cyan: [0, 255, 255, 1], darkblue: [0, 0, 139, 1], darkcyan: [0, 139, 139, 1], darkgoldenrod: [184, 134, 11, 1], darkgray: [169, 169, 169, 1], darkgreen: [0, 100, 0, 1], darkgrey: [169, 169, 169, 1], darkkhaki: [189, 183, 107, 1], darkmagenta: [139, 0, 139, 1], darkolivegreen: [85, 107, 47, 1], darkorange: [255, 140, 0, 1], darkorchid: [153, 50, 204, 1], darkred: [139, 0, 0, 1], darksalmon: [233, 150, 122, 1], darkseagreen: [143, 188, 143, 1], darkslateblue: [72, 61, 139, 1], darkslategray: [47, 79, 79, 1], darkslategrey: [47, 79, 79, 1], darkturquoise: [0, 206, 209, 1], darkviolet: [148, 0, 211, 1], deeppink: [255, 20, 147, 1], deepskyblue: [0, 191, 255, 1], dimgray: [105, 105, 105, 1], dimgrey: [105, 105, 105, 1], dodgerblue: [30, 144, 255, 1], firebrick: [178, 34, 34, 1], floralwhite: [255, 250, 240, 1], forestgreen: [34, 139, 34, 1], fuchsia: [255, 0, 255, 1], gainsboro: [220, 220, 220, 1], ghostwhite: [248, 248, 255, 1], gold: [255, 215, 0, 1], goldenrod: [218, 165, 32, 1], gray: [128, 128, 128, 1], green: [0, 128, 0, 1], greenyellow: [173, 255, 47, 1], grey: [128, 128, 128, 1], honeydew: [240, 255, 240, 1], hotpink: [255, 105, 180, 1], indianred: [205, 92, 92, 1], indigo: [75, 0, 130, 1], ivory: [255, 255, 240, 1], khaki: [240, 230, 140, 1], lavender: [230, 230, 250, 1], lavenderblush: [255, 240, 245, 1], lawngreen: [124, 252, 0, 1], lemonchiffon: [255, 250, 205, 1], lightblue: [173, 216, 230, 1], lightcoral: [240, 128, 128, 1], lightcyan: [224, 255, 255, 1], lightgoldenrodyellow: [250, 250, 210, 1], lightgray: [211, 211, 211, 1], lightgreen: [144, 238, 144, 1], lightgrey: [211, 211, 211, 1], lightpink: [255, 182, 193, 1], lightsalmon: [255, 160, 122, 1], lightseagreen: [32, 178, 170, 1], lightskyblue: [135, 206, 250, 1], lightslategray: [119, 136, 153, 1], lightslategrey: [119, 136, 153, 1], lightsteelblue: [176, 196, 222, 1], lightyellow: [255, 255, 224, 1], lime: [0, 255, 0, 1], limegreen: [50, 205, 50, 1], linen: [250, 240, 230, 1], magenta: [255, 0, 255, 1], maroon: [128, 0, 0, 1], mediumaquamarine: [102, 205, 170, 1], mediumblue: [0, 0, 205, 1], mediumorchid: [186, 85, 211, 1], mediumpurple: [147, 112, 219, 1], mediumseagreen: [60, 179, 113, 1], mediumslateblue: [123, 104, 238, 1], mediumspringgreen: [0, 250, 154, 1], mediumturquoise: [72, 209, 204, 1], mediumvioletred: [199, 21, 133, 1], midnightblue: [25, 25, 112, 1], mintcream: [245, 255, 250, 1], mistyrose: [255, 228, 225, 1], moccasin: [255, 228, 181, 1], navajowhite: [255, 222, 173, 1], navy: [0, 0, 128, 1], oldlace: [253, 245, 230, 1], olive: [128, 128, 0, 1], olivedrab: [107, 142, 35, 1], orange: [255, 165, 0, 1], orangered: [255, 69, 0, 1], orchid: [218, 112, 214, 1], palegoldenrod: [238, 232, 170, 1], palegreen: [152, 251, 152, 1], paleturquoise: [175, 238, 238, 1], palevioletred: [219, 112, 147, 1], papayawhip: [255, 239, 213, 1], peachpuff: [255, 218, 185, 1], peru: [205, 133, 63, 1], pink: [255, 192, 203, 1], plum: [221, 160, 221, 1], powderblue: [176, 224, 230, 1], purple: [128, 0, 128, 1], red: [255, 0, 0, 1], rosybrown: [188, 143, 143, 1], royalblue: [65, 105, 225, 1], saddlebrown: [139, 69, 19, 1], salmon: [250, 128, 114, 1], sandybrown: [244, 164, 96, 1], seagreen: [46, 139, 87, 1], seashell: [255, 245, 238, 1], sienna: [160, 82, 45, 1], silver: [192, 192, 192, 1], skyblue: [135, 206, 235, 1], slateblue: [106, 90, 205, 1], slategray: [112, 128, 144, 1], slategrey: [112, 128, 144, 1], snow: [255, 250, 250, 1], springgreen: [0, 255, 127, 1], steelblue: [70, 130, 180, 1], tan: [210, 180, 140, 1], teal: [0, 128, 128, 1], thistle: [216, 191, 216, 1], tomato: [255, 99, 71, 1], transparent: [0, 0, 0, 0], turquoise: [64, 224, 208, 1], violet: [238, 130, 238, 1], wheat: [245, 222, 179, 1], white: [255, 255, 255, 1], whitesmoke: [245, 245, 245, 1], yellow: [255, 255, 0, 1], yellowgreen: [154, 205, 50, 1] }; var colorType = typeWithKeywords(['currentColor'], { zero: function() { return [0, 0, 0, 0]; }, _premultiply: function(value) { var alpha = value[3]; return [value[0] * alpha, value[1] * alpha, value[2] * alpha]; }, add: function(base, delta) { var alpha = Math.min(base[3] + delta[3], 1); if (alpha === 0) { return [0, 0, 0, 0]; } base = this._premultiply(base); delta = this._premultiply(delta); return [(base[0] + delta[0]) / alpha, (base[1] + delta[1]) / alpha, (base[2] + delta[2]) / alpha, alpha]; }, interpolate: function(from, to, f) { var alpha = clamp(interp(from[3], to[3], f), 0, 1); if (alpha === 0) { return [0, 0, 0, 0]; } from = this._premultiply(from); to = this._premultiply(to); return [interp(from[0], to[0], f) / alpha, interp(from[1], to[1], f) / alpha, interp(from[2], to[2], f) / alpha, alpha]; }, toCssValue: function(value) { return 'rgba(' + Math.round(value[0]) + ', ' + Math.round(value[1]) + ', ' + Math.round(value[2]) + ', ' + value[3] + ')'; }, fromCssValue: function(value) { // http://dev.w3.org/csswg/css-color/#color var out = []; var regexResult = colorHashRE.exec(value); if (regexResult) { if (value.length !== 4 && value.length !== 7) { return undefined; } var out = []; regexResult.shift(); for (var i = 0; i < 3; i++) { if (regexResult[i].length === 1) { regexResult[i] = regexResult[i] + regexResult[i]; } var v = Math.max(Math.min(parseInt(regexResult[i], 16), 255), 0); out[i] = v; } out.push(1.0); } var regexResult = colorRE.exec(value); if (regexResult) { regexResult.shift(); var type = regexResult.shift().substr(0, 3); for (var i = 0; i < 3; i++) { var m = 1; if (regexResult[i][regexResult[i].length - 1] === '%') { regexResult[i] = regexResult[i].substr(0, regexResult[i].length - 1); m = 255.0 / 100.0; } if (type === 'rgb') { out[i] = clamp(Math.round(parseInt(regexResult[i], 10) * m), 0, 255); } else { out[i] = parseInt(regexResult[i], 10); } } // Convert hsl values to rgb value if (type === 'hsl') { out = hsl2rgb.apply(null, out); } if (typeof regexResult[3] !== 'undefined') { out[3] = Math.max(Math.min(parseFloat(regexResult[3]), 1.0), 0.0); } else { out.push(1.0); } } if (out.some(isNaN)) { return undefined; } if (out.length > 0) { return out; } return namedColors[value]; } }); var convertToDeg = function(num, type) { switch (type) { case 'grad': return num / 400 * 360; case 'rad': return num / 2 / Math.PI * 360; case 'turn': return num * 360; default: return num; } }; var extractValue = function(values, pos, hasUnits) { var value = Number(values[pos]); if (!hasUnits) { return value; } var type = values[pos + 1]; if (type === '') { type = 'px'; } var result = {}; result[type] = value; return result; }; var extractValues = function(values, numValues, hasOptionalValue, hasUnits) { var result = []; for (var i = 0; i < numValues; i++) { result.push(extractValue(values, 1 + 2 * i, hasUnits)); } if (hasOptionalValue && values[1 + 2 * numValues]) { result.push(extractValue(values, 1 + 2 * numValues, hasUnits)); } return result; }; var SPACES = '\\s*'; var NUMBER = '[+-]?(?:\\d+|\\d*\\.\\d+)'; var RAW_OPEN_BRACKET = '\\('; var RAW_CLOSE_BRACKET = '\\)'; var RAW_COMMA = ','; var UNIT = '[a-zA-Z%]*'; var START = '^'; function capture(x) { return '(' + x + ')'; } function optional(x) { return '(?:' + x + ')?'; } var OPEN_BRACKET = [SPACES, RAW_OPEN_BRACKET, SPACES].join(''); var CLOSE_BRACKET = [SPACES, RAW_CLOSE_BRACKET, SPACES].join(''); var COMMA = [SPACES, RAW_COMMA, SPACES].join(''); var UNIT_NUMBER = [capture(NUMBER), capture(UNIT)].join(''); function transformRE(name, numParms, hasOptionalParm) { var tokenList = [START, SPACES, name, OPEN_BRACKET]; for (var i = 0; i < numParms - 1; i++) { tokenList.push(UNIT_NUMBER); tokenList.push(COMMA); } tokenList.push(UNIT_NUMBER); if (hasOptionalParm) { tokenList.push(optional([COMMA, UNIT_NUMBER].join(''))); } tokenList.push(CLOSE_BRACKET); return new RegExp(tokenList.join('')); } function buildMatcher(name, numValues, hasOptionalValue, hasUnits, baseValue) { var baseName = name; if (baseValue) { if (name[name.length - 1] === 'X' || name[name.length - 1] === 'Y') { baseName = name.substring(0, name.length - 1); } else if (name[name.length - 1] === 'Z') { baseName = name.substring(0, name.length - 1) + '3d'; } } var f = function(x) { var r = extractValues(x, numValues, hasOptionalValue, hasUnits); if (baseValue !== undefined) { if (name[name.length - 1] === 'X') { r.push(baseValue); } else if (name[name.length - 1] === 'Y') { r = [baseValue].concat(r); } else if (name[name.length - 1] === 'Z') { r = [baseValue, baseValue].concat(r); } else if (hasOptionalValue) { while (r.length < 2) { if (baseValue === 'copy') { r.push(r[0]); } else { r.push(baseValue); } } } } return r; }; return [transformRE(name, numValues, hasOptionalValue), f, baseName]; } function buildRotationMatcher(name, numValues, hasOptionalValue, baseValue) { var m = buildMatcher(name, numValues, hasOptionalValue, true, baseValue); var f = function(x) { var r = m[1](x); return r.map(function(v) { var result = 0; for (var type in v) { result += convertToDeg(v[type], type); } return result; }); }; return [m[0], f, m[2]]; } function build3DRotationMatcher() { var m = buildMatcher('rotate3d', 4, false, true); var f = function(x) { var r = m[1](x); var out = []; for (var i = 0; i < 3; i++) { out.push(r[i].px); } var angle = 0; for (var unit in r[3]) { angle += convertToDeg(r[3][unit], unit); } out.push(angle); return out; }; return [m[0], f, m[2]]; } var transformREs = [ buildRotationMatcher('rotate', 1, false), buildRotationMatcher('rotateX', 1, false), buildRotationMatcher('rotateY', 1, false), buildRotationMatcher('rotateZ', 1, false), build3DRotationMatcher(), buildRotationMatcher('skew', 1, true, 0), buildRotationMatcher('skewX', 1, false), buildRotationMatcher('skewY', 1, false), buildMatcher('translateX', 1, false, true, {px: 0}), buildMatcher('translateY', 1, false, true, {px: 0}), buildMatcher('translateZ', 1, false, true, {px: 0}), buildMatcher('translate', 1, true, true, {px: 0}), buildMatcher('translate3d', 3, false, true), buildMatcher('scale', 1, true, false, 'copy'), buildMatcher('scaleX', 1, false, false, 1), buildMatcher('scaleY', 1, false, false, 1), buildMatcher('scaleZ', 1, false, false, 1), buildMatcher('scale3d', 3, false, false), buildMatcher('perspective', 1, false, true), buildMatcher('matrix', 6, false, false), buildMatcher('matrix3d', 16, false, false) ]; var decomposeMatrix = (function() { // this is only ever used on the perspective matrix, which has 0, 0, 0, 1 as // last column function determinant(m) { return m[0][0] * m[1][1] * m[2][2] + m[1][0] * m[2][1] * m[0][2] + m[2][0] * m[0][1] * m[1][2] - m[0][2] * m[1][1] * m[2][0] - m[1][2] * m[2][1] * m[0][0] - m[2][2] * m[0][1] * m[1][0]; } // from Wikipedia: // // [A B]^-1 = [A^-1 + A^-1B(D - CA^-1B)^-1CA^-1 -A^-1B(D - CA^-1B)^-1] // [C D] [-(D - CA^-1B)^-1CA^-1 (D - CA^-1B)^-1 ] // // Therefore // // [A [0]]^-1 = [A^-1 [0]] // [C 1 ] [ -CA^-1 1 ] function inverse(m) { var iDet = 1 / determinant(m); var a = m[0][0], b = m[0][1], c = m[0][2]; var d = m[1][0], e = m[1][1], f = m[1][2]; var g = m[2][0], h = m[2][1], k = m[2][2]; var Ainv = [ [(e * k - f * h) * iDet, (c * h - b * k) * iDet, (b * f - c * e) * iDet, 0], [(f * g - d * k) * iDet, (a * k - c * g) * iDet, (c * d - a * f) * iDet, 0], [(d * h - e * g) * iDet, (g * b - a * h) * iDet, (a * e - b * d) * iDet, 0] ]; var lastRow = []; for (var i = 0; i < 3; i++) { var val = 0; for (var j = 0; j < 3; j++) { val += m[3][j] * Ainv[j][i]; } lastRow.push(val); } lastRow.push(1); Ainv.push(lastRow); return Ainv; } function transposeMatrix4(m) { return [[m[0][0], m[1][0], m[2][0], m[3][0]], [m[0][1], m[1][1], m[2][1], m[3][1]], [m[0][2], m[1][2], m[2][2], m[3][2]], [m[0][3], m[1][3], m[2][3], m[3][3]]]; } function multVecMatrix(v, m) { var result = []; for (var i = 0; i < 4; i++) { var val = 0; for (var j = 0; j < 4; j++) { val += v[j] * m[j][i]; } result.push(val); } return result; } function normalize(v) { var len = length(v); return [v[0] / len, v[1] / len, v[2] / len]; } function length(v) { return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); } function combine(v1, v2, v1s, v2s) { return [v1s * v1[0] + v2s * v2[0], v1s * v1[1] + v2s * v2[1], v1s * v1[2] + v2s * v2[2]]; } function cross(v1, v2) { return [v1[1] * v2[2] - v1[2] * v2[1], v1[2] * v2[0] - v1[0] * v2[2], v1[0] * v2[1] - v1[1] * v2[0]]; } // TODO: Implement 2D matrix decomposition. // http://dev.w3.org/csswg/css-transforms/#decomposing-a-2d-matrix function decomposeMatrix(matrix) { var m3d = [ matrix.slice(0, 4), matrix.slice(4, 8), matrix.slice(8, 12), matrix.slice(12, 16) ]; // skip normalization step as m3d[3][3] should always be 1 if (m3d[3][3] !== 1) { throw 'attempt to decompose non-normalized matrix'; } var perspectiveMatrix = m3d.concat(); // copy m3d for (var i = 0; i < 3; i++) { perspectiveMatrix[i][3] = 0; } if (determinant(perspectiveMatrix) === 0) { return false; } var rhs = []; var perspective; if (m3d[0][3] !== 0 || m3d[1][3] !== 0 || m3d[2][3] !== 0) { rhs.push(m3d[0][3]); rhs.push(m3d[1][3]); rhs.push(m3d[2][3]); rhs.push(m3d[3][3]); var inversePerspectiveMatrix = inverse(perspectiveMatrix); var transposedInversePerspectiveMatrix = transposeMatrix4(inversePerspectiveMatrix); perspective = multVecMatrix(rhs, transposedInversePerspectiveMatrix); } else { perspective = [0, 0, 0, 1]; } var translate = m3d[3].slice(0, 3); var row = []; row.push(m3d[0].slice(0, 3)); var scale = []; scale.push(length(row[0])); row[0] = normalize(row[0]); var skew = []; row.push(m3d[1].slice(0, 3)); skew.push(dot(row[0], row[1])); row[1] = combine(row[1], row[0], 1.0, -skew[0]); scale.push(length(row[1])); row[1] = normalize(row[1]); skew[0] /= scale[1]; row.push(m3d[2].slice(0, 3)); skew.push(dot(row[0], row[2])); row[2] = combine(row[2], row[0], 1.0, -skew[1]); skew.push(dot(row[1], row[2])); row[2] = combine(row[2], row[1], 1.0, -skew[2]); scale.push(length(row[2])); row[2] = normalize(row[2]); skew[1] /= scale[2]; skew[2] /= scale[2]; var pdum3 = cross(row[1], row[2]); if (dot(row[0], pdum3) < 0) { for (var i = 0; i < 3; i++) { scale[i] *= -1; row[i][0] *= -1; row[i][1] *= -1; row[i][2] *= -1; } } var t = row[0][0] + row[1][1] + row[2][2] + 1; var s; var quaternion; if (t > 1e-4) { s = 0.5 / Math.sqrt(t); quaternion = [ (row[2][1] - row[1][2]) * s, (row[0][2] - row[2][0]) * s, (row[1][0] - row[0][1]) * s, 0.25 / s ]; } else if (row[0][0] > row[1][1] && row[0][0] > row[2][2]) { s = Math.sqrt(1 + row[0][0] - row[1][1] - row[2][2]) * 2.0; quaternion = [ 0.25 * s, (row[0][1] + row[1][0]) / s, (row[0][2] + row[2][0]) / s, (row[2][1] - row[1][2]) / s ]; } else if (row[1][1] > row[2][2]) { s = Math.sqrt(1.0 + row[1][1] - row[0][0] - row[2][2]) * 2.0; quaternion = [ (row[0][1] + row[1][0]) / s, 0.25 * s, (row[1][2] + row[2][1]) / s, (row[0][2] - row[2][0]) / s ]; } else { s = Math.sqrt(1.0 + row[2][2] - row[0][0] - row[1][1]) * 2.0; quaternion = [ (row[0][2] + row[2][0]) / s, (row[1][2] + row[2][1]) / s, 0.25 * s, (row[1][0] - row[0][1]) / s ]; } return { translate: translate, scale: scale, skew: skew, quaternion: quaternion, perspective: perspective }; } return decomposeMatrix; })(); function dot(v1, v2) { var result = 0; for (var i = 0; i < v1.length; i++) { result += v1[i] * v2[i]; } return result; } function multiplyMatrices(a, b) { return [ a[0] * b[0] + a[4] * b[1] + a[8] * b[2] + a[12] * b[3], a[1] * b[0] + a[5] * b[1] + a[9] * b[2] + a[13] * b[3], a[2] * b[0] + a[6] * b[1] + a[10] * b[2] + a[14] * b[3], a[3] * b[0] + a[7] * b[1] + a[11] * b[2] + a[15] * b[3], a[0] * b[4] + a[4] * b[5] + a[8] * b[6] + a[12] * b[7], a[1] * b[4] + a[5] * b[5] + a[9] * b[6] + a[13] * b[7], a[2] * b[4] + a[6] * b[5] + a[10] * b[6] + a[14] * b[7], a[3] * b[4] + a[7] * b[5] + a[11] * b[6] + a[15] * b[7], a[0] * b[8] + a[4] * b[9] + a[8] * b[10] + a[12] * b[11], a[1] * b[8] + a[5] * b[9] + a[9] * b[10] + a[13] * b[11], a[2] * b[8] + a[6] * b[9] + a[10] * b[10] + a[14] * b[11], a[3] * b[8] + a[7] * b[9] + a[11] * b[10] + a[15] * b[11], a[0] * b[12] + a[4] * b[13] + a[8] * b[14] + a[12] * b[15], a[1] * b[12] + a[5] * b[13] + a[9] * b[14] + a[13] * b[15], a[2] * b[12] + a[6] * b[13] + a[10] * b[14] + a[14] * b[15], a[3] * b[12] + a[7] * b[13] + a[11] * b[14] + a[15] * b[15] ]; } function convertItemToMatrix(item) { switch (item.t) { case 'rotateX': var angle = item.d * Math.PI / 180; return [1, 0, 0, 0, 0, Math.cos(angle), Math.sin(angle), 0, 0, -Math.sin(angle), Math.cos(angle), 0, 0, 0, 0, 1]; case 'rotateY': var angle = item.d * Math.PI / 180; return [Math.cos(angle), 0, -Math.sin(angle), 0, 0, 1, 0, 0, Math.sin(angle), 0, Math.cos(angle), 0, 0, 0, 0, 1]; case 'rotate': case 'rotateZ': var angle = item.d * Math.PI / 180; return [Math.cos(angle), Math.sin(angle), 0, 0, -Math.sin(angle), Math.cos(angle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; case 'rotate3d': var x = item.d[0]; var y = item.d[1]; var z = item.d[2]; var sqrLength = x * x + y * y + z * z; if (sqrLength === 0) { x = 1; y = 0; z = 0; } else if (sqrLength !== 1) { var length = Math.sqrt(sqrLength); x /= length; y /= length; z /= length; } var s = Math.sin(item.d[3] * Math.PI / 360); var sc = s * Math.cos(item.d[3] * Math.PI / 360); var sq = s * s; return [ 1 - 2 * (y * y + z * z) * sq, 2 * (x * y * sq + z * sc), 2 * (x * z * sq - y * sc), 0, 2 * (x * y * sq - z * sc), 1 - 2 * (x * x + z * z) * sq, 2 * (y * z * sq + x * sc), 0, 2 * (x * z * sq + y * sc), 2 * (y * z * sq - x * sc), 1 - 2 * (x * x + y * y) * sq, 0, 0, 0, 0, 1 ]; case 'scale': return [item.d[0], 0, 0, 0, 0, item.d[1], 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; case 'scale3d': return [item.d[0], 0, 0, 0, 0, item.d[1], 0, 0, 0, 0, item.d[2], 0, 0, 0, 0, 1]; case 'skew': return [1, Math.tan(item.d[1] * Math.PI / 180), 0, 0, Math.tan(item.d[0] * Math.PI / 180), 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; case 'skewX': return [1, 0, 0, 0, Math.tan(item.d * Math.PI / 180), 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; case 'skewY': return [1, Math.tan(item.d * Math.PI / 180), 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; // TODO: Work out what to do with non-px values. case 'translate': return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, item.d[0].px, item.d[1].px, 0, 1]; case 'translate3d': return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, item.d[0].px, item.d[1].px, item.d[2].px, 1]; case 'perspective': return [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1 / item.d.px, 0, 0, 0, 1]; case 'matrix': return [item.d[0], item.d[1], 0, 0, item.d[2], item.d[3], 0, 0, 0, 0, 1, 0, item.d[4], item.d[5], 0, 1]; case 'matrix3d': return item.d; default: ASSERT_ENABLED && assert(false, 'Transform item type ' + item.t + ' conversion to matrix not yet implemented.'); } } function convertToMatrix(transformList) { if (transformList.length === 0) { return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; } return transformList.map(convertItemToMatrix).reduce(multiplyMatrices); } var composeMatrix = (function() { function multiply(a, b) { var result = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (var i = 0; i < 4; i++) { for (var j = 0; j < 4; j++) { for (var k = 0; k < 4; k++) { result[i][j] += b[i][k] * a[k][j]; } } } return result; } function is2D(m) { return ( m[0][2] == 0 && m[0][3] == 0 && m[1][2] == 0 && m[1][3] == 0 && m[2][0] == 0 && m[2][1] == 0 && m[2][2] == 1 && m[2][3] == 0 && m[3][2] == 0 && m[3][3] == 1); } function composeMatrix(translate, scale, skew, quat, perspective) { var matrix = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]; for (var i = 0; i < 4; i++) { matrix[i][3] = perspective[i]; } for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { matrix[3][i] += translate[j] * matrix[j][i]; } } var x = quat[0], y = quat[1], z = quat[2], w = quat[3]; var rotMatrix = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]; rotMatrix[0][0] = 1 - 2 * (y * y + z * z); rotMatrix[0][1] = 2 * (x * y - z * w); rotMatrix[0][2] = 2 * (x * z + y * w); rotMatrix[1][0] = 2 * (x * y + z * w); rotMatrix[1][1] = 1 - 2 * (x * x + z * z); rotMatrix[1][2] = 2 * (y * z - x * w); rotMatrix[2][0] = 2 * (x * z - y * w); rotMatrix[2][1] = 2 * (y * z + x * w); rotMatrix[2][2] = 1 - 2 * (x * x + y * y); matrix = multiply(matrix, rotMatrix); var temp = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]; if (skew[2]) { temp[2][1] = skew[2]; matrix = multiply(matrix, temp); } if (skew[1]) { temp[2][1] = 0; temp[2][0] = skew[0]; matrix = multiply(matrix, temp); } if (skew[0]) { temp[2][0] = 0; temp[1][0] = skew[0]; matrix = multiply(matrix, temp); } for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { matrix[i][j] *= scale[i]; } } if (is2D(matrix)) { return { t: 'matrix', d: [matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1], matrix[3][0], matrix[3][1]] }; } return { t: 'matrix3d', d: matrix[0].concat(matrix[1], matrix[2], matrix[3]) }; } return composeMatrix; })(); function interpolateDecomposedTransformsWithMatrices(fromM, toM, f) { var product = dot(fromM.quaternion, toM.quaternion); product = clamp(product, -1.0, 1.0); var quat = []; if (product === 1.0) { quat = fromM.quaternion; } else { var theta = Math.acos(product); var w = Math.sin(f * theta) * 1 / Math.sqrt(1 - product * product); for (var i = 0; i < 4; i++) { quat.push(fromM.quaternion[i] * (Math.cos(f * theta) - product * w) + toM.quaternion[i] * w); } } var translate = interp(fromM.translate, toM.translate, f); var scale = interp(fromM.scale, toM.scale, f); var skew = interp(fromM.skew, toM.skew, f); var perspective = interp(fromM.perspective, toM.perspective, f); return composeMatrix(translate, scale, skew, quat, perspective); } function interpTransformValue(from, to, f) { var type = from.t ? from.t : to.t; switch (type) { case 'matrix': case 'matrix3d': ASSERT_ENABLED && assert(false, 'Must use matrix decomposition when interpolating raw matrices'); // Transforms with unitless parameters. case 'rotate': case 'rotateX': case 'rotateY': case 'rotateZ': case 'rotate3d': case 'scale': case 'scaleX': case 'scaleY': case 'scaleZ': case 'scale3d': case 'skew': case 'skewX': case 'skewY': return {t: type, d: interp(from.d, to.d, f, type)}; default: // Transforms with lengthType parameters. var result = []; var maxVal; if (from.d && to.d) { maxVal = Math.max(from.d.length, to.d.length); } else if (from.d) { maxVal = from.d.length; } else { maxVal = to.d.length; } for (var j = 0; j < maxVal; j++) { var fromVal = from.d ? from.d[j] : {}; var toVal = to.d ? to.d[j] : {}; result.push(lengthType.interpolate(fromVal, toVal, f)); } return {t: type, d: result}; } } function isMatrix(item) { return item.t[0] === 'm'; } // The CSSWG decided to disallow scientific notation in CSS property strings // (see http://lists.w3.org/Archives/Public/www-style/2010Feb/0050.html). // We need this function to hakonitize all numbers before adding them to // property strings. // TODO: Apply this function to all property strings function n(num) { return Number(num).toFixed(4); } var transformType = { add: function(base, delta) { return base.concat(delta); }, interpolate: function(from, to, f) { var out = []; for (var i = 0; i < Math.min(from.length, to.length); i++) { if (from[i].t !== to[i].t || isMatrix(from[i])) { break; } out.push(interpTransformValue(from[i], to[i], f)); } if (i < Math.min(from.length, to.length) || from.some(isMatrix) || to.some(isMatrix)) { if (from.decompositionPair !== to) { from.decompositionPair = to; from.decomposition = decomposeMatrix(convertToMatrix(from.slice(i))); } if (to.decompositionPair !== from) { to.decompositionPair = from; to.decomposition = decomposeMatrix(convertToMatrix(to.slice(i))); } out.push(interpolateDecomposedTransformsWithMatrices( from.decomposition, to.decomposition, f)); return out; } for (; i < from.length; i++) { out.push(interpTransformValue(from[i], {t: null, d: null}, f)); } for (; i < to.length; i++) { out.push(interpTransformValue({t: null, d: null}, to[i], f)); } return out; }, toCssValue: function(value, svgMode) { // TODO: fix this :) var out = ''; for (var i = 0; i < value.length; i++) { ASSERT_ENABLED && assert( value[i].t, 'transform type should be resolved by now'); switch (value[i].t) { case 'rotate': case 'rotateX': case 'rotateY': case 'rotateZ': case 'skewX': case 'skewY': var unit = svgMode ? '' : 'deg'; out += value[i].t + '(' + value[i].d + unit + ') '; break; case 'skew': var unit = svgMode ? '' : 'deg'; out += value[i].t + '(' + value[i].d[0] + unit; if (value[i].d[1] === 0) { out += ') '; } else { out += ', ' + value[i].d[1] + unit + ') '; } break; case 'rotate3d': var unit = svgMode ? '' : 'deg'; out += value[i].t + '(' + value[i].d[0] + ', ' + value[i].d[1] + ', ' + value[i].d[2] + ', ' + value[i].d[3] + unit + ') '; break; case 'translateX': case 'translateY': case 'translateZ': case 'perspective': out += value[i].t + '(' + lengthType.toCssValue(value[i].d[0]) + ') '; break; case 'translate': if (svgMode) { if (value[i].d[1] === undefined) { out += value[i].t + '(' + value[i].d[0].px + ') '; } else { out += ( value[i].t + '(' + value[i].d[0].px + ', ' + value[i].d[1].px + ') '); } break; } if (value[i].d[1] === undefined) { out += value[i].t + '(' + lengthType.toCssValue(value[i].d[0]) + ') '; } else { out += value[i].t + '(' + lengthType.toCssValue(value[i].d[0]) + ', ' + lengthType.toCssValue(value[i].d[1]) + ') '; } break; case 'translate3d': var values = value[i].d.map(lengthType.toCssValue); out += value[i].t + '(' + values[0] + ', ' + values[1] + ', ' + values[2] + ') '; break; case 'scale': if (value[i].d[0] === value[i].d[1]) { out += value[i].t + '(' + value[i].d[0] + ') '; } else { out += value[i].t + '(' + value[i].d[0] + ', ' + value[i].d[1] + ') '; } break; case 'scaleX': case 'scaleY': case 'scaleZ': out += value[i].t + '(' + value[i].d[0] + ') '; break; case 'scale3d': out += value[i].t + '(' + value[i].d[0] + ', ' + value[i].d[1] + ', ' + value[i].d[2] + ') '; break; case 'matrix': case 'matrix3d': out += value[i].t + '(' + value[i].d.map(n).join(', ') + ') '; break; } } return out.substring(0, out.length - 1); }, fromCssValue: function(value) { // TODO: fix this :) if (value === undefined) { return undefined; } var result = []; while (value.length > 0) { var r; for (var i = 0; i < transformREs.length; i++) { var reSpec = transformREs[i]; r = reSpec[0].exec(value); if (r) { result.push({t: reSpec[2], d: reSpec[1](r)}); value = value.substring(r[0].length); break; } } if (!isDefinedAndNotNull(r)) { return result; } } return result; } }; var propertyTypes = { backgroundColor: colorType, backgroundPosition: positionListType, borderBottomColor: colorType, borderBottomLeftRadius: percentLengthType, borderBottomRightRadius: percentLengthType, borderBottomWidth: lengthType, borderLeftColor: colorType, borderLeftWidth: lengthType, borderRightColor: colorType, borderRightWidth: lengthType, borderSpacing: lengthType, borderTopColor: colorType, borderTopLeftRadius: percentLengthType, borderTopRightRadius: percentLengthType, borderTopWidth: lengthType, bottom: percentLengthAutoType, boxShadow: shadowType, clip: typeWithKeywords(['auto'], rectangleType), color: colorType, cx: lengthType, cy: lengthType, dx: lengthType, dy: lengthType, fill: colorType, floodColor: colorType, // TODO: Handle these keywords properly. fontSize: typeWithKeywords(['smaller', 'larger'], percentLengthType), fontWeight: typeWithKeywords(['lighter', 'bolder'], fontWeightType), height: percentLengthAutoType, left: percentLengthAutoType, letterSpacing: typeWithKeywords(['normal'], lengthType), lightingColor: colorType, lineHeight: percentLengthType, // TODO: Should support numberType as well. marginBottom: lengthAutoType, marginLeft: lengthAutoType, marginRight: lengthAutoType, marginTop: lengthAutoType, maxHeight: typeWithKeywords( ['none', 'max-content', 'min-content', 'fill-available', 'fit-content'], percentLengthType), maxWidth: typeWithKeywords( ['none', 'max-content', 'min-content', 'fill-available', 'fit-content'], percentLengthType), minHeight: typeWithKeywords( ['max-content', 'min-content', 'fill-available', 'fit-content'], percentLengthType), minWidth: typeWithKeywords( ['max-content', 'min-content', 'fill-available', 'fit-content'], percentLengthType), opacity: numberType, outlineColor: typeWithKeywords(['invert'], colorType), outlineOffset: lengthType, outlineWidth: lengthType, paddingBottom: lengthType, paddingLeft: lengthType, paddingRight: lengthType, paddingTop: lengthType, perspective: typeWithKeywords(['none'], lengthType), perspectiveOrigin: originType, r: lengthType, right: percentLengthAutoType, stopColor: colorType, stroke: colorType, textIndent: typeWithKeywords(['each-line', 'hanging'], percentLengthType), textShadow: shadowType, top: percentLengthAutoType, transform: transformType, transformOrigin: originType, verticalAlign: typeWithKeywords([ 'baseline', 'sub', 'super', 'text-top', 'text-bottom', 'middle', 'top', 'bottom' ], percentLengthType), visibility: visibilityType, width: typeWithKeywords([ 'border-box', 'content-box', 'auto', 'max-content', 'min-content', 'available', 'fit-content' ], percentLengthType), wordSpacing: typeWithKeywords(['normal'], percentLengthType), x: lengthType, y: lengthType, zIndex: typeWithKeywords(['auto'], integerType) }; var svgProperties = { 'cx': 1, 'cy': 1, 'dx': 1, 'dy': 1, 'fill': 1, 'floodColor': 1, 'height': 1, 'lightingColor': 1, 'r': 1, 'stopColor': 1, 'stroke': 1, 'width': 1, 'x': 1, 'y': 1 }; var borderWidthAliases = { initial: '3px', thin: '1px', medium: '3px', thick: '5px' }; var propertyValueAliases = { backgroundColor: { initial: 'transparent' }, backgroundPosition: { initial: '0% 0%' }, borderBottomColor: { initial: 'currentColor' }, borderBottomLeftRadius: { initial: '0px' }, borderBottomRightRadius: { initial: '0px' }, borderBottomWidth: borderWidthAliases, borderLeftColor: { initial: 'currentColor' }, borderLeftWidth: borderWidthAliases, borderRightColor: { initial: 'currentColor' }, borderRightWidth: borderWidthAliases, // Spec says this should be 0 but in practise it is 2px. borderSpacing: { initial: '2px' }, borderTopColor: { initial: 'currentColor' }, borderTopLeftRadius: { initial: '0px' }, borderTopRightRadius: { initial: '0px' }, borderTopWidth: borderWidthAliases, bottom: { initial: 'auto' }, clip: { initial: 'rect(0px, 0px, 0px, 0px)' }, color: { initial: 'black' }, // Depends on user agent. fontSize: { initial: '100%', 'xx-small': '60%', 'x-small': '75%', 'small': '89%', 'medium': '100%', 'large': '120%', 'x-large': '150%', 'xx-large': '200%' }, fontWeight: { initial: '400', normal: '400', bold: '700' }, height: { initial: 'auto' }, left: { initial: 'auto' }, letterSpacing: { initial: 'normal' }, lineHeight: { initial: '120%', normal: '120%' }, marginBottom: { initial: '0px' }, marginLeft: { initial: '0px' }, marginRight: { initial: '0px' }, marginTop: { initial: '0px' }, maxHeight: { initial: 'none' }, maxWidth: { initial: 'none' }, minHeight: { initial: '0px' }, minWidth: { initial: '0px' }, opacity: { initial: '1.0' }, outlineColor: { initial: 'invert' }, outlineOffset: { initial: '0px' }, outlineWidth: borderWidthAliases, paddingBottom: { initial: '0px' }, paddingLeft: { initial: '0px' }, paddingRight: { initial: '0px' }, paddingTop: { initial: '0px' }, right: { initial: 'auto' }, textIndent: { initial: '0px' }, textShadow: { initial: '0px 0px 0px transparent', none: '0px 0px 0px transparent' }, top: { initial: 'auto' }, transform: { initial: '', none: '' }, verticalAlign: { initial: '0px' }, visibility: { initial: 'visible' }, width: { initial: 'auto' }, wordSpacing: { initial: 'normal' }, zIndex: { initial: 'auto' } }; var propertyIsSVGAttrib = function(property, target) { return target.namespaceURI === 'http://www.w3.org/2000/svg' && property in svgProperties; }; var getType = function(property) { return propertyTypes[property] || nonNumericType; }; var add = function(property, base, delta) { if (delta === rawNeutralValue) { return base; } if (base === 'inherit' || delta === 'inherit') { return nonNumericType.add(base, delta); } return getType(property).add(base, delta); }; /** * Interpolate the given property name (f*100)% of the way from 'from' to 'to'. * 'from' and 'to' are both raw values already converted from CSS value * strings. Requires the target element to be able to determine whether the * given property is an SVG attribute or not, as this impacts the conversion of * the interpolated value back into a CSS value string for transform * translations. * * e.g. interpolate('transform', elem, 'rotate(40deg)', 'rotate(50deg)', 0.3); * will return 'rotate(43deg)'. */ var interpolate = function(property, from, to, f) { ASSERT_ENABLED && assert( isDefinedAndNotNull(from) && isDefinedAndNotNull(to), 'Both to and from values should be specified for interpolation'); if (from === 'inherit' || to === 'inherit') { return nonNumericType.interpolate(from, to, f); } if (f === 0) { return from; } if (f === 1) { return to; } return getType(property).interpolate(from, to, f); }; /** * Convert the provided interpolable value for the provided property to a CSS * value string. Note that SVG transforms do not require units for translate * or rotate values while CSS properties require 'px' or 'deg' units. */ var toCssValue = function(property, value, svgMode) { if (value === 'inherit') { return value; } return getType(property).toCssValue(value, svgMode); }; var fromCssValue = function(property, value) { if (value === cssNeutralValue) { return rawNeutralValue; } if (value === 'inherit') { return value; } if (property in propertyValueAliases && value in propertyValueAliases[property]) { value = propertyValueAliases[property][value]; } var result = getType(property).fromCssValue(value); // Currently we'll hit this assert if input to the API is bad. To avoid this, // we should eliminate invalid values when normalizing the list of keyframes. // See the TODO in isSupportedPropertyValue(). ASSERT_ENABLED && assert(isDefinedAndNotNull(result), 'Invalid property value "' + value + '" for property "' + property + '"'); return result; }; // Sentinel values var cssNeutralValue = {}; var rawNeutralValue = {}; /** @constructor */ var CompositableValue = function() { }; CompositableValue.prototype = { compositeOnto: abstractMethod, // This is purely an optimization. dependsOnUnderlyingValue: function() { return true; } }; /** @constructor */ var AddReplaceCompositableValue = function(value, composite) { this.value = value; this.composite = composite; ASSERT_ENABLED && assert( !(this.value === cssNeutralValue && this.composite === 'replace'), 'Should never replace-composite the neutral value'); }; AddReplaceCompositableValue.prototype = createObject( CompositableValue.prototype, { compositeOnto: function(property, underlyingValue) { switch (this.composite) { case 'replace': return this.value; case 'add': return add(property, underlyingValue, this.value); default: ASSERT_ENABLED && assert( false, 'Invalid composite operation ' + this.composite); } }, dependsOnUnderlyingValue: function() { return this.composite === 'add'; } }); /** @constructor */ var BlendedCompositableValue = function(startValue, endValue, fraction) { this.startValue = startValue; this.endValue = endValue; this.fraction = fraction; }; BlendedCompositableValue.prototype = createObject( CompositableValue.prototype, { compositeOnto: function(property, underlyingValue) { return interpolate(property, this.startValue.compositeOnto(property, underlyingValue), this.endValue.compositeOnto(property, underlyingValue), this.fraction); }, dependsOnUnderlyingValue: function() { // Travis crashes here randomly in Chrome beta and unstable, // this try catch is to help debug the problem. try { return this.startValue.dependsOnUnderlyingValue() || this.endValue.dependsOnUnderlyingValue(); } catch (error) { throw new Error( error + '\n JSON.stringify(this) = ' + JSON.stringify(this)); } } }); /** @constructor */ var CompositedPropertyMap = function(target) { this.properties = {}; this.baseValues = {}; this.target = target; }; CompositedPropertyMap.prototype = { addValue: function(property, animValue) { if (!(property in this.properties)) { this.properties[property] = []; } if (!(animValue instanceof CompositableValue)) { throw new TypeError('expected CompositableValue'); } this.properties[property].push(animValue); }, stackDependsOnUnderlyingValue: function(stack) { for (var i = 0; i < stack.length; i++) { if (!stack[i].dependsOnUnderlyingValue()) { return false; } } return true; }, clear: function() { for (var property in this.properties) { if (this.stackDependsOnUnderlyingValue(this.properties[property])) { clearValue(this.target, property); } } }, captureBaseValues: function() { for (var property in this.properties) { var stack = this.properties[property]; if (stack.length > 0 && this.stackDependsOnUnderlyingValue(stack)) { var baseValue = fromCssValue(property, getValue(this.target, property)); // TODO: Decide what to do with elements not in the DOM. ASSERT_ENABLED && assert( isDefinedAndNotNull(baseValue) && baseValue !== '', 'Base value should always be set. ' + 'Is the target element in the DOM?'); this.baseValues[property] = baseValue; } else { this.baseValues[property] = undefined; } } }, applyAnimatedValues: function() { for (var property in this.properties) { var valuesToComposite = this.properties[property]; if (valuesToComposite.length === 0) { continue; } var baseValue = this.baseValues[property]; var i = valuesToComposite.length - 1; while (i > 0 && valuesToComposite[i].dependsOnUnderlyingValue()) { i--; } for (; i < valuesToComposite.length; i++) { baseValue = valuesToComposite[i].compositeOnto(property, baseValue); } ASSERT_ENABLED && assert( isDefinedAndNotNull(baseValue) && baseValue !== '', 'Value should always be set after compositing'); var isSvgMode = propertyIsSVGAttrib(property, this.target); setValue(this.target, property, toCssValue(property, baseValue, isSvgMode)); this.properties[property] = []; } } }; var cssStyleDeclarationAttribute = { cssText: true, length: true, parentRule: true, 'var': true }; var cssStyleDeclarationMethodModifiesStyle = { getPropertyValue: false, getPropertyCSSValue: false, removeProperty: true, getPropertyPriority: false, setProperty: true, item: false }; var copyInlineStyle = function(sourceStyle, destinationStyle) { for (var i = 0; i < sourceStyle.length; i++) { var property = sourceStyle[i]; destinationStyle[property] = sourceStyle[property]; } }; var retickThenGetComputedStyle = function() { repeatLastTick(); ensureOriginalGetComputedStyle(); return window.getComputedStyle.apply(this, arguments); }; // This redundant flag is to support Safari which has trouble determining // function object equality during an animation. var isGetComputedStylePatched = false; var originalGetComputedStyle = window.getComputedStyle; var ensureRetickBeforeGetComputedStyle = function() { if (!isGetComputedStylePatched) { Object.defineProperty(window, 'getComputedStyle', configureDescriptor({ value: retickThenGetComputedStyle })); isGetComputedStylePatched = true; } }; var ensureOriginalGetComputedStyle = function() { if (isGetComputedStylePatched) { Object.defineProperty(window, 'getComputedStyle', configureDescriptor({ value: originalGetComputedStyle })); isGetComputedStylePatched = false; } }; // Changing the inline style of an element under animation may require the // animation to be recomputed ontop of the new inline style if // getComputedStyle() is called inbetween setting the style and the next // animation frame. // We modify getComputedStyle() to re-evaluate the animations only if it is // called instead of re-evaluating them here potentially unnecessarily. var animatedInlineStyleChanged = function() { maybeRestartAnimation(); ensureRetickBeforeGetComputedStyle(); }; /** @constructor */ var AnimatedCSSStyleDeclaration = function(element) { ASSERT_ENABLED && assert( !(element.style instanceof AnimatedCSSStyleDeclaration), 'Element must not already have an animated style attached.'); // Stores the inline style of the element on its behalf while the // polyfill uses the element's inline style to simulate web animations. // This is needed to fake regular inline style CSSOM access on the element. this._surrogateElement = createDummyElement(); this._style = element.style; this._length = 0; this._isAnimatedProperty = {}; // Populate the surrogate element's inline style. copyInlineStyle(this._style, this._surrogateElement.style); this._updateIndices(); }; AnimatedCSSStyleDeclaration.prototype = { get cssText() { return this._surrogateElement.style.cssText; }, set cssText(text) { var isAffectedProperty = {}; for (var i = 0; i < this._surrogateElement.style.length; i++) { isAffectedProperty[this._surrogateElement.style[i]] = true; } this._surrogateElement.style.cssText = text; this._updateIndices(); for (var i = 0; i < this._surrogateElement.style.length; i++) { isAffectedProperty[this._surrogateElement.style[i]] = true; } for (var property in isAffectedProperty) { if (!this._isAnimatedProperty[property]) { this._style.setProperty(property, this._surrogateElement.style.getPropertyValue(property)); } } animatedInlineStyleChanged(); }, get length() { return this._surrogateElement.style.length; }, get parentRule() { return this._style.parentRule; }, get 'var'() { return this._style.var; }, _updateIndices: function() { while (this._length < this._surrogateElement.style.length) { Object.defineProperty(this, this._length, { configurable: true, enumerable: false, get: (function(index) { return function() { return this._surrogateElement.style[index]; }; })(this._length) }); this._length++; } while (this._length > this._surrogateElement.style.length) { this._length--; Object.defineProperty(this, this._length, { configurable: true, enumerable: false, value: undefined }); } }, _clearAnimatedProperty: function(property) { this._style[property] = this._surrogateElement.style[property]; this._isAnimatedProperty[property] = false; }, _setAnimatedProperty: function(property, value) { this._style[property] = value; this._isAnimatedProperty[property] = true; } }; for (var method in cssStyleDeclarationMethodModifiesStyle) { AnimatedCSSStyleDeclaration.prototype[method] = (function(method, modifiesStyle) { return function() { var result = this._surrogateElement.style[method].apply( this._surrogateElement.style, arguments); if (modifiesStyle) { if (!this._isAnimatedProperty[arguments[0]]) { this._style[method].apply(this._style, arguments); } this._updateIndices(); animatedInlineStyleChanged(); } return result; } })(method, cssStyleDeclarationMethodModifiesStyle[method]); } for (var property in document.documentElement.style) { if (cssStyleDeclarationAttribute[property] || property in cssStyleDeclarationMethodModifiesStyle) { continue; } (function(property) { Object.defineProperty(AnimatedCSSStyleDeclaration.prototype, property, configureDescriptor({ get: function() { return this._surrogateElement.style[property]; }, set: function(value) { this._surrogateElement.style[property] = value; this._updateIndices(); if (!this._isAnimatedProperty[property]) { this._style[property] = value; } animatedInlineStyleChanged(); } })); })(property); } // This function is a fallback for when we can't replace an element's style with // AnimatatedCSSStyleDeclaration and must patch the existing style to behave // in a similar way. // Only the methods listed in cssStyleDeclarationMethodModifiesStyle will // be patched to behave in the same manner as a native implementation, // getter properties like style.left or style[0] will be tainted by the // polyfill's animation engine. var patchInlineStyleForAnimation = function(style) { var surrogateElement = document.createElement('div'); copyInlineStyle(style, surrogateElement.style); var isAnimatedProperty = {}; for (var method in cssStyleDeclarationMethodModifiesStyle) { if (!(method in style)) { continue; } Object.defineProperty(style, method, configureDescriptor({ value: (function(method, originalMethod, modifiesStyle) { return function() { var result = surrogateElement.style[method].apply( surrogateElement.style, arguments); if (modifiesStyle) { if (!isAnimatedProperty[arguments[0]]) { originalMethod.apply(style, arguments); } animatedInlineStyleChanged(); } return result; } })(method, style[method], cssStyleDeclarationMethodModifiesStyle[method]) })); } style._clearAnimatedProperty = function(property) { this[property] = surrogateElement.style[property]; isAnimatedProperty[property] = false; }; style._setAnimatedProperty = function(property, value) { this[property] = value; isAnimatedProperty[property] = true; }; }; /** @constructor */ var Compositor = function() { this.targets = []; }; Compositor.prototype = { setAnimatedValue: function(target, property, animValue) { if (target !== null) { if (target._animProperties === undefined) { target._animProperties = new CompositedPropertyMap(target); this.targets.push(target); } target._animProperties.addValue(property, animValue); } }, applyAnimatedValues: function() { for (var i = 0; i < this.targets.length; i++) { this.targets[i]._animProperties.clear(); } for (var i = 0; i < this.targets.length; i++) { this.targets[i]._animProperties.captureBaseValues(); } for (var i = 0; i < this.targets.length; i++) { this.targets[i]._animProperties.applyAnimatedValues(); } } }; var ensureTargetInitialised = function(property, target) { if (propertyIsSVGAttrib(property, target)) { ensureTargetSVGInitialised(property, target); } else { ensureTargetCSSInitialised(target); } }; var ensureTargetSVGInitialised = function(property, target) { if (!isDefinedAndNotNull(target._actuals)) { target._actuals = {}; target._bases = {}; target.actuals = {}; target._getAttribute = target.getAttribute; target._setAttribute = target.setAttribute; target.getAttribute = function(name) { if (isDefinedAndNotNull(target._bases[name])) { return target._bases[name]; } return target._getAttribute(name); }; target.setAttribute = function(name, value) { if (isDefinedAndNotNull(target._actuals[name])) { target._bases[name] = value; } else { target._setAttribute(name, value); } }; } if (!isDefinedAndNotNull(target._actuals[property])) { var baseVal = target.getAttribute(property); target._actuals[property] = 0; target._bases[property] = baseVal; Object.defineProperty(target.actuals, property, configureDescriptor({ set: function(value) { if (value === null) { target._actuals[property] = target._bases[property]; target._setAttribute(property, target._bases[property]); } else { target._actuals[property] = value; target._setAttribute(property, value); } }, get: function() { return target._actuals[property]; } })); } }; var ensureTargetCSSInitialised = function(target) { if (target.style._webAnimationsStyleInitialised) { return; } try { var animatedStyle = new AnimatedCSSStyleDeclaration(target); Object.defineProperty(target, 'style', configureDescriptor({ get: function() { return animatedStyle; } })); } catch (error) { patchInlineStyleForAnimation(target.style); } target.style._webAnimationsStyleInitialised = true; }; var setValue = function(target, property, value) { ensureTargetInitialised(property, target); property = prefixProperty(property); if (propertyIsSVGAttrib(property, target)) { target.actuals[property] = value; } else { target.style._setAnimatedProperty(property, value); } }; var clearValue = function(target, property) { ensureTargetInitialised(property, target); property = prefixProperty(property); if (propertyIsSVGAttrib(property, target)) { target.actuals[property] = null; } else { target.style._clearAnimatedProperty(property); } }; var getValue = function(target, property) { ensureTargetInitialised(property, target); property = prefixProperty(property); if (propertyIsSVGAttrib(property, target)) { return target.actuals[property]; } else { return getComputedStyle(target)[property]; } }; var rafScheduled = false; var compositor = new Compositor(); var usePerformanceTiming = typeof window.performance === 'object' && typeof window.performance.timing === 'object' && typeof window.performance.now === 'function'; // Don't use a local named requestAnimationFrame, to avoid potential problems // with hoisting. var nativeRaf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; var raf; if (nativeRaf) { raf = function(callback) { nativeRaf(function() { callback(clockMillis()); }); }; } else { raf = function(callback) { setTimeout(function() { callback(clockMillis()); }, 1000 / 60); }; } var clockMillis = function() { return usePerformanceTiming ? window.performance.now() : Date.now(); }; // Set up the zero times for document time. Document time is relative to the // document load event. var documentTimeZeroAsRafTime; var documentTimeZeroAsClockTime; var load; if (usePerformanceTiming) { load = function() { // RAF time is relative to the navigationStart event. documentTimeZeroAsRafTime = window.performance.timing.loadEventStart - window.performance.timing.navigationStart; // performance.now() uses the same origin as RAF time. documentTimeZeroAsClockTime = documentTimeZeroAsRafTime; }; } else { // The best approximation we have for the relevant clock and RAF times is to // listen to the load event. load = function() { raf(function(rafTime) { documentTimeZeroAsRafTime = rafTime; }); documentTimeZeroAsClockTime = Date.now(); }; } // Start timing when load event fires or if this script is processed when // document loading is already complete. if (document.readyState === 'complete') { // When performance timing is unavailable and this script is loaded // dynamically, document zero time is incorrect. // Warn the user in this case. if (!usePerformanceTiming) { console.warn( 'Web animations can\'t discover document zero time when ' + 'asynchronously loaded in the absence of performance timing.'); } load(); } else { addEventListener('load', function() { load(); if (usePerformanceTiming) { // We use setTimeout() to clear cachedClockTimeMillis at the end of a // frame, but this will not run until after other load handlers. We need // those handlers to pick up the new value of clockMillis(), so we must // clear the cached value. cachedClockTimeMillis = undefined; } }); } // A cached document time for use during the current callstack. var cachedClockTimeMillis; // Calculates one time relative to another, returning null if the zero time is // undefined. var relativeTime = function(time, zeroTime) { return isDefined(zeroTime) ? time - zeroTime : null; }; var lastClockTimeMillis; var cachedClockTime = function() { // Cache a document time for the remainder of this callstack. if (!isDefined(cachedClockTimeMillis)) { cachedClockTimeMillis = clockMillis(); lastClockTimeMillis = cachedClockTimeMillis; setTimeout(function() { cachedClockTimeMillis = undefined; }, 0); } return cachedClockTimeMillis; }; // These functions should be called in every stack that could possibly modify // the effect results that have already been calculated for the current tick. var modifyCurrentAnimationStateDepth = 0; var enterModifyCurrentAnimationState = function() { modifyCurrentAnimationStateDepth++; }; var exitModifyCurrentAnimationState = function(updateCallback) { modifyCurrentAnimationStateDepth--; // updateCallback is set to null when we know we can't possibly affect the // current state (eg. a TimedItem which is not attached to a player). We track // the depth of recursive calls trigger just one repeat per entry. Only the // updateCallback from the outermost call is considered, this allows certain // locatations (eg. constructors) to override nested calls that would // otherwise set updateCallback unconditionally. if (modifyCurrentAnimationStateDepth === 0 && updateCallback) { updateCallback(); } }; var repeatLastTick = function() { if (isDefined(lastTickTime)) { ticker(lastTickTime, true); } }; var playerSortFunction = function(a, b) { var result = a.startTime - b.startTime; return result !== 0 ? result : a._sequenceNumber - b._sequenceNumber; }; var lastTickTime; var ticker = function(rafTime, isRepeat) { // Don't tick till the page is loaded.... if (!isDefined(documentTimeZeroAsRafTime)) { raf(ticker); return; } if (!isRepeat) { if (rafTime < lastClockTimeMillis) { rafTime = lastClockTimeMillis; } lastTickTime = rafTime; cachedClockTimeMillis = rafTime; } // Clear any modifications to getComputedStyle. ensureOriginalGetComputedStyle(); // Get animations for this sample. We order by AnimationPlayer then by DFS // order within each AnimationPlayer's tree. if (!playersAreSorted) { PLAYERS.sort(playerSortFunction); playersAreSorted = true; } var finished = true; var paused = true; var animations = []; var finishedPlayers = []; PLAYERS.forEach(function(player) { player._update(); finished = finished && !player._hasFutureAnimation(); if (!player._hasFutureEffect()) { finishedPlayers.push(player); } paused = paused && player.paused; player._getLeafItemsInEffect(animations); }); // Apply animations in order for (var i = 0; i < animations.length; i++) { if (animations[i] instanceof Animation) { animations[i]._sample(); } } // Generate events PLAYERS.forEach(function(player) { player._generateEvents(); }); // Remove finished players. Warning: _deregisterFromTimeline modifies // the PLAYER list. It should not be called from within a PLAYERS.forEach // loop directly. finishedPlayers.forEach(function(player) { player._deregisterFromTimeline(); playersAreSorted = false; }); // Composite animated values into element styles compositor.applyAnimatedValues(); if (!isRepeat) { if (finished || paused) { rafScheduled = false; } else { raf(ticker); } cachedClockTimeMillis = undefined; } }; // Multiplication where zero multiplied by any value (including infinity) // gives zero. var multiplyZeroGivesZero = function(a, b) { return (a === 0 || b === 0) ? 0 : a * b; }; var maybeRestartAnimation = function() { if (rafScheduled) { return; } raf(ticker); rafScheduled = true; }; var DOCUMENT_TIMELINE = new AnimationTimeline(constructorToken); // attempt to override native implementation try { Object.defineProperty(document, 'timeline', { configurable: true, get: function() { return DOCUMENT_TIMELINE } }); } catch (e) { } // maintain support for Safari try { document.timeline = DOCUMENT_TIMELINE; } catch (e) { } window.Element.prototype.animate = function(effect, timing) { var anim = new Animation(this, effect, timing); DOCUMENT_TIMELINE.play(anim); return anim.player; }; window.Element.prototype.getCurrentPlayers = function() { return PLAYERS.filter((function(player) { return player._isCurrent() && player._isTargetingElement(this); }).bind(this)); }; window.Element.prototype.getCurrentAnimations = function() { var animations = []; PLAYERS.forEach((function(player) { if (player._isCurrent()) { player._getAnimationsTargetingElement(this, animations); } }).bind(this)); return animations; }; window.Animation = Animation; window.AnimationEffect = AnimationEffect; window.AnimationGroup = AnimationGroup; window.AnimationPlayer = AnimationPlayer; window.AnimationSequence = AnimationSequence; window.AnimationTimeline = AnimationTimeline; window.KeyframeEffect = KeyframeEffect; window.MediaReference = MediaReference; window.MotionPathEffect = MotionPathEffect; window.PseudoElementReference = PseudoElementReference; window.TimedItem = TimedItem; window.TimedItemList = TimedItemList; window.Timing = Timing; window.TimingEvent = TimingEvent; window.TimingGroup = TimingGroup; window._WebAnimationsTestingUtilities = { _constructorToken: constructorToken, _deprecated: deprecated, _positionListType: positionListType, _hsl2rgb: hsl2rgb, _types: propertyTypes, _knownPlayers: PLAYERS, _pacedTimingFunction: PacedTimingFunction, _prefixProperty: prefixProperty, _propertyIsSVGAttrib: propertyIsSVGAttrib }; defineDeprecatedProperty(window, 'Timeline', function() { deprecated('Timeline', '2014-04-08', 'Please use AnimationTimeline instead.'); return AnimationTimeline; }); })();