Skip to content

Commit 4732d02

Browse files
Parse math functions in animation-range-start and animation-range-end
1 parent 5d44058 commit 4732d02

File tree

3 files changed

+147
-29
lines changed

3 files changed

+147
-29
lines changed

demo/view-timeline/with-math-value-range.html

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,15 @@
8484
<div style="height: 99px"></div>
8585
<div style="border-top: 1px solid red"></div>
8686
</div>
87-
87+
<input type="radio" name="range-type" id="as-string" checked>
88+
<label for="as-string">Range as string</label>
89+
<input type="radio" name="range-type" id="as-timeline-range-offset">
90+
<label for="as-timeline-range-offset">Range as TimelineRangeOffset </label>
8891
</body>
8992
<script src="../../dist/scroll-timeline.js"></script>
9093
<script type="text/javascript">
9194
"use strict";
92-
95+
let ranges = [ "cover calc(0% + 2 * 100px)", "cover calc(100% - 100px)"]
9396
const progressBars = document.querySelectorAll('.progress-bar-progress');
9497
const createProgressAnimation = (bar, rangeStart, rangeEnd, axis, inset = 'auto') => {
9598
const subject = document.getElementById('subject');
@@ -112,17 +115,39 @@
112115
});
113116
createProgressAnimation(progressBars[0], { rangeName: 'cover', offset: CSS.percent(0) },
114117
{ rangeName: 'cover', offset: CSS.percent(100) }, axis);
115-
createProgressAnimation(progressBars[1],
116-
{
117-
rangeName: "cover",
118-
offset: new CSSMathSum(CSS.percent(0), new CSSMathProduct(CSS.number(2), CSS.px(100))),
119-
},
120-
{
121-
rangeName: "cover",
122-
offset: new CSSMathSum(CSS.percent(100), CSS.px(-100)),
123-
}, axis)
118+
createProgressAnimation(progressBars[1], ranges[0], ranges[1], axis);
124119
};
125120

121+
document.querySelectorAll('input').forEach(input => {
122+
input.addEventListener('change', (evt) => {
123+
document.getAnimations().forEach(anim => {
124+
anim.cancel();
125+
});
126+
127+
switch (event.target.name) {
128+
case 'range-type':
129+
const selection = event.target.id;
130+
switch (selection) {
131+
case 'as-string':
132+
ranges = [ "cover calc(0% + 2 * 100px)", "cover calc(100% - 100px)"]
133+
break;
134+
case 'as-timeline-range-offset':
135+
ranges = [{
136+
rangeName: 'cover',
137+
offset: new CSSMathSum(CSS.percent(0), new CSSMathProduct(CSS.number(2), CSS.px(100))),
138+
},
139+
{
140+
rangeName: 'cover',
141+
offset: new CSSMathSum(CSS.percent(100), CSS.px(-100)),
142+
}];
143+
break;
144+
}
145+
}
146+
147+
createAnimations();
148+
});
149+
});
150+
126151
createAnimations();
127152
</script>
128153
</html>

src/proxy-animation.js

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,25 +1731,25 @@ function parseTimelineRangeOffset(value, position) {
17311731
if (value instanceof Object) {
17321732
if (value.rangeName != undefined) {
17331733
rangeName = value.rangeName;
1734-
};
1734+
}
17351735

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

17441744
if (parts.length === 1) {
17451745
if (ANIMATION_RANGE_NAMES.includes(parts[0])) {
17461746
rangeName = parts[0];
17471747
} else {
1748-
offset = parts[0];
1748+
offset = CSSNumericValue.parse(parts[0]);
17491749
}
17501750
} else if (parts.length === 2) {
17511751
rangeName = parts[0];
1752-
offset = parts[1];
1752+
offset = CSSNumericValue.parse(parts[1]);
17531753
}
17541754
}
17551755

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

1761-
// Validate and process offset
1762-
// TODO: support more than % and px. Don’t forget about calc() along with that.
1763-
if (!(offset instanceof Object)) {
1764-
if (offset.endsWith('%')) {
1765-
offset = CSS.percent(parseFloat(offset));
1766-
} else if (offset.endsWith('px')) {
1767-
offset = CSS.px(parseFloat(offset));
1768-
} else if (offset.endsWith('em')) {
1769-
offset = CSS.em(parseFloat(offset))
1770-
} else {
1771-
throw TypeError("Invalid range offset. Only % and px are supported (for now)");
1772-
}
1773-
}
1774-
17751761
return { rangeName, offset };
17761762
}
17771763

src/proxy-cssom.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414
import { createAType, invertType, multiplyTypes, to, toSum } from "./numeric-values";
15+
import {simplifyCalculation} from './simplify-calculation';
1516

1617
export function installCSSOM() {
1718
// Object for storing details associated with an object which are to be kept
@@ -68,7 +69,113 @@ export function installCSSOM() {
6869
}
6970
}
7071

72+
/**
73+
* Parse a CSSUnitValue from the passed string
74+
* @param {string} str
75+
* @return {CSSUnitValue}
76+
*/
77+
function parseCSSUnitValue(str) {
78+
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|%)?$/;
79+
const match = str.match(UNIT_VALUE_REGEXP);
80+
if (match) {
81+
let [_, v, unit] = match;
82+
if (typeof unit === 'undefined') {
83+
unit = 'number';
84+
} else if (unit === '%') {
85+
unit = 'percent';
86+
}
87+
return new CSSUnitValue(parseFloat(v), unit);
88+
} else {
89+
throw new SyntaxError(`Unsupported syntax ${str}`);
90+
}
91+
}
92+
93+
/**
94+
* Parse the string as a CSSMathProduct
95+
* @param {string} str
96+
* @return {CSSMathProduct}
97+
*/
98+
function parseCSSMathProduct(str) {
99+
let values = [];
100+
const tokens = ['*', ...str.split(/(?<!\([^\)]*)([*/])(?![^\(]*\))/)];
101+
for (let i = 0; i < tokens.length; i += 2) {
102+
let op = tokens[i];
103+
let val = tokens[i + 1];
104+
if (op.trim() === '*') {
105+
values.push(parseCSSNumericValue(val));
106+
} else if (op.trim() === '/') {
107+
values.push(new CSSMathInvert(parseCSSNumericValue(val)));
108+
}
109+
}
110+
return new CSSMathProduct(...values);
111+
}
112+
113+
/**
114+
* Parse the string as a CSSMathSum
115+
* @param {string} str
116+
* @return {CSSMathSum}
117+
*/
118+
function parseCSSMathSum(str) {
119+
let values = [];
120+
const tokens = ['+', ...str.split(/(?<!\([^\)]*)([+-])(?![^\(]*\))/)];
121+
for (let i = 0; i < tokens.length; i += 2) {
122+
let op = tokens[i];
123+
let val = tokens[i + 1];
124+
if (op.trim() === '+') {
125+
values.push(parseCSSMathProduct(val));
126+
} else if (op.trim() === '-') {
127+
values.push(new CSSMathNegate(parseCSSMathProduct(val)));
128+
}
129+
}
130+
return new CSSMathSum(...values);
131+
}
132+
133+
/**
134+
* Parse math function form the passed string and return a matching CSSMathValue
135+
* @param {string} str
136+
* @return {CSSMathValue}
137+
*/
138+
function parseMathFunction(str) {
139+
const MATH_VALUE_REGEXP = /^(calc|min|max)?\((.*)\)$/;
140+
const match = str.match(MATH_VALUE_REGEXP);
141+
if (match) {
142+
let [_, operation = 'parens', value] = match;
143+
switch (operation) {
144+
case 'calc':
145+
case 'parens':
146+
return parseCSSMathSum(value);
147+
case 'min':
148+
return new CSSMathMin(...value.split(',').map(parseCSSNumericValue));
149+
case 'max':
150+
return new CSSMathMax(...value.split(',').map(parseCSSNumericValue));
151+
}
152+
} else {
153+
throw new SyntaxError(`Unsupported syntax ${str}`);
154+
}
155+
}
156+
157+
/**
158+
* A naive parsing function parsing the input string and returning a CSSNumericValue.
159+
* It supports simple expressions as 'calc(10em + 10px)'
160+
*
161+
* @param {string} value
162+
* @return {CSSNumericValue}
163+
*/
164+
function parseCSSNumericValue(value) {
165+
value = value.trim();
166+
if (value.match(/^[a-z(]/i)) {
167+
return parseMathFunction(value);
168+
} else {
169+
return parseCSSUnitValue(value);
170+
}
171+
}
172+
71173
const cssOMTypes = {
174+
'CSSNumericValue': class {
175+
static parse(value) {
176+
return simplifyCalculation(parseCSSNumericValue(value), {});
177+
}
178+
},
72179
'CSSUnitValue': class {
73180
constructor(value, unit) {
74181
privateDetails.set(this, {

0 commit comments

Comments
 (0)