@google/model-viewer
Version:
Easily display interactive 3D models on the web and in AR!
1,204 lines (1,192 loc) • 1.62 MB
JavaScript
const HAS_WEBXR_DEVICE_API = navigator.xr != null &&
self.XRSession != null && navigator.xr.supportsSession != null;
const HAS_WEBXR_HIT_TEST_API = HAS_WEBXR_DEVICE_API && self.XRSession.prototype.requestHitTest;
const HAS_RESIZE_OBSERVER = self.ResizeObserver != null;
const HAS_INTERSECTION_OBSERVER = self.IntersectionObserver != null;
const IS_WEBXR_AR_CANDIDATE = HAS_WEBXR_HIT_TEST_API;
const IS_MOBILE = (() => {
const userAgent = navigator.userAgent || navigator.vendor || self.opera;
let check = false;
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i
.test(userAgent) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i
.test(userAgent.substr(0, 4))) {
check = true;
}
return check;
})();
const HAS_OFFSCREEN_CANVAS = Boolean(self.OffscreenCanvas);
const OFFSCREEN_CANVAS_SUPPORT_BITMAP = Boolean(self.OffscreenCanvas) &&
Boolean(self.OffscreenCanvas.prototype.transferToImageBitmap);
const IS_ANDROID = /android/i.test(navigator.userAgent);
const IS_IOS = (/iPad|iPhone|iPod/.test(navigator.userAgent) && !self.MSStream) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const IS_AR_QUICKLOOK_CANDIDATE = (() => {
const tempAnchor = document.createElement('a');
return Boolean(tempAnchor.relList && tempAnchor.relList.supports &&
tempAnchor.relList.supports('ar'));
})();
const IS_IOS_CHROME = IS_IOS && /CriOS\//.test(navigator.userAgent);
const IS_IOS_SAFARI = IS_IOS && /Safari\//.test(navigator.userAgent);
const IS_IE11 = !(window.ActiveXObject) && 'ActiveXObject' in window;
const deserializeUrl = (url) => (url != null && url !== 'null') ? toFullUrl(url) : null;
const assertIsArCandidate = () => {
if (IS_WEBXR_AR_CANDIDATE) {
return;
}
const missingApis = [];
if (!HAS_WEBXR_DEVICE_API) {
missingApis.push('WebXR Device API');
}
if (!HAS_WEBXR_HIT_TEST_API) {
missingApis.push('WebXR Hit Test API');
}
throw new Error(`The following APIs are required for AR, but are missing in this browser: ${missingApis.join(', ')}`);
};
const toFullUrl = (partialUrl) => {
const url = new URL(partialUrl, window.location.toString());
return url.toString();
};
const throttle = (fn, ms) => {
let timer = null;
const throttled = (...args) => {
if (timer != null) {
return;
}
fn(...args);
timer = self.setTimeout(() => timer = null, ms);
};
throttled.flush = () => {
if (timer != null) {
self.clearTimeout(timer);
timer = null;
}
};
return throttled;
};
const debounce = (fn, ms) => {
let timer = null;
return (...args) => {
if (timer != null) {
self.clearTimeout(timer);
}
timer = self.setTimeout(() => {
timer = null;
fn(...args);
}, ms);
};
};
const step = (edge, value) => {
return value < edge ? 0 : 1;
};
const clamp = (value, lowerLimit, upperLimit) => Math.max(lowerLimit === -Infinity ? value : lowerLimit, Math.min(upperLimit === Infinity ? value : upperLimit, value));
const CAPPED_DEVICE_PIXEL_RATIO = 1;
const resolveDpr = (() => {
const HAS_META_VIEWPORT_TAG = (() => {
const metas = document.head != null ?
Array.from(document.head.querySelectorAll('meta')) :
[];
for (const meta of metas) {
if (meta.name === 'viewport') {
return true;
}
}
return false;
})();
if (!HAS_META_VIEWPORT_TAG) {
console.warn('No <meta name="viewport"> detected; <model-viewer> will cap pixel density at 1.');
}
return () => HAS_META_VIEWPORT_TAG ? window.devicePixelRatio :
CAPPED_DEVICE_PIXEL_RATIO;
})();
const getFirstMapKey = (map) => {
if (map.keys != null) {
return map.keys().next().value || null;
}
let firstKey = null;
try {
map.forEach((_value, key, _map) => {
firstKey = key;
throw new Error();
});
}
catch (_error) {
}
return firstKey;
};
const expect = chai.expect;
suite('utils', () => {
suite('deserializeUrl', () => {
test('returns a string given a string', () => {
expect(deserializeUrl('foo')).to.be.a('string');
});
test('returns null given a null-ish value', () => {
expect(deserializeUrl(null)).to.be.equal(null);
});
test('yields a url on the same origin for relative paths', () => {
const { origin } = window.location;
expect(deserializeUrl('foo').indexOf(origin)).to.be.equal(0);
});
});
suite('resolveDpr', () => {
suite('when <meta name="viewport"> is present', () => {
test('resolves the device pixel ratio', () => {
const resolvedDpr = resolveDpr();
const actualDpr = self.devicePixelRatio;
expect(resolvedDpr).to.be.equal(actualDpr);
});
});
suite('when <meta name="viewport"> is not present', () => {
test.skip('caps the device pixel ratio to 1', () => {
});
});
});
suite('step', () => {
test('returns 0 for values below edge', () => {
expect(step(0.5, 0.1)).to.be.equal(0);
});
test('returns 1 for values above edge', () => {
expect(step(0.5, 0.9)).to.be.equal(1);
});
});
suite('clamp', () => {
test('numbers below lower limit adjusted to lower limit', () => {
expect(clamp(1.0, 2.0, 3.0)).to.be.equal(2.0);
});
test('numbers above upper limit adjusted to upper limit', () => {
expect(clamp(4.0, 2.0, 3.0)).to.be.equal(3.0);
});
test('numbers within lower and upper limits unchanged', () => {
expect(clamp(2.5, 2.0, 3.0)).to.be.equal(2.5);
});
});
});
const numberNode = (value, unit) => ({ type: 'number', number: value, unit });
const parseExpressions = (() => {
const cache = {};
const MAX_PARSE_ITERATIONS = 1000;
return (inputString) => {
const cacheKey = inputString;
if (cacheKey in cache) {
return cache[cacheKey];
}
const expressions = [];
let parseIterations = 0;
while (inputString) {
if (++parseIterations > MAX_PARSE_ITERATIONS) {
inputString = '';
break;
}
const expressionParseResult = parseExpression(inputString);
const expression = expressionParseResult.nodes[0];
if (expression == null || expression.terms.length === 0) {
break;
}
expressions.push(expression);
inputString = expressionParseResult.remainingInput;
}
return cache[cacheKey] = expressions;
};
})();
const parseExpression = (() => {
const IS_IDENT_RE = /^(\-\-|[a-z\u0240-\uffff])/i;
const IS_OPERATOR_RE = /^([\*\+\/]|[\-]\s)/i;
const IS_EXPRESSION_END_RE = /^[\),]/;
const FUNCTION_ARGUMENTS_FIRST_TOKEN = '(';
const HEX_FIRST_TOKEN = '#';
return (inputString) => {
const terms = [];
while (inputString.length) {
inputString = inputString.trim();
if (IS_EXPRESSION_END_RE.test(inputString)) {
break;
}
else if (inputString[0] === FUNCTION_ARGUMENTS_FIRST_TOKEN) {
const { nodes, remainingInput } = parseFunctionArguments(inputString);
inputString = remainingInput;
terms.push({
type: 'function',
name: { type: 'ident', value: 'calc' },
arguments: nodes
});
}
else if (IS_IDENT_RE.test(inputString)) {
const identParseResult = parseIdent(inputString);
const identNode = identParseResult.nodes[0];
inputString = identParseResult.remainingInput;
if (inputString[0] === FUNCTION_ARGUMENTS_FIRST_TOKEN) {
const { nodes, remainingInput } = parseFunctionArguments(inputString);
terms.push({ type: 'function', name: identNode, arguments: nodes });
inputString = remainingInput;
}
else {
terms.push(identNode);
}
}
else if (IS_OPERATOR_RE.test(inputString)) {
terms.push({ type: 'operator', value: inputString[0] });
inputString = inputString.slice(1);
}
else {
const { nodes, remainingInput } = inputString[0] === HEX_FIRST_TOKEN ?
parseHex(inputString) :
parseNumber(inputString);
if (nodes.length === 0) {
break;
}
terms.push(nodes[0]);
inputString = remainingInput;
}
}
return { nodes: [{ type: 'expression', terms }], remainingInput: inputString };
};
})();
const parseIdent = (() => {
const NOT_IDENT_RE = /[^a-z^0-9^_^\-^\u0240-\uffff]/i;
return (inputString) => {
const match = inputString.match(NOT_IDENT_RE);
const ident = match == null ? inputString : inputString.substr(0, match.index);
const remainingInput = match == null ? '' : inputString.substr(match.index);
return { nodes: [{ type: 'ident', value: ident }], remainingInput };
};
})();
const parseNumber = (() => {
const NOT_VALUE_RE = /[^0-9\.\-]|$/;
const UNIT_RE = /^[a-z%]+/i;
const ALLOWED_UNITS = /^(m|mm|cm|rad|deg|[%])$/;
return (inputString) => {
const notValueMatch = inputString.match(NOT_VALUE_RE);
const value = notValueMatch == null ?
inputString :
inputString.substr(0, notValueMatch.index);
inputString = notValueMatch == null ?
inputString :
inputString.slice(notValueMatch.index);
const unitMatch = inputString.match(UNIT_RE);
let unit = unitMatch != null && unitMatch[0] !== '' ? unitMatch[0] : null;
const remainingInput = unitMatch == null ? inputString : inputString.slice(unit.length);
if (unit != null && !ALLOWED_UNITS.test(unit)) {
unit = null;
}
return {
nodes: [{
type: 'number',
number: parseFloat(value) || 0,
unit: unit
}],
remainingInput
};
};
})();
const parseHex = (() => {
const HEX_RE = /^[a-f0-9]*/i;
return (inputString) => {
inputString = inputString.slice(1).trim();
const hexMatch = inputString.match(HEX_RE);
const nodes = hexMatch == null ? [] : [{ type: 'hex', value: hexMatch[0] }];
return {
nodes,
remainingInput: hexMatch == null ? inputString :
inputString.slice(hexMatch[0].length)
};
};
})();
const parseFunctionArguments = (inputString) => {
const expressionNodes = [];
inputString = inputString.slice(1).trim();
while (inputString.length) {
const expressionParseResult = parseExpression(inputString);
expressionNodes.push(expressionParseResult.nodes[0]);
inputString = expressionParseResult.remainingInput.trim();
if (inputString[0] === ',') {
inputString = inputString.slice(1).trim();
}
else if (inputString[0] === ')') {
inputString = inputString.slice(1);
break;
}
}
return { nodes: expressionNodes, remainingInput: inputString };
};
const $visitedTypes = Symbol('visitedTypes');
class ASTWalker {
constructor(visitedTypes) {
this[$visitedTypes] = visitedTypes;
}
walk(ast, callback) {
const remaining = ast.slice();
while (remaining.length) {
const next = remaining.shift();
if (this[$visitedTypes].indexOf(next.type) > -1) {
callback(next);
}
switch (next.type) {
case 'expression':
remaining.unshift(...next.terms);
break;
case 'function':
remaining.unshift(next.name, ...next.arguments);
break;
}
}
}
}
const ZERO = Object.freeze({ type: 'number', number: 0, unit: null });
const elementFromLocalPoint = (document, x, y) => {
const host = (document === window.document) ?
window.document.body :
document.host;
const actualDocument = window.ShadyCSS ? window.document : document;
const boundingRect = host.getBoundingClientRect();
return actualDocument.elementFromPoint(boundingRect.left + x, boundingRect.top + y);
};
const pickShadowDescendant = (element, x = 0, y = 0) => {
return element.shadowRoot != null ?
elementFromLocalPoint(element.shadowRoot, x, y) :
null;
};
const timePasses = (ms = 0) => new Promise(resolve => setTimeout(resolve, ms));
const until = async (predicate) => {
while (!predicate()) {
await timePasses();
}
};
const rafPasses = () => new Promise(resolve => requestAnimationFrame(() => resolve()));
const textureMatchesMeta = (texture, meta) => !!(texture && texture.userData &&
Object.keys(meta).reduce((matches, key) => {
return matches && meta[key] === texture.userData[key];
}, true));
const waitForEvent = (target, eventName, predicate = null) => new Promise(resolve => {
function handler(event) {
if (!predicate || predicate(event)) {
resolve(event);
target.removeEventListener(eventName, handler);
}
}
target.addEventListener(eventName, handler);
});
const dispatchSyntheticEvent = (target, type, properties = {
clientX: 0,
clientY: 0,
deltaY: 1.0
}) => {
const event = new CustomEvent(type, { cancelable: true, bubbles: true });
Object.assign(event, properties);
target.dispatchEvent(event);
return event;
};
const ASSETS_DIRECTORY = '../examples/assets/';
const assetPath = (name) => deserializeUrl(`${ASSETS_DIRECTORY}${name}`);
const isInDocumentTree = (node) => {
let root = node.getRootNode();
while (root !== node && root != null) {
if (root.nodeType === Node.DOCUMENT_NODE) {
return root === document;
}
root = root.host && root.host.getRootNode();
}
return false;
};
const spy = (object, property, descriptor) => {
let sourcePrototype = object;
while (sourcePrototype != null &&
!sourcePrototype.hasOwnProperty(property)) {
sourcePrototype = sourcePrototype.__proto__;
}
if (sourcePrototype == null) {
throw new Error(`Cannnot spy property "${property}" on ${object}`);
}
const originalDescriptor = Object.getOwnPropertyDescriptor(sourcePrototype, property);
if (originalDescriptor == null) {
throw new Error(`Cannot read descriptor of "${property}" on ${object}`);
}
Object.defineProperty(sourcePrototype, property, descriptor);
return () => {
Object.defineProperty(sourcePrototype, property, originalDescriptor);
};
};
const expressionNode = (terms) => ({ type: 'expression', terms });
const hexNode = (value) => ({ type: 'hex', value });
const identNode = (value) => ({ type: 'ident', value });
const operatorNode = (value) => ({ type: 'operator', value });
const functionNode = (name, args) => ({ type: 'function', name: identNode(name), arguments: args });
const expect$1 = chai.expect;
suite('parsers', () => {
suite('parseExpressions', () => {
test('parses single numbers', () => {
expect$1(parseExpressions('123rad')).to.be.eql([expressionNode([numberNode(123, 'rad')])]);
});
test('parses number tuples', () => {
expect$1(parseExpressions('123rad 3.14deg -1m 2cm')).to.be.eql([
expressionNode([
numberNode(123, 'rad'),
numberNode(3.14, 'deg'),
numberNode(-1, 'm'),
numberNode(2, 'cm')
])
]);
});
test('parses hex colors', () => {
expect$1(parseExpressions('#fff')).to.be.eql([expressionNode([hexNode('fff')])]);
expect$1(parseExpressions('#abc123')).to.be.eql([expressionNode([hexNode('abc123')])]);
expect$1(parseExpressions('#daf012ee')).to.be.eql([expressionNode([hexNode('daf012ee')])]);
});
test('parses functions', () => {
expect$1(parseExpressions('rgba(255, 123, 0, 0.25)'))
.to.be.eql([expressionNode([functionNode('rgba', [
expressionNode([numberNode(255, null)]),
expressionNode([numberNode(123, null)]),
expressionNode([numberNode(0, null)]),
expressionNode([numberNode(0.25, null)]),
])])]);
});
test('parses nested functions', () => {
expect$1(parseExpressions('rgba(255, calc(100 + var(--blue)), 0, 0.25)'))
.to.be.eql([expressionNode([functionNode('rgba', [
expressionNode([numberNode(255, null)]),
expressionNode([
functionNode('calc', [
expressionNode([
numberNode(100, null),
operatorNode('+'),
functionNode('var', [
expressionNode([
identNode('--blue')
])
])
])
])
]),
expressionNode([numberNode(0, null)]),
expressionNode([numberNode(0.25, null)]),
])])]);
});
test('parses calc algebra', () => {
expect$1(parseExpressions('1m - -2rad / 3 + 4deg * -10.5')).to.be.eql([
expressionNode([
numberNode(1, 'm'),
operatorNode('-'),
numberNode(-2, 'rad'),
operatorNode('/'),
numberNode(3, null),
operatorNode('+'),
numberNode(4, 'deg'),
operatorNode('*'),
numberNode(-10.5, null)
])
]);
});
suite('failure cases', () => {
suite('mismatched parens', () => {
test('trailing paren is gracefully dropped', () => {
expect$1(parseExpressions('calc(calc(123)))')).to.be.eql([
expressionNode([
functionNode('calc', [
expressionNode([
functionNode('calc', [
expressionNode([
numberNode(123, null)
])
])
])
])
])
]);
});
});
});
});
suite('ASTWalker', () => {
test('only walks configured node types', () => {
const astWalker = new ASTWalker(['number', 'ident']);
const ast = parseExpressions('calc(123 + var(--size))');
let visitedNodes = 0;
astWalker.walk(ast, (node) => {
expect$1(node.type === 'number' || node.type === 'ident').to.be.true;
visitedNodes++;
});
expect$1(visitedNodes).to.be.equal(4);
});
});
});
const degreesToRadians = (numberNode$$1, fallbackRadianValue = 0) => {
let { number, unit } = numberNode$$1;
if (!isFinite(number)) {
number = fallbackRadianValue;
unit = 'rad';
}
else if (numberNode$$1.unit === 'rad' || numberNode$$1.unit == null) {
return numberNode$$1;
}
const valueIsDegrees = unit === 'deg' && number != null;
const value = valueIsDegrees ? number : 0;
const radians = value * Math.PI / 180;
return { type: 'number', number: radians, unit: 'rad' };
};
const radiansToDegrees = (numberNode$$1, fallbackDegreeValue = 0) => {
let { number, unit } = numberNode$$1;
if (!isFinite(number)) {
number = fallbackDegreeValue;
unit = 'deg';
}
else if (numberNode$$1.unit === 'deg') {
return numberNode$$1;
}
const valueIsRadians = (unit === null || unit === 'rad') && number != null;
const value = valueIsRadians ? number : 0;
const degrees = value * 180 / Math.PI;
return { type: 'number', number: degrees, unit: 'deg' };
};
const lengthToBaseMeters = (numberNode$$1, fallbackMeterValue = 0) => {
let { number, unit } = numberNode$$1;
if (!isFinite(number)) {
number = fallbackMeterValue;
unit = 'm';
}
else if (numberNode$$1.unit === 'm') {
return numberNode$$1;
}
let scale;
switch (unit) {
default:
scale = 1;
break;
case 'cm':
scale = 1 / 100;
break;
case 'mm':
scale = 1 / 1000;
break;
}
const value = scale * number;
return { type: 'number', number: value, unit: 'm' };
};
const normalizeUnit = (() => {
const identity = (node) => node;
const unitNormalizers = {
'rad': identity,
'deg': degreesToRadians,
'm': identity,
'mm': lengthToBaseMeters,
'cm': lengthToBaseMeters
};
return (node, fallback = ZERO) => {
let { number, unit } = node;
if (!isFinite(number)) {
number = fallback.number;
unit = fallback.unit;
}
if (unit == null) {
return node;
}
const normalize = unitNormalizers[unit];
if (normalize == null) {
return fallback;
}
return normalize(node);
};
})();
const expect$2 = chai.expect;
suite('conversions', () => {
suite('degreesToRadians', () => {
test('converts a number expressed in degrees to radians', () => {
expect$2(degreesToRadians(numberNode(180, 'deg')))
.to.be.eql(numberNode(Math.PI, 'rad'));
});
test('passes through numbers expressed in radians', () => {
expect$2(degreesToRadians(numberNode(1, 'rad')))
.to.be.eql(numberNode(1, 'rad'));
});
test('passes through numbers without a unit', () => {
expect$2(degreesToRadians(numberNode(1, null)))
.to.be.eql(numberNode(1, null));
});
});
suite('radiansToDegrees', () => {
test('converts a number expressed in radians to degrees', () => {
expect$2(radiansToDegrees(numberNode(Math.PI, 'rad')))
.to.be.eql(numberNode(180, 'deg'));
});
test('passes through numbers expressed in degrees', () => {
expect$2(radiansToDegrees(numberNode(1, 'deg')))
.to.be.eql(numberNode(1, 'deg'));
});
test('treats numbers without a unit as radians', () => {
expect$2(radiansToDegrees(numberNode(Math.PI, null)))
.to.be.eql(numberNode(180, 'deg'));
});
});
suite('lengthToBaseMeters', () => {
test('passes through numbers expressed in base meters', () => {
expect$2(lengthToBaseMeters(numberNode(1, 'm')))
.to.be.eql(numberNode(1, 'm'));
});
test('converts numbers expressed in centimeters to base meters', () => {
expect$2(lengthToBaseMeters(numberNode(123, 'cm')))
.to.be.eql(numberNode(1.23, 'm'));
});
test('converts numbers expressed in millimeters to base meters', () => {
expect$2(lengthToBaseMeters(numberNode(1234, 'mm')))
.to.be.eql(numberNode(1.234, 'm'));
});
});
suite('normalizeUnit', () => {
test('normalizes angles to radians', () => {
expect$2(normalizeUnit(numberNode(180, 'deg')))
.to.be.eql(numberNode(Math.PI, 'rad'));
expect$2(normalizeUnit(numberNode(180, 'rad')))
.to.be.eql(numberNode(180, 'rad'));
});
test('normalizes lengths to base meters', () => {
expect$2(normalizeUnit(numberNode(1, 'm'))).to.be.eql(numberNode(1, 'm'));
expect$2(normalizeUnit(numberNode(1000, 'mm')))
.to.be.eql(numberNode(1, 'm'));
});
});
});
const enumerationDeserializer = (allowedNames) => (valueString) => {
try {
const expressions = parseExpressions(valueString);
const names = (expressions.length ? expressions[0].terms : [])
.filter((valueNode) => valueNode && valueNode.type === 'ident')
.map(valueNode => valueNode.value)
.filter(name => allowedNames.indexOf(name) > -1);
const result = new Set();
for (const name of names) {
result.add(name);
}
return result;
}
catch (_error) {
}
return new Set();
};
const expect$3 = chai.expect;
suite('deserializers', () => {
suite('enumerationDeserializer', () => {
let animals;
let deserializeAnimals;
setup(() => {
animals = ['elephant', 'octopus', 'chinchilla'];
deserializeAnimals = enumerationDeserializer(animals);
});
test('yields the members of the enumeration in the input string', () => {
const deserialized = deserializeAnimals('elephant chinchilla');
expect$3(deserialized.size).to.be.equal(2);
expect$3(deserialized.has('elephant')).to.be.true;
expect$3(deserialized.has('chinchilla')).to.be.true;
});
test('filters out non-members of the enumeration', () => {
const deserialized = deserializeAnimals('octopus paris');
expect$3(deserialized.size).to.be.equal(1);
expect$3(deserialized.has('octopus')).to.be.true;
});
test('yields an empty set from null input', () => {
const deserialized = deserializeAnimals(null);
expect$3(deserialized.size).to.be.equal(0);
});
});
});
var _a, _b, _c;
const $evaluate = Symbol('evaluate');
const $lastValue = Symbol('lastValue');
class Evaluator {
constructor() {
this[_a] = null;
}
static evaluatableFor(node, basis = ZERO) {
if (node instanceof Evaluator) {
return node;
}
if (node.type === 'number') {
if (node.unit === '%') {
return new PercentageEvaluator(node, basis);
}
return node;
}
switch (node.name.value) {
case 'calc':
return new CalcEvaluator(node, basis);
case 'env':
return new EnvEvaluator(node);
}
return ZERO;
}
static evaluate(evaluatable) {
if (evaluatable instanceof Evaluator) {
return evaluatable.evaluate();
}
return evaluatable;
}
static isConstant(evaluatable) {
if (evaluatable instanceof Evaluator) {
return evaluatable.isConstant;
}
return true;
}
static applyIntrinsics(evaluated, intrinsics) {
const { basis, keywords } = intrinsics;
const { auto } = keywords;
return basis.map((basisNode, index) => {
const autoSubstituteNode = auto[index] == null ? basisNode : auto[index];
let evaluatedNode = evaluated[index] ? evaluated[index] : autoSubstituteNode;
if (evaluatedNode.type === 'ident') {
const keyword = evaluatedNode.value;
if (keyword in keywords) {
evaluatedNode = keywords[keyword][index];
}
}
if (evaluatedNode == null || evaluatedNode.type === 'ident') {
evaluatedNode = autoSubstituteNode;
}
if (evaluatedNode.unit === '%') {
return numberNode(evaluatedNode.number / 100 * basisNode.number, basisNode.unit);
}
evaluatedNode = normalizeUnit(evaluatedNode, basisNode);
if (evaluatedNode.unit !== basisNode.unit) {
return basisNode;
}
return evaluatedNode;
});
}
get isConstant() {
return false;
}
evaluate() {
if (!this.isConstant || this[$lastValue] == null) {
this[$lastValue] = this[$evaluate]();
}
return this[$lastValue];
}
}
_a = $lastValue;
const $percentage = Symbol('percentage');
const $basis = Symbol('basis');
class PercentageEvaluator extends Evaluator {
constructor(percentage, basis) {
super();
this[$percentage] = percentage;
this[$basis] = basis;
}
get isConstant() {
return true;
}
[$evaluate]() {
return numberNode(this[$percentage].number / 100 * this[$basis].number, this[$basis].unit);
}
}
const $identNode = Symbol('identNode');
class EnvEvaluator extends Evaluator {
constructor(envFunction) {
super();
this[_b] = null;
const identNode = envFunction.arguments.length ? envFunction.arguments[0].terms[0] : null;
if (identNode != null && identNode.type === 'ident') {
this[$identNode] = identNode;
}
}
get isConstant() {
return false;
}
;
[(_b = $identNode, $evaluate)]() {
if (this[$identNode] != null) {
switch (this[$identNode].value) {
case 'window-scroll-y':
const verticalScrollPosition = window.pageYOffset;
const verticalScrollMax = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
const scrollY = verticalScrollPosition /
(verticalScrollMax - window.innerHeight) ||
0;
return { type: 'number', number: scrollY, unit: null };
}
}
return ZERO;
}
}
const IS_MULTIPLICATION_RE = /[\*\/]/;
const $evaluator = Symbol('evalutor');
class CalcEvaluator extends Evaluator {
constructor(calcFunction, basis = ZERO) {
super();
this[_c] = null;
if (calcFunction.arguments.length !== 1) {
return;
}
const terms = calcFunction.arguments[0].terms.slice();
const secondOrderTerms = [];
while (terms.length) {
const term = terms.shift();
if (secondOrderTerms.length > 0) {
const previousTerm = secondOrderTerms[secondOrderTerms.length - 1];
if (previousTerm.type === 'operator' &&
IS_MULTIPLICATION_RE.test(previousTerm.value)) {
const operator = secondOrderTerms.pop();
const leftValue = secondOrderTerms.pop();
if (leftValue == null) {
return;
}
secondOrderTerms.push(new OperatorEvaluator(operator, Evaluator.evaluatableFor(leftValue, basis), Evaluator.evaluatableFor(term, basis)));
continue;
}
}
secondOrderTerms.push(term.type === 'operator' ? term :
Evaluator.evaluatableFor(term, basis));
}
while (secondOrderTerms.length > 2) {
const [left, operator, right] = secondOrderTerms.splice(0, 3);
if (operator.type !== 'operator') {
return;
}
secondOrderTerms.unshift(new OperatorEvaluator(operator, Evaluator.evaluatableFor(left, basis), Evaluator.evaluatableFor(right, basis)));
}
if (secondOrderTerms.length === 1) {
this[$evaluator] = secondOrderTerms[0];
}
}
get isConstant() {
return this[$evaluator] == null || Evaluator.isConstant(this[$evaluator]);
}
[(_c = $evaluator, $evaluate)]() {
return this[$evaluator] != null ? Evaluator.evaluate(this[$evaluator]) :
ZERO;
}
}
const $operator = Symbol('operator');
const $left = Symbol('left');
const $right = Symbol('right');
class OperatorEvaluator extends Evaluator {
constructor(operator, left, right) {
super();
this[$operator] = operator;
this[$left] = left;
this[$right] = right;
}
get isConstant() {
return Evaluator.isConstant(this[$left]) &&
Evaluator.isConstant(this[$right]);
}
[$evaluate]() {
const leftNode = normalizeUnit(Evaluator.evaluate(this[$left]));
const rightNode = normalizeUnit(Evaluator.evaluate(this[$right]));
const { number: leftValue, unit: leftUnit } = leftNode;
const { number: rightValue, unit: rightUnit } = rightNode;
if (rightUnit != null && leftUnit != null && rightUnit != leftUnit) {
return ZERO;
}
const unit = leftUnit || rightUnit;
let value;
switch (this[$operator].value) {
case '+':
value = leftValue + rightValue;
break;
case '-':
value = leftValue - rightValue;
break;
case '/':
value = leftValue / rightValue;
break;
case '*':
value = leftValue * rightValue;
break;
default:
return ZERO;
}
return { type: 'number', number: value, unit };
}
}
const $evaluatables = Symbol('evaluatables');
const $intrinsics = Symbol('intrinsics');
class StyleEvaluator extends Evaluator {
constructor(expressions, intrinsics) {
super();
this[$intrinsics] = intrinsics;
const firstExpression = expressions[0];
const terms = firstExpression != null ? firstExpression.terms : [];
this[$evaluatables] =
intrinsics.basis.map((basisNode, index) => {
const term = terms[index];
if (term == null) {
return { type: 'ident', value: 'auto' };
}
if (term.type === 'ident') {
return term;
}
return Evaluator.evaluatableFor(term, basisNode);
});
}
get isConstant() {
for (const evaluatable of this[$evaluatables]) {
if (!Evaluator.isConstant(evaluatable)) {
return false;
}
}
return true;
}
[$evaluate]() {
const evaluated = this[$evaluatables].map(evaluatable => Evaluator.evaluate(evaluatable));
return Evaluator.applyIntrinsics(evaluated, this[$intrinsics])
.map(numberNode$$1 => numberNode$$1.number);
}
}
const expect$4 = chai.expect;
suite('evaluators', () => {
suite('EnvEvaluator', () => {
test('is never constant', () => {
const evaluator = new EnvEvaluator(functionNode('env', []));
expect$4(evaluator.isConstant).to.be.false;
});
test('with no arguments, evaluates to zero', () => {
const evaluator = new EnvEvaluator(functionNode('env', []));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(0, null));
});
suite('window-scroll-y', () => {
test('evaluates to current top-level scroll position', () => {
const evaluator = new EnvEvaluator(functionNode('env', [expressionNode([identNode('window-scroll-y')])]));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(0, null));
const scrollPosition = 10000;
const scrollMax = 20000;
const restorePageYOffset = spy(window, 'pageYOffset', { value: scrollPosition });
const restoreBodyScrollHeight = spy(document.documentElement, 'clientHeight', { value: scrollMax });
expect$4(evaluator.evaluate())
.to.be.eql(numberNode(scrollPosition / (scrollMax - window.innerHeight), null));
restorePageYOffset();
restoreBodyScrollHeight();
});
});
});
suite('PercentageEvaluator', () => {
test('multiplies the percentage by the basis', () => {
const evaluator = new PercentageEvaluator(numberNode(200, '%'), numberNode(1, 'm'));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(2, 'm'));
});
});
suite('CalcEvaluator', () => {
test('is constant if its operands are all numbers', () => {
const evaluator = new CalcEvaluator(functionNode('calc', [expressionNode([numberNode(1, null), operatorNode('+'), numberNode(1, null)])]));
expect$4(evaluator.isConstant).to.be.true;
});
test('is constant if nested functions are constant', () => {
const evaluator = new CalcEvaluator(functionNode('calc', [expressionNode([
numberNode(1, null),
operatorNode('+'),
functionNode('calc', [expressionNode([numberNode(1, null)])])
])]));
expect$4(evaluator.isConstant).to.be.true;
});
test('is not constant if any nested function is not constant', () => {
const evaluator = new CalcEvaluator(functionNode('calc', [
expressionNode([
numberNode(1, null),
operatorNode('+'),
functionNode('calc', [expressionNode([numberNode(1, null)])]),
operatorNode('+'),
functionNode('env', [expressionNode([identNode('window-scroll-y')])])
])
]));
expect$4(evaluator.isConstant).to.be.false;
});
test('with no arguments, evaluates to zero', () => {
const evaluator = new CalcEvaluator(functionNode('env', []));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(0, null));
});
test('evaluates basic addition', () => {
const evaluator = new CalcEvaluator(functionNode('calc', [expressionNode([numberNode(1, null), operatorNode('+'), numberNode(1, null)])]));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(2, null));
});
test('evaluates basic subtraction', () => {
const evaluator = new CalcEvaluator(functionNode('calc', [expressionNode([numberNode(1, null), operatorNode('-'), numberNode(1, null)])]));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(0, null));
});
test('evaluates basic multiplication', () => {
const evaluator = new CalcEvaluator(functionNode('calc', [expressionNode([numberNode(5, null), operatorNode('*'), numberNode(4, null)])]));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(20, null));
});
test('evaluates basic division', () => {
const evaluator = new CalcEvaluator(functionNode('calc', [
expressionNode([numberNode(100, null), operatorNode('/'), numberNode(10, null)])
]));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(10, null));
});
test('evaluates complex algebraic expressions', () => {
const evaluator = new CalcEvaluator(functionNode('calc', [
expressionNode([
numberNode(1, null),
operatorNode('+'),
numberNode(2, null),
operatorNode('*'),
numberNode(2.5, null),
operatorNode('-'),
numberNode(-2, null),
operatorNode('/'),
numberNode(-1, null)
]),
]));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(4, null));
});
test('evaluates algebra with nested functions', () => {
test('evaluates basic addition', () => {
const evaluator = new CalcEvaluator(functionNode('calc', [expressionNode([
numberNode(1, null),
operatorNode('+'),
functionNode('calc', [expressionNode([numberNode(1, null)])])
])]));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(2, null));
});
});
});
suite('OperatorEvaluator', () => {
test('evaluates zero for an unknown operator', () => {
const evaluator = new OperatorEvaluator(operatorNode('$'), numberNode(1, null), numberNode(1, null));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(0, null));
});
test('evaluates zero for operands with mismatching unit types', () => {
const evaluator = new OperatorEvaluator(operatorNode('+'), numberNode(1, 'm'), numberNode(1, 'rad'));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(0, null));
});
test('normalizes to base meters for length operands', () => {
const evaluator = new OperatorEvaluator(operatorNode('+'), numberNode(1, 'm'), numberNode(1000, 'mm'));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(2, 'm'));
});
test('normalizes to radians for angular operands', () => {
const evaluator = new OperatorEvaluator(operatorNode('+'), numberNode(180, 'deg'), numberNode(Math.PI, 'rad'));
expect$4(evaluator.evaluate()).to.be.eql(numberNode(2 * Math.PI, 'rad'));
});
});
suite('VectorEvaluator', () => {
suite('with SphericalIntrinsics', () => {
let intrinsics;
setup(() => {
intrinsics = {
basis: [numberNode(1, 'rad'), numberNode(1, 'rad'), numberNode(1, 'm')],
keywords: {
auto: [numberNode(2, 'rad'), null, numberNode(200, '%')]
}
};
});
test('evaluates to defaults (e.g., auto) for omitted expressions', () => {
const evaluator = new StyleEvaluator([], intrinsics);
expect$4(evaluator.evaluate()).to.be.eql([2, 1, 2]);
});
test('substitutes the keyword auto for the related intrinsic value', () => {
const evaluator = new StyleEvaluator([expressionNode([
identNode('auto')
])], intrinsics);
expect$4(evaluator.evaluate()).to.be.eql([2, 1, 2]);
});
test('treats missing values as equivalent to auto', () => {
const evaluatorOne = new StyleEvaluator([expressionNode([])], intrinsics);
const evaluatorTwo = new StyleEvaluator([expressionNode([
identNode('auto'),
identNode('auto'),
identNode('auto')
])], intrinsics);
expect$4(evaluatorOne.evaluate()).to.be.eql(evaluatorTwo.evaluate());
});
test('scales the basis by an input percentage', () => {
const evaluator = new StyleEvaluator([expressionNode([
numberNode(300, '%')
])], intrinsics);
expect$4(evaluator.evaluate()).to.be.eql([3, 1, 2]);
});
test('evaluates spherical values from basic expressions', () => {
const evaluator = new StyleEvaluator([expressionNode([
numberNode(1, 'rad'),
numberNode(180, 'deg'),
numberNode(100, 'cm')
])], intrinsics);
expect$4(evaluator.evaluate()).to.be.eql([1, Math.PI, 1]);
});
test('applies a percentage at any expression depth to the basis', () => {
const evaluator = new StyleEvaluator([expressionNode([
numberNode(150, '%'),
numberNode(180, 'deg'),
functionNode('calc', [
expressionNode([
numberNode(200, '%'),
operatorNode('*'),
functionNode('calc', [expressionNode([numberNode(3, 'm')])]),
])
])
])], intrinsics);
expect$4(evaluator.evaluate()).to.be.eql([1.5, Math.PI, 6]);
});
test('evaluates spherical values from complex expressions', () => {
const evaluator = new StyleEvaluator([expressionNode([
numberNode(1, 'rad'),
numberNode(180, 'deg'),
functionNode('calc', [
expressionNode([
numberNode(1, 'm'),
operatorNode('+'),
functionNode('calc', [expressionNode([numberNode(1, null)])]),
operatorNode('+'),
functionNode('env', [expressionNode([identNode('window-scroll-y')])])
])
])
])], intrinsics);
expect$4(evaluator.evaluate()).to.be.eql([1, Math.PI, 2]);
});
});
});
});
var _a$1, _b$1, _c$1, _d;
const $instances = Symbol('instances');
const $activateListener = Symbol('activateListener');
const $deactivateListener = Symbol('deactivateListener');
const $notifyInstances = Symbol('notifyInstances');
const $notify = Symbol('notify');
const $scrollCallback = Symbol('callback');
class ScrollObserver {
constructor(callback) {
this[$scrollCallback] = callback;
}
static [$notifyInstances]() {
for (const instance of ScrollObserver[$instances]) {
instance[$notify]();
}
}
static [(_a$1 = $instances, $activateListener)]() {
window.addEventListener('scroll', this[$notifyInstances], { passive: true });
}
static [$deactivateListener]() {
window.removeEventListener('scroll', this[$notifyInstances]);
}
observe() {
if (ScrollObserver[$instances].size === 0) {
ScrollObserver[$activateListener]();
}
ScrollObserver[$instances].add(this);
}
disconnect() {
ScrollObserver[$instances].delete(this);
if (ScrollObserver[$instances].size === 0) {
ScrollObserver[$deactivateListener]();
}
}
[$notify]() {
this[$scrollCallback]();
}
;
}
ScrollObserver[_a$1] = new Set();
const $computeStyleCallback = Symbol('computeStyleCallback');
const $astWalker = Symbol('astWalker');
const $dependencies = Symbol('dependencies');
const $scrollHandler = Symbol('scrollHandler');
const $onScroll = Symbol('onScroll');
class StyleEffector {
constructor(callback) {
this[_b$1] = {};
this[_c$1] = new ASTWalker(['function']);
this[_d] = () => this[$onScroll]();
this[$computeStyleCallback] = callback;
}
observeEffectsFor(ast) {
const newDependencies = {};
const oldDependencies = this[$dependencies];
this[$astWalker].walk(ast, functionNode => {
const { name } = functionNode;
const firstArgument = functionNode.arguments[0];
const firstTerm = firstArgument.terms[0];
if (name.value !==