UNPKG

@google/model-viewer

Version:

Easily display interactive 3D models on the web and in AR!

1,204 lines (1,192 loc) 1.62 MB
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 !==