Skip to content

Commit

Permalink
Parse math functions in animation-range-start and animation-range-end
Browse files Browse the repository at this point in the history
  • Loading branch information
johannesodland committed Nov 25, 2023
1 parent 5d44058 commit 4732d02
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 29 deletions.
47 changes: 36 additions & 11 deletions demo/view-timeline/with-math-value-range.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,15 @@
<div style="height: 99px"></div>
<div style="border-top: 1px solid red"></div>
</div>

<input type="radio" name="range-type" id="as-string" checked>
<label for="as-string">Range as string</label>
<input type="radio" name="range-type" id="as-timeline-range-offset">
<label for="as-timeline-range-offset">Range as TimelineRangeOffset </label>
</body>
<script src="../../dist/scroll-timeline.js"></script>
<script type="text/javascript">
"use strict";

let ranges = [ "cover calc(0% + 2 * 100px)", "cover calc(100% - 100px)"]
const progressBars = document.querySelectorAll('.progress-bar-progress');
const createProgressAnimation = (bar, rangeStart, rangeEnd, axis, inset = 'auto') => {
const subject = document.getElementById('subject');
Expand All @@ -112,17 +115,39 @@
});
createProgressAnimation(progressBars[0], { rangeName: 'cover', offset: CSS.percent(0) },
{ rangeName: 'cover', offset: CSS.percent(100) }, axis);
createProgressAnimation(progressBars[1],
{
rangeName: "cover",
offset: new CSSMathSum(CSS.percent(0), new CSSMathProduct(CSS.number(2), CSS.px(100))),
},
{
rangeName: "cover",
offset: new CSSMathSum(CSS.percent(100), CSS.px(-100)),
}, axis)
createProgressAnimation(progressBars[1], ranges[0], ranges[1], axis);
};

document.querySelectorAll('input').forEach(input => {
input.addEventListener('change', (evt) => {
document.getAnimations().forEach(anim => {
anim.cancel();
});

switch (event.target.name) {
case 'range-type':
const selection = event.target.id;
switch (selection) {
case 'as-string':
ranges = [ "cover calc(0% + 2 * 100px)", "cover calc(100% - 100px)"]
break;
case 'as-timeline-range-offset':
ranges = [{
rangeName: 'cover',
offset: new CSSMathSum(CSS.percent(0), new CSSMathProduct(CSS.number(2), CSS.px(100))),
},
{
rangeName: 'cover',
offset: new CSSMathSum(CSS.percent(100), CSS.px(-100)),
}];
break;
}
}

createAnimations();
});
});

createAnimations();
</script>
</html>
22 changes: 4 additions & 18 deletions src/proxy-animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -1731,25 +1731,25 @@ function parseTimelineRangeOffset(value, position) {
if (value instanceof Object) {
if (value.rangeName != undefined) {
rangeName = value.rangeName;
};
}

if (value.offset !== undefined) {
offset = value.offset;
}
}
// Author passed in something like `"cover 100%"`
else {
const parts = value.split(' ');
const parts = value.split(new RegExp(`(${ANIMATION_RANGE_NAMES.join('|')})`)).map(part => part.trim()).filter(Boolean);

if (parts.length === 1) {
if (ANIMATION_RANGE_NAMES.includes(parts[0])) {
rangeName = parts[0];
} else {
offset = parts[0];
offset = CSSNumericValue.parse(parts[0]);
}
} else if (parts.length === 2) {
rangeName = parts[0];
offset = parts[1];
offset = CSSNumericValue.parse(parts[1]);
}
}

Expand All @@ -1758,20 +1758,6 @@ function parseTimelineRangeOffset(value, position) {
throw TypeError("Invalid range name");
}

// Validate and process offset
// TODO: support more than % and px. Don’t forget about calc() along with that.
if (!(offset instanceof Object)) {
if (offset.endsWith('%')) {
offset = CSS.percent(parseFloat(offset));
} else if (offset.endsWith('px')) {
offset = CSS.px(parseFloat(offset));
} else if (offset.endsWith('em')) {
offset = CSS.em(parseFloat(offset))
} else {
throw TypeError("Invalid range offset. Only % and px are supported (for now)");
}
}

return { rangeName, offset };
}

Expand Down
107 changes: 107 additions & 0 deletions src/proxy-cssom.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { createAType, invertType, multiplyTypes, to, toSum } from "./numeric-values";
import {simplifyCalculation} from './simplify-calculation';

export function installCSSOM() {
// Object for storing details associated with an object which are to be kept
Expand Down Expand Up @@ -68,7 +69,113 @@ export function installCSSOM() {
}
}

/**
* Parse a CSSUnitValue from the passed string
* @param {string} str
* @return {CSSUnitValue}
*/
function parseCSSUnitValue(str) {
const UNIT_VALUE_REGEXP = /^(-?\d*[.]?\d+)(r?em|r?ex|r?cap|r?ch|r?ic|r?lh|[sld]?v(w|h|i|b|min|max)|cm|mm|Q|in|pt|pc|px|%)?$/;
const match = str.match(UNIT_VALUE_REGEXP);
if (match) {
let [_, v, unit] = match;
if (typeof unit === 'undefined') {
unit = 'number';
} else if (unit === '%') {
unit = 'percent';
}
return new CSSUnitValue(parseFloat(v), unit);
} else {
throw new SyntaxError(`Unsupported syntax ${str}`);
}
}

/**
* Parse the string as a CSSMathProduct
* @param {string} str
* @return {CSSMathProduct}
*/
function parseCSSMathProduct(str) {
let values = [];
const tokens = ['*', ...str.split(/(?<!\([^\)]*)([*/])(?![^\(]*\))/)];
for (let i = 0; i < tokens.length; i += 2) {
let op = tokens[i];
let val = tokens[i + 1];
if (op.trim() === '*') {
values.push(parseCSSNumericValue(val));
} else if (op.trim() === '/') {
values.push(new CSSMathInvert(parseCSSNumericValue(val)));
}
}
return new CSSMathProduct(...values);
}

/**
* Parse the string as a CSSMathSum
* @param {string} str
* @return {CSSMathSum}
*/
function parseCSSMathSum(str) {
let values = [];
const tokens = ['+', ...str.split(/(?<!\([^\)]*)([+-])(?![^\(]*\))/)];
for (let i = 0; i < tokens.length; i += 2) {
let op = tokens[i];
let val = tokens[i + 1];
if (op.trim() === '+') {
values.push(parseCSSMathProduct(val));
} else if (op.trim() === '-') {
values.push(new CSSMathNegate(parseCSSMathProduct(val)));
}
}
return new CSSMathSum(...values);
}

/**
* Parse math function form the passed string and return a matching CSSMathValue
* @param {string} str
* @return {CSSMathValue}
*/
function parseMathFunction(str) {
const MATH_VALUE_REGEXP = /^(calc|min|max)?\((.*)\)$/;
const match = str.match(MATH_VALUE_REGEXP);
if (match) {
let [_, operation = 'parens', value] = match;
switch (operation) {
case 'calc':
case 'parens':
return parseCSSMathSum(value);
case 'min':
return new CSSMathMin(...value.split(',').map(parseCSSNumericValue));
case 'max':
return new CSSMathMax(...value.split(',').map(parseCSSNumericValue));
}
} else {
throw new SyntaxError(`Unsupported syntax ${str}`);
}
}

/**
* A naive parsing function parsing the input string and returning a CSSNumericValue.
* It supports simple expressions as 'calc(10em + 10px)'
*
* @param {string} value
* @return {CSSNumericValue}
*/
function parseCSSNumericValue(value) {
value = value.trim();
if (value.match(/^[a-z(]/i)) {
return parseMathFunction(value);
} else {
return parseCSSUnitValue(value);
}
}

const cssOMTypes = {
'CSSNumericValue': class {
static parse(value) {
return simplifyCalculation(parseCSSNumericValue(value), {});
}
},
'CSSUnitValue': class {
constructor(value, unit) {
privateDetails.set(this, {
Expand Down

0 comments on commit 4732d02

Please sign in to comment.