UNPKG

@google/model-viewer

Version:

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

397 lines 16.4 kB
/* @license * Copyright 2019 Google LLC. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var _a, _b, _c; import { normalizeUnit } from './conversions'; import { numberNode, ZERO } from './parsers'; const $evaluate = Symbol('evaluate'); const $lastValue = Symbol('lastValue'); /** * An Evaluator is used to derive a computed style from part (or all) of a CSS * expression AST. This construct is particularly useful for complex ASTs * containing function calls such as calc, var and env. Such styles could be * costly to re-evaluate on every frame (and in some cases we may try to do * that). The Evaluator construct allows us to mark sub-trees of the AST as * constant, so that only the dynamic parts are re-evaluated. It also separates * one-time AST preparation work from work that necessarily has to happen upon * each evaluation. */ export class Evaluator { constructor() { this[_a] = null; } /** * An Evaluatable is a NumberNode or an Evaluator that evaluates a NumberNode * as the result of invoking its evaluate method. This is mainly used to * ensure that CSS function nodes are cast to the corresponding Evaluators * that will resolve the result of the function, but is also used to ensure * that a percentage nested at arbitrary depth in the expression will always * be evaluated against the correct basis. */ 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; } /** * If the input is an Evaluator, returns the result of evaluating it. * Otherwise, returns the input. * * This is a helper to aide in resolving a NumberNode without conditionally * checking if the Evaluatable is an Evaluator everywhere. */ static evaluate(evaluatable) { if (evaluatable instanceof Evaluator) { return evaluatable.evaluate(); } return evaluatable; } /** * If the input is an Evaluator, returns the value of its isConstant property. * Returns true for all other input values. */ static isConstant(evaluatable) { if (evaluatable instanceof Evaluator) { return evaluatable.isConstant; } return true; } /** * This method applies a set of structured intrinsic metadata to an evaluated * result from a parsed CSS-like string of expressions. Intrinsics provide * sufficient metadata (e.g., basis values, analogs for keywords) such that * omitted values in the input string can be backfilled, and keywords can be * converted to concrete numbers. * * The result of applying intrinsics is a tuple of NumberNode values whose * units match the units used by the basis of the intrinsics. * * The following is a high-level description of how intrinsics are applied: * * 1. Determine the value of 'auto' for the current term * 2. If there is no corresponding input value for this term, substitute the * 'auto' value. * 3. If the term is an IdentNode, treat it as a keyword and perform the * appropriate substitution. * 4. If the term is still null, fallback to the 'auto' value * 5. If the term is a percentage, apply it to the basis and return that * value * 6. Normalize the unit of the term * 7. If the term's unit does not match the basis unit, return the basis * value * 8. Return the term as is */ static applyIntrinsics(evaluated, intrinsics) { const { basis, keywords } = intrinsics; const { auto } = keywords; return basis.map((basisNode, index) => { // Use an auto value if we have it, otherwise the auto value is the basis: const autoSubstituteNode = auto[index] == null ? basisNode : auto[index]; // If the evaluated nodes do not have a node at the current // index, fallback to the "auto" substitute right away: let evaluatedNode = evaluated[index] ? evaluated[index] : autoSubstituteNode; // Any ident node is considered a keyword: if (evaluatedNode.type === 'ident') { const keyword = evaluatedNode.value; // Substitute any keywords for concrete values first: if (keyword in keywords) { evaluatedNode = keywords[keyword][index]; } } // If we don't have a NumberNode at this point, fall back to whatever // is specified for auto: if (evaluatedNode == null || evaluatedNode.type === 'ident') { evaluatedNode = autoSubstituteNode; } // For percentages, we always apply the percentage to the basis value: if (evaluatedNode.unit === '%') { return numberNode(evaluatedNode.number / 100 * basisNode.number, basisNode.unit); } // Otherwise, normalize whatever we have: evaluatedNode = normalizeUnit(evaluatedNode, basisNode); // If the normalized units do not match, return the basis as a fallback: if (evaluatedNode.unit !== basisNode.unit) { return basisNode; } // Finally, return the evaluated node with intrinsics applied: return evaluatedNode; }); } /** * If true, the Evaluator will only evaluate its AST one time. If false, the * Evaluator will re-evaluate the AST each time that the public evaluate * method is invoked. */ get isConstant() { return false; } /** * Evaluate the Evaluator and return the result. If the Evaluator is constant, * the corresponding AST will only be evaluated once, and the result of * evaluating it the first time will be returned on all subsequent * evaluations. */ evaluate() { if (!this.isConstant || this[$lastValue] == null) { this[$lastValue] = this[$evaluate](); } return this[$lastValue]; } } _a = $lastValue; const $percentage = Symbol('percentage'); const $basis = Symbol('basis'); /** * A PercentageEvaluator scales a given basis value by a given percentage value. * The evaluated result is always considered to be constant. */ export 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'); /** * Evaluator for CSS-like env() functions. Currently, only one environment * variable is accepted as an argument for such functions: window-scroll-y. * * The env() Evaluator is explicitly dynamic because it always refers to * external state that changes as the user scrolls, so it should always be * re-evaluated to ensure we get the most recent value. * * Some important notes about this feature include: * * - There is no such thing as a "window-scroll-y" CSS environment variable in * any stable browser at the time that this comment is being written. * - The actual CSS env() function accepts a second argument as a fallback for * the case that the specified first argument isn't set; our syntax does not * support this second argument. * * @see https://developer.mozilla.org/en-US/docs/Web/CSS/env */ export 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'); /** * Evaluator for CSS-like calc() functions. Our implementation of calc() * evaluation currently support nested function calls, an unlimited number of * terms, and all four algebraic operators (+, -, * and /). * * The Evaluator is marked as constant unless the calc expression contains an * internal env expression at any depth, in which case it will be marked as * dynamic. * * @see https://www.w3.org/TR/css-values-3/#calc-syntax * @see https://developer.mozilla.org/en-US/docs/Web/CSS/calc */ export 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))); } // There should only be one combined evaluator at this point: 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'); /** * An Evaluator for the operators found inside CSS calc() functions. * The evaluator accepts an operator and left/right operands. The operands can * be any valid expression term typically allowed inside a CSS calc function. * * As detail of this implementation, the only supported unit types are angles * expressed as radians or degrees, and lengths expressed as meters, centimeters * or millimeters. * * @see https://developer.mozilla.org/en-US/docs/Web/CSS/calc */ export 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; // Disallow operations for mismatched normalized units e.g., m and rad: if (rightUnit != null && leftUnit != null && rightUnit != leftUnit) { return ZERO; } // NOTE(cdata): rules for calc type checking are defined here // https://drafts.csswg.org/css-values-3/#calc-type-checking // This is a simplification and may not hold up once we begin to support // additional unit types: 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'); /** * A VectorEvaluator evaluates a series of numeric terms that usually represent * a data structure such as a multi-dimensional vector or a spherical * * The form of the evaluator's result is determined by the Intrinsics that are * given to it when it is constructed. For example, spherical intrinsics would * establish two angle terms and a length term, so the result of evaluating the * evaluator that is configured with spherical intrinsics is a three element * array where the first two elements represent angles in radians and the third * element representing a length in meters. */ export 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 => numberNode.number); } } //# sourceMappingURL=evaluators.js.map