Skip to content

Commit

Permalink
feat: add support expressions for additional math logic (#1657)
Browse files Browse the repository at this point in the history
  • Loading branch information
SorsOps authored Mar 25, 2023
1 parent 712ad27 commit 5cfb0e0
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 4 deletions.
12 changes: 12 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "Test current file",
"program": "${workspaceRoot}/node_modules/jest/bin/jest",
"args": [
"--config=./jest.config.ts",
"--json",
"--testPathPattern=${fileBasenameNoExtension}"
],
"console": "integratedTerminal"
},
{
"type": "node",
"request": "launch",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"cy:open": "cypress open",
"cy:run": "cypress run --headless",
"serve": "serve dist",
"lint":"eslint . --quiet --fix",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
Expand Down
27 changes: 26 additions & 1 deletion src/utils/alias/__tests__/getAliasValue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,11 +463,36 @@ describe('getAliasValue', () => {
},
type: TokenTypes.BORDER,
},
{
name: 'clamped', input: 'clamped($xx,2,4)', value: 2, type: TokenTypes.DIMENSION,
},
{
name: 'clamp', input: 'clamp($xx,2,4)', value: 'clamp(1,2,4)', type: TokenTypes.DIMENSION,
},
{
name: 'xx',
input: '1',
value: 1,
type: TokenTypes.DIMENSION,
},
{
name: 'yy',
input: '0.2',
value: 0.2,
type: TokenTypes.DIMENSION,
},
{
// Note that we cannot do {sample(cubicBezier1D($yy,$yy),$yy)}px to inject px values, it must have a semantic intermediary as shown in the following
name: 'cubicSample', input: 'sample(cubicBezier1D($yy,$yy),$yy)', value: 0.104, type: TokenTypes.DIMENSION,
},
{
name: 'cubicSamplePx', input: '{cubicSample}px', value: '0.104px', type: TokenTypes.DIMENSION,
},
];

allTokens.forEach((token) => {
it(`alias ${token.name}`, () => {
// @TODO check this test typing
// @TODO check this test typing,
expect(getAliasValue({ ...token, value: token.input, type: token.type } as SingleToken, allTokens as unknown as SingleToken[], false)).toEqual(token.value);
});
});
Expand Down
31 changes: 31 additions & 0 deletions src/utils/math/__tests__/checkAndEvaluateMath.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,35 @@ describe('checkAndEvaluateMath', () => {
expect(checkAndEvaluateMath('2px * 2px')).toEqual('2px * 2px');
expect(checkAndEvaluateMath('2px + 10%')).toEqual('2px + 10%');
});

it('older clamp continues to work correctly', () => {
expect(checkAndEvaluateMath('clamp(5,2,4)')).toEqual('clamp(5,2,4)');
expect(checkAndEvaluateMath('clamp(0,2,4)')).toEqual('clamp(0,2,4)');
expect(checkAndEvaluateMath('clamp(3,2,4)')).toEqual('clamp(3,2,4)');
});

it('clamps expressions correctly', () => {
expect(checkAndEvaluateMath('clamped(5,2,4)')).toEqual(4);
expect(checkAndEvaluateMath('clamped(0,2,4)')).toEqual(2);
expect(checkAndEvaluateMath('clamped(3,2,4)')).toEqual(3);
});

it('normalized values as expected', () => {
expect(checkAndEvaluateMath('norm(10,5,15)')).toEqual(0.5);
expect(checkAndEvaluateMath('norm(1,1,88)')).toEqual(0);
expect(checkAndEvaluateMath('norm(3,0,10)')).toEqual(0.3);
});

it('lerps values as expected', () => {
expect(checkAndEvaluateMath('lerp(0.5,5,15)')).toEqual(10);
expect(checkAndEvaluateMath('lerp(0,1,88)')).toEqual(1);
expect(checkAndEvaluateMath('lerp(1,47,94)')).toEqual(94);
});

it('samples the curve correctly', () => {
expect(checkAndEvaluateMath('sample(cubicBezier1D(1,0),0.5)')).toEqual(0.5);
expect(checkAndEvaluateMath('sample(cubicBezier1D(0.33, 0.66),0.8)')).toEqual(0.797);
expect(checkAndEvaluateMath('sample(cubicBezier1D(0.45,0.34),0.2)')).toEqual(0.213);
expect(checkAndEvaluateMath('sample(cubicBezier1D(0.45,0.34),0.2)')).toEqual(0.213);
});
});
57 changes: 54 additions & 3 deletions src/utils/math/checkAndEvaluateMath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,68 @@ import { Root } from 'postcss-calc-ast-parser/dist/types/ast';

const parser = new Parser();

/**
* Clamps the value of x between min and max
* @param x
* @param min
* @param max
* @returns
*/
parser.functions.clamped = (x: number, min: number, max: number): number => Math.max(Math.min(x, max), min);

/**
* One dimensional linear interpolation
* @param x Normalized value between 0 and 1
* @param min
* @param max
* @returns
*/
parser.functions.lerp = (x: number, start: number, end: number): number => start + (end - start) * x;

/**
* Returns a normalized value between 0 - 1.
* @param x
* @param start
* @param end
* @returns
*/
parser.functions.norm = (x: number, start: number, end: number): number => (x - start) / (end - start);

/**
* Creates a one dimensional cubicBezier
* @remarks We have to do a significant overhaul to the system to support multidimensional functions. Seems like expr-eval can support neither array or property accessors
* @param x1
* @param x2
* @returns
*/
parser.functions.cubicBezier1D = (x1: number, x2: number) => {
const xx = [0, x1, x2, 1];

return (t: number) => {
const coeffs = [(1 - t) ** 3, 3 * (1 - t) ** 2 * t, 3 * (1 - t) * t ** 2, t ** 3];
const x = coeffs.reduce((acc, c, i) => acc + c * xx[i], 0);
return x;
};
};

// eslint-disable-next-line
parser.functions.sample = (func: Function, ...args: any[]) => {
return func(...args);
};

export function checkAndEvaluateMath(expr: string) {
let calcParsed: Root;

try {
calcParsed = calcAstParser.parse(expr);
} catch (ex) {
return expr;
}

const calcReduced = calcAstParser.reduceExpression(calcParsed);

let unitlessExpr = expr;
let unit = '';
let unit;

if (calcReduced && calcReduced.type !== 'Number') {
unitlessExpr = expr.replace(new RegExp(calcReduced.unit, 'ig'), '');
Expand All @@ -24,13 +75,13 @@ export function checkAndEvaluateMath(expr: string) {
let evaluated: number;

try {
evaluated = parser.evaluate(unitlessExpr);
evaluated = parser.evaluate(`${unitlessExpr}`);
} catch (ex) {
return expr;
}
try {
return unit ? `${evaluated}${unit}` : Number.parseFloat(evaluated.toFixed(3));
} catch {
} catch (ex) {
return expr;
}
}

0 comments on commit 5cfb0e0

Please sign in to comment.