diff --git a/demo/view-timeline/with-math-value-range.html b/demo/view-timeline/with-math-value-range.html
new file mode 100644
index 00000000..b99d20a7
--- /dev/null
+++ b/demo/view-timeline/with-math-value-range.html
@@ -0,0 +1,128 @@
+
+
+
+
+
+ view-timeline demo
+
+
+
+
+
+
cover
+
cover calc(0% + 200px) calc(100% - 100px)
+
+
+
+
+
+
+
+
+
+
diff --git a/src/numeric-values.js b/src/numeric-values.js
new file mode 100644
index 00000000..aaf251c4
--- /dev/null
+++ b/src/numeric-values.js
@@ -0,0 +1,408 @@
+/**
+ * @typedef {{[string]: integer}} UnitMap
+ * @typedef {[number, UnitMap]} SumValueItem
+ * @typedef {SumValueItem[]} SumValue
+ * @typedef {null} Failure
+ * @typedef {{[string]: integer} & {percentHint: string | undefined}} Type
+ */
+
+const failure = null;
+const baseTypes = ["percent", "length", "angle", "time", "frequency", "resolution", "flex"];
+
+const unitGroups = {
+ // https://www.w3.org/TR/css-values-4/#font-relative-lengths
+ fontRelativeLengths: {
+ units: new Set(["em", "rem", "ex", "rex", "cap", "rcap", "ch", "rch", "ic", "ric", "lh", "rlh"])
+ },
+ // https://www.w3.org/TR/css-values-4/#viewport-relative-lengths
+ viewportRelativeLengths: {
+ units: new Set(
+ ["vw", "lvw", "svw", "dvw", "vh", "lvh", "svh", "dvh", "vi", "lvi", "svi", "dvi", "vb", "lvb", "svb", "dvb",
+ "vmin", "lvmin", "svmin", "dvmin", "vmax", "lvmax", "svmax", "dvmax"])
+ },
+ // https://www.w3.org/TR/css-values-4/#absolute-lengths
+ absoluteLengths: {
+ units: new Set(["cm", "mm", "Q", "in", "pt", "pc", "px"]),
+ compatible: true,
+ canonicalUnit: "px",
+ ratios: {
+ "cm": 96 / 2.54, "mm": (96 / 2.54) / 10, "Q": (96 / 2.54) / 40, "in": 96, "pc": 96 / 6, "pt": 96 / 72, "px": 1
+ }
+ },
+ // https://www.w3.org/TR/css-values-4/#angles
+ angle: {
+ units: new Set(["deg", "grad", "rad", "turn"]),
+ compatible: true,
+ canonicalUnit: "deg",
+ ratios: {
+ "deg": 1, "grad": 360 / 400, "rad": 180 / Math.PI, "turn": 360
+ }
+ },
+ // https://www.w3.org/TR/css-values-4/#time
+ time: {
+ units: new Set(["s", "ms"]),
+ compatible: true,
+ canonicalUnit: "s",
+ ratios: {
+ "s": 1, "ms": 1 / 1000
+ }
+ },
+ // https://www.w3.org/TR/css-values-4/#frequency
+ frequency: {
+ units: new Set(["hz", "khz"]),
+ compatible: true,
+ canonicalUnit: "hz",
+ ratios: {
+ "hz": 1, "khz": 1000
+ }
+ },
+ // https://www.w3.org/TR/css-values-4/#resolution
+ resolution: {
+ units: new Set(["dpi", "dpcm", "dppx"]),
+ compatible: true,
+ canonicalUnit: "dppx",
+ ratios: {
+ "dpi": 1 / 96, "dpcm": 2.54 / 96, "dppx": 1
+ }
+ }
+};
+
+const unitToCompatibleUnitsMap = new Map();
+for (const group of Object.values(unitGroups)) {
+ if (!group.compatible) {
+ continue;
+ }
+ for (const unit of group.units) {
+ unitToCompatibleUnitsMap.set(unit, group);
+ }
+}
+
+export function getSetOfCompatibleUnits(unit) {
+ return unitToCompatibleUnitsMap.get(unit);
+}
+
+/**
+ * Implementation of `product of two unit maps` from css-typed-om-1:
+ * https://www.w3.org/TR/css-typed-om-1/#product-of-two-unit-maps
+ *
+ * @param {UnitMap} units1 map of units (strings) to powers (integers)
+ * @param {UnitMap} units2 map of units (strings) to powers (integers)
+ * @return {UnitMap} map of units (strings) to powers (integers)
+ */
+function productOfTwoUnitMaps(units1, units2) {
+ // 1. Let result be a copy of units1.
+ const result = {...units1};
+ // 2. For each unit → power in units2:
+ for (const unit of Object.keys(units2)) {
+ if (result[unit]) {
+ // 1. If result[unit] exists, increment result[unit] by power.
+ result[unit] += units2[unit];
+ } else {
+ // 2. Otherwise, set result[unit] to power.
+ result[unit] = units2[unit];
+ }
+ }
+ // 3. Return result.
+ return result;
+}
+
+/**
+ * Implementation of `create a type` from css-typed-om-1:
+ * https://www.w3.org/TR/css-typed-om-1/#create-a-type
+ *
+ * @param {string} unit
+ * @return {Type|Failure}
+ */
+export function createAType(unit) {
+ if (unit === "number") {
+ return {};
+ } else if (unit === "percent") {
+ return {"percent": 1};
+ } else if (unitGroups.absoluteLengths.units.has(unit) || unitGroups.fontRelativeLengths.units.has(unit) ||
+ unitGroups.viewportRelativeLengths.units.has(unit)) {
+ return {"length": 1};
+ } else if (unitGroups.angle.units.has(unit)) {
+ return {"angle": 1};
+ } else if (unitGroups.time.units.has(unit)) {
+ return {"time": 1};
+ } else if (unitGroups.frequency.units.has(unit)) {
+ return {"frequency": 1};
+ } else if (unitGroups.resolution.units.has(unit)) {
+ return {"resolution": 1};
+ } else if (unit === "fr") {
+ return {"flex": 1};
+ } else {
+ return failure;
+ }
+}
+
+/**
+ * Partial implementation of `create a sum value` from css-typed-om-1:
+ * https://www.w3.org/TR/css-typed-om-1/#create-a-sum-value
+ *
+ * Supports CSSUnitValue, CSSMathProduct and CSSMathInvert with a CSSUnitValue value.
+ * Other types are not supported, and will throw an error.
+ *
+ * @param {CSSNumericValue} cssNumericValue
+ * @return {SumValue} Abstract representation of a CSSNumericValue as a sum of numbers with (possibly complex) units
+ */
+export function createSumValue(cssNumericValue) {
+ if (cssNumericValue instanceof CSSUnitValue) {
+ let {unit, value} = cssNumericValue;
+ // Let unit be the value of this’s unit internal slot, and value be the value of this’s value internal slot.
+ // If unit is a member of a set of compatible units, and is not the set’s canonical unit,
+ // multiply value by the conversion ratio between unit and the canonical unit, and change unit to the canonical unit.
+ const compatibleUnits = getSetOfCompatibleUnits(cssNumericValue.unit);
+ if (compatibleUnits && unit !== compatibleUnits.canonicalUnit) {
+ value *= compatibleUnits.ratios[unit];
+ unit = compatibleUnits.canonicalUnit;
+ }
+
+ if (unit === "number") {
+ // If unit is "number", return «(value, «[ ]»)».
+ return [[value, {}]];
+ } else {
+ // Otherwise, return «(value, «[unit → 1]»)».
+ return [[value, {[unit]: 1}]];
+ }
+ } else if (cssNumericValue instanceof CSSMathInvert) {
+ if (!(cssNumericValue.value instanceof CSSUnitValue)) {
+ // Limit implementation to CSSMathInvert of CSSUnitValue
+ throw new Error("Not implemented");
+ }
+ // 1. Let values be the result of creating a sum value from this’s value internal slot.
+ const values = createSumValue(cssNumericValue.value);
+ // 2. If values is failure, return failure.
+ if (values === failure) {
+ return failure;
+ }
+ // 3. If the length of values is more than one, return failure.
+ if (values.length > 1) {
+ return failure;
+ }
+ // 4. Invert (find the reciprocal of) the value of the item in values, and negate the value of each entry in its unit map.
+ const item = values[0];
+ const tempUnionMap = {};
+ for (const [unit, power] of Object.entries(item[1])) {
+ tempUnionMap[unit] = -1 * power;
+ }
+ values[0] = [1 / item[0], tempUnionMap];
+
+ // 5. Return values.
+ return values;
+ } else if (cssNumericValue instanceof CSSMathProduct) {
+ // 1. Let values initially be the sum value «(1, «[ ]»)». (I.e. what you’d get from 1.)
+
+ let values = [[1, {}]];
+
+ // 2. For each item in this’s values internal slot:
+ for (const item of cssNumericValue.values) {
+ // 1. Let new values be the result of creating a sum value from item. Let temp initially be an empty list.
+ const newValues = createSumValue(item);
+ const temp = [];
+ // 2. If new values is failure, return failure.
+ if (newValues === failure) {
+ return failure;
+ }
+ // 3. For each item1 in values:
+ for (const item1 of values) {
+ // 1. For each item2 in new values:
+ for (const item2 of newValues) {
+ // 1. Let item be a tuple with its value set to the product of the values of item1 and item2, and its unit
+ // map set to the product of the unit maps of item1 and item2, with all entries with a zero value removed.
+ // 2. Append item to temp.
+ temp.push([item1[0] * item2[0], productOfTwoUnitMaps(item1[1], item2[1])]);
+ }
+ }
+ // 4. Set values to temp.
+ values = temp;
+ }
+ // Return values.
+ return values;
+ } else {
+ throw new Error("Not implemented");
+ }
+}
+
+
+/**
+ * Implementation of `to(unit)` for CSSNumericValue from css-typed-om-1:
+ * https://www.w3.org/TR/css-typed-om-1/#dom-cssnumericvalue-to
+ *
+ * Converts an existing CSSNumeric value into another with the specified unit, if possible.
+ *
+ * @param {CSSNumericValue} cssNumericValue value to convert
+ * @param {string} unit
+ * @return {CSSUnitValue}
+ */
+export function to(cssNumericValue, unit) {
+ // Let type be the result of creating a type from unit. If type is failure, throw a SyntaxError.
+ const type = createAType(unit);
+ if (type === failure) {
+ throw new SyntaxError("The string did not match the expected pattern.");
+ }
+
+ // Let sum be the result of creating a sum value from this.
+ const sumValue = createSumValue(cssNumericValue);
+
+ // If sum is failure, throw a TypeError.
+ if (!sumValue) {
+ throw new TypeError();
+ }
+
+ // If sum has more than one item, throw a TypeError.
+ if (sumValue.length > 1) {
+ throw new TypeError("Sum has more than one item");
+ }
+
+ // Otherwise, let item be the result of creating a CSSUnitValue
+ // from the sole item in sum, then converting it to unit.
+ const item = convertCSSUnitValue(createCSSUnitValue(sumValue[0]), unit);
+
+
+ // If item is failure, throw a TypeError.
+ if (item === failure) {
+ throw new TypeError();
+ }
+ // Return item.
+ return item;
+}
+
+/**
+ * Implementation of `create a CSSUnitValue from a sum value item` from css-typed-om-1:
+ * https://www.w3.org/TR/css-typed-om-1/#create-a-cssunitvalue-from-a-sum-value-item
+ *
+ * @param {SumValueItem} sumValueItem a tuple of a value, and a unit map
+ * @return {CSSUnitValue|Failure}
+ */
+export function createCSSUnitValue(sumValueItem) {
+ const [value, unitMap] = sumValueItem;
+ // When asked to create a CSSUnitValue from a sum value item item, perform the following steps:
+ // If item has more than one entry in its unit map, return failure.
+ const entries = Object.entries(unitMap);
+ if (entries.length > 1) {
+ return failure;
+ }
+ // If item has no entries in its unit map, return a new CSSUnitValue whose unit internal slot is set to "number",
+ // and whose value internal slot is set to item’s value.
+ if (entries.length === 0) {
+ return new CSSUnitValue(value, "number");
+ }
+ // Otherwise, item has a single entry in its unit map. If that entry’s value is anything other than 1, return failure.
+ const entry = entries[0];
+ if (entry[1] !== 1) {
+ return failure;
+ }
+ // Otherwise, return a new CSSUnitValue whose unit internal slot is set to that entry’s key, and whose value internal slot is set to item’s value.
+ else {
+ return new CSSUnitValue(value, entry[0]);
+ }
+}
+
+/**
+ * Implementation of `convert a CSSUnitValue` from css-typed-om-1:
+ * https://www.w3.org/TR/css-typed-om-1/#convert-a-cssunitvalue
+
+ * @param {CSSUnitValue} cssUnitValue
+ * @param {string} unit
+ * @return {CSSUnitValue|Failure}
+ */
+export function convertCSSUnitValue(cssUnitValue, unit) {
+ // Let old unit be the value of this’s unit internal slot, and old value be the value of this’s value internal slot.
+ const oldUnit = cssUnitValue.unit;
+ const oldValue = cssUnitValue.value;
+ // If old unit and unit are not compatible units, return failure.
+ const oldCompatibleUnitGroup = getSetOfCompatibleUnits(oldUnit);
+ const compatibleUnitGroup = getSetOfCompatibleUnits(unit);
+ if (!compatibleUnitGroup || oldCompatibleUnitGroup !== compatibleUnitGroup) {
+ return failure;
+ }
+ // Return a new CSSUnitValue whose unit internal slot is set to unit, and whose value internal slot is set to
+ // old value multiplied by the conversation ratio between old unit and unit.
+ return new CSSUnitValue(oldValue * compatibleUnitGroup.ratios[oldUnit] / compatibleUnitGroup.ratios[unit], unit);
+}
+
+/**
+ * Partial implementation of `toSum(...units)`:
+ * https://www.w3.org/TR/css-typed-om-1/#dom-cssnumericvalue-tosum
+ *
+ * The implementation is restricted to conversion without units.
+ * It simplifies a CSSNumericValue into a minimal sum of CSSUnitValues.
+ * Will throw an error if called with units.
+ *
+ * @param {CSSNumericValue} cssNumericValue value to convert to a CSSMathSum
+ * @param {string[]} units Not supported in this implementation
+ * @return {CSSMathSum}
+ */
+export function toSum(cssNumericValue, ...units) {
+ // The toSum(...units) method converts an existing CSSNumericValue this into a CSSMathSum of only CSSUnitValues
+ // with the specified units, if possible. (It’s like to(), but allows the result to have multiple units in it.)
+ // If called without any units, it just simplifies this into a minimal sum of CSSUnitValues.
+ // When called, it must perform the following steps:
+ //
+ // For each unit in units, if the result of creating a type from unit is failure, throw a SyntaxError.
+ //
+ if (units && units.length) {
+ // Only unitless method calls are implemented in this polyfill
+ throw new Error("Not implemented");
+ }
+
+ // Let sum be the result of creating a sum value from this. If sum is failure, throw a TypeError.
+ const sum = createSumValue(cssNumericValue);
+
+ // Let values be the result of creating a CSSUnitValue for each item in sum. If any item of values is failure,
+ // throw a TypeError.
+ const values = sum.map(item => createCSSUnitValue(item));
+ if (values.some(value => value === failure)) {
+ throw new TypeError("Type error");
+ }
+
+ // If units is empty, sort values in code point order according to the unit internal slot of its items,
+ // then return a new CSSMathSum object whose values internal slot is set to values.
+ return new CSSMathSum(...values);
+}
+
+/**
+ * Implementation of `invert a type` from css-typed-om-1 Editors Draft:
+ * https://drafts.css-houdini.org/css-typed-om/
+ *
+ * @param {Type} type
+ * @return {Type}
+ */
+export function invertType(type) {
+ // To invert a type type, perform the following steps:
+ // Let result be a new type with an initially empty ordered map and an initially null percent hint
+ // For each unit → exponent of type, set result[unit] to (-1 * exponent).
+ // Return result.
+ const result = {};
+ for (const baseType of baseTypes) {
+ result[baseType] = -1 * type[baseType];
+ }
+ return result;
+}
+
+/**
+ * Implementation of `multiply two types` from css-typed-om-1 Editor's Draft:
+ * https://drafts.css-houdini.org/css-typed-om/#cssnumericvalue-multiply-two-types
+ *
+ * @param {Type} type1 a map of base types to integers and an associated percent hint
+ * @param {Type} type2 a map of base types to integers and an associated percent hint
+ * @return {Type|Failure}
+ */
+export function multiplyTypes(type1, type2) {
+ if (type1.percentHint && type2.percentHint && type1.percentHint !== type2.percentHint) {
+ return failure;
+ }
+ const finalType = {
+ ...type1, percentHint: type1.percentHint ?? type2.percentHint,
+ };
+
+ for (const baseType of baseTypes) {
+ if (!type2[baseType]) {
+ continue;
+ }
+ finalType[baseType] ??= 0;
+ finalType[baseType] += type2[baseType];
+ }
+ return finalType;
+}
\ No newline at end of file
diff --git a/src/proxy-cssom.js b/src/proxy-cssom.js
index da068b66..656d4553 100644
--- a/src/proxy-cssom.js
+++ b/src/proxy-cssom.js
@@ -11,6 +11,7 @@
// 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.
+import { createAType, invertType, multiplyTypes, to, toSum } from "./numeric-values";
export function installCSSOM() {
// Object for storing details associated with an object which are to be kept
@@ -88,6 +89,20 @@ export function installCSSOM() {
return privateDetails.get(this).unit;
}
+ to(unit) {
+ return to(this, unit)
+ }
+
+ toSum(...units) {
+ return toSum(this, ...units)
+ }
+
+ type() {
+ const details = privateDetails.get(this)
+ // The type of a CSSUnitValue is the result of creating a type from its unit internal slot.
+ return createAType(details.unit)
+ }
+
toString() {
const details = privateDetails.get(this);
return `${details.value}${displayUnit(details.unit)}`;
@@ -114,6 +129,16 @@ export function installCSSOM() {
constructor(values) {
super(arguments, 'product', 'calc', ' * ');
}
+
+ toSum(...units) {
+ return toSum(this, ...units)
+ }
+
+ type() {
+ const values = privateDetails.get(this).values;
+ // The type is the result of multiplying the types of each of the items in its values internal slot.
+ return values.map(v => v.type()).reduce(multiplyTypes)
+ }
},
'CSSMathNegate': class extends MathOperation {
@@ -134,6 +159,12 @@ export function installCSSOM() {
get value() {
return privateDetails.get(this).values[1];
}
+
+ type() {
+ const details = privateDetails.get(this)
+ // The type of a CSSUnitValue is the result of creating a type from its unit internal slot.
+ return invertType(details.values[1].type())
+ }
},
'CSSMathMax': class extends MathOperation {
diff --git a/src/scroll-timeline-base.js b/src/scroll-timeline-base.js
index 8050c56c..f37a23d2 100644
--- a/src/scroll-timeline-base.js
+++ b/src/scroll-timeline-base.js
@@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { installCSSOM } from "./proxy-cssom.js";
+import {installCSSOM} from "./proxy-cssom.js";
+import {simplifyCalculation} from "./simplify-calculation";
+
installCSSOM();
const DEFAULT_TIMELINE_AXIS = 'block';
@@ -659,20 +661,26 @@ function parseInset(value, containerSize) {
// Calculate the fractional offset of a (phase, percent) pair relative to the
// full cover range.
-export function relativePosition(timeline, phase, percent) {
+export function relativePosition(timeline, phase, offset) {
const phaseRange = range(timeline, phase);
const coverRange = range(timeline, 'cover');
- return calculateRelativePosition(phaseRange, percent, coverRange);
+ return calculateRelativePosition(phaseRange, offset, coverRange);
}
-export function calculateRelativePosition(phaseRange, percent, coverRange) {
+
+
+export function calculateRelativePosition(phaseRange, offset, coverRange) {
if (!phaseRange || !coverRange)
return 0;
- const fraction = percent.value / 100;
- const offset =
- (phaseRange.end - phaseRange.start) * fraction + phaseRange.start;
- return (offset - coverRange.start) / (coverRange.end - coverRange.start);
+ const info = {percentageReference: new CSSUnitValue(phaseRange.end - phaseRange.start, "px")};
+ const simplifiedRangeOffset = simplifyCalculation(offset, info);
+ if (!(simplifiedRangeOffset instanceof CSSUnitValue) || simplifiedRangeOffset.unit !== 'px') {
+ throw new Error(`Unsupported offset '${simplifiedRangeOffset.toString()}'`)
+ }
+
+ const offsetPX = simplifiedRangeOffset.value + phaseRange.start;
+ return (offsetPX - coverRange.start) / (coverRange.end - coverRange.start);
}
// https://drafts.csswg.org/scroll-animations-1/#view-progress-timelines
diff --git a/src/simplify-calculation.js b/src/simplify-calculation.js
new file mode 100644
index 00000000..da76e7fb
--- /dev/null
+++ b/src/simplify-calculation.js
@@ -0,0 +1,301 @@
+import {isCanonical} from "./utils";
+
+/**
+ * @typedef {{percentageReference: CSSUnitValue}} Info
+ */
+
+/**
+ * Groups a list of objects by a given string keyed property
+ *
+ * @template T
+ * @param {T[]} items
+ * @param {string} key string key
+ * @return {Map}
+ */
+function groupBy(items, key) {
+ return items.reduce((groups, item) => {
+ if (groups.has(item[key])) {
+ groups.get(item[key]).push(item);
+ } else {
+ groups.set(item[key], [item]);
+ }
+ return groups;
+ }, new Map());
+}
+
+/**
+ * Partitions a list into a tuple of lists.
+ * The first item in the tuple contains a list of items that pass the test provided by the callback function.
+ * The second item in the tuple contains the remaining items
+ *
+ * @template T
+ * @param {T[]} items
+ * @param {(item:T) => boolean} callbackFn Returns truthy if item should be put in the first list in the tuple, falsy if it should be put in the second list.
+ * @return {[T[],T[]]}
+ */
+function partition(items, callbackFn) {
+ const partA = [];
+ const partB = [];
+ for (const item of items) {
+ if (callbackFn(item)) {
+ partA.push(item);
+ } else {
+ partB.push(item);
+ }
+ }
+ return [partA, partB];
+}
+
+/**
+ * Partial implementation of `simplify a calculation tree` applied to CSSNumericValue
+ * https://www.w3.org/TR/css-values-4/#simplify-a-calculation-tree
+ *
+ * @param {CSSNumericValue} root
+ * @param {Info} info information used to resolve
+ * @return {CSSNumericValue}
+ */
+export function simplifyCalculation(root, info) {
+ function simplifyNumericArray(values) {
+ return Array.from(values).map((value) => simplifyCalculation(value, info));
+ }
+
+ // To simplify a calculation tree root:
+ if (root instanceof CSSUnitValue) {
+ // 1. If root is a numeric value:
+
+ if (root.unit === "percent" && info.percentageReference) {
+ // 1. If root is a percentage that will be resolved against another value, and there is enough information
+ // available to resolve it, do so, and express the resulting numeric value in the appropriate canonical unit.
+ // Return the value.
+ const resolvedValue = (root.value / 100) * info.percentageReference.value;
+ const resolvedUnit = info.percentageReference.unit;
+ return new CSSUnitValue(resolvedValue, resolvedUnit);
+ }
+
+ // 2. If root is a dimension that is not expressed in its canonical unit, and there is enough information available
+ // to convert it to the canonical unit, do so, and return the value.
+
+ // Use Typed OM toSum() to convert values in compatible sets to canonical units
+ const sum = root.toSum();
+ if (sum && sum.values.length === 1) {
+ root = sum.values[0];
+ }
+ // TODO: handle relative lengths
+
+ // 3. If root is a , return its numeric value.
+ // 4. Otherwise, return root.
+ return root;
+ }
+
+ // 2. If root is any other leaf node (not an operator node):
+ if (!root.operator) {
+ // 1. If there is enough information available to determine its numeric value, return its value, expressed in the value’s canonical unit.
+ // 2. Otherwise, return root.
+ return root;
+ }
+
+ // 3. At this point, root is an operator node. Simplify all the calculation children of root.
+ switch (root.operator) {
+ case "sum":
+ root = new CSSMathSum(...simplifyNumericArray(root.values));
+ break;
+ case "product":
+ root = new CSSMathProduct(...simplifyNumericArray(root.values));
+ break;
+ case "negate":
+ root = new CSSMathNegate(simplifyCalculation(root.value, info));
+ break;
+ case "clamp":
+ root = new CSSMathClamp(simplifyCalculation(root.lower, info), simplifyCalculation(root.value, info),
+ simplifyCalculation(root.upper, info));
+ break;
+ case "invert":
+ root = new CSSMathInvert(simplifyCalculation(root.value, info));
+ break;
+ case "min":
+ root = new CSSMathMin(...simplifyNumericArray(root.values));
+ break;
+ case "max":
+ root = new CSSMathMax(...simplifyNumericArray(root.values));
+ break;
+ }
+
+ // 4. If root is an operator node that’s not one of the calc-operator nodes, and all of its calculation children are
+ // numeric values with enough information to compute the operation root represents, return the result of running
+ // root’s operation using its children, expressed in the result’s canonical unit.
+ if (root instanceof CSSMathMin || root instanceof CSSMathMax) {
+ const children = Array.from(root.values);
+ if (children.every(
+ (child) => child instanceof CSSUnitValue && child.unit !== "percent" && isCanonical(child.unit) && child.unit ===
+ children[0].unit)) {
+
+ const result = Math[root.operator].apply(Math, children.map(({value}) => value));
+ return new CSSUnitValue(result, children[0].unit);
+ }
+ }
+
+ // Note: If a percentage is left at this point, it will usually block simplification of the node, since it needs to be
+ // resolved against another value using information not currently available. (Otherwise, it would have been converted
+ // to a different value in an earlier step.) This includes operations such as "min", since percentages might resolve
+ // against a negative basis, and thus end up with an opposite comparative relationship than the raw percentage value
+ // would seem to indicate.
+ //
+ // However, "raw" percentages—ones which do not resolve against another value, such as in opacity—might not block
+ // simplification.
+
+ // 5. If root is a Min or Max node, attempt to partially simplify it:
+ if (root instanceof CSSMathMin || root instanceof CSSMathMax) {
+ const children = Array.from(root.values);
+ const [numeric, rest] = partition(children, (child) => child instanceof CSSUnitValue && child.unit !== "percent");
+ const unitGroups = Array.from(groupBy(numeric, "unit").values());
+ // 1. For each node child of root’s children:
+ //
+ // If child is a numeric value with enough information to compare magnitudes with another child of the same
+ // unit (see note in previous step), and there are other children of root that are numeric children with the same
+ // unit, combine all such children with the appropriate operator per root, and replace child with the result,
+ // removing all other child nodes involved.
+ const hasComparableChildren = unitGroups.some(group => group.length > 0);
+ if (hasComparableChildren) {
+ const combinedGroups = unitGroups.map(group => {
+ const result = Math[root.operator].apply(Math, group.map(({value}) => value));
+ return new CSSUnitValue(result, group[0].unit);
+ });
+ if (root instanceof CSSMathMin) {
+ root = new CSSMathMin(...combinedGroups, ...rest);
+ } else {
+ root = new CSSMathMax(...combinedGroups, ...rest);
+ }
+ }
+
+ // 2. Return root.
+ return root;
+ }
+
+ // If root is a Negate node:
+ //
+ // If root’s child is a numeric value, return an equivalent numeric value, but with the value negated (0 - value).
+ // If root’s child is a Negate node, return the child’s child.
+ // Return root.
+ if (root instanceof CSSMathNegate) {
+ if (root.value instanceof CSSUnitValue) {
+ return new CSSUnitValue(0 - root.value.value, root.value.unit);
+ } else if (root.value instanceof CSSMathNegate) {
+ return root.value.value;
+ } else {
+ return root;
+ }
+ }
+
+ // If root is an Invert node:
+ //
+ // If root’s child is a number (not a percentage or dimension) return the reciprocal of the child’s value.
+ // If root’s child is an Invert node, return the child’s child.
+ // Return root.
+ if (root instanceof CSSMathInvert) {
+ if (root.value instanceof CSSMathInvert) {
+ return root.value.value;
+ } else {
+ return root;
+ }
+ }
+
+ // If root is a Sum node:
+ if (root instanceof CSSMathSum) {
+ let children = [];
+ // For each of root’s children that are Sum nodes, replace them with their children.
+ for (const value of root.values) {
+ if (value instanceof CSSMathSum) {
+ children.push(...value.values);
+ } else {
+ children.push(value);
+ }
+ }
+
+ // For each set of root’s children that are numeric values with identical units, remove those children and
+ // replace them with a single numeric value containing the sum of the removed nodes, and with the same unit.
+ //
+ // (E.g. combine numbers, combine percentages, combine px values, etc.)
+ function sumValuesWithSameUnit(values) {
+ const numericValues = values.filter((c) => c instanceof CSSUnitValue);
+ const nonNumericValues = values.filter((c) => !(c instanceof CSSUnitValue));
+
+ const summedNumericValues = Array.from(groupBy(numericValues, "unit").entries())
+ .map(([unit, values]) => {
+ const sum = values.reduce((a, {value}) => a + value, 0);
+ return new CSSUnitValue(sum, unit);
+ });
+ return [...nonNumericValues, ...summedNumericValues];
+ }
+
+ children = sumValuesWithSameUnit(children);
+
+ // If root has only a single child at this point, return the child. Otherwise, return root.
+ // NOTE: Zero-valued terms cannot be simply removed from a Sum; they can only be combined with other values
+ // that have identical units. (This is because the mere presence of a unit, even with a zero value,
+ // can sometimes imply a change in behavior.)
+ if (children.length === 1) {
+ return children[0];
+ } else {
+ return new CSSMathSum(...children);
+ }
+ }
+
+ // If root is a Product node:
+ //
+ // For each of root’s children that are Product nodes, replace them with their children.
+ if (root instanceof CSSMathProduct) {
+ let children = [];
+ for (const value of root.values) {
+ if (value instanceof CSSMathProduct) {
+ children.push(...value.values);
+ } else {
+ children.push(value);
+ }
+ }
+
+ // If root has multiple children that are numbers (not percentages or dimensions), remove them and replace them with
+ // a single number containing the product of the removed nodes.
+ const [numbers, rest] = partition(children, (child) => child instanceof CSSUnitValue && child.unit === "number");
+ if (numbers.length > 1) {
+ const product = numbers.reduce((a, {value}) => a * value, 1);
+ children = [new CSSUnitValue(product, "number"), ...rest];
+ }
+
+ // If root contains only two children, one of which is a number (not a percentage or dimension) and the other of
+ // which is a Sum whose children are all numeric values, multiply all of the Sum’s children by the number,
+ // then return the Sum.
+ if (children.length === 2) {
+ let numeric, sum;
+ for (const child of children) {
+ if (child instanceof CSSUnitValue && child.unit === "number") {
+ numeric = child;
+ } else if (child instanceof CSSMathSum && [...child.values].every((c) => c instanceof CSSUnitValue)) {
+ sum = child;
+ }
+ }
+ if (numeric && sum) {
+ return new CSSMathSum(
+ ...[...sum.values].map((value) => new CSSUnitValue(value.value * numeric.value, value.unit)));
+ }
+ }
+
+ // If root contains only numeric values and/or Invert nodes containing numeric values, and multiplying the types of
+ // all the children (noting that the type of an Invert node is the inverse of its child’s type) results in a type
+ // that matches any of the types that a math function can resolve to, return the result of multiplying all the values
+ // of the children (noting that the value of an Invert node is the reciprocal of its child’s value),
+ // expressed in the result’s canonical unit.
+ if (children.every((child) => (child instanceof CSSUnitValue && isCanonical(child.unit)) ||
+ (child instanceof CSSMathInvert && child.value instanceof CSSUnitValue && isCanonical(child.value.unit)))) {
+ // Use CSS Typed OM to multiply types
+ const sum = new CSSMathProduct(...children).toSum();
+ if (sum && sum.values.length === 1) {
+ return sum.values[0];
+ }
+ }
+
+ // Return root.
+ return new CSSMathProduct(...children);
+ }
+ // Return root.
+ return root;
+}
diff --git a/src/utils.js b/src/utils.js
index e18f1625..4b5d18b8 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -11,4 +11,10 @@ export function parseLength(obj, acceptStr) {
return new CSSUnitValue(value, unit);
}
return null;
+}
+
+const canonicalUnits = new Set(["px", "deg", "s", "hz", "dppx", "number", "fr"]);
+
+export function isCanonical(unit) {
+ return canonicalUnits.has(unit.toLowerCase());
}
\ No newline at end of file