timing-object
Version:
An implementation of the timing object specification.
478 lines (461 loc) • 22.7 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@babel/runtime/helpers/classCallCheck'), require('@babel/runtime/helpers/createClass'), require('@babel/runtime/helpers/defineProperty'), require('@babel/runtime/helpers/possibleConstructorReturn'), require('@babel/runtime/helpers/getPrototypeOf'), require('@babel/runtime/helpers/inherits')) :
typeof define === 'function' && define.amd ? define(['exports', '@babel/runtime/helpers/classCallCheck', '@babel/runtime/helpers/createClass', '@babel/runtime/helpers/defineProperty', '@babel/runtime/helpers/possibleConstructorReturn', '@babel/runtime/helpers/getPrototypeOf', '@babel/runtime/helpers/inherits'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.timingObject = {}, global._classCallCheck, global._createClass, global._defineProperty, global._possibleConstructorReturn, global._getPrototypeOf, global._inherits));
})(this, (function (exports, _classCallCheck, _createClass, _defineProperty, _possibleConstructorReturn, _getPrototypeOf, _inherits) { 'use strict';
var createCalculateDelta = function createCalculateDelta(calculatePositiveRealSolution) {
return function (vector, startPosition, endPosition) {
var deltaToStartPosition = calculatePositiveRealSolution(vector, startPosition);
var deltaToEndPosition = calculatePositiveRealSolution(vector, endPosition);
// @todo Is it theoretically possible to have both deltas?
if (deltaToStartPosition !== null && deltaToEndPosition !== null) {
if (deltaToStartPosition < deltaToEndPosition) {
return deltaToStartPosition;
}
return deltaToEndPosition;
}
if (deltaToStartPosition !== null) {
return deltaToStartPosition;
}
if (deltaToEndPosition !== null) {
return deltaToEndPosition;
}
return null;
};
};
var createCalculatePositiveRealSolution = function createCalculatePositiveRealSolution(calculateRealSolutions) {
return function (_ref, x) {
var acceleration = _ref.acceleration,
position = _ref.position,
velocity = _ref.velocity;
var results = calculateRealSolutions(position, velocity, acceleration, x);
if (results.length === 0) {
return null;
}
if (results.length === 1) {
if (results[0] > 0) {
return results[0];
}
return null;
}
if (results.length === 2) {
if (results[1] < 0) {
return null;
}
if (results[0] > 0) {
return results[0];
}
if (results[1] > 0) {
return results[1];
}
}
return null;
};
};
var createCalculateTimeoutDelay = function createCalculateTimeoutDelay(calculateDelta) {
return function (vector, startPosition, endPosition) {
var delta = calculateDelta(vector, startPosition, endPosition);
if (delta === null || delta === Number.POSITIVE_INFINITY) {
return null;
}
return delta;
};
};
var createEventTargetConstructor = function createEventTargetConstructor(createEventTarget, wrapEventListener) {
return /*#__PURE__*/function () {
function EventTarget() {
_classCallCheck(this, EventTarget);
this._listeners = new WeakMap();
this._nativeEventTarget = createEventTarget();
}
return _createClass(EventTarget, [{
key: "addEventListener",
value: function addEventListener(type, listener, options) {
if (listener !== null) {
var wrappedEventListener = this._listeners.get(listener);
if (wrappedEventListener === undefined) {
wrappedEventListener = wrapEventListener(this, listener);
if (typeof listener === 'function') {
this._listeners.set(listener, wrappedEventListener);
}
}
this._nativeEventTarget.addEventListener(type, wrappedEventListener, options);
}
}
}, {
key: "dispatchEvent",
value: function dispatchEvent(event) {
return this._nativeEventTarget.dispatchEvent(event);
}
}, {
key: "removeEventListener",
value: function removeEventListener(type, listener, options) {
var wrappedEventListener = listener === null ? undefined : this._listeners.get(listener);
this._nativeEventTarget.removeEventListener(type, wrappedEventListener === undefined ? null : wrappedEventListener, options);
}
}]);
}();
};
var createEventTargetFactory = function createEventTargetFactory(window) {
return function () {
if (window === null) {
throw new Error('A native EventTarget could not be created.');
}
return window.document.createElement('p');
};
};
var createIllegalValueError = function createIllegalValueError() {
try {
return new DOMException('', 'IllegalValueError');
} catch (err) {
// @todo err.code;
err.name = 'IllegalValueError';
return err;
}
};
var createInvalidStateError = function createInvalidStateError() {
try {
return new DOMException('', 'InvalidStateError');
} catch (err) {
err.code = 11;
err.name = 'InvalidStateError';
return err;
}
};
function ownKeys$1(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread$1(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$1(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$1(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
var createTimingObjectConstructor = function createTimingObjectConstructor(calculateTimeoutDelay, createIllegalValueError, createInvalidStateError, eventTargetConstructor, filterTimingStateVectorUpdate, performance, setTimeout, translateTimingStateVector) {
return /*#__PURE__*/function (_eventTargetConstruct) {
function _class() {
var _this;
var timingProviderSourceOrVector = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var startPosition = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Number.NEGATIVE_INFINITY;
var endPosition = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : Number.POSITIVE_INFINITY;
_classCallCheck(this, _class);
_this = _callSuper(this, _class);
var _ref = timingProviderSourceOrVector.update === undefined ? {
timingProviderSource: null,
vector: timingProviderSourceOrVector
} : {
timingProviderSource: timingProviderSourceOrVector,
vector: {}
},
timingProviderSource = _ref.timingProviderSource,
vector = _ref.vector;
_this._endPosition = timingProviderSource === null ? endPosition : timingProviderSource.endPosition;
_this._onchange = null;
_this._onerror = null;
_this._onreadystatechange = null;
_this._readyState = timingProviderSource === null ? 'open' : timingProviderSource.readyState;
_this._skew = timingProviderSource === null ? 0 : timingProviderSource.skew;
_this._startPosition = timingProviderSource === null ? startPosition : timingProviderSource.startPosition;
_this._timingProviderSource = timingProviderSource;
_this._timeoutId = null;
_this._vector = timingProviderSource === null ? _objectSpread$1(_objectSpread$1({
acceleration: 0,
position: 0,
velocity: 0
}, filterTimingStateVectorUpdate(vector)), {}, {
timestamp: performance.now() / 1000
}) : timingProviderSource.vector;
// @todo The spec doesn't require to check if the endPosition is actually greater than the startPosition.
if (endPosition < _this._vector.position) {
_this._vector = _objectSpread$1(_objectSpread$1({}, _this._vector), {}, {
acceleration: 0,
position: endPosition,
velocity: 0
});
}
if (startPosition > _this._vector.position) {
_this._vector = _objectSpread$1(_objectSpread$1({}, _this._vector), {}, {
acceleration: 0,
position: startPosition,
velocity: 0
});
}
/*
* @todo Check if the vector would leave the range immediately.
* @todo The specification requires to run this._setInternalTimeout() only if the vector had to be modified above but it
* probably should run in either case.
* https://webtiming.github.io/timingobject/#x5-1-create-a-new-timing-object
*/
_this._setInternalTimeout();
if (timingProviderSource === null) {
setTimeout(function () {
return _this.dispatchEvent(new Event('readystatechange'));
});
} else {
var onAdjust = function onAdjust() {
_this._skew = timingProviderSource.skew;
/*
* @todo Process skew change with newSkew as parameter.
* https://webtiming.github.io/timingobject/#x5-7-process-skew-change
* https://webtiming.github.io/timingobject/#x5-10-calculate-skew-adjustment
*/
};
var onChange = function onChange() {
return _this._setInternalVector(timingProviderSource.vector);
};
var _onReadyStateChange = function onReadyStateChange() {
if (_this._isAllowedTransition(timingProviderSource.readyState)) {
_this._readyState = timingProviderSource.readyState;
} else {
_this._readyState = 'closed';
timingProviderSource.removeEventListener('adjust', onAdjust);
timingProviderSource.removeEventListener('change', onChange);
timingProviderSource.removeEventListener('readystatechange', _onReadyStateChange);
}
if (timingProviderSource.error !== null) {
setTimeout(function () {
return _this.dispatchEvent(new ErrorEvent('error', {
error: timingProviderSource.error
}));
});
}
setTimeout(function () {
return _this.dispatchEvent(new Event('readystatechange'));
});
};
timingProviderSource.addEventListener('adjust', onAdjust);
timingProviderSource.addEventListener('change', onChange);
timingProviderSource.addEventListener('readystatechange', _onReadyStateChange);
}
return _this;
}
_inherits(_class, _eventTargetConstruct);
return _createClass(_class, [{
key: "endPosition",
get: function get() {
return this._endPosition;
}
}, {
key: "onchange",
get: function get() {
return this._onchange === null ? this._onchange : this._onchange[0];
},
set: function set(value) {
if (this._onchange !== null) {
this.removeEventListener('change', this._onchange[1]);
}
if (typeof value === 'function') {
var boundListener = value.bind(this);
this.addEventListener('change', boundListener);
this._onchange = [value, boundListener];
} else {
this._onchange = null;
}
}
}, {
key: "onerror",
get: function get() {
return this._onerror === null ? this._onerror : this._onerror[0];
},
set: function set(value) {
if (this._onerror !== null) {
this.removeEventListener('error', this._onerror[1]);
}
if (typeof value === 'function') {
var boundListener = value.bind(this);
this.addEventListener('error', boundListener);
this._onerror = [value, boundListener];
} else {
this._onerror = null;
}
}
}, {
key: "onreadystatechange",
get: function get() {
return this._onreadystatechange === null ? this._onreadystatechange : this._onreadystatechange[0];
},
set: function set(value) {
if (this._onreadystatechange !== null) {
this.removeEventListener('readystatechange', this._onreadystatechange[1]);
}
if (typeof value === 'function') {
var boundListener = value.bind(this);
this.addEventListener('readystatechange', boundListener);
this._onreadystatechange = [value, boundListener];
} else {
this._onreadystatechange = null;
}
}
}, {
key: "readyState",
get: function get() {
return this._readyState;
}
}, {
key: "startPosition",
get: function get() {
return this._startPosition;
}
}, {
key: "timingProviderSource",
get: function get() {
return this._timingProviderSource;
}
}, {
key: "query",
value: function query() {
if (this._readyState !== 'open') {
throw createInvalidStateError();
}
var timestamp = performance.now() / 1000;
// @todo Compute the delta by gradually applying the skew.
var delta = this._timingProviderSource === null ? timestamp - this._vector.timestamp : timestamp + this._skew - this._vector.timestamp;
var vector = translateTimingStateVector(this._vector, delta);
if (this._endPosition < vector.position || this._startPosition > vector.position) {
this._setInternalVector(_objectSpread$1(_objectSpread$1({}, vector), {}, {
acceleration: 0,
position: this._endPosition < vector.position ? this._endPosition : this._startPosition,
velocity: 0
}));
return this.query();
}
return vector;
}
}, {
key: "update",
value: function update(newVector) {
if (this._readyState !== 'open') {
return Promise.reject(createInvalidStateError());
}
if (this._timingProviderSource !== null) {
var promise = this._timingProviderSource.update(newVector);
if (promise instanceof Promise) {
return promise;
}
return Promise.reject(new TypeError('The timingProviderSource failed to return a promise.'));
}
var filteredVector = filterTimingStateVectorUpdate(newVector);
// Return immediately if there is nothing to update.
if (Object.keys(filteredVector).length === 0) {
return Promise.resolve();
}
var normalizedNewVector = _objectSpread$1(_objectSpread$1({}, this.query()), filteredVector);
var position = normalizedNewVector.position,
velocity = normalizedNewVector.velocity,
acceleration = normalizedNewVector.acceleration;
if (position < this._startPosition || position > this._endPosition || position === this._startPosition && (velocity < 0 || velocity === 0 && acceleration < 0) || position === this._endPosition && (velocity > 0 || velocity === 0 && acceleration > 0)) {
return Promise.reject(createIllegalValueError());
}
this._setInternalVector(normalizedNewVector);
return Promise.resolve();
}
}, {
key: "_isAllowedTransition",
value: function _isAllowedTransition(readyState) {
return this._readyState === 'closing' && readyState === 'closed' || this._readyState === 'connecting' || this._readyState === 'open' && (readyState === 'closed' || readyState === 'closing');
}
}, {
key: "_setInternalTimeout",
value: function _setInternalTimeout() {
var _this2 = this;
if (this._timeoutId !== null) {
clearTimeout(this._timeoutId);
this._timeoutId = null;
}
if (this._endPosition === Number.POSITIVE_INFINITY && this._startPosition === Number.NEGATIVE_INFINITY || this._vector.acceleration === 0 && this._vector.velocity === 0) {
return;
}
var delay = calculateTimeoutDelay(this._vector, this._startPosition, this._endPosition);
if (delay === null) {
return;
}
this._timeoutId = setTimeout(function () {
return _this2.query();
}, delay);
}
}, {
key: "_setInternalVector",
value: function _setInternalVector(vector) {
var _this3 = this;
this._vector = vector;
this._setInternalTimeout();
setTimeout(function () {
return _this3.dispatchEvent(new Event('change'));
});
}
}]);
}(eventTargetConstructor);
};
var createWindow = function createWindow() {
return typeof window === 'undefined' ? null : window;
};
var calculateRealSolutions = function calculateRealSolutions(position, velocity, acceleration, x) {
// The position is constant.
if (acceleration === 0 && velocity === 0) {
if (position !== x) {
return [];
}
return [0];
}
// The velocity is constant and not equal to zero.
if (acceleration === 0) {
return [(x - position) / velocity];
}
// The velocity is accelerated.
// This equals p/2 of the pq-formula.
var firstSummand = velocity / acceleration;
// This equals sqrt((p/2 ** 2) - q) of the pq-formula.
var secondSummand = Math.sqrt(Math.pow(firstSummand, 2) - 2 / acceleration * (position - x));
return [secondSummand - firstSummand, -(secondSummand + firstSummand)].filter(function (solution) {
return !isNaN(solution);
}).sort();
};
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
var filterTimingStateVectorUpdate = function filterTimingStateVectorUpdate(vector) {
if (vector === undefined) {
return {};
}
var filteredVector = vector.acceleration !== null && vector.acceleration !== undefined ? {
acceleration: vector.acceleration
} : {};
if (vector.position !== null && vector.position !== undefined) {
filteredVector = _objectSpread(_objectSpread({}, filteredVector), {}, {
position: vector.position
});
}
if (vector.velocity !== null && vector.velocity !== undefined) {
return _objectSpread(_objectSpread({}, filteredVector), {}, {
velocity: vector.velocity
});
}
return filteredVector;
};
var translateTimingStateVector = function translateTimingStateVector(vector, delta) {
var acceleration = vector.acceleration,
position = vector.position,
timestamp = vector.timestamp,
velocity = vector.velocity;
return {
acceleration: acceleration,
position: position + velocity * delta + 0.5 * acceleration * Math.pow(delta, 2),
timestamp: timestamp + delta,
velocity: velocity + acceleration * delta
};
};
var wrapEventListener = function wrapEventListener(target, eventListener) {
return function (event) {
var descriptor = {
value: target
};
Object.defineProperties(event, {
currentTarget: descriptor,
target: descriptor
});
if (typeof eventListener === 'function') {
return eventListener.call(target, event);
}
return eventListener.handleEvent.call(target, event);
};
};
var timingObjectConstructor = createTimingObjectConstructor(createCalculateTimeoutDelay(createCalculateDelta(createCalculatePositiveRealSolution(calculateRealSolutions))), createIllegalValueError, createInvalidStateError, createEventTargetConstructor(createEventTargetFactory(createWindow()), wrapEventListener), filterTimingStateVectorUpdate, performance, setTimeout, translateTimingStateVector);
// @todo Expose an isSupported flag which checks for performance.now() support.
exports.TimingObject = timingObjectConstructor;
exports.filterTimingStateVectorUpdate = filterTimingStateVectorUpdate;
exports.translateTimingStateVector = translateTimingStateVector;
}));