From 703f90c52afd517d7b8f1c68e9d78295aef81a11 Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Sun, 23 Mar 2025 13:36:48 -0400 Subject: [PATCH 01/10] minify translate functions #75 --- README.md | 11 +- dist/index-umd-web.js | 636 ++++++++++++++++++-- dist/index.cjs | 636 ++++++++++++++++++-- dist/lib/ast/features/calc.js | 5 - dist/lib/ast/features/index.js | 1 + dist/lib/ast/features/inlinecssvariables.js | 5 - dist/lib/ast/features/prefix.js | 5 - dist/lib/ast/features/shorthand.js | 15 +- dist/lib/renderer/color/color.js | 4 + dist/lib/renderer/render.js | 9 + dist/lib/syntax/syntax.js | 10 +- dist/lib/validation/utils/list.js | 19 +- jsr.json | 2 +- package.json | 2 +- src/lib/ast/features/calc.ts | 8 - src/lib/ast/features/index.ts | 3 +- src/lib/ast/features/inlinecssvariables.ts | 8 - src/lib/ast/features/prefix.ts | 8 - src/lib/ast/features/shorthand.ts | 20 +- src/lib/ast/features/transform.ts | 77 +++ src/lib/ast/transform/compute.ts | 185 ++++++ src/lib/ast/transform/convert.ts | 36 ++ src/lib/ast/transform/index.ts | 0 src/lib/ast/transform/matrix.ts | 98 +++ src/lib/ast/transform/minify.ts | 141 +++++ src/lib/ast/transform/perspective.ts | 9 + src/lib/ast/transform/rotate.ts | 75 +++ src/lib/ast/transform/scale.ts | 39 ++ src/lib/ast/transform/skew.ts | 32 + src/lib/ast/transform/translate.ts | 33 + src/lib/ast/transform/utils.ts | 395 ++++++++++++ src/lib/renderer/color/color.ts | 4 + src/lib/renderer/render.ts | 11 +- src/lib/syntax/syntax.ts | 9 + src/lib/validation/utils/list.ts | 28 + test/specs/code/index.js | 5 +- test/specs/code/nesting.js | 135 +---- test/specs/code/transform.js | 118 ++++ 38 files changed, 2590 insertions(+), 247 deletions(-) create mode 100644 src/lib/ast/features/transform.ts create mode 100644 src/lib/ast/transform/compute.ts create mode 100644 src/lib/ast/transform/convert.ts create mode 100644 src/lib/ast/transform/index.ts create mode 100644 src/lib/ast/transform/matrix.ts create mode 100644 src/lib/ast/transform/minify.ts create mode 100644 src/lib/ast/transform/perspective.ts create mode 100644 src/lib/ast/transform/rotate.ts create mode 100644 src/lib/ast/transform/scale.ts create mode 100644 src/lib/ast/transform/skew.ts create mode 100644 src/lib/ast/transform/translate.ts create mode 100644 src/lib/ast/transform/utils.ts create mode 100644 test/specs/code/transform.js diff --git a/README.md b/README.md index a41e463e..468eb58b 100644 --- a/README.md +++ b/README.md @@ -176,13 +176,18 @@ Include ParseOptions and RenderOptions - src: string, optional. original css file location to be used with sourcemap, also used to resolve url(). - sourcemap: boolean, optional. preserve node location data. -> Misc Options +> Ast Traversal Options + +- visitor: VisitorNodeMap, optional. node visitor used to transform the ast. + +> Urls and \@import Options +- resolveImport: boolean, optional. replace @import rule by the content of the referenced stylesheet. - resolveUrls: boolean, optional. resolve css 'url()' according to the parameters 'src' and 'cwd' -- resolveImport: boolean, optional. replace @import rule by the content of its referenced stylesheet. + +> Misc Options - removeCharset: boolean, optional. remove @charset. - cwd: string, optional. destination directory used to resolve url(). -- visitor: VisitorNodeMap, optional. node visitor used to transform the ast. - signal: AbortSignal, optional. abort parsing. #### RenderOptions diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 43276ff7..13bdb1ed 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -2108,6 +2108,10 @@ // @ts-ignore return token.typ == exports.EnumToken.PercentageTokenType ? token.val / 100 : +token.val; } + /** + * convert angle to turn + * @param token + */ function getAngle(token) { if (token.typ == exports.EnumToken.IdenTokenType) { if (token.val == 'none') { @@ -2460,7 +2464,7 @@ } return x; } - function compute(a, b, op) { + function compute$1(a, b, op) { if (typeof a == 'number' && typeof b == 'number') { switch (op) { case exports.EnumToken.Add: @@ -2706,7 +2710,7 @@ } } // @ts-ignore - const val = compute(v1, v2, op); + const val = compute$1(v1, v2, op); // typ = typeof val == 'number' ? EnumToken.NumberTokenType : EnumToken.FractionTokenType; const token = { ...(l.typ == exports.EnumToken.NumberTokenType ? r : l), @@ -3730,6 +3734,15 @@ token.chi[0].val?.typ != exports.EnumToken.FractionTokenType) { return token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache, reducer), ''); } + // if (token.typ == EnumToken.FunctionTokenType && transformFunctions.includes(token.val)) { + // + // const children = token.val.startsWith('matrix') ? null : stripCommaToken(token.chi.slice()) as Token[]; + // + // if (children != null) { + // + // return token.val + '(' + children.reduce((acc: string, curr: Token) => acc + (acc.length > 0 ? ' ' : '') + renderToken(curr, options, cache, reducer), '') + ')'; + // } + // } // @ts-ignore return ( /* options.minify && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce(reducer, '') + ')'; case exports.EnumToken.MatchExpressionTokenType: @@ -3960,6 +3973,14 @@ const fontFormat = ['collection', 'embedded-opentype', 'opentype', 'svg', 'truetype', 'woff', 'woff2']; const colorFontTech = ['color-colrv0', 'color-colrv1', 'color-svg', 'color-sbix', 'color-cbdt']; const fontFeaturesTech = ['features-opentype', 'features-aat', 'features-graphite', 'incremental-patch', 'incremental-range', 'incremental-auto', 'variations', 'palettes']; + const transformFunctions = [ + 'matrix', 'translate', 'scale', 'rotate', 'skew', 'perspective', + 'translateX', 'translateY', 'translateZ', + 'scaleX', 'scaleY', 'scaleZ', + 'rotateX', 'rotateY', 'rotateZ', + 'skewX', 'skewY', + 'rotate3d', 'translate3d', 'scale3d', 'matrix3d' + ]; // https://drafts.csswg.org/mediaqueries/#media-types const mediaTypes = ['all', 'print', 'screen', /* deprecated */ @@ -10882,7 +10903,7 @@ chi: [] }, 'pos', { ...objectProperties, value: { ind: 0, lin: 1, col: 0 } }); // return minify(doParseSyntax(syntaxes, tokenize(syntaxes), root)) as ValidationRootToken; - return minify$1(transform$1(doParseSyntax(syntax, tokenize(syntax), root))); + return minify$2(transform$1(doParseSyntax(syntax, tokenize(syntax), root))); } function matchParens(syntax, iterator) { let item; @@ -11133,12 +11154,12 @@ const t = { typ: ValidationTokenEnum.Root, chi: token.prelude }; doParseSyntax(syntax, t.chi[Symbol.iterator](), t); token.prelude = t.chi; - minify$1(token.prelude); + minify$2(token.prelude); } } // @ts-ignore if (token?.chi?.length > 0) { - minify$1(doParseSyntax(syntax, token.chi[Symbol.iterator](), token)); + minify$2(doParseSyntax(syntax, token.chi[Symbol.iterator](), token)); } } else { @@ -11591,7 +11612,7 @@ } return position; } - function minify$1(ast) { + function minify$2(ast) { if (Array.isArray(ast)) { // @ts-ignore while (ast.length > 0 && ast[0].typ == ValidationTokenEnum.Whitespace) { @@ -11604,7 +11625,7 @@ for (let i = 0; i < ast.length; i++) { // if ([ValidationTokenEnum.ColumnToken, ValidationTokenEnum.PipeToken, ValidationTokenEnum.AmpersandToken].includes(ast[i].typ)) { // for (const j of (ast[i] as ValidationPipeToken | ValidationColumnToken | ValidationAmpersandToken).l) { - minify$1(ast[i]); + minify$2(ast[i]); // } // for (const j of (ast[i] as ValidationPipeToken | ValidationColumnToken | ValidationAmpersandToken).r) { // @@ -11636,18 +11657,18 @@ // if ([ValidationTokenEnum.ColumnToken, ValidationTokenEnum.PipeToken, ValidationTokenEnum.AmpersandToken].includes(ast.typ)) { // for (const j of (ast as ValidationPipeToken | ValidationColumnToken | ValidationAmpersandToken).l) { if ('l' in ast) { - minify$1(ast.l); + minify$2(ast.l); } // } // for (const j of (ast as ValidationPipeToken | ValidationColumnToken | ValidationAmpersandToken).r) { if ('r' in ast) { - minify$1(ast.r); + minify$2(ast.r); } if ('chi' in ast) { - minify$1(ast.chi); + minify$2(ast.chi); } if ('prelude' in ast) { - minify$1(ast.prelude); + minify$2(ast.prelude); } return ast; } @@ -11788,6 +11809,23 @@ return true; } + function stripCommaToken(tokenList) { + let result = []; + let last = null; + for (let i = 0; i < tokenList.length; i++) { + if (tokenList[i].typ == exports.EnumToken.CommaTokenType && last != null && last.typ == exports.EnumToken.CommaTokenType) { + return null; + } + if (tokenList[i].typ != exports.EnumToken.WhitespaceTokenType) { + last = tokenList[i]; + } + if (tokenList[i].typ == exports.EnumToken.CommentTokenType || tokenList[i].typ == exports.EnumToken.CommaTokenType) { + continue; + } + result.push(tokenList[i]); + } + return result; + } function splitTokenList(tokenList, split = [exports.EnumToken.CommaTokenType]) { return tokenList.reduce((acc, curr) => { if (curr.typ == exports.EnumToken.CommentTokenType) { @@ -16335,11 +16373,6 @@ } static register(options) { if (options.removePrefix) { - for (const feature of options.features) { - if (feature instanceof ComputePrefixFeature) { - return; - } - } // @ts-ignore options.features.push(new ComputePrefixFeature(options)); } @@ -16470,11 +16503,6 @@ } static register(options) { if (options.inlineCssVariables) { - for (const feature of options.features) { - if (feature instanceof InlineCssVariablesFeature) { - return; - } - } // @ts-ignore options.features.push(new InlineCssVariablesFeature()); } @@ -17466,15 +17494,10 @@ class ComputeShorthandFeature { static get ordering() { - return 2; + return 3; } static register(options) { if (options.computeShorthand) { - for (const feature of options.features) { - if (feature instanceof ComputeShorthandFeature) { - return; - } - } // @ts-ignore options.features.push(new ComputeShorthandFeature(options)); } @@ -17484,18 +17507,20 @@ const j = ast.chi.length; let k = 0; let properties = new PropertyList(options); + const rules = []; // @ts-ignore for (; k < j; k++) { // @ts-ignore const node = ast.chi[k]; if (node.typ == exports.EnumToken.CommentNodeType || node.typ == exports.EnumToken.DeclarationNodeType) { properties.add(node); - continue; } - break; + else { + rules.push(node); + } } // @ts-ignore - ast.chi = [...properties].concat(ast.chi.slice(k)); + ast.chi = [...properties, ...rules]; return ast; } } @@ -17506,11 +17531,6 @@ } static register(options) { if (options.computeCalcExpression) { - for (const feature of options.features) { - if (feature instanceof ComputeCalcExpressionFeature) { - return; - } - } // @ts-ignore options.features.push(new ComputeCalcExpressionFeature()); } @@ -17619,12 +17639,556 @@ } } + function determinant(matrix) { + return matrix[0][0] * matrix[1][1] * matrix[2][2] * matrix[3][3] - matrix[0][0] * matrix[1][2] * matrix[2][3] * matrix[3][1] - + matrix[0][1] * matrix[1][0] * matrix[2][3] * matrix[3][2] + matrix[0][1] * matrix[1][2] * matrix[2][0] * matrix[3][3] - + matrix[0][2] * matrix[1][0] * matrix[2][1] * matrix[3][3] + matrix[0][2] * matrix[1][1] * matrix[2][0] * matrix[3][2] - + matrix[0][3] * matrix[1][0] * matrix[2][1] * matrix[3][2] + matrix[0][3] * matrix[1][1] * matrix[2][2] * matrix[3][0]; + } + function identity() { + return [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]; + } + function inverse(matrix) { + // Create augmented matrix [matrix | identity] + let augmented = matrix.map((row, i) => [ + ...row, + ...(i === 0 ? [1, 0, 0, 0] : + i === 1 ? [0, 1, 0, 0] : + i === 2 ? [0, 0, 1, 0] : + [0, 0, 0, 1]) + ]); + // Gaussian elimination with partial pivoting + for (let col = 0; col < 4; col++) { + // Find pivot row with maximum absolute value + let maxRow = col; + let maxVal = Math.abs(augmented[col][col]); + for (let row = col + 1; row < 4; row++) { + let val = Math.abs(augmented[row][col]); + if (val > maxVal) { + maxVal = val; + maxRow = row; + } + } + // Check for singularity + if (maxVal < 1e-10) { + throw new Error("Matrix is singular and cannot be inverted"); + } + // Swap rows if necessary + if (maxRow !== col) { + [augmented[col], augmented[maxRow]] = [augmented[maxRow], augmented[col]]; + } + // Scale pivot row to make pivot element 1 + let pivot = augmented[col][col]; + for (let j = 0; j < 8; j++) { + augmented[col][j] /= pivot; + } + // Eliminate column in other rows + for (let row = 0; row < 4; row++) { + if (row !== col) { + let factor = augmented[row][col]; + for (let j = 0; j < 8; j++) { + augmented[row][j] -= factor * augmented[col][j]; + } + } + } + } + // Extract the inverse from the right side of the augmented matrix + return augmented.map(row => row.slice(4)); + } + function transpose(matrix) { + // Crée une nouvelle matrice vide 4x4 + // @ts-ignore + let transposed = [[], [], [], []]; + // Parcourt chaque ligne et colonne pour transposer + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + transposed[j][i] = matrix[i][j]; + } + } + return transposed; + } + function multVecMatrix(vector, matrix) { + const result = [0, 0, 0, 0]; + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + result[i] += matrix[i][j] * vector[j]; + } + } + return result; + } + function pLength(point) { + // Calcul de la norme euclidienne + return Math.sqrt(point[0] * point[0] + point[1] * point[1] + point[2] * point[2]); + } + function normalize(point) { + const [x, y, z] = point; + const norm = Math.sqrt(point[0] * point[0] + point[1] * point[1] + point[2] * point[2]); + return norm === 0 ? [0, 0, 0] : [x / norm, y / norm, z / norm]; + } + function dot(point1, point2) { + return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2]; + } + function combine(point1, point2, ascl, bscl) { + return [point1[0] * ascl + point2[0] * bscl, point1[1] * ascl + point2[1] * bscl, point1[2] * ascl + point2[2] * bscl]; + } + function cross(a, b) { + return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; + } + function decompose(matrix) { + // Normalize the matrix. + if (matrix[3][3] === 0) { + return null; + } + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + matrix[i][j] /= matrix[3][3]; + } + } + // perspectiveMatrix is used to solve for perspective, but it also provides + // an easy way to test for singularity of the upper 3x3 component. + let perspectiveMatrix = matrix; + for (let i = 0; i < 3; i++) { + perspectiveMatrix[i][3] = 0; + } + perspectiveMatrix[3][3] = 1; + if (determinant(perspectiveMatrix) === 0) { + return null; + } + let rightHandSide = [0, 0, 0, 0]; + let perspective = [0, 0, 0, 0]; + let translate = [0, 0, 0]; + // First, isolate perspective. + if (matrix[0][3] !== 0 || matrix[1][3] !== 0 || matrix[2][3] !== 0) { + // rightHandSide is the right hand side of the equation. + rightHandSide[0] = matrix[0][3]; + rightHandSide[1] = matrix[1][3]; + rightHandSide[2] = matrix[2][3]; + rightHandSide[3] = matrix[3][3]; + // Solve the equation by inverting perspectiveMatrix and multiplying + // rightHandSide by the inverse. + let inversePerspectiveMatrix = inverse(perspectiveMatrix); + let transposedInversePerspectiveMatrix = transpose(inversePerspectiveMatrix); + perspective = multVecMatrix(rightHandSide, transposedInversePerspectiveMatrix); + } + else { + // No perspective. + perspective[0] = perspective[1] = perspective[2] = 0; + perspective[3] = 1; + } + // Next take care of translation + for (let i = 0; i < 3; i++) { + translate[i] = matrix[3][i]; + } + let row = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; + // Now get scale and shear. 'row' is a 3 element array of 3 component vectors + for (let i = 0; i < 3; i++) { + row[i][0] = matrix[i][0]; + row[i][1] = matrix[i][1]; + row[i][2] = matrix[i][2]; + } + let scale = [0, 0, 0]; + let skew = [0, 0, 0]; + // Compute X scale factor and normalize first row. + scale[0] = pLength(row[0]); + row[0] = normalize(row[0]); + // Compute XY shear factor and make 2nd row orthogonal to 1st. + skew[0] = dot(row[0], row[1]); + row[1] = combine(row[1], row[0], 1.0, -skew[0]); + // Now, compute Y scale and normalize 2nd row. + scale[1] = pLength(row[1]); + row[1] = normalize(row[1]); + skew[0] /= scale[1]; + // Compute XZ and YZ shears, orthogonalize 3rd row + skew[1] = dot(row[0], row[2]); + row[2] = combine(row[2], row[0], 1.0, -skew[1]); + skew[2] = dot(row[1], row[2]); + row[2] = combine(row[2], row[1], 1.0, -skew[2]); + // Next, get Z scale and normalize 3rd row. + scale[2] = pLength(row[2]); + row[2] = normalize(row[2]); + skew[1] /= scale[2]; + skew[2] /= scale[2]; + // At this point, the matrix (in rows) is orthonormal. + // Check for a coordinate system flip. If the determinant + // is -1, then negate the matrix and the scaling factors. + let pdum3 = cross(row[1], row[2]); + if (dot(row[0], pdum3) < 0) { + for (let i = 0; i < 3; i++) { + scale[i] *= -1; + row[i][0] *= -1; + row[i][1] *= -1; + row[i][2] *= -1; + } + } + let quaternion = [0, 0, 0, 0]; + // Now, get the rotations out + quaternion[0] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] - row[1][1] - row[2][2], 0)); + quaternion[1] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] + row[1][1] - row[2][2], 0)); + quaternion[2] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] - row[1][1] + row[2][2], 0)); + quaternion[3] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] + row[1][1] + row[2][2], 0)); + if (row[2][1] > row[1][2]) { + quaternion[0] = -quaternion[0]; + } + if (row[0][2] > row[2][0]) { + quaternion[1] = -quaternion[1]; + } + if (row[1][0] > row[0][1]) { + quaternion[2] = -quaternion[2]; + } + return { + skew, + scale, + translate, + perspective, + quaternion + }; + } + + // https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#absolute_length_units + function length2Px(value) { + if (value.typ == exports.EnumToken.NumberTokenType) { + return +value.val; + } + switch (value.unit) { + case 'cm': + // @ts-ignore + return value.val * 37.8; + case 'mm': + // @ts-ignore + return value.val * 3.78; + case 'Q': + // @ts-ignore + return value.val * 37.8 / 40; + case 'in': + // @ts-ignore + return value.val / 96; + case 'pc': + // @ts-ignore + return value.val / 16; + case 'pt': + // @ts-ignore + return value.val * 4 / 3; + case 'px': + return +value.val; + } + return null; + } + + function translateX(x, matrix) { + matrix[3][0] = x; + return matrix; + } + function translateY(y, matrix) { + matrix[3][1] = y; + return matrix; + } + function translateZ(z, matrix) { + matrix[3][2] = z; + return matrix; + } + function translate(translate, matrix) { + matrix[3][0] = translate[0]; + matrix[3][1] = translate[1] ?? 0; + matrix[3][2] = translate[2] ?? 0; + return matrix; + } + + function compute(transformList) { + transformList = transformList.slice(); + stripCommaToken(transformList); + if (transformList.length == 0) { + return null; + } + const matrix = identity(); + let values = []; + let val; + for (let i = 0; i < transformList.length; i++) { + if (transformList[i].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (transformList[i].typ != exports.EnumToken.FunctionTokenType || !transformFunctions.includes(transformList[i].val)) { + return null; + } + switch (transformList[i].val) { + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': + { + values.length = 0; + const children = stripCommaToken(transformList[i].chi.slice()); + if (children == null || children.length == 0) { + return null; + } + const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; + console.error([transformList[i].val, valCount]); + if (children.length == 1 && children[0].typ == exports.EnumToken.IdenTokenType && children[0].val == 'none') { + values.fill(0, 0, valCount); + } + else { + for (let j = 0; j < children.length; j++) { + if (children[j].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + val = length2Px(children[j]); + if (val == null) { + return null; + } + values.push(val); + } + } + if (values.length == 0 || values.length > valCount) { + return null; + } + if (transformList[i].val == 'translateX') { + translateX(values[0], matrix); + } + else if (transformList[i].val == 'translateY') { + translateY(values[0], matrix); + } + else if (transformList[i].val == 'translateZ') { + translateZ(values[0], matrix); + } + else { + // @ts-ignore + translate(values, matrix); + } + } + break; + // case 'rotate': + // case 'rotateX': + // case 'rotateY': + // // case 'rotateZ': + // // case 'rotate3d': + // + // { + // let x: number = 0; + // let y: number = 0; + // let z: number = 0; + // + // let angle: number; + // let values: number[] = []; + // + // if ((transformList[i] as FunctionToken).val == 'rotateX' || (transformList[i] as FunctionToken).val == 'rotateY' || (transformList[i] as FunctionToken).val == 'rotateZ') { + // + // for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { + // + // if (child.typ == EnumToken.WhitespaceTokenType) { + // + // continue; + // } + // + // // if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { + // // + // // return null; + // // } + // + // if (child.typ == EnumToken.NumberTokenType && +child.val > 0) { + // + // return null; + // } + // + // angle = getAngle(child as AngleToken | NumberToken); + // + // if (angle == null) { + // + // return null; + // } + // + // values.push(angle * 2 * Math.PI); + // + // if ((transformList[i] as FunctionToken).val == 'rotateX') { + // + // x = 1; + // } else if ((transformList[i] as FunctionToken).val == 'rotateY') { + // + // y = 1; + // } else if ((transformList[i] as FunctionToken).val == 'rotateZ') { + // + // z = 1; + // } + // } + // + // if (values.length != 1) { + // + // return null; + // } + // + // console.error({values}); + // + // return null; + // } + // + // else if ((transformList[i] as FunctionToken).val == 'rotate') { + // + // } + // } + // + // break; + default: + return null; + // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); + } + } + return matrix; + } + + function minify$1(matrix) { + const decomposed = decompose(matrix); + if (decomposed == null) { + return null; + } + const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + // check identity + if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { + transforms.delete('translate'); + } + if (decomposed.scale[0] == 1 && decomposed.scale[1] == 1 && decomposed.scale[2] == 1) { + transforms.delete('scale'); + } + if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0 && decomposed.skew[2] == 0) { + transforms.delete('skew'); + } + if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { + transforms.delete('perspective'); + } + if (transforms.size == 0) { + // identity + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '1' } + ] + } + ]; + } + if (transforms.size == 1) { + if (transforms.has('translate')) { + let coordinates = new Set(['x', 'y', 'z']); + for (let i = 0; i < 3; i++) { + if (decomposed.translate[i] == 0) { + coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); + } + } + if (coordinates.size == 3) { + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }]; + } + if (coordinates.size == 1) { + if (coordinates.has('x')) { + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] + }]; + } + let axis = coordinates.has('y') ? 'y' : 'z'; + let index = axis == 'y' ? 1 : 2; + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate' + axis.toUpperCase(), + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] + }]; + } + if (coordinates.has('z')) { + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }]; + } + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } + ] + }]; + } + } + return null; + } + + class TransformCssFeature { + static get ordering() { + return 4; + } + static register(options) { + // @ts-ignore + if (options.minify || options.computeCalcExpression || options.computeShorthand) { + // @ts-ignore + options.features.push(new TransformCssFeature()); + } + } + run(ast) { + if (!('chi' in ast)) { + return; + } + let i = 0; + let node; + // @ts-ignore + for (; i < ast.chi.length; i++) { + // @ts-ignore + node = ast.chi[i]; + if (node.typ != exports.EnumToken.DeclarationNodeType || + (!node.nam.startsWith('--') && !node.nam.match(/^(-[a-z]+-)?transform$/))) { + continue; + } + const children = node.val.slice(); + consumeWhitespace(children); + const result = compute(children); + if (result == null) { + // console.error({result}); + return; + } + decompose(result); + const minified = minify$1(result); + // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); + if (minified != null) { + node.val = minified; + } + } + } + } + var allFeatures = /*#__PURE__*/Object.freeze({ __proto__: null, ComputeCalcExpressionFeature: ComputeCalcExpressionFeature, ComputePrefixFeature: ComputePrefixFeature, ComputeShorthandFeature: ComputeShorthandFeature, - InlineCssVariablesFeature: InlineCssVariablesFeature + InlineCssVariablesFeature: InlineCssVariablesFeature, + TransformCssFeature: TransformCssFeature }); const combinators = ['+', '>', '~', '||', '|']; diff --git a/dist/index.cjs b/dist/index.cjs index cb4229d0..7a78ad8b 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -2107,6 +2107,10 @@ function getNumber(token) { // @ts-ignore return token.typ == exports.EnumToken.PercentageTokenType ? token.val / 100 : +token.val; } +/** + * convert angle to turn + * @param token + */ function getAngle(token) { if (token.typ == exports.EnumToken.IdenTokenType) { if (token.val == 'none') { @@ -2459,7 +2463,7 @@ function gcd(x, y) { } return x; } -function compute(a, b, op) { +function compute$1(a, b, op) { if (typeof a == 'number' && typeof b == 'number') { switch (op) { case exports.EnumToken.Add: @@ -2705,7 +2709,7 @@ function doEvaluate(l, r, op) { } } // @ts-ignore - const val = compute(v1, v2, op); + const val = compute$1(v1, v2, op); // typ = typeof val == 'number' ? EnumToken.NumberTokenType : EnumToken.FractionTokenType; const token = { ...(l.typ == exports.EnumToken.NumberTokenType ? r : l), @@ -3729,6 +3733,15 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer, token.chi[0].val?.typ != exports.EnumToken.FractionTokenType) { return token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache, reducer), ''); } + // if (token.typ == EnumToken.FunctionTokenType && transformFunctions.includes(token.val)) { + // + // const children = token.val.startsWith('matrix') ? null : stripCommaToken(token.chi.slice()) as Token[]; + // + // if (children != null) { + // + // return token.val + '(' + children.reduce((acc: string, curr: Token) => acc + (acc.length > 0 ? ' ' : '') + renderToken(curr, options, cache, reducer), '') + ')'; + // } + // } // @ts-ignore return ( /* options.minify && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce(reducer, '') + ')'; case exports.EnumToken.MatchExpressionTokenType: @@ -3959,6 +3972,14 @@ const dimensionUnits = new Set([ const fontFormat = ['collection', 'embedded-opentype', 'opentype', 'svg', 'truetype', 'woff', 'woff2']; const colorFontTech = ['color-colrv0', 'color-colrv1', 'color-svg', 'color-sbix', 'color-cbdt']; const fontFeaturesTech = ['features-opentype', 'features-aat', 'features-graphite', 'incremental-patch', 'incremental-range', 'incremental-auto', 'variations', 'palettes']; +const transformFunctions = [ + 'matrix', 'translate', 'scale', 'rotate', 'skew', 'perspective', + 'translateX', 'translateY', 'translateZ', + 'scaleX', 'scaleY', 'scaleZ', + 'rotateX', 'rotateY', 'rotateZ', + 'skewX', 'skewY', + 'rotate3d', 'translate3d', 'scale3d', 'matrix3d' +]; // https://drafts.csswg.org/mediaqueries/#media-types const mediaTypes = ['all', 'print', 'screen', /* deprecated */ @@ -10881,7 +10902,7 @@ function parseSyntax(syntax) { chi: [] }, 'pos', { ...objectProperties, value: { ind: 0, lin: 1, col: 0 } }); // return minify(doParseSyntax(syntaxes, tokenize(syntaxes), root)) as ValidationRootToken; - return minify$1(transform$1(doParseSyntax(syntax, tokenize(syntax), root))); + return minify$2(transform$1(doParseSyntax(syntax, tokenize(syntax), root))); } function matchParens(syntax, iterator) { let item; @@ -11132,12 +11153,12 @@ function matchAtRule(syntax, iterator) { const t = { typ: ValidationTokenEnum.Root, chi: token.prelude }; doParseSyntax(syntax, t.chi[Symbol.iterator](), t); token.prelude = t.chi; - minify$1(token.prelude); + minify$2(token.prelude); } } // @ts-ignore if (token?.chi?.length > 0) { - minify$1(doParseSyntax(syntax, token.chi[Symbol.iterator](), token)); + minify$2(doParseSyntax(syntax, token.chi[Symbol.iterator](), token)); } } else { @@ -11590,7 +11611,7 @@ function move(position, chr) { } return position; } -function minify$1(ast) { +function minify$2(ast) { if (Array.isArray(ast)) { // @ts-ignore while (ast.length > 0 && ast[0].typ == ValidationTokenEnum.Whitespace) { @@ -11603,7 +11624,7 @@ function minify$1(ast) { for (let i = 0; i < ast.length; i++) { // if ([ValidationTokenEnum.ColumnToken, ValidationTokenEnum.PipeToken, ValidationTokenEnum.AmpersandToken].includes(ast[i].typ)) { // for (const j of (ast[i] as ValidationPipeToken | ValidationColumnToken | ValidationAmpersandToken).l) { - minify$1(ast[i]); + minify$2(ast[i]); // } // for (const j of (ast[i] as ValidationPipeToken | ValidationColumnToken | ValidationAmpersandToken).r) { // @@ -11635,18 +11656,18 @@ function minify$1(ast) { // if ([ValidationTokenEnum.ColumnToken, ValidationTokenEnum.PipeToken, ValidationTokenEnum.AmpersandToken].includes(ast.typ)) { // for (const j of (ast as ValidationPipeToken | ValidationColumnToken | ValidationAmpersandToken).l) { if ('l' in ast) { - minify$1(ast.l); + minify$2(ast.l); } // } // for (const j of (ast as ValidationPipeToken | ValidationColumnToken | ValidationAmpersandToken).r) { if ('r' in ast) { - minify$1(ast.r); + minify$2(ast.r); } if ('chi' in ast) { - minify$1(ast.chi); + minify$2(ast.chi); } if ('prelude' in ast) { - minify$1(ast.prelude); + minify$2(ast.prelude); } return ast; } @@ -11787,6 +11808,23 @@ function consumeWhitespace(tokens) { return true; } +function stripCommaToken(tokenList) { + let result = []; + let last = null; + for (let i = 0; i < tokenList.length; i++) { + if (tokenList[i].typ == exports.EnumToken.CommaTokenType && last != null && last.typ == exports.EnumToken.CommaTokenType) { + return null; + } + if (tokenList[i].typ != exports.EnumToken.WhitespaceTokenType) { + last = tokenList[i]; + } + if (tokenList[i].typ == exports.EnumToken.CommentTokenType || tokenList[i].typ == exports.EnumToken.CommaTokenType) { + continue; + } + result.push(tokenList[i]); + } + return result; +} function splitTokenList(tokenList, split = [exports.EnumToken.CommaTokenType]) { return tokenList.reduce((acc, curr) => { if (curr.typ == exports.EnumToken.CommentTokenType) { @@ -16434,11 +16472,6 @@ class ComputePrefixFeature { } static register(options) { if (options.removePrefix) { - for (const feature of options.features) { - if (feature instanceof ComputePrefixFeature) { - return; - } - } // @ts-ignore options.features.push(new ComputePrefixFeature(options)); } @@ -16569,11 +16602,6 @@ class InlineCssVariablesFeature { } static register(options) { if (options.inlineCssVariables) { - for (const feature of options.features) { - if (feature instanceof InlineCssVariablesFeature) { - return; - } - } // @ts-ignore options.features.push(new InlineCssVariablesFeature()); } @@ -17565,15 +17593,10 @@ class PropertyList { class ComputeShorthandFeature { static get ordering() { - return 2; + return 3; } static register(options) { if (options.computeShorthand) { - for (const feature of options.features) { - if (feature instanceof ComputeShorthandFeature) { - return; - } - } // @ts-ignore options.features.push(new ComputeShorthandFeature(options)); } @@ -17583,18 +17606,20 @@ class ComputeShorthandFeature { const j = ast.chi.length; let k = 0; let properties = new PropertyList(options); + const rules = []; // @ts-ignore for (; k < j; k++) { // @ts-ignore const node = ast.chi[k]; if (node.typ == exports.EnumToken.CommentNodeType || node.typ == exports.EnumToken.DeclarationNodeType) { properties.add(node); - continue; } - break; + else { + rules.push(node); + } } // @ts-ignore - ast.chi = [...properties].concat(ast.chi.slice(k)); + ast.chi = [...properties, ...rules]; return ast; } } @@ -17605,11 +17630,6 @@ class ComputeCalcExpressionFeature { } static register(options) { if (options.computeCalcExpression) { - for (const feature of options.features) { - if (feature instanceof ComputeCalcExpressionFeature) { - return; - } - } // @ts-ignore options.features.push(new ComputeCalcExpressionFeature()); } @@ -17718,12 +17738,556 @@ class ComputeCalcExpressionFeature { } } +function determinant(matrix) { + return matrix[0][0] * matrix[1][1] * matrix[2][2] * matrix[3][3] - matrix[0][0] * matrix[1][2] * matrix[2][3] * matrix[3][1] - + matrix[0][1] * matrix[1][0] * matrix[2][3] * matrix[3][2] + matrix[0][1] * matrix[1][2] * matrix[2][0] * matrix[3][3] - + matrix[0][2] * matrix[1][0] * matrix[2][1] * matrix[3][3] + matrix[0][2] * matrix[1][1] * matrix[2][0] * matrix[3][2] - + matrix[0][3] * matrix[1][0] * matrix[2][1] * matrix[3][2] + matrix[0][3] * matrix[1][1] * matrix[2][2] * matrix[3][0]; +} +function identity() { + return [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]; +} +function inverse(matrix) { + // Create augmented matrix [matrix | identity] + let augmented = matrix.map((row, i) => [ + ...row, + ...(i === 0 ? [1, 0, 0, 0] : + i === 1 ? [0, 1, 0, 0] : + i === 2 ? [0, 0, 1, 0] : + [0, 0, 0, 1]) + ]); + // Gaussian elimination with partial pivoting + for (let col = 0; col < 4; col++) { + // Find pivot row with maximum absolute value + let maxRow = col; + let maxVal = Math.abs(augmented[col][col]); + for (let row = col + 1; row < 4; row++) { + let val = Math.abs(augmented[row][col]); + if (val > maxVal) { + maxVal = val; + maxRow = row; + } + } + // Check for singularity + if (maxVal < 1e-10) { + throw new Error("Matrix is singular and cannot be inverted"); + } + // Swap rows if necessary + if (maxRow !== col) { + [augmented[col], augmented[maxRow]] = [augmented[maxRow], augmented[col]]; + } + // Scale pivot row to make pivot element 1 + let pivot = augmented[col][col]; + for (let j = 0; j < 8; j++) { + augmented[col][j] /= pivot; + } + // Eliminate column in other rows + for (let row = 0; row < 4; row++) { + if (row !== col) { + let factor = augmented[row][col]; + for (let j = 0; j < 8; j++) { + augmented[row][j] -= factor * augmented[col][j]; + } + } + } + } + // Extract the inverse from the right side of the augmented matrix + return augmented.map(row => row.slice(4)); +} +function transpose(matrix) { + // Crée une nouvelle matrice vide 4x4 + // @ts-ignore + let transposed = [[], [], [], []]; + // Parcourt chaque ligne et colonne pour transposer + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + transposed[j][i] = matrix[i][j]; + } + } + return transposed; +} +function multVecMatrix(vector, matrix) { + const result = [0, 0, 0, 0]; + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + result[i] += matrix[i][j] * vector[j]; + } + } + return result; +} +function pLength(point) { + // Calcul de la norme euclidienne + return Math.sqrt(point[0] * point[0] + point[1] * point[1] + point[2] * point[2]); +} +function normalize(point) { + const [x, y, z] = point; + const norm = Math.sqrt(point[0] * point[0] + point[1] * point[1] + point[2] * point[2]); + return norm === 0 ? [0, 0, 0] : [x / norm, y / norm, z / norm]; +} +function dot(point1, point2) { + return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2]; +} +function combine(point1, point2, ascl, bscl) { + return [point1[0] * ascl + point2[0] * bscl, point1[1] * ascl + point2[1] * bscl, point1[2] * ascl + point2[2] * bscl]; +} +function cross(a, b) { + return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; +} +function decompose(matrix) { + // Normalize the matrix. + if (matrix[3][3] === 0) { + return null; + } + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + matrix[i][j] /= matrix[3][3]; + } + } + // perspectiveMatrix is used to solve for perspective, but it also provides + // an easy way to test for singularity of the upper 3x3 component. + let perspectiveMatrix = matrix; + for (let i = 0; i < 3; i++) { + perspectiveMatrix[i][3] = 0; + } + perspectiveMatrix[3][3] = 1; + if (determinant(perspectiveMatrix) === 0) { + return null; + } + let rightHandSide = [0, 0, 0, 0]; + let perspective = [0, 0, 0, 0]; + let translate = [0, 0, 0]; + // First, isolate perspective. + if (matrix[0][3] !== 0 || matrix[1][3] !== 0 || matrix[2][3] !== 0) { + // rightHandSide is the right hand side of the equation. + rightHandSide[0] = matrix[0][3]; + rightHandSide[1] = matrix[1][3]; + rightHandSide[2] = matrix[2][3]; + rightHandSide[3] = matrix[3][3]; + // Solve the equation by inverting perspectiveMatrix and multiplying + // rightHandSide by the inverse. + let inversePerspectiveMatrix = inverse(perspectiveMatrix); + let transposedInversePerspectiveMatrix = transpose(inversePerspectiveMatrix); + perspective = multVecMatrix(rightHandSide, transposedInversePerspectiveMatrix); + } + else { + // No perspective. + perspective[0] = perspective[1] = perspective[2] = 0; + perspective[3] = 1; + } + // Next take care of translation + for (let i = 0; i < 3; i++) { + translate[i] = matrix[3][i]; + } + let row = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; + // Now get scale and shear. 'row' is a 3 element array of 3 component vectors + for (let i = 0; i < 3; i++) { + row[i][0] = matrix[i][0]; + row[i][1] = matrix[i][1]; + row[i][2] = matrix[i][2]; + } + let scale = [0, 0, 0]; + let skew = [0, 0, 0]; + // Compute X scale factor and normalize first row. + scale[0] = pLength(row[0]); + row[0] = normalize(row[0]); + // Compute XY shear factor and make 2nd row orthogonal to 1st. + skew[0] = dot(row[0], row[1]); + row[1] = combine(row[1], row[0], 1.0, -skew[0]); + // Now, compute Y scale and normalize 2nd row. + scale[1] = pLength(row[1]); + row[1] = normalize(row[1]); + skew[0] /= scale[1]; + // Compute XZ and YZ shears, orthogonalize 3rd row + skew[1] = dot(row[0], row[2]); + row[2] = combine(row[2], row[0], 1.0, -skew[1]); + skew[2] = dot(row[1], row[2]); + row[2] = combine(row[2], row[1], 1.0, -skew[2]); + // Next, get Z scale and normalize 3rd row. + scale[2] = pLength(row[2]); + row[2] = normalize(row[2]); + skew[1] /= scale[2]; + skew[2] /= scale[2]; + // At this point, the matrix (in rows) is orthonormal. + // Check for a coordinate system flip. If the determinant + // is -1, then negate the matrix and the scaling factors. + let pdum3 = cross(row[1], row[2]); + if (dot(row[0], pdum3) < 0) { + for (let i = 0; i < 3; i++) { + scale[i] *= -1; + row[i][0] *= -1; + row[i][1] *= -1; + row[i][2] *= -1; + } + } + let quaternion = [0, 0, 0, 0]; + // Now, get the rotations out + quaternion[0] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] - row[1][1] - row[2][2], 0)); + quaternion[1] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] + row[1][1] - row[2][2], 0)); + quaternion[2] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] - row[1][1] + row[2][2], 0)); + quaternion[3] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] + row[1][1] + row[2][2], 0)); + if (row[2][1] > row[1][2]) { + quaternion[0] = -quaternion[0]; + } + if (row[0][2] > row[2][0]) { + quaternion[1] = -quaternion[1]; + } + if (row[1][0] > row[0][1]) { + quaternion[2] = -quaternion[2]; + } + return { + skew, + scale, + translate, + perspective, + quaternion + }; +} + +// https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#absolute_length_units +function length2Px(value) { + if (value.typ == exports.EnumToken.NumberTokenType) { + return +value.val; + } + switch (value.unit) { + case 'cm': + // @ts-ignore + return value.val * 37.8; + case 'mm': + // @ts-ignore + return value.val * 3.78; + case 'Q': + // @ts-ignore + return value.val * 37.8 / 40; + case 'in': + // @ts-ignore + return value.val / 96; + case 'pc': + // @ts-ignore + return value.val / 16; + case 'pt': + // @ts-ignore + return value.val * 4 / 3; + case 'px': + return +value.val; + } + return null; +} + +function translateX(x, matrix) { + matrix[3][0] = x; + return matrix; +} +function translateY(y, matrix) { + matrix[3][1] = y; + return matrix; +} +function translateZ(z, matrix) { + matrix[3][2] = z; + return matrix; +} +function translate(translate, matrix) { + matrix[3][0] = translate[0]; + matrix[3][1] = translate[1] ?? 0; + matrix[3][2] = translate[2] ?? 0; + return matrix; +} + +function compute(transformList) { + transformList = transformList.slice(); + stripCommaToken(transformList); + if (transformList.length == 0) { + return null; + } + const matrix = identity(); + let values = []; + let val; + for (let i = 0; i < transformList.length; i++) { + if (transformList[i].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (transformList[i].typ != exports.EnumToken.FunctionTokenType || !transformFunctions.includes(transformList[i].val)) { + return null; + } + switch (transformList[i].val) { + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': + { + values.length = 0; + const children = stripCommaToken(transformList[i].chi.slice()); + if (children == null || children.length == 0) { + return null; + } + const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; + console.error([transformList[i].val, valCount]); + if (children.length == 1 && children[0].typ == exports.EnumToken.IdenTokenType && children[0].val == 'none') { + values.fill(0, 0, valCount); + } + else { + for (let j = 0; j < children.length; j++) { + if (children[j].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + val = length2Px(children[j]); + if (val == null) { + return null; + } + values.push(val); + } + } + if (values.length == 0 || values.length > valCount) { + return null; + } + if (transformList[i].val == 'translateX') { + translateX(values[0], matrix); + } + else if (transformList[i].val == 'translateY') { + translateY(values[0], matrix); + } + else if (transformList[i].val == 'translateZ') { + translateZ(values[0], matrix); + } + else { + // @ts-ignore + translate(values, matrix); + } + } + break; + // case 'rotate': + // case 'rotateX': + // case 'rotateY': + // // case 'rotateZ': + // // case 'rotate3d': + // + // { + // let x: number = 0; + // let y: number = 0; + // let z: number = 0; + // + // let angle: number; + // let values: number[] = []; + // + // if ((transformList[i] as FunctionToken).val == 'rotateX' || (transformList[i] as FunctionToken).val == 'rotateY' || (transformList[i] as FunctionToken).val == 'rotateZ') { + // + // for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { + // + // if (child.typ == EnumToken.WhitespaceTokenType) { + // + // continue; + // } + // + // // if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { + // // + // // return null; + // // } + // + // if (child.typ == EnumToken.NumberTokenType && +child.val > 0) { + // + // return null; + // } + // + // angle = getAngle(child as AngleToken | NumberToken); + // + // if (angle == null) { + // + // return null; + // } + // + // values.push(angle * 2 * Math.PI); + // + // if ((transformList[i] as FunctionToken).val == 'rotateX') { + // + // x = 1; + // } else if ((transformList[i] as FunctionToken).val == 'rotateY') { + // + // y = 1; + // } else if ((transformList[i] as FunctionToken).val == 'rotateZ') { + // + // z = 1; + // } + // } + // + // if (values.length != 1) { + // + // return null; + // } + // + // console.error({values}); + // + // return null; + // } + // + // else if ((transformList[i] as FunctionToken).val == 'rotate') { + // + // } + // } + // + // break; + default: + return null; + // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); + } + } + return matrix; +} + +function minify$1(matrix) { + const decomposed = decompose(matrix); + if (decomposed == null) { + return null; + } + const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + // check identity + if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { + transforms.delete('translate'); + } + if (decomposed.scale[0] == 1 && decomposed.scale[1] == 1 && decomposed.scale[2] == 1) { + transforms.delete('scale'); + } + if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0 && decomposed.skew[2] == 0) { + transforms.delete('skew'); + } + if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { + transforms.delete('perspective'); + } + if (transforms.size == 0) { + // identity + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '1' } + ] + } + ]; + } + if (transforms.size == 1) { + if (transforms.has('translate')) { + let coordinates = new Set(['x', 'y', 'z']); + for (let i = 0; i < 3; i++) { + if (decomposed.translate[i] == 0) { + coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); + } + } + if (coordinates.size == 3) { + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }]; + } + if (coordinates.size == 1) { + if (coordinates.has('x')) { + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] + }]; + } + let axis = coordinates.has('y') ? 'y' : 'z'; + let index = axis == 'y' ? 1 : 2; + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate' + axis.toUpperCase(), + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] + }]; + } + if (coordinates.has('z')) { + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }]; + } + return [{ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } + ] + }]; + } + } + return null; +} + +class TransformCssFeature { + static get ordering() { + return 4; + } + static register(options) { + // @ts-ignore + if (options.minify || options.computeCalcExpression || options.computeShorthand) { + // @ts-ignore + options.features.push(new TransformCssFeature()); + } + } + run(ast) { + if (!('chi' in ast)) { + return; + } + let i = 0; + let node; + // @ts-ignore + for (; i < ast.chi.length; i++) { + // @ts-ignore + node = ast.chi[i]; + if (node.typ != exports.EnumToken.DeclarationNodeType || + (!node.nam.startsWith('--') && !node.nam.match(/^(-[a-z]+-)?transform$/))) { + continue; + } + const children = node.val.slice(); + consumeWhitespace(children); + const result = compute(children); + if (result == null) { + // console.error({result}); + return; + } + decompose(result); + const minified = minify$1(result); + // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); + if (minified != null) { + node.val = minified; + } + } + } +} + var allFeatures = /*#__PURE__*/Object.freeze({ __proto__: null, ComputeCalcExpressionFeature: ComputeCalcExpressionFeature, ComputePrefixFeature: ComputePrefixFeature, ComputeShorthandFeature: ComputeShorthandFeature, - InlineCssVariablesFeature: InlineCssVariablesFeature + InlineCssVariablesFeature: InlineCssVariablesFeature, + TransformCssFeature: TransformCssFeature }); const combinators = ['+', '>', '~', '||', '|']; diff --git a/dist/lib/ast/features/calc.js b/dist/lib/ast/features/calc.js index 6923cb43..803a38e0 100644 --- a/dist/lib/ast/features/calc.js +++ b/dist/lib/ast/features/calc.js @@ -10,11 +10,6 @@ class ComputeCalcExpressionFeature { } static register(options) { if (options.computeCalcExpression) { - for (const feature of options.features) { - if (feature instanceof ComputeCalcExpressionFeature) { - return; - } - } // @ts-ignore options.features.push(new ComputeCalcExpressionFeature()); } diff --git a/dist/lib/ast/features/index.js b/dist/lib/ast/features/index.js index 8c01f644..479c5fb0 100644 --- a/dist/lib/ast/features/index.js +++ b/dist/lib/ast/features/index.js @@ -2,3 +2,4 @@ export { ComputePrefixFeature } from './prefix.js'; export { InlineCssVariablesFeature } from './inlinecssvariables.js'; export { ComputeShorthandFeature } from './shorthand.js'; export { ComputeCalcExpressionFeature } from './calc.js'; +export { TransformCssFeature } from './transform.js'; diff --git a/dist/lib/ast/features/inlinecssvariables.js b/dist/lib/ast/features/inlinecssvariables.js index 559adf91..bf4b03e3 100644 --- a/dist/lib/ast/features/inlinecssvariables.js +++ b/dist/lib/ast/features/inlinecssvariables.js @@ -31,11 +31,6 @@ class InlineCssVariablesFeature { } static register(options) { if (options.inlineCssVariables) { - for (const feature of options.features) { - if (feature instanceof InlineCssVariablesFeature) { - return; - } - } // @ts-ignore options.features.push(new InlineCssVariablesFeature()); } diff --git a/dist/lib/ast/features/prefix.js b/dist/lib/ast/features/prefix.js index 97832258..44b7a603 100644 --- a/dist/lib/ast/features/prefix.js +++ b/dist/lib/ast/features/prefix.js @@ -17,11 +17,6 @@ class ComputePrefixFeature { } static register(options) { if (options.removePrefix) { - for (const feature of options.features) { - if (feature instanceof ComputePrefixFeature) { - return; - } - } // @ts-ignore options.features.push(new ComputePrefixFeature(options)); } diff --git a/dist/lib/ast/features/shorthand.js b/dist/lib/ast/features/shorthand.js index c7911f97..5f59f029 100644 --- a/dist/lib/ast/features/shorthand.js +++ b/dist/lib/ast/features/shorthand.js @@ -9,15 +9,10 @@ import '../../parser/utils/config.js'; class ComputeShorthandFeature { static get ordering() { - return 2; + return 3; } static register(options) { if (options.computeShorthand) { - for (const feature of options.features) { - if (feature instanceof ComputeShorthandFeature) { - return; - } - } // @ts-ignore options.features.push(new ComputeShorthandFeature(options)); } @@ -27,18 +22,20 @@ class ComputeShorthandFeature { const j = ast.chi.length; let k = 0; let properties = new PropertyList(options); + const rules = []; // @ts-ignore for (; k < j; k++) { // @ts-ignore const node = ast.chi[k]; if (node.typ == EnumToken.CommentNodeType || node.typ == EnumToken.DeclarationNodeType) { properties.add(node); - continue; } - break; + else { + rules.push(node); + } } // @ts-ignore - ast.chi = [...properties].concat(ast.chi.slice(k)); + ast.chi = [...properties, ...rules]; return ast; } } diff --git a/dist/lib/renderer/color/color.js b/dist/lib/renderer/color/color.js index a1d39304..7f2ecadb 100644 --- a/dist/lib/renderer/color/color.js +++ b/dist/lib/renderer/color/color.js @@ -531,6 +531,10 @@ function getNumber(token) { // @ts-ignore return token.typ == EnumToken.PercentageTokenType ? token.val / 100 : +token.val; } +/** + * convert angle to turn + * @param token + */ function getAngle(token) { if (token.typ == EnumToken.IdenTokenType) { if (token.val == 'none') { diff --git a/dist/lib/renderer/render.js b/dist/lib/renderer/render.js index 78a5dcc3..b1329b97 100644 --- a/dist/lib/renderer/render.js +++ b/dist/lib/renderer/render.js @@ -436,6 +436,15 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer, token.chi[0].val?.typ != EnumToken.FractionTokenType) { return token.chi.reduce((acc, curr) => acc + renderToken(curr, options, cache, reducer), ''); } + // if (token.typ == EnumToken.FunctionTokenType && transformFunctions.includes(token.val)) { + // + // const children = token.val.startsWith('matrix') ? null : stripCommaToken(token.chi.slice()) as Token[]; + // + // if (children != null) { + // + // return token.val + '(' + children.reduce((acc: string, curr: Token) => acc + (acc.length > 0 ? ' ' : '') + renderToken(curr, options, cache, reducer), '') + ')'; + // } + // } // @ts-ignore return ( /* options.minify && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/token.val ?? '') + '(' + token.chi.reduce(reducer, '') + ')'; case EnumToken.MatchExpressionTokenType: diff --git a/dist/lib/syntax/syntax.js b/dist/lib/syntax/syntax.js index 0b219a21..1edc21bf 100644 --- a/dist/lib/syntax/syntax.js +++ b/dist/lib/syntax/syntax.js @@ -19,6 +19,14 @@ const dimensionUnits = new Set([ const fontFormat = ['collection', 'embedded-opentype', 'opentype', 'svg', 'truetype', 'woff', 'woff2']; const colorFontTech = ['color-colrv0', 'color-colrv1', 'color-svg', 'color-sbix', 'color-cbdt']; const fontFeaturesTech = ['features-opentype', 'features-aat', 'features-graphite', 'incremental-patch', 'incremental-range', 'incremental-auto', 'variations', 'palettes']; +const transformFunctions = [ + 'matrix', 'translate', 'scale', 'rotate', 'skew', 'perspective', + 'translateX', 'translateY', 'translateZ', + 'scaleX', 'scaleY', 'scaleZ', + 'rotateX', 'rotateY', 'rotateZ', + 'skewX', 'skewY', + 'rotate3d', 'translate3d', 'scale3d', 'matrix3d' +]; // https://drafts.csswg.org/mediaqueries/#media-types const mediaTypes = ['all', 'print', 'screen', /* deprecated */ @@ -812,4 +820,4 @@ function isWhiteSpace(codepoint) { codepoint == 0xa || codepoint == 0xc || codepoint == 0xd; } -export { colorFontTech, fontFeaturesTech, fontFormat, isAngle, isAtKeyword, isColor, isColorspace, isDigit, isDimension, isFlex, isFrequency, isFunction, isHash, isHexColor, isHueInterpolationMethod, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNonPrintable, isNumber, isPercentage, isPolarColorspace, isPseudo, isRectangularOrthogonalColorspace, isResolution, isTime, isWhiteSpace, mathFuncs, mediaTypes, mozExtensions, parseDimension, pseudoElements, webkitExtensions, webkitPseudoAliasMap }; +export { colorFontTech, fontFeaturesTech, fontFormat, isAngle, isAtKeyword, isColor, isColorspace, isDigit, isDimension, isFlex, isFrequency, isFunction, isHash, isHexColor, isHueInterpolationMethod, isIdent, isIdentCodepoint, isIdentStart, isLength, isNewLine, isNonPrintable, isNumber, isPercentage, isPolarColorspace, isPseudo, isRectangularOrthogonalColorspace, isResolution, isTime, isWhiteSpace, mathFuncs, mediaTypes, mozExtensions, parseDimension, pseudoElements, transformFunctions, webkitExtensions, webkitPseudoAliasMap }; diff --git a/dist/lib/validation/utils/list.js b/dist/lib/validation/utils/list.js index d4ac4017..ac367a0f 100644 --- a/dist/lib/validation/utils/list.js +++ b/dist/lib/validation/utils/list.js @@ -6,6 +6,23 @@ import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import '../../parser/utils/config.js'; +function stripCommaToken(tokenList) { + let result = []; + let last = null; + for (let i = 0; i < tokenList.length; i++) { + if (tokenList[i].typ == EnumToken.CommaTokenType && last != null && last.typ == EnumToken.CommaTokenType) { + return null; + } + if (tokenList[i].typ != EnumToken.WhitespaceTokenType) { + last = tokenList[i]; + } + if (tokenList[i].typ == EnumToken.CommentTokenType || tokenList[i].typ == EnumToken.CommaTokenType) { + continue; + } + result.push(tokenList[i]); + } + return result; +} function splitTokenList(tokenList, split = [EnumToken.CommaTokenType]) { return tokenList.reduce((acc, curr) => { if (curr.typ == EnumToken.CommentTokenType) { @@ -21,4 +38,4 @@ function splitTokenList(tokenList, split = [EnumToken.CommaTokenType]) { }, [[]]); } -export { splitTokenList }; +export { splitTokenList, stripCommaToken }; diff --git a/jsr.json b/jsr.json index defc0aa2..cf3de4bd 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@tbela99/css-parser", - "version": "0.9.1", + "version": "0.9.2-alpha1", "publish": { "include": [ "src", diff --git a/package.json b/package.json index 34140a6c..16aaf232 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "v0.9.1", + "version": "v0.9.2-alpha1", "exports": { ".": "./dist/node/index.js", "./node": "./dist/node/index.js", diff --git a/src/lib/ast/features/calc.ts b/src/lib/ast/features/calc.ts index 8ebc2c4b..1caf04cd 100644 --- a/src/lib/ast/features/calc.ts +++ b/src/lib/ast/features/calc.ts @@ -27,14 +27,6 @@ export class ComputeCalcExpressionFeature { if (options.computeCalcExpression) { - for (const feature of options.features) { - - if (feature instanceof ComputeCalcExpressionFeature) { - - return - } - } - // @ts-ignore options.features.push(new ComputeCalcExpressionFeature()); } diff --git a/src/lib/ast/features/index.ts b/src/lib/ast/features/index.ts index 0ca3622d..2423df56 100644 --- a/src/lib/ast/features/index.ts +++ b/src/lib/ast/features/index.ts @@ -1,4 +1,5 @@ export * from './prefix.ts'; export * from './inlinecssvariables.ts'; export * from './shorthand.ts'; -export * from './calc.ts' \ No newline at end of file +export * from './calc.ts'; +export * from './transform.ts'; \ No newline at end of file diff --git a/src/lib/ast/features/inlinecssvariables.ts b/src/lib/ast/features/inlinecssvariables.ts index 03560fe1..8a844736 100644 --- a/src/lib/ast/features/inlinecssvariables.ts +++ b/src/lib/ast/features/inlinecssvariables.ts @@ -61,14 +61,6 @@ export class InlineCssVariablesFeature { if (options.inlineCssVariables) { - for (const feature of options.features) { - - if (feature instanceof InlineCssVariablesFeature) { - - return; - } - } - // @ts-ignore options.features.push(new InlineCssVariablesFeature()); } diff --git a/src/lib/ast/features/prefix.ts b/src/lib/ast/features/prefix.ts index 186e1af2..c01c2fe2 100644 --- a/src/lib/ast/features/prefix.ts +++ b/src/lib/ast/features/prefix.ts @@ -25,14 +25,6 @@ export class ComputePrefixFeature { if (options.removePrefix) { - for (const feature of options.features) { - - if (feature instanceof ComputePrefixFeature) { - - return; - } - } - // @ts-ignore options.features.push(new ComputePrefixFeature(options)); } diff --git a/src/lib/ast/features/shorthand.ts b/src/lib/ast/features/shorthand.ts index 2768f315..8baece06 100644 --- a/src/lib/ast/features/shorthand.ts +++ b/src/lib/ast/features/shorthand.ts @@ -2,6 +2,7 @@ import {PropertyList} from "../../parser/declaration/index.ts"; import {EnumToken} from "../types.ts"; import type { AstAtRule, + AstNode, AstRule, AstRuleStyleSheet, MinifyFeatureOptions, @@ -11,21 +12,13 @@ import type { export class ComputeShorthandFeature { static get ordering() { - return 2; + return 3; } static register(options: MinifyFeatureOptions) { if (options.computeShorthand) { - for (const feature of options.features) { - - if (feature instanceof ComputeShorthandFeature) { - - return; - } - } - // @ts-ignore options.features.push(new ComputeShorthandFeature(options)); } @@ -37,6 +30,7 @@ export class ComputeShorthandFeature { const j: number = ast.chi.length; let k: number = 0; let properties: PropertyList = new PropertyList(options); + const rules: AstNode[] = []; // @ts-ignore for (; k < j; k++) { @@ -47,14 +41,16 @@ export class ComputeShorthandFeature { if (node.typ == EnumToken.CommentNodeType || node.typ == EnumToken.DeclarationNodeType) { properties.add(node); - continue; } - break; + else { + + rules.push(node); + } } // @ts-ignore - ast.chi = [...properties].concat(ast.chi.slice(k)); + ast.chi = [...properties, ...rules]; return ast; } } \ No newline at end of file diff --git a/src/lib/ast/features/transform.ts b/src/lib/ast/features/transform.ts new file mode 100644 index 00000000..b5b42b55 --- /dev/null +++ b/src/lib/ast/features/transform.ts @@ -0,0 +1,77 @@ +import type { + AstAtRule, + AstDeclaration, + AstNode, + AstRule, + MinifyFeatureOptions, + Token +} from "../../../@types/index.d.ts"; +import {EnumToken} from "../types"; +import {consumeWhitespace} from "../../validation/utils"; +import {compute} from "../transform/compute.ts"; +import {decompose} from "../transform/utils.ts"; +import {minify} from "../transform/minify.ts"; + +export class TransformCssFeature { + + static get ordering(): number { + return 4; + } + + static register(options: MinifyFeatureOptions): void { + + // @ts-ignore + if (options.minify || options.computeCalcExpression || options.computeShorthand) { + + // @ts-ignore + options.features.push(new TransformCssFeature()); + } + } + + run(ast: AstRule | AstAtRule): void { + + if (!('chi' in ast)) { + + return; + } + + let i: number = 0; + let node: AstNode | AstDeclaration; + + // @ts-ignore + for (; i < ast.chi.length; i++) { + + // @ts-ignore + node = ast.chi[i] as AstNode | AstDeclaration; + + if ( + node.typ != EnumToken.DeclarationNodeType || + (!node.nam.startsWith('--') && !node.nam.match(/^(-[a-z]+-)?transform$/))) { + + continue; + } + + const children: Token[] = (node as AstDeclaration).val.slice(); + + consumeWhitespace(children); + + const result = compute(children as Token[]); + + if (result == null) { + + // console.error({result}); + return; + } + + const decomposed = decompose(result); + const minified = minify(result); + + // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); + + if (minified != null) { + + (node as AstDeclaration).val = minified; + } + } + } +} diff --git a/src/lib/ast/transform/compute.ts b/src/lib/ast/transform/compute.ts new file mode 100644 index 00000000..c97621f9 --- /dev/null +++ b/src/lib/ast/transform/compute.ts @@ -0,0 +1,185 @@ +import type {FunctionToken, LengthToken, Token} from "../../../@types/token.d.ts"; +import {identity, Matrix} from "./utils.ts"; +import {EnumToken} from "../types.ts"; +import {length2Px} from "./convert.ts"; +import {transformFunctions} from "../../syntax/index.ts"; +import {stripCommaToken} from "../../validation/utils"; +import {translate, translate3d, translateX, translateY, translateZ} from "./translate.ts"; + +export function compute(transformList: Token[]): Matrix | null { + + transformList = transformList.slice(); + stripCommaToken(transformList); + + if (transformList.length == 0) { + + return null; + } + + const matrix: Matrix = identity(); + let values: number[] = []; + let val: number | null; + + for (let i = 0; i < transformList.length; i++) { + + if (transformList[i].typ == EnumToken.WhitespaceTokenType) { + + continue; + } + + if (transformList[i].typ != EnumToken.FunctionTokenType || !transformFunctions.includes((transformList[i] as FunctionToken).val)) { + + return null; + } + + switch ((transformList[i] as FunctionToken).val) { + + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': { + + values.length = 0; + const children = stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]; + + if (children == null || children.length == 0) { + + return null; + } + + const valCount: number = (transformList[i] as FunctionToken).val == 'translate3d' || (transformList[i] as FunctionToken).val == 'translate' ? 3 : 1; + + console.error([(transformList[i] as FunctionToken).val, valCount]); + + if (children.length == 1 && children[0].typ == EnumToken.IdenTokenType && children[0].val == 'none') { + + values.fill(0, 0, valCount); + + } else { + + for (let j = 0; j < children.length; j++) { + + if (children[j].typ == EnumToken.WhitespaceTokenType) { + + continue; + } + + val = length2Px(children[j] as LengthToken); + + if (val == null) { + + return null; + } + + values.push(val); + } + + } + + if (values.length == 0 || values.length > valCount) { + + return null; + } + + if ((transformList[i] as FunctionToken).val == 'translateX') { + + translateX(values[0], matrix); + + } else if ((transformList[i] as FunctionToken).val == 'translateY') { + + translateY(values[0], matrix); + + } else if ((transformList[i] as FunctionToken).val == 'translateZ') { + + translateZ(values[0], matrix); + } + + else { + + // @ts-ignore + translate(values as [number] | [number, number], matrix); + } + } + break; + + // case 'rotate': + // case 'rotateX': + // case 'rotateY': + // // case 'rotateZ': + // // case 'rotate3d': + // + // { + // let x: number = 0; + // let y: number = 0; + // let z: number = 0; + // + // let angle: number; + // let values: number[] = []; + // + // if ((transformList[i] as FunctionToken).val == 'rotateX' || (transformList[i] as FunctionToken).val == 'rotateY' || (transformList[i] as FunctionToken).val == 'rotateZ') { + // + // for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { + // + // if (child.typ == EnumToken.WhitespaceTokenType) { + // + // continue; + // } + // + // // if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { + // // + // // return null; + // // } + // + // if (child.typ == EnumToken.NumberTokenType && +child.val > 0) { + // + // return null; + // } + // + // angle = getAngle(child as AngleToken | NumberToken); + // + // if (angle == null) { + // + // return null; + // } + // + // values.push(angle * 2 * Math.PI); + // + // if ((transformList[i] as FunctionToken).val == 'rotateX') { + // + // x = 1; + // } else if ((transformList[i] as FunctionToken).val == 'rotateY') { + // + // y = 1; + // } else if ((transformList[i] as FunctionToken).val == 'rotateZ') { + // + // z = 1; + // } + // } + // + // if (values.length != 1) { + // + // return null; + // } + // + // console.error({values}); + // + // return null; + // } + // + // else if ((transformList[i] as FunctionToken).val == 'rotate') { + // + // } + // } + // + // break; + + default: + + return null; + // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); + } + } + + return matrix; +} \ No newline at end of file diff --git a/src/lib/ast/transform/convert.ts b/src/lib/ast/transform/convert.ts new file mode 100644 index 00000000..c3e4273d --- /dev/null +++ b/src/lib/ast/transform/convert.ts @@ -0,0 +1,36 @@ +import type {LengthToken, NumberToken} from "../../../@types/token.d.ts"; +import {EnumToken} from "../types.ts"; + +// https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#absolute_length_units +export function length2Px(value: LengthToken | NumberToken): number | null { + + if (value.typ == EnumToken.NumberTokenType) { + + return +value.val; + } + + switch (value.unit) { + case 'cm': + // @ts-ignore + return value.val * 37.8; + case 'mm': + // @ts-ignore + return value.val * 3.78; + case 'Q': + // @ts-ignore + return value.val * 37.8 / 40; + case 'in': + // @ts-ignore + return value.val / 96; + case 'pc': + // @ts-ignore + return value.val / 16; + case 'pt': + // @ts-ignore + return value.val * 4 / 3; + case 'px': + return +value.val; + } + + return null; +} \ No newline at end of file diff --git a/src/lib/ast/transform/index.ts b/src/lib/ast/transform/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/lib/ast/transform/matrix.ts b/src/lib/ast/transform/matrix.ts new file mode 100644 index 00000000..89d3cf8b --- /dev/null +++ b/src/lib/ast/transform/matrix.ts @@ -0,0 +1,98 @@ +import {identity, is2DMatrix, Matrix} from "./utils.ts"; +import {EnumToken} from "../types.ts"; +import type {Token} from "../../../@types/index.d.ts"; +import {reduceNumber} from "../../renderer"; + +export function matrix(values: [number, number, number, number, number, number] | [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]): Matrix | null { + + const matrix = identity(); + + if (values.length === 6) { + + matrix[0][0] = values[0]; + matrix[0][1] = values[1]; + matrix[1][0] = values[2]; + matrix[1][1] = values[3]; + matrix[3][0] = values[4]; + matrix[3][1] = values[5]; + } else if (values.length === 16) { + + matrix[0][0] = values[0]; + matrix[0][1] = values[1]; + matrix[0][2] = values[2]; + matrix[0][3] = values[3]; + matrix[1][0] = values[4]; + matrix[1][1] = values[5]; + matrix[1][2] = values[6]; + matrix[1][3] = values[7]; + matrix[2][0] = values[8]; + matrix[2][1] = values[9]; + matrix[2][2] = values[10]; + matrix[2][3] = values[11]; + matrix[3][0] = values[12]; + matrix[3][1] = values[13]; + matrix[3][2] = values[14]; + matrix[3][3] = values[15]; + } else { + + throw new RangeError('expecting 6 or 16 values'); + } + + return matrix; +} + +export function serialize(matrix: Matrix): Token { + + if (is2DMatrix(matrix)) { + + // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset + return { + typ: EnumToken.FunctionTokenType, + val: 'matrix', + chi: [ + matrix[0][0], + matrix[0][1], + matrix[1][0], + matrix[1][1], + matrix[3][0], + matrix[3][1] + ].reduce((acc,t) => { + + if (acc.length > 0) { + + acc.push({ typ: EnumToken.CommaTokenType }); + } + + acc.push({ + typ: EnumToken.NumberTokenType, + val: reduceNumber(t) + }) + + return acc + }, [] as Token[]) + } + } + + + return { + typ: EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: matrix[0].concat(matrix[1]).concat(matrix[2]).concat(matrix[3]).reduce((acc, t) => { + + if (acc.length > 0) { + + acc.push({ typ: EnumToken.CommaTokenType }); + } + + acc.push({ + typ: EnumToken.NumberTokenType, + val: reduceNumber(t) + + }); + + return acc; + + return acc; + }, [] as Token[]) + } +} \ No newline at end of file diff --git a/src/lib/ast/transform/minify.ts b/src/lib/ast/transform/minify.ts new file mode 100644 index 00000000..6ce36666 --- /dev/null +++ b/src/lib/ast/transform/minify.ts @@ -0,0 +1,141 @@ +import {decompose, Matrix} from "./utils.ts"; +import {EnumToken} from "../types.ts"; +import {Token} from "../../../@types"; + + +export function minify(matrix: Matrix): Token[] | null { + + const decomposed = decompose(matrix); + + if (decomposed == null) { + + return null; + } + + const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + + // check identity + if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { + + transforms.delete('translate'); + } + + if (decomposed.scale[0] == 1 && decomposed.scale[1] == 1 && decomposed.scale[2] == 1) { + + transforms.delete('scale'); + } + + if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0 && decomposed.skew[2] == 0) { + + transforms.delete('skew'); + } + + if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { + + transforms.delete('perspective'); + } + + if (transforms.size == 0) { + + // identity + return [{ + typ: EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + {typ: EnumToken.NumberTokenType, val: '1'} + ] + } + ]; + } + + if (transforms.size == 1) { + + if (transforms.has('translate')) { + + let coordinates = new Set(['x', 'y', 'z']); + + for (let i = 0; i < 3; i++) { + + if (decomposed.translate[i] == 0) { + + coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); + } + } + + if (coordinates.size == 3) { + + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px'} + ] + }] + } + + if (coordinates.size == 1) { + + if (coordinates.has('x')) { + + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [{typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}] + }] + } + + let axis: string = coordinates.has('y') ? 'y' : 'z'; + let index: number = axis == 'y' ? 1 : 2; + + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate' + axis.toUpperCase(), + chi: [{typ: EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px'}] + }] + } + + if (coordinates.has('z')) { + + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px'} + ] + }] + } + + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'} + ] + }] + } + } + + return null; +} \ No newline at end of file diff --git a/src/lib/ast/transform/perspective.ts b/src/lib/ast/transform/perspective.ts new file mode 100644 index 00000000..f36793c4 --- /dev/null +++ b/src/lib/ast/transform/perspective.ts @@ -0,0 +1,9 @@ +import {identity, Matrix} from "./utils.ts"; + +export function perspective(z: number): Matrix { + + const matrix: Matrix = identity(); + matrix[2][3] = -1 / z; + + return matrix; +} \ No newline at end of file diff --git a/src/lib/ast/transform/rotate.ts b/src/lib/ast/transform/rotate.ts new file mode 100644 index 00000000..a3dd1da0 --- /dev/null +++ b/src/lib/ast/transform/rotate.ts @@ -0,0 +1,75 @@ +import {Matrix} from "./utils.ts"; + +/** + * angle in radian + * @param angle + * @param x + * @param y + * @param z + * @param matrix + */ +export function rotate3D(angle: number, x: number, y: number, z: number, matrix:Matrix): Matrix { + + const sc: number= Math.sin(angle/2)*Math.cos(angle/2); + const sq: number = Math.sin(angle/2) * Math.sin(angle/2); + + const norm: number = Math.sqrt(x * x + y * y + z * z); + const unit: number = norm === 0 ? 0 : 1 / norm; + + x *= unit; + y *= unit; + z *= unit; + + matrix[0][0] = 1 - 2 * (y*y + z*z) * sq; + matrix[0][1] = 2 * (x * y * sq - z*sc); + matrix[0][2] = 2 * (x*z*sq + y*sc); + + matrix[1][0] = 2 * (x*y*sq + z*sc); + matrix[1][1] = 1 - 2 * (x*x + z*z) * sq; + matrix[1][2] = 2 * (y*z*sq - x*sc); + + matrix[2][0] = 2 * (x*z*sq - y*sc); + matrix[2][1] = 2 * (y*z*sq + x*sc); + matrix[2][2] = 1 - 2 * (x*x + y*y) * sq; + + return matrix; +} + +// export function rotateX(x: number): Matrix { +// +// const matrix: Matrix = identity(); +// +// const angle: number = x * Math.PI / 180; +// +// matrix[1][1] = Math.cos(angle); +// matrix[1][2] = Math.sin(angle); +// matrix[0][2] = -1; +// +// return matrix; +// } +// +// export function rotateY(y: number): Matrix { +// +// const matrix: Matrix = identity(); +// +// const angle: number = y * Math.PI / 180; +// matrix[0][0] = matrix[2][2] = Math.cos(angle); +// matrix[0][1] = matrix[2][0] = Math.sin(angle); +// matrix[0][2] = -1; +// +// return matrix; +// } +// +// export function rotateZ(z: number): Matrix { +// +// const matrix: Matrix = identity(); +// +// const angle: number = z * Math.PI / 180; +// matrix[0][0] = matrix[1][1] = Math.cos(angle); +// matrix[0][1] = matrix[1][0] = -Math.sin(angle); +// matrix[2][0] = -1; +// +// return matrix; +// } +// +// export const rotate = rotateZ; \ No newline at end of file diff --git a/src/lib/ast/transform/scale.ts b/src/lib/ast/transform/scale.ts new file mode 100644 index 00000000..ea114ce4 --- /dev/null +++ b/src/lib/ast/transform/scale.ts @@ -0,0 +1,39 @@ +import {identity, Matrix} from "./utils.ts"; + +export function scaleX(x: number): Matrix { + + + const matrix: Matrix = identity(); + + matrix[0][0] = x; + + return matrix; +} + +export function scaleY(y: number): Matrix { + + const matrix: Matrix = identity(); + + matrix[1][1] = y; + + return matrix; +} + +export function scaleZ(z: number): Matrix { + + const matrix: Matrix = identity(); + + matrix[2][2] = z; + + return matrix; +} + +export function scale(x: number, y?: number): Matrix { + + const matrix: Matrix = identity(); + + matrix[0][0] = x; + matrix[1][1] = y ?? x; + + return matrix; +} \ No newline at end of file diff --git a/src/lib/ast/transform/skew.ts b/src/lib/ast/transform/skew.ts new file mode 100644 index 00000000..cefeb2cd --- /dev/null +++ b/src/lib/ast/transform/skew.ts @@ -0,0 +1,32 @@ +import {identity, Matrix} from "./utils.ts"; + +export function skewX(x: number): Matrix { + + const matrix: Matrix = identity(); + + matrix[2][0] = Math.tan( x * Math.PI / 180); + return matrix; +} + +export function skewY(y: number): Matrix { + + const matrix: Matrix = identity(); + + matrix[0][1] = Math.tan( y * Math.PI / 180); + return matrix; +} + +// convert angle to radian +export function skew(x: number, y?: number): Matrix { + + const matrix: Matrix = identity(); + + matrix[2][0] = Math.tan( x * Math.PI / 180); + + if (y != null) { + + matrix[0][1] = Math.tan(y * Math.PI / 180); + } + + return matrix; +} \ No newline at end of file diff --git a/src/lib/ast/transform/translate.ts b/src/lib/ast/transform/translate.ts new file mode 100644 index 00000000..a09683e0 --- /dev/null +++ b/src/lib/ast/transform/translate.ts @@ -0,0 +1,33 @@ +import {Matrix} from "./utils.ts"; + +export function translateX(x: number, matrix: Matrix): Matrix { + + matrix[3][0] = x; + + return matrix; +} + +export function translateY(y: number, matrix: Matrix): Matrix { + + matrix[3][1] = y; + + return matrix; +} + +export function translateZ(z: number, matrix: Matrix): Matrix { + + matrix[3][2] = z; + + return matrix; +} + +export function translate(translate: [number] | [number, number] | [number, number, number], matrix: Matrix): Matrix { + + matrix[3][0] = translate[0]; + matrix[3][1] = translate[1] ?? 0; + matrix[3][2] = translate[2] ?? 0; + + return matrix; +} + +export const translate3d = translate; \ No newline at end of file diff --git a/src/lib/ast/transform/utils.ts b/src/lib/ast/transform/utils.ts new file mode 100644 index 00000000..dd1fcdf1 --- /dev/null +++ b/src/lib/ast/transform/utils.ts @@ -0,0 +1,395 @@ +export declare type Point = [number, number, number]; +export declare type Vector = [number, number, number, number]; +export declare type Matrix = [Vector, Vector, Vector, Vector]; + +interface DecomposedMatrix3D { + skew: [number, number, number]; + scale: [number, number, number]; + translate : [number, number, number]; + perspective : [number, number, number, number]; + quaternion : [number, number, number, number]; +} + +function determinant(matrix: Matrix): number { + return matrix[0][0] * matrix[1][1] * matrix[2][2] * matrix[3][3] - matrix[0][0] * matrix[1][2] * matrix[2][3] * matrix[3][1] - + matrix[0][1] * matrix[1][0] * matrix[2][3] * matrix[3][2] + matrix[0][1] * matrix[1][2] * matrix[2][0] * matrix[3][3] - + matrix[0][2] * matrix[1][0] * matrix[2][1] * matrix[3][3] + matrix[0][2] * matrix[1][1] * matrix[2][0] * matrix[3][2] - + matrix[0][3] * matrix[1][0] * matrix[2][1] * matrix[3][2] + matrix[0][3] * matrix[1][1] * matrix[2][2] * matrix[3][0]; +} + +export function identity(): Matrix { + + return [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] as Matrix; +} + + +function inverse(matrix: Matrix): Matrix { + + // Create augmented matrix [matrix | identity] + let augmented: Matrix = matrix.map((row, i) => [ + ...row, + ...(i === 0 ? [1, 0, 0, 0] : + i === 1 ? [0, 1, 0, 0] : + i === 2 ? [0, 0, 1, 0] : + [0, 0, 0, 1]) + ]) as Matrix; + + // Gaussian elimination with partial pivoting + for (let col = 0; col < 4; col++) { + // Find pivot row with maximum absolute value + let maxRow = col; + let maxVal = Math.abs(augmented[col][col]); + + for (let row = col + 1; row < 4; row++) { + + let val = Math.abs(augmented[row][col]); + + if (val > maxVal) { + + maxVal = val; + maxRow = row; + } + } + + // Check for singularity + if (maxVal < 1e-10) { + + throw new Error("Matrix is singular and cannot be inverted"); + } + + // Swap rows if necessary + if (maxRow !== col) { + + [augmented[col], augmented[maxRow]] = [augmented[maxRow], augmented[col]]; + } + + // Scale pivot row to make pivot element 1 + let pivot = augmented[col][col]; + + for (let j = 0; j < 8; j++) { + + augmented[col][j] /= pivot; + } + + // Eliminate column in other rows + for (let row = 0; row < 4; row++) { + + if (row !== col) { + + let factor = augmented[row][col]; + + for (let j = 0; j < 8; j++) { + + augmented[row][j] -= factor * augmented[col][j]; + } + } + } + } + + // Extract the inverse from the right side of the augmented matrix + return augmented.map(row => row.slice(4)) as Matrix; +} + +function transpose(matrix: Matrix): Matrix { + // Crée une nouvelle matrice vide 4x4 + // @ts-ignore + let transposed: Matrix = [[], [], [], []] as Matrix; + + // Parcourt chaque ligne et colonne pour transposer + for (let i = 0; i < 4; i++) { + + for (let j = 0; j < 4; j++) { + + transposed[j][i] = matrix[i][j]; + } + } + + return transposed; +} + +function multVecMatrix(vector: Vector, matrix: Matrix): Vector { + + const result:Vector = [0, 0, 0, 0]; + + for (let i = 0; i < 4; i++) { + + for (let j = 0; j < 4; j++) { + + result[i] += matrix[i][j] * vector[j]; + } + } + + return result; +} + +function pLength(point: Point): number { + + // Calcul de la norme euclidienne + return Math.sqrt(point[0] * point[0] + point[1] * point[1] + point[2] * point[2]); +} + +function normalize(point: Point): Point { + + const [x, y, z] = point; + const norm: number = Math.sqrt(point[0] * point[0] + point[1] * point[1] + point[2] * point[2]); + return norm === 0 ? [0, 0, 0] : [x / norm, y / norm, z / norm]; +} + +function dot(point1: Point, point2: Point): number { + + return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2]; +} + +function combine(point1: Point, point2: Point, ascl: number, bscl: number): Point { + + return [point1[0] * ascl + point2[0] * bscl, point1[1] * ascl + point2[1] * bscl, point1[2] * ascl + point2[2] * bscl]; +} + +function cross(a: Point, b: Point): Point { + return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; +} +function multiply(matrixA: Matrix, matrixB: Matrix): Matrix { + + let result: Matrix = Array(4).fill(0).map(() => Array(4).fill(0)) as Matrix; + + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + for (let k = 0; k < 4; k++) { + result[i][j] += matrixA[i][k] * matrixB[k][j]; + } + } + } + + return result; +} + +export function decompose(matrix: Matrix): DecomposedMatrix3D | null { +// Normalize the matrix. + if (matrix[3][3] === 0) { + + return null; + } + + for (let i = 0; i < 4; i++) { + + for (let j = 0; j < 4; j++) { + + matrix[i][j] /= matrix[3][3]; + } + } + +// perspectiveMatrix is used to solve for perspective, but it also provides +// an easy way to test for singularity of the upper 3x3 component. + let perspectiveMatrix: Matrix = matrix; + + for (let i = 0; i < 3; i++) { + + perspectiveMatrix[i][3] = 0; + } + + perspectiveMatrix[3][3] = 1; + + if (determinant(perspectiveMatrix) === 0) { + + return null; + } + + let rightHandSide: Vector = [0, 0, 0, 0]; + let perspective: Vector = [0, 0, 0, 0]; + let translate: [number, number, number] = [0, 0, 0]; + +// First, isolate perspective. + if (matrix[0][3] !== 0 || matrix[1][3] !== 0 || matrix[2][3] !== 0) { + // rightHandSide is the right hand side of the equation. + rightHandSide[0] = matrix[0][3]; + rightHandSide[1] = matrix[1][3]; + rightHandSide[2] = matrix[2][3]; + rightHandSide[3] = matrix[3][3]; + + // Solve the equation by inverting perspectiveMatrix and multiplying + // rightHandSide by the inverse. + let inversePerspectiveMatrix = inverse(perspectiveMatrix); + let transposedInversePerspectiveMatrix = transpose(inversePerspectiveMatrix); + perspective = multVecMatrix(rightHandSide, transposedInversePerspectiveMatrix); + } else { + // No perspective. + perspective[0] = perspective[1] = perspective[2] = 0; + perspective[3] = 1; + } + +// Next take care of translation + for (let i = 0; i < 3; i++) { + + translate[i] = matrix[3][i]; + } + + let row: [[number, number, number], [number, number, number], [number, number, number]] = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; + +// Now get scale and shear. 'row' is a 3 element array of 3 component vectors + for (let i = 0; i < 3; i++) { + row[i][0] = matrix[i][0]; + row[i][1] = matrix[i][1]; + row[i][2] = matrix[i][2]; + } + + let scale: [number, number, number] = [0, 0, 0]; + let skew: [number, number, number] = [0, 0, 0]; + +// Compute X scale factor and normalize first row. + scale[0] = pLength(row[0]); + row[0] = normalize(row[0]); + +// Compute XY shear factor and make 2nd row orthogonal to 1st. + skew[0] = dot(row[0], row[1]); + row[1] = combine(row[1], row[0], 1.0, -skew[0]); + +// Now, compute Y scale and normalize 2nd row. + scale[1] = pLength(row[1]); + row[1] = normalize(row[1]); + skew[0] /= scale[1]; + +// Compute XZ and YZ shears, orthogonalize 3rd row + skew[1] = dot(row[0], row[2]); + row[2] = combine(row[2], row[0], 1.0, -skew[1]); + skew[2] = dot(row[1], row[2]); + row[2] = combine(row[2], row[1], 1.0, -skew[2]); + +// Next, get Z scale and normalize 3rd row. + scale[2] = pLength(row[2]); + row[2] = normalize(row[2]); + skew[1] /= scale[2]; + skew[2] /= scale[2]; + +// At this point, the matrix (in rows) is orthonormal. +// Check for a coordinate system flip. If the determinant +// is -1, then negate the matrix and the scaling factors. + let pdum3 = cross(row[1], row[2]); + if (dot(row[0], pdum3) < 0) { + for (let i = 0; i < 3; i++) { + scale[i] *= -1; + row[i][0] *= -1; + row[i][1] *= -1; + row[i][2] *= -1; + } + } + + let quaternion: [number, number, number, number] = [0, 0, 0, 0]; + +// Now, get the rotations out + quaternion[0] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] - row[1][1] - row[2][2], 0)); + quaternion[1] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] + row[1][1] - row[2][2], 0)); + quaternion[2] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] - row[1][1] + row[2][2], 0)); + quaternion[3] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] + row[1][1] + row[2][2], 0)); + + if (row[2][1] > row[1][2]) { + + quaternion[0] = -quaternion[0]; + } + + if (row[0][2] > row[2][0]) { + + quaternion[1] = -quaternion[1]; + } + + if (row[1][0] > row[0][1]) { + + quaternion[2] = -quaternion[2]; + } + + return { + skew, + scale, + translate, + perspective, + quaternion + }; +} + +export function recompose( + translate: [number, number, number], + scale: [number, number, number], + skew: [number, number, number], + perspective: [number, number, number, number], + quaternion: [number, number, number, number] +): Matrix { + + let matrix: Matrix = identity(); +// apply perspective + for (let i = 0; i < 4; i++) { + matrix[i][3] = perspective[i]; + } + +// apply translation + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 3; j++) { + matrix[3][i] += translate[j] * matrix[j][i]; + } + } + +// apply rotation + let x = quaternion[0]; + let y = quaternion[1]; + let z = quaternion[2]; + let w = quaternion[3]; + + const rotationMatrix: Matrix = identity(); +// Construct a composite rotation matrix from the quaternion values +// rotationMatrix is an identity 4x4 matrix initially + rotationMatrix[0][0] = 1 - 2 * (y * y + z * z); + rotationMatrix[0][1] = 2 * (x * y - z * w); + rotationMatrix[0][2] = 2 * (x * z + y * w); + rotationMatrix[1][0] = 2 * (x * y + z * w); + rotationMatrix[1][1] = 1 - 2 * (x * x + z * z); + rotationMatrix[1][2] = 2 * (y * z - x * w); + rotationMatrix[2][0] = 2 * (x * z - y * w); + rotationMatrix[2][1] = 2 * (y * z + x * w); + rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); + + matrix = multiply(matrix, rotationMatrix); + + let temp: Matrix = identity(); + +// apply skew +// temp is an identity 4x4 matrix initially + if (skew[2]) { + temp[2][1] = skew[2]; + matrix = multiply(matrix, temp); + } + + if (skew[1]) { + temp[2][1] = 0; + temp[2][0] = skew[1]; + matrix = multiply(matrix, temp); + } + + if (skew[0]) { + temp[2][0] = 0; + temp[1][0] = skew[0]; + matrix = multiply(matrix, temp); + } + +// apply scale + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 4; j++) { + matrix[i][j] *= scale[i]; + } + } + + return matrix; +} + +// https://drafts.csswg.org/css-transforms-1/#2d-matrix +export function is2DMatrix(matrix: Matrix): boolean { + + // m13,m14, m23, m24, m31, m32, m34, m43 are all 0 + return matrix[0][2] === 0 && + matrix[0][3] === 0 && + matrix[1][2] === 0 && + matrix[1][3] === 0 && + matrix[2][0] === 0 && + matrix[2][1] === 0 && + matrix[2][3] === 0 && + matrix[3][2] === 0 && + matrix[2][2] === 1 && + matrix[3][3] === 1; +} \ No newline at end of file diff --git a/src/lib/renderer/color/color.ts b/src/lib/renderer/color/color.ts index d9d78bdf..78765964 100644 --- a/src/lib/renderer/color/color.ts +++ b/src/lib/renderer/color/color.ts @@ -752,6 +752,10 @@ export function getNumber(token: NumberToken | PercentageToken | IdentToken): nu return token.typ == EnumToken.PercentageTokenType ? token.val / 100 : +token.val; } +/** + * convert angle to turn + * @param token + */ export function getAngle(token: NumberToken | AngleToken | IdentToken): number { if (token.typ == EnumToken.IdenTokenType) { diff --git a/src/lib/renderer/render.ts b/src/lib/renderer/render.ts index 5065c5f4..c40675e1 100644 --- a/src/lib/renderer/render.ts +++ b/src/lib/renderer/render.ts @@ -684,6 +684,16 @@ export function renderToken(token: Token, options: RenderOptions = {}, cache: { return token.chi.reduce((acc: string, curr: Token) => acc + renderToken(curr, options, cache, reducer), '') } + // if (token.typ == EnumToken.FunctionTokenType && transformFunctions.includes(token.val)) { + // + // const children = token.val.startsWith('matrix') ? null : stripCommaToken(token.chi.slice()) as Token[]; + // + // if (children != null) { + // + // return token.val + '(' + children.reduce((acc: string, curr: Token) => acc + (acc.length > 0 ? ' ' : '') + renderToken(curr, options, cache, reducer), '') + ')'; + // } + // } + // @ts-ignore return (/* options.minify && 'Pseudo-class-func' == token.typ && token.val.slice(0, 2) == '::' ? token.val.slice(1) :*/ token.val ?? '') + '(' + token.chi.reduce(reducer, '') + ')'; @@ -1019,6 +1029,5 @@ export function renderToken(token: Token, options: RenderOptions = {}, cache: { } errors?.push({action: 'ignore', message: `render: unexpected token ${JSON.stringify(token, null, 1)}`}); - return ''; } \ No newline at end of file diff --git a/src/lib/syntax/syntax.ts b/src/lib/syntax/syntax.ts index 03df6430..68359c7d 100644 --- a/src/lib/syntax/syntax.ts +++ b/src/lib/syntax/syntax.ts @@ -28,6 +28,15 @@ export const fontFormat: string[] = ['collection', 'embedded-opentype', 'opentyp export const colorFontTech: string[] = ['color-colrv0', 'color-colrv1', 'color-svg', 'color-sbix', 'color-cbdt']; export const fontFeaturesTech: string[] = ['features-opentype', 'features-aat', 'features-graphite', 'incremental-patch', 'incremental-range', 'incremental-auto', 'variations', 'palettes']; +export const transformFunctions: string[] = [ + 'matrix', 'translate', 'scale', 'rotate', 'skew', 'perspective', + 'translateX', 'translateY', 'translateZ', + 'scaleX', 'scaleY', 'scaleZ', + 'rotateX', 'rotateY', 'rotateZ', + 'skewX', 'skewY', + 'rotate3d', 'translate3d', 'scale3d', 'matrix3d' +]; + // https://drafts.csswg.org/mediaqueries/#media-types export const mediaTypes: string[] = ['all', 'print', 'screen', /* deprecated */ diff --git a/src/lib/validation/utils/list.ts b/src/lib/validation/utils/list.ts index 578f6faa..ea7d98da 100644 --- a/src/lib/validation/utils/list.ts +++ b/src/lib/validation/utils/list.ts @@ -1,6 +1,34 @@ import {EnumToken} from "../../ast"; import type {Token} from "../../../@types"; +export function stripCommaToken(tokenList: Token[]): Token[] | null { + + let result: Token[] = []; + let last: Token | null = null; + + for (let i = 0; i < tokenList.length; i++) { + + if (tokenList[i].typ == EnumToken.CommaTokenType && last != null && last.typ == EnumToken.CommaTokenType) { + + return null; + } + + if (tokenList[i].typ != EnumToken.WhitespaceTokenType) { + + last = tokenList[i]; + } + + if (tokenList[i].typ == EnumToken.CommentTokenType || tokenList[i].typ == EnumToken.CommaTokenType) { + + continue; + } + + result.push(tokenList[i]); + } + + return result; +} + export function splitTokenList(tokenList: Token[], split: EnumToken[] = [EnumToken.CommaTokenType]): Token[][] { return tokenList.reduce((acc: Token[][], curr: Token): Token[][] => { diff --git a/test/specs/code/index.js b/test/specs/code/index.js index 68f18f07..af056bd9 100644 --- a/test/specs/code/index.js +++ b/test/specs/code/index.js @@ -18,5 +18,6 @@ export * as visitors from './visitors.js'; export * as walk from './walk.js'; export * as validation from './validation.js'; export * as atRules from './at-rules.js'; -export * as lenient from './lenient.js' -export * as minify from './minify.js' \ No newline at end of file +export * as lenient from './lenient.js'; +export * as minify from './minify.js'; +export * as transform from './transform.js'; \ No newline at end of file diff --git a/test/specs/code/nesting.js b/test/specs/code/nesting.js index a8ddb857..4b6448ee 100644 --- a/test/specs/code/nesting.js +++ b/test/specs/code/nesting.js @@ -554,111 +554,38 @@ table.colortable { }`)); }); - // see https://www.w3.org/TR/css-nesting-1/#conditionals - /* - .header { - font-size: 40px; - } - - @media (max-width: 760px) { - .header { - font-size: 24px; - } - } - */ - /* - - // .parent { - // color: red; - // } - // .parent - // - // /* - // Valid because it begins with a combinator, which is a - // form of relative selector. - // */ -// > .descendant { -// border: 1px solid black; -// } -// .parent -// -// /* -// Valid because it's equivalent of *.img, which is a complex -// selector. Complex selectors are also a form of relative -// selector. -// */ -// .nested { -// font-style: italic; -// } -// .parent -// -// /* Not valid. Type (element) selectors are not relative selectors. */ -// img { -// box-shadow: 0 0 10px 5px rgba(0 0 0 .5); -// } -// } -// */ - /* - .foo { - display: grid; - } + it('mix rules and declarations #24', function () { - @media (width => 30em) { - .foo { - grid-auto-flow: column; - } - } - */ - /* - @media, @supports) - - @layer - - @scope - - @container - - */ - -// it('nesting #16', function () { -// const nesting3 = ` -// .header { -// background-color: white; -// } -// -// .dark .header { -// background-color: blue; -// } -// `; -// return transform(nesting3, { -// minify: true, nestingRules: true, resolveImport: true -// }).then((result) => expect(result.code).equals(`.nav-pills{.nav-link.active,.show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}}`)); -// }); - -// it('nesting #17', function () { -// const nesting3 = ` -// .header { -// font-size: 40px; -// } -// -// @media (max-width: 760px) { -// .header { -// font-size: 24px; -// } -// } -// `; -// return transform(nesting3, { -// minify: true, nestingRules: true, resolveImport: true -// }).then((result) => expect(result.code).equals(`.header { -// font-size: 40px -// -// @media (max-width: 760px ) { -// & { -// font-size: 24px; -// } -// } -// }`)); -// }); + const css = ` +article { + color: green; + & { color: blue; } + color: red; + & { color: green; } +} +`; + + return transform(css, { + expandNestingRules: true + }).then(result => expect(result.code).equals(`article{color:green}`)); + }); + + + it('mix rules and declarations #24', function () { + + const css = ` +article { + color: green; + & { color: blue; } + color: red; + & { color: green; } +} +`; + + return transform(css, { + expandNestingRules: false + }).then(result => expect(result.code).equals(`article{color:red;&{color:blue}&{color:green}}`)); + }); }); } \ No newline at end of file diff --git a/test/specs/code/transform.js b/test/specs/code/transform.js new file mode 100644 index 00000000..977eac80 --- /dev/null +++ b/test/specs/code/transform.js @@ -0,0 +1,118 @@ +export function run(describe, expect, transform, parse, render, dirname, readFile) { + + describe('CSS translate', function () { + it('translateY #1', function () { + const nesting1 = ` + + .now { + transform: translate(0,-100px) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: translateY(-100px) +}`)); + }); + + it('translateX #2', function () { + const nesting1 = ` + + .now { + transform: translate(-100px, 0) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: translate(-100px) +}`)); + }); + + it('translateX #2', function () { + const nesting1 = ` + + .now { + transform: translate3d(-100px, 0, 0) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: translate(-100px) +}`)); + }); + + it('translateY #4', function () { + const nesting1 = ` + + .now { + transform: translate3d(0, 0, -100px) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: translateZ(-100px) +}`)); + }); + + it('translate(0) #5', function () { + const nesting1 = ` + + .now { + transform: translateX(0) translateY(0) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: scale(1) +}`)); + }); + + it('translate(0) #5', function () { + const nesting1 = ` + + .now { + transform: translateX(0) translateY(0) translateZ(0) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: scale(1) +}`)); + }); + + it('translateZ #6', function () { + const nesting1 = ` + + .now { + transform: translateX(0) translateY(0) translateZ(14px) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: translateZ(14px) +}`)); + }); + + it('translateZ #7', function () { + const nesting1 = ` + + .now { + transform: translateX(14px) translateY(0) translateZ(14px) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: translate(14px,0,14px) +}`)); + }); + + }); + +} \ No newline at end of file From 1a88b6651f575af76a68f823882d30e9f3f0700e Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Sun, 23 Mar 2025 13:44:40 -0400 Subject: [PATCH 02/10] add missing files #75 --- dist/lib/ast/features/transform.js | 55 ++++++++ dist/lib/ast/features/translate.js | 59 ++++++++ dist/lib/ast/transform/compute.js | 155 +++++++++++++++++++++ dist/lib/ast/transform/convert.js | 33 +++++ dist/lib/ast/transform/matrix.js | 46 +++++++ dist/lib/ast/transform/minify.js | 110 +++++++++++++++ dist/lib/ast/transform/translate.js | 20 +++ dist/lib/ast/transform/utils.js | 206 ++++++++++++++++++++++++++++ 8 files changed, 684 insertions(+) create mode 100644 dist/lib/ast/features/transform.js create mode 100644 dist/lib/ast/features/translate.js create mode 100644 dist/lib/ast/transform/compute.js create mode 100644 dist/lib/ast/transform/convert.js create mode 100644 dist/lib/ast/transform/matrix.js create mode 100644 dist/lib/ast/transform/minify.js create mode 100644 dist/lib/ast/transform/translate.js create mode 100644 dist/lib/ast/transform/utils.js diff --git a/dist/lib/ast/features/transform.js b/dist/lib/ast/features/transform.js new file mode 100644 index 00000000..67d30706 --- /dev/null +++ b/dist/lib/ast/features/transform.js @@ -0,0 +1,55 @@ +import { EnumToken } from '../types.js'; +import { consumeWhitespace } from '../../validation/utils/whitespace.js'; +import '../minify.js'; +import '../walk.js'; +import '../../parser/parse.js'; +import '../../renderer/color/utils/constants.js'; +import '../../renderer/sourcemap/lib/encode.js'; +import '../../parser/utils/config.js'; +import { compute } from '../transform/compute.js'; +import { decompose } from '../transform/utils.js'; +import { minify } from '../transform/minify.js'; + +class TransformCssFeature { + static get ordering() { + return 4; + } + static register(options) { + // @ts-ignore + if (options.minify || options.computeCalcExpression || options.computeShorthand) { + // @ts-ignore + options.features.push(new TransformCssFeature()); + } + } + run(ast) { + if (!('chi' in ast)) { + return; + } + let i = 0; + let node; + // @ts-ignore + for (; i < ast.chi.length; i++) { + // @ts-ignore + node = ast.chi[i]; + if (node.typ != EnumToken.DeclarationNodeType || + (!node.nam.startsWith('--') && !node.nam.match(/^(-[a-z]+-)?transform$/))) { + continue; + } + const children = node.val.slice(); + consumeWhitespace(children); + const result = compute(children); + if (result == null) { + // console.error({result}); + return; + } + decompose(result); + const minified = minify(result); + // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); + if (minified != null) { + node.val = minified; + } + } + } +} + +export { TransformCssFeature }; diff --git a/dist/lib/ast/features/translate.js b/dist/lib/ast/features/translate.js new file mode 100644 index 00000000..18ad1ed6 --- /dev/null +++ b/dist/lib/ast/features/translate.js @@ -0,0 +1,59 @@ +import { EnumToken } from '../types.js'; +import { consumeWhitespace } from '../../validation/utils/whitespace.js'; +import '../minify.js'; +import '../walk.js'; +import '../../parser/parse.js'; +import '../../renderer/color/utils/constants.js'; +import '../../renderer/sourcemap/lib/encode.js'; +import '../../parser/utils/config.js'; + +class TranslateCssFeature { + static get ordering() { + return 4; + } + static register(options) { + // @ts-ignore + if (options.minify || options.computeCalcExpression || options.computeShorthand) { + // for (const feature of options.features) { + // + // if (feature instanceof ComputeCalcExpressionFeature) { + // + // return + // } + // } + // @ts-ignore + options.features.push(new TranslateCssFeature()); + } + } + run(ast) { + if (!('chi' in ast)) { + return; + } + let i = 0; + // @ts-ignore + for (; i < ast.chi.length; i++) { + const node = ast.chi[i]; + if (node.typ != EnumToken.DeclarationNodeType || + (!node.nam.startsWith('--') && !node.nam.match(/^(-[a-z]+-)?transform$/))) { + continue; + } + const translationMap = { + x: null, + y: null, + z: null + }; + const children = node.val.slice(); + consumeWhitespace(children); + for (const value of children) { + if (value.typ == EnumToken.FunctionTokenType && value.val == 'translate3d') { + translationMap.x = value.chi[0]; + translationMap.y = value.chi[1]; + translationMap.z = value.chi[2]; + } + } + console.error({ node, translationMap }); + } + } +} + +export { TranslateCssFeature }; diff --git a/dist/lib/ast/transform/compute.js b/dist/lib/ast/transform/compute.js new file mode 100644 index 00000000..d390f554 --- /dev/null +++ b/dist/lib/ast/transform/compute.js @@ -0,0 +1,155 @@ +import { identity } from './utils.js'; +import { EnumToken } from '../types.js'; +import { length2Px } from './convert.js'; +import { transformFunctions } from '../../syntax/syntax.js'; +import '../minify.js'; +import '../walk.js'; +import '../../parser/parse.js'; +import '../../parser/utils/config.js'; +import '../../renderer/color/utils/constants.js'; +import '../../renderer/sourcemap/lib/encode.js'; +import { stripCommaToken } from '../../validation/utils/list.js'; +import { translateX, translateY, translateZ, translate } from './translate.js'; + +function compute(transformList) { + transformList = transformList.slice(); + stripCommaToken(transformList); + if (transformList.length == 0) { + return null; + } + const matrix = identity(); + let values = []; + let val; + for (let i = 0; i < transformList.length; i++) { + if (transformList[i].typ == EnumToken.WhitespaceTokenType) { + continue; + } + if (transformList[i].typ != EnumToken.FunctionTokenType || !transformFunctions.includes(transformList[i].val)) { + return null; + } + switch (transformList[i].val) { + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': + { + values.length = 0; + const children = stripCommaToken(transformList[i].chi.slice()); + if (children == null || children.length == 0) { + return null; + } + const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; + console.error([transformList[i].val, valCount]); + if (children.length == 1 && children[0].typ == EnumToken.IdenTokenType && children[0].val == 'none') { + values.fill(0, 0, valCount); + } + else { + for (let j = 0; j < children.length; j++) { + if (children[j].typ == EnumToken.WhitespaceTokenType) { + continue; + } + val = length2Px(children[j]); + if (val == null) { + return null; + } + values.push(val); + } + } + if (values.length == 0 || values.length > valCount) { + return null; + } + if (transformList[i].val == 'translateX') { + translateX(values[0], matrix); + } + else if (transformList[i].val == 'translateY') { + translateY(values[0], matrix); + } + else if (transformList[i].val == 'translateZ') { + translateZ(values[0], matrix); + } + else { + // @ts-ignore + translate(values, matrix); + } + } + break; + // case 'rotate': + // case 'rotateX': + // case 'rotateY': + // // case 'rotateZ': + // // case 'rotate3d': + // + // { + // let x: number = 0; + // let y: number = 0; + // let z: number = 0; + // + // let angle: number; + // let values: number[] = []; + // + // if ((transformList[i] as FunctionToken).val == 'rotateX' || (transformList[i] as FunctionToken).val == 'rotateY' || (transformList[i] as FunctionToken).val == 'rotateZ') { + // + // for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { + // + // if (child.typ == EnumToken.WhitespaceTokenType) { + // + // continue; + // } + // + // // if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { + // // + // // return null; + // // } + // + // if (child.typ == EnumToken.NumberTokenType && +child.val > 0) { + // + // return null; + // } + // + // angle = getAngle(child as AngleToken | NumberToken); + // + // if (angle == null) { + // + // return null; + // } + // + // values.push(angle * 2 * Math.PI); + // + // if ((transformList[i] as FunctionToken).val == 'rotateX') { + // + // x = 1; + // } else if ((transformList[i] as FunctionToken).val == 'rotateY') { + // + // y = 1; + // } else if ((transformList[i] as FunctionToken).val == 'rotateZ') { + // + // z = 1; + // } + // } + // + // if (values.length != 1) { + // + // return null; + // } + // + // console.error({values}); + // + // return null; + // } + // + // else if ((transformList[i] as FunctionToken).val == 'rotate') { + // + // } + // } + // + // break; + default: + return null; + // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); + } + } + return matrix; +} + +export { compute }; diff --git a/dist/lib/ast/transform/convert.js b/dist/lib/ast/transform/convert.js new file mode 100644 index 00000000..19d41f22 --- /dev/null +++ b/dist/lib/ast/transform/convert.js @@ -0,0 +1,33 @@ +import { EnumToken } from '../types.js'; + +// https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#absolute_length_units +function length2Px(value) { + if (value.typ == EnumToken.NumberTokenType) { + return +value.val; + } + switch (value.unit) { + case 'cm': + // @ts-ignore + return value.val * 37.8; + case 'mm': + // @ts-ignore + return value.val * 3.78; + case 'Q': + // @ts-ignore + return value.val * 37.8 / 40; + case 'in': + // @ts-ignore + return value.val / 96; + case 'pc': + // @ts-ignore + return value.val / 16; + case 'pt': + // @ts-ignore + return value.val * 4 / 3; + case 'px': + return +value.val; + } + return null; +} + +export { length2Px }; diff --git a/dist/lib/ast/transform/matrix.js b/dist/lib/ast/transform/matrix.js new file mode 100644 index 00000000..5b724969 --- /dev/null +++ b/dist/lib/ast/transform/matrix.js @@ -0,0 +1,46 @@ +import { is2DMatrix } from './utils.js'; +import { EnumToken } from '../types.js'; +import { reduceNumber } from '../../renderer/render.js'; + +function serialize(matrix) { + if (is2DMatrix(matrix)) { + // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset + return { + typ: EnumToken.FunctionTokenType, + val: 'matrix', + chi: [ + matrix[0][0], + matrix[0][1], + matrix[1][0], + matrix[1][1], + matrix[3][0], + matrix[3][1] + ].reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: EnumToken.CommaTokenType }); + } + acc.push({ + typ: EnumToken.NumberTokenType, + val: reduceNumber(t) + }); + return acc; + }, []) + }; + } + return { + typ: EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: matrix[0].concat(matrix[1]).concat(matrix[2]).concat(matrix[3]).reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: EnumToken.CommaTokenType }); + } + acc.push({ + typ: EnumToken.NumberTokenType, + val: reduceNumber(t) + }); + return acc; + }, []) + }; +} + +export { serialize }; diff --git a/dist/lib/ast/transform/minify.js b/dist/lib/ast/transform/minify.js new file mode 100644 index 00000000..63bcf20b --- /dev/null +++ b/dist/lib/ast/transform/minify.js @@ -0,0 +1,110 @@ +import { decompose } from './utils.js'; +import { EnumToken } from '../types.js'; + +function minify(matrix) { + const decomposed = decompose(matrix); + if (decomposed == null) { + return null; + } + const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + // check identity + if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { + transforms.delete('translate'); + } + if (decomposed.scale[0] == 1 && decomposed.scale[1] == 1 && decomposed.scale[2] == 1) { + transforms.delete('scale'); + } + if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0 && decomposed.skew[2] == 0) { + transforms.delete('skew'); + } + if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { + transforms.delete('perspective'); + } + if (transforms.size == 0) { + // identity + return [{ + typ: EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + { typ: EnumToken.NumberTokenType, val: '1' } + ] + } + ]; + } + if (transforms.size == 1) { + if (transforms.has('translate')) { + let coordinates = new Set(['x', 'y', 'z']); + for (let i = 0; i < 3; i++) { + if (decomposed.translate[i] == 0) { + coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); + } + } + if (coordinates.size == 3) { + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }]; + } + if (coordinates.size == 1) { + if (coordinates.has('x')) { + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [{ typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] + }]; + } + let axis = coordinates.has('y') ? 'y' : 'z'; + let index = axis == 'y' ? 1 : 2; + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate' + axis.toUpperCase(), + chi: [{ typ: EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] + }]; + } + if (coordinates.has('z')) { + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }]; + } + return [{ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } + ] + }]; + } + } + return null; +} + +export { minify }; diff --git a/dist/lib/ast/transform/translate.js b/dist/lib/ast/transform/translate.js new file mode 100644 index 00000000..3d173cc7 --- /dev/null +++ b/dist/lib/ast/transform/translate.js @@ -0,0 +1,20 @@ +function translateX(x, matrix) { + matrix[3][0] = x; + return matrix; +} +function translateY(y, matrix) { + matrix[3][1] = y; + return matrix; +} +function translateZ(z, matrix) { + matrix[3][2] = z; + return matrix; +} +function translate(translate, matrix) { + matrix[3][0] = translate[0]; + matrix[3][1] = translate[1] ?? 0; + matrix[3][2] = translate[2] ?? 0; + return matrix; +} + +export { translate, translateX, translateY, translateZ }; diff --git a/dist/lib/ast/transform/utils.js b/dist/lib/ast/transform/utils.js new file mode 100644 index 00000000..63ed2792 --- /dev/null +++ b/dist/lib/ast/transform/utils.js @@ -0,0 +1,206 @@ +function determinant(matrix) { + return matrix[0][0] * matrix[1][1] * matrix[2][2] * matrix[3][3] - matrix[0][0] * matrix[1][2] * matrix[2][3] * matrix[3][1] - + matrix[0][1] * matrix[1][0] * matrix[2][3] * matrix[3][2] + matrix[0][1] * matrix[1][2] * matrix[2][0] * matrix[3][3] - + matrix[0][2] * matrix[1][0] * matrix[2][1] * matrix[3][3] + matrix[0][2] * matrix[1][1] * matrix[2][0] * matrix[3][2] - + matrix[0][3] * matrix[1][0] * matrix[2][1] * matrix[3][2] + matrix[0][3] * matrix[1][1] * matrix[2][2] * matrix[3][0]; +} +function identity() { + return [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]; +} +function inverse(matrix) { + // Create augmented matrix [matrix | identity] + let augmented = matrix.map((row, i) => [ + ...row, + ...(i === 0 ? [1, 0, 0, 0] : + i === 1 ? [0, 1, 0, 0] : + i === 2 ? [0, 0, 1, 0] : + [0, 0, 0, 1]) + ]); + // Gaussian elimination with partial pivoting + for (let col = 0; col < 4; col++) { + // Find pivot row with maximum absolute value + let maxRow = col; + let maxVal = Math.abs(augmented[col][col]); + for (let row = col + 1; row < 4; row++) { + let val = Math.abs(augmented[row][col]); + if (val > maxVal) { + maxVal = val; + maxRow = row; + } + } + // Check for singularity + if (maxVal < 1e-10) { + throw new Error("Matrix is singular and cannot be inverted"); + } + // Swap rows if necessary + if (maxRow !== col) { + [augmented[col], augmented[maxRow]] = [augmented[maxRow], augmented[col]]; + } + // Scale pivot row to make pivot element 1 + let pivot = augmented[col][col]; + for (let j = 0; j < 8; j++) { + augmented[col][j] /= pivot; + } + // Eliminate column in other rows + for (let row = 0; row < 4; row++) { + if (row !== col) { + let factor = augmented[row][col]; + for (let j = 0; j < 8; j++) { + augmented[row][j] -= factor * augmented[col][j]; + } + } + } + } + // Extract the inverse from the right side of the augmented matrix + return augmented.map(row => row.slice(4)); +} +function transpose(matrix) { + // Crée une nouvelle matrice vide 4x4 + // @ts-ignore + let transposed = [[], [], [], []]; + // Parcourt chaque ligne et colonne pour transposer + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + transposed[j][i] = matrix[i][j]; + } + } + return transposed; +} +function multVecMatrix(vector, matrix) { + const result = [0, 0, 0, 0]; + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + result[i] += matrix[i][j] * vector[j]; + } + } + return result; +} +function pLength(point) { + // Calcul de la norme euclidienne + return Math.sqrt(point[0] * point[0] + point[1] * point[1] + point[2] * point[2]); +} +function normalize(point) { + const [x, y, z] = point; + const norm = Math.sqrt(point[0] * point[0] + point[1] * point[1] + point[2] * point[2]); + return norm === 0 ? [0, 0, 0] : [x / norm, y / norm, z / norm]; +} +function dot(point1, point2) { + return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2]; +} +function combine(point1, point2, ascl, bscl) { + return [point1[0] * ascl + point2[0] * bscl, point1[1] * ascl + point2[1] * bscl, point1[2] * ascl + point2[2] * bscl]; +} +function cross(a, b) { + return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; +} +function decompose(matrix) { + // Normalize the matrix. + if (matrix[3][3] === 0) { + return null; + } + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + matrix[i][j] /= matrix[3][3]; + } + } + // perspectiveMatrix is used to solve for perspective, but it also provides + // an easy way to test for singularity of the upper 3x3 component. + let perspectiveMatrix = matrix; + for (let i = 0; i < 3; i++) { + perspectiveMatrix[i][3] = 0; + } + perspectiveMatrix[3][3] = 1; + if (determinant(perspectiveMatrix) === 0) { + return null; + } + let rightHandSide = [0, 0, 0, 0]; + let perspective = [0, 0, 0, 0]; + let translate = [0, 0, 0]; + // First, isolate perspective. + if (matrix[0][3] !== 0 || matrix[1][3] !== 0 || matrix[2][3] !== 0) { + // rightHandSide is the right hand side of the equation. + rightHandSide[0] = matrix[0][3]; + rightHandSide[1] = matrix[1][3]; + rightHandSide[2] = matrix[2][3]; + rightHandSide[3] = matrix[3][3]; + // Solve the equation by inverting perspectiveMatrix and multiplying + // rightHandSide by the inverse. + let inversePerspectiveMatrix = inverse(perspectiveMatrix); + let transposedInversePerspectiveMatrix = transpose(inversePerspectiveMatrix); + perspective = multVecMatrix(rightHandSide, transposedInversePerspectiveMatrix); + } + else { + // No perspective. + perspective[0] = perspective[1] = perspective[2] = 0; + perspective[3] = 1; + } + // Next take care of translation + for (let i = 0; i < 3; i++) { + translate[i] = matrix[3][i]; + } + let row = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; + // Now get scale and shear. 'row' is a 3 element array of 3 component vectors + for (let i = 0; i < 3; i++) { + row[i][0] = matrix[i][0]; + row[i][1] = matrix[i][1]; + row[i][2] = matrix[i][2]; + } + let scale = [0, 0, 0]; + let skew = [0, 0, 0]; + // Compute X scale factor and normalize first row. + scale[0] = pLength(row[0]); + row[0] = normalize(row[0]); + // Compute XY shear factor and make 2nd row orthogonal to 1st. + skew[0] = dot(row[0], row[1]); + row[1] = combine(row[1], row[0], 1.0, -skew[0]); + // Now, compute Y scale and normalize 2nd row. + scale[1] = pLength(row[1]); + row[1] = normalize(row[1]); + skew[0] /= scale[1]; + // Compute XZ and YZ shears, orthogonalize 3rd row + skew[1] = dot(row[0], row[2]); + row[2] = combine(row[2], row[0], 1.0, -skew[1]); + skew[2] = dot(row[1], row[2]); + row[2] = combine(row[2], row[1], 1.0, -skew[2]); + // Next, get Z scale and normalize 3rd row. + scale[2] = pLength(row[2]); + row[2] = normalize(row[2]); + skew[1] /= scale[2]; + skew[2] /= scale[2]; + // At this point, the matrix (in rows) is orthonormal. + // Check for a coordinate system flip. If the determinant + // is -1, then negate the matrix and the scaling factors. + let pdum3 = cross(row[1], row[2]); + if (dot(row[0], pdum3) < 0) { + for (let i = 0; i < 3; i++) { + scale[i] *= -1; + row[i][0] *= -1; + row[i][1] *= -1; + row[i][2] *= -1; + } + } + let quaternion = [0, 0, 0, 0]; + // Now, get the rotations out + quaternion[0] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] - row[1][1] - row[2][2], 0)); + quaternion[1] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] + row[1][1] - row[2][2], 0)); + quaternion[2] = 0.5 * Math.sqrt(Math.max(1 - row[0][0] - row[1][1] + row[2][2], 0)); + quaternion[3] = 0.5 * Math.sqrt(Math.max(1 + row[0][0] + row[1][1] + row[2][2], 0)); + if (row[2][1] > row[1][2]) { + quaternion[0] = -quaternion[0]; + } + if (row[0][2] > row[2][0]) { + quaternion[1] = -quaternion[1]; + } + if (row[1][0] > row[0][1]) { + quaternion[2] = -quaternion[2]; + } + return { + skew, + scale, + translate, + perspective, + quaternion + }; +} + +export { decompose, identity }; From 0205371eb048eb0d8c5928f636e92558acf9f354 Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Sun, 23 Mar 2025 16:26:42 -0400 Subject: [PATCH 03/10] fix parsing color-mix() with currentcolor and var() #75 --- README.md | 2 ++ dist/index-umd-web.js | 42 ++++++++++++++++++++++++++--- dist/index.cjs | 42 ++++++++++++++++++++++++++--- dist/lib/ast/features/transform.js | 3 +-- dist/lib/ast/transform/compute.js | 2 +- dist/lib/renderer/color/colormix.js | 3 +++ dist/lib/renderer/color/lab.js | 5 ++++ dist/lib/renderer/color/lch.js | 5 ++++ dist/lib/renderer/color/oklab.js | 5 ++++ dist/lib/renderer/color/oklch.js | 5 ++++ dist/lib/renderer/color/srgb.js | 5 +++- dist/lib/renderer/render.js | 10 ++++++- src/lib/ast/features/transform.ts | 3 +-- src/lib/ast/transform/compute.ts | 2 +- src/lib/renderer/color/colormix.ts | 8 ++++-- src/lib/renderer/color/lab.ts | 8 ++++++ src/lib/renderer/color/lch.ts | 8 ++++++ src/lib/renderer/color/oklab.ts | 10 ++++++- src/lib/renderer/color/oklch.ts | 9 ++++++- src/lib/renderer/color/srgb.ts | 9 +++++-- src/lib/renderer/render.ts | 15 ++++++++++- test/specs/code/color.js | 19 +++++++++++++ 22 files changed, 197 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 468eb58b..124f7e97 100644 --- a/README.md +++ b/README.md @@ -706,6 +706,8 @@ for (const {node, parent, root} of walk(ast)) { ## Minification +- [x] minify keyframs +- [x] minify transform - [x] evaluate math functions calc(), clamp(), min(), max(), round(), mod(), rem(), sin(), cos(), tan(), asin(), acos(), atan(), atan2(), pow(), sqrt(), hypot(), log(), exp(), abs(), sign() - [x] multi-pass minification diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 3504fd2a..97eb5f15 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -556,6 +556,11 @@ } function getLCHComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore @@ -609,6 +614,11 @@ } function getOKLCHComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore @@ -668,6 +678,11 @@ } function getOKLABComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore @@ -848,6 +863,11 @@ } function getLABComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore @@ -998,7 +1018,10 @@ return rgb; } function oklch2srgb(token) { - const [l, c, h, alpha] = getOKLCHComponents(token); + const [l, c, h, alpha] = getOKLCHComponents(token) ?? {}; + if (l == null || c == null || h == null) { + return null; + } // @ts-ignore const rgb = OKLab_to_sRGB(...lch2labvalues(l, c, h)); if (alpha != 1) { @@ -2173,6 +2196,9 @@ return [h1, h2]; } function colorMix(colorSpace, hueInterpolationMethod, color1, percentage1, color2, percentage2) { + if (color1.val == 'currentcolor' || color2.val == 'currentcolor') { + return null; + } if (hueInterpolationMethod != null && isRectangularOrthogonalColorspace(colorSpace)) { return null; } @@ -3451,7 +3477,6 @@ const indentSub = indents[level + 1]; switch (data.typ) { case exports.EnumToken.DeclarationNodeType: - console.error({ options }); return `${data.nam}:${options.indent}${(options.minify ? filterValues(data.val) : data.val).reduce(reducer, '')}`; case exports.EnumToken.CommentNodeType: case exports.EnumToken.CDOCOMMNodeType: @@ -3639,6 +3664,15 @@ if (value != null) { token = value; } + else { + token.chi = children.reduce((acc, curr, index) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push(...curr); + return acc; + }, []); + } } if (token.cal == 'rel' && ['rgb', 'hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color'].includes(token.val)) { const chi = getComponents(token); @@ -17879,7 +17913,7 @@ return null; } const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; - console.error([transformList[i].val, valCount]); + // console.error([(transformList[i] as FunctionToken).val, valCount]); if (children.length == 1 && children[0].typ == exports.EnumToken.IdenTokenType && children[0].val == 'none') { values.fill(0, 0, valCount); } @@ -18129,7 +18163,7 @@ // console.error({result}); return; } - decompose(result); + // const decomposed = decompose(result); const minified = minify$1(result); // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); if (minified != null) { diff --git a/dist/index.cjs b/dist/index.cjs index 5a9f84b9..1433201c 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -555,6 +555,11 @@ function xyz2lchvalues(x, y, z, alpha) { } function getLCHComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore @@ -608,6 +613,11 @@ function srgb2oklch(r, g, blue, alpha) { } function getOKLCHComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore @@ -667,6 +677,11 @@ function srgb2oklab(r, g, blue, alpha) { } function getOKLABComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore @@ -847,6 +862,11 @@ function lch2labvalues(l, c, h, a = null) { } function getLABComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![exports.EnumToken.NumberTokenType, exports.EnumToken.PercentageTokenType, exports.EnumToken.AngleTokenType, exports.EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore @@ -997,7 +1017,10 @@ function oklab2srgb(token) { return rgb; } function oklch2srgb(token) { - const [l, c, h, alpha] = getOKLCHComponents(token); + const [l, c, h, alpha] = getOKLCHComponents(token) ?? {}; + if (l == null || c == null || h == null) { + return null; + } // @ts-ignore const rgb = OKLab_to_sRGB(...lch2labvalues(l, c, h)); if (alpha != 1) { @@ -2172,6 +2195,9 @@ function interpolateHue(interpolationMethod, h1, h2) { return [h1, h2]; } function colorMix(colorSpace, hueInterpolationMethod, color1, percentage1, color2, percentage2) { + if (color1.val == 'currentcolor' || color2.val == 'currentcolor') { + return null; + } if (hueInterpolationMethod != null && isRectangularOrthogonalColorspace(colorSpace)) { return null; } @@ -3450,7 +3476,6 @@ function renderAstNode(data, options, sourcemap, position, errors, reducer, cach const indentSub = indents[level + 1]; switch (data.typ) { case exports.EnumToken.DeclarationNodeType: - console.error({ options }); return `${data.nam}:${options.indent}${(options.minify ? filterValues(data.val) : data.val).reduce(reducer, '')}`; case exports.EnumToken.CommentNodeType: case exports.EnumToken.CDOCOMMNodeType: @@ -3638,6 +3663,15 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer, if (value != null) { token = value; } + else { + token.chi = children.reduce((acc, curr, index) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push(...curr); + return acc; + }, []); + } } if (token.cal == 'rel' && ['rgb', 'hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color'].includes(token.val)) { const chi = getComponents(token); @@ -17978,7 +18012,7 @@ function compute(transformList) { return null; } const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; - console.error([transformList[i].val, valCount]); + // console.error([(transformList[i] as FunctionToken).val, valCount]); if (children.length == 1 && children[0].typ == exports.EnumToken.IdenTokenType && children[0].val == 'none') { values.fill(0, 0, valCount); } @@ -18228,7 +18262,7 @@ class TransformCssFeature { // console.error({result}); return; } - decompose(result); + // const decomposed = decompose(result); const minified = minify$1(result); // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); if (minified != null) { diff --git a/dist/lib/ast/features/transform.js b/dist/lib/ast/features/transform.js index 67d30706..e4adf3f1 100644 --- a/dist/lib/ast/features/transform.js +++ b/dist/lib/ast/features/transform.js @@ -7,7 +7,6 @@ import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import '../../parser/utils/config.js'; import { compute } from '../transform/compute.js'; -import { decompose } from '../transform/utils.js'; import { minify } from '../transform/minify.js'; class TransformCssFeature { @@ -42,7 +41,7 @@ class TransformCssFeature { // console.error({result}); return; } - decompose(result); + // const decomposed = decompose(result); const minified = minify(result); // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); if (minified != null) { diff --git a/dist/lib/ast/transform/compute.js b/dist/lib/ast/transform/compute.js index d390f554..12055d21 100644 --- a/dist/lib/ast/transform/compute.js +++ b/dist/lib/ast/transform/compute.js @@ -40,7 +40,7 @@ function compute(transformList) { return null; } const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; - console.error([transformList[i].val, valCount]); + // console.error([(transformList[i] as FunctionToken).val, valCount]); if (children.length == 1 && children[0].typ == EnumToken.IdenTokenType && children[0].val == 'none') { values.fill(0, 0, valCount); } diff --git a/dist/lib/renderer/color/colormix.js b/dist/lib/renderer/color/colormix.js index ea483cd9..c9c66c64 100644 --- a/dist/lib/renderer/color/colormix.js +++ b/dist/lib/renderer/color/colormix.js @@ -57,6 +57,9 @@ function interpolateHue(interpolationMethod, h1, h2) { return [h1, h2]; } function colorMix(colorSpace, hueInterpolationMethod, color1, percentage1, color2, percentage2) { + if (color1.val == 'currentcolor' || color2.val == 'currentcolor') { + return null; + } if (hueInterpolationMethod != null && isRectangularOrthogonalColorspace(colorSpace)) { return null; } diff --git a/dist/lib/renderer/color/lab.js b/dist/lib/renderer/color/lab.js index be79f797..f88f5f9e 100644 --- a/dist/lib/renderer/color/lab.js +++ b/dist/lib/renderer/color/lab.js @@ -87,6 +87,11 @@ function lch2labvalues(l, c, h, a = null) { } function getLABComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType, EnumToken.AngleTokenType, EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore diff --git a/dist/lib/renderer/color/lch.js b/dist/lib/renderer/color/lch.js index f0183310..79d5b2cb 100644 --- a/dist/lib/renderer/color/lch.js +++ b/dist/lib/renderer/color/lch.js @@ -59,6 +59,11 @@ function xyz2lchvalues(x, y, z, alpha) { } function getLCHComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType, EnumToken.AngleTokenType, EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore diff --git a/dist/lib/renderer/color/oklab.js b/dist/lib/renderer/color/oklab.js index 18e3cc31..6732384d 100644 --- a/dist/lib/renderer/color/oklab.js +++ b/dist/lib/renderer/color/oklab.js @@ -52,6 +52,11 @@ function srgb2oklab(r, g, blue, alpha) { } function getOKLABComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType, EnumToken.AngleTokenType, EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore diff --git a/dist/lib/renderer/color/oklch.js b/dist/lib/renderer/color/oklch.js index 1aa652a5..58a36e58 100644 --- a/dist/lib/renderer/color/oklch.js +++ b/dist/lib/renderer/color/oklch.js @@ -44,6 +44,11 @@ function srgb2oklch(r, g, blue, alpha) { } function getOKLCHComponents(token) { const components = getComponents(token); + for (let i = 0; i < components.length; i++) { + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType, EnumToken.AngleTokenType, EnumToken.IdenTokenType].includes(components[i].typ)) { + return []; + } + } // @ts-ignore let t = components[0]; // @ts-ignore diff --git a/dist/lib/renderer/color/srgb.js b/dist/lib/renderer/color/srgb.js index 9ce8f6c5..b0c96e75 100644 --- a/dist/lib/renderer/color/srgb.js +++ b/dist/lib/renderer/color/srgb.js @@ -115,7 +115,10 @@ function oklab2srgb(token) { return rgb; } function oklch2srgb(token) { - const [l, c, h, alpha] = getOKLCHComponents(token); + const [l, c, h, alpha] = getOKLCHComponents(token) ?? {}; + if (l == null || c == null || h == null) { + return null; + } // @ts-ignore const rgb = OKLab_to_sRGB(...lch2labvalues(l, c, h)); if (alpha != 1) { diff --git a/dist/lib/renderer/render.js b/dist/lib/renderer/render.js index 5912b1a1..1ef59150 100644 --- a/dist/lib/renderer/render.js +++ b/dist/lib/renderer/render.js @@ -152,7 +152,6 @@ function renderAstNode(data, options, sourcemap, position, errors, reducer, cach const indentSub = indents[level + 1]; switch (data.typ) { case EnumToken.DeclarationNodeType: - console.error({ options }); return `${data.nam}:${options.indent}${(options.minify ? filterValues(data.val) : data.val).reduce(reducer, '')}`; case EnumToken.CommentNodeType: case EnumToken.CDOCOMMNodeType: @@ -340,6 +339,15 @@ function renderToken(token, options = {}, cache = Object.create(null), reducer, if (value != null) { token = value; } + else { + token.chi = children.reduce((acc, curr, index) => { + if (acc.length > 0) { + acc.push({ typ: EnumToken.CommaTokenType }); + } + acc.push(...curr); + return acc; + }, []); + } } if (token.cal == 'rel' && ['rgb', 'hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color'].includes(token.val)) { const chi = getComponents(token); diff --git a/src/lib/ast/features/transform.ts b/src/lib/ast/features/transform.ts index b5b42b55..4f87337e 100644 --- a/src/lib/ast/features/transform.ts +++ b/src/lib/ast/features/transform.ts @@ -9,7 +9,6 @@ import type { import {EnumToken} from "../types"; import {consumeWhitespace} from "../../validation/utils"; import {compute} from "../transform/compute.ts"; -import {decompose} from "../transform/utils.ts"; import {minify} from "../transform/minify.ts"; export class TransformCssFeature { @@ -63,7 +62,7 @@ export class TransformCssFeature { return; } - const decomposed = decompose(result); + // const decomposed = decompose(result); const minified = minify(result); // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); diff --git a/src/lib/ast/transform/compute.ts b/src/lib/ast/transform/compute.ts index c97621f9..ae6205e2 100644 --- a/src/lib/ast/transform/compute.ts +++ b/src/lib/ast/transform/compute.ts @@ -50,7 +50,7 @@ export function compute(transformList: Token[]): Matrix | null { const valCount: number = (transformList[i] as FunctionToken).val == 'translate3d' || (transformList[i] as FunctionToken).val == 'translate' ? 3 : 1; - console.error([(transformList[i] as FunctionToken).val, valCount]); + // console.error([(transformList[i] as FunctionToken).val, valCount]); if (children.length == 1 && children[0].typ == EnumToken.IdenTokenType && children[0].val == 'none') { diff --git a/src/lib/renderer/color/colormix.ts b/src/lib/renderer/color/colormix.ts index bed6c1ba..c6649d1f 100644 --- a/src/lib/renderer/color/colormix.ts +++ b/src/lib/renderer/color/colormix.ts @@ -8,8 +8,7 @@ import {srgb2hsl} from "./hsl"; import {srgb2hwb} from "./hwb"; import {srgb2lab} from "./lab"; import {srgb2p3values} from "./p3"; -import {getComponents, powerlessColorComponent} from "./utils"; -import {eq} from "../../parser/utils/eq"; +import {getComponents} from "./utils"; import {srgb2oklch} from "./oklch"; import {srgb2oklab} from "./oklab"; import {srgb2a98values} from "./a98rgb"; @@ -71,6 +70,11 @@ function interpolateHue(interpolationMethod: IdentToken, h1: number, h2: number) export function colorMix(colorSpace: IdentToken, hueInterpolationMethod: IdentToken | null, color1: ColorToken, percentage1: PercentageToken | null, color2: ColorToken, percentage2: PercentageToken | null): ColorToken | null { + if (color1.val == 'currentcolor' || color2.val == 'currentcolor') { + + return null; + } + if (hueInterpolationMethod != null && isRectangularOrthogonalColorspace(colorSpace)) { return null; diff --git a/src/lib/renderer/color/lab.ts b/src/lib/renderer/color/lab.ts index fed69afc..5db33cc3 100644 --- a/src/lib/renderer/color/lab.ts +++ b/src/lib/renderer/color/lab.ts @@ -112,6 +112,14 @@ export function getLABComponents(token: ColorToken) { const components: Token[] = getComponents(token); + for (let i = 0; i < components.length; i++) { + + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType, EnumToken.AngleTokenType, EnumToken.IdenTokenType].includes(components[i].typ)) { + + return []; + } + } + // @ts-ignore let t: NumberToken | PercentageToken = components[0]; diff --git a/src/lib/renderer/color/lch.ts b/src/lib/renderer/color/lch.ts index 6ec1d2d0..275c1d88 100644 --- a/src/lib/renderer/color/lch.ts +++ b/src/lib/renderer/color/lch.ts @@ -88,6 +88,14 @@ export function getLCHComponents(token: ColorToken): number[] { const components: Token[] = getComponents(token); + for (let i = 0; i < components.length; i++) { + + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType, EnumToken.AngleTokenType, EnumToken.IdenTokenType].includes(components[i].typ)) { + + return []; + } + } + // @ts-ignore let t: NumberToken | PercentageToken = components[0]; diff --git a/src/lib/renderer/color/oklab.ts b/src/lib/renderer/color/oklab.ts index 41c5422b..5119a650 100644 --- a/src/lib/renderer/color/oklab.ts +++ b/src/lib/renderer/color/oklab.ts @@ -1,5 +1,5 @@ import {getComponents, multiplyMatrices} from "./utils"; -import {srgb2lsrgbvalues, hex2srgb, hsl2srgb, hwb2srgb, lab2srgb, lch2srgb, rgb2srgb, lsrgb2srgbvalues} from "./srgb"; +import {hex2srgb, hsl2srgb, hwb2srgb, lab2srgb, lch2srgb, lsrgb2srgbvalues, rgb2srgb, srgb2lsrgbvalues} from "./srgb"; import type {ColorToken, NumberToken, PercentageToken, Token} from "../../../@types/index.d.ts"; import {getNumber} from "./color"; import {EnumToken} from "../../ast"; @@ -76,6 +76,14 @@ export function getOKLABComponents(token: ColorToken): number[] { const components: Token[] = getComponents(token); + for (let i = 0; i < components.length; i++) { + + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType, EnumToken.AngleTokenType, EnumToken.IdenTokenType].includes(components[i].typ)) { + + return []; + } + } + // @ts-ignore let t: NumberToken | PercentageToken = components[0]; diff --git a/src/lib/renderer/color/oklch.ts b/src/lib/renderer/color/oklch.ts index 51fc4dd2..77f289f1 100644 --- a/src/lib/renderer/color/oklch.ts +++ b/src/lib/renderer/color/oklch.ts @@ -13,7 +13,6 @@ import { rgb2oklab, srgb2oklab } from "./oklab"; -import {eq} from "../../parser/utils/eq"; export function hex2oklch(token: ColorToken): number[] { @@ -67,6 +66,14 @@ export function getOKLCHComponents(token: ColorToken): number[] { const components: Token[] = getComponents(token); + for (let i = 0; i < components.length; i++) { + + if (![EnumToken.NumberTokenType, EnumToken.PercentageTokenType, EnumToken.AngleTokenType, EnumToken.IdenTokenType].includes(components[i].typ)) { + + return []; + } + } + // @ts-ignore let t: NumberToken | PercentageToken = components[0]; diff --git a/src/lib/renderer/color/srgb.ts b/src/lib/renderer/color/srgb.ts index 69783c7d..0b00f81f 100644 --- a/src/lib/renderer/color/srgb.ts +++ b/src/lib/renderer/color/srgb.ts @@ -171,9 +171,14 @@ export function oklab2srgb(token: ColorToken): number[] { return rgb; } -export function oklch2srgb(token: ColorToken): number[] { +export function oklch2srgb(token: ColorToken): number[] | null { - const [l, c, h, alpha] = getOKLCHComponents(token); + const [l, c, h, alpha] = getOKLCHComponents(token) ?? {}; + + if (l == null || c == null || h == null) { + + return null; + } // @ts-ignore const rgb: number[] = OKLab_to_sRGB(...lch2labvalues(l, c, h)); diff --git a/src/lib/renderer/render.ts b/src/lib/renderer/render.ts index a13428a8..03efc4bb 100644 --- a/src/lib/renderer/render.ts +++ b/src/lib/renderer/render.ts @@ -257,7 +257,6 @@ function renderAstNode(data: AstNode, options: RenderOptions, sourcemap: SourceM case EnumToken.DeclarationNodeType: - console.error({options}); return `${(data).nam}:${options.indent}${(options.minify ? filterValues((data).val) : (data).val).reduce(reducer, '')}`; case EnumToken.CommentNodeType: @@ -545,6 +544,20 @@ export function renderToken(token: Token, options: RenderOptions = {}, cache: { token = value; } + + else { + + token.chi = children.reduce((acc, curr, index) => { + + if (acc.length > 0) { + + acc.push({ typ: EnumToken.CommaTokenType }); + } + + acc.push(...curr); + return acc; + }, [] as Token[]); + } } if (token.cal == 'rel' && ['rgb', 'hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color'].includes(token.val)) { diff --git a/test/specs/code/color.js b/test/specs/code/color.js index 7e93a5ac..1ecf0ffa 100644 --- a/test/specs/code/color.js +++ b/test/specs/code/color.js @@ -1275,6 +1275,25 @@ color: lch(from slateblue calc(l * sin(pi / 4)) c h); ; `).then(result => expect(render(result.ast, {minify: false}).code).equals(`a { color: #453ba9 +}`)); + }); + + it('current color #130', function () { + return transform(` + + .now { + color: color-mix(in oklch, currentcolor 35% , #0000 ); + background-color: color-mix( + in oklab, + oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, + black + ); + transform: rotateY(180deg); +} +`, {beautify: true}).then(result => expect(result.code).equals(`.now { + color: color-mix(in oklch,currentcolor 35%,#0000); + background-color: color-mix(in oklab,oklch(var(--btn-color,var(--b2))/var(--tw-bg-opacity,1)) 90%,#000); + transform: rotateY(180deg) }`)); }); } \ No newline at end of file From e8cc0c20fb162ffc897b324c4a54e9aa5bbb4329 Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Fri, 28 Mar 2025 00:26:39 -0400 Subject: [PATCH 04/10] adding transform rotation #75 --- LICENSE.md | 2 +- dist/index-umd-web.js | 446 +++++-- dist/index.cjs | 446 +++++-- dist/lib/ast/features/transform.js | 3 +- dist/lib/ast/features/translate.js | 59 - dist/lib/ast/transform/compute.js | 131 +- dist/lib/ast/transform/matrix.js | 46 - dist/lib/ast/transform/minify.js | 261 +++- dist/lib/ast/transform/utils.js | 25 + dist/lib/renderer/color/prophotoRgb.js | 56 - dist/lib/validation/at-rules/media.js | 2 +- dist/lib/validation/declaration.js | 94 -- dist/lib/validation/syntax.js | 1509 ------------------------ dist/lib/validation/syntaxes/image.js | 29 - jsr.json | 2 +- package.json | 2 +- src/lib/ast/features/transform.ts | 3 +- src/lib/ast/transform/compute.ts | 164 +-- src/lib/ast/transform/matrix.ts | 28 +- src/lib/ast/transform/minify.ts | 275 ++++- src/lib/ast/transform/rotate.ts | 47 +- src/lib/ast/transform/utils.ts | 30 + src/lib/validation/at-rules/media.ts | 2 +- test/specs/code/malformed.js | 2 +- test/specs/code/transform.js | 62 +- 25 files changed, 1365 insertions(+), 2361 deletions(-) delete mode 100644 dist/lib/ast/features/translate.js delete mode 100644 dist/lib/ast/transform/matrix.js delete mode 100644 dist/lib/renderer/color/prophotoRgb.js delete mode 100644 dist/lib/validation/declaration.js delete mode 100644 dist/lib/validation/syntax.js delete mode 100644 dist/lib/validation/syntaxes/image.js diff --git a/LICENSE.md b/LICENSE.md index 4dec3113..36f2198f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Thierry Bela +Copyright (c) 2023-present Thierry Bela Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 97eb5f15..e5585bbb 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -12721,7 +12721,7 @@ if (chi[0].typ == exports.EnumToken.MediaQueryConditionTokenType) { return chi[0].l.typ == exports.EnumToken.IdenTokenType; } - console.error(chi[0].parent); + // console.error(chi[0].parent); return false; } function validateMediaFeature(token) { @@ -17826,9 +17826,34 @@ if (row[1][0] > row[0][1]) { quaternion[2] = -quaternion[2]; } + // const rad2deg = 180 / Math.PI; + // const theta: number = Math.atan2(matrix[1][0], matrix[0][0]) * rad2deg; + // const zAxis = + // + // console.error({theta}); + // + // const rotation: [number, number, number] = [-Math.asin(matrix[0][2]) * rad2deg, 0, 0]; + // apply rotation + let x = quaternion[0]; + let y = quaternion[1]; + let z = quaternion[2]; + let w = quaternion[3]; + const rotationMatrix = identity(); + // Construct a composite rotation matrix from the quaternion values + // rotationMatrix is an identity 4x4 matrix initially + rotationMatrix[0][0] = 1 - 2 * (y * y + z * z); + rotationMatrix[0][1] = 2 * (x * y - z * w); + rotationMatrix[0][2] = 2 * (x * z + y * w); + rotationMatrix[1][0] = 2 * (x * y + z * w); + rotationMatrix[1][1] = 1 - 2 * (x * x + z * z); + rotationMatrix[1][2] = 2 * (y * z - x * w); + rotationMatrix[2][0] = 2 * (x * z - y * w); + rotationMatrix[2][1] = 2 * (y * z + x * w); + rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); return { skew, scale, + // rotate: [+rx.toPrecision(12), +ry.toPrecision(12), +rz.toPrecision(12)], translate, perspective, quaternion @@ -17884,6 +17909,34 @@ return matrix; } + /** + * angle in radian + * @param angle + * @param x + * @param y + * @param z + * @param matrix + */ + function rotate3D(angle, x, y, z, matrix) { + const sc = Math.sin(angle / 2) * Math.cos(angle / 2); + const sq = Math.sin(angle / 2) * Math.sin(angle / 2); + const norm = Math.sqrt(x * x + y * y + z * z); + const unit = norm === 0 ? 0 : 1 / norm; + x *= unit; + y *= unit; + z *= unit; + matrix[0][0] = 1 - 2 * (y * y + z * z) * sq; + matrix[0][1] = 2 * (x * y * sq - z * sc); + matrix[0][2] = 2 * (x * z * sq + y * sc); + matrix[1][0] = 2 * (x * y * sq + z * sc); + matrix[1][1] = 1 - 2 * (x * x + z * z) * sq; + matrix[1][2] = 2 * (y * z * sq - x * sc); + matrix[2][0] = 2 * (x * z * sq - y * sc); + matrix[2][1] = 2 * (y * z * sq + x * sc); + matrix[2][2] = 1 - 2 * (x * x + y * y) * sq; + return matrix; + } + function compute(transformList) { transformList = transformList.slice(); stripCommaToken(transformList); @@ -17923,7 +17976,7 @@ continue; } val = length2Px(children[j]); - if (val == null) { + if (typeof val != 'number' || Number.isNaN(val)) { return null; } values.push(val); @@ -17947,76 +18000,63 @@ } } break; - // case 'rotate': - // case 'rotateX': - // case 'rotateY': - // // case 'rotateZ': - // // case 'rotate3d': - // - // { - // let x: number = 0; - // let y: number = 0; - // let z: number = 0; - // - // let angle: number; - // let values: number[] = []; - // - // if ((transformList[i] as FunctionToken).val == 'rotateX' || (transformList[i] as FunctionToken).val == 'rotateY' || (transformList[i] as FunctionToken).val == 'rotateZ') { - // - // for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { - // - // if (child.typ == EnumToken.WhitespaceTokenType) { - // - // continue; - // } - // - // // if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { - // // - // // return null; - // // } - // - // if (child.typ == EnumToken.NumberTokenType && +child.val > 0) { - // - // return null; - // } - // - // angle = getAngle(child as AngleToken | NumberToken); - // - // if (angle == null) { - // - // return null; - // } - // - // values.push(angle * 2 * Math.PI); - // - // if ((transformList[i] as FunctionToken).val == 'rotateX') { - // - // x = 1; - // } else if ((transformList[i] as FunctionToken).val == 'rotateY') { - // - // y = 1; - // } else if ((transformList[i] as FunctionToken).val == 'rotateZ') { - // - // z = 1; - // } - // } - // - // if (values.length != 1) { - // - // return null; - // } - // - // console.error({values}); - // - // return null; - // } - // - // else if ((transformList[i] as FunctionToken).val == 'rotate') { - // - // } - // } - // - // break; + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + // + { + let x = 0; + let y = 0; + let z = 0; + let angle; + let values = []; + let valuesCount = transformList[i].val == 'rotate3d' ? 4 : 1; + if (['rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d'].includes(transformList[i].val)) { + for (const child of stripCommaToken(transformList[i].chi.slice())) { + if (child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + values.push(child); + if (transformList[i].val == 'rotateX') { + x = 1; + } + else if (transformList[i].val == 'rotateY') { + y = 1; + } + else if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { + z = 1; + } + } + // console.error({values, valuesCount}); + if (values.length != valuesCount) { + return null; + } + if (transformList[i].val == 'rotate3d') { + x = getNumber(values[0]); + y = getNumber(values[1]); + z = getNumber(values[2]); + } + angle = getAngle(values.at(-1)); + if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { + return null; + } + // console.error([x, y, z, angle * 2 * Math.PI]); + // if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { + rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + // console.error({matrix}); + // continue; + // } + // console.error({values}); + // return null; + } + // else + // if ((transformList[i] as FunctionToken).val == 'rotate') { + // + // } + } + break; default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); @@ -18031,6 +18071,7 @@ return null; } const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + const result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { transforms.delete('translate'); @@ -18044,17 +18085,18 @@ if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { transforms.delete('perspective'); } - if (transforms.size == 0) { - // identity - return [{ - typ: exports.EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '1' } - ] - } - ]; - } + // if (transforms.size == 0) { + // + // // identity + // return [{ + // typ: EnumToken.FunctionTokenType, + // val: 'scale', + // chi: [ + // {typ: EnumToken.NumberTokenType, val: '1'} + // ] + // } + // ]; + // } if (transforms.size == 1) { if (transforms.has('translate')) { let coordinates = new Set(['x', 'y', 'z']); @@ -18064,54 +18106,57 @@ } } if (coordinates.size == 3) { - return [{ + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); + } + else if (coordinates.size == 1) { + if (coordinates.has('x')) { + result.push({ typ: exports.EnumToken.FunctionTokenType, val: 'translate', - chi: [ - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } - ] - }]; - } - if (coordinates.size == 1) { - if (coordinates.has('x')) { - return [{ - typ: exports.EnumToken.FunctionTokenType, - val: 'translate', - chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] - }]; + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] + }); } - let axis = coordinates.has('y') ? 'y' : 'z'; - let index = axis == 'y' ? 1 : 2; - return [{ + else { + let axis = coordinates.has('y') ? 'y' : 'z'; + let index = axis == 'y' ? 1 : 2; + result.push({ typ: exports.EnumToken.FunctionTokenType, val: 'translate' + axis.toUpperCase(), chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] - }]; + }); + } } - if (coordinates.has('z')) { - return [{ - typ: exports.EnumToken.FunctionTokenType, - val: 'translate', - chi: [ - decomposed.translate[0] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - decomposed.translate[1] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } - ] - }]; + else if (coordinates.has('z')) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); } - return [{ + else { + result.push({ typ: exports.EnumToken.FunctionTokenType, val: 'translate', chi: [ @@ -18125,10 +18170,159 @@ 'val': '0' } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } ] - }]; + }); + } } } - return null; + { + const { x, y, z, angle } = getRotation3D(matrix); + // console.error({x, y, z, angle}); + if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { + if (y == 0 && z == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotateX', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && z == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotateY', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && y == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotate', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotate3d', + chi: [ + { + typ: exports.EnumToken.NumberTokenType, + val: '' + x + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.NumberTokenType, + val: '' + y + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.NumberTokenType, + val: '' + z + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + } + } + // scale(x, x) -> scale(x) + // scale(1, sy) -> scaleY(sy) + // scale3d(1, 1, sz) -> scaleZ(sz) + // scaleX() scale(Y) scaleZ() -> scale3d() + // identity + return result.length == 0 ? [ + { + typ: exports.EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '1' } + ] + } + ] : result; + } + // Fonction pour calculer rotate3d à partir de matrix3d + function getRotation3D(matrix) { + // Extraire la sous-matrice 3x3 de rotation + const r11 = matrix[0][0], r12 = matrix[0][1], r13 = matrix[0][2]; + const r21 = matrix[1][0], r22 = matrix[1][1], r23 = matrix[1][2]; + const r31 = matrix[2][0], r32 = matrix[2][1], r33 = matrix[2][2]; + // Calculer la trace (somme des éléments diagonaux) + const trace = r11 + r22 + r33; + // Calculer l’angle de rotation (en radians) + const cosTheta = (trace - 1) / 2; + // Calculer sin(θ) avec le signe correct + const sinThetaRaw = Math.sqrt(1 - cosTheta * cosTheta); + const xRaw = r32 - r23; // -0.467517 + const yRaw = r13 - r31; // 0.776535 + const zRaw = r21 - r12; // 0.776535 + // Déterminer le signe de sin(θ) basé sur la direction + const sinTheta = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; + // Calculer l’angle avec atan2 + const angle = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(12); + let x, y, z; + if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° + const x1 = r32 - r23; + const y1 = r13 - r31; + const z1 = r21 - r12; + switch (Math.max(x1, y1, z1)) { + case x1: + x = 1; + y = 0; + z = 0; + break; + case y1: + x = 0; + y = 1; + z = 0; + break; + default: + x = 0; + y = 0; + z = 1; + break; + } + } + else { + x = (r32 - r23) / (2 * sinTheta); + y = (r13 - r31) / (2 * sinTheta); + z = (r21 - r12) / (2 * sinTheta); + } + // Normaliser le vecteur (optionnel, mais utile pour vérification) + const length = Math.sqrt(x * x + y * y + z * z); + if (length > 0) { + x /= length; + y /= length; + z /= length; + } + const x1 = gcd(x, gcd(y, z)); + if (x1 != 0) { + x /= x1; + y /= x1; + z /= x1; + } + return { x, y, z, angle }; } class TransformCssFeature { @@ -18163,7 +18357,7 @@ // console.error({result}); return; } - // const decomposed = decompose(result); + decompose(result); const minified = minify$1(result); // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); if (minified != null) { diff --git a/dist/index.cjs b/dist/index.cjs index 1433201c..d24333f0 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -12820,7 +12820,7 @@ function validateMediaCondition(token, atRule) { if (chi[0].typ == exports.EnumToken.MediaQueryConditionTokenType) { return chi[0].l.typ == exports.EnumToken.IdenTokenType; } - console.error(chi[0].parent); + // console.error(chi[0].parent); return false; } function validateMediaFeature(token) { @@ -17925,9 +17925,34 @@ function decompose(matrix) { if (row[1][0] > row[0][1]) { quaternion[2] = -quaternion[2]; } + // const rad2deg = 180 / Math.PI; + // const theta: number = Math.atan2(matrix[1][0], matrix[0][0]) * rad2deg; + // const zAxis = + // + // console.error({theta}); + // + // const rotation: [number, number, number] = [-Math.asin(matrix[0][2]) * rad2deg, 0, 0]; + // apply rotation + let x = quaternion[0]; + let y = quaternion[1]; + let z = quaternion[2]; + let w = quaternion[3]; + const rotationMatrix = identity(); + // Construct a composite rotation matrix from the quaternion values + // rotationMatrix is an identity 4x4 matrix initially + rotationMatrix[0][0] = 1 - 2 * (y * y + z * z); + rotationMatrix[0][1] = 2 * (x * y - z * w); + rotationMatrix[0][2] = 2 * (x * z + y * w); + rotationMatrix[1][0] = 2 * (x * y + z * w); + rotationMatrix[1][1] = 1 - 2 * (x * x + z * z); + rotationMatrix[1][2] = 2 * (y * z - x * w); + rotationMatrix[2][0] = 2 * (x * z - y * w); + rotationMatrix[2][1] = 2 * (y * z + x * w); + rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); return { skew, scale, + // rotate: [+rx.toPrecision(12), +ry.toPrecision(12), +rz.toPrecision(12)], translate, perspective, quaternion @@ -17983,6 +18008,34 @@ function translate(translate, matrix) { return matrix; } +/** + * angle in radian + * @param angle + * @param x + * @param y + * @param z + * @param matrix + */ +function rotate3D(angle, x, y, z, matrix) { + const sc = Math.sin(angle / 2) * Math.cos(angle / 2); + const sq = Math.sin(angle / 2) * Math.sin(angle / 2); + const norm = Math.sqrt(x * x + y * y + z * z); + const unit = norm === 0 ? 0 : 1 / norm; + x *= unit; + y *= unit; + z *= unit; + matrix[0][0] = 1 - 2 * (y * y + z * z) * sq; + matrix[0][1] = 2 * (x * y * sq - z * sc); + matrix[0][2] = 2 * (x * z * sq + y * sc); + matrix[1][0] = 2 * (x * y * sq + z * sc); + matrix[1][1] = 1 - 2 * (x * x + z * z) * sq; + matrix[1][2] = 2 * (y * z * sq - x * sc); + matrix[2][0] = 2 * (x * z * sq - y * sc); + matrix[2][1] = 2 * (y * z * sq + x * sc); + matrix[2][2] = 1 - 2 * (x * x + y * y) * sq; + return matrix; +} + function compute(transformList) { transformList = transformList.slice(); stripCommaToken(transformList); @@ -18022,7 +18075,7 @@ function compute(transformList) { continue; } val = length2Px(children[j]); - if (val == null) { + if (typeof val != 'number' || Number.isNaN(val)) { return null; } values.push(val); @@ -18046,76 +18099,63 @@ function compute(transformList) { } } break; - // case 'rotate': - // case 'rotateX': - // case 'rotateY': - // // case 'rotateZ': - // // case 'rotate3d': - // - // { - // let x: number = 0; - // let y: number = 0; - // let z: number = 0; - // - // let angle: number; - // let values: number[] = []; - // - // if ((transformList[i] as FunctionToken).val == 'rotateX' || (transformList[i] as FunctionToken).val == 'rotateY' || (transformList[i] as FunctionToken).val == 'rotateZ') { - // - // for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { - // - // if (child.typ == EnumToken.WhitespaceTokenType) { - // - // continue; - // } - // - // // if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { - // // - // // return null; - // // } - // - // if (child.typ == EnumToken.NumberTokenType && +child.val > 0) { - // - // return null; - // } - // - // angle = getAngle(child as AngleToken | NumberToken); - // - // if (angle == null) { - // - // return null; - // } - // - // values.push(angle * 2 * Math.PI); - // - // if ((transformList[i] as FunctionToken).val == 'rotateX') { - // - // x = 1; - // } else if ((transformList[i] as FunctionToken).val == 'rotateY') { - // - // y = 1; - // } else if ((transformList[i] as FunctionToken).val == 'rotateZ') { - // - // z = 1; - // } - // } - // - // if (values.length != 1) { - // - // return null; - // } - // - // console.error({values}); - // - // return null; - // } - // - // else if ((transformList[i] as FunctionToken).val == 'rotate') { - // - // } - // } - // - // break; + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + // + { + let x = 0; + let y = 0; + let z = 0; + let angle; + let values = []; + let valuesCount = transformList[i].val == 'rotate3d' ? 4 : 1; + if (['rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d'].includes(transformList[i].val)) { + for (const child of stripCommaToken(transformList[i].chi.slice())) { + if (child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + values.push(child); + if (transformList[i].val == 'rotateX') { + x = 1; + } + else if (transformList[i].val == 'rotateY') { + y = 1; + } + else if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { + z = 1; + } + } + // console.error({values, valuesCount}); + if (values.length != valuesCount) { + return null; + } + if (transformList[i].val == 'rotate3d') { + x = getNumber(values[0]); + y = getNumber(values[1]); + z = getNumber(values[2]); + } + angle = getAngle(values.at(-1)); + if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { + return null; + } + // console.error([x, y, z, angle * 2 * Math.PI]); + // if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { + rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + // console.error({matrix}); + // continue; + // } + // console.error({values}); + // return null; + } + // else + // if ((transformList[i] as FunctionToken).val == 'rotate') { + // + // } + } + break; default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); @@ -18130,6 +18170,7 @@ function minify$1(matrix) { return null; } const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + const result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { transforms.delete('translate'); @@ -18143,17 +18184,18 @@ function minify$1(matrix) { if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { transforms.delete('perspective'); } - if (transforms.size == 0) { - // identity - return [{ - typ: exports.EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '1' } - ] - } - ]; - } + // if (transforms.size == 0) { + // + // // identity + // return [{ + // typ: EnumToken.FunctionTokenType, + // val: 'scale', + // chi: [ + // {typ: EnumToken.NumberTokenType, val: '1'} + // ] + // } + // ]; + // } if (transforms.size == 1) { if (transforms.has('translate')) { let coordinates = new Set(['x', 'y', 'z']); @@ -18163,54 +18205,57 @@ function minify$1(matrix) { } } if (coordinates.size == 3) { - return [{ + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); + } + else if (coordinates.size == 1) { + if (coordinates.has('x')) { + result.push({ typ: exports.EnumToken.FunctionTokenType, val: 'translate', - chi: [ - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } - ] - }]; - } - if (coordinates.size == 1) { - if (coordinates.has('x')) { - return [{ - typ: exports.EnumToken.FunctionTokenType, - val: 'translate', - chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] - }]; + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] + }); } - let axis = coordinates.has('y') ? 'y' : 'z'; - let index = axis == 'y' ? 1 : 2; - return [{ + else { + let axis = coordinates.has('y') ? 'y' : 'z'; + let index = axis == 'y' ? 1 : 2; + result.push({ typ: exports.EnumToken.FunctionTokenType, val: 'translate' + axis.toUpperCase(), chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] - }]; + }); + } } - if (coordinates.has('z')) { - return [{ - typ: exports.EnumToken.FunctionTokenType, - val: 'translate', - chi: [ - decomposed.translate[0] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - decomposed.translate[1] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } - ] - }]; + else if (coordinates.has('z')) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); } - return [{ + else { + result.push({ typ: exports.EnumToken.FunctionTokenType, val: 'translate', chi: [ @@ -18224,10 +18269,159 @@ function minify$1(matrix) { 'val': '0' } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } ] - }]; + }); + } } } - return null; + { + const { x, y, z, angle } = getRotation3D(matrix); + // console.error({x, y, z, angle}); + if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { + if (y == 0 && z == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotateX', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && z == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotateY', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && y == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotate', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotate3d', + chi: [ + { + typ: exports.EnumToken.NumberTokenType, + val: '' + x + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.NumberTokenType, + val: '' + y + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.NumberTokenType, + val: '' + z + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + } + } + // scale(x, x) -> scale(x) + // scale(1, sy) -> scaleY(sy) + // scale3d(1, 1, sz) -> scaleZ(sz) + // scaleX() scale(Y) scaleZ() -> scale3d() + // identity + return result.length == 0 ? [ + { + typ: exports.EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '1' } + ] + } + ] : result; +} +// Fonction pour calculer rotate3d à partir de matrix3d +function getRotation3D(matrix) { + // Extraire la sous-matrice 3x3 de rotation + const r11 = matrix[0][0], r12 = matrix[0][1], r13 = matrix[0][2]; + const r21 = matrix[1][0], r22 = matrix[1][1], r23 = matrix[1][2]; + const r31 = matrix[2][0], r32 = matrix[2][1], r33 = matrix[2][2]; + // Calculer la trace (somme des éléments diagonaux) + const trace = r11 + r22 + r33; + // Calculer l’angle de rotation (en radians) + const cosTheta = (trace - 1) / 2; + // Calculer sin(θ) avec le signe correct + const sinThetaRaw = Math.sqrt(1 - cosTheta * cosTheta); + const xRaw = r32 - r23; // -0.467517 + const yRaw = r13 - r31; // 0.776535 + const zRaw = r21 - r12; // 0.776535 + // Déterminer le signe de sin(θ) basé sur la direction + const sinTheta = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; + // Calculer l’angle avec atan2 + const angle = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(12); + let x, y, z; + if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° + const x1 = r32 - r23; + const y1 = r13 - r31; + const z1 = r21 - r12; + switch (Math.max(x1, y1, z1)) { + case x1: + x = 1; + y = 0; + z = 0; + break; + case y1: + x = 0; + y = 1; + z = 0; + break; + default: + x = 0; + y = 0; + z = 1; + break; + } + } + else { + x = (r32 - r23) / (2 * sinTheta); + y = (r13 - r31) / (2 * sinTheta); + z = (r21 - r12) / (2 * sinTheta); + } + // Normaliser le vecteur (optionnel, mais utile pour vérification) + const length = Math.sqrt(x * x + y * y + z * z); + if (length > 0) { + x /= length; + y /= length; + z /= length; + } + const x1 = gcd(x, gcd(y, z)); + if (x1 != 0) { + x /= x1; + y /= x1; + z /= x1; + } + return { x, y, z, angle }; } class TransformCssFeature { @@ -18262,7 +18456,7 @@ class TransformCssFeature { // console.error({result}); return; } - // const decomposed = decompose(result); + decompose(result); const minified = minify$1(result); // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); if (minified != null) { diff --git a/dist/lib/ast/features/transform.js b/dist/lib/ast/features/transform.js index e4adf3f1..6d2ed867 100644 --- a/dist/lib/ast/features/transform.js +++ b/dist/lib/ast/features/transform.js @@ -8,6 +8,7 @@ import '../../renderer/sourcemap/lib/encode.js'; import '../../parser/utils/config.js'; import { compute } from '../transform/compute.js'; import { minify } from '../transform/minify.js'; +import { decompose } from '../transform/utils.js'; class TransformCssFeature { static get ordering() { @@ -41,7 +42,7 @@ class TransformCssFeature { // console.error({result}); return; } - // const decomposed = decompose(result); + decompose(result); const minified = minify(result); // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); if (minified != null) { diff --git a/dist/lib/ast/features/translate.js b/dist/lib/ast/features/translate.js deleted file mode 100644 index 18ad1ed6..00000000 --- a/dist/lib/ast/features/translate.js +++ /dev/null @@ -1,59 +0,0 @@ -import { EnumToken } from '../types.js'; -import { consumeWhitespace } from '../../validation/utils/whitespace.js'; -import '../minify.js'; -import '../walk.js'; -import '../../parser/parse.js'; -import '../../renderer/color/utils/constants.js'; -import '../../renderer/sourcemap/lib/encode.js'; -import '../../parser/utils/config.js'; - -class TranslateCssFeature { - static get ordering() { - return 4; - } - static register(options) { - // @ts-ignore - if (options.minify || options.computeCalcExpression || options.computeShorthand) { - // for (const feature of options.features) { - // - // if (feature instanceof ComputeCalcExpressionFeature) { - // - // return - // } - // } - // @ts-ignore - options.features.push(new TranslateCssFeature()); - } - } - run(ast) { - if (!('chi' in ast)) { - return; - } - let i = 0; - // @ts-ignore - for (; i < ast.chi.length; i++) { - const node = ast.chi[i]; - if (node.typ != EnumToken.DeclarationNodeType || - (!node.nam.startsWith('--') && !node.nam.match(/^(-[a-z]+-)?transform$/))) { - continue; - } - const translationMap = { - x: null, - y: null, - z: null - }; - const children = node.val.slice(); - consumeWhitespace(children); - for (const value of children) { - if (value.typ == EnumToken.FunctionTokenType && value.val == 'translate3d') { - translationMap.x = value.chi[0]; - translationMap.y = value.chi[1]; - translationMap.z = value.chi[2]; - } - } - console.error({ node, translationMap }); - } - } -} - -export { TranslateCssFeature }; diff --git a/dist/lib/ast/transform/compute.js b/dist/lib/ast/transform/compute.js index 12055d21..18c1eb5e 100644 --- a/dist/lib/ast/transform/compute.js +++ b/dist/lib/ast/transform/compute.js @@ -6,10 +6,12 @@ import '../minify.js'; import '../walk.js'; import '../../parser/parse.js'; import '../../parser/utils/config.js'; +import { getNumber, getAngle } from '../../renderer/color/color.js'; import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import { stripCommaToken } from '../../validation/utils/list.js'; import { translateX, translateY, translateZ, translate } from './translate.js'; +import { rotate3D } from './rotate.js'; function compute(transformList) { transformList = transformList.slice(); @@ -50,7 +52,7 @@ function compute(transformList) { continue; } val = length2Px(children[j]); - if (val == null) { + if (typeof val != 'number' || Number.isNaN(val)) { return null; } values.push(val); @@ -74,76 +76,63 @@ function compute(transformList) { } } break; - // case 'rotate': - // case 'rotateX': - // case 'rotateY': - // // case 'rotateZ': - // // case 'rotate3d': - // - // { - // let x: number = 0; - // let y: number = 0; - // let z: number = 0; - // - // let angle: number; - // let values: number[] = []; - // - // if ((transformList[i] as FunctionToken).val == 'rotateX' || (transformList[i] as FunctionToken).val == 'rotateY' || (transformList[i] as FunctionToken).val == 'rotateZ') { - // - // for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { - // - // if (child.typ == EnumToken.WhitespaceTokenType) { - // - // continue; - // } - // - // // if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { - // // - // // return null; - // // } - // - // if (child.typ == EnumToken.NumberTokenType && +child.val > 0) { - // - // return null; - // } - // - // angle = getAngle(child as AngleToken | NumberToken); - // - // if (angle == null) { - // - // return null; - // } - // - // values.push(angle * 2 * Math.PI); - // - // if ((transformList[i] as FunctionToken).val == 'rotateX') { - // - // x = 1; - // } else if ((transformList[i] as FunctionToken).val == 'rotateY') { - // - // y = 1; - // } else if ((transformList[i] as FunctionToken).val == 'rotateZ') { - // - // z = 1; - // } - // } - // - // if (values.length != 1) { - // - // return null; - // } - // - // console.error({values}); - // - // return null; - // } - // - // else if ((transformList[i] as FunctionToken).val == 'rotate') { - // - // } - // } - // - // break; + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + // + { + let x = 0; + let y = 0; + let z = 0; + let angle; + let values = []; + let valuesCount = transformList[i].val == 'rotate3d' ? 4 : 1; + if (['rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d'].includes(transformList[i].val)) { + for (const child of stripCommaToken(transformList[i].chi.slice())) { + if (child.typ == EnumToken.WhitespaceTokenType) { + continue; + } + values.push(child); + if (transformList[i].val == 'rotateX') { + x = 1; + } + else if (transformList[i].val == 'rotateY') { + y = 1; + } + else if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { + z = 1; + } + } + // console.error({values, valuesCount}); + if (values.length != valuesCount) { + return null; + } + if (transformList[i].val == 'rotate3d') { + x = getNumber(values[0]); + y = getNumber(values[1]); + z = getNumber(values[2]); + } + angle = getAngle(values.at(-1)); + if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { + return null; + } + // console.error([x, y, z, angle * 2 * Math.PI]); + // if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { + rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + // console.error({matrix}); + // continue; + // } + // console.error({values}); + // return null; + } + // else + // if ((transformList[i] as FunctionToken).val == 'rotate') { + // + // } + } + break; default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); diff --git a/dist/lib/ast/transform/matrix.js b/dist/lib/ast/transform/matrix.js deleted file mode 100644 index 5b724969..00000000 --- a/dist/lib/ast/transform/matrix.js +++ /dev/null @@ -1,46 +0,0 @@ -import { is2DMatrix } from './utils.js'; -import { EnumToken } from '../types.js'; -import { reduceNumber } from '../../renderer/render.js'; - -function serialize(matrix) { - if (is2DMatrix(matrix)) { - // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset - return { - typ: EnumToken.FunctionTokenType, - val: 'matrix', - chi: [ - matrix[0][0], - matrix[0][1], - matrix[1][0], - matrix[1][1], - matrix[3][0], - matrix[3][1] - ].reduce((acc, t) => { - if (acc.length > 0) { - acc.push({ typ: EnumToken.CommaTokenType }); - } - acc.push({ - typ: EnumToken.NumberTokenType, - val: reduceNumber(t) - }); - return acc; - }, []) - }; - } - return { - typ: EnumToken.FunctionTokenType, - val: 'matrix3d', - chi: matrix[0].concat(matrix[1]).concat(matrix[2]).concat(matrix[3]).reduce((acc, t) => { - if (acc.length > 0) { - acc.push({ typ: EnumToken.CommaTokenType }); - } - acc.push({ - typ: EnumToken.NumberTokenType, - val: reduceNumber(t) - }); - return acc; - }, []) - }; -} - -export { serialize }; diff --git a/dist/lib/ast/transform/minify.js b/dist/lib/ast/transform/minify.js index 63bcf20b..4eda0545 100644 --- a/dist/lib/ast/transform/minify.js +++ b/dist/lib/ast/transform/minify.js @@ -1,5 +1,6 @@ import { decompose } from './utils.js'; import { EnumToken } from '../types.js'; +import { gcd } from '../math/math.js'; function minify(matrix) { const decomposed = decompose(matrix); @@ -7,6 +8,7 @@ function minify(matrix) { return null; } const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + const result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { transforms.delete('translate'); @@ -20,17 +22,18 @@ function minify(matrix) { if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { transforms.delete('perspective'); } - if (transforms.size == 0) { - // identity - return [{ - typ: EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - { typ: EnumToken.NumberTokenType, val: '1' } - ] - } - ]; - } + // if (transforms.size == 0) { + // + // // identity + // return [{ + // typ: EnumToken.FunctionTokenType, + // val: 'scale', + // chi: [ + // {typ: EnumToken.NumberTokenType, val: '1'} + // ] + // } + // ]; + // } if (transforms.size == 1) { if (transforms.has('translate')) { let coordinates = new Set(['x', 'y', 'z']); @@ -40,54 +43,57 @@ function minify(matrix) { } } if (coordinates.size == 3) { - return [{ - typ: EnumToken.FunctionTokenType, - val: 'translate', - chi: [ - { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: EnumToken.CommaTokenType }, - { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: EnumToken.CommaTokenType }, - { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } - ] - }]; + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); } - if (coordinates.size == 1) { + else if (coordinates.size == 1) { if (coordinates.has('x')) { - return [{ - typ: EnumToken.FunctionTokenType, - val: 'translate', - chi: [{ typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] - }]; + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [{ typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] + }); } - let axis = coordinates.has('y') ? 'y' : 'z'; - let index = axis == 'y' ? 1 : 2; - return [{ + else { + let axis = coordinates.has('y') ? 'y' : 'z'; + let index = axis == 'y' ? 1 : 2; + result.push({ typ: EnumToken.FunctionTokenType, val: 'translate' + axis.toUpperCase(), chi: [{ typ: EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] - }]; + }); + } } - if (coordinates.has('z')) { - return [{ - typ: EnumToken.FunctionTokenType, - val: 'translate', - chi: [ - decomposed.translate[0] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: EnumToken.CommaTokenType }, - decomposed.translate[1] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: EnumToken.CommaTokenType }, - { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } - ] - }]; + else if (coordinates.has('z')) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); } - return [{ + else { + result.push({ typ: EnumToken.FunctionTokenType, val: 'translate', chi: [ @@ -101,10 +107,159 @@ function minify(matrix) { 'val': '0' } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } ] - }]; + }); + } + } + } + { + const { x, y, z, angle } = getRotation3D(matrix); + // console.error({x, y, z, angle}); + if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { + if (y == 0 && z == 0) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotateX', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && z == 0) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotateY', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && y == 0) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotate', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotate3d', + chi: [ + { + typ: EnumToken.NumberTokenType, + val: '' + x + }, + { typ: EnumToken.CommaTokenType }, + { + typ: EnumToken.NumberTokenType, + val: '' + y + }, + { typ: EnumToken.CommaTokenType }, + { + typ: EnumToken.NumberTokenType, + val: '' + z + }, + { typ: EnumToken.CommaTokenType }, + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } } } - return null; + // scale(x, x) -> scale(x) + // scale(1, sy) -> scaleY(sy) + // scale3d(1, 1, sz) -> scaleZ(sz) + // scaleX() scale(Y) scaleZ() -> scale3d() + // identity + return result.length == 0 ? [ + { + typ: EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + { typ: EnumToken.NumberTokenType, val: '1' } + ] + } + ] : result; +} +// Fonction pour calculer rotate3d à partir de matrix3d +function getRotation3D(matrix) { + // Extraire la sous-matrice 3x3 de rotation + const r11 = matrix[0][0], r12 = matrix[0][1], r13 = matrix[0][2]; + const r21 = matrix[1][0], r22 = matrix[1][1], r23 = matrix[1][2]; + const r31 = matrix[2][0], r32 = matrix[2][1], r33 = matrix[2][2]; + // Calculer la trace (somme des éléments diagonaux) + const trace = r11 + r22 + r33; + // Calculer l’angle de rotation (en radians) + const cosTheta = (trace - 1) / 2; + // Calculer sin(θ) avec le signe correct + const sinThetaRaw = Math.sqrt(1 - cosTheta * cosTheta); + const xRaw = r32 - r23; // -0.467517 + const yRaw = r13 - r31; // 0.776535 + const zRaw = r21 - r12; // 0.776535 + // Déterminer le signe de sin(θ) basé sur la direction + const sinTheta = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; + // Calculer l’angle avec atan2 + const angle = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(12); + let x, y, z; + if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° + const x1 = r32 - r23; + const y1 = r13 - r31; + const z1 = r21 - r12; + switch (Math.max(x1, y1, z1)) { + case x1: + x = 1; + y = 0; + z = 0; + break; + case y1: + x = 0; + y = 1; + z = 0; + break; + default: + x = 0; + y = 0; + z = 1; + break; + } + } + else { + x = (r32 - r23) / (2 * sinTheta); + y = (r13 - r31) / (2 * sinTheta); + z = (r21 - r12) / (2 * sinTheta); + } + // Normaliser le vecteur (optionnel, mais utile pour vérification) + const length = Math.sqrt(x * x + y * y + z * z); + if (length > 0) { + x /= length; + y /= length; + z /= length; + } + const x1 = gcd(x, gcd(y, z)); + if (x1 != 0) { + x /= x1; + y /= x1; + z /= x1; + } + return { x, y, z, angle }; } export { minify }; diff --git a/dist/lib/ast/transform/utils.js b/dist/lib/ast/transform/utils.js index 63ed2792..c82ae742 100644 --- a/dist/lib/ast/transform/utils.js +++ b/dist/lib/ast/transform/utils.js @@ -194,9 +194,34 @@ function decompose(matrix) { if (row[1][0] > row[0][1]) { quaternion[2] = -quaternion[2]; } + // const rad2deg = 180 / Math.PI; + // const theta: number = Math.atan2(matrix[1][0], matrix[0][0]) * rad2deg; + // const zAxis = + // + // console.error({theta}); + // + // const rotation: [number, number, number] = [-Math.asin(matrix[0][2]) * rad2deg, 0, 0]; + // apply rotation + let x = quaternion[0]; + let y = quaternion[1]; + let z = quaternion[2]; + let w = quaternion[3]; + const rotationMatrix = identity(); + // Construct a composite rotation matrix from the quaternion values + // rotationMatrix is an identity 4x4 matrix initially + rotationMatrix[0][0] = 1 - 2 * (y * y + z * z); + rotationMatrix[0][1] = 2 * (x * y - z * w); + rotationMatrix[0][2] = 2 * (x * z + y * w); + rotationMatrix[1][0] = 2 * (x * y + z * w); + rotationMatrix[1][1] = 1 - 2 * (x * x + z * z); + rotationMatrix[1][2] = 2 * (y * z - x * w); + rotationMatrix[2][0] = 2 * (x * z - y * w); + rotationMatrix[2][1] = 2 * (y * z + x * w); + rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); return { skew, scale, + // rotate: [+rx.toPrecision(12), +ry.toPrecision(12), +rz.toPrecision(12)], translate, perspective, quaternion diff --git a/dist/lib/renderer/color/prophotoRgb.js b/dist/lib/renderer/color/prophotoRgb.js deleted file mode 100644 index f11819ff..00000000 --- a/dist/lib/renderer/color/prophotoRgb.js +++ /dev/null @@ -1,56 +0,0 @@ -import { srgb2xyz } from './xyz.js'; -import { XYZ_D65_to_D50, xyzd502srgb } from './xyzd50.js'; - -function prophotorgb2srgbvalues(r, g, b, a = null) { - // @ts-ignore - return xyzd502srgb(...prophotorgb2xyz50(r, g, b, a)); -} -function srgb2prophotorgbvalues(r, g, b, a) { - // @ts-ignore - return xyz50_to_prophotorgb(...XYZ_D65_to_D50(...srgb2xyz(r, g, b, a))); -} -function prophotorgb2lin_ProPhoto(r, g, b, a = null) { - return [r, g, b].map(v => { - let abs = Math.abs(v); - if (abs >= 16 / 512) { - return Math.sign(v) * Math.pow(abs, 1.8); - } - return v / 16; - }).concat(a == null || a == 1 ? [] : [a]); -} -function prophotorgb2xyz50(r, g, b, a = null) { - [r, g, b, a] = prophotorgb2lin_ProPhoto(r, g, b, a); - const xyz = [ - 0.7977666449006423 * r + - 0.1351812974005331 * g + - 0.0313477341283922 * b, - 0.2880748288194013 * r + - 0.7118352342418731 * g + - 0.0000899369387256 * b, - 0.8251046025104602 * b - ]; - return xyz.concat(a == null || a == 1 ? [] : [a]); -} -function xyz50_to_prophotorgb(x, y, z, a) { - // @ts-ignore - return gam_prophotorgb(...[ - x * 1.3457868816471585 - - y * 0.2555720873797946 - - 0.0511018649755453 * z, - x * -0.5446307051249019 + - y * 1.5082477428451466 + - 0.0205274474364214 * z, - 1.2119675456389452 * z - ].concat(a == null || a == 1 ? [] : [a])); -} -function gam_prophotorgb(r, g, b, a) { - return [r, g, b].map(v => { - let abs = Math.abs(v); - if (abs >= 1 / 512) { - return Math.sign(v) * Math.pow(abs, 1 / 1.8); - } - return 16 * v; - }).concat(a == null || a == 1 ? [] : [a]); -} - -export { prophotorgb2srgbvalues, srgb2prophotorgbvalues }; diff --git a/dist/lib/validation/at-rules/media.js b/dist/lib/validation/at-rules/media.js index a041a7fd..84cdb84b 100644 --- a/dist/lib/validation/at-rules/media.js +++ b/dist/lib/validation/at-rules/media.js @@ -241,7 +241,7 @@ function validateMediaCondition(token, atRule) { if (chi[0].typ == EnumToken.MediaQueryConditionTokenType) { return chi[0].l.typ == EnumToken.IdenTokenType; } - console.error(chi[0].parent); + // console.error(chi[0].parent); return false; } function validateMediaFeature(token) { diff --git a/dist/lib/validation/declaration.js b/dist/lib/validation/declaration.js deleted file mode 100644 index 3e435937..00000000 --- a/dist/lib/validation/declaration.js +++ /dev/null @@ -1,94 +0,0 @@ -import { EnumToken, ValidationLevel } from '../ast/types.js'; -import '../ast/minify.js'; -import '../ast/walk.js'; -import '../parser/parse.js'; -import '../renderer/color/utils/constants.js'; -import '../renderer/sourcemap/lib/encode.js'; -import '../parser/utils/config.js'; -import { getSyntaxConfig, getParsedSyntax } from './config.js'; -import { validateSyntax } from './syntax.js'; - -function validateDeclaration(declaration, options, root) { - const config = getSyntaxConfig(); - let name = declaration.nam; - if (!(name in config.declarations) && !(name in config.syntaxes)) { - if (name[0] == '-') { - const match = /^-([a-z]+)-(.*)$/.exec(name); - if (match != null) { - name = match[2]; - } - } - } - // root is at-rule - check if declaration allowed - if (root?.typ == EnumToken.AtRuleNodeType) { - // - const syntax = getParsedSyntax("atRules" /* ValidationSyntaxGroupEnum.AtRules */, '@' + root.nam)?.[0]; - if (syntax != null) { - if (!('chi' in syntax)) { - return { - valid: ValidationLevel.Drop, - node: declaration, - syntax, - error: 'declaration not allowed here' - }; - } - if (name.startsWith('--')) { - return { - valid: ValidationLevel.Valid, - node: declaration, - syntax: null, - error: '' - }; - } - // console.error({syntax}); - const config = getSyntaxConfig(); - // @ts-ignore - const obj = config["atRules" /* ValidationSyntaxGroupEnum.AtRules */]['@' + root.nam]; - if ('descriptors' in obj) { - const descriptors = obj.descriptors; - if (!(name in descriptors)) { - return { - valid: ValidationLevel.Drop, - node: declaration, - syntax: `<${declaration.nam}>`, - error: `declaration <${declaration.nam}> is not allowed in <@${root.nam}>` - }; - } - const syntax = getParsedSyntax("atRules" /* ValidationSyntaxGroupEnum.AtRules */, ['@' + root.nam, 'descriptors', name]); - return validateSyntax(syntax, declaration.val, root, options); - } - // console.error({name}); - // if (!(name in config.declarations) && !(name in config.syntaxes)) { - // - // return { - // - // valid: ValidationLevel.Drop, - // node: declaration, - // syntax: null, - // error: `unknown declaration "${declaration.nam}"` - // } - // } - // - // return validateSyntax((syntax as ValidationAtRuleDefinitionToken).chi as ValidationToken[], [declaration], root, options); - } - } - if (name.startsWith('--')) { - return { - valid: ValidationLevel.Valid, - node: declaration, - syntax: null, - error: '' - }; - } - if (!(name in config.declarations) && !(name in config.syntaxes)) { - return { - valid: ValidationLevel.Drop, - node: declaration, - syntax: `<${declaration.nam}>`, - error: `unknown declaration "${declaration.nam}"` - }; - } - return validateSyntax(getParsedSyntax("declarations" /* ValidationSyntaxGroupEnum.Declarations */, name), declaration.val); -} - -export { validateDeclaration }; diff --git a/dist/lib/validation/syntax.js b/dist/lib/validation/syntax.js deleted file mode 100644 index c47a1999..00000000 --- a/dist/lib/validation/syntax.js +++ /dev/null @@ -1,1509 +0,0 @@ -import { ValidationTokenEnum, specialValues } from './parser/types.js'; -import { renderSyntax } from './parser/parse.js'; -import { ValidationLevel, EnumToken, funcLike } from '../ast/types.js'; -import '../ast/minify.js'; -import '../ast/walk.js'; -import '../parser/parse.js'; -import { isLength } from '../syntax/syntax.js'; -import '../parser/utils/config.js'; -import { renderToken } from '../renderer/render.js'; -import { getSyntaxConfig, getParsedSyntax } from './config.js'; -import { validateSelector } from './selector.js'; -import './syntaxes/complex-selector.js'; -import { validateImage } from './syntaxes/image.js'; - -const config = getSyntaxConfig(); -function consumeToken(tokens) { - tokens.shift(); -} -function consumeSyntax(syntaxes) { - syntaxes.shift(); -} -function splice(tokens, matches) { - if (matches.length == 0) { - return tokens; - } - // @ts-ignore - const index = tokens.indexOf(matches.at(-1)); - if (index > -1) { - tokens.splice(0, index + 1); - } - return tokens; -} -function validateSyntax(syntaxes, tokens, root, options, context = { level: 0 }) { - console.error(JSON.stringify({ - syntax: syntaxes?.reduce?.((acc, curr) => acc + renderSyntax(curr), ''), - // syntaxes, - tokens: tokens.reduce((acc, curr) => acc + renderToken(curr), ''), - s: new Error('bar').stack - }, null, 1)); - if (syntaxes == null) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: tokens[0] ?? null, - syntax: null, - error: 'no matching syntaxes found', - tokens - }; - } - let token = null; - let syntax; - let result = null; - let validSyntax = false; - let matched = false; - const matches = []; - tokens = tokens.slice(); - syntaxes = syntaxes.slice(); - tokens = tokens.slice(); - if (context.cache == null) { - context.cache = new WeakMap; - } - if (context.tokens == null) { - context.tokens = tokens.slice(); - } - context = { ...context }; - main: while (tokens.length > 0) { - if (syntaxes.length == 0) { - break; - } - token = tokens[0]; - syntax = syntaxes[0]; - // @ts-ignore - context.position = context.tokens.indexOf(token); - const cached = context.cache.get(token)?.get(syntax.text) ?? null; - if (cached != null) { - if (cached.error.length > 0) { - return { ...cached, tokens, node: cached.valid == ValidationLevel.Valid ? null : token }; - } - syntaxes.shift(); - tokens.shift(); - continue; - } - if (token.typ == EnumToken.DescendantCombinatorTokenType) { - tokens.shift(); - if (syntax.typ == ValidationTokenEnum.Whitespace) { - syntaxes.shift(); - } - continue; - } - else if (syntax.typ == ValidationTokenEnum.Whitespace) { - syntaxes.shift(); - if (token.typ == EnumToken.WhitespaceTokenType) { - tokens.shift(); - } - continue; - } - else if (syntax.typ == ValidationTokenEnum.Block && EnumToken.AtRuleTokenType == token.typ && ('chi' in token)) { - syntaxes.shift(); - tokens.shift(); - // @ts-ignore - matches.push(token); - continue; - } - if (syntax.isOptional) { - if (!context.cache.has(token)) { - context.cache.set(token, new Map); - } - if (context.cache.get(token).has(syntax.text)) { - result = context.cache.get(token).get(syntax.text); - return { ...result, tokens, node: result.valid == ValidationLevel.Valid ? null : token }; - } - // @ts-ignore - const { isOptional, ...c } = syntax; - // @ts-ignore - let result2; - // @ts-ignore - result2 = validateSyntax([c], tokens, root, options, context); - if (result2.valid == ValidationLevel.Valid && result2.matches.length > 0) { - tokens = result2.tokens; - // splice(tokens, result2.matches); - // tokens = result2.tokens; - // @ts-ignore - matches.push(...result2.matches); - matched = true; - result = result2; - } - else { - syntaxes.shift(); - continue; - } - syntaxes.shift(); - if (syntaxes.length == 0) { - // @ts-ignore - return { - valid: ValidationLevel.Valid, - matches: result2.matches, - node: result2.node, - syntax: result2.syntax, - error: result2.error, - tokens - }; - } - continue; - } - if (syntax.isList) { - let index = -1; - // @ts-ignore - let { isList, ...c } = syntax; - // const c: ValidationToken = {...syntaxes, isList: false} as ValidationToken; - let result2 = null; - validSyntax = false; - do { - for (let i = index + 1; i < tokens.length; i++) { - if (tokens[i].typ == EnumToken.CommaTokenType) { - index = i; - break; - } - } - if (tokens[index + 1]?.typ == EnumToken.CommaTokenType) { - return { - valid: ValidationLevel.Drop, - matches, - node: tokens[0], - syntax, - error: 'unexpected token', - tokens - }; - } - if (index == -1) { - index = tokens.length; - } - if (index == 0) { - break; - } - // @ts-ignore - result2 = validateSyntax([c], tokens.slice(0, index), root, options, context); - matched = result2.valid == ValidationLevel.Valid && result2.matches.length > 0; - if (matched) { - const l = tokens.length; - validSyntax = true; - // @ts-ignore - // matches.push(...result2.matches); - // splice(tokens, result2.matches); - if (tokens.length == 1 && tokens[0].typ == EnumToken.CommaTokenType) { - return { - valid: ValidationLevel.Drop, - matches, - node: tokens[0], - syntax, - error: 'unexpected token', - tokens - }; - } - tokens = tokens.slice(index); - result = result2; - // @ts-ignore - matches.push(...result2.matches); - if (result.tokens.length > 0) { - if (index == -1) { - tokens = result.tokens; - } - else { - tokens = tokens.slice(index - result.tokens.length); - } - } - else if (index > 0) { - tokens = tokens.slice(index); - } - index = -1; - if (l == tokens.length) { - break; - } - } - else { - break; - } - } while (tokens.length > 0); - // if (level == 0) { - // } - if (!matched) { - return { - valid: ValidationLevel.Drop, - // @ts-ignore - matches: [...new Set(matches)], - node: token, - syntax, - error: 'unexpected token', - tokens - }; - } - syntaxes.shift(); - continue; - } - if (syntax.isRepeatable) { - // @ts-ignore - let { isRepeatable, ...c } = syntax; - let result2 = null; - validSyntax = false; - let l = tokens.length; - let tok = null; - do { - // @ts-ignore - result2 = validateSyntax([c], tokens, root, options, context); - if (result2.matches.length == 0 && result2.error.length > 0) { - syntaxes.shift(); - break main; - } - if (result2.valid == ValidationLevel.Valid) { - tokens = result2.tokens; - // @ts-ignore - matches.push(...result2.matches); - result = result2; - if (l == tokens.length) { - if (tok == tokens[0]) { - break; - } - if (result2.matches.length == 0 && tokens.length > 0) { - tokens = result2.tokens; - tok = tokens[0]; - continue; - } - break; - } - if (matches.length == 0) { - tokens = result2.tokens; - } - l = tokens.length; - continue; - } - break; - } while (result2.valid == ValidationLevel.Valid && tokens.length > 0); - // if (lastResult != null) { - // - // splice(tokens, lastResult.matches); - // // tokens = lastResult.tokens; - // } - syntaxes.shift(); - continue; - } - // at least one match - if (syntax.isRepeatableGroup) { - validSyntax = false; - let count = 0; - let l = tokens.length; - let result2 = null; - do { - // @ts-ignore - const { isRepeatableGroup, ...c } = syntax; - // @ts-ignore - result2 = validateSyntax([c], tokens, root, options, context); - if (result2.valid == ValidationLevel.Drop || result2.matches.length == 0) { - if (count > 0) { - syntaxes.shift(); - // if (result2.matches.length == 0) { - tokens = result2.tokens; - // break main; - if (syntaxes.length == 0) { - return result2; - } - break main; - } - return result2; - } - if (result2.valid == ValidationLevel.Valid && result2.matches.length > 0) { - count++; - // lastResult = result; - validSyntax = true; - tokens = result2.tokens; - // splice(tokens, result2.matches); - // tokens = result2.tokens; - // @ts-ignore - matches.push(...result2.matches); - result = result2; - if (l == tokens.length) { - break; - } - l = tokens.length; - } - else { - break; - } - } while (tokens.length > 0 && result.valid == ValidationLevel.Valid); - // if (lastResult != null) { - // - // splice(tokens, lastResult.matches); - // // tokens = lastResult.tokens; - // } - // at least one match is expected - if (!validSyntax /* || result.matches.length == 0 */) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - node: token, - tokens, - syntax, - error: 'unexpected token', - matches: [] - }; - } - syntaxes.shift(); - continue; - } - if (syntax.atLeastOnce) { - const { atLeastOnce, ...c } = syntax; - result = validateSyntax([c], tokens, root, options, context); - if (result.valid == ValidationLevel.Drop) { - return result; - } - splice(tokens, result.matches); - // tokens = result.tokens; - // @ts-ignore - matches.push(...result.matches); - let l = tokens.length; - let r = validateSyntax([c], tokens, root, options, context); - while (r.valid == ValidationLevel.Valid) { - splice(tokens, r.matches); - // tokens = r.tokens; - r = validateSyntax([c], tokens, root, options, context); - if (l == tokens.length) { - break; - } - if (r.valid == ValidationLevel.Valid && r.matches.length > 0) { - // @ts-ignore - matches.push(...result.matches); - } - l = tokens.length; - } - syntaxes.shift(); - continue; - } - // @ts-ignore - if (syntax.occurence != null) { - // @ts-ignore - const { occurence, ...c } = syntax; - // && syntaxes.occurence.max != null - // consume all tokens - let match = 1; - // @ts-ignore - result = validateSyntax([c], tokens, root, options, context); - if (result.valid == ValidationLevel.Drop) { - return result; - } - if (result.matches.length == 0) { - syntaxes.shift(); - continue; - } - // splice(tokens, result.matches); - // tokens = result.tokens; - // @ts-ignore - matches.push(...result.matches); - matched = true; - tokens = result.tokens; - while (occurence.max == null || match < occurence.max) { - // trim whitespace - if (tokens[0]?.typ == EnumToken.WhitespaceTokenType) { - tokens.shift(); - } - // @ts-ignore - let r = validateSyntax([c], tokens, root, options, context); - if (r.valid != ValidationLevel.Valid || r.matches.length == 0) { - break; - } - result = r; - // splice(tokens, r.matches); - // tokens = r.tokens; - // @ts-ignore - matches.push(...result.matches); - match++; - tokens = r.tokens; - result = r; - if (tokens.length == 0 || (occurence.max != null && match >= occurence.max)) { - break; - } - // @ts-ignore - // r = validateSyntax([c], tokens, root, options, context); - } - syntaxes.shift(); - continue; - } - // @ts-ignore - if (syntax.typ == ValidationTokenEnum.Whitespace) { - if (token.typ == EnumToken.WhitespaceTokenType) { - tokens.shift(); - } - syntaxes.shift(); - continue; - } - // @ts-ignore - if (token.val != null && specialValues.includes(token.val)) { - matched = true; - result = { - valid: ValidationLevel.Valid, - matches: [token], - node: null, - syntax, - error: '', - tokens - }; - // @ts-ignore - matches.push(...result.matches); - } - else { - result = doValidateSyntax(syntax, token, tokens, root, options, context); - matched = result.valid == ValidationLevel.Valid && result.matches.length > 0; - if (matched) { - // splice(tokens, result.matches); - tokens = result.tokens; - // @ts-ignore - matches.push(...result.matches); - } - } - if (result.valid == ValidationLevel.Drop) { - // @ts-ignore - return { ...result, matches, tokens, node: result.valid == ValidationLevel.Valid ? null : token }; - } - consumeSyntax(syntaxes); - if (tokens.length == 0) { - return result; - } - } - if (result?.valid == ValidationLevel.Valid) { - // splice(tokens, result.matches); - tokens = result.tokens; - // @ts-ignore - matches.push(...result.matches); - } - if ( /* result == null && */tokens.length == 0 && syntaxes.length > 0) { - validSyntax = isOptionalSyntax(syntaxes); - } - if (result == null) { - result = { - valid: validSyntax ? ValidationLevel.Valid : ValidationLevel.Drop, - matches, - node: validSyntax ? null : tokens[0] ?? null, - // @ts-ignore - syntax, - error: validSyntax ? '' : 'unexpected token', - tokens - }; - } - if (token != null) { - if (!context.cache.has(token)) { - context.cache.set(token, new Map); - } - context.cache.get(token).set(syntax.text, result); - } - if (result != null) { - // @ts-ignore - return { ...result, matches: [...(new Set(matches))] }; - } - return result; -} -function isOptionalSyntax(syntaxes) { - return syntaxes.length > 0 && syntaxes.every(t => t.typ == ValidationTokenEnum.Whitespace || t.isOptional || t.isRepeatable || (t.typ == ValidationTokenEnum.PropertyType && isOptionalSyntax(getParsedSyntax("syntaxes" /* ValidationSyntaxGroupEnum.Syntaxes */, t.val) ?? getParsedSyntax("declarations" /* ValidationSyntaxGroupEnum.Declarations */, t.val) ?? []))); -} -function doValidateSyntax(syntax, token, tokens, root, options, context) { - let valid = false; - let result; - let children; - let queue; - let matches; - let child; - let astNodes = new Set; - if (token.typ == EnumToken.NestingSelectorTokenType && syntax.typ == 2) { - valid = root != null && 'relative-selector' == syntax.val; - return { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - switch (syntax.typ) { - case ValidationTokenEnum.Comma: - valid = token.typ === EnumToken.CommaTokenType; - // @ts-ignore - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - break; - case ValidationTokenEnum.AtRule: - if (token.typ != EnumToken.AtRuleNodeType) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'expecting at-rule', - tokens - }; - } - if (token.nam != syntax.val) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: `expecting '@${syntax.val}' but found '@${token.nam}'`, - tokens - }; - } - if (root == null) { - return { - valid: ValidationLevel.Valid, - matches: [token], - node: null, - syntax, - error: '', - tokens - }; - } - if (root.typ != EnumToken.AtRuleNodeType) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'not allowed here', - tokens - }; - } - if (!('chi' in token)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: '@at-rule must have children', - tokens - }; - } - // @ts-ignore - result = { - valid: ValidationLevel.Valid, - matches: [token], - node: null, - syntax, - error: '', - tokens - }; - break; - case ValidationTokenEnum.AtRuleDefinition: - if (token.typ != EnumToken.AtRuleNodeType) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'expecting at-rule', - tokens - }; - } - if ('chi' in syntax && !('chi' in token)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: '@at-rule must have children', - tokens - }; - } - if ('chi' in token && !('chi' in token)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'children not allowed here', - tokens - }; - } - const s = getParsedSyntax("atRules" /* ValidationSyntaxGroupEnum.AtRules */, '@' + token.nam); - if ('prelude' in syntax) { - if (!('tokens' in token)) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'expected at-rule prelude', - tokens - }; - } - result = validateSyntax(s[0].prelude, token.tokens, root, options, { - ...context, - tokens: null, - level: context.level + 1 - }); - if (result.valid == ValidationLevel.Drop) { - return result; - } - } - const hasBody = 'chi' in s[0]; - if ('chi' in token) { - if (!hasBody) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'unexpected at-rule body', - tokens - }; - } - } - else if (hasBody) { - // @ts-ignore - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'expecting at-rule body', - tokens - }; - } - break; - case ValidationTokenEnum.DeclarationType: - // @ts-ignore - result = validateSyntax(getParsedSyntax("declarations" /* ValidationSyntaxGroupEnum.Declarations */, syntax.val), [token], root, options, context); - break; - case ValidationTokenEnum.Keyword: - valid = (token.typ == EnumToken.IdenTokenType && token.val.localeCompare(syntax.val, 'en', { sensitivity: 'base' }) == 0) || - (token.typ == EnumToken.ColorTokenType && token.kin == 'lit' && syntax.val.localeCompare(token.val, 'en', { sensitivity: 'base' }) == 0); - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - break; - case ValidationTokenEnum.SemiColon: - valid = root == null || [EnumToken.RuleNodeType, EnumToken.AtRuleNodeType, EnumToken.StyleSheetNodeType].includes(root.typ); - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - break; - case ValidationTokenEnum.Separator: - valid = token.typ == EnumToken.LiteralTokenType && token.val != '/'; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - break; - case ValidationTokenEnum.PropertyType: - // - if ('image' == syntax.val) { - valid = token.typ == EnumToken.UrlFunctionTokenType || token.typ == EnumToken.ImageFunctionTokenType; - if (!valid) { - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'unexpected ', - tokens - }; - } - result = validateImage(token); - } - else if (['media-feature', 'mf-plain'].includes(syntax.val)) { - valid = token.typ == EnumToken.DeclarationNodeType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if (syntax.val == 'pseudo-page') { - valid = token.typ == EnumToken.PseudoClassTokenType && [':left', ':right', ':first', ':blank'].includes(token.val); - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if (syntax.val == 'page-body') { - if (token.typ == EnumToken.DeclarationNodeType) { - valid = true; - // @ts-ignore - result = { - valid: ValidationLevel.Valid, - matches: [token], - node: null, - syntax, - error: '', - tokens - }; - while (tokens.length > 0 && [EnumToken.DeclarationNodeType].includes(tokens[0].typ)) { - // @ts-ignore - result.matches.push(tokens.shift()); - } - } - else if (token.typ == EnumToken.AtRuleNodeType) { - result = validateSyntax(getParsedSyntax("syntaxes" /* ValidationSyntaxGroupEnum.Syntaxes */, 'page-margin-box-type'), [token], root, options, context); - } - } - else if (syntax.val == 'group-rule-body') { - valid = [EnumToken.AtRuleNodeType, EnumToken.RuleNodeType].includes(token.typ); - if (!valid && token.typ == EnumToken.DeclarationNodeType && root?.typ == EnumToken.AtRuleNodeType && root.nam == 'media') { - // allowed only if nested rule - let parent = root; - while (parent != null) { - if (parent.typ == EnumToken.RuleNodeType) { - valid = true; - break; - } - // @ts-ignore - parent = parent.parent; - } - } - // @ts-ignore - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'token is not allowed as a child', - tokens - }; - if (!valid) { - return result; - } - } - // - else if ('type-selector' == syntax.val) { - valid = (token.typ == EnumToken.UniversalSelectorTokenType) || - token.typ == EnumToken.IdenTokenType || (token.typ == EnumToken.NameSpaceAttributeTokenType && - (token.l == null || token.l.typ == EnumToken.IdenTokenType || - (token.l.typ == EnumToken.LiteralTokenType && token.l.val == '*')) && - token.r.typ == EnumToken.IdenTokenType); - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('wq-name' == syntax.val) { - valid = token.typ == EnumToken.IdenTokenType || (token.typ == EnumToken.NameSpaceAttributeTokenType && - (token.l == null || token.l.typ == EnumToken.IdenTokenType || (token.l.typ == EnumToken.LiteralTokenType && token.l.val == '*')) && - token.r.typ == EnumToken.IdenTokenType); - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if (EnumToken.UniversalSelectorTokenType == token.typ && 'subclass-selector' == syntax.val) { - valid = true; - result = { - valid: ValidationLevel.Valid, - matches: [token], - node: null, - syntax, - error: '', - tokens - }; - } - else if ('attribute-selector' == syntax.val) { - valid = token.typ == EnumToken.AttrTokenType && token.chi.length > 0; - if (valid) { - const children = token.chi.filter(t => t.typ != EnumToken.WhitespaceTokenType && t.typ != EnumToken.CommaTokenType); - valid = children.length == 1 && [ - EnumToken.IdenTokenType, - EnumToken.NameSpaceAttributeTokenType, - EnumToken.MatchExpressionTokenType - ].includes(children[0].typ); - if (valid && children[0].typ == EnumToken.MatchExpressionTokenType) { - const t = children[0]; - valid = [ - EnumToken.IdenTokenType, - EnumToken.NameSpaceAttributeTokenType - ].includes(t.l.typ) && - (t.op == null || ([ - EnumToken.DelimTokenType, EnumToken.DashMatchTokenType, - EnumToken.StartMatchTokenType, EnumToken.ContainMatchTokenType, - EnumToken.EndMatchTokenType, EnumToken.IncludeMatchTokenType - ].includes(t.op.typ) && - t.r != null && - [ - EnumToken.StringTokenType, - EnumToken.IdenTokenType - ].includes(t.r.typ))); - if (valid && t.attr != null) { - const s = getParsedSyntax("syntaxes" /* ValidationSyntaxGroupEnum.Syntaxes */, 'attr-modifier')[0]; - valid = s.chi.some((l) => l.some((r) => r.val == t.attr)); - } - } - } - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - if (!valid) { - return result; - } - } - else if ('combinator' == syntax.val) { - valid = [ - EnumToken.DescendantCombinatorTokenType, - EnumToken.SubsequentSiblingCombinatorTokenType, - EnumToken.NextSiblingCombinatorTokenType, - EnumToken.ChildCombinatorTokenType, - EnumToken.ColumnCombinatorTokenType - ].includes(token.typ); - if (valid) { - // @ts-ignore - const position = context.tokens.indexOf(token); - if (root == null) { - valid = position > 0 && context.tokens[position - 1]?.typ != EnumToken.CommaTokenType; - } - } - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - if (!valid) { - return result; - } - } - else if ('ident-token' == syntax.val) { - valid = token.typ == EnumToken.IdenTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('hex-color' == syntax.val) { - valid = token.typ == EnumToken.ColorTokenType && token.kin == 'hex'; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('resolution' == syntax.val) { - valid = token.typ == EnumToken.ResolutionTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('angle' == syntax.val) { - valid = token.typ == EnumToken.AngleTokenType || (token.typ == EnumToken.NumberTokenType && token.val == '0'); - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('time' == syntax.val) { - valid = token.typ == EnumToken.TimingFunctionTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('ident' == syntax.val) { - valid = token.typ == EnumToken.IdenTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if (['id-selector', 'hash-token'].includes(syntax.val)) { - valid = token.typ == EnumToken.HashTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if (['integer', 'number'].includes(syntax.val)) { - // valid = token.typ == EnumToken.NumberTokenType; - valid = token.typ == EnumToken.NumberTokenType && ('integer' != syntax.val || Number.isInteger(+token.val)); - if (valid && 'range' in syntax) { - const value = Number(token.val); - const range = syntax.range; - valid = value >= range[0] && (range[1] == null || value <= range[1]); - } - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('length' == syntax.val) { - valid = isLength(token) || (token.typ == EnumToken.NumberTokenType && token.val == '0'); - // @ts-ignore - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('percentage' == syntax.val) { - valid = token.typ == EnumToken.PercentageTokenType || (token.typ == EnumToken.NumberTokenType && token.val == '0'); - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('dashed-ident' == syntax.val) { - valid = token.typ == EnumToken.DashedIdenTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('custom-ident' == syntax.val) { - valid = token.typ == EnumToken.DashedIdenTokenType || token.typ == EnumToken.IdenTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('custom-property-name' == syntax.val) { - valid = token.typ == EnumToken.DashedIdenTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('string' == syntax.val) { - valid = token.typ == EnumToken.StringTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('declaration-value' == syntax.val) { - valid = token.typ != EnumToken.LiteralTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('url' == syntax.val) { - valid = token.typ == EnumToken.UrlFunctionTokenType || token.typ == EnumToken.StringTokenType; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('declaration' == syntax.val) { - valid = token.typ == EnumToken.DeclarationNodeType && (token.nam.startsWith(('--')) || token.nam in config.declarations || token.nam in config.syntaxes); - if (!valid) { - // @ts-ignore - result = { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'unexpected token', - tokens - }; - } - else if (token.nam.startsWith(('--'))) { - result = { - valid: ValidationLevel.Valid, - matches: [token], - node: null, - syntax, - error: '', - tokens - }; - } - else { - result = validateSyntax(getParsedSyntax("declarations" /* ValidationSyntaxGroupEnum.Declarations */, token.nam) ?? getParsedSyntax("syntaxes" /* ValidationSyntaxGroupEnum.Syntaxes */, token.nam), token.val, token, options, { - ...context, - tokens: null, - level: 0 - }); - if (result.valid == ValidationLevel.Valid && result.error.length == 0) { - tokens = result.tokens; - } - } - } - else if ('class-selector' == syntax.val) { - valid = EnumToken.ClassSelectorTokenType == token.typ || EnumToken.UniversalSelectorTokenType == token.typ; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - // else if ('complex-selector' == (syntaxes as ValidationPropertyToken).val) { - // - // result = validateSyntax(getParsedSyntax(ValidationSyntaxGroupEnum.Syntaxes, (syntaxes as ValidationPropertyToken).val) as ValidationToken[], tokens, root as AstNode, options, context); - // - // } - else if (['pseudo-element-selector', 'pseudo-class-selector'].includes(syntax.val)) { - valid = false; - if (token.typ == EnumToken.PseudoClassTokenType) { - let val = token.val; - if (val == ':before' || val == ':after') { - val = ':' + val; - } - valid = val in config.selectors; - if (!valid && val.match(/^:?:-/) != null) { - const match = token.val.match(/^(:?:)(-[^-]+-)(.*)$/); - if (match != null) { - valid = true; - } - } - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'invalid pseudo class', - tokens - }; - } - else if (token.typ == EnumToken.PseudoClassFuncTokenType) { - let key = token.val in config.selectors ? token.val : token.val + '()'; - valid = key in config.selectors; - if (!valid && token.val.match(/^:?:-/)) { - const match = token.val.match(/^(:?:)(-[^-]+-)(.*)$/); - if (match != null) { - key = match[1] + match[3] in config.selectors ? match[1] + match[3] : match[1] + match[3] + '()'; - valid = key in config.selectors; - } - } - const s = getParsedSyntax("selectors" /* ValidationSyntaxGroupEnum.Selectors */, key); - if (s != null) { - const r = s[0]; - if (r.typ != ValidationTokenEnum.PseudoClassFunctionToken) { - valid = false; - } - else { - result = validateSyntax(s[0].chi, token.chi, root, options, { - ...context, - tokens: null, - level: context.level + 1 - }); - break; - } - } - } - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - // - // - else if ('relative-selector' == syntax.val) { - if (tokens.length == 1 && token.typ == EnumToken.NestingSelectorTokenType) { - return { - valid: ValidationLevel.Valid, - matches: [token], - node: token, - syntax, - error: '', - tokens - }; - } - result = validateSyntax(getParsedSyntax("syntaxes" /* ValidationSyntaxGroupEnum.Syntaxes */, syntax.val), token.typ == EnumToken.NestingSelectorTokenType ? tokens.slice(1) : tokens, root, options, context); - } - // - // - else if (['forgiving-selector-list', 'forgiving-relative-selector-list'].includes(syntax.val)) { - // @ts-ignore - result = { tokens: tokens.slice(), ...validateSelector(tokens, options, root) }; - } - // https://github.com/mdn/data/pull/186#issuecomment-369604537 - else if (syntax.val.endsWith('-token')) { - const val = syntax.val; - valid = true; - switch (val) { - case 'function-token': - valid = token.typ != EnumToken.ParensTokenType && funcLike.includes(token.typ); - break; - case 'ident-token': - valid = token.typ == EnumToken.DashedIdenTokenType || token.typ == EnumToken.IdenTokenType; - break; - case 'hash-token': - valid = token.typ == EnumToken.HashTokenType; - break; - case 'string-token': - valid = token.typ == EnumToken.StringTokenType; - break; - default: - console.error(new Error(`unhandled syntax: '<${val}>'`)); - break; - } - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else if ('wq-name' == syntax.val) { - valid = token.typ == EnumToken.IdenTokenType || (token.typ == EnumToken.NameSpaceAttributeTokenType && - (token.l == null || token.l.typ == EnumToken.IdenTokenType || (token.l.typ == EnumToken.LiteralTokenType && token.l.val == '*')) && - token.r.typ == EnumToken.IdenTokenType); - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: token, - syntax, - error: valid ? '' : 'unexpected token', - tokens - }; - } - else { - const val = syntax.val; - // https://github.com/mdn/data/pull/186#issuecomment-369604537 - if (val == 'any-value') { - return { - valid: ValidationLevel.Valid, - matches: [token], - node: null, - syntax, - error: '', - tokens - }; - } - else if (val in config.declarations || val in config.syntaxes) { - result = validateSyntax(getParsedSyntax("syntaxes" /* ValidationSyntaxGroupEnum.Syntaxes */, val) ?? getParsedSyntax("declarations" /* ValidationSyntaxGroupEnum.Declarations */, val), tokens, root, options, context); - } - else { - // @ts-ignore - result = { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'unexpected token', - tokens - }; - } - } - break; - case ValidationTokenEnum.Parens: - case ValidationTokenEnum.Function: - if (syntax.typ == ValidationTokenEnum.Parens) { - valid = token.typ == EnumToken.ParensTokenType; - } - else { - valid = 'chi' in token && 'val' in token && - token.val.localeCompare(syntax.val, 'en', { sensitivity: 'base' }) == 0; - } - result = !valid ? - // @ts-ignore - { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: 'unexpected token', - tokens - } : validateSyntax(syntax.chi, token.chi, root, options, { - ...context, - tokens: null, - level: context.level + 1 - }); - break; - case ValidationTokenEnum.ValidationFunctionDefinition: - valid = 'val' in token && 'chi' in token; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: '', - tokens - }; - if (result.valid == ValidationLevel.Valid) { - valid = token.val.localeCompare(syntax.val, 'en', { sensitivity: 'base' }) == 0; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - error: '', - syntax, - tokens - }; - if (result.valid == ValidationLevel.Valid) { - const s = getParsedSyntax("syntaxes" /* ValidationSyntaxGroupEnum.Syntaxes */, syntax.val + '()'); // config[ValidationSyntaxGroupEnum.Syntaxes][(syntaxes as ValidationFunctionDefinitionToken).val + '()'] as ValidationSyntaxNode; - result = validateSyntax(s, tokens, root, options, context); - } - } - break; - case ValidationTokenEnum.Bracket: - result = validateSyntax(syntax.chi, tokens, root, options, context); - break; - case ValidationTokenEnum.PipeToken: - for (const lines of syntax.chi) { - result = validateSyntax(lines, tokens, root, options, context); - if (result.valid == ValidationLevel.Valid) { - break; - } - } - break; - case ValidationTokenEnum.AmpersandToken: - children = [...syntax.l.slice(), ...syntax.r.slice()]; - matches = []; - queue = []; - let m = []; - for (let j = 0; j < children.length; j++) { - const res = validateSyntax([children[j]], tokens, root, options, context); - // @ts-ignore - if (res.valid == ValidationLevel.Valid) { - m.push(...res.matches); - matches.push(...children.splice(j, 1)); - j = 0; - // @ts-ignore - astNodes.delete(token); - consumeToken(tokens); - token = tokens[0]; - if (token == null) { - break; - } - // @ts-ignore - astNodes.add(token); - } - } - if (astNodes.size > 0) { - // @ts-ignore - tokens.unshift(...astNodes); - astNodes = new Set(); - } - valid = matches.length > 0; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: m, - node: valid ? null : token, - syntax, - error: valid ? '' : 'expecting token', - tokens - }; - break; - case ValidationTokenEnum.ColumnArrayToken: - { - matches = []; - queue = []; - const children = syntax.chi; - let child; - while (child = children.shift()) { - result = validateSyntax([child], tokens, root, options, context); - if (result.valid == ValidationLevel.Valid) { - matches.push(child); - consumeToken(tokens); - token = tokens[0]; - if (queue.length > 0) { - children.unshift(...queue); - queue = []; - } - if (token == null) { - break; - } - } - else { - queue.push(child); - } - } - valid = matches.length > 0; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'expecting token', - tokens - }; - } - break; - case ValidationTokenEnum.ColumnToken: - children = [...syntax.l.slice(), ...syntax.r.slice()]; - matches = []; - queue = []; - while ((child = children.shift())) { - const res = validateSyntax([child], tokens, root, options, context); - if (res.valid == ValidationLevel.Valid) { - matches.push(child); - consumeToken(tokens); - token = tokens[0]; - if (queue.length > 0) { - children.unshift(...queue); - queue = []; - } - if (token == null) { - break; - } - } - else { - queue.push(child); - } - } - valid = matches.length > 0; - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'expecting token', - tokens - }; - break; - case ValidationTokenEnum.StringToken: - valid = token.typ == EnumToken.StringTokenType && syntax.val.slice(1, -1) == token.val.slice(1, -1); - return { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'expecting token', - tokens - }; - case ValidationTokenEnum.PseudoClassFunctionToken: - valid = token.typ == EnumToken.PseudoClassFuncTokenType; - if (valid) { - let key = token.val in config.selectors ? token.val : token.val + '()'; - const s = getParsedSyntax("syntaxes" /* ValidationSyntaxGroupEnum.Syntaxes */, key); - valid = s != null && validateSyntax(s, token.chi, root, options, { - ...context, - tokens: null, - level: context.level + 1 - }).valid == ValidationLevel.Valid; - } - result = { - valid: valid ? ValidationLevel.Valid : ValidationLevel.Drop, - matches: valid ? [token] : [], - node: valid ? null : token, - syntax, - error: valid ? '' : 'invalid token', - tokens - }; - break; - case ValidationTokenEnum.DeclarationDefinitionToken: - if (token.typ != EnumToken.DeclarationNodeType || token.nam != syntax.nam) { - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax, - error: '', - tokens - }; - } - return validateSyntax([syntax.val], token.val, root, options, context); - default: - throw new Error('not implemented: ' + JSON.stringify({ syntax, token, tokens }, null, 1)); - } - // @ts-ignore - return result; -} - -export { validateSyntax }; diff --git a/dist/lib/validation/syntaxes/image.js b/dist/lib/validation/syntaxes/image.js deleted file mode 100644 index 5730a028..00000000 --- a/dist/lib/validation/syntaxes/image.js +++ /dev/null @@ -1,29 +0,0 @@ -import { EnumToken, ValidationLevel } from '../../ast/types.js'; -import '../../ast/minify.js'; -import '../../ast/walk.js'; -import '../../parser/parse.js'; -import '../../renderer/color/utils/constants.js'; -import '../../renderer/sourcemap/lib/encode.js'; -import '../../parser/utils/config.js'; -import { validateSyntax } from '../syntax.js'; -import { getParsedSyntax } from '../config.js'; -import { validateURL } from './url.js'; - -function validateImage(token) { - if (token.typ == EnumToken.UrlFunctionTokenType) { - return validateURL(token); - } - if (token.typ == EnumToken.ImageFunctionTokenType) { - return validateSyntax(getParsedSyntax("syntaxes" /* ValidationSyntaxGroupEnum.Syntaxes */, token.val + '()'), token.chi); - } - return { - valid: ValidationLevel.Drop, - matches: [], - node: token, - syntax: 'image()', - error: 'expected or ', - tokens: [] - }; -} - -export { validateImage }; diff --git a/jsr.json b/jsr.json index cf3de4bd..41630303 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@tbela99/css-parser", - "version": "0.9.2-alpha1", + "version": "0.9.2-alpha2", "publish": { "include": [ "src", diff --git a/package.json b/package.json index 8a703d3f..2bacfca2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "v0.9.2-alpha1", + "version": "v0.9.2-alpha2", "exports": { ".": "./dist/node/index.js", "./node": "./dist/node/index.js", diff --git a/src/lib/ast/features/transform.ts b/src/lib/ast/features/transform.ts index 4f87337e..0e960e50 100644 --- a/src/lib/ast/features/transform.ts +++ b/src/lib/ast/features/transform.ts @@ -10,6 +10,7 @@ import {EnumToken} from "../types"; import {consumeWhitespace} from "../../validation/utils"; import {compute} from "../transform/compute.ts"; import {minify} from "../transform/minify.ts"; +import {decompose} from "../transform/utils.ts"; export class TransformCssFeature { @@ -62,7 +63,7 @@ export class TransformCssFeature { return; } - // const decomposed = decompose(result); + const decomposed = decompose(result); const minified = minify(result); // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); diff --git a/src/lib/ast/transform/compute.ts b/src/lib/ast/transform/compute.ts index ae6205e2..9a3e8db1 100644 --- a/src/lib/ast/transform/compute.ts +++ b/src/lib/ast/transform/compute.ts @@ -1,10 +1,12 @@ -import type {FunctionToken, LengthToken, Token} from "../../../@types/token.d.ts"; +import type {AngleToken, FunctionToken, LengthToken, NumberToken, Token} from "../../../@types/token.d.ts"; import {identity, Matrix} from "./utils.ts"; import {EnumToken} from "../types.ts"; import {length2Px} from "./convert.ts"; import {transformFunctions} from "../../syntax/index.ts"; import {stripCommaToken} from "../../validation/utils"; import {translate, translate3d, translateX, translateY, translateZ} from "./translate.ts"; +import {getAngle, getNumber} from "../../renderer/color"; +import {rotate, rotate3D} from "./rotate.ts"; export function compute(transformList: Token[]): Matrix | null { @@ -67,12 +69,12 @@ export function compute(transformList: Token[]): Matrix | null { val = length2Px(children[j] as LengthToken); - if (val == null) { + if (typeof val != 'number' || Number.isNaN(val)) { return null; } - values.push(val); + values.push(val as number); } } @@ -93,9 +95,7 @@ export function compute(transformList: Token[]): Matrix | null { } else if ((transformList[i] as FunctionToken).val == 'translateZ') { translateZ(values[0], matrix); - } - - else { + } else { // @ts-ignore translate(values as [number] | [number, number], matrix); @@ -103,81 +103,91 @@ export function compute(transformList: Token[]): Matrix | null { } break; - // case 'rotate': - // case 'rotateX': - // case 'rotateY': - // // case 'rotateZ': - // // case 'rotate3d': - // - // { - // let x: number = 0; - // let y: number = 0; - // let z: number = 0; - // - // let angle: number; - // let values: number[] = []; - // - // if ((transformList[i] as FunctionToken).val == 'rotateX' || (transformList[i] as FunctionToken).val == 'rotateY' || (transformList[i] as FunctionToken).val == 'rotateZ') { - // - // for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { - // - // if (child.typ == EnumToken.WhitespaceTokenType) { - // - // continue; - // } - // - // // if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { - // // - // // return null; - // // } - // - // if (child.typ == EnumToken.NumberTokenType && +child.val > 0) { - // - // return null; - // } - // - // angle = getAngle(child as AngleToken | NumberToken); - // - // if (angle == null) { - // - // return null; - // } - // - // values.push(angle * 2 * Math.PI); - // - // if ((transformList[i] as FunctionToken).val == 'rotateX') { - // - // x = 1; - // } else if ((transformList[i] as FunctionToken).val == 'rotateY') { - // - // y = 1; - // } else if ((transformList[i] as FunctionToken).val == 'rotateZ') { - // - // z = 1; - // } - // } - // - // if (values.length != 1) { - // - // return null; - // } - // - // console.error({values}); - // - // return null; - // } - // - // else if ((transformList[i] as FunctionToken).val == 'rotate') { - // - // } - // } - // - // break; + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + // + { + let x: number = 0; + let y: number = 0; + let z: number = 0; + + let angle: number; + let values: Token[] = []; + let valuesCount: number = (transformList[i] as FunctionToken).val == 'rotate3d' ? 4 : 1; + + if (['rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d'].includes((transformList[i] as FunctionToken).val)) { + + for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { + + if (child.typ == EnumToken.WhitespaceTokenType) { + + continue; + } + + values.push(child); + + if ((transformList[i] as FunctionToken).val == 'rotateX') { + + x = 1; + } else if ((transformList[i] as FunctionToken).val == 'rotateY') { + + y = 1; + } else if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { + + z = 1; + } + } + + // console.error({values, valuesCount}); + + if (values.length != valuesCount) { + + return null; + } + + if ((transformList[i] as FunctionToken).val == 'rotate3d') { + + x = getNumber(values[0] as NumberToken); + y = getNumber(values[1] as NumberToken); + z = getNumber(values[2] as NumberToken); + } + + angle = getAngle(values.at(-1) as AngleToken | NumberToken); + + if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { + + return null; + } + + // console.error([x, y, z, angle * 2 * Math.PI]); + + // if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { + + rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + // console.error({matrix}); + // continue; + // } + + // console.error({values}); + + // return null; + } + + // else + // if ((transformList[i] as FunctionToken).val == 'rotate') { + // + // } + } + + break; default: return null; - // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); + // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); } } diff --git a/src/lib/ast/transform/matrix.ts b/src/lib/ast/transform/matrix.ts index 89d3cf8b..ae5998a9 100644 --- a/src/lib/ast/transform/matrix.ts +++ b/src/lib/ast/transform/matrix.ts @@ -73,26 +73,28 @@ export function serialize(matrix: Matrix): Token { } } + let m: Token[] = []; - return { - typ: EnumToken.FunctionTokenType, - val: 'matrix3d', - chi: matrix[0].concat(matrix[1]).concat(matrix[2]).concat(matrix[3]).reduce((acc, t) => { + for (let i = 0; i < matrix.length; i++) { - if (acc.length > 0) { + for (let j = 0; j < matrix[i].length; j ++) { - acc.push({ typ: EnumToken.CommaTokenType }); + if (m.length > 0) { + + m.push({typ: EnumToken.CommaTokenType}) } - acc.push({ + m.push({ typ: EnumToken.NumberTokenType, - val: reduceNumber(t) - - }); + val: reduceNumber(matrix[j][i]) + }) + } + } - return acc; - return acc; - }, [] as Token[]) + return { + typ: EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: m } } \ No newline at end of file diff --git a/src/lib/ast/transform/minify.ts b/src/lib/ast/transform/minify.ts index 6ce36666..b46eb7bb 100644 --- a/src/lib/ast/transform/minify.ts +++ b/src/lib/ast/transform/minify.ts @@ -1,6 +1,7 @@ import {decompose, Matrix} from "./utils.ts"; import {EnumToken} from "../types.ts"; import {Token} from "../../../@types"; +import {gcd} from "../math/math.ts"; export function minify(matrix: Matrix): Token[] | null { @@ -13,6 +14,10 @@ export function minify(matrix: Matrix): Token[] | null { } const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + const rotations = new Set(['x', 'y', 'z']); + const scales = new Set(['x', 'y', 'z']); + + const result: Token[] = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { @@ -35,18 +40,18 @@ export function minify(matrix: Matrix): Token[] | null { transforms.delete('perspective'); } - if (transforms.size == 0) { - - // identity - return [{ - typ: EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - {typ: EnumToken.NumberTokenType, val: '1'} - ] - } - ]; - } + // if (transforms.size == 0) { + // + // // identity + // return [{ + // typ: EnumToken.FunctionTokenType, + // val: 'scale', + // chi: [ + // {typ: EnumToken.NumberTokenType, val: '1'} + // ] + // } + // ]; + // } if (transforms.size == 1) { @@ -64,7 +69,7 @@ export function minify(matrix: Matrix): Token[] | null { if (coordinates.size == 3) { - return [{ + result.push({ typ: EnumToken.FunctionTokenType, val: 'translate', chi: [ @@ -74,33 +79,30 @@ export function minify(matrix: Matrix): Token[] | null { {typ: EnumToken.CommaTokenType}, {typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px'} ] - }] - } - - if (coordinates.size == 1) { + }) + } else if (coordinates.size == 1) { if (coordinates.has('x')) { - return [{ + result.push({ typ: EnumToken.FunctionTokenType, val: 'translate', chi: [{typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}] - }] - } + }); + } else { - let axis: string = coordinates.has('y') ? 'y' : 'z'; - let index: number = axis == 'y' ? 1 : 2; + let axis: string = coordinates.has('y') ? 'y' : 'z'; + let index: number = axis == 'y' ? 1 : 2; - return [{ - typ: EnumToken.FunctionTokenType, - val: 'translate' + axis.toUpperCase(), - chi: [{typ: EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px'}] - }] - } - - if (coordinates.has('z')) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate' + axis.toUpperCase(), + chi: [{typ: EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px'}] + }); + } + } else if (coordinates.has('z')) { - return [{ + result.push({ typ: EnumToken.FunctionTokenType, val: 'translate', chi: [ @@ -116,26 +118,203 @@ export function minify(matrix: Matrix): Token[] | null { {typ: EnumToken.CommaTokenType}, {typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px'} ] - }] + }); + } else { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'} + ] + }); + } + } + } + + { + + const {x, y, z, angle} = getRotation3D(matrix); + + // console.error({x, y, z, angle}); + + if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { + + if (y == 0 && z == 0) { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotateX', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } else if (x == 0 && z == 0) { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotateY', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } else if (x == 0 && y == 0) { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotate', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } else { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotate3d', + chi: [ + { + typ: EnumToken.NumberTokenType, + val: '' + x + }, + {typ: EnumToken.CommaTokenType}, + { + typ: EnumToken.NumberTokenType, + val: '' + y + }, + {typ: EnumToken.CommaTokenType}, + { + typ: EnumToken.NumberTokenType, + val: '' + z + }, + {typ: EnumToken.CommaTokenType}, + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); } + } + } + + // scale(x, x) -> scale(x) + // scale(1, sy) -> scaleY(sy) + // scale3d(1, 1, sz) -> scaleZ(sz) + // scaleX() scale(Y) scaleZ() -> scale3d() + + // identity + return result.length == 0 ? [ + { + typ: EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + {typ: EnumToken.NumberTokenType, val: '1'} + ] + } + ] : result; +} + +// Fonction pour calculer rotate3d à partir de matrix3d +function getRotation3D(matrix: Matrix): { x: number, y: number, z: number, angle: number } { + + // Extraire la sous-matrice 3x3 de rotation + const r11: number = matrix[0][0], r12: number = matrix[0][1], r13: number = matrix[0][2]; + const r21: number = matrix[1][0], r22: number = matrix[1][1], r23: number = matrix[1][2]; + const r31: number = matrix[2][0], r32: number = matrix[2][1], r33: number = matrix[2][2]; + + // Calculer la trace (somme des éléments diagonaux) + const trace: number = r11 + r22 + r33; + + // Calculer l’angle de rotation (en radians) + const cosTheta: number = (trace - 1) / 2; + + // Calculer sin(θ) avec le signe correct + const sinThetaRaw: number = Math.sqrt(1 - cosTheta * cosTheta); + const xRaw: number = r32 - r23; // -0.467517 + const yRaw: number = r13 - r31; // 0.776535 + const zRaw: number = r21 - r12; // 0.776535 + // Déterminer le signe de sin(θ) basé sur la direction + const sinTheta: number = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; + + // Calculer l’angle avec atan2 + const angle: number = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(12); + let x, y, z; + + if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° + + const x1: number = r32 - r23; + const y1: number = r13 - r31; + const z1: number = r21 - r12; + + switch (Math.max(x1, y1, z1)) { + + case x1: + + x = 1; + y = 0; + z = 0; + break - return [{ - typ: EnumToken.FunctionTokenType, - val: 'translate', - chi: [ - decomposed.translate[0] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, - {typ: EnumToken.CommaTokenType}, - decomposed.translate[1] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'} - ] - }] + case y1: + + x = 0; + y = 1; + z = 0; + break; + + default: + + x = 0; + y = 0; + z = 1; + break; } + + } else { + x = (r32 - r23) / (2 * sinTheta); + y = (r13 - r31) / (2 * sinTheta); + z = (r21 - r12) / (2 * sinTheta); + } + + // Normaliser le vecteur (optionnel, mais utile pour vérification) + const length: number = Math.sqrt(x * x + y * y + z * z); + + if (length > 0) { + x /= length; + y /= length; + z /= length; + } + + const x1: number = gcd(x, gcd(y, z)); + + if (x1 != 0) { + + x /= x1; + y /= x1; + z /= x1; } - return null; + return {x, y, z, angle}; } \ No newline at end of file diff --git a/src/lib/ast/transform/rotate.ts b/src/lib/ast/transform/rotate.ts index a3dd1da0..00845b37 100644 --- a/src/lib/ast/transform/rotate.ts +++ b/src/lib/ast/transform/rotate.ts @@ -35,41 +35,12 @@ export function rotate3D(angle: number, x: number, y: number, z: number, matrix: return matrix; } -// export function rotateX(x: number): Matrix { -// -// const matrix: Matrix = identity(); -// -// const angle: number = x * Math.PI / 180; -// -// matrix[1][1] = Math.cos(angle); -// matrix[1][2] = Math.sin(angle); -// matrix[0][2] = -1; -// -// return matrix; -// } -// -// export function rotateY(y: number): Matrix { -// -// const matrix: Matrix = identity(); -// -// const angle: number = y * Math.PI / 180; -// matrix[0][0] = matrix[2][2] = Math.cos(angle); -// matrix[0][1] = matrix[2][0] = Math.sin(angle); -// matrix[0][2] = -1; -// -// return matrix; -// } -// -// export function rotateZ(z: number): Matrix { -// -// const matrix: Matrix = identity(); -// -// const angle: number = z * Math.PI / 180; -// matrix[0][0] = matrix[1][1] = Math.cos(angle); -// matrix[0][1] = matrix[1][0] = -Math.sin(angle); -// matrix[2][0] = -1; -// -// return matrix; -// } -// -// export const rotate = rotateZ; \ No newline at end of file +export function rotate(angle: number, matrix: Matrix): Matrix { + + matrix[0][0] = Math.cos(angle); + matrix[0][1] = Math.sin(angle); + matrix[1][0] = -Math.sin(angle); + matrix[1][1] = Math.cos(angle); + + return matrix; +} \ No newline at end of file diff --git a/src/lib/ast/transform/utils.ts b/src/lib/ast/transform/utils.ts index dd1fcdf1..2abd0543 100644 --- a/src/lib/ast/transform/utils.ts +++ b/src/lib/ast/transform/utils.ts @@ -5,6 +5,7 @@ export declare type Matrix = [Vector, Vector, Vector, Vector]; interface DecomposedMatrix3D { skew: [number, number, number]; scale: [number, number, number]; + // rotate: [number, number, number]; translate : [number, number, number]; perspective : [number, number, number, number]; quaternion : [number, number, number, number]; @@ -296,9 +297,37 @@ export function decompose(matrix: Matrix): DecomposedMatrix3D | null { quaternion[2] = -quaternion[2]; } + // const rad2deg = 180 / Math.PI; + // const theta: number = Math.atan2(matrix[1][0], matrix[0][0]) * rad2deg; + // const zAxis = + // + // console.error({theta}); + // + // const rotation: [number, number, number] = [-Math.asin(matrix[0][2]) * rad2deg, 0, 0]; + +// apply rotation + let x = quaternion[0]; + let y = quaternion[1]; + let z = quaternion[2]; + let w = quaternion[3]; + + const rotationMatrix: Matrix = identity(); +// Construct a composite rotation matrix from the quaternion values +// rotationMatrix is an identity 4x4 matrix initially + rotationMatrix[0][0] = 1 - 2 * (y * y + z * z); + rotationMatrix[0][1] = 2 * (x * y - z * w); + rotationMatrix[0][2] = 2 * (x * z + y * w); + rotationMatrix[1][0] = 2 * (x * y + z * w); + rotationMatrix[1][1] = 1 - 2 * (x * x + z * z); + rotationMatrix[1][2] = 2 * (y * z - x * w); + rotationMatrix[2][0] = 2 * (x * z - y * w); + rotationMatrix[2][1] = 2 * (y * z + x * w); + rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); + return { skew, scale, + // rotate: [+rx.toPrecision(12), +ry.toPrecision(12), +rz.toPrecision(12)], translate, perspective, quaternion @@ -344,6 +373,7 @@ export function recompose( rotationMatrix[2][0] = 2 * (x * z - y * w); rotationMatrix[2][1] = 2 * (y * z + x * w); rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); + // console.error({rotationMatrix}); matrix = multiply(matrix, rotationMatrix); diff --git a/src/lib/validation/at-rules/media.ts b/src/lib/validation/at-rules/media.ts index 05651c50..cca892c1 100644 --- a/src/lib/validation/at-rules/media.ts +++ b/src/lib/validation/at-rules/media.ts @@ -332,7 +332,7 @@ export function validateMediaCondition(token: Token, atRule: AstAtRule): boolean return chi[0].l.typ == EnumToken.IdenTokenType; } - console.error(chi[0].parent); + // console.error(chi[0].parent); return false; } diff --git a/test/specs/code/malformed.js b/test/specs/code/malformed.js index 9892312c..76c35c24 100644 --- a/test/specs/code/malformed.js +++ b/test/specs/code/malformed.js @@ -173,7 +173,7 @@ color: preserveLicense: true }).code).equals(`a { color: cyan; - transform: rotate(3.1416rad) + transform: rotate(179.999579080068deg) }`)); }); }); diff --git a/test/specs/code/transform.js b/test/specs/code/transform.js index 977eac80..696552c5 100644 --- a/test/specs/code/transform.js +++ b/test/specs/code/transform.js @@ -9,7 +9,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: translateY(-100px) }`)); @@ -23,7 +23,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: translate(-100px) }`)); @@ -37,7 +37,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: translate(-100px) }`)); @@ -51,7 +51,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: translateZ(-100px) }`)); @@ -65,7 +65,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: scale(1) }`)); @@ -79,7 +79,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: scale(1) }`)); @@ -93,7 +93,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: translateZ(14px) }`)); @@ -107,7 +107,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: translate(14px,0,14px) }`)); @@ -115,4 +115,50 @@ export function run(describe, expect, transform, parse, render, dirname, readFil }); + describe('CSS rotate', function () { + + it('rotate3d #8', function () { + const nesting1 = ` + + .now { + transform: rotate3d(1, 0, 0, 45deg); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: rotateX(45deg) +}`)); + }); + + it('rotateZ #9', function () { + const nesting1 = ` + + .now { + transform: rotateZ(45deg); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: rotate(45deg) +}`)); + }); + + + it('rotateZ #9', function () { + const nesting1 = ` + + .now { + transform: rotate3d(2, -1, -1, -0.2turn); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: rotate3d(2,-1,-1,-72deg) +}`)); + }); + + }); } \ No newline at end of file From c4366975fd4be1b3e47cead0ab2f561707b6f4b5 Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Sat, 5 Apr 2025 16:12:59 -0400 Subject: [PATCH 05/10] compute transform matrix #75 --- dist/index-umd-web.js | 859 ++++++++++++++++++---------- dist/index.cjs | 859 ++++++++++++++++++---------- dist/lib/ast/features/transform.js | 31 +- dist/lib/ast/math/math.js | 5 + dist/lib/ast/transform/compute.js | 197 +++++-- dist/lib/ast/transform/matrix.js | 50 ++ dist/lib/ast/transform/minify.js | 260 ++++----- dist/lib/ast/transform/rotate.js | 40 ++ dist/lib/ast/transform/scale.js | 38 ++ dist/lib/ast/transform/translate.js | 22 +- dist/lib/ast/transform/utils.js | 123 +++- src/lib/ast/features/transform.ts | 44 +- src/lib/ast/math/math.ts | 7 + src/lib/ast/transform/compute.ts | 235 ++++++-- src/lib/ast/transform/matrix.ts | 12 +- src/lib/ast/transform/minify.ts | 300 +++++----- src/lib/ast/transform/rotate.ts | 40 +- src/lib/ast/transform/scale.ts | 47 +- src/lib/ast/transform/translate.ts | 23 +- src/lib/ast/transform/utils.ts | 276 +++++++-- test/specs/code/malformed.js | 2 +- test/specs/code/transform.js | 46 +- 22 files changed, 2385 insertions(+), 1131 deletions(-) create mode 100644 dist/lib/ast/transform/matrix.js create mode 100644 dist/lib/ast/transform/rotate.js create mode 100644 dist/lib/ast/transform/scale.js diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index e5585bbb..82f4b487 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -2484,6 +2484,11 @@ if (x == 0 || y == 0) { return 1; } + if (y > x) { + t = x; + x = y; + y = t; + } while (y) { t = y; y = x % y; @@ -17717,6 +17722,9 @@ return norm === 0 ? [0, 0, 0] : [x / norm, y / norm, z / norm]; } function dot(point1, point2) { + if (point1.length === 4 && point2.length === 4) { + return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2] + point1[3] * point2[3]; + } return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2]; } function combine(point1, point2, ascl, bscl) { @@ -17725,6 +17733,17 @@ function cross(a, b) { return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; } + function multiply(matrixA, matrixB) { + let result = Array(4).fill(0).map(() => Array(4).fill(0)); + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + for (let k = 0; k < 4; k++) { + result[j][i] += matrixA[k][i] * matrixB[j][k]; + } + } + } + return result; + } function decompose(matrix) { // Normalize the matrix. if (matrix[3][3] === 0) { @@ -17826,13 +17845,6 @@ if (row[1][0] > row[0][1]) { quaternion[2] = -quaternion[2]; } - // const rad2deg = 180 / Math.PI; - // const theta: number = Math.atan2(matrix[1][0], matrix[0][0]) * rad2deg; - // const zAxis = - // - // console.error({theta}); - // - // const rotation: [number, number, number] = [-Math.asin(matrix[0][2]) * rad2deg, 0, 0]; // apply rotation let x = quaternion[0]; let y = quaternion[1]; @@ -17850,15 +17862,105 @@ rotationMatrix[2][0] = 2 * (x * z - y * w); rotationMatrix[2][1] = 2 * (y * z + x * w); rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); + const { x: x1, y: y1, z: z1, angle } = getRotation3D(rotationMatrix); return { - skew, - scale, - // rotate: [+rx.toPrecision(12), +ry.toPrecision(12), +rz.toPrecision(12)], - translate, + skew: toZero(skew), + scale: toZero(scale), + rotate: toZero([x1, y1, z1, angle]), + translate: toZero(translate), perspective, quaternion }; } + function toZero(v) { + for (let i = 0; i < v.length; i++) { + if (Math.abs(v[i]) <= 1e-6) { + v[i] = 0; + } + else { + v[i] = +v[i].toPrecision(6); + } + } + return v; + } + // Fonction pour calculer rotate3d à partir de matrix3d + function getRotation3D(matrix) { + // Extraire la sous-matrice 3x3 de rotation + const r11 = matrix[0][0], r12 = matrix[0][1], r13 = matrix[0][2]; + const r21 = matrix[1][0], r22 = matrix[1][1], r23 = matrix[1][2]; + const r31 = matrix[2][0], r32 = matrix[2][1], r33 = matrix[2][2]; + // Calculer la trace (somme des éléments diagonaux) + const trace = r11 + r22 + r33; + // Calculer l’angle de rotation (en radians) + const cosTheta = (trace - 1) / 2; + // Calculer sin(θ) avec le signe correct + const sinThetaRaw = Math.sqrt(1 - cosTheta * cosTheta); + const xRaw = r32 - r23; // -0.467517 + const yRaw = r13 - r31; // 0.776535 + const zRaw = r21 - r12; // 0.776535 + // Déterminer le signe de sin(θ) basé sur la direction + const sinTheta = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; + // Calculer l’angle avec atan2 + const angle = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(6); + let x = 0, y = 0, z = 0; + if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° + const x1 = +r11.toPrecision(6); + const y1 = +r22.toPrecision(6); + const z1 = +r33.toPrecision(6); + const max = Math.max(x1, y1, z1); + x = y = z = 0; + if (max === x1) { + x = 1; + } + if (max === y1) { + y = 1; + } + if (max === z1) { + z = 1; + } + } + else { + x = (r32 - r23) / (2 * sinTheta); + y = (r13 - r31) / (2 * sinTheta); + z = (r21 - r12) / (2 * sinTheta); + } + // Normaliser le vecteur (optionnel, mais utile pour vérification) + const length = Math.sqrt(x * x + y * y + z * z); + if (length > 0) { + x /= length; + y /= length; + z /= length; + } + const pc = Math.abs(gcd(x, gcd(y, z))); + if (pc > 0.1 && pc <= Math.abs(x) && pc <= Math.abs(y) && pc <= Math.abs(z)) { + x /= pc; + y /= pc; + z /= pc; + } + else { + const min = Math.min(Math.abs(x), Math.abs(y), Math.abs(z)); + if (min > 0.1) { + x = +(x / min).toPrecision(6); + y = +(y / min).toPrecision(6); + z = +(z / min).toPrecision(6); + } + } + return { x, y, z, angle }; + } + // https://drafts.csswg.org/css-transforms-1/#2d-matrix + function is2DMatrix(matrix) { + // m13,m14, m23, m24, m31, m32, m34, m43 are all 0 + return matrix[0][2] === 0 && + matrix[0][3] === 0 && + matrix[1][2] === 0 && + matrix[1][3] === 0 && + matrix[2][0] === 0 && + matrix[2][1] === 0 && + matrix[2][3] === 0 && + matrix[3][2] === 0 && + matrix[2][2] === 1 && + matrix[3][3] === 1; + } // https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#absolute_length_units function length2Px(value) { @@ -17890,23 +17992,27 @@ return null; } - function translateX(x, matrix) { + function translateX(x, from) { + const matrix = identity(); matrix[3][0] = x; - return matrix; + return multiply(from, matrix); } - function translateY(y, matrix) { + function translateY(y, from) { + const matrix = identity(); matrix[3][1] = y; - return matrix; + return multiply(from, matrix); } - function translateZ(z, matrix) { + function translateZ(z, from) { + const matrix = identity(); matrix[3][2] = z; - return matrix; + return multiply(from, matrix); } - function translate(translate, matrix) { + function translate(translate, from) { + const matrix = identity(); matrix[3][0] = translate[0]; matrix[3][1] = translate[1] ?? 0; matrix[3][2] = translate[2] ?? 0; - return matrix; + return multiply(from, matrix); } /** @@ -17915,9 +18021,10 @@ * @param x * @param y * @param z - * @param matrix + * @param from */ - function rotate3D(angle, x, y, z, matrix) { + function rotate3D(angle, x, y, z, from) { + const matrix = identity(); const sc = Math.sin(angle / 2) * Math.cos(angle / 2); const sq = Math.sin(angle / 2) * Math.sin(angle / 2); const norm = Math.sqrt(x * x + y * y + z * z); @@ -17926,151 +18033,70 @@ y *= unit; z *= unit; matrix[0][0] = 1 - 2 * (y * y + z * z) * sq; - matrix[0][1] = 2 * (x * y * sq - z * sc); - matrix[0][2] = 2 * (x * z * sq + y * sc); - matrix[1][0] = 2 * (x * y * sq + z * sc); + matrix[0][1] = 2 * (x * y * sq + z * sc); + matrix[0][2] = 2 * (x * z * sq - y * sc); + matrix[1][0] = 2 * (x * y * sq - z * sc); matrix[1][1] = 1 - 2 * (x * x + z * z) * sq; - matrix[1][2] = 2 * (y * z * sq - x * sc); - matrix[2][0] = 2 * (x * z * sq - y * sc); - matrix[2][1] = 2 * (y * z * sq + x * sc); + matrix[1][2] = 2 * (y * z * sq + x * sc); + matrix[2][0] = 2 * (x * z * sq + y * sc); + matrix[2][1] = 2 * (y * z * sq - x * sc); matrix[2][2] = 1 - 2 * (x * x + y * y) * sq; - return matrix; + return multiply(from, matrix); + } + function rotate(angle, from) { + const matrix = identity(); + matrix[0][0] = Math.cos(angle); + matrix[0][1] = Math.sin(angle); + matrix[1][0] = -Math.sin(angle); + matrix[1][1] = Math.cos(angle); + return multiply(from, matrix); } - function compute(transformList) { - transformList = transformList.slice(); - stripCommaToken(transformList); - if (transformList.length == 0) { - return null; - } + function scaleX(x, from) { const matrix = identity(); - let values = []; - let val; - for (let i = 0; i < transformList.length; i++) { - if (transformList[i].typ == exports.EnumToken.WhitespaceTokenType) { - continue; - } - if (transformList[i].typ != exports.EnumToken.FunctionTokenType || !transformFunctions.includes(transformList[i].val)) { - return null; - } - switch (transformList[i].val) { - case 'translate': - case 'translateX': - case 'translateY': - case 'translateZ': - case 'translate3d': - { - values.length = 0; - const children = stripCommaToken(transformList[i].chi.slice()); - if (children == null || children.length == 0) { - return null; - } - const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; - // console.error([(transformList[i] as FunctionToken).val, valCount]); - if (children.length == 1 && children[0].typ == exports.EnumToken.IdenTokenType && children[0].val == 'none') { - values.fill(0, 0, valCount); - } - else { - for (let j = 0; j < children.length; j++) { - if (children[j].typ == exports.EnumToken.WhitespaceTokenType) { - continue; - } - val = length2Px(children[j]); - if (typeof val != 'number' || Number.isNaN(val)) { - return null; - } - values.push(val); - } - } - if (values.length == 0 || values.length > valCount) { - return null; - } - if (transformList[i].val == 'translateX') { - translateX(values[0], matrix); - } - else if (transformList[i].val == 'translateY') { - translateY(values[0], matrix); - } - else if (transformList[i].val == 'translateZ') { - translateZ(values[0], matrix); - } - else { - // @ts-ignore - translate(values, matrix); - } - } - break; - case 'rotate': - case 'rotateX': - case 'rotateY': - case 'rotateZ': - case 'rotate3d': - // - { - let x = 0; - let y = 0; - let z = 0; - let angle; - let values = []; - let valuesCount = transformList[i].val == 'rotate3d' ? 4 : 1; - if (['rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d'].includes(transformList[i].val)) { - for (const child of stripCommaToken(transformList[i].chi.slice())) { - if (child.typ == exports.EnumToken.WhitespaceTokenType) { - continue; - } - values.push(child); - if (transformList[i].val == 'rotateX') { - x = 1; - } - else if (transformList[i].val == 'rotateY') { - y = 1; - } - else if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { - z = 1; - } - } - // console.error({values, valuesCount}); - if (values.length != valuesCount) { - return null; - } - if (transformList[i].val == 'rotate3d') { - x = getNumber(values[0]); - y = getNumber(values[1]); - z = getNumber(values[2]); - } - angle = getAngle(values.at(-1)); - if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { - return null; - } - // console.error([x, y, z, angle * 2 * Math.PI]); - // if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { - rotate3D(angle * 2 * Math.PI, x, y, z, matrix); - // console.error({matrix}); - // continue; - // } - // console.error({values}); - // return null; - } - // else - // if ((transformList[i] as FunctionToken).val == 'rotate') { - // - // } - } - break; - default: - return null; - // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); - } - } - return matrix; + matrix[0][0] = x; + // matrix[1][1] = 1; + // matrix[2][2] = 1; + return multiply(from, matrix); + } + function scaleY(y, from) { + const matrix = identity(); + // matrix[0][0] = 1; + matrix[1][1] = y; + // matrix[2][2] = 1; + return multiply(from, matrix); + } + function scaleZ(z, from) { + const matrix = identity(); + // matrix[0][0] = 1; + // matrix[1][1] = 1; + matrix[2][2] = z; + return multiply(from, matrix); + } + function scale(x, y, from) { + const matrix = identity(); + matrix[0][0] = x; + matrix[1][1] = y; + return multiply(from, matrix); + } + function scale3d(x, y, z, from) { + const matrix = identity(); + matrix[0][0] = x; + matrix[1][1] = y; + matrix[2][2] = z; + return multiply(from, matrix); } function minify$1(matrix) { const decomposed = decompose(matrix); + // console.error({decomposed}); if (decomposed == null) { return null; } - const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + const transforms = new Set([ + 'translate', 'scale', 'skew', 'perspective', 'rotate' + ]); + const scales = new Set(['x', 'y', 'z']); const result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { @@ -18085,97 +18111,138 @@ if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { transforms.delete('perspective'); } - // if (transforms.size == 0) { - // - // // identity - // return [{ - // typ: EnumToken.FunctionTokenType, - // val: 'scale', - // chi: [ - // {typ: EnumToken.NumberTokenType, val: '1'} - // ] - // } - // ]; - // } - if (transforms.size == 1) { - if (transforms.has('translate')) { - let coordinates = new Set(['x', 'y', 'z']); - for (let i = 0; i < 3; i++) { - if (decomposed.translate[i] == 0) { - coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); - } - } - if (coordinates.size == 3) { + if (decomposed.rotate[3] == 0) { + transforms.delete('rotate'); + } + if (transforms.has('translate')) { + let coordinates = new Set(['x', 'y', 'z']); + for (let i = 0; i < 3; i++) { + if (decomposed.translate[i] == 0) { + coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); + } + } + if (coordinates.size == 3) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); + } + else if (coordinates.size == 1) { + if (coordinates.has('x')) { result.push({ typ: exports.EnumToken.FunctionTokenType, val: 'translate', + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] + }); + } + else { + let axis = coordinates.has('y') ? 'y' : 'z'; + let index = axis == 'y' ? 1 : 2; + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate' + axis.toUpperCase(), + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] + }); + } + } + else if (coordinates.has('z')) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } + ] + }); + } + } + // scale(x, x) -> scale(x) + // scale(1, sy) -> scaleY(sy) + // scale3d(1, 1, sz) -> scaleZ(sz) + // scaleX() scale(Y) scaleZ() -> scale3d() + if (transforms.has('scale')) { + const [sx, sy, sz] = decomposed.scale; + if (!(sx == 1 && sy == 1 && sz == 1)) { + if (sz == 1) { + scales.delete('z'); + } + if (sy == 1) { + scales.delete('y'); + } + if (sx == 1) { + scales.delete('x'); + } + if (scales.size == 1) { + let prefix = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale' + prefix, chi: [ - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + { typ: exports.EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx) } ] }); } - else if (coordinates.size == 1) { - if (coordinates.has('x')) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'translate', - chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] - }); - } - else { - let axis = coordinates.has('y') ? 'y' : 'z'; - let index = axis == 'y' ? 1 : 2; - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'translate' + axis.toUpperCase(), - chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] - }); - } - } - else if (coordinates.has('z')) { + else if (!scales.has('z')) { result.push({ typ: exports.EnumToken.FunctionTokenType, - val: 'translate', + val: 'scale', chi: [ - decomposed.translate[0] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - decomposed.translate[1] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, + { typ: exports.EnumToken.WhitespaceTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, ] }); } else { result.push({ typ: exports.EnumToken.FunctionTokenType, - val: 'translate', + val: 'scale3d', chi: [ - decomposed.translate[0] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, { typ: exports.EnumToken.CommaTokenType }, - decomposed.translate[1] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } + { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sz } ] }); } } } - { - const { x, y, z, angle } = getRotation3D(matrix); + if (transforms.has('rotate')) { + const [x, y, z, angle] = decomposed.rotate; // console.error({x, y, z, angle}); if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { if (y == 0 && z == 0) { @@ -18247,82 +18314,276 @@ } } } - // scale(x, x) -> scale(x) - // scale(1, sy) -> scaleY(sy) - // scale3d(1, 1, sz) -> scaleZ(sz) - // scaleX() scale(Y) scaleZ() -> scale3d() // identity - return result.length == 0 ? [ + return transforms.size > 1 ? null : result.length == 0 || eq(result, identity()) ? [ { + typ: exports.EnumToken.IdenTokenType, + val: 'none' + } + ] : result; + } + + function serialize(matrix) { + if (is2DMatrix(matrix)) { + // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset + return { typ: exports.EnumToken.FunctionTokenType, - val: 'scale', + val: 'matrix', chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '1' } - ] + matrix[0][0], + matrix[1][1], + matrix[1][0], + matrix[1][1], + matrix[3][0], + matrix[3][1] + ].reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(t.toPrecision(6)) + }); + return acc; + }, []) + }; + } + let m = []; + // console.error(JSON.stringify({matrix},null, 1)); + for (let i = 0; i < matrix.length; i++) { + for (let j = 0; j < matrix[i].length; j++) { + if (m.length > 0) { + m.push({ typ: exports.EnumToken.CommaTokenType }); + } + m.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(matrix[i][j].toPrecision(6)) + }); } - ] : result; + } + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: m + }; } - // Fonction pour calculer rotate3d à partir de matrix3d - function getRotation3D(matrix) { - // Extraire la sous-matrice 3x3 de rotation - const r11 = matrix[0][0], r12 = matrix[0][1], r13 = matrix[0][2]; - const r21 = matrix[1][0], r22 = matrix[1][1], r23 = matrix[1][2]; - const r31 = matrix[2][0], r32 = matrix[2][1], r33 = matrix[2][2]; - // Calculer la trace (somme des éléments diagonaux) - const trace = r11 + r22 + r33; - // Calculer l’angle de rotation (en radians) - const cosTheta = (trace - 1) / 2; - // Calculer sin(θ) avec le signe correct - const sinThetaRaw = Math.sqrt(1 - cosTheta * cosTheta); - const xRaw = r32 - r23; // -0.467517 - const yRaw = r13 - r31; // 0.776535 - const zRaw = r21 - r12; // 0.776535 - // Déterminer le signe de sin(θ) basé sur la direction - const sinTheta = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; - // Calculer l’angle avec atan2 - const angle = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(12); - let x, y, z; - if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° - const x1 = r32 - r23; - const y1 = r13 - r31; - const z1 = r21 - r12; - switch (Math.max(x1, y1, z1)) { - case x1: - x = 1; - y = 0; - z = 0; + + function compute(transformLists) { + transformLists = transformLists.slice(); + stripCommaToken(transformLists); + if (transformLists.length == 0) { + return null; + } + const tokens = []; + let matrix; + for (const transformList of splitTransformList(transformLists)) { + matrix = computeMatrix(transformList); + if (matrix == null) { + return null; + } + tokens.push(matrix); + } + // for (let i = 0; i < tokens.length; i++) { + // + // for (let j = 0; j < tokens[i].length; j++) { + // + // toZero(tokens[i][j]); + // } + // } + return tokens.reduce((acc, t) => acc.concat(minify$1(t) ?? serialize(t)), []); + } + function computeMatrix(transformList) { + let matrix = identity(); + let values = []; + let val; + let i = 0; + for (; i < transformList.length; i++) { + if (transformList[i].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (transformList[i].typ != exports.EnumToken.FunctionTokenType || !transformFunctions.includes(transformList[i].val)) { + return null; + } + switch (transformList[i].val) { + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': + { + values.length = 0; + const children = stripCommaToken(transformList[i].chi.slice()); + if (children == null || children.length == 0) { + return null; + } + const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; + if (children.length == 1 && children[0].typ == exports.EnumToken.IdenTokenType && children[0].val == 'none') { + values.fill(0, 0, valCount); + } + else { + for (let j = 0; j < children.length; j++) { + if (children[j].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + val = length2Px(children[j]); + if (typeof val != 'number' || Number.isNaN(val)) { + return null; + } + values.push(val); + } + } + if (values.length == 0 || values.length > valCount) { + return null; + } + if (transformList[i].val == 'translateX') { + matrix = translateX(values[0], matrix); + } + else if (transformList[i].val == 'translateY') { + matrix = translateY(values[0], matrix); + } + else if (transformList[i].val == 'translateZ') { + matrix = translateZ(values[0], matrix); + } + else { + // @ts-ignore + matrix = translate(values, matrix); + } + } break; - case y1: - x = 0; - y = 1; - z = 0; + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + { + let x = 0; + let y = 0; + let z = 0; + let angle; + let values = []; + let valuesCount = transformList[i].val == 'rotate3d' ? 4 : 1; + for (const child of stripCommaToken(transformList[i].chi.slice())) { + if (child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + values.push(child); + if (transformList[i].val == 'rotateX') { + x = 1; + } + else if (transformList[i].val == 'rotateY') { + y = 1; + } + else if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { + z = 1; + } + } + if (values.length != valuesCount) { + return null; + } + if (transformList[i].val == 'rotate3d') { + x = getNumber(values[0]); + y = getNumber(values[1]); + z = getNumber(values[2]); + } + angle = getAngle(values.at(-1)); + if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { + return null; + } + if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { + matrix = rotate(angle * 2 * Math.PI, matrix); + } + else { + matrix = rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + } + } break; - default: - x = 0; - y = 0; - z = 1; + case 'scale': + case 'scaleX': + case 'scaleY': + case 'scaleZ': + case 'scale3d': { + values.length = 0; + let child; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (child.typ != exports.EnumToken.NumberTokenType) { + return null; + } + values.push(getNumber(child)); + } + if (values.length == 0) { + return null; + } + if (transformList[i].val == 'scale3d') { + if (values.length != 3) { + return null; + } + matrix = scale3d(...values, matrix); + break; + } + if (transformList[i].val == 'scale') { + if (values.length != 1 && values.length != 2) { + return null; + } + matrix = scale(values[0], values[1] ?? values[0], matrix); + break; + } + if (values.length != 1) { + return null; + } + else if (transformList[i].val == 'scaleX') { + matrix = scaleX(values[0], matrix); + } + else if (transformList[i].val == 'scaleY') { + matrix = scaleY(values[0], matrix); + } + else if (transformList[i].val == 'scaleZ') { + matrix = scaleZ(values[0], matrix); + } break; + } + default: + return null; + // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); } } - else { - x = (r32 - r23) / (2 * sinTheta); - y = (r13 - r31) / (2 * sinTheta); - z = (r21 - r12) / (2 * sinTheta); - } - // Normaliser le vecteur (optionnel, mais utile pour vérification) - const length = Math.sqrt(x * x + y * y + z * z); - if (length > 0) { - x /= length; - y /= length; - z /= length; - } - const x1 = gcd(x, gcd(y, z)); - if (x1 != 0) { - x /= x1; - y /= x1; - z /= x1; + return matrix; + } + function splitTransformList(transformList) { + let pattern = null; + const tokens = []; + for (let i = 0; i < transformList.length; i++) { + if (transformList[i].typ == exports.EnumToken.CommentTokenType || transformList[i].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (pattern == null || (transformList[i].typ == exports.EnumToken.FunctionTokenType && !transformList[i].val.startsWith(pattern))) { + if (transformList[i].typ == exports.EnumToken.FunctionTokenType) { + if (transformList[i].val.startsWith('scale')) { + pattern = 'scale'; + } + else if (transformList[i].val.startsWith('rotate')) { + pattern = 'rotate'; + } + else if (transformList[i].val.startsWith('translate')) { + pattern = 'translate'; + } + else { + pattern = null; + } + tokens.push([transformList[i]]); + continue; + } + } + if (pattern != null && transformList[i].typ == exports.EnumToken.FunctionTokenType && transformList[i].val.startsWith(pattern)) { + tokens[tokens.length - 1].push(transformList[i]); + continue; + } + tokens.push([transformList[i]]); } - return { x, y, z, angle }; + return tokens; } class TransformCssFeature { @@ -18352,17 +18613,27 @@ } const children = node.val.slice(); consumeWhitespace(children); - const result = compute(children); + let result = compute(children); if (result == null) { - // console.error({result}); return; } - decompose(result); - const minified = minify$1(result); - // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); - if (minified != null) { - node.val = minified; - } + // console.error(JSON.stringify({result}, null, 1)); + // console.error({result, t: result.map(t => minify(t) ?? t + // )}); + // console.error({result: decompose2(result)}); + // const decomposed =decompose(result); + // const minified = minify(result); + const matrix = computeMatrix(result); + if (matrix != null) { + const m = serialize(matrix); + if (renderToken(m).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = [m]; + } + } + // console.error({result, serialized: renderToken()}); + // if (minified != null) { + node.val = result; + // } } } } diff --git a/dist/index.cjs b/dist/index.cjs index d24333f0..8ac1e834 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -2483,6 +2483,11 @@ function gcd(x, y) { if (x == 0 || y == 0) { return 1; } + if (y > x) { + t = x; + x = y; + y = t; + } while (y) { t = y; y = x % y; @@ -17816,6 +17821,9 @@ function normalize(point) { return norm === 0 ? [0, 0, 0] : [x / norm, y / norm, z / norm]; } function dot(point1, point2) { + if (point1.length === 4 && point2.length === 4) { + return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2] + point1[3] * point2[3]; + } return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2]; } function combine(point1, point2, ascl, bscl) { @@ -17824,6 +17832,17 @@ function combine(point1, point2, ascl, bscl) { function cross(a, b) { return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; } +function multiply(matrixA, matrixB) { + let result = Array(4).fill(0).map(() => Array(4).fill(0)); + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + for (let k = 0; k < 4; k++) { + result[j][i] += matrixA[k][i] * matrixB[j][k]; + } + } + } + return result; +} function decompose(matrix) { // Normalize the matrix. if (matrix[3][3] === 0) { @@ -17925,13 +17944,6 @@ function decompose(matrix) { if (row[1][0] > row[0][1]) { quaternion[2] = -quaternion[2]; } - // const rad2deg = 180 / Math.PI; - // const theta: number = Math.atan2(matrix[1][0], matrix[0][0]) * rad2deg; - // const zAxis = - // - // console.error({theta}); - // - // const rotation: [number, number, number] = [-Math.asin(matrix[0][2]) * rad2deg, 0, 0]; // apply rotation let x = quaternion[0]; let y = quaternion[1]; @@ -17949,15 +17961,105 @@ function decompose(matrix) { rotationMatrix[2][0] = 2 * (x * z - y * w); rotationMatrix[2][1] = 2 * (y * z + x * w); rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); + const { x: x1, y: y1, z: z1, angle } = getRotation3D(rotationMatrix); return { - skew, - scale, - // rotate: [+rx.toPrecision(12), +ry.toPrecision(12), +rz.toPrecision(12)], - translate, + skew: toZero(skew), + scale: toZero(scale), + rotate: toZero([x1, y1, z1, angle]), + translate: toZero(translate), perspective, quaternion }; } +function toZero(v) { + for (let i = 0; i < v.length; i++) { + if (Math.abs(v[i]) <= 1e-6) { + v[i] = 0; + } + else { + v[i] = +v[i].toPrecision(6); + } + } + return v; +} +// Fonction pour calculer rotate3d à partir de matrix3d +function getRotation3D(matrix) { + // Extraire la sous-matrice 3x3 de rotation + const r11 = matrix[0][0], r12 = matrix[0][1], r13 = matrix[0][2]; + const r21 = matrix[1][0], r22 = matrix[1][1], r23 = matrix[1][2]; + const r31 = matrix[2][0], r32 = matrix[2][1], r33 = matrix[2][2]; + // Calculer la trace (somme des éléments diagonaux) + const trace = r11 + r22 + r33; + // Calculer l’angle de rotation (en radians) + const cosTheta = (trace - 1) / 2; + // Calculer sin(θ) avec le signe correct + const sinThetaRaw = Math.sqrt(1 - cosTheta * cosTheta); + const xRaw = r32 - r23; // -0.467517 + const yRaw = r13 - r31; // 0.776535 + const zRaw = r21 - r12; // 0.776535 + // Déterminer le signe de sin(θ) basé sur la direction + const sinTheta = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; + // Calculer l’angle avec atan2 + const angle = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(6); + let x = 0, y = 0, z = 0; + if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° + const x1 = +r11.toPrecision(6); + const y1 = +r22.toPrecision(6); + const z1 = +r33.toPrecision(6); + const max = Math.max(x1, y1, z1); + x = y = z = 0; + if (max === x1) { + x = 1; + } + if (max === y1) { + y = 1; + } + if (max === z1) { + z = 1; + } + } + else { + x = (r32 - r23) / (2 * sinTheta); + y = (r13 - r31) / (2 * sinTheta); + z = (r21 - r12) / (2 * sinTheta); + } + // Normaliser le vecteur (optionnel, mais utile pour vérification) + const length = Math.sqrt(x * x + y * y + z * z); + if (length > 0) { + x /= length; + y /= length; + z /= length; + } + const pc = Math.abs(gcd(x, gcd(y, z))); + if (pc > 0.1 && pc <= Math.abs(x) && pc <= Math.abs(y) && pc <= Math.abs(z)) { + x /= pc; + y /= pc; + z /= pc; + } + else { + const min = Math.min(Math.abs(x), Math.abs(y), Math.abs(z)); + if (min > 0.1) { + x = +(x / min).toPrecision(6); + y = +(y / min).toPrecision(6); + z = +(z / min).toPrecision(6); + } + } + return { x, y, z, angle }; +} +// https://drafts.csswg.org/css-transforms-1/#2d-matrix +function is2DMatrix(matrix) { + // m13,m14, m23, m24, m31, m32, m34, m43 are all 0 + return matrix[0][2] === 0 && + matrix[0][3] === 0 && + matrix[1][2] === 0 && + matrix[1][3] === 0 && + matrix[2][0] === 0 && + matrix[2][1] === 0 && + matrix[2][3] === 0 && + matrix[3][2] === 0 && + matrix[2][2] === 1 && + matrix[3][3] === 1; +} // https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units#absolute_length_units function length2Px(value) { @@ -17989,23 +18091,27 @@ function length2Px(value) { return null; } -function translateX(x, matrix) { +function translateX(x, from) { + const matrix = identity(); matrix[3][0] = x; - return matrix; + return multiply(from, matrix); } -function translateY(y, matrix) { +function translateY(y, from) { + const matrix = identity(); matrix[3][1] = y; - return matrix; + return multiply(from, matrix); } -function translateZ(z, matrix) { +function translateZ(z, from) { + const matrix = identity(); matrix[3][2] = z; - return matrix; + return multiply(from, matrix); } -function translate(translate, matrix) { +function translate(translate, from) { + const matrix = identity(); matrix[3][0] = translate[0]; matrix[3][1] = translate[1] ?? 0; matrix[3][2] = translate[2] ?? 0; - return matrix; + return multiply(from, matrix); } /** @@ -18014,9 +18120,10 @@ function translate(translate, matrix) { * @param x * @param y * @param z - * @param matrix + * @param from */ -function rotate3D(angle, x, y, z, matrix) { +function rotate3D(angle, x, y, z, from) { + const matrix = identity(); const sc = Math.sin(angle / 2) * Math.cos(angle / 2); const sq = Math.sin(angle / 2) * Math.sin(angle / 2); const norm = Math.sqrt(x * x + y * y + z * z); @@ -18025,151 +18132,70 @@ function rotate3D(angle, x, y, z, matrix) { y *= unit; z *= unit; matrix[0][0] = 1 - 2 * (y * y + z * z) * sq; - matrix[0][1] = 2 * (x * y * sq - z * sc); - matrix[0][2] = 2 * (x * z * sq + y * sc); - matrix[1][0] = 2 * (x * y * sq + z * sc); + matrix[0][1] = 2 * (x * y * sq + z * sc); + matrix[0][2] = 2 * (x * z * sq - y * sc); + matrix[1][0] = 2 * (x * y * sq - z * sc); matrix[1][1] = 1 - 2 * (x * x + z * z) * sq; - matrix[1][2] = 2 * (y * z * sq - x * sc); - matrix[2][0] = 2 * (x * z * sq - y * sc); - matrix[2][1] = 2 * (y * z * sq + x * sc); + matrix[1][2] = 2 * (y * z * sq + x * sc); + matrix[2][0] = 2 * (x * z * sq + y * sc); + matrix[2][1] = 2 * (y * z * sq - x * sc); matrix[2][2] = 1 - 2 * (x * x + y * y) * sq; - return matrix; + return multiply(from, matrix); +} +function rotate(angle, from) { + const matrix = identity(); + matrix[0][0] = Math.cos(angle); + matrix[0][1] = Math.sin(angle); + matrix[1][0] = -Math.sin(angle); + matrix[1][1] = Math.cos(angle); + return multiply(from, matrix); } -function compute(transformList) { - transformList = transformList.slice(); - stripCommaToken(transformList); - if (transformList.length == 0) { - return null; - } +function scaleX(x, from) { const matrix = identity(); - let values = []; - let val; - for (let i = 0; i < transformList.length; i++) { - if (transformList[i].typ == exports.EnumToken.WhitespaceTokenType) { - continue; - } - if (transformList[i].typ != exports.EnumToken.FunctionTokenType || !transformFunctions.includes(transformList[i].val)) { - return null; - } - switch (transformList[i].val) { - case 'translate': - case 'translateX': - case 'translateY': - case 'translateZ': - case 'translate3d': - { - values.length = 0; - const children = stripCommaToken(transformList[i].chi.slice()); - if (children == null || children.length == 0) { - return null; - } - const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; - // console.error([(transformList[i] as FunctionToken).val, valCount]); - if (children.length == 1 && children[0].typ == exports.EnumToken.IdenTokenType && children[0].val == 'none') { - values.fill(0, 0, valCount); - } - else { - for (let j = 0; j < children.length; j++) { - if (children[j].typ == exports.EnumToken.WhitespaceTokenType) { - continue; - } - val = length2Px(children[j]); - if (typeof val != 'number' || Number.isNaN(val)) { - return null; - } - values.push(val); - } - } - if (values.length == 0 || values.length > valCount) { - return null; - } - if (transformList[i].val == 'translateX') { - translateX(values[0], matrix); - } - else if (transformList[i].val == 'translateY') { - translateY(values[0], matrix); - } - else if (transformList[i].val == 'translateZ') { - translateZ(values[0], matrix); - } - else { - // @ts-ignore - translate(values, matrix); - } - } - break; - case 'rotate': - case 'rotateX': - case 'rotateY': - case 'rotateZ': - case 'rotate3d': - // - { - let x = 0; - let y = 0; - let z = 0; - let angle; - let values = []; - let valuesCount = transformList[i].val == 'rotate3d' ? 4 : 1; - if (['rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d'].includes(transformList[i].val)) { - for (const child of stripCommaToken(transformList[i].chi.slice())) { - if (child.typ == exports.EnumToken.WhitespaceTokenType) { - continue; - } - values.push(child); - if (transformList[i].val == 'rotateX') { - x = 1; - } - else if (transformList[i].val == 'rotateY') { - y = 1; - } - else if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { - z = 1; - } - } - // console.error({values, valuesCount}); - if (values.length != valuesCount) { - return null; - } - if (transformList[i].val == 'rotate3d') { - x = getNumber(values[0]); - y = getNumber(values[1]); - z = getNumber(values[2]); - } - angle = getAngle(values.at(-1)); - if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { - return null; - } - // console.error([x, y, z, angle * 2 * Math.PI]); - // if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { - rotate3D(angle * 2 * Math.PI, x, y, z, matrix); - // console.error({matrix}); - // continue; - // } - // console.error({values}); - // return null; - } - // else - // if ((transformList[i] as FunctionToken).val == 'rotate') { - // - // } - } - break; - default: - return null; - // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); - } - } - return matrix; + matrix[0][0] = x; + // matrix[1][1] = 1; + // matrix[2][2] = 1; + return multiply(from, matrix); +} +function scaleY(y, from) { + const matrix = identity(); + // matrix[0][0] = 1; + matrix[1][1] = y; + // matrix[2][2] = 1; + return multiply(from, matrix); +} +function scaleZ(z, from) { + const matrix = identity(); + // matrix[0][0] = 1; + // matrix[1][1] = 1; + matrix[2][2] = z; + return multiply(from, matrix); +} +function scale(x, y, from) { + const matrix = identity(); + matrix[0][0] = x; + matrix[1][1] = y; + return multiply(from, matrix); +} +function scale3d(x, y, z, from) { + const matrix = identity(); + matrix[0][0] = x; + matrix[1][1] = y; + matrix[2][2] = z; + return multiply(from, matrix); } function minify$1(matrix) { const decomposed = decompose(matrix); + // console.error({decomposed}); if (decomposed == null) { return null; } - const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + const transforms = new Set([ + 'translate', 'scale', 'skew', 'perspective', 'rotate' + ]); + const scales = new Set(['x', 'y', 'z']); const result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { @@ -18184,97 +18210,138 @@ function minify$1(matrix) { if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { transforms.delete('perspective'); } - // if (transforms.size == 0) { - // - // // identity - // return [{ - // typ: EnumToken.FunctionTokenType, - // val: 'scale', - // chi: [ - // {typ: EnumToken.NumberTokenType, val: '1'} - // ] - // } - // ]; - // } - if (transforms.size == 1) { - if (transforms.has('translate')) { - let coordinates = new Set(['x', 'y', 'z']); - for (let i = 0; i < 3; i++) { - if (decomposed.translate[i] == 0) { - coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); - } - } - if (coordinates.size == 3) { + if (decomposed.rotate[3] == 0) { + transforms.delete('rotate'); + } + if (transforms.has('translate')) { + let coordinates = new Set(['x', 'y', 'z']); + for (let i = 0; i < 3; i++) { + if (decomposed.translate[i] == 0) { + coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); + } + } + if (coordinates.size == 3) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); + } + else if (coordinates.size == 1) { + if (coordinates.has('x')) { result.push({ typ: exports.EnumToken.FunctionTokenType, val: 'translate', + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] + }); + } + else { + let axis = coordinates.has('y') ? 'y' : 'z'; + let index = axis == 'y' ? 1 : 2; + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate' + axis.toUpperCase(), + chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] + }); + } + } + else if (coordinates.has('z')) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: exports.EnumToken.NumberTokenType, + 'val': '0' + } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } + ] + }); + } + } + // scale(x, x) -> scale(x) + // scale(1, sy) -> scaleY(sy) + // scale3d(1, 1, sz) -> scaleZ(sz) + // scaleX() scale(Y) scaleZ() -> scale3d() + if (transforms.has('scale')) { + const [sx, sy, sz] = decomposed.scale; + if (!(sx == 1 && sy == 1 && sz == 1)) { + if (sz == 1) { + scales.delete('z'); + } + if (sy == 1) { + scales.delete('y'); + } + if (sx == 1) { + scales.delete('x'); + } + if (scales.size == 1) { + let prefix = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale' + prefix, chi: [ - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + { typ: exports.EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx) } ] }); } - else if (coordinates.size == 1) { - if (coordinates.has('x')) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'translate', - chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] - }); - } - else { - let axis = coordinates.has('y') ? 'y' : 'z'; - let index = axis == 'y' ? 1 : 2; - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'translate' + axis.toUpperCase(), - chi: [{ typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] - }); - } - } - else if (coordinates.has('z')) { + else if (!scales.has('z')) { result.push({ typ: exports.EnumToken.FunctionTokenType, - val: 'translate', + val: 'scale', chi: [ - decomposed.translate[0] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - decomposed.translate[1] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, + { typ: exports.EnumToken.WhitespaceTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, ] }); } else { result.push({ typ: exports.EnumToken.FunctionTokenType, - val: 'translate', + val: 'scale3d', chi: [ - decomposed.translate[0] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, { typ: exports.EnumToken.CommaTokenType }, - decomposed.translate[1] == 0 ? { - typ: exports.EnumToken.NumberTokenType, - 'val': '0' - } : { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } + { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sz } ] }); } } } - { - const { x, y, z, angle } = getRotation3D(matrix); + if (transforms.has('rotate')) { + const [x, y, z, angle] = decomposed.rotate; // console.error({x, y, z, angle}); if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { if (y == 0 && z == 0) { @@ -18346,82 +18413,276 @@ function minify$1(matrix) { } } } - // scale(x, x) -> scale(x) - // scale(1, sy) -> scaleY(sy) - // scale3d(1, 1, sz) -> scaleZ(sz) - // scaleX() scale(Y) scaleZ() -> scale3d() // identity - return result.length == 0 ? [ + return transforms.size > 1 ? null : result.length == 0 || eq(result, identity()) ? [ { + typ: exports.EnumToken.IdenTokenType, + val: 'none' + } + ] : result; +} + +function serialize(matrix) { + if (is2DMatrix(matrix)) { + // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset + return { typ: exports.EnumToken.FunctionTokenType, - val: 'scale', + val: 'matrix', chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '1' } - ] + matrix[0][0], + matrix[1][1], + matrix[1][0], + matrix[1][1], + matrix[3][0], + matrix[3][1] + ].reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(t.toPrecision(6)) + }); + return acc; + }, []) + }; + } + let m = []; + // console.error(JSON.stringify({matrix},null, 1)); + for (let i = 0; i < matrix.length; i++) { + for (let j = 0; j < matrix[i].length; j++) { + if (m.length > 0) { + m.push({ typ: exports.EnumToken.CommaTokenType }); + } + m.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(matrix[i][j].toPrecision(6)) + }); } - ] : result; + } + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: m + }; } -// Fonction pour calculer rotate3d à partir de matrix3d -function getRotation3D(matrix) { - // Extraire la sous-matrice 3x3 de rotation - const r11 = matrix[0][0], r12 = matrix[0][1], r13 = matrix[0][2]; - const r21 = matrix[1][0], r22 = matrix[1][1], r23 = matrix[1][2]; - const r31 = matrix[2][0], r32 = matrix[2][1], r33 = matrix[2][2]; - // Calculer la trace (somme des éléments diagonaux) - const trace = r11 + r22 + r33; - // Calculer l’angle de rotation (en radians) - const cosTheta = (trace - 1) / 2; - // Calculer sin(θ) avec le signe correct - const sinThetaRaw = Math.sqrt(1 - cosTheta * cosTheta); - const xRaw = r32 - r23; // -0.467517 - const yRaw = r13 - r31; // 0.776535 - const zRaw = r21 - r12; // 0.776535 - // Déterminer le signe de sin(θ) basé sur la direction - const sinTheta = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; - // Calculer l’angle avec atan2 - const angle = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(12); - let x, y, z; - if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° - const x1 = r32 - r23; - const y1 = r13 - r31; - const z1 = r21 - r12; - switch (Math.max(x1, y1, z1)) { - case x1: - x = 1; - y = 0; - z = 0; + +function compute(transformLists) { + transformLists = transformLists.slice(); + stripCommaToken(transformLists); + if (transformLists.length == 0) { + return null; + } + const tokens = []; + let matrix; + for (const transformList of splitTransformList(transformLists)) { + matrix = computeMatrix(transformList); + if (matrix == null) { + return null; + } + tokens.push(matrix); + } + // for (let i = 0; i < tokens.length; i++) { + // + // for (let j = 0; j < tokens[i].length; j++) { + // + // toZero(tokens[i][j]); + // } + // } + return tokens.reduce((acc, t) => acc.concat(minify$1(t) ?? serialize(t)), []); +} +function computeMatrix(transformList) { + let matrix = identity(); + let values = []; + let val; + let i = 0; + for (; i < transformList.length; i++) { + if (transformList[i].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (transformList[i].typ != exports.EnumToken.FunctionTokenType || !transformFunctions.includes(transformList[i].val)) { + return null; + } + switch (transformList[i].val) { + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': + { + values.length = 0; + const children = stripCommaToken(transformList[i].chi.slice()); + if (children == null || children.length == 0) { + return null; + } + const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; + if (children.length == 1 && children[0].typ == exports.EnumToken.IdenTokenType && children[0].val == 'none') { + values.fill(0, 0, valCount); + } + else { + for (let j = 0; j < children.length; j++) { + if (children[j].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + val = length2Px(children[j]); + if (typeof val != 'number' || Number.isNaN(val)) { + return null; + } + values.push(val); + } + } + if (values.length == 0 || values.length > valCount) { + return null; + } + if (transformList[i].val == 'translateX') { + matrix = translateX(values[0], matrix); + } + else if (transformList[i].val == 'translateY') { + matrix = translateY(values[0], matrix); + } + else if (transformList[i].val == 'translateZ') { + matrix = translateZ(values[0], matrix); + } + else { + // @ts-ignore + matrix = translate(values, matrix); + } + } break; - case y1: - x = 0; - y = 1; - z = 0; + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + { + let x = 0; + let y = 0; + let z = 0; + let angle; + let values = []; + let valuesCount = transformList[i].val == 'rotate3d' ? 4 : 1; + for (const child of stripCommaToken(transformList[i].chi.slice())) { + if (child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + values.push(child); + if (transformList[i].val == 'rotateX') { + x = 1; + } + else if (transformList[i].val == 'rotateY') { + y = 1; + } + else if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { + z = 1; + } + } + if (values.length != valuesCount) { + return null; + } + if (transformList[i].val == 'rotate3d') { + x = getNumber(values[0]); + y = getNumber(values[1]); + z = getNumber(values[2]); + } + angle = getAngle(values.at(-1)); + if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { + return null; + } + if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { + matrix = rotate(angle * 2 * Math.PI, matrix); + } + else { + matrix = rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + } + } break; - default: - x = 0; - y = 0; - z = 1; + case 'scale': + case 'scaleX': + case 'scaleY': + case 'scaleZ': + case 'scale3d': { + values.length = 0; + let child; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (child.typ != exports.EnumToken.NumberTokenType) { + return null; + } + values.push(getNumber(child)); + } + if (values.length == 0) { + return null; + } + if (transformList[i].val == 'scale3d') { + if (values.length != 3) { + return null; + } + matrix = scale3d(...values, matrix); + break; + } + if (transformList[i].val == 'scale') { + if (values.length != 1 && values.length != 2) { + return null; + } + matrix = scale(values[0], values[1] ?? values[0], matrix); + break; + } + if (values.length != 1) { + return null; + } + else if (transformList[i].val == 'scaleX') { + matrix = scaleX(values[0], matrix); + } + else if (transformList[i].val == 'scaleY') { + matrix = scaleY(values[0], matrix); + } + else if (transformList[i].val == 'scaleZ') { + matrix = scaleZ(values[0], matrix); + } break; + } + default: + return null; + // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); } } - else { - x = (r32 - r23) / (2 * sinTheta); - y = (r13 - r31) / (2 * sinTheta); - z = (r21 - r12) / (2 * sinTheta); - } - // Normaliser le vecteur (optionnel, mais utile pour vérification) - const length = Math.sqrt(x * x + y * y + z * z); - if (length > 0) { - x /= length; - y /= length; - z /= length; - } - const x1 = gcd(x, gcd(y, z)); - if (x1 != 0) { - x /= x1; - y /= x1; - z /= x1; + return matrix; +} +function splitTransformList(transformList) { + let pattern = null; + const tokens = []; + for (let i = 0; i < transformList.length; i++) { + if (transformList[i].typ == exports.EnumToken.CommentTokenType || transformList[i].typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (pattern == null || (transformList[i].typ == exports.EnumToken.FunctionTokenType && !transformList[i].val.startsWith(pattern))) { + if (transformList[i].typ == exports.EnumToken.FunctionTokenType) { + if (transformList[i].val.startsWith('scale')) { + pattern = 'scale'; + } + else if (transformList[i].val.startsWith('rotate')) { + pattern = 'rotate'; + } + else if (transformList[i].val.startsWith('translate')) { + pattern = 'translate'; + } + else { + pattern = null; + } + tokens.push([transformList[i]]); + continue; + } + } + if (pattern != null && transformList[i].typ == exports.EnumToken.FunctionTokenType && transformList[i].val.startsWith(pattern)) { + tokens[tokens.length - 1].push(transformList[i]); + continue; + } + tokens.push([transformList[i]]); } - return { x, y, z, angle }; + return tokens; } class TransformCssFeature { @@ -18451,17 +18712,27 @@ class TransformCssFeature { } const children = node.val.slice(); consumeWhitespace(children); - const result = compute(children); + let result = compute(children); if (result == null) { - // console.error({result}); return; } - decompose(result); - const minified = minify$1(result); - // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); - if (minified != null) { - node.val = minified; - } + // console.error(JSON.stringify({result}, null, 1)); + // console.error({result, t: result.map(t => minify(t) ?? t + // )}); + // console.error({result: decompose2(result)}); + // const decomposed =decompose(result); + // const minified = minify(result); + const matrix = computeMatrix(result); + if (matrix != null) { + const m = serialize(matrix); + if (renderToken(m).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = [m]; + } + } + // console.error({result, serialized: renderToken()}); + // if (minified != null) { + node.val = result; + // } } } } diff --git a/dist/lib/ast/features/transform.js b/dist/lib/ast/features/transform.js index 6d2ed867..eec5b050 100644 --- a/dist/lib/ast/features/transform.js +++ b/dist/lib/ast/features/transform.js @@ -3,12 +3,11 @@ import { consumeWhitespace } from '../../validation/utils/whitespace.js'; import '../minify.js'; import '../walk.js'; import '../../parser/parse.js'; +import { renderToken } from '../../renderer/render.js'; import '../../renderer/color/utils/constants.js'; -import '../../renderer/sourcemap/lib/encode.js'; import '../../parser/utils/config.js'; -import { compute } from '../transform/compute.js'; -import { minify } from '../transform/minify.js'; -import { decompose } from '../transform/utils.js'; +import { compute, computeMatrix } from '../transform/compute.js'; +import { serialize } from '../transform/matrix.js'; class TransformCssFeature { static get ordering() { @@ -37,17 +36,27 @@ class TransformCssFeature { } const children = node.val.slice(); consumeWhitespace(children); - const result = compute(children); + let result = compute(children); if (result == null) { - // console.error({result}); return; } - decompose(result); - const minified = minify(result); - // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); - if (minified != null) { - node.val = minified; + // console.error(JSON.stringify({result}, null, 1)); + // console.error({result, t: result.map(t => minify(t) ?? t + // )}); + // console.error({result: decompose2(result)}); + // const decomposed =decompose(result); + // const minified = minify(result); + const matrix = computeMatrix(result); + if (matrix != null) { + const m = serialize(matrix); + if (renderToken(m).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = [m]; + } } + // console.error({result, serialized: renderToken()}); + // if (minified != null) { + node.val = result; + // } } } } diff --git a/dist/lib/ast/math/math.js b/dist/lib/ast/math/math.js index 5bc758ce..becc17cb 100644 --- a/dist/lib/ast/math/math.js +++ b/dist/lib/ast/math/math.js @@ -8,6 +8,11 @@ function gcd(x, y) { if (x == 0 || y == 0) { return 1; } + if (y > x) { + t = x; + x = y; + y = t; + } while (y) { t = y; y = x % y; diff --git a/dist/lib/ast/transform/compute.js b/dist/lib/ast/transform/compute.js index 18c1eb5e..65c2b64e 100644 --- a/dist/lib/ast/transform/compute.js +++ b/dist/lib/ast/transform/compute.js @@ -11,18 +11,41 @@ import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import { stripCommaToken } from '../../validation/utils/list.js'; import { translateX, translateY, translateZ, translate } from './translate.js'; -import { rotate3D } from './rotate.js'; +import { rotate, rotate3D } from './rotate.js'; +import { scale3d, scale, scaleX, scaleY, scaleZ } from './scale.js'; +import { minify } from './minify.js'; +import { serialize } from './matrix.js'; -function compute(transformList) { - transformList = transformList.slice(); - stripCommaToken(transformList); - if (transformList.length == 0) { +function compute(transformLists) { + transformLists = transformLists.slice(); + stripCommaToken(transformLists); + if (transformLists.length == 0) { return null; } - const matrix = identity(); + const tokens = []; + let matrix; + for (const transformList of splitTransformList(transformLists)) { + matrix = computeMatrix(transformList); + if (matrix == null) { + return null; + } + tokens.push(matrix); + } + // for (let i = 0; i < tokens.length; i++) { + // + // for (let j = 0; j < tokens[i].length; j++) { + // + // toZero(tokens[i][j]); + // } + // } + return tokens.reduce((acc, t) => acc.concat(minify(t) ?? serialize(t)), []); +} +function computeMatrix(transformList) { + let matrix = identity(); let values = []; let val; - for (let i = 0; i < transformList.length; i++) { + let i = 0; + for (; i < transformList.length; i++) { if (transformList[i].typ == EnumToken.WhitespaceTokenType) { continue; } @@ -42,7 +65,6 @@ function compute(transformList) { return null; } const valCount = transformList[i].val == 'translate3d' || transformList[i].val == 'translate' ? 3 : 1; - // console.error([(transformList[i] as FunctionToken).val, valCount]); if (children.length == 1 && children[0].typ == EnumToken.IdenTokenType && children[0].val == 'none') { values.fill(0, 0, valCount); } @@ -62,17 +84,17 @@ function compute(transformList) { return null; } if (transformList[i].val == 'translateX') { - translateX(values[0], matrix); + matrix = translateX(values[0], matrix); } else if (transformList[i].val == 'translateY') { - translateY(values[0], matrix); + matrix = translateY(values[0], matrix); } else if (transformList[i].val == 'translateZ') { - translateZ(values[0], matrix); + matrix = translateZ(values[0], matrix); } else { // @ts-ignore - translate(values, matrix); + matrix = translate(values, matrix); } } break; @@ -81,7 +103,6 @@ function compute(transformList) { case 'rotateY': case 'rotateZ': case 'rotate3d': - // { let x = 0; let y = 0; @@ -89,50 +110,89 @@ function compute(transformList) { let angle; let values = []; let valuesCount = transformList[i].val == 'rotate3d' ? 4 : 1; - if (['rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d'].includes(transformList[i].val)) { - for (const child of stripCommaToken(transformList[i].chi.slice())) { - if (child.typ == EnumToken.WhitespaceTokenType) { - continue; - } - values.push(child); - if (transformList[i].val == 'rotateX') { - x = 1; - } - else if (transformList[i].val == 'rotateY') { - y = 1; - } - else if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { - z = 1; - } + for (const child of stripCommaToken(transformList[i].chi.slice())) { + if (child.typ == EnumToken.WhitespaceTokenType) { + continue; } - // console.error({values, valuesCount}); - if (values.length != valuesCount) { - return null; + values.push(child); + if (transformList[i].val == 'rotateX') { + x = 1; } - if (transformList[i].val == 'rotate3d') { - x = getNumber(values[0]); - y = getNumber(values[1]); - z = getNumber(values[2]); + else if (transformList[i].val == 'rotateY') { + y = 1; } - angle = getAngle(values.at(-1)); - if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { - return null; + else if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { + z = 1; } - // console.error([x, y, z, angle * 2 * Math.PI]); - // if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { - rotate3D(angle * 2 * Math.PI, x, y, z, matrix); - // console.error({matrix}); - // continue; - // } - // console.error({values}); - // return null; - } - // else - // if ((transformList[i] as FunctionToken).val == 'rotate') { - // - // } + } + if (values.length != valuesCount) { + return null; + } + if (transformList[i].val == 'rotate3d') { + x = getNumber(values[0]); + y = getNumber(values[1]); + z = getNumber(values[2]); + } + angle = getAngle(values.at(-1)); + if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { + return null; + } + if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { + matrix = rotate(angle * 2 * Math.PI, matrix); + } + else { + matrix = rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + } } break; + case 'scale': + case 'scaleX': + case 'scaleY': + case 'scaleZ': + case 'scale3d': { + values.length = 0; + let child; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == EnumToken.CommentTokenType || child.typ == EnumToken.WhitespaceTokenType) { + continue; + } + if (child.typ != EnumToken.NumberTokenType) { + return null; + } + values.push(getNumber(child)); + } + if (values.length == 0) { + return null; + } + if (transformList[i].val == 'scale3d') { + if (values.length != 3) { + return null; + } + matrix = scale3d(...values, matrix); + break; + } + if (transformList[i].val == 'scale') { + if (values.length != 1 && values.length != 2) { + return null; + } + matrix = scale(values[0], values[1] ?? values[0], matrix); + break; + } + if (values.length != 1) { + return null; + } + else if (transformList[i].val == 'scaleX') { + matrix = scaleX(values[0], matrix); + } + else if (transformList[i].val == 'scaleY') { + matrix = scaleY(values[0], matrix); + } + else if (transformList[i].val == 'scaleZ') { + matrix = scaleZ(values[0], matrix); + } + break; + } default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); @@ -140,5 +200,38 @@ function compute(transformList) { } return matrix; } +function splitTransformList(transformList) { + let pattern = null; + const tokens = []; + for (let i = 0; i < transformList.length; i++) { + if (transformList[i].typ == EnumToken.CommentTokenType || transformList[i].typ == EnumToken.WhitespaceTokenType) { + continue; + } + if (pattern == null || (transformList[i].typ == EnumToken.FunctionTokenType && !transformList[i].val.startsWith(pattern))) { + if (transformList[i].typ == EnumToken.FunctionTokenType) { + if (transformList[i].val.startsWith('scale')) { + pattern = 'scale'; + } + else if (transformList[i].val.startsWith('rotate')) { + pattern = 'rotate'; + } + else if (transformList[i].val.startsWith('translate')) { + pattern = 'translate'; + } + else { + pattern = null; + } + tokens.push([transformList[i]]); + continue; + } + } + if (pattern != null && transformList[i].typ == EnumToken.FunctionTokenType && transformList[i].val.startsWith(pattern)) { + tokens[tokens.length - 1].push(transformList[i]); + continue; + } + tokens.push([transformList[i]]); + } + return tokens; +} -export { compute }; +export { compute, computeMatrix }; diff --git a/dist/lib/ast/transform/matrix.js b/dist/lib/ast/transform/matrix.js new file mode 100644 index 00000000..5824a221 --- /dev/null +++ b/dist/lib/ast/transform/matrix.js @@ -0,0 +1,50 @@ +import { is2DMatrix } from './utils.js'; +import { EnumToken } from '../types.js'; +import { reduceNumber } from '../../renderer/render.js'; + +function serialize(matrix) { + if (is2DMatrix(matrix)) { + // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset + return { + typ: EnumToken.FunctionTokenType, + val: 'matrix', + chi: [ + matrix[0][0], + matrix[1][1], + matrix[1][0], + matrix[1][1], + matrix[3][0], + matrix[3][1] + ].reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: EnumToken.CommaTokenType }); + } + acc.push({ + typ: EnumToken.NumberTokenType, + val: reduceNumber(t.toPrecision(6)) + }); + return acc; + }, []) + }; + } + let m = []; + // console.error(JSON.stringify({matrix},null, 1)); + for (let i = 0; i < matrix.length; i++) { + for (let j = 0; j < matrix[i].length; j++) { + if (m.length > 0) { + m.push({ typ: EnumToken.CommaTokenType }); + } + m.push({ + typ: EnumToken.NumberTokenType, + val: reduceNumber(matrix[i][j].toPrecision(6)) + }); + } + } + return { + typ: EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: m + }; +} + +export { serialize }; diff --git a/dist/lib/ast/transform/minify.js b/dist/lib/ast/transform/minify.js index 4eda0545..65db9d5a 100644 --- a/dist/lib/ast/transform/minify.js +++ b/dist/lib/ast/transform/minify.js @@ -1,13 +1,17 @@ -import { decompose } from './utils.js'; +import { decompose, identity } from './utils.js'; import { EnumToken } from '../types.js'; -import { gcd } from '../math/math.js'; +import { eq } from '../../parser/utils/eq.js'; function minify(matrix) { const decomposed = decompose(matrix); + // console.error({decomposed}); if (decomposed == null) { return null; } - const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + const transforms = new Set([ + 'translate', 'scale', 'skew', 'perspective', 'rotate' + ]); + const scales = new Set(['x', 'y', 'z']); const result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { @@ -22,97 +26,138 @@ function minify(matrix) { if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { transforms.delete('perspective'); } - // if (transforms.size == 0) { - // - // // identity - // return [{ - // typ: EnumToken.FunctionTokenType, - // val: 'scale', - // chi: [ - // {typ: EnumToken.NumberTokenType, val: '1'} - // ] - // } - // ]; - // } - if (transforms.size == 1) { - if (transforms.has('translate')) { - let coordinates = new Set(['x', 'y', 'z']); - for (let i = 0; i < 3; i++) { - if (decomposed.translate[i] == 0) { - coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); - } + if (decomposed.rotate[3] == 0) { + transforms.delete('rotate'); + } + if (transforms.has('translate')) { + let coordinates = new Set(['x', 'y', 'z']); + for (let i = 0; i < 3; i++) { + if (decomposed.translate[i] == 0) { + coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); } - if (coordinates.size == 3) { + } + if (coordinates.size == 3) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); + } + else if (coordinates.size == 1) { + if (coordinates.has('x')) { result.push({ typ: EnumToken.FunctionTokenType, val: 'translate', + chi: [{ typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] + }); + } + else { + let axis = coordinates.has('y') ? 'y' : 'z'; + let index = axis == 'y' ? 1 : 2; + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate' + axis.toUpperCase(), + chi: [{ typ: EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] + }); + } + } + else if (coordinates.has('z')) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + ] + }); + } + else { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: EnumToken.CommaTokenType }, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } + ] + }); + } + } + // scale(x, x) -> scale(x) + // scale(1, sy) -> scaleY(sy) + // scale3d(1, 1, sz) -> scaleZ(sz) + // scaleX() scale(Y) scaleZ() -> scale3d() + if (transforms.has('scale')) { + const [sx, sy, sz] = decomposed.scale; + if (!(sx == 1 && sy == 1 && sz == 1)) { + if (sz == 1) { + scales.delete('z'); + } + if (sy == 1) { + scales.delete('y'); + } + if (sx == 1) { + scales.delete('x'); + } + if (scales.size == 1) { + let prefix = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'scale' + prefix, chi: [ - { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: EnumToken.CommaTokenType }, - { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: EnumToken.CommaTokenType }, - { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + { typ: EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx) } ] }); } - else if (coordinates.size == 1) { - if (coordinates.has('x')) { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'translate', - chi: [{ typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }] - }); - } - else { - let axis = coordinates.has('y') ? 'y' : 'z'; - let index = axis == 'y' ? 1 : 2; - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'translate' + axis.toUpperCase(), - chi: [{ typ: EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px' }] - }); - } - } - else if (coordinates.has('z')) { + else if (!scales.has('z')) { result.push({ typ: EnumToken.FunctionTokenType, - val: 'translate', + val: 'scale', chi: [ - decomposed.translate[0] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, - { typ: EnumToken.CommaTokenType }, - decomposed.translate[1] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' }, - { typ: EnumToken.CommaTokenType }, - { typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px' } + { typ: EnumToken.NumberTokenType, val: '' + sx }, + { typ: EnumToken.WhitespaceTokenType }, + { typ: EnumToken.NumberTokenType, val: '' + sy }, ] }); } else { result.push({ typ: EnumToken.FunctionTokenType, - val: 'translate', + val: 'scale3d', chi: [ - decomposed.translate[0] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, + { typ: EnumToken.NumberTokenType, val: '' + sx }, { typ: EnumToken.CommaTokenType }, - decomposed.translate[1] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : { typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px' } + { typ: EnumToken.NumberTokenType, val: '' + sy }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.NumberTokenType, val: '' + sz } ] }); } } } - { - const { x, y, z, angle } = getRotation3D(matrix); + if (transforms.has('rotate')) { + const [x, y, z, angle] = decomposed.rotate; // console.error({x, y, z, angle}); if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { if (y == 0 && z == 0) { @@ -184,82 +229,13 @@ function minify(matrix) { } } } - // scale(x, x) -> scale(x) - // scale(1, sy) -> scaleY(sy) - // scale3d(1, 1, sz) -> scaleZ(sz) - // scaleX() scale(Y) scaleZ() -> scale3d() // identity - return result.length == 0 ? [ + return transforms.size > 1 ? null : result.length == 0 || eq(result, identity()) ? [ { - typ: EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - { typ: EnumToken.NumberTokenType, val: '1' } - ] + typ: EnumToken.IdenTokenType, + val: 'none' } ] : result; } -// Fonction pour calculer rotate3d à partir de matrix3d -function getRotation3D(matrix) { - // Extraire la sous-matrice 3x3 de rotation - const r11 = matrix[0][0], r12 = matrix[0][1], r13 = matrix[0][2]; - const r21 = matrix[1][0], r22 = matrix[1][1], r23 = matrix[1][2]; - const r31 = matrix[2][0], r32 = matrix[2][1], r33 = matrix[2][2]; - // Calculer la trace (somme des éléments diagonaux) - const trace = r11 + r22 + r33; - // Calculer l’angle de rotation (en radians) - const cosTheta = (trace - 1) / 2; - // Calculer sin(θ) avec le signe correct - const sinThetaRaw = Math.sqrt(1 - cosTheta * cosTheta); - const xRaw = r32 - r23; // -0.467517 - const yRaw = r13 - r31; // 0.776535 - const zRaw = r21 - r12; // 0.776535 - // Déterminer le signe de sin(θ) basé sur la direction - const sinTheta = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; - // Calculer l’angle avec atan2 - const angle = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(12); - let x, y, z; - if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° - const x1 = r32 - r23; - const y1 = r13 - r31; - const z1 = r21 - r12; - switch (Math.max(x1, y1, z1)) { - case x1: - x = 1; - y = 0; - z = 0; - break; - case y1: - x = 0; - y = 1; - z = 0; - break; - default: - x = 0; - y = 0; - z = 1; - break; - } - } - else { - x = (r32 - r23) / (2 * sinTheta); - y = (r13 - r31) / (2 * sinTheta); - z = (r21 - r12) / (2 * sinTheta); - } - // Normaliser le vecteur (optionnel, mais utile pour vérification) - const length = Math.sqrt(x * x + y * y + z * z); - if (length > 0) { - x /= length; - y /= length; - z /= length; - } - const x1 = gcd(x, gcd(y, z)); - if (x1 != 0) { - x /= x1; - y /= x1; - z /= x1; - } - return { x, y, z, angle }; -} export { minify }; diff --git a/dist/lib/ast/transform/rotate.js b/dist/lib/ast/transform/rotate.js new file mode 100644 index 00000000..52c0aaf4 --- /dev/null +++ b/dist/lib/ast/transform/rotate.js @@ -0,0 +1,40 @@ +import { identity, multiply } from './utils.js'; + +/** + * angle in radian + * @param angle + * @param x + * @param y + * @param z + * @param from + */ +function rotate3D(angle, x, y, z, from) { + const matrix = identity(); + const sc = Math.sin(angle / 2) * Math.cos(angle / 2); + const sq = Math.sin(angle / 2) * Math.sin(angle / 2); + const norm = Math.sqrt(x * x + y * y + z * z); + const unit = norm === 0 ? 0 : 1 / norm; + x *= unit; + y *= unit; + z *= unit; + matrix[0][0] = 1 - 2 * (y * y + z * z) * sq; + matrix[0][1] = 2 * (x * y * sq + z * sc); + matrix[0][2] = 2 * (x * z * sq - y * sc); + matrix[1][0] = 2 * (x * y * sq - z * sc); + matrix[1][1] = 1 - 2 * (x * x + z * z) * sq; + matrix[1][2] = 2 * (y * z * sq + x * sc); + matrix[2][0] = 2 * (x * z * sq + y * sc); + matrix[2][1] = 2 * (y * z * sq - x * sc); + matrix[2][2] = 1 - 2 * (x * x + y * y) * sq; + return multiply(from, matrix); +} +function rotate(angle, from) { + const matrix = identity(); + matrix[0][0] = Math.cos(angle); + matrix[0][1] = Math.sin(angle); + matrix[1][0] = -Math.sin(angle); + matrix[1][1] = Math.cos(angle); + return multiply(from, matrix); +} + +export { rotate, rotate3D }; diff --git a/dist/lib/ast/transform/scale.js b/dist/lib/ast/transform/scale.js new file mode 100644 index 00000000..9f302f4a --- /dev/null +++ b/dist/lib/ast/transform/scale.js @@ -0,0 +1,38 @@ +import { identity, multiply } from './utils.js'; + +function scaleX(x, from) { + const matrix = identity(); + matrix[0][0] = x; + // matrix[1][1] = 1; + // matrix[2][2] = 1; + return multiply(from, matrix); +} +function scaleY(y, from) { + const matrix = identity(); + // matrix[0][0] = 1; + matrix[1][1] = y; + // matrix[2][2] = 1; + return multiply(from, matrix); +} +function scaleZ(z, from) { + const matrix = identity(); + // matrix[0][0] = 1; + // matrix[1][1] = 1; + matrix[2][2] = z; + return multiply(from, matrix); +} +function scale(x, y, from) { + const matrix = identity(); + matrix[0][0] = x; + matrix[1][1] = y; + return multiply(from, matrix); +} +function scale3d(x, y, z, from) { + const matrix = identity(); + matrix[0][0] = x; + matrix[1][1] = y; + matrix[2][2] = z; + return multiply(from, matrix); +} + +export { scale, scale3d, scaleX, scaleY, scaleZ }; diff --git a/dist/lib/ast/transform/translate.js b/dist/lib/ast/transform/translate.js index 3d173cc7..1314cf83 100644 --- a/dist/lib/ast/transform/translate.js +++ b/dist/lib/ast/transform/translate.js @@ -1,20 +1,26 @@ -function translateX(x, matrix) { +import { identity, multiply } from './utils.js'; + +function translateX(x, from) { + const matrix = identity(); matrix[3][0] = x; - return matrix; + return multiply(from, matrix); } -function translateY(y, matrix) { +function translateY(y, from) { + const matrix = identity(); matrix[3][1] = y; - return matrix; + return multiply(from, matrix); } -function translateZ(z, matrix) { +function translateZ(z, from) { + const matrix = identity(); matrix[3][2] = z; - return matrix; + return multiply(from, matrix); } -function translate(translate, matrix) { +function translate(translate, from) { + const matrix = identity(); matrix[3][0] = translate[0]; matrix[3][1] = translate[1] ?? 0; matrix[3][2] = translate[2] ?? 0; - return matrix; + return multiply(from, matrix); } export { translate, translateX, translateY, translateZ }; diff --git a/dist/lib/ast/transform/utils.js b/dist/lib/ast/transform/utils.js index c82ae742..20adf91d 100644 --- a/dist/lib/ast/transform/utils.js +++ b/dist/lib/ast/transform/utils.js @@ -1,3 +1,5 @@ +import { gcd } from '../math/math.js'; + function determinant(matrix) { return matrix[0][0] * matrix[1][1] * matrix[2][2] * matrix[3][3] - matrix[0][0] * matrix[1][2] * matrix[2][3] * matrix[3][1] - matrix[0][1] * matrix[1][0] * matrix[2][3] * matrix[3][2] + matrix[0][1] * matrix[1][2] * matrix[2][0] * matrix[3][3] - @@ -85,6 +87,9 @@ function normalize(point) { return norm === 0 ? [0, 0, 0] : [x / norm, y / norm, z / norm]; } function dot(point1, point2) { + if (point1.length === 4 && point2.length === 4) { + return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2] + point1[3] * point2[3]; + } return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2]; } function combine(point1, point2, ascl, bscl) { @@ -93,6 +98,17 @@ function combine(point1, point2, ascl, bscl) { function cross(a, b) { return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; } +function multiply(matrixA, matrixB) { + let result = Array(4).fill(0).map(() => Array(4).fill(0)); + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + for (let k = 0; k < 4; k++) { + result[j][i] += matrixA[k][i] * matrixB[j][k]; + } + } + } + return result; +} function decompose(matrix) { // Normalize the matrix. if (matrix[3][3] === 0) { @@ -194,13 +210,6 @@ function decompose(matrix) { if (row[1][0] > row[0][1]) { quaternion[2] = -quaternion[2]; } - // const rad2deg = 180 / Math.PI; - // const theta: number = Math.atan2(matrix[1][0], matrix[0][0]) * rad2deg; - // const zAxis = - // - // console.error({theta}); - // - // const rotation: [number, number, number] = [-Math.asin(matrix[0][2]) * rad2deg, 0, 0]; // apply rotation let x = quaternion[0]; let y = quaternion[1]; @@ -218,14 +227,104 @@ function decompose(matrix) { rotationMatrix[2][0] = 2 * (x * z - y * w); rotationMatrix[2][1] = 2 * (y * z + x * w); rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); + const { x: x1, y: y1, z: z1, angle } = getRotation3D(rotationMatrix); return { - skew, - scale, - // rotate: [+rx.toPrecision(12), +ry.toPrecision(12), +rz.toPrecision(12)], - translate, + skew: toZero(skew), + scale: toZero(scale), + rotate: toZero([x1, y1, z1, angle]), + translate: toZero(translate), perspective, quaternion }; } +function toZero(v) { + for (let i = 0; i < v.length; i++) { + if (Math.abs(v[i]) <= 1e-6) { + v[i] = 0; + } + else { + v[i] = +v[i].toPrecision(6); + } + } + return v; +} +// Fonction pour calculer rotate3d à partir de matrix3d +function getRotation3D(matrix) { + // Extraire la sous-matrice 3x3 de rotation + const r11 = matrix[0][0], r12 = matrix[0][1], r13 = matrix[0][2]; + const r21 = matrix[1][0], r22 = matrix[1][1], r23 = matrix[1][2]; + const r31 = matrix[2][0], r32 = matrix[2][1], r33 = matrix[2][2]; + // Calculer la trace (somme des éléments diagonaux) + const trace = r11 + r22 + r33; + // Calculer l’angle de rotation (en radians) + const cosTheta = (trace - 1) / 2; + // Calculer sin(θ) avec le signe correct + const sinThetaRaw = Math.sqrt(1 - cosTheta * cosTheta); + const xRaw = r32 - r23; // -0.467517 + const yRaw = r13 - r31; // 0.776535 + const zRaw = r21 - r12; // 0.776535 + // Déterminer le signe de sin(θ) basé sur la direction + const sinTheta = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; + // Calculer l’angle avec atan2 + const angle = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(6); + let x = 0, y = 0, z = 0; + if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° + const x1 = +r11.toPrecision(6); + const y1 = +r22.toPrecision(6); + const z1 = +r33.toPrecision(6); + const max = Math.max(x1, y1, z1); + x = y = z = 0; + if (max === x1) { + x = 1; + } + if (max === y1) { + y = 1; + } + if (max === z1) { + z = 1; + } + } + else { + x = (r32 - r23) / (2 * sinTheta); + y = (r13 - r31) / (2 * sinTheta); + z = (r21 - r12) / (2 * sinTheta); + } + // Normaliser le vecteur (optionnel, mais utile pour vérification) + const length = Math.sqrt(x * x + y * y + z * z); + if (length > 0) { + x /= length; + y /= length; + z /= length; + } + const pc = Math.abs(gcd(x, gcd(y, z))); + if (pc > 0.1 && pc <= Math.abs(x) && pc <= Math.abs(y) && pc <= Math.abs(z)) { + x /= pc; + y /= pc; + z /= pc; + } + else { + const min = Math.min(Math.abs(x), Math.abs(y), Math.abs(z)); + if (min > 0.1) { + x = +(x / min).toPrecision(6); + y = +(y / min).toPrecision(6); + z = +(z / min).toPrecision(6); + } + } + return { x, y, z, angle }; +} +// https://drafts.csswg.org/css-transforms-1/#2d-matrix +function is2DMatrix(matrix) { + // m13,m14, m23, m24, m31, m32, m34, m43 are all 0 + return matrix[0][2] === 0 && + matrix[0][3] === 0 && + matrix[1][2] === 0 && + matrix[1][3] === 0 && + matrix[2][0] === 0 && + matrix[2][1] === 0 && + matrix[2][3] === 0 && + matrix[3][2] === 0 && + matrix[2][2] === 1 && + matrix[3][3] === 1; +} -export { decompose, identity }; +export { decompose, identity, is2DMatrix, multiply, toZero }; diff --git a/src/lib/ast/features/transform.ts b/src/lib/ast/features/transform.ts index 0e960e50..3d921509 100644 --- a/src/lib/ast/features/transform.ts +++ b/src/lib/ast/features/transform.ts @@ -8,9 +8,9 @@ import type { } from "../../../@types/index.d.ts"; import {EnumToken} from "../types"; import {consumeWhitespace} from "../../validation/utils"; -import {compute} from "../transform/compute.ts"; -import {minify} from "../transform/minify.ts"; -import {decompose} from "../transform/utils.ts"; +import {compute, computeMatrix} from "../transform/compute.ts"; +import {renderToken} from "../../renderer"; +import {serialize} from "../transform/matrix.ts"; export class TransformCssFeature { @@ -42,7 +42,7 @@ export class TransformCssFeature { for (; i < ast.chi.length; i++) { // @ts-ignore - node = ast.chi[i] as AstNode | AstDeclaration; + node = ast.chi[i] as AstNode | AstDeclaration; if ( node.typ != EnumToken.DeclarationNodeType || @@ -55,23 +55,39 @@ export class TransformCssFeature { consumeWhitespace(children); - const result = compute(children as Token[]); + let result = compute(children as Token[]); - if (result == null) { + if (result == null) { - // console.error({result}); - return; - } + return; + } + + // console.error(JSON.stringify({result}, null, 1)); + // console.error({result, t: result.map(t => minify(t) ?? t + // )}); + + // console.error({result: decompose2(result)}); + // const decomposed =decompose(result); + // const minified = minify(result); + + const matrix = computeMatrix(result); - const decomposed = decompose(result); - const minified = minify(result); + if (matrix != null) { - // console.error({result, decomposed, minify: minify(result), serialized: renderToken(serialize(result))}); + const m = serialize(matrix); - if (minified != null) { + if (renderToken(m).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - (node as AstDeclaration).val = minified; + result = [m]; + } } + + // console.error({result, serialized: renderToken()}); + + // if (minified != null) { + + (node as AstDeclaration).val = result; + // } } } } diff --git a/src/lib/ast/math/math.ts b/src/lib/ast/math/math.ts index 79aea797..969abe0a 100644 --- a/src/lib/ast/math/math.ts +++ b/src/lib/ast/math/math.ts @@ -14,6 +14,13 @@ export function gcd (x: number, y: number): number { return 1; } + if (y > x) { + + t = x; + x = y; + y = t; + } + while (y) { t = y; diff --git a/src/lib/ast/transform/compute.ts b/src/lib/ast/transform/compute.ts index 9a3e8db1..50677918 100644 --- a/src/lib/ast/transform/compute.ts +++ b/src/lib/ast/transform/compute.ts @@ -4,25 +4,57 @@ import {EnumToken} from "../types.ts"; import {length2Px} from "./convert.ts"; import {transformFunctions} from "../../syntax/index.ts"; import {stripCommaToken} from "../../validation/utils"; -import {translate, translate3d, translateX, translateY, translateZ} from "./translate.ts"; +import {translate, translateX, translateY, translateZ} from "./translate.ts"; import {getAngle, getNumber} from "../../renderer/color"; import {rotate, rotate3D} from "./rotate.ts"; +import {scale, scale3d, scaleX, scaleY, scaleZ} from "./scale.ts"; +import {minify} from "./minify.ts"; +import {serialize} from "./matrix.ts"; -export function compute(transformList: Token[]): Matrix | null { +export function compute(transformLists: Token[]): Token[] | null { - transformList = transformList.slice(); - stripCommaToken(transformList); + transformLists = transformLists.slice(); + stripCommaToken(transformLists); - if (transformList.length == 0) { + if (transformLists.length == 0) { return null; } - const matrix: Matrix = identity(); + const tokens: Matrix[] = []; + let matrix: Matrix | null; + + for (const transformList of splitTransformList(transformLists)) { + + matrix = computeMatrix(transformList); + + if (matrix == null) { + + return null; + } + + tokens.push(matrix); + } + + // for (let i = 0; i < tokens.length; i++) { + // + // for (let j = 0; j < tokens[i].length; j++) { + // + // toZero(tokens[i][j]); + // } + // } + + return tokens.reduce((acc, t) => acc.concat(minify(t) ?? serialize(t)), [] as Token[]); +} + +export function computeMatrix(transformList: Token[]): Matrix | null { + + let matrix: Matrix = identity(); let values: number[] = []; let val: number | null; + let i: number = 0; - for (let i = 0; i < transformList.length; i++) { + for (; i < transformList.length; i++) { if (transformList[i].typ == EnumToken.WhitespaceTokenType) { @@ -52,8 +84,6 @@ export function compute(transformList: Token[]): Matrix | null { const valCount: number = (transformList[i] as FunctionToken).val == 'translate3d' || (transformList[i] as FunctionToken).val == 'translate' ? 3 : 1; - // console.error([(transformList[i] as FunctionToken).val, valCount]); - if (children.length == 1 && children[0].typ == EnumToken.IdenTokenType && children[0].val == 'none') { values.fill(0, 0, valCount); @@ -76,7 +106,6 @@ export function compute(transformList: Token[]): Matrix | null { values.push(val as number); } - } if (values.length == 0 || values.length > valCount) { @@ -86,19 +115,19 @@ export function compute(transformList: Token[]): Matrix | null { if ((transformList[i] as FunctionToken).val == 'translateX') { - translateX(values[0], matrix); + matrix = translateX(values[0], matrix); } else if ((transformList[i] as FunctionToken).val == 'translateY') { - translateY(values[0], matrix); + matrix = translateY(values[0], matrix); } else if ((transformList[i] as FunctionToken).val == 'translateZ') { - translateZ(values[0], matrix); + matrix = translateZ(values[0], matrix); } else { // @ts-ignore - translate(values as [number] | [number, number], matrix); + matrix = translate(values as [number] | [number, number], matrix); } } break; @@ -107,9 +136,7 @@ export function compute(transformList: Token[]): Matrix | null { case 'rotateX': case 'rotateY': case 'rotateZ': - case 'rotate3d': - // - { + case 'rotate3d': { let x: number = 0; let y: number = 0; let z: number = 0; @@ -118,71 +145,127 @@ export function compute(transformList: Token[]): Matrix | null { let values: Token[] = []; let valuesCount: number = (transformList[i] as FunctionToken).val == 'rotate3d' ? 4 : 1; - if (['rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d'].includes((transformList[i] as FunctionToken).val)) { + for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { - for (const child of stripCommaToken((transformList[i] as FunctionToken).chi.slice()) as Token[]) { + if (child.typ == EnumToken.WhitespaceTokenType) { - if (child.typ == EnumToken.WhitespaceTokenType) { - - continue; - } + continue; + } - values.push(child); + values.push(child); - if ((transformList[i] as FunctionToken).val == 'rotateX') { + if ((transformList[i] as FunctionToken).val == 'rotateX') { - x = 1; - } else if ((transformList[i] as FunctionToken).val == 'rotateY') { + x = 1; + } else if ((transformList[i] as FunctionToken).val == 'rotateY') { - y = 1; - } else if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { + y = 1; + } else if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { - z = 1; - } + z = 1; } + } - // console.error({values, valuesCount}); + if (values.length != valuesCount) { - if (values.length != valuesCount) { + return null; + } - return null; + if ((transformList[i] as FunctionToken).val == 'rotate3d') { + + x = getNumber(values[0] as NumberToken); + y = getNumber(values[1] as NumberToken); + z = getNumber(values[2] as NumberToken); + } + + angle = getAngle(values.at(-1) as AngleToken | NumberToken); + + if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { + + return null; + } + + if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { + + matrix = rotate(angle * 2 * Math.PI, matrix); + } else { + + matrix = rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + } + } + + break; + + case 'scale': + case 'scaleX': + case 'scaleY': + case 'scaleZ': + case 'scale3d': { + + values.length = 0; + + let child: Token; + + for (let k = 0; k < (transformList[i] as FunctionToken).chi.length; k++) { + + child = (transformList[i] as FunctionToken).chi[k]; + + if (child.typ == EnumToken.CommentTokenType || child.typ == EnumToken.WhitespaceTokenType) { + + continue; } - if ((transformList[i] as FunctionToken).val == 'rotate3d') { + if (child.typ != EnumToken.NumberTokenType) { - x = getNumber(values[0] as NumberToken); - y = getNumber(values[1] as NumberToken); - z = getNumber(values[2] as NumberToken); + return null; } - angle = getAngle(values.at(-1) as AngleToken | NumberToken); + values.push(getNumber(child)); + } - if ([x, y, z, angle].some(t => typeof t != 'number' || Number.isNaN(+t))) { + if (values.length == 0) { + + return null; + } + + if ((transformList[i] as FunctionToken).val == 'scale3d') { + + if (values.length != 3) { return null; } - // console.error([x, y, z, angle * 2 * Math.PI]); + matrix = scale3d(...values as [number, number, number], matrix); + break; + } - // if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { + if ((transformList[i] as FunctionToken).val == 'scale') { - rotate3D(angle * 2 * Math.PI, x, y, z, matrix); - // console.error({matrix}); - // continue; - // } + if (values.length != 1 && values.length != 2) { - // console.error({values}); + return null; + } - // return null; + matrix = scale(values[0], values[1] ?? values[0], matrix); + break; } - // else - // if ((transformList[i] as FunctionToken).val == 'rotate') { - // - // } - } + if (values.length != 1) { + + return null; + } else if ((transformList[i] as FunctionToken).val == 'scaleX') { + + matrix = scaleX(values[0], matrix); + } else if ((transformList[i] as FunctionToken).val == 'scaleY') { + + matrix = scaleY(values[0], matrix); + } else if ((transformList[i] as FunctionToken).val == 'scaleZ') { + + matrix = scaleZ(values[0], matrix); + } break; + } default: @@ -191,5 +274,49 @@ export function compute(transformList: Token[]): Matrix | null { } } - return matrix; + return matrix +} + +function splitTransformList(transformList: Token[]): Token[][] { + + let pattern: string | null = null; + + const tokens: Token[][] = []; + + for (let i = 0; i < transformList.length; i++) { + + if (transformList[i].typ == EnumToken.CommentTokenType || transformList[i].typ == EnumToken.WhitespaceTokenType) { + + continue; + } + + if (pattern == null || (transformList[i].typ == EnumToken.FunctionTokenType && !(transformList[i] as FunctionToken).val.startsWith(pattern))) { + + if (transformList[i].typ == EnumToken.FunctionTokenType) { + + if ((transformList[i] as FunctionToken).val.startsWith('scale')) { + pattern = 'scale'; + } else if ((transformList[i] as FunctionToken).val.startsWith('rotate')) { + pattern = 'rotate'; + } else if ((transformList[i] as FunctionToken).val.startsWith('translate')) { + pattern = 'translate'; + } else { + pattern = null; + } + + tokens.push([transformList[i]]); + continue; + } + } + + if (pattern != null && transformList[i].typ == EnumToken.FunctionTokenType && (transformList[i] as FunctionToken).val.startsWith(pattern)) { + + tokens[tokens.length - 1].push(transformList[i]); + continue; + } + + tokens.push([transformList[i]]); + } + + return tokens; } \ No newline at end of file diff --git a/src/lib/ast/transform/matrix.ts b/src/lib/ast/transform/matrix.ts index ae5998a9..2c7333a1 100644 --- a/src/lib/ast/transform/matrix.ts +++ b/src/lib/ast/transform/matrix.ts @@ -1,7 +1,7 @@ import {identity, is2DMatrix, Matrix} from "./utils.ts"; import {EnumToken} from "../types.ts"; import type {Token} from "../../../@types/index.d.ts"; -import {reduceNumber} from "../../renderer"; +import {reduceNumber} from "../../renderer/render.ts"; export function matrix(values: [number, number, number, number, number, number] | [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]): Matrix | null { @@ -51,7 +51,7 @@ export function serialize(matrix: Matrix): Token { val: 'matrix', chi: [ matrix[0][0], - matrix[0][1], + matrix[1][1], matrix[1][0], matrix[1][1], matrix[3][0], @@ -65,7 +65,7 @@ export function serialize(matrix: Matrix): Token { acc.push({ typ: EnumToken.NumberTokenType, - val: reduceNumber(t) + val: reduceNumber(t.toPrecision(6)) }) return acc @@ -75,6 +75,8 @@ export function serialize(matrix: Matrix): Token { let m: Token[] = []; + // console.error(JSON.stringify({matrix},null, 1)); + for (let i = 0; i < matrix.length; i++) { for (let j = 0; j < matrix[i].length; j ++) { @@ -86,7 +88,7 @@ export function serialize(matrix: Matrix): Token { m.push({ typ: EnumToken.NumberTokenType, - val: reduceNumber(matrix[j][i]) + val: reduceNumber(matrix[i][j].toPrecision(6)) }) } } @@ -97,4 +99,4 @@ export function serialize(matrix: Matrix): Token { val: 'matrix3d', chi: m } -} \ No newline at end of file +} diff --git a/src/lib/ast/transform/minify.ts b/src/lib/ast/transform/minify.ts index b46eb7bb..10f54d47 100644 --- a/src/lib/ast/transform/minify.ts +++ b/src/lib/ast/transform/minify.ts @@ -1,19 +1,21 @@ -import {decompose, Matrix} from "./utils.ts"; +import {decompose, identity, Matrix} from "./utils.ts"; import {EnumToken} from "../types.ts"; import {Token} from "../../../@types"; -import {gcd} from "../math/math.ts"; +import {eq} from "../../parser/utils/eq.ts"; export function minify(matrix: Matrix): Token[] | null { const decomposed = decompose(matrix); + // console.error({decomposed}); if (decomposed == null) { return null; } - const transforms = new Set(['translate', 'scale', 'skew', 'perspective']); + const transforms = new Set([ + 'translate', 'scale', 'skew', 'perspective', 'rotate']); const rotations = new Set(['x', 'y', 'z']); const scales = new Set(['x', 'y', 'z']); @@ -40,109 +42,167 @@ export function minify(matrix: Matrix): Token[] | null { transforms.delete('perspective'); } - // if (transforms.size == 0) { - // - // // identity - // return [{ - // typ: EnumToken.FunctionTokenType, - // val: 'scale', - // chi: [ - // {typ: EnumToken.NumberTokenType, val: '1'} - // ] - // } - // ]; - // } + if (decomposed.rotate[3] == 0) { - if (transforms.size == 1) { + transforms.delete('rotate'); + } - if (transforms.has('translate')) { + if (transforms.has('translate')) { - let coordinates = new Set(['x', 'y', 'z']); + let coordinates = new Set(['x', 'y', 'z']); - for (let i = 0; i < 3; i++) { + for (let i = 0; i < 3; i++) { - if (decomposed.translate[i] == 0) { + if (decomposed.translate[i] == 0) { - coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); - } + coordinates.delete(i == 0 ? 'x' : i == 1 ? 'y' : 'z'); } + } + + if (coordinates.size == 3) { - if (coordinates.size == 3) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px'} + ] + }) + } else if (coordinates.size == 1) { + + if (coordinates.has('x')) { result.push({ typ: EnumToken.FunctionTokenType, val: 'translate', - chi: [ - {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, - {typ: EnumToken.CommaTokenType}, - {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'}, - {typ: EnumToken.CommaTokenType}, - {typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px'} - ] - }) - } else if (coordinates.size == 1) { + chi: [{typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}] + }); + } else { + + let axis: string = coordinates.has('y') ? 'y' : 'z'; + let index: number = axis == 'y' ? 1 : 2; + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate' + axis.toUpperCase(), + chi: [{typ: EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px'}] + }); + } + } else if (coordinates.has('z')) { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px'} + ] + }); + } else { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'translate', + chi: [ + decomposed.translate[0] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, + {typ: EnumToken.CommaTokenType}, + decomposed.translate[1] == 0 ? { + typ: EnumToken.NumberTokenType, + 'val': '0' + } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'} + ] + }); + } + } + + // scale(x, x) -> scale(x) + // scale(1, sy) -> scaleY(sy) + // scale3d(1, 1, sz) -> scaleZ(sz) + // scaleX() scale(Y) scaleZ() -> scale3d() + + if (transforms.has('scale')) { - if (coordinates.has('x')) { + const [sx, sy, sz] = decomposed.scale; - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'translate', - chi: [{typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}] - }); - } else { + if (!(sx == 1 && sy == 1 && sz == 1)) { + + if (sz == 1) { + + scales.delete('z'); + } - let axis: string = coordinates.has('y') ? 'y' : 'z'; - let index: number = axis == 'y' ? 1 : 2; + if (sy == 1) { + + scales.delete('y'); + } + + if (sx == 1) { + + scales.delete('x'); + } - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'translate' + axis.toUpperCase(), - chi: [{typ: EnumToken.LengthTokenType, val: decomposed.translate[index] + '', unit: 'px'}] - }); - } - } else if (coordinates.has('z')) { + if (scales.size == 1) { + + let prefix: string = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; result.push({ typ: EnumToken.FunctionTokenType, - val: 'translate', + val: 'scale' + prefix, chi: [ - decomposed.translate[0] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, - {typ: EnumToken.CommaTokenType}, - decomposed.translate[1] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'}, - {typ: EnumToken.CommaTokenType}, - {typ: EnumToken.LengthTokenType, val: decomposed.translate[2] + '', unit: 'px'} + {typ: EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx)} ] }); - } else { + } + + else if (!scales.has('z')) { result.push({ typ: EnumToken.FunctionTokenType, - val: 'translate', + val: 'scale', chi: [ - decomposed.translate[0] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, + {typ: EnumToken.NumberTokenType, val: '' + sx}, + {typ: EnumToken.WhitespaceTokenType}, + {typ: EnumToken.NumberTokenType, val: '' + sy}, + ] + }); + } + + else { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'scale3d', + chi: [ + {typ: EnumToken.NumberTokenType, val: '' + sx}, {typ: EnumToken.CommaTokenType}, - decomposed.translate[1] == 0 ? { - typ: EnumToken.NumberTokenType, - 'val': '0' - } : {typ: EnumToken.LengthTokenType, val: decomposed.translate[1] + '', unit: 'px'} + {typ: EnumToken.NumberTokenType, val: '' + sy}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.NumberTokenType, val: '' + sz} ] }); } } } - { + if (transforms.has('rotate')) { - const {x, y, z, angle} = getRotation3D(matrix); + const [x, y, z, angle] = decomposed.rotate; // console.error({x, y, z, angle}); @@ -219,102 +279,12 @@ export function minify(matrix: Matrix): Token[] | null { } } - // scale(x, x) -> scale(x) - // scale(1, sy) -> scaleY(sy) - // scale3d(1, 1, sz) -> scaleZ(sz) - // scaleX() scale(Y) scaleZ() -> scale3d() // identity - return result.length == 0 ? [ + return transforms.size >1 ? null : result.length == 0 || eq(result, identity()) ? [ { - typ: EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - {typ: EnumToken.NumberTokenType, val: '1'} - ] + typ: EnumToken.IdenTokenType, + val: 'none' } ] : result; } - -// Fonction pour calculer rotate3d à partir de matrix3d -function getRotation3D(matrix: Matrix): { x: number, y: number, z: number, angle: number } { - - // Extraire la sous-matrice 3x3 de rotation - const r11: number = matrix[0][0], r12: number = matrix[0][1], r13: number = matrix[0][2]; - const r21: number = matrix[1][0], r22: number = matrix[1][1], r23: number = matrix[1][2]; - const r31: number = matrix[2][0], r32: number = matrix[2][1], r33: number = matrix[2][2]; - - // Calculer la trace (somme des éléments diagonaux) - const trace: number = r11 + r22 + r33; - - // Calculer l’angle de rotation (en radians) - const cosTheta: number = (trace - 1) / 2; - - // Calculer sin(θ) avec le signe correct - const sinThetaRaw: number = Math.sqrt(1 - cosTheta * cosTheta); - const xRaw: number = r32 - r23; // -0.467517 - const yRaw: number = r13 - r31; // 0.776535 - const zRaw: number = r21 - r12; // 0.776535 - // Déterminer le signe de sin(θ) basé sur la direction - const sinTheta: number = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; - - // Calculer l’angle avec atan2 - const angle: number = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(12); - let x, y, z; - - if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° - - const x1: number = r32 - r23; - const y1: number = r13 - r31; - const z1: number = r21 - r12; - - switch (Math.max(x1, y1, z1)) { - - case x1: - - x = 1; - y = 0; - z = 0; - break - - case y1: - - x = 0; - y = 1; - z = 0; - break; - - default: - - x = 0; - y = 0; - z = 1; - break; - } - - } else { - x = (r32 - r23) / (2 * sinTheta); - y = (r13 - r31) / (2 * sinTheta); - z = (r21 - r12) / (2 * sinTheta); - } - - // Normaliser le vecteur (optionnel, mais utile pour vérification) - const length: number = Math.sqrt(x * x + y * y + z * z); - - if (length > 0) { - x /= length; - y /= length; - z /= length; - } - - const x1: number = gcd(x, gcd(y, z)); - - if (x1 != 0) { - - x /= x1; - y /= x1; - z /= x1; - } - - return {x, y, z, angle}; -} \ No newline at end of file diff --git a/src/lib/ast/transform/rotate.ts b/src/lib/ast/transform/rotate.ts index 00845b37..8ae77ac4 100644 --- a/src/lib/ast/transform/rotate.ts +++ b/src/lib/ast/transform/rotate.ts @@ -1,4 +1,4 @@ -import {Matrix} from "./utils.ts"; +import {identity, Matrix, multiply} from "./utils.ts"; /** * angle in radian @@ -6,12 +6,13 @@ import {Matrix} from "./utils.ts"; * @param x * @param y * @param z - * @param matrix + * @param from */ -export function rotate3D(angle: number, x: number, y: number, z: number, matrix:Matrix): Matrix { +export function rotate3D(angle: number, x: number, y: number, z: number, from: Matrix): Matrix { - const sc: number= Math.sin(angle/2)*Math.cos(angle/2); - const sq: number = Math.sin(angle/2) * Math.sin(angle/2); + const matrix: Matrix = identity(); + const sc: number = Math.sin(angle / 2) * Math.cos(angle / 2); + const sq: number = Math.sin(angle / 2) * Math.sin(angle / 2); const norm: number = Math.sqrt(x * x + y * y + z * z); const unit: number = norm === 0 ? 0 : 1 / norm; @@ -20,27 +21,30 @@ export function rotate3D(angle: number, x: number, y: number, z: number, matrix: y *= unit; z *= unit; - matrix[0][0] = 1 - 2 * (y*y + z*z) * sq; - matrix[0][1] = 2 * (x * y * sq - z*sc); - matrix[0][2] = 2 * (x*z*sq + y*sc); + matrix[0][0] = 1 - 2 * (y * y + z * z) * sq; + matrix[0][1] = 2 * (x * y * sq + z * sc); + matrix[0][2] = 2 * (x * z * sq - y * sc); - matrix[1][0] = 2 * (x*y*sq + z*sc); - matrix[1][1] = 1 - 2 * (x*x + z*z) * sq; - matrix[1][2] = 2 * (y*z*sq - x*sc); + matrix[1][0] = 2 * (x * y * sq - z * sc); + matrix[1][1] = 1 - 2 * (x * x + z * z) * sq; + matrix[1][2] = 2 * (y * z * sq + x * sc); - matrix[2][0] = 2 * (x*z*sq - y*sc); - matrix[2][1] = 2 * (y*z*sq + x*sc); - matrix[2][2] = 1 - 2 * (x*x + y*y) * sq; + matrix[2][0] = 2 * (x * z * sq + y * sc); + matrix[2][1] = 2 * (y * z * sq - x * sc); + matrix[2][2] = 1 - 2 * (x * x + y * y) * sq; - return matrix; + return multiply(from, matrix); } -export function rotate(angle: number, matrix: Matrix): Matrix { +export function rotate(angle: number, from: Matrix): Matrix { + const matrix: Matrix = identity(); matrix[0][0] = Math.cos(angle); matrix[0][1] = Math.sin(angle); matrix[1][0] = -Math.sin(angle); matrix[1][1] = Math.cos(angle); - return matrix; -} \ No newline at end of file + return multiply(from, matrix); +} + +export const rotateZ = rotate; \ No newline at end of file diff --git a/src/lib/ast/transform/scale.ts b/src/lib/ast/transform/scale.ts index ea114ce4..537c6dd8 100644 --- a/src/lib/ast/transform/scale.ts +++ b/src/lib/ast/transform/scale.ts @@ -1,39 +1,50 @@ -import {identity, Matrix} from "./utils.ts"; +import {identity, Matrix, multiply} from "./utils.ts"; -export function scaleX(x: number): Matrix { - - - const matrix: Matrix = identity(); +export function scaleX(x: number, from: Matrix): Matrix { + const matrix = identity(); matrix[0][0] = x; + // matrix[1][1] = 1; + // matrix[2][2] = 1; - return matrix; + return multiply(from, matrix); } -export function scaleY(y: number): Matrix { - - const matrix: Matrix = identity(); +export function scaleY(y: number, from: Matrix): Matrix { + const matrix = identity(); + // matrix[0][0] = 1; matrix[1][1] = y; + // matrix[2][2] = 1; - return matrix; + return multiply(from, matrix); } -export function scaleZ(z: number): Matrix { - - const matrix: Matrix = identity(); +export function scaleZ(z: number, from: Matrix): Matrix { + const matrix = identity(); + // matrix[0][0] = 1; + // matrix[1][1] = 1; matrix[2][2] = z; - return matrix; + return multiply(from, matrix) as Matrix; } -export function scale(x: number, y?: number): Matrix { +export function scale(x: number, y: number, from: Matrix): Matrix { - const matrix: Matrix = identity(); + const matrix = identity(); + matrix[0][0] = x; + matrix[1][1] = y; + return multiply(from, matrix); +} + +export function scale3d(x: number, y: number, z: number, from: Matrix): Matrix { + + const matrix = identity(); matrix[0][0] = x; - matrix[1][1] = y ?? x; + matrix[1][1] = y; + matrix[2][2] = z; - return matrix; + return multiply(from, matrix); } \ No newline at end of file diff --git a/src/lib/ast/transform/translate.ts b/src/lib/ast/transform/translate.ts index a09683e0..65d24a78 100644 --- a/src/lib/ast/transform/translate.ts +++ b/src/lib/ast/transform/translate.ts @@ -1,33 +1,38 @@ -import {Matrix} from "./utils.ts"; +import {identity, Matrix, multiply} from "./utils.ts"; -export function translateX(x: number, matrix: Matrix): Matrix { +export function translateX(x: number, from: Matrix): Matrix { + + const matrix = identity(); matrix[3][0] = x; - return matrix; + return multiply(from, matrix) as Matrix; } -export function translateY(y: number, matrix: Matrix): Matrix { +export function translateY(y: number, from: Matrix): Matrix { + const matrix = identity() matrix[3][1] = y; - return matrix; + return multiply(from, matrix) as Matrix; } -export function translateZ(z: number, matrix: Matrix): Matrix { +export function translateZ(z: number, from: Matrix): Matrix { + const matrix = identity(); matrix[3][2] = z; - return matrix; + return multiply(from, matrix) as Matrix; } -export function translate(translate: [number] | [number, number] | [number, number, number], matrix: Matrix): Matrix { +export function translate(translate: [number] | [number, number] | [number, number, number], from: Matrix): Matrix { + const matrix = identity(); matrix[3][0] = translate[0]; matrix[3][1] = translate[1] ?? 0; matrix[3][2] = translate[2] ?? 0; - return matrix; + return multiply(from, matrix) as Matrix; } export const translate3d = translate; \ No newline at end of file diff --git a/src/lib/ast/transform/utils.ts b/src/lib/ast/transform/utils.ts index 2abd0543..e4ab8a70 100644 --- a/src/lib/ast/transform/utils.ts +++ b/src/lib/ast/transform/utils.ts @@ -1,3 +1,5 @@ +import {gcd} from "../math/math.ts"; + export declare type Point = [number, number, number]; export declare type Vector = [number, number, number, number]; export declare type Matrix = [Vector, Vector, Vector, Vector]; @@ -5,10 +7,10 @@ export declare type Matrix = [Vector, Vector, Vector, Vector]; interface DecomposedMatrix3D { skew: [number, number, number]; scale: [number, number, number]; - // rotate: [number, number, number]; - translate : [number, number, number]; - perspective : [number, number, number, number]; - quaternion : [number, number, number, number]; + rotate: [number, number, number, number]; + translate: [number, number, number]; + perspective: [number, number, number, number]; + quaternion: [number, number, number, number]; } function determinant(matrix: Matrix): number { @@ -110,7 +112,7 @@ function transpose(matrix: Matrix): Matrix { function multVecMatrix(vector: Vector, matrix: Matrix): Vector { - const result:Vector = [0, 0, 0, 0]; + const result: Vector = [0, 0, 0, 0]; for (let i = 0; i < 4; i++) { @@ -136,7 +138,40 @@ function normalize(point: Point): Point { return norm === 0 ? [0, 0, 0] : [x / norm, y / norm, z / norm]; } -function dot(point1: Point, point2: Point): number { +function interpolate(quaternionA: [number, number, number, number], quaternionB: [number, number, number, number], t: number = 1) { + + let product = dot(quaternionA, quaternionB); + + const quaternionDst: [number, number, number, number] = [0, 0, 0, 0]; +// Clamp product to -1.0 <= product <= 1.0 + product = Math.min(product, 1.0); + product = Math.max(product, -1.0); + + if (Math.abs(product) === 1.0) { + return quaternionA; + } + + const theta = Math.acos(product); + const w = Math.sin(t * theta) / Math.sqrt(1 - product * product); + + for (let i = 0; i < 4; i++) { + quaternionA[i] *= Math.cos(t * theta) - product * w; + quaternionB[i] *= w; + quaternionDst[i] = quaternionA[i] + quaternionB[i]; + } + + return; +} + +function dot(point1: Point, point2: Point): number; +function dot(point1: [number, number, number, number], point2: [number, number, number, number]): number; + +function dot(point1: Point | [number, number, number, number], point2: Point | [number, number, number, number]): number { + + if (point1.length === 4 && point2.length === 4) { + + return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2] + point1[3] * point2[3]; + } return point1[0] * point2[0] + point1[1] * point2[1] + point1[2] * point2[2]; } @@ -149,14 +184,15 @@ function combine(point1: Point, point2: Point, ascl: number, bscl: number): Poin function cross(a: Point, b: Point): Point { return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; } -function multiply(matrixA: Matrix, matrixB: Matrix): Matrix { + +export function multiply(matrixA: Matrix, matrixB: Matrix): Matrix { let result: Matrix = Array(4).fill(0).map(() => Array(4).fill(0)) as Matrix; for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { for (let k = 0; k < 4; k++) { - result[i][j] += matrixA[i][k] * matrixB[k][j]; + result[j][i] += matrixA[k][i] * matrixB[j][k]; } } } @@ -164,6 +200,87 @@ function multiply(matrixA: Matrix, matrixB: Matrix): Matrix { return result; } +export function decompose2(matrix: Matrix) { + + let row0x = matrix[0][0] + let row0y = matrix[1][0] + let row1x = matrix[0][1] + let row1y = matrix[1][1] + + // @ts-ignore + const translate: [number, number] = [] as [number, number]; + // @ts-ignore + const scale: [number, number] = [] as [number, number]; + + translate[0] = matrix[0][3] + translate[1] = matrix[1][3] + + scale[0] = Math.sqrt(row0x * row0x + row0y * row0y) + scale[1] = Math.sqrt(row1x * row1x + row1y * row1y) + +// If determinant is negative, one axis was flipped. + let determinant = row0x * row1y - row0y * row1x + if (determinant < 0) { + + // Flip axis with minimum unit vector dot product. + if (row0x < row1y) { + + scale[0] = -scale[0] + } else { + + scale[1] = -scale[1] + } + } + +// Renormalize matrix to remove scale. + if (scale[0]) { + + row0x *= 1 / scale[0] + row0y *= 1 / scale[0] + } + + + if (scale[1]) { + + row1x *= 1 / scale[1] + row1y *= 1 / scale[1] + } + +// Compute rotation and renormalize matrix. + let angle = Math.atan2(row0y, row0x); + + if (angle) { + + let sn = -row0y + // Rotate(-angle) = [cos(angle), sin(angle), -sin(angle), cos(angle)] + // = [row0x, -row0y, row0y, row0x] + // Thanks to the normalization above. + let cs = row0x + let m11 = row0x + let m12 = row0y + let m21 = row1x + let m22 = row1y + row0x = cs * m11 + sn * m21 + row0y = cs * m12 + sn * m22 + row1x = -sn * m11 + cs * m21 + row1y = -sn * m12 + cs * m22 + } + + let m11 = row0x + let m12 = row0y + let m21 = row1x + let m22 = row1y + +// Convert into degrees because our rotation functions expect it. +// angle = rad2deg(angle) + + angle *= 180 / Math.PI; + + return { + translate, scale, angle: +angle.toPrecision(12), m11, m12, m21, m22 + } +} + export function decompose(matrix: Matrix): DecomposedMatrix3D | null { // Normalize the matrix. if (matrix[3][3] === 0) { @@ -297,19 +414,11 @@ export function decompose(matrix: Matrix): DecomposedMatrix3D | null { quaternion[2] = -quaternion[2]; } - // const rad2deg = 180 / Math.PI; - // const theta: number = Math.atan2(matrix[1][0], matrix[0][0]) * rad2deg; - // const zAxis = - // - // console.error({theta}); - // - // const rotation: [number, number, number] = [-Math.asin(matrix[0][2]) * rad2deg, 0, 0]; - // apply rotation - let x = quaternion[0]; - let y = quaternion[1]; - let z = quaternion[2]; - let w = quaternion[3]; + let x: number = quaternion[0]; + let y: number = quaternion[1]; + let z: number = quaternion[2]; + let w: number = quaternion[3]; const rotationMatrix: Matrix = identity(); // Construct a composite rotation matrix from the quaternion values @@ -324,16 +433,120 @@ export function decompose(matrix: Matrix): DecomposedMatrix3D | null { rotationMatrix[2][1] = 2 * (y * z + x * w); rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); + const {x: x1, y: y1, z: z1, angle} = getRotation3D(rotationMatrix); + return { - skew, - scale, - // rotate: [+rx.toPrecision(12), +ry.toPrecision(12), +rz.toPrecision(12)], - translate, + skew: toZero(skew) as [number, number, number], + scale: toZero(scale) as [number, number, number], + rotate: toZero([x1, y1, z1, angle]) as [number, number, number, number], + translate: toZero(translate) as [number, number, number], perspective, quaternion }; } +export function toZero(v: [number, number] | [number, number, number] | [number, number, number, number]) { + + for (let i = 0; i < v.length; i++) { + + if (Math.abs(v[i]) <= 1e-6) { + + v[i] = 0; + } else { + + v[i] = +v[i].toPrecision(6); + } + } + + return v; +} + + +// Fonction pour calculer rotate3d à partir de matrix3d +function getRotation3D(matrix: Matrix): { x: number, y: number, z: number, angle: number } { + + // Extraire la sous-matrice 3x3 de rotation + const r11: number = matrix[0][0], r12: number = matrix[0][1], r13: number = matrix[0][2]; + const r21: number = matrix[1][0], r22: number = matrix[1][1], r23: number = matrix[1][2]; + const r31: number = matrix[2][0], r32: number = matrix[2][1], r33: number = matrix[2][2]; + + // Calculer la trace (somme des éléments diagonaux) + const trace: number = r11 + r22 + r33; + + // Calculer l’angle de rotation (en radians) + const cosTheta: number = (trace - 1) / 2; + + // Calculer sin(θ) avec le signe correct + const sinThetaRaw: number = Math.sqrt(1 - cosTheta * cosTheta); + const xRaw: number = r32 - r23; // -0.467517 + const yRaw: number = r13 - r31; // 0.776535 + const zRaw: number = r21 - r12; // 0.776535 + // Déterminer le signe de sin(θ) basé sur la direction + const sinTheta: number = (xRaw < 0 && yRaw > 0 && zRaw > 0) ? -sinThetaRaw : sinThetaRaw; + + // Calculer l’angle avec atan2 + const angle: number = +(Math.atan2(sinTheta, cosTheta) * 180 / Math.PI).toFixed(6); + let x = 0, y = 0, z = 0; + + if (Math.abs(sinTheta) < 1e-6) { // Cas où l’angle est proche de 0° ou 180° + + const x1: number = +r11.toPrecision(6); + const y1: number = +r22.toPrecision(6); + const z1: number = +r33.toPrecision(6); + const max: number = Math.max(x1, y1, z1); + + x = y = z = 0; + + if (max === x1) { + x = 1; + } + + if (max === y1) { + y = 1; + } + + if (max === z1) { + z = 1; + } + + } else { + x = (r32 - r23) / (2 * sinTheta); + y = (r13 - r31) / (2 * sinTheta); + z = (r21 - r12) / (2 * sinTheta); + } + + // Normaliser le vecteur (optionnel, mais utile pour vérification) + const length: number = Math.sqrt(x * x + y * y + z * z); + + if (length > 0) { + x /= length; + y /= length; + z /= length; + } + + const pc = Math.abs(gcd(x, gcd(y, z))); + + + if (pc > 0.1 && pc <= Math.abs(x) && pc <= Math.abs(y) && pc <= Math.abs(z)) { + + x /= pc; + y /= pc; + z /= pc; + } else { + + const min = Math.min(Math.abs(x), Math.abs(y), Math.abs(z)); + + if (min > 0.1) { + + x = +(x / min).toPrecision(6); + y = +(y / min).toPrecision(6); + z = +(z / min).toPrecision(6); + } + } + + return {x, y, z, angle}; +} + export function recompose( translate: [number, number, number], scale: [number, number, number], @@ -373,7 +586,6 @@ export function recompose( rotationMatrix[2][0] = 2 * (x * z - y * w); rotationMatrix[2][1] = 2 * (y * z + x * w); rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); - // console.error({rotationMatrix}); matrix = multiply(matrix, rotationMatrix); @@ -413,13 +625,13 @@ export function is2DMatrix(matrix: Matrix): boolean { // m13,m14, m23, m24, m31, m32, m34, m43 are all 0 return matrix[0][2] === 0 && - matrix[0][3] === 0 && - matrix[1][2] === 0 && - matrix[1][3] === 0 && - matrix[2][0] === 0 && - matrix[2][1] === 0 && - matrix[2][3] === 0 && - matrix[3][2] === 0 && + matrix[0][3] === 0 && + matrix[1][2] === 0 && + matrix[1][3] === 0 && + matrix[2][0] === 0 && + matrix[2][1] === 0 && + matrix[2][3] === 0 && + matrix[3][2] === 0 && matrix[2][2] === 1 && matrix[3][3] === 1; } \ No newline at end of file diff --git a/test/specs/code/malformed.js b/test/specs/code/malformed.js index 76c35c24..e7f9cecd 100644 --- a/test/specs/code/malformed.js +++ b/test/specs/code/malformed.js @@ -173,7 +173,7 @@ color: preserveLicense: true }).code).equals(`a { color: cyan; - transform: rotate(179.999579080068deg) + transform: rotate(180deg) }`)); }); }); diff --git a/test/specs/code/transform.js b/test/specs/code/transform.js index 696552c5..9a8615e1 100644 --- a/test/specs/code/transform.js +++ b/test/specs/code/transform.js @@ -67,7 +67,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil return transform(nesting1, { beautify: true }).then((result) => expect(result.code).equals(`.now { - transform: scale(1) + transform: none }`)); }); @@ -81,7 +81,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil return transform(nesting1, { beautify: true }).then((result) => expect(result.code).equals(`.now { - transform: scale(1) + transform: none }`)); }); @@ -160,5 +160,47 @@ export function run(describe, expect, transform, parse, render, dirname, readFil }`)); }); + it('rotateZ #10', function () { + const nesting1 = ` + + .now { + transform: scaleX(0.5) scaleY( 1) scaleZ(1.7) rotate3d(1, 1, 1, 67deg) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: scale3d(.5,1,1.7)rotate3d(1,1,1,67deg) +}`)); + }); + + it('rotateY #11', function () { + const nesting1 = ` + + .now { + transform: rotateY(180deg) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: rotateY(180deg) +}`)); + }); + + it('rotate3d #12', function () { + const nesting1 = ` + + .now { + transform: rotate3d(1, 1, 1, 180deg) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: rotate3d(1,1,1,180deg) +}`)); + }); + }); } \ No newline at end of file From 7ba49b76461a1a9ac65b099af2075b711d6aaf51 Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Sun, 6 Apr 2025 04:42:58 -0400 Subject: [PATCH 06/10] scale() support #75 --- dist/index-umd-web.js | 102 ++++++++++++++------------ dist/index.cjs | 102 ++++++++++++++------------ dist/lib/ast/transform/compute.js | 11 ++- dist/lib/ast/transform/minify.js | 2 +- jsr.json | 2 +- package.json | 2 +- src/lib/ast/transform/compute.ts | 16 +++- src/lib/ast/transform/minify.ts | 2 +- test/specs/code/transform.js | 117 ++++++++++++++++++++++++++++++ 9 files changed, 254 insertions(+), 102 deletions(-) diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 82f4b487..46d36f30 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -18221,7 +18221,7 @@ val: 'scale', chi: [ { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, - { typ: exports.EnumToken.WhitespaceTokenType }, + { typ: exports.EnumToken.CommaTokenType }, { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, ] }); @@ -18323,51 +18323,6 @@ ] : result; } - function serialize(matrix) { - if (is2DMatrix(matrix)) { - // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset - return { - typ: exports.EnumToken.FunctionTokenType, - val: 'matrix', - chi: [ - matrix[0][0], - matrix[1][1], - matrix[1][0], - matrix[1][1], - matrix[3][0], - matrix[3][1] - ].reduce((acc, t) => { - if (acc.length > 0) { - acc.push({ typ: exports.EnumToken.CommaTokenType }); - } - acc.push({ - typ: exports.EnumToken.NumberTokenType, - val: reduceNumber(t.toPrecision(6)) - }); - return acc; - }, []) - }; - } - let m = []; - // console.error(JSON.stringify({matrix},null, 1)); - for (let i = 0; i < matrix.length; i++) { - for (let j = 0; j < matrix[i].length; j++) { - if (m.length > 0) { - m.push({ typ: exports.EnumToken.CommaTokenType }); - } - m.push({ - typ: exports.EnumToken.NumberTokenType, - val: reduceNumber(matrix[i][j].toPrecision(6)) - }); - } - } - return { - typ: exports.EnumToken.FunctionTokenType, - val: 'matrix3d', - chi: m - }; - } - function compute(transformLists) { transformLists = transformLists.slice(); stripCommaToken(transformLists); @@ -18390,7 +18345,15 @@ // toZero(tokens[i][j]); // } // } - return tokens.reduce((acc, t) => acc.concat(minify$1(t) ?? serialize(t)), []); + const result = []; + for (const token of tokens) { + let t = minify$1(token); + if (t == null) { + return null; + } + result.push(...t); + } + return result; } function computeMatrix(transformList) { let matrix = identity(); @@ -18586,6 +18549,51 @@ return tokens; } + function serialize(matrix) { + if (is2DMatrix(matrix)) { + // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix', + chi: [ + matrix[0][0], + matrix[1][1], + matrix[1][0], + matrix[1][1], + matrix[3][0], + matrix[3][1] + ].reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(t.toPrecision(6)) + }); + return acc; + }, []) + }; + } + let m = []; + // console.error(JSON.stringify({matrix},null, 1)); + for (let i = 0; i < matrix.length; i++) { + for (let j = 0; j < matrix[i].length; j++) { + if (m.length > 0) { + m.push({ typ: exports.EnumToken.CommaTokenType }); + } + m.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(matrix[i][j].toPrecision(6)) + }); + } + } + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: m + }; + } + class TransformCssFeature { static get ordering() { return 4; diff --git a/dist/index.cjs b/dist/index.cjs index 8ac1e834..463cb459 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -18320,7 +18320,7 @@ function minify$1(matrix) { val: 'scale', chi: [ { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, - { typ: exports.EnumToken.WhitespaceTokenType }, + { typ: exports.EnumToken.CommaTokenType }, { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, ] }); @@ -18422,51 +18422,6 @@ function minify$1(matrix) { ] : result; } -function serialize(matrix) { - if (is2DMatrix(matrix)) { - // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset - return { - typ: exports.EnumToken.FunctionTokenType, - val: 'matrix', - chi: [ - matrix[0][0], - matrix[1][1], - matrix[1][0], - matrix[1][1], - matrix[3][0], - matrix[3][1] - ].reduce((acc, t) => { - if (acc.length > 0) { - acc.push({ typ: exports.EnumToken.CommaTokenType }); - } - acc.push({ - typ: exports.EnumToken.NumberTokenType, - val: reduceNumber(t.toPrecision(6)) - }); - return acc; - }, []) - }; - } - let m = []; - // console.error(JSON.stringify({matrix},null, 1)); - for (let i = 0; i < matrix.length; i++) { - for (let j = 0; j < matrix[i].length; j++) { - if (m.length > 0) { - m.push({ typ: exports.EnumToken.CommaTokenType }); - } - m.push({ - typ: exports.EnumToken.NumberTokenType, - val: reduceNumber(matrix[i][j].toPrecision(6)) - }); - } - } - return { - typ: exports.EnumToken.FunctionTokenType, - val: 'matrix3d', - chi: m - }; -} - function compute(transformLists) { transformLists = transformLists.slice(); stripCommaToken(transformLists); @@ -18489,7 +18444,15 @@ function compute(transformLists) { // toZero(tokens[i][j]); // } // } - return tokens.reduce((acc, t) => acc.concat(minify$1(t) ?? serialize(t)), []); + const result = []; + for (const token of tokens) { + let t = minify$1(token); + if (t == null) { + return null; + } + result.push(...t); + } + return result; } function computeMatrix(transformList) { let matrix = identity(); @@ -18685,6 +18648,51 @@ function splitTransformList(transformList) { return tokens; } +function serialize(matrix) { + if (is2DMatrix(matrix)) { + // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix', + chi: [ + matrix[0][0], + matrix[1][1], + matrix[1][0], + matrix[1][1], + matrix[3][0], + matrix[3][1] + ].reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(t.toPrecision(6)) + }); + return acc; + }, []) + }; + } + let m = []; + // console.error(JSON.stringify({matrix},null, 1)); + for (let i = 0; i < matrix.length; i++) { + for (let j = 0; j < matrix[i].length; j++) { + if (m.length > 0) { + m.push({ typ: exports.EnumToken.CommaTokenType }); + } + m.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(matrix[i][j].toPrecision(6)) + }); + } + } + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: m + }; +} + class TransformCssFeature { static get ordering() { return 4; diff --git a/dist/lib/ast/transform/compute.js b/dist/lib/ast/transform/compute.js index 65c2b64e..be362895 100644 --- a/dist/lib/ast/transform/compute.js +++ b/dist/lib/ast/transform/compute.js @@ -14,7 +14,6 @@ import { translateX, translateY, translateZ, translate } from './translate.js'; import { rotate, rotate3D } from './rotate.js'; import { scale3d, scale, scaleX, scaleY, scaleZ } from './scale.js'; import { minify } from './minify.js'; -import { serialize } from './matrix.js'; function compute(transformLists) { transformLists = transformLists.slice(); @@ -38,7 +37,15 @@ function compute(transformLists) { // toZero(tokens[i][j]); // } // } - return tokens.reduce((acc, t) => acc.concat(minify(t) ?? serialize(t)), []); + const result = []; + for (const token of tokens) { + let t = minify(token); + if (t == null) { + return null; + } + result.push(...t); + } + return result; } function computeMatrix(transformList) { let matrix = identity(); diff --git a/dist/lib/ast/transform/minify.js b/dist/lib/ast/transform/minify.js index 65db9d5a..defb48e1 100644 --- a/dist/lib/ast/transform/minify.js +++ b/dist/lib/ast/transform/minify.js @@ -136,7 +136,7 @@ function minify(matrix) { val: 'scale', chi: [ { typ: EnumToken.NumberTokenType, val: '' + sx }, - { typ: EnumToken.WhitespaceTokenType }, + { typ: EnumToken.CommaTokenType }, { typ: EnumToken.NumberTokenType, val: '' + sy }, ] }); diff --git a/jsr.json b/jsr.json index 41630303..b025bdce 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@tbela99/css-parser", - "version": "0.9.2-alpha2", + "version": "0.9.2-alpha3", "publish": { "include": [ "src", diff --git a/package.json b/package.json index 2bacfca2..fae958fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "v0.9.2-alpha2", + "version": "v0.9.2-alpha3", "exports": { ".": "./dist/node/index.js", "./node": "./dist/node/index.js", diff --git a/src/lib/ast/transform/compute.ts b/src/lib/ast/transform/compute.ts index 50677918..47e79ba8 100644 --- a/src/lib/ast/transform/compute.ts +++ b/src/lib/ast/transform/compute.ts @@ -9,7 +9,6 @@ import {getAngle, getNumber} from "../../renderer/color"; import {rotate, rotate3D} from "./rotate.ts"; import {scale, scale3d, scaleX, scaleY, scaleZ} from "./scale.ts"; import {minify} from "./minify.ts"; -import {serialize} from "./matrix.ts"; export function compute(transformLists: Token[]): Token[] | null { @@ -43,8 +42,21 @@ export function compute(transformLists: Token[]): Token[] | null { // toZero(tokens[i][j]); // } // } + const result: Token[] = []; - return tokens.reduce((acc, t) => acc.concat(minify(t) ?? serialize(t)), [] as Token[]); + for (const token of tokens) { + + let t = minify(token); + + if (t == null) { + + return null; + } + + result.push(...t); + } + + return result; } export function computeMatrix(transformList: Token[]): Matrix | null { diff --git a/src/lib/ast/transform/minify.ts b/src/lib/ast/transform/minify.ts index 10f54d47..265b6379 100644 --- a/src/lib/ast/transform/minify.ts +++ b/src/lib/ast/transform/minify.ts @@ -177,7 +177,7 @@ export function minify(matrix: Matrix): Token[] | null { val: 'scale', chi: [ {typ: EnumToken.NumberTokenType, val: '' + sx}, - {typ: EnumToken.WhitespaceTokenType}, + {typ: EnumToken.CommaTokenType}, {typ: EnumToken.NumberTokenType, val: '' + sy}, ] }); diff --git a/test/specs/code/transform.js b/test/specs/code/transform.js index 9a8615e1..a82db02b 100644 --- a/test/specs/code/transform.js +++ b/test/specs/code/transform.js @@ -203,4 +203,121 @@ export function run(describe, expect, transform, parse, render, dirname, readFil }); }); + + describe('CSS scale', function () { + + it('scale3d #13', function () { + const nesting1 = ` + + .now { + transform: scaleX(0.5) scaleY(0.5) scaleZ(0.5); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: scale3d(.5,.5,.5) +}`)); + }); + + it('scale #14', function () { + const nesting1 = ` + + .now { + transform: scaleX(1) scaleY(1) scaleZ(1); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: none +}`)); + }); + + + it('scale #15', function () { + const nesting1 = ` + + .now { + transform: scaleX(1) scaleY(1) ; +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: none +}`)); + }); + + it('scale #16', function () { + const nesting1 = ` + + .now { + transform: scaleX(1) scaleZ(1); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: none +}`)); + }); + + it('scale #17', function () { + const nesting1 = ` + + .now { + transform: scaleY(1) scaleZ(1); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: none +}`)); + }); + + it('rotate3d #18', function () { + const nesting1 = ` + + .now { + transform: scaleX(1); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: none +}`)); + }); + + it('scale #19', function () { + const nesting1 = ` + + .now { + transform: scaleX(1.5) scaleY(2); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: scale(1.5,2) +}`)); + }); + + it('scale #20', function () { + const nesting1 = ` + + .now { + transform:scaleX(0) scaleY( 0) scaleZ(0); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: scaleX(0)scaleY(0)scaleZ(0) +}`)); + }); + + }); } \ No newline at end of file From d60f3dadf4735ba1583b5c0c7315a2b894a0b01d Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Mon, 7 Apr 2025 09:50:39 -0400 Subject: [PATCH 07/10] implement skew() #75 --- dist/index-umd-web.js | 545 +++++++++++++++------------- dist/index.cjs | 545 +++++++++++++++------------- dist/lib/ast/features/transform.js | 26 +- dist/lib/ast/transform/compute.js | 140 ++++--- dist/lib/ast/transform/matrix.js | 4 +- dist/lib/ast/transform/minify.js | 269 +++++++------- dist/lib/ast/transform/translate.js | 10 +- dist/lib/ast/transform/utils.js | 1 + jsr.json | 2 +- package.json | 2 +- src/lib/ast/features/transform.ts | 37 +- src/lib/ast/transform/compute.ts | 92 +++-- src/lib/ast/transform/matrix.ts | 4 +- src/lib/ast/transform/minify.ts | 274 +++++++------- src/lib/ast/transform/skew.ts | 25 +- src/lib/ast/transform/translate.ts | 22 +- src/lib/ast/transform/utils.ts | 7 +- test/specs/code/transform.js | 46 ++- 18 files changed, 1132 insertions(+), 919 deletions(-) diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 46d36f30..edbd4816 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -17864,6 +17864,7 @@ rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); const { x: x1, y: y1, z: z1, angle } = getRotation3D(rotationMatrix); return { + // @ts-ignore skew: toZero(skew), scale: toZero(scale), rotate: toZero([x1, y1, z1, angle]), @@ -18011,7 +18012,13 @@ const matrix = identity(); matrix[3][0] = translate[0]; matrix[3][1] = translate[1] ?? 0; - matrix[3][2] = translate[2] ?? 0; + return multiply(from, matrix); + } + function translate3d(translate, from) { + const matrix = identity(); + matrix[3][0] = translate[0]; + matrix[3][1] = translate[1]; + matrix[3][2] = translate[2]; return multiply(from, matrix); } @@ -18089,14 +18096,12 @@ function minify$1(matrix) { const decomposed = decompose(matrix); - // console.error({decomposed}); if (decomposed == null) { return null; } - const transforms = new Set([ - 'translate', 'scale', 'skew', 'perspective', 'rotate' - ]); + const transforms = new Set(['translate', 'scale', 'skew', 'perspective', 'rotate']); const scales = new Set(['x', 'y', 'z']); + const skew = new Set(['x', 'y']); const result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { @@ -18105,7 +18110,7 @@ if (decomposed.scale[0] == 1 && decomposed.scale[1] == 1 && decomposed.scale[2] == 1) { transforms.delete('scale'); } - if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0 && decomposed.skew[2] == 0) { + if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0) { transforms.delete('skew'); } if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { @@ -18124,7 +18129,7 @@ if (coordinates.size == 3) { result.push({ typ: exports.EnumToken.FunctionTokenType, - val: 'translate', + val: 'translate3d', chi: [ { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, { typ: exports.EnumToken.CommaTokenType }, @@ -18189,133 +18194,156 @@ }); } } - // scale(x, x) -> scale(x) - // scale(1, sy) -> scaleY(sy) - // scale3d(1, 1, sz) -> scaleZ(sz) - // scaleX() scale(Y) scaleZ() -> scale3d() if (transforms.has('scale')) { const [sx, sy, sz] = decomposed.scale; - if (!(sx == 1 && sy == 1 && sz == 1)) { - if (sz == 1) { - scales.delete('z'); - } - if (sy == 1) { - scales.delete('y'); - } - if (sx == 1) { - scales.delete('x'); - } - if (scales.size == 1) { - let prefix = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'scale' + prefix, - chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx) } - ] - }); - } - else if (!scales.has('z')) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, - ] - }); - } - else { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'scale3d', - chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.NumberTokenType, val: '' + sz } - ] - }); - } + if (sz == 1) { + scales.delete('z'); + } + if (sy == 1) { + scales.delete('y'); + } + if (sx == 1) { + scales.delete('x'); + } + if (scales.size == 1) { + let prefix = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale' + prefix, + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx) } + ] + }); + } + else if (!scales.has('z')) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale3d', + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sz } + ] + }); } } if (transforms.has('rotate')) { const [x, y, z, angle] = decomposed.rotate; - // console.error({x, y, z, angle}); - if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { - if (y == 0 && z == 0) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'rotateX', - chi: [ - { - typ: exports.EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } - else if (x == 0 && z == 0) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'rotateY', - chi: [ - { - typ: exports.EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } - else if (x == 0 && y == 0) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'rotate', - chi: [ - { - typ: exports.EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } - else { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'rotate3d', - chi: [ - { - typ: exports.EnumToken.NumberTokenType, - val: '' + x - }, - { typ: exports.EnumToken.CommaTokenType }, - { - typ: exports.EnumToken.NumberTokenType, - val: '' + y - }, - { typ: exports.EnumToken.CommaTokenType }, - { - typ: exports.EnumToken.NumberTokenType, - val: '' + z - }, - { typ: exports.EnumToken.CommaTokenType }, - { - typ: exports.EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } + if (y == 0 && z == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotateX', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && z == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotateY', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && y == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotate', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotate3d', + chi: [ + { + typ: exports.EnumToken.NumberTokenType, + val: '' + x + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.NumberTokenType, + val: '' + y + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.NumberTokenType, + val: '' + z + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + } + if (transforms.has('skew')) { + if (decomposed.skew[0] == 0) { + skew.delete('x'); + } + if (decomposed.skew[1] == 0) { + skew.delete('y'); + } + // console.error({skew}); + for (let i = 0; i < 2; i++) { + decomposed.skew[i] = +(Math.atan(decomposed.skew[i]) * 180 / Math.PI).toPrecision(6); + } + if (skew.size == 1) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'skew' + (skew.has('x') ? '' : 'Y'), + chi: [ + { typ: exports.EnumToken.AngleTokenType, val: '' + decomposed.skew[0], unit: 'deg' } + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'skew', + chi: [ + { typ: exports.EnumToken.AngleTokenType, val: '' + decomposed.skew[0], unit: 'deg' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.AngleTokenType, val: '' + decomposed.skew[1], unit: 'deg' } + ] + }); } } // identity - return transforms.size > 1 ? null : result.length == 0 || eq(result, identity()) ? [ + return result.length == 0 || eq(result, identity()) ? [ { typ: exports.EnumToken.IdenTokenType, val: 'none' @@ -18323,40 +18351,90 @@ ] : result; } + function skewX(x, from) { + const matrix = identity(); + matrix[1][0] = Math.tan(x); + return multiply(from, matrix); + } + function skewY(y, from) { + const matrix = identity(); + matrix[0][1] = Math.tan(y); + return multiply(from, matrix); + } + // convert angle to radian + function skew(values, from) { + const matrix = identity(); + matrix[1][0] = Math.tan(values[0]); + if (values.length > 1) { + matrix[0][1] = Math.tan(values[1]); + } + return multiply(from, matrix); + } + + function serialize(matrix) { + if (is2DMatrix(matrix)) { + // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix', + chi: [ + matrix[0][0], + matrix[0][1], + matrix[1][0], + matrix[1][1], + matrix[3][0], + matrix[3][1] + ].reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(t.toPrecision(6)) + }); + return acc; + }, []) + }; + } + let m = []; + // console.error(JSON.stringify({matrix},null, 1)); + for (let i = 0; i < matrix.length; i++) { + for (let j = 0; j < matrix[i].length; j++) { + if (m.length > 0) { + m.push({ typ: exports.EnumToken.CommaTokenType }); + } + m.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(matrix[j][i].toPrecision(6)) + }); + } + } + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: m + }; + } + function compute(transformLists) { transformLists = transformLists.slice(); stripCommaToken(transformLists); if (transformLists.length == 0) { return null; } - const tokens = []; - let matrix; + let matrix = identity(); for (const transformList of splitTransformList(transformLists)) { - matrix = computeMatrix(transformList); + matrix = computeMatrix(transformList, matrix); if (matrix == null) { return null; } - tokens.push(matrix); - } - // for (let i = 0; i < tokens.length; i++) { - // - // for (let j = 0; j < tokens[i].length; j++) { - // - // toZero(tokens[i][j]); - // } - // } - const result = []; - for (const token of tokens) { - let t = minify$1(token); - if (t == null) { - return null; - } - result.push(...t); } - return result; + return { + result: minify$1(matrix), + matrix: serialize(matrix) + }; } - function computeMatrix(transformList) { - let matrix = identity(); + function computeMatrix(transformList, matrix) { let values = []; let val; let i = 0; @@ -18407,9 +18485,12 @@ else if (transformList[i].val == 'translateZ') { matrix = translateZ(values[0], matrix); } + else if (transformList[i].val == 'translate') { + matrix = translate(values, matrix); + } else { // @ts-ignore - matrix = translate(values, matrix); + matrix = translate3d(values, matrix); } } break; @@ -18464,50 +18545,80 @@ case 'scaleX': case 'scaleY': case 'scaleZ': - case 'scale3d': { - values.length = 0; - let child; - for (let k = 0; k < transformList[i].chi.length; k++) { - child = transformList[i].chi[k]; - if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { - continue; + case 'scale3d': + { + values.length = 0; + let child; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (child.typ != exports.EnumToken.NumberTokenType) { + return null; + } + values.push(getNumber(child)); } - if (child.typ != exports.EnumToken.NumberTokenType) { + if (values.length == 0) { return null; } - values.push(getNumber(child)); - } - if (values.length == 0) { - return null; - } - if (transformList[i].val == 'scale3d') { - if (values.length != 3) { + if (transformList[i].val == 'scale3d') { + if (values.length != 3) { + return null; + } + matrix = scale3d(...values, matrix); + break; + } + if (transformList[i].val == 'scale') { + if (values.length != 1 && values.length != 2) { + return null; + } + matrix = scale(values[0], values[1] ?? values[0], matrix); + break; + } + if (values.length != 1) { return null; } - matrix = scale3d(...values, matrix); - break; + else if (transformList[i].val == 'scaleX') { + matrix = scaleX(values[0], matrix); + } + else if (transformList[i].val == 'scaleY') { + matrix = scaleY(values[0], matrix); + } + else if (transformList[i].val == 'scaleZ') { + matrix = scaleZ(values[0], matrix); + } } - if (transformList[i].val == 'scale') { - if (values.length != 1 && values.length != 2) { + break; + case 'skew': + case 'skewX': + case 'skewY': + { + values.length = 0; + let child; + let value; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + value = getAngle(child); + if (value == null) { + return null; + } + values.push(value * 2 * Math.PI); + } + if (values.length == 0 || (values.length > (transformList[i].val == 'skew' ? 2 : 1))) { return null; } - matrix = scale(values[0], values[1] ?? values[0], matrix); - break; - } - if (values.length != 1) { - return null; - } - else if (transformList[i].val == 'scaleX') { - matrix = scaleX(values[0], matrix); - } - else if (transformList[i].val == 'scaleY') { - matrix = scaleY(values[0], matrix); - } - else if (transformList[i].val == 'scaleZ') { - matrix = scaleZ(values[0], matrix); + if (transformList[i].val == 'skew') { + matrix = skew(values, matrix); + } + else { + matrix = transformList[i].val == 'skewX' ? skewX(values[0], matrix) : skewY(values[0], matrix); + } } break; - } default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); @@ -18549,51 +18660,6 @@ return tokens; } - function serialize(matrix) { - if (is2DMatrix(matrix)) { - // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset - return { - typ: exports.EnumToken.FunctionTokenType, - val: 'matrix', - chi: [ - matrix[0][0], - matrix[1][1], - matrix[1][0], - matrix[1][1], - matrix[3][0], - matrix[3][1] - ].reduce((acc, t) => { - if (acc.length > 0) { - acc.push({ typ: exports.EnumToken.CommaTokenType }); - } - acc.push({ - typ: exports.EnumToken.NumberTokenType, - val: reduceNumber(t.toPrecision(6)) - }); - return acc; - }, []) - }; - } - let m = []; - // console.error(JSON.stringify({matrix},null, 1)); - for (let i = 0; i < matrix.length; i++) { - for (let j = 0; j < matrix[i].length; j++) { - if (m.length > 0) { - m.push({ typ: exports.EnumToken.CommaTokenType }); - } - m.push({ - typ: exports.EnumToken.NumberTokenType, - val: reduceNumber(matrix[i][j].toPrecision(6)) - }); - } - } - return { - typ: exports.EnumToken.FunctionTokenType, - val: 'matrix3d', - chi: m - }; - } - class TransformCssFeature { static get ordering() { return 4; @@ -18621,27 +18687,16 @@ } const children = node.val.slice(); consumeWhitespace(children); - let result = compute(children); - if (result == null) { + let { result, matrix } = compute(children) ?? {}; + // console.error({result, matrix}); + // console.error({result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), matrix: matrix == null ? null : renderToken(matrix)}); + if (result == null || matrix == null) { return; } - // console.error(JSON.stringify({result}, null, 1)); - // console.error({result, t: result.map(t => minify(t) ?? t - // )}); - // console.error({result: decompose2(result)}); - // const decomposed =decompose(result); - // const minified = minify(result); - const matrix = computeMatrix(result); - if (matrix != null) { - const m = serialize(matrix); - if (renderToken(m).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - result = [m]; - } - } - // console.error({result, serialized: renderToken()}); - // if (minified != null) { + if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = [matrix]; + } node.val = result; - // } } } } diff --git a/dist/index.cjs b/dist/index.cjs index 463cb459..e82e2703 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -17963,6 +17963,7 @@ function decompose(matrix) { rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); const { x: x1, y: y1, z: z1, angle } = getRotation3D(rotationMatrix); return { + // @ts-ignore skew: toZero(skew), scale: toZero(scale), rotate: toZero([x1, y1, z1, angle]), @@ -18110,7 +18111,13 @@ function translate(translate, from) { const matrix = identity(); matrix[3][0] = translate[0]; matrix[3][1] = translate[1] ?? 0; - matrix[3][2] = translate[2] ?? 0; + return multiply(from, matrix); +} +function translate3d(translate, from) { + const matrix = identity(); + matrix[3][0] = translate[0]; + matrix[3][1] = translate[1]; + matrix[3][2] = translate[2]; return multiply(from, matrix); } @@ -18188,14 +18195,12 @@ function scale3d(x, y, z, from) { function minify$1(matrix) { const decomposed = decompose(matrix); - // console.error({decomposed}); if (decomposed == null) { return null; } - const transforms = new Set([ - 'translate', 'scale', 'skew', 'perspective', 'rotate' - ]); + const transforms = new Set(['translate', 'scale', 'skew', 'perspective', 'rotate']); const scales = new Set(['x', 'y', 'z']); + const skew = new Set(['x', 'y']); const result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { @@ -18204,7 +18209,7 @@ function minify$1(matrix) { if (decomposed.scale[0] == 1 && decomposed.scale[1] == 1 && decomposed.scale[2] == 1) { transforms.delete('scale'); } - if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0 && decomposed.skew[2] == 0) { + if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0) { transforms.delete('skew'); } if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { @@ -18223,7 +18228,7 @@ function minify$1(matrix) { if (coordinates.size == 3) { result.push({ typ: exports.EnumToken.FunctionTokenType, - val: 'translate', + val: 'translate3d', chi: [ { typ: exports.EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, { typ: exports.EnumToken.CommaTokenType }, @@ -18288,133 +18293,156 @@ function minify$1(matrix) { }); } } - // scale(x, x) -> scale(x) - // scale(1, sy) -> scaleY(sy) - // scale3d(1, 1, sz) -> scaleZ(sz) - // scaleX() scale(Y) scaleZ() -> scale3d() if (transforms.has('scale')) { const [sx, sy, sz] = decomposed.scale; - if (!(sx == 1 && sy == 1 && sz == 1)) { - if (sz == 1) { - scales.delete('z'); - } - if (sy == 1) { - scales.delete('y'); - } - if (sx == 1) { - scales.delete('x'); - } - if (scales.size == 1) { - let prefix = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'scale' + prefix, - chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx) } - ] - }); - } - else if (!scales.has('z')) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, - ] - }); - } - else { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'scale3d', - chi: [ - { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, - { typ: exports.EnumToken.CommaTokenType }, - { typ: exports.EnumToken.NumberTokenType, val: '' + sz } - ] - }); - } + if (sz == 1) { + scales.delete('z'); + } + if (sy == 1) { + scales.delete('y'); + } + if (sx == 1) { + scales.delete('x'); + } + if (scales.size == 1) { + let prefix = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale' + prefix, + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx) } + ] + }); + } + else if (!scales.has('z')) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'scale3d', + chi: [ + { typ: exports.EnumToken.NumberTokenType, val: '' + sx }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sy }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.NumberTokenType, val: '' + sz } + ] + }); } } if (transforms.has('rotate')) { const [x, y, z, angle] = decomposed.rotate; - // console.error({x, y, z, angle}); - if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { - if (y == 0 && z == 0) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'rotateX', - chi: [ - { - typ: exports.EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } - else if (x == 0 && z == 0) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'rotateY', - chi: [ - { - typ: exports.EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } - else if (x == 0 && y == 0) { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'rotate', - chi: [ - { - typ: exports.EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } - else { - result.push({ - typ: exports.EnumToken.FunctionTokenType, - val: 'rotate3d', - chi: [ - { - typ: exports.EnumToken.NumberTokenType, - val: '' + x - }, - { typ: exports.EnumToken.CommaTokenType }, - { - typ: exports.EnumToken.NumberTokenType, - val: '' + y - }, - { typ: exports.EnumToken.CommaTokenType }, - { - typ: exports.EnumToken.NumberTokenType, - val: '' + z - }, - { typ: exports.EnumToken.CommaTokenType }, - { - typ: exports.EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } + if (y == 0 && z == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotateX', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && z == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotateY', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && y == 0) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotate', + chi: [ + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'rotate3d', + chi: [ + { + typ: exports.EnumToken.NumberTokenType, + val: '' + x + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.NumberTokenType, + val: '' + y + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.NumberTokenType, + val: '' + z + }, + { typ: exports.EnumToken.CommaTokenType }, + { + typ: exports.EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + } + if (transforms.has('skew')) { + if (decomposed.skew[0] == 0) { + skew.delete('x'); + } + if (decomposed.skew[1] == 0) { + skew.delete('y'); + } + // console.error({skew}); + for (let i = 0; i < 2; i++) { + decomposed.skew[i] = +(Math.atan(decomposed.skew[i]) * 180 / Math.PI).toPrecision(6); + } + if (skew.size == 1) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'skew' + (skew.has('x') ? '' : 'Y'), + chi: [ + { typ: exports.EnumToken.AngleTokenType, val: '' + decomposed.skew[0], unit: 'deg' } + ] + }); + } + else { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'skew', + chi: [ + { typ: exports.EnumToken.AngleTokenType, val: '' + decomposed.skew[0], unit: 'deg' }, + { typ: exports.EnumToken.CommaTokenType }, + { typ: exports.EnumToken.AngleTokenType, val: '' + decomposed.skew[1], unit: 'deg' } + ] + }); } } // identity - return transforms.size > 1 ? null : result.length == 0 || eq(result, identity()) ? [ + return result.length == 0 || eq(result, identity()) ? [ { typ: exports.EnumToken.IdenTokenType, val: 'none' @@ -18422,40 +18450,90 @@ function minify$1(matrix) { ] : result; } +function skewX(x, from) { + const matrix = identity(); + matrix[1][0] = Math.tan(x); + return multiply(from, matrix); +} +function skewY(y, from) { + const matrix = identity(); + matrix[0][1] = Math.tan(y); + return multiply(from, matrix); +} +// convert angle to radian +function skew(values, from) { + const matrix = identity(); + matrix[1][0] = Math.tan(values[0]); + if (values.length > 1) { + matrix[0][1] = Math.tan(values[1]); + } + return multiply(from, matrix); +} + +function serialize(matrix) { + if (is2DMatrix(matrix)) { + // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix', + chi: [ + matrix[0][0], + matrix[0][1], + matrix[1][0], + matrix[1][1], + matrix[3][0], + matrix[3][1] + ].reduce((acc, t) => { + if (acc.length > 0) { + acc.push({ typ: exports.EnumToken.CommaTokenType }); + } + acc.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(t.toPrecision(6)) + }); + return acc; + }, []) + }; + } + let m = []; + // console.error(JSON.stringify({matrix},null, 1)); + for (let i = 0; i < matrix.length; i++) { + for (let j = 0; j < matrix[i].length; j++) { + if (m.length > 0) { + m.push({ typ: exports.EnumToken.CommaTokenType }); + } + m.push({ + typ: exports.EnumToken.NumberTokenType, + val: reduceNumber(matrix[j][i].toPrecision(6)) + }); + } + } + return { + typ: exports.EnumToken.FunctionTokenType, + val: 'matrix3d', + chi: m + }; +} + function compute(transformLists) { transformLists = transformLists.slice(); stripCommaToken(transformLists); if (transformLists.length == 0) { return null; } - const tokens = []; - let matrix; + let matrix = identity(); for (const transformList of splitTransformList(transformLists)) { - matrix = computeMatrix(transformList); + matrix = computeMatrix(transformList, matrix); if (matrix == null) { return null; } - tokens.push(matrix); - } - // for (let i = 0; i < tokens.length; i++) { - // - // for (let j = 0; j < tokens[i].length; j++) { - // - // toZero(tokens[i][j]); - // } - // } - const result = []; - for (const token of tokens) { - let t = minify$1(token); - if (t == null) { - return null; - } - result.push(...t); } - return result; + return { + result: minify$1(matrix), + matrix: serialize(matrix) + }; } -function computeMatrix(transformList) { - let matrix = identity(); +function computeMatrix(transformList, matrix) { let values = []; let val; let i = 0; @@ -18506,9 +18584,12 @@ function computeMatrix(transformList) { else if (transformList[i].val == 'translateZ') { matrix = translateZ(values[0], matrix); } + else if (transformList[i].val == 'translate') { + matrix = translate(values, matrix); + } else { // @ts-ignore - matrix = translate(values, matrix); + matrix = translate3d(values, matrix); } } break; @@ -18563,50 +18644,80 @@ function computeMatrix(transformList) { case 'scaleX': case 'scaleY': case 'scaleZ': - case 'scale3d': { - values.length = 0; - let child; - for (let k = 0; k < transformList[i].chi.length; k++) { - child = transformList[i].chi[k]; - if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { - continue; + case 'scale3d': + { + values.length = 0; + let child; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (child.typ != exports.EnumToken.NumberTokenType) { + return null; + } + values.push(getNumber(child)); } - if (child.typ != exports.EnumToken.NumberTokenType) { + if (values.length == 0) { return null; } - values.push(getNumber(child)); - } - if (values.length == 0) { - return null; - } - if (transformList[i].val == 'scale3d') { - if (values.length != 3) { + if (transformList[i].val == 'scale3d') { + if (values.length != 3) { + return null; + } + matrix = scale3d(...values, matrix); + break; + } + if (transformList[i].val == 'scale') { + if (values.length != 1 && values.length != 2) { + return null; + } + matrix = scale(values[0], values[1] ?? values[0], matrix); + break; + } + if (values.length != 1) { return null; } - matrix = scale3d(...values, matrix); - break; + else if (transformList[i].val == 'scaleX') { + matrix = scaleX(values[0], matrix); + } + else if (transformList[i].val == 'scaleY') { + matrix = scaleY(values[0], matrix); + } + else if (transformList[i].val == 'scaleZ') { + matrix = scaleZ(values[0], matrix); + } } - if (transformList[i].val == 'scale') { - if (values.length != 1 && values.length != 2) { + break; + case 'skew': + case 'skewX': + case 'skewY': + { + values.length = 0; + let child; + let value; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + value = getAngle(child); + if (value == null) { + return null; + } + values.push(value * 2 * Math.PI); + } + if (values.length == 0 || (values.length > (transformList[i].val == 'skew' ? 2 : 1))) { return null; } - matrix = scale(values[0], values[1] ?? values[0], matrix); - break; - } - if (values.length != 1) { - return null; - } - else if (transformList[i].val == 'scaleX') { - matrix = scaleX(values[0], matrix); - } - else if (transformList[i].val == 'scaleY') { - matrix = scaleY(values[0], matrix); - } - else if (transformList[i].val == 'scaleZ') { - matrix = scaleZ(values[0], matrix); + if (transformList[i].val == 'skew') { + matrix = skew(values, matrix); + } + else { + matrix = transformList[i].val == 'skewX' ? skewX(values[0], matrix) : skewY(values[0], matrix); + } } break; - } default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); @@ -18648,51 +18759,6 @@ function splitTransformList(transformList) { return tokens; } -function serialize(matrix) { - if (is2DMatrix(matrix)) { - // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset - return { - typ: exports.EnumToken.FunctionTokenType, - val: 'matrix', - chi: [ - matrix[0][0], - matrix[1][1], - matrix[1][0], - matrix[1][1], - matrix[3][0], - matrix[3][1] - ].reduce((acc, t) => { - if (acc.length > 0) { - acc.push({ typ: exports.EnumToken.CommaTokenType }); - } - acc.push({ - typ: exports.EnumToken.NumberTokenType, - val: reduceNumber(t.toPrecision(6)) - }); - return acc; - }, []) - }; - } - let m = []; - // console.error(JSON.stringify({matrix},null, 1)); - for (let i = 0; i < matrix.length; i++) { - for (let j = 0; j < matrix[i].length; j++) { - if (m.length > 0) { - m.push({ typ: exports.EnumToken.CommaTokenType }); - } - m.push({ - typ: exports.EnumToken.NumberTokenType, - val: reduceNumber(matrix[i][j].toPrecision(6)) - }); - } - } - return { - typ: exports.EnumToken.FunctionTokenType, - val: 'matrix3d', - chi: m - }; -} - class TransformCssFeature { static get ordering() { return 4; @@ -18720,27 +18786,16 @@ class TransformCssFeature { } const children = node.val.slice(); consumeWhitespace(children); - let result = compute(children); - if (result == null) { + let { result, matrix } = compute(children) ?? {}; + // console.error({result, matrix}); + // console.error({result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), matrix: matrix == null ? null : renderToken(matrix)}); + if (result == null || matrix == null) { return; } - // console.error(JSON.stringify({result}, null, 1)); - // console.error({result, t: result.map(t => minify(t) ?? t - // )}); - // console.error({result: decompose2(result)}); - // const decomposed =decompose(result); - // const minified = minify(result); - const matrix = computeMatrix(result); - if (matrix != null) { - const m = serialize(matrix); - if (renderToken(m).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - result = [m]; - } - } - // console.error({result, serialized: renderToken()}); - // if (minified != null) { + if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = [matrix]; + } node.val = result; - // } } } } diff --git a/dist/lib/ast/features/transform.js b/dist/lib/ast/features/transform.js index eec5b050..64a16eba 100644 --- a/dist/lib/ast/features/transform.js +++ b/dist/lib/ast/features/transform.js @@ -6,8 +6,7 @@ import '../../parser/parse.js'; import { renderToken } from '../../renderer/render.js'; import '../../renderer/color/utils/constants.js'; import '../../parser/utils/config.js'; -import { compute, computeMatrix } from '../transform/compute.js'; -import { serialize } from '../transform/matrix.js'; +import { compute } from '../transform/compute.js'; class TransformCssFeature { static get ordering() { @@ -36,27 +35,16 @@ class TransformCssFeature { } const children = node.val.slice(); consumeWhitespace(children); - let result = compute(children); - if (result == null) { + let { result, matrix } = compute(children) ?? {}; + // console.error({result, matrix}); + // console.error({result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), matrix: matrix == null ? null : renderToken(matrix)}); + if (result == null || matrix == null) { return; } - // console.error(JSON.stringify({result}, null, 1)); - // console.error({result, t: result.map(t => minify(t) ?? t - // )}); - // console.error({result: decompose2(result)}); - // const decomposed =decompose(result); - // const minified = minify(result); - const matrix = computeMatrix(result); - if (matrix != null) { - const m = serialize(matrix); - if (renderToken(m).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - result = [m]; - } + if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = [matrix]; } - // console.error({result, serialized: renderToken()}); - // if (minified != null) { node.val = result; - // } } } } diff --git a/dist/lib/ast/transform/compute.js b/dist/lib/ast/transform/compute.js index be362895..29309cce 100644 --- a/dist/lib/ast/transform/compute.js +++ b/dist/lib/ast/transform/compute.js @@ -6,14 +6,16 @@ import '../minify.js'; import '../walk.js'; import '../../parser/parse.js'; import '../../parser/utils/config.js'; -import { getNumber, getAngle } from '../../renderer/color/color.js'; +import { getAngle, getNumber } from '../../renderer/color/color.js'; import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import { stripCommaToken } from '../../validation/utils/list.js'; -import { translateX, translateY, translateZ, translate } from './translate.js'; +import { translateX, translateY, translateZ, translate, translate3d } from './translate.js'; import { rotate, rotate3D } from './rotate.js'; import { scale3d, scale, scaleX, scaleY, scaleZ } from './scale.js'; import { minify } from './minify.js'; +import { skew, skewX, skewY } from './skew.js'; +import { serialize } from './matrix.js'; function compute(transformLists) { transformLists = transformLists.slice(); @@ -21,34 +23,19 @@ function compute(transformLists) { if (transformLists.length == 0) { return null; } - const tokens = []; - let matrix; + let matrix = identity(); for (const transformList of splitTransformList(transformLists)) { - matrix = computeMatrix(transformList); + matrix = computeMatrix(transformList, matrix); if (matrix == null) { return null; } - tokens.push(matrix); - } - // for (let i = 0; i < tokens.length; i++) { - // - // for (let j = 0; j < tokens[i].length; j++) { - // - // toZero(tokens[i][j]); - // } - // } - const result = []; - for (const token of tokens) { - let t = minify(token); - if (t == null) { - return null; - } - result.push(...t); } - return result; + return { + result: minify(matrix), + matrix: serialize(matrix) + }; } -function computeMatrix(transformList) { - let matrix = identity(); +function computeMatrix(transformList, matrix) { let values = []; let val; let i = 0; @@ -99,9 +86,12 @@ function computeMatrix(transformList) { else if (transformList[i].val == 'translateZ') { matrix = translateZ(values[0], matrix); } + else if (transformList[i].val == 'translate') { + matrix = translate(values, matrix); + } else { // @ts-ignore - matrix = translate(values, matrix); + matrix = translate3d(values, matrix); } } break; @@ -156,50 +146,80 @@ function computeMatrix(transformList) { case 'scaleX': case 'scaleY': case 'scaleZ': - case 'scale3d': { - values.length = 0; - let child; - for (let k = 0; k < transformList[i].chi.length; k++) { - child = transformList[i].chi[k]; - if (child.typ == EnumToken.CommentTokenType || child.typ == EnumToken.WhitespaceTokenType) { - continue; - } - if (child.typ != EnumToken.NumberTokenType) { + case 'scale3d': + { + values.length = 0; + let child; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == EnumToken.CommentTokenType || child.typ == EnumToken.WhitespaceTokenType) { + continue; + } + if (child.typ != EnumToken.NumberTokenType) { + return null; + } + values.push(getNumber(child)); + } + if (values.length == 0) { return null; } - values.push(getNumber(child)); - } - if (values.length == 0) { - return null; - } - if (transformList[i].val == 'scale3d') { - if (values.length != 3) { + if (transformList[i].val == 'scale3d') { + if (values.length != 3) { + return null; + } + matrix = scale3d(...values, matrix); + break; + } + if (transformList[i].val == 'scale') { + if (values.length != 1 && values.length != 2) { + return null; + } + matrix = scale(values[0], values[1] ?? values[0], matrix); + break; + } + if (values.length != 1) { return null; } - matrix = scale3d(...values, matrix); - break; + else if (transformList[i].val == 'scaleX') { + matrix = scaleX(values[0], matrix); + } + else if (transformList[i].val == 'scaleY') { + matrix = scaleY(values[0], matrix); + } + else if (transformList[i].val == 'scaleZ') { + matrix = scaleZ(values[0], matrix); + } } - if (transformList[i].val == 'scale') { - if (values.length != 1 && values.length != 2) { + break; + case 'skew': + case 'skewX': + case 'skewY': + { + values.length = 0; + let child; + let value; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == EnumToken.CommentTokenType || child.typ == EnumToken.WhitespaceTokenType) { + continue; + } + value = getAngle(child); + if (value == null) { + return null; + } + values.push(value * 2 * Math.PI); + } + if (values.length == 0 || (values.length > (transformList[i].val == 'skew' ? 2 : 1))) { return null; } - matrix = scale(values[0], values[1] ?? values[0], matrix); - break; - } - if (values.length != 1) { - return null; - } - else if (transformList[i].val == 'scaleX') { - matrix = scaleX(values[0], matrix); - } - else if (transformList[i].val == 'scaleY') { - matrix = scaleY(values[0], matrix); - } - else if (transformList[i].val == 'scaleZ') { - matrix = scaleZ(values[0], matrix); + if (transformList[i].val == 'skew') { + matrix = skew(values, matrix); + } + else { + matrix = transformList[i].val == 'skewX' ? skewX(values[0], matrix) : skewY(values[0], matrix); + } } break; - } default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); diff --git a/dist/lib/ast/transform/matrix.js b/dist/lib/ast/transform/matrix.js index 5824a221..03fe9748 100644 --- a/dist/lib/ast/transform/matrix.js +++ b/dist/lib/ast/transform/matrix.js @@ -10,7 +10,7 @@ function serialize(matrix) { val: 'matrix', chi: [ matrix[0][0], - matrix[1][1], + matrix[0][1], matrix[1][0], matrix[1][1], matrix[3][0], @@ -36,7 +36,7 @@ function serialize(matrix) { } m.push({ typ: EnumToken.NumberTokenType, - val: reduceNumber(matrix[i][j].toPrecision(6)) + val: reduceNumber(matrix[j][i].toPrecision(6)) }); } } diff --git a/dist/lib/ast/transform/minify.js b/dist/lib/ast/transform/minify.js index defb48e1..f0faf54b 100644 --- a/dist/lib/ast/transform/minify.js +++ b/dist/lib/ast/transform/minify.js @@ -4,14 +4,12 @@ import { eq } from '../../parser/utils/eq.js'; function minify(matrix) { const decomposed = decompose(matrix); - // console.error({decomposed}); if (decomposed == null) { return null; } - const transforms = new Set([ - 'translate', 'scale', 'skew', 'perspective', 'rotate' - ]); + const transforms = new Set(['translate', 'scale', 'skew', 'perspective', 'rotate']); const scales = new Set(['x', 'y', 'z']); + const skew = new Set(['x', 'y']); const result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { @@ -20,7 +18,7 @@ function minify(matrix) { if (decomposed.scale[0] == 1 && decomposed.scale[1] == 1 && decomposed.scale[2] == 1) { transforms.delete('scale'); } - if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0 && decomposed.skew[2] == 0) { + if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0) { transforms.delete('skew'); } if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { @@ -39,7 +37,7 @@ function minify(matrix) { if (coordinates.size == 3) { result.push({ typ: EnumToken.FunctionTokenType, - val: 'translate', + val: 'translate3d', chi: [ { typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px' }, { typ: EnumToken.CommaTokenType }, @@ -104,133 +102,156 @@ function minify(matrix) { }); } } - // scale(x, x) -> scale(x) - // scale(1, sy) -> scaleY(sy) - // scale3d(1, 1, sz) -> scaleZ(sz) - // scaleX() scale(Y) scaleZ() -> scale3d() if (transforms.has('scale')) { const [sx, sy, sz] = decomposed.scale; - if (!(sx == 1 && sy == 1 && sz == 1)) { - if (sz == 1) { - scales.delete('z'); - } - if (sy == 1) { - scales.delete('y'); - } - if (sx == 1) { - scales.delete('x'); - } - if (scales.size == 1) { - let prefix = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'scale' + prefix, - chi: [ - { typ: EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx) } - ] - }); - } - else if (!scales.has('z')) { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - { typ: EnumToken.NumberTokenType, val: '' + sx }, - { typ: EnumToken.CommaTokenType }, - { typ: EnumToken.NumberTokenType, val: '' + sy }, - ] - }); - } - else { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'scale3d', - chi: [ - { typ: EnumToken.NumberTokenType, val: '' + sx }, - { typ: EnumToken.CommaTokenType }, - { typ: EnumToken.NumberTokenType, val: '' + sy }, - { typ: EnumToken.CommaTokenType }, - { typ: EnumToken.NumberTokenType, val: '' + sz } - ] - }); - } + if (sz == 1) { + scales.delete('z'); + } + if (sy == 1) { + scales.delete('y'); + } + if (sx == 1) { + scales.delete('x'); + } + if (scales.size == 1) { + let prefix = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'scale' + prefix, + chi: [ + { typ: EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx) } + ] + }); + } + else if (!scales.has('z')) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + { typ: EnumToken.NumberTokenType, val: '' + sx }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.NumberTokenType, val: '' + sy }, + ] + }); + } + else { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'scale3d', + chi: [ + { typ: EnumToken.NumberTokenType, val: '' + sx }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.NumberTokenType, val: '' + sy }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.NumberTokenType, val: '' + sz } + ] + }); } } if (transforms.has('rotate')) { const [x, y, z, angle] = decomposed.rotate; - // console.error({x, y, z, angle}); - if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { - if (y == 0 && z == 0) { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'rotateX', - chi: [ - { - typ: EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } - else if (x == 0 && z == 0) { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'rotateY', - chi: [ - { - typ: EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } - else if (x == 0 && y == 0) { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'rotate', - chi: [ - { - typ: EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } - else { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'rotate3d', - chi: [ - { - typ: EnumToken.NumberTokenType, - val: '' + x - }, - { typ: EnumToken.CommaTokenType }, - { - typ: EnumToken.NumberTokenType, - val: '' + y - }, - { typ: EnumToken.CommaTokenType }, - { - typ: EnumToken.NumberTokenType, - val: '' + z - }, - { typ: EnumToken.CommaTokenType }, - { - typ: EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } + if (y == 0 && z == 0) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotateX', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && z == 0) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotateY', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else if (x == 0 && y == 0) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotate', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + else { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotate3d', + chi: [ + { + typ: EnumToken.NumberTokenType, + val: '' + x + }, + { typ: EnumToken.CommaTokenType }, + { + typ: EnumToken.NumberTokenType, + val: '' + y + }, + { typ: EnumToken.CommaTokenType }, + { + typ: EnumToken.NumberTokenType, + val: '' + z + }, + { typ: EnumToken.CommaTokenType }, + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } + } + if (transforms.has('skew')) { + if (decomposed.skew[0] == 0) { + skew.delete('x'); + } + if (decomposed.skew[1] == 0) { + skew.delete('y'); + } + // console.error({skew}); + for (let i = 0; i < 2; i++) { + decomposed.skew[i] = +(Math.atan(decomposed.skew[i]) * 180 / Math.PI).toPrecision(6); + } + if (skew.size == 1) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'skew' + (skew.has('x') ? '' : 'Y'), + chi: [ + { typ: EnumToken.AngleTokenType, val: '' + decomposed.skew[0], unit: 'deg' } + ] + }); + } + else { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'skew', + chi: [ + { typ: EnumToken.AngleTokenType, val: '' + decomposed.skew[0], unit: 'deg' }, + { typ: EnumToken.CommaTokenType }, + { typ: EnumToken.AngleTokenType, val: '' + decomposed.skew[1], unit: 'deg' } + ] + }); } } // identity - return transforms.size > 1 ? null : result.length == 0 || eq(result, identity()) ? [ + return result.length == 0 || eq(result, identity()) ? [ { typ: EnumToken.IdenTokenType, val: 'none' diff --git a/dist/lib/ast/transform/translate.js b/dist/lib/ast/transform/translate.js index 1314cf83..24606775 100644 --- a/dist/lib/ast/transform/translate.js +++ b/dist/lib/ast/transform/translate.js @@ -19,8 +19,14 @@ function translate(translate, from) { const matrix = identity(); matrix[3][0] = translate[0]; matrix[3][1] = translate[1] ?? 0; - matrix[3][2] = translate[2] ?? 0; + return multiply(from, matrix); +} +function translate3d(translate, from) { + const matrix = identity(); + matrix[3][0] = translate[0]; + matrix[3][1] = translate[1]; + matrix[3][2] = translate[2]; return multiply(from, matrix); } -export { translate, translateX, translateY, translateZ }; +export { translate, translate3d, translateX, translateY, translateZ }; diff --git a/dist/lib/ast/transform/utils.js b/dist/lib/ast/transform/utils.js index 20adf91d..3fa9d440 100644 --- a/dist/lib/ast/transform/utils.js +++ b/dist/lib/ast/transform/utils.js @@ -229,6 +229,7 @@ function decompose(matrix) { rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); const { x: x1, y: y1, z: z1, angle } = getRotation3D(rotationMatrix); return { + // @ts-ignore skew: toZero(skew), scale: toZero(scale), rotate: toZero([x1, y1, z1, angle]), diff --git a/jsr.json b/jsr.json index b025bdce..74af0a66 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@tbela99/css-parser", - "version": "0.9.2-alpha3", + "version": "0.9.2-alpha4", "publish": { "include": [ "src", diff --git a/package.json b/package.json index fae958fc..90fa73cd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "v0.9.2-alpha3", + "version": "v0.9.2-alpha4", "exports": { ".": "./dist/node/index.js", "./node": "./dist/node/index.js", diff --git a/src/lib/ast/features/transform.ts b/src/lib/ast/features/transform.ts index 3d921509..e2942f8b 100644 --- a/src/lib/ast/features/transform.ts +++ b/src/lib/ast/features/transform.ts @@ -8,9 +8,8 @@ import type { } from "../../../@types/index.d.ts"; import {EnumToken} from "../types"; import {consumeWhitespace} from "../../validation/utils"; -import {compute, computeMatrix} from "../transform/compute.ts"; +import {compute} from "../transform/compute.ts"; import {renderToken} from "../../renderer"; -import {serialize} from "../transform/matrix.ts"; export class TransformCssFeature { @@ -55,39 +54,23 @@ export class TransformCssFeature { consumeWhitespace(children); - let result = compute(children as Token[]); + let {result, matrix} = compute(children as Token[]) ?? {}; - if (result == null) { + // console.error({result, matrix}); - return; - } - - // console.error(JSON.stringify({result}, null, 1)); - // console.error({result, t: result.map(t => minify(t) ?? t - // )}); - - // console.error({result: decompose2(result)}); - // const decomposed =decompose(result); - // const minified = minify(result); - - const matrix = computeMatrix(result); + // console.error({result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), matrix: matrix == null ? null : renderToken(matrix)}); - if (matrix != null) { + if (result == null || matrix == null) { - const m = serialize(matrix); - - if (renderToken(m).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - - result = [m]; - } + return; } - // console.error({result, serialized: renderToken()}); + if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - // if (minified != null) { + result = [matrix]; + } - (node as AstDeclaration).val = result; - // } + (node as AstDeclaration).val = result; } } } diff --git a/src/lib/ast/transform/compute.ts b/src/lib/ast/transform/compute.ts index 47e79ba8..1875d36e 100644 --- a/src/lib/ast/transform/compute.ts +++ b/src/lib/ast/transform/compute.ts @@ -4,13 +4,18 @@ import {EnumToken} from "../types.ts"; import {length2Px} from "./convert.ts"; import {transformFunctions} from "../../syntax/index.ts"; import {stripCommaToken} from "../../validation/utils"; -import {translate, translateX, translateY, translateZ} from "./translate.ts"; +import {translate, translate3d, translateX, translateY, translateZ} from "./translate.ts"; import {getAngle, getNumber} from "../../renderer/color"; import {rotate, rotate3D} from "./rotate.ts"; import {scale, scale3d, scaleX, scaleY, scaleZ} from "./scale.ts"; import {minify} from "./minify.ts"; +import {skew, skewX, skewY} from "./skew.ts"; +import {serialize} from "./matrix.ts"; -export function compute(transformLists: Token[]): Token[] | null { +export function compute(transformLists: Token[]): { + result: Token[] | null; + matrix: Token +} | null { transformLists = transformLists.slice(); stripCommaToken(transformLists); @@ -20,48 +25,26 @@ export function compute(transformLists: Token[]): Token[] | null { return null; } - const tokens: Matrix[] = []; - let matrix: Matrix | null; + let matrix: Matrix | null = identity(); for (const transformList of splitTransformList(transformLists)) { - matrix = computeMatrix(transformList); + matrix = computeMatrix(transformList, matrix); if (matrix == null) { return null; } - - tokens.push(matrix); } - // for (let i = 0; i < tokens.length; i++) { - // - // for (let j = 0; j < tokens[i].length; j++) { - // - // toZero(tokens[i][j]); - // } - // } - const result: Token[] = []; - - for (const token of tokens) { - - let t = minify(token); - - if (t == null) { - - return null; - } - - result.push(...t); + return { + result: minify(matrix), + matrix: serialize(matrix) } - - return result; } -export function computeMatrix(transformList: Token[]): Matrix | null { +export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | null { - let matrix: Matrix = identity(); let values: number[] = []; let val: number | null; let i: number = 0; @@ -136,10 +119,13 @@ export function computeMatrix(transformList: Token[]): Matrix | null { } else if ((transformList[i] as FunctionToken).val == 'translateZ') { matrix = translateZ(values[0], matrix); + } else if ((transformList[i] as FunctionToken).val == 'translate') { + + matrix = translate(values as [number] | [number, number], matrix); } else { // @ts-ignore - matrix = translate(values as [number] | [number, number], matrix); + matrix = translate3d(values as [number] | [number, number], matrix); } } break; @@ -275,10 +261,54 @@ export function computeMatrix(transformList: Token[]): Matrix | null { matrix = scaleZ(values[0], matrix); } + } break; + + case 'skew': + case 'skewX': + case 'skewY': { + + values.length = 0; + + let child: Token; + let value: number | null; + + for (let k = 0; k < (transformList[i] as FunctionToken).chi.length; k++) { + + child = (transformList[i] as FunctionToken).chi[k]; + + if (child.typ == EnumToken.CommentTokenType || child.typ == EnumToken.WhitespaceTokenType) { + + continue; + } + + value = getAngle(child as AngleToken | NumberToken); + + if (value == null) { + + return null; + } + + values.push(value * 2 * Math.PI); + } + + if (values.length == 0 || (values.length > ((transformList[i] as FunctionToken).val == 'skew' ? 2 : 1))) { + + return null; + } + + if ((transformList[i] as FunctionToken).val == 'skew') { + + matrix = skew(values as [number] | [number, number], matrix); + } else { + + matrix = (transformList[i] as FunctionToken).val == 'skewX' ? skewX(values[0], matrix) : skewY(values[0], matrix); + } } + break; + default: return null; diff --git a/src/lib/ast/transform/matrix.ts b/src/lib/ast/transform/matrix.ts index 2c7333a1..1a56a1f2 100644 --- a/src/lib/ast/transform/matrix.ts +++ b/src/lib/ast/transform/matrix.ts @@ -51,7 +51,7 @@ export function serialize(matrix: Matrix): Token { val: 'matrix', chi: [ matrix[0][0], - matrix[1][1], + matrix[0][1], matrix[1][0], matrix[1][1], matrix[3][0], @@ -88,7 +88,7 @@ export function serialize(matrix: Matrix): Token { m.push({ typ: EnumToken.NumberTokenType, - val: reduceNumber(matrix[i][j].toPrecision(6)) + val: reduceNumber(matrix[j][i].toPrecision(6)) }) } } diff --git a/src/lib/ast/transform/minify.ts b/src/lib/ast/transform/minify.ts index 265b6379..07400dbc 100644 --- a/src/lib/ast/transform/minify.ts +++ b/src/lib/ast/transform/minify.ts @@ -8,16 +8,14 @@ export function minify(matrix: Matrix): Token[] | null { const decomposed = decompose(matrix); - // console.error({decomposed}); if (decomposed == null) { return null; } - const transforms = new Set([ - 'translate', 'scale', 'skew', 'perspective', 'rotate']); - const rotations = new Set(['x', 'y', 'z']); + const transforms = new Set(['translate', 'scale', 'skew', 'perspective', 'rotate']); const scales = new Set(['x', 'y', 'z']); + const skew = new Set(['x', 'y']); const result: Token[] = []; @@ -32,7 +30,7 @@ export function minify(matrix: Matrix): Token[] | null { transforms.delete('scale'); } - if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0 && decomposed.skew[2] == 0) { + if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0) { transforms.delete('skew'); } @@ -63,7 +61,7 @@ export function minify(matrix: Matrix): Token[] | null { result.push({ typ: EnumToken.FunctionTokenType, - val: 'translate', + val: 'translate3d', chi: [ {typ: EnumToken.LengthTokenType, val: decomposed.translate[0] + '', unit: 'px'}, {typ: EnumToken.CommaTokenType}, @@ -131,157 +129,181 @@ export function minify(matrix: Matrix): Token[] | null { } } - // scale(x, x) -> scale(x) - // scale(1, sy) -> scaleY(sy) - // scale3d(1, 1, sz) -> scaleZ(sz) - // scaleX() scale(Y) scaleZ() -> scale3d() - - if (transforms.has('scale')) { + if (transforms.has('scale')) { const [sx, sy, sz] = decomposed.scale; - if (!(sx == 1 && sy == 1 && sz == 1)) { + if (sz == 1) { - if (sz == 1) { + scales.delete('z'); + } - scales.delete('z'); - } + if (sy == 1) { - if (sy == 1) { + scales.delete('y'); + } - scales.delete('y'); - } + if (sx == 1) { - if (sx == 1) { + scales.delete('x'); + } - scales.delete('x'); - } + if (scales.size == 1) { - if (scales.size == 1) { + let prefix: string = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; - let prefix: string = scales.has('x') ? '' : scales.has('y') ? 'Y' : 'Z'; + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'scale' + prefix, + chi: [ + {typ: EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx)} + ] + }); + } else if (!scales.has('z')) { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'scale' + prefix, - chi: [ - {typ: EnumToken.NumberTokenType, val: '' + (prefix == 'Z' ? sz : prefix == 'Y' ? sy : sx)} - ] - }); - } + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'scale', + chi: [ + {typ: EnumToken.NumberTokenType, val: '' + sx}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.NumberTokenType, val: '' + sy}, + ] + }); + } else { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'scale3d', + chi: [ + {typ: EnumToken.NumberTokenType, val: '' + sx}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.NumberTokenType, val: '' + sy}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.NumberTokenType, val: '' + sz} + ] + }); + } + } - else if (!scales.has('z')) { + if (transforms.has('rotate')) { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'scale', - chi: [ - {typ: EnumToken.NumberTokenType, val: '' + sx}, - {typ: EnumToken.CommaTokenType}, - {typ: EnumToken.NumberTokenType, val: '' + sy}, - ] - }); - } + const [x, y, z, angle] = decomposed.rotate; - else { + if (y == 0 && z == 0) { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'scale3d', - chi: [ - {typ: EnumToken.NumberTokenType, val: '' + sx}, - {typ: EnumToken.CommaTokenType}, - {typ: EnumToken.NumberTokenType, val: '' + sy}, - {typ: EnumToken.CommaTokenType}, - {typ: EnumToken.NumberTokenType, val: '' + sz} - ] - }); - } + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotateX', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } else if (x == 0 && z == 0) { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotateY', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } else if (x == 0 && y == 0) { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotate', + chi: [ + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); + } else { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'rotate3d', + chi: [ + { + typ: EnumToken.NumberTokenType, + val: '' + x + }, + {typ: EnumToken.CommaTokenType}, + { + typ: EnumToken.NumberTokenType, + val: '' + y + }, + {typ: EnumToken.CommaTokenType}, + { + typ: EnumToken.NumberTokenType, + val: '' + z + }, + {typ: EnumToken.CommaTokenType}, + { + typ: EnumToken.AngleTokenType, + val: '' + angle, + unit: 'deg' + } + ] + }); } } - if (transforms.has('rotate')) { + if (transforms.has('skew')) { - const [x, y, z, angle] = decomposed.rotate; + if (decomposed.skew[0] == 0) { - // console.error({x, y, z, angle}); + skew.delete('x'); + } - if (angle != 0 && !(x == 0 && y == 0 && z == 0)) { + if (decomposed.skew[1] == 0) { - if (y == 0 && z == 0) { + skew.delete('y'); + } - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'rotateX', - chi: [ - { - typ: EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } else if (x == 0 && z == 0) { + // console.error({skew}); - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'rotateY', - chi: [ - { - typ: EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } else if (x == 0 && y == 0) { + for (let i = 0; i < 2; i++) { - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'rotate', - chi: [ - { - typ: EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } else { + decomposed.skew[i] = +(Math.atan(decomposed.skew[i]) * 180 / Math.PI).toPrecision(6); + } - result.push({ - typ: EnumToken.FunctionTokenType, - val: 'rotate3d', - chi: [ - { - typ: EnumToken.NumberTokenType, - val: '' + x - }, - {typ: EnumToken.CommaTokenType}, - { - typ: EnumToken.NumberTokenType, - val: '' + y - }, - {typ: EnumToken.CommaTokenType}, - { - typ: EnumToken.NumberTokenType, - val: '' + z - }, - {typ: EnumToken.CommaTokenType}, - { - typ: EnumToken.AngleTokenType, - val: '' + angle, - unit: 'deg' - } - ] - }); - } + if (skew.size == 1) { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'skew' + (skew.has('x') ? '' : 'Y'), + chi: [ + {typ: EnumToken.AngleTokenType, val: '' + decomposed.skew[0], unit: 'deg'} + ] + }); + } else { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'skew', + chi: [ + {typ: EnumToken.AngleTokenType, val: '' + decomposed.skew[0], unit: 'deg'}, + {typ: EnumToken.CommaTokenType}, + {typ: EnumToken.AngleTokenType, val: '' + decomposed.skew[1], unit: 'deg'} + ] + }); } } - // identity - return transforms.size >1 ? null : result.length == 0 || eq(result, identity()) ? [ + return result.length == 0 || eq(result, identity()) ? [ { typ: EnumToken.IdenTokenType, val: 'none' diff --git a/src/lib/ast/transform/skew.ts b/src/lib/ast/transform/skew.ts index cefeb2cd..ae62373d 100644 --- a/src/lib/ast/transform/skew.ts +++ b/src/lib/ast/transform/skew.ts @@ -1,32 +1,33 @@ -import {identity, Matrix} from "./utils.ts"; +import {identity, Matrix, multiply} from "./utils.ts"; -export function skewX(x: number): Matrix { +export function skewX(x: number, from: Matrix): Matrix { const matrix: Matrix = identity(); - matrix[2][0] = Math.tan( x * Math.PI / 180); - return matrix; + matrix[1][0] = Math.tan(x); + + return multiply(from, matrix); } -export function skewY(y: number): Matrix { +export function skewY(y: number, from: Matrix): Matrix { const matrix: Matrix = identity(); - matrix[0][1] = Math.tan( y * Math.PI / 180); - return matrix; + matrix[0][1] = Math.tan(y); + return multiply(from, matrix); } // convert angle to radian -export function skew(x: number, y?: number): Matrix { +export function skew(values: [number] | [number, number], from: Matrix): Matrix { const matrix: Matrix = identity(); - matrix[2][0] = Math.tan( x * Math.PI / 180); + matrix[1][0] = Math.tan(values[0]); - if (y != null) { + if (values.length > 1) { - matrix[0][1] = Math.tan(y * Math.PI / 180); + matrix[0][1] = Math.tan(values[1]!); } - return matrix; + return multiply(from, matrix); } \ No newline at end of file diff --git a/src/lib/ast/transform/translate.ts b/src/lib/ast/transform/translate.ts index 65d24a78..2df494e2 100644 --- a/src/lib/ast/transform/translate.ts +++ b/src/lib/ast/transform/translate.ts @@ -2,7 +2,7 @@ import {identity, Matrix, multiply} from "./utils.ts"; export function translateX(x: number, from: Matrix): Matrix { - const matrix = identity(); + const matrix: Matrix = identity(); matrix[3][0] = x; @@ -11,7 +11,7 @@ export function translateX(x: number, from: Matrix): Matrix { export function translateY(y: number, from: Matrix): Matrix { - const matrix = identity() + const matrix: Matrix = identity() matrix[3][1] = y; return multiply(from, matrix) as Matrix; @@ -19,20 +19,28 @@ export function translateY(y: number, from: Matrix): Matrix { export function translateZ(z: number, from: Matrix): Matrix { - const matrix = identity(); + const matrix: Matrix = identity(); matrix[3][2] = z; return multiply(from, matrix) as Matrix; } -export function translate(translate: [number] | [number, number] | [number, number, number], from: Matrix): Matrix { +export function translate(translate: [number] | [number, number], from: Matrix): Matrix { - const matrix = identity(); + const matrix: Matrix = identity(); matrix[3][0] = translate[0]; matrix[3][1] = translate[1] ?? 0; - matrix[3][2] = translate[2] ?? 0; return multiply(from, matrix) as Matrix; } -export const translate3d = translate; \ No newline at end of file +export function translate3d(translate: [number, number, number], from: Matrix): Matrix { + + const matrix: Matrix = identity(); + matrix[3][0] = translate[0]; + matrix[3][1] = translate[1]; + matrix[3][2] = translate[2]; + + return multiply(from, matrix) as Matrix; +} + diff --git a/src/lib/ast/transform/utils.ts b/src/lib/ast/transform/utils.ts index e4ab8a70..cf90a138 100644 --- a/src/lib/ast/transform/utils.ts +++ b/src/lib/ast/transform/utils.ts @@ -190,8 +190,11 @@ export function multiply(matrixA: Matrix, matrixB: Matrix): Matrix { let result: Matrix = Array(4).fill(0).map(() => Array(4).fill(0)) as Matrix; for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + for (let k = 0; k < 4; k++) { + result[j][i] += matrixA[k][i] * matrixB[j][k]; } } @@ -282,7 +285,8 @@ export function decompose2(matrix: Matrix) { } export function decompose(matrix: Matrix): DecomposedMatrix3D | null { -// Normalize the matrix. + + // Normalize the matrix. if (matrix[3][3] === 0) { return null; @@ -436,6 +440,7 @@ export function decompose(matrix: Matrix): DecomposedMatrix3D | null { const {x: x1, y: y1, z: z1, angle} = getRotation3D(rotationMatrix); return { + // @ts-ignore skew: toZero(skew) as [number, number, number], scale: toZero(scale) as [number, number, number], rotate: toZero([x1, y1, z1, angle]) as [number, number, number, number], diff --git a/test/specs/code/transform.js b/test/specs/code/transform.js index a82db02b..d03a7678 100644 --- a/test/specs/code/transform.js +++ b/test/specs/code/transform.js @@ -125,7 +125,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: rotateX(45deg) }`)); @@ -139,7 +139,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: rotate(45deg) }`)); @@ -154,7 +154,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: rotate3d(2,-1,-1,-72deg) }`)); @@ -168,7 +168,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: scale3d(.5,1,1.7)rotate3d(1,1,1,67deg) }`)); @@ -182,7 +182,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: rotateY(180deg) }`)); @@ -196,7 +196,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: rotate3d(1,1,1,180deg) }`)); @@ -214,7 +214,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: scale3d(.5,.5,.5) }`)); @@ -228,7 +228,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -243,7 +243,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -257,7 +257,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -271,7 +271,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -285,7 +285,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -299,7 +299,7 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: scale(1.5,2) }`)); @@ -313,11 +313,29 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true }).then((result) => expect(result.code).equals(`.now { transform: scaleX(0)scaleY(0)scaleZ(0) }`)); }); }); + + describe('CSS skew', function () { + + it('skew #21', function () { + const nesting1 = ` + + .now { + transform: translate(100px, 100px) rotate(1215deg) skewX(10deg); +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: translate(100px,100px)rotate(135deg)skew(10deg) +}`)); + }); + + }); } \ No newline at end of file From 0de861b996798a720f6fd64bf2c2c55ebd33bde3 Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Fri, 11 Apr 2025 22:59:47 -0400 Subject: [PATCH 08/10] add missing files #75 --- CHANGELOG.md | 8 ++ dist/index-umd-web.js | 164 +++++++++++++++++++++----- dist/index.cjs | 164 +++++++++++++++++++++----- dist/lib/ast/features/transform.js | 17 ++- dist/lib/ast/transform/compute.js | 88 +++++++++++++- dist/lib/ast/transform/minify.js | 16 ++- dist/lib/ast/transform/perspective.js | 10 ++ dist/lib/ast/transform/skew.js | 23 ++++ dist/lib/ast/transform/utils.js | 37 +++--- src/lib/ast/features/transform.ts | 18 +-- src/lib/ast/transform/compute.ts | 138 +++++++++++++++++++++- src/lib/ast/transform/matrix.ts | 1 + src/lib/ast/transform/minify.ts | 38 +++++- src/lib/ast/transform/perspective.ts | 12 +- src/lib/ast/transform/utils.ts | 44 ++++--- test/specs/code/transform.js | 17 +++ 16 files changed, 674 insertions(+), 121 deletions(-) create mode 100644 dist/lib/ast/transform/perspective.js create mode 100644 dist/lib/ast/transform/skew.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b273d254..c539f403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ # v0.9.2 +- [ ] CSS transform module level 2 + - translate + - scale + - rotate + - skew + - perspective + - matrix + - matrix3d - [x] keyframes - [x] remove consecutive keyframes with the same name - [x] reduce keyframe selector 'from' to '0%' diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index edbd4816..5aeffd8c 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -17744,14 +17744,20 @@ } return result; } - function decompose(matrix) { + function decompose(original) { // Normalize the matrix. - if (matrix[3][3] === 0) { + if (original[3][3] === 0) { return null; } - for (let i = 0; i < 4; i++) { - for (let j = 0; j < 4; j++) { - matrix[i][j] /= matrix[3][3]; + // @ts-ignore + const matrix = original.reduce((acc, curr) => acc.concat([curr.slice()]), []); + const div = Math.abs(1 / matrix[3][3]); + // const div = 1 / matrix[3][3]; + if (div != 1) { + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + matrix[i][j] *= div; + } } } // perspectiveMatrix is used to solve for perspective, but it also provides @@ -17766,14 +17772,14 @@ } let rightHandSide = [0, 0, 0, 0]; let perspective = [0, 0, 0, 0]; - let translate = [0, 0, 0]; + let translate = [matrix[3][0], matrix[3][1], matrix[3][2]]; // First, isolate perspective. - if (matrix[0][3] !== 0 || matrix[1][3] !== 0 || matrix[2][3] !== 0) { + if (original[0][3] !== 0 || original[1][3] !== 0 || original[2][3] !== 0) { // rightHandSide is the right hand side of the equation. - rightHandSide[0] = matrix[0][3]; - rightHandSide[1] = matrix[1][3]; - rightHandSide[2] = matrix[2][3]; - rightHandSide[3] = matrix[3][3]; + rightHandSide[0] = original[0][3]; + rightHandSide[1] = original[1][3]; + rightHandSide[2] = original[2][3]; + rightHandSide[3] = original[3][3]; // Solve the equation by inverting perspectiveMatrix and multiplying // rightHandSide by the inverse. let inversePerspectiveMatrix = inverse(perspectiveMatrix); @@ -17786,9 +17792,10 @@ perspective[3] = 1; } // Next take care of translation - for (let i = 0; i < 3; i++) { - translate[i] = matrix[3][i]; - } + // for (let i = 0; i < 3; i++) { + // + // translate[i] = matrix[3][i]; + // } let row = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; // Now get scale and shear. 'row' is a 3 element array of 3 component vectors for (let i = 0; i < 3; i++) { @@ -17869,7 +17876,7 @@ scale: toZero(scale), rotate: toZero([x1, y1, z1, angle]), translate: toZero(translate), - perspective, + perspective: original[2][3] == 0 ? null : +(-1 / original[2][3]).toPrecision(6), quaternion }; } @@ -18094,7 +18101,7 @@ return multiply(from, matrix); } - function minify$1(matrix) { + function minify$1(matrix, names) { const decomposed = decompose(matrix); if (decomposed == null) { return null; @@ -18102,7 +18109,7 @@ const transforms = new Set(['translate', 'scale', 'skew', 'perspective', 'rotate']); const scales = new Set(['x', 'y', 'z']); const skew = new Set(['x', 'y']); - const result = []; + let result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { transforms.delete('translate'); @@ -18113,7 +18120,7 @@ if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0) { transforms.delete('skew'); } - if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { + if (decomposed.perspective == null) { transforms.delete('perspective'); } if (decomposed.rotate[3] == 0) { @@ -18317,7 +18324,6 @@ if (decomposed.skew[1] == 0) { skew.delete('y'); } - // console.error({skew}); for (let i = 0; i < 2; i++) { decomposed.skew[i] = +(Math.atan(decomposed.skew[i]) * 180 / Math.PI).toPrecision(6); } @@ -18342,6 +18348,15 @@ }); } } + if (transforms.has('perspective')) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'perspective', + chi: [ + { typ: exports.EnumToken.Length, val: '' + decomposed.perspective, unit: 'px' }, + ] + }); + } // identity return result.length == 0 || eq(result, identity()) ? [ { @@ -18416,6 +18431,13 @@ }; } + function perspective(x, from) { + const matrix = identity(); + // @ts-ignore + matrix[2][3] = typeof x == 'object' && x.val == 'none' ? 0 : x == 0 ? Number.NEGATIVE_INFINITY : -1 / x; + return multiply(from, matrix); + } + function compute(transformLists) { transformLists = transformLists.slice(); stripCommaToken(transformLists); @@ -18423,15 +18445,72 @@ return null; } let matrix = identity(); + const names = new Set; + const cumulative = []; for (const transformList of splitTransformList(transformLists)) { matrix = computeMatrix(transformList, matrix); + switch (transformList[0].val) { + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': + if (names.has('translate')) { + names.delete('translate'); + } + names.add('translate'); + break; + case 'scale': + case 'scaleX': + case 'scaleY': + case 'scaleZ': + case 'scale3d': + if (names.has('scale')) { + names.delete('scale'); + } + names.add('scale'); + break; + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + if (names.has('rotate')) { + names.delete('rotate'); + } + names.add('rotate'); + break; + case 'skew': + case 'skewX': + case 'skewY': + if (names.has('skew')) { + names.delete('skew'); + } + names.add('skew'); + break; + case 'perspective': + if (names.has('perspective')) { + names.delete('perspective'); + } + names.add('perspective'); + break; + case 'matrix': + case 'matrix3d': + if (names.has('matrix')) { + names.delete('matrix'); + } + names.add('matrix'); + break; + } if (matrix == null) { return null; } + cumulative.push(...(minify$1(computeMatrix(transformList, identity())) ?? transformList)); } return { - result: minify$1(matrix), - matrix: serialize(matrix) + // result: minify(matrix, [...names]), + matrix: serialize(matrix), + cumulative }; } function computeMatrix(transformList, matrix) { @@ -18619,6 +18698,32 @@ } } break; + case 'perspective': + { + const values = []; + let child; + let value; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (child.typ == exports.EnumToken.IdenTokenType && child.val == 'none') { + values.push(child); + continue; + } + value = length2Px(child); + if (value == null) { + return null; + } + values.push(value); + } + if (values.length != 1) { + return null; + } + matrix = perspective(values[0], matrix); + } + break; default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); @@ -18687,16 +18792,21 @@ } const children = node.val.slice(); consumeWhitespace(children); - let { result, matrix } = compute(children) ?? {}; + let { matrix, cumulative } = compute(children) ?? {}; // console.error({result, matrix}); - // console.error({result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), matrix: matrix == null ? null : renderToken(matrix)}); - if (result == null || matrix == null) { + // console.error( + // { + // // result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), + // matrix: matrix == null ? null : renderToken(matrix), + // cumulative: cumulative == null ? null : cumulative.reduce((acc, curr) => acc + renderToken(curr), '') + // }); + if (matrix == null) { return; } - if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - result = [matrix]; + if (renderToken(matrix).length < cumulative.reduce((acc, t) => acc + renderToken(t), '').length) { + cumulative = [matrix]; } - node.val = result; + node.val = cumulative; } } } diff --git a/dist/index.cjs b/dist/index.cjs index e82e2703..1a27d2e1 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -17843,14 +17843,20 @@ function multiply(matrixA, matrixB) { } return result; } -function decompose(matrix) { +function decompose(original) { // Normalize the matrix. - if (matrix[3][3] === 0) { + if (original[3][3] === 0) { return null; } - for (let i = 0; i < 4; i++) { - for (let j = 0; j < 4; j++) { - matrix[i][j] /= matrix[3][3]; + // @ts-ignore + const matrix = original.reduce((acc, curr) => acc.concat([curr.slice()]), []); + const div = Math.abs(1 / matrix[3][3]); + // const div = 1 / matrix[3][3]; + if (div != 1) { + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + matrix[i][j] *= div; + } } } // perspectiveMatrix is used to solve for perspective, but it also provides @@ -17865,14 +17871,14 @@ function decompose(matrix) { } let rightHandSide = [0, 0, 0, 0]; let perspective = [0, 0, 0, 0]; - let translate = [0, 0, 0]; + let translate = [matrix[3][0], matrix[3][1], matrix[3][2]]; // First, isolate perspective. - if (matrix[0][3] !== 0 || matrix[1][3] !== 0 || matrix[2][3] !== 0) { + if (original[0][3] !== 0 || original[1][3] !== 0 || original[2][3] !== 0) { // rightHandSide is the right hand side of the equation. - rightHandSide[0] = matrix[0][3]; - rightHandSide[1] = matrix[1][3]; - rightHandSide[2] = matrix[2][3]; - rightHandSide[3] = matrix[3][3]; + rightHandSide[0] = original[0][3]; + rightHandSide[1] = original[1][3]; + rightHandSide[2] = original[2][3]; + rightHandSide[3] = original[3][3]; // Solve the equation by inverting perspectiveMatrix and multiplying // rightHandSide by the inverse. let inversePerspectiveMatrix = inverse(perspectiveMatrix); @@ -17885,9 +17891,10 @@ function decompose(matrix) { perspective[3] = 1; } // Next take care of translation - for (let i = 0; i < 3; i++) { - translate[i] = matrix[3][i]; - } + // for (let i = 0; i < 3; i++) { + // + // translate[i] = matrix[3][i]; + // } let row = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; // Now get scale and shear. 'row' is a 3 element array of 3 component vectors for (let i = 0; i < 3; i++) { @@ -17968,7 +17975,7 @@ function decompose(matrix) { scale: toZero(scale), rotate: toZero([x1, y1, z1, angle]), translate: toZero(translate), - perspective, + perspective: original[2][3] == 0 ? null : +(-1 / original[2][3]).toPrecision(6), quaternion }; } @@ -18193,7 +18200,7 @@ function scale3d(x, y, z, from) { return multiply(from, matrix); } -function minify$1(matrix) { +function minify$1(matrix, names) { const decomposed = decompose(matrix); if (decomposed == null) { return null; @@ -18201,7 +18208,7 @@ function minify$1(matrix) { const transforms = new Set(['translate', 'scale', 'skew', 'perspective', 'rotate']); const scales = new Set(['x', 'y', 'z']); const skew = new Set(['x', 'y']); - const result = []; + let result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { transforms.delete('translate'); @@ -18212,7 +18219,7 @@ function minify$1(matrix) { if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0) { transforms.delete('skew'); } - if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { + if (decomposed.perspective == null) { transforms.delete('perspective'); } if (decomposed.rotate[3] == 0) { @@ -18416,7 +18423,6 @@ function minify$1(matrix) { if (decomposed.skew[1] == 0) { skew.delete('y'); } - // console.error({skew}); for (let i = 0; i < 2; i++) { decomposed.skew[i] = +(Math.atan(decomposed.skew[i]) * 180 / Math.PI).toPrecision(6); } @@ -18441,6 +18447,15 @@ function minify$1(matrix) { }); } } + if (transforms.has('perspective')) { + result.push({ + typ: exports.EnumToken.FunctionTokenType, + val: 'perspective', + chi: [ + { typ: exports.EnumToken.Length, val: '' + decomposed.perspective, unit: 'px' }, + ] + }); + } // identity return result.length == 0 || eq(result, identity()) ? [ { @@ -18515,6 +18530,13 @@ function serialize(matrix) { }; } +function perspective(x, from) { + const matrix = identity(); + // @ts-ignore + matrix[2][3] = typeof x == 'object' && x.val == 'none' ? 0 : x == 0 ? Number.NEGATIVE_INFINITY : -1 / x; + return multiply(from, matrix); +} + function compute(transformLists) { transformLists = transformLists.slice(); stripCommaToken(transformLists); @@ -18522,15 +18544,72 @@ function compute(transformLists) { return null; } let matrix = identity(); + const names = new Set; + const cumulative = []; for (const transformList of splitTransformList(transformLists)) { matrix = computeMatrix(transformList, matrix); + switch (transformList[0].val) { + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': + if (names.has('translate')) { + names.delete('translate'); + } + names.add('translate'); + break; + case 'scale': + case 'scaleX': + case 'scaleY': + case 'scaleZ': + case 'scale3d': + if (names.has('scale')) { + names.delete('scale'); + } + names.add('scale'); + break; + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + if (names.has('rotate')) { + names.delete('rotate'); + } + names.add('rotate'); + break; + case 'skew': + case 'skewX': + case 'skewY': + if (names.has('skew')) { + names.delete('skew'); + } + names.add('skew'); + break; + case 'perspective': + if (names.has('perspective')) { + names.delete('perspective'); + } + names.add('perspective'); + break; + case 'matrix': + case 'matrix3d': + if (names.has('matrix')) { + names.delete('matrix'); + } + names.add('matrix'); + break; + } if (matrix == null) { return null; } + cumulative.push(...(minify$1(computeMatrix(transformList, identity())) ?? transformList)); } return { - result: minify$1(matrix), - matrix: serialize(matrix) + // result: minify(matrix, [...names]), + matrix: serialize(matrix), + cumulative }; } function computeMatrix(transformList, matrix) { @@ -18718,6 +18797,32 @@ function computeMatrix(transformList, matrix) { } } break; + case 'perspective': + { + const values = []; + let child; + let value; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == exports.EnumToken.CommentTokenType || child.typ == exports.EnumToken.WhitespaceTokenType) { + continue; + } + if (child.typ == exports.EnumToken.IdenTokenType && child.val == 'none') { + values.push(child); + continue; + } + value = length2Px(child); + if (value == null) { + return null; + } + values.push(value); + } + if (values.length != 1) { + return null; + } + matrix = perspective(values[0], matrix); + } + break; default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); @@ -18786,16 +18891,21 @@ class TransformCssFeature { } const children = node.val.slice(); consumeWhitespace(children); - let { result, matrix } = compute(children) ?? {}; + let { matrix, cumulative } = compute(children) ?? {}; // console.error({result, matrix}); - // console.error({result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), matrix: matrix == null ? null : renderToken(matrix)}); - if (result == null || matrix == null) { + // console.error( + // { + // // result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), + // matrix: matrix == null ? null : renderToken(matrix), + // cumulative: cumulative == null ? null : cumulative.reduce((acc, curr) => acc + renderToken(curr), '') + // }); + if (matrix == null) { return; } - if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - result = [matrix]; + if (renderToken(matrix).length < cumulative.reduce((acc, t) => acc + renderToken(t), '').length) { + cumulative = [matrix]; } - node.val = result; + node.val = cumulative; } } } diff --git a/dist/lib/ast/features/transform.js b/dist/lib/ast/features/transform.js index 64a16eba..00f64406 100644 --- a/dist/lib/ast/features/transform.js +++ b/dist/lib/ast/features/transform.js @@ -35,16 +35,21 @@ class TransformCssFeature { } const children = node.val.slice(); consumeWhitespace(children); - let { result, matrix } = compute(children) ?? {}; + let { matrix, cumulative } = compute(children) ?? {}; // console.error({result, matrix}); - // console.error({result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), matrix: matrix == null ? null : renderToken(matrix)}); - if (result == null || matrix == null) { + // console.error( + // { + // // result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), + // matrix: matrix == null ? null : renderToken(matrix), + // cumulative: cumulative == null ? null : cumulative.reduce((acc, curr) => acc + renderToken(curr), '') + // }); + if (matrix == null) { return; } - if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - result = [matrix]; + if (renderToken(matrix).length < cumulative.reduce((acc, t) => acc + renderToken(t), '').length) { + cumulative = [matrix]; } - node.val = result; + node.val = cumulative; } } } diff --git a/dist/lib/ast/transform/compute.js b/dist/lib/ast/transform/compute.js index 29309cce..0bcf01b1 100644 --- a/dist/lib/ast/transform/compute.js +++ b/dist/lib/ast/transform/compute.js @@ -16,6 +16,7 @@ import { scale3d, scale, scaleX, scaleY, scaleZ } from './scale.js'; import { minify } from './minify.js'; import { skew, skewX, skewY } from './skew.js'; import { serialize } from './matrix.js'; +import { perspective } from './perspective.js'; function compute(transformLists) { transformLists = transformLists.slice(); @@ -24,15 +25,72 @@ function compute(transformLists) { return null; } let matrix = identity(); + const names = new Set; + const cumulative = []; for (const transformList of splitTransformList(transformLists)) { matrix = computeMatrix(transformList, matrix); + switch (transformList[0].val) { + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': + if (names.has('translate')) { + names.delete('translate'); + } + names.add('translate'); + break; + case 'scale': + case 'scaleX': + case 'scaleY': + case 'scaleZ': + case 'scale3d': + if (names.has('scale')) { + names.delete('scale'); + } + names.add('scale'); + break; + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + if (names.has('rotate')) { + names.delete('rotate'); + } + names.add('rotate'); + break; + case 'skew': + case 'skewX': + case 'skewY': + if (names.has('skew')) { + names.delete('skew'); + } + names.add('skew'); + break; + case 'perspective': + if (names.has('perspective')) { + names.delete('perspective'); + } + names.add('perspective'); + break; + case 'matrix': + case 'matrix3d': + if (names.has('matrix')) { + names.delete('matrix'); + } + names.add('matrix'); + break; + } if (matrix == null) { return null; } + cumulative.push(...(minify(computeMatrix(transformList, identity())) ?? transformList)); } return { - result: minify(matrix), - matrix: serialize(matrix) + // result: minify(matrix, [...names]), + matrix: serialize(matrix), + cumulative }; } function computeMatrix(transformList, matrix) { @@ -220,6 +278,32 @@ function computeMatrix(transformList, matrix) { } } break; + case 'perspective': + { + const values = []; + let child; + let value; + for (let k = 0; k < transformList[i].chi.length; k++) { + child = transformList[i].chi[k]; + if (child.typ == EnumToken.CommentTokenType || child.typ == EnumToken.WhitespaceTokenType) { + continue; + } + if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { + values.push(child); + continue; + } + value = length2Px(child); + if (value == null) { + return null; + } + values.push(value); + } + if (values.length != 1) { + return null; + } + matrix = perspective(values[0], matrix); + } + break; default: return null; // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); diff --git a/dist/lib/ast/transform/minify.js b/dist/lib/ast/transform/minify.js index f0faf54b..7a81dd2b 100644 --- a/dist/lib/ast/transform/minify.js +++ b/dist/lib/ast/transform/minify.js @@ -2,7 +2,7 @@ import { decompose, identity } from './utils.js'; import { EnumToken } from '../types.js'; import { eq } from '../../parser/utils/eq.js'; -function minify(matrix) { +function minify(matrix, names) { const decomposed = decompose(matrix); if (decomposed == null) { return null; @@ -10,7 +10,7 @@ function minify(matrix) { const transforms = new Set(['translate', 'scale', 'skew', 'perspective', 'rotate']); const scales = new Set(['x', 'y', 'z']); const skew = new Set(['x', 'y']); - const result = []; + let result = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { transforms.delete('translate'); @@ -21,7 +21,7 @@ function minify(matrix) { if (decomposed.skew[0] == 0 && decomposed.skew[1] == 0) { transforms.delete('skew'); } - if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { + if (decomposed.perspective == null) { transforms.delete('perspective'); } if (decomposed.rotate[3] == 0) { @@ -225,7 +225,6 @@ function minify(matrix) { if (decomposed.skew[1] == 0) { skew.delete('y'); } - // console.error({skew}); for (let i = 0; i < 2; i++) { decomposed.skew[i] = +(Math.atan(decomposed.skew[i]) * 180 / Math.PI).toPrecision(6); } @@ -250,6 +249,15 @@ function minify(matrix) { }); } } + if (transforms.has('perspective')) { + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'perspective', + chi: [ + { typ: EnumToken.Length, val: '' + decomposed.perspective, unit: 'px' }, + ] + }); + } // identity return result.length == 0 || eq(result, identity()) ? [ { diff --git a/dist/lib/ast/transform/perspective.js b/dist/lib/ast/transform/perspective.js new file mode 100644 index 00000000..15a35151 --- /dev/null +++ b/dist/lib/ast/transform/perspective.js @@ -0,0 +1,10 @@ +import { identity, multiply } from './utils.js'; + +function perspective(x, from) { + const matrix = identity(); + // @ts-ignore + matrix[2][3] = typeof x == 'object' && x.val == 'none' ? 0 : x == 0 ? Number.NEGATIVE_INFINITY : -1 / x; + return multiply(from, matrix); +} + +export { perspective }; diff --git a/dist/lib/ast/transform/skew.js b/dist/lib/ast/transform/skew.js new file mode 100644 index 00000000..bc04525a --- /dev/null +++ b/dist/lib/ast/transform/skew.js @@ -0,0 +1,23 @@ +import { identity, multiply } from './utils.js'; + +function skewX(x, from) { + const matrix = identity(); + matrix[1][0] = Math.tan(x); + return multiply(from, matrix); +} +function skewY(y, from) { + const matrix = identity(); + matrix[0][1] = Math.tan(y); + return multiply(from, matrix); +} +// convert angle to radian +function skew(values, from) { + const matrix = identity(); + matrix[1][0] = Math.tan(values[0]); + if (values.length > 1) { + matrix[0][1] = Math.tan(values[1]); + } + return multiply(from, matrix); +} + +export { skew, skewX, skewY }; diff --git a/dist/lib/ast/transform/utils.js b/dist/lib/ast/transform/utils.js index 3fa9d440..6d14d5df 100644 --- a/dist/lib/ast/transform/utils.js +++ b/dist/lib/ast/transform/utils.js @@ -109,14 +109,20 @@ function multiply(matrixA, matrixB) { } return result; } -function decompose(matrix) { +function decompose(original) { // Normalize the matrix. - if (matrix[3][3] === 0) { + if (original[3][3] === 0) { return null; } - for (let i = 0; i < 4; i++) { - for (let j = 0; j < 4; j++) { - matrix[i][j] /= matrix[3][3]; + // @ts-ignore + const matrix = original.reduce((acc, curr) => acc.concat([curr.slice()]), []); + const div = Math.abs(1 / matrix[3][3]); + // const div = 1 / matrix[3][3]; + if (div != 1) { + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + matrix[i][j] *= div; + } } } // perspectiveMatrix is used to solve for perspective, but it also provides @@ -131,14 +137,14 @@ function decompose(matrix) { } let rightHandSide = [0, 0, 0, 0]; let perspective = [0, 0, 0, 0]; - let translate = [0, 0, 0]; + let translate = [matrix[3][0], matrix[3][1], matrix[3][2]]; // First, isolate perspective. - if (matrix[0][3] !== 0 || matrix[1][3] !== 0 || matrix[2][3] !== 0) { + if (original[0][3] !== 0 || original[1][3] !== 0 || original[2][3] !== 0) { // rightHandSide is the right hand side of the equation. - rightHandSide[0] = matrix[0][3]; - rightHandSide[1] = matrix[1][3]; - rightHandSide[2] = matrix[2][3]; - rightHandSide[3] = matrix[3][3]; + rightHandSide[0] = original[0][3]; + rightHandSide[1] = original[1][3]; + rightHandSide[2] = original[2][3]; + rightHandSide[3] = original[3][3]; // Solve the equation by inverting perspectiveMatrix and multiplying // rightHandSide by the inverse. let inversePerspectiveMatrix = inverse(perspectiveMatrix); @@ -151,9 +157,10 @@ function decompose(matrix) { perspective[3] = 1; } // Next take care of translation - for (let i = 0; i < 3; i++) { - translate[i] = matrix[3][i]; - } + // for (let i = 0; i < 3; i++) { + // + // translate[i] = matrix[3][i]; + // } let row = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; // Now get scale and shear. 'row' is a 3 element array of 3 component vectors for (let i = 0; i < 3; i++) { @@ -234,7 +241,7 @@ function decompose(matrix) { scale: toZero(scale), rotate: toZero([x1, y1, z1, angle]), translate: toZero(translate), - perspective, + perspective: original[2][3] == 0 ? null : +(-1 / original[2][3]).toPrecision(6), quaternion }; } diff --git a/src/lib/ast/features/transform.ts b/src/lib/ast/features/transform.ts index e2942f8b..2c18f825 100644 --- a/src/lib/ast/features/transform.ts +++ b/src/lib/ast/features/transform.ts @@ -54,23 +54,27 @@ export class TransformCssFeature { consumeWhitespace(children); - let {result, matrix} = compute(children as Token[]) ?? {}; + let {matrix, cumulative} = compute(children as Token[]) ?? {}; // console.error({result, matrix}); + // console.error( + // { + // // result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), + // matrix: matrix == null ? null : renderToken(matrix), + // cumulative: cumulative == null ? null : cumulative.reduce((acc, curr) => acc + renderToken(curr), '') + // }); - // console.error({result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), matrix: matrix == null ? null : renderToken(matrix)}); - - if (result == null || matrix == null) { + if ( matrix == null) { return; } - if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + if (renderToken(matrix).length < cumulative.reduce((acc, t) => acc + renderToken(t), '').length) { - result = [matrix]; + cumulative = [matrix]; } - (node as AstDeclaration).val = result; + (node as AstDeclaration).val = cumulative; } } } diff --git a/src/lib/ast/transform/compute.ts b/src/lib/ast/transform/compute.ts index 1875d36e..b5f6f8e1 100644 --- a/src/lib/ast/transform/compute.ts +++ b/src/lib/ast/transform/compute.ts @@ -1,4 +1,4 @@ -import type {AngleToken, FunctionToken, LengthToken, NumberToken, Token} from "../../../@types/token.d.ts"; +import type {AngleToken, FunctionToken, IdentToken, LengthToken, NumberToken, Token} from "../../../@types/token.d.ts"; import {identity, Matrix} from "./utils.ts"; import {EnumToken} from "../types.ts"; import {length2Px} from "./convert.ts"; @@ -11,10 +11,12 @@ import {scale, scale3d, scaleX, scaleY, scaleZ} from "./scale.ts"; import {minify} from "./minify.ts"; import {skew, skewX, skewY} from "./skew.ts"; import {serialize} from "./matrix.ts"; +import {perspective} from "./perspective.ts"; export function compute(transformLists: Token[]): { - result: Token[] | null; - matrix: Token + // result: Token[] | null; + matrix: Token, + cumulative: Token[] } | null { transformLists = transformLists.slice(); @@ -26,20 +28,104 @@ export function compute(transformLists: Token[]): { } let matrix: Matrix | null = identity(); + const names: Set = new Set; + const cumulative: Token[] = []; for (const transformList of splitTransformList(transformLists)) { matrix = computeMatrix(transformList, matrix); + switch ((transformList[0] as FunctionToken).val) { + + case 'translate': + case 'translateX': + case 'translateY': + case 'translateZ': + case 'translate3d': + + if(names.has('translate')) { + + names.delete('translate'); + } + + names.add('translate'); + break; + + case 'scale': + case 'scaleX': + case 'scaleY': + case 'scaleZ': + case 'scale3d': + + if(names.has('scale')) { + + names.delete('scale'); + } + + names.add('scale'); + + break; + + case 'rotate': + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate3d': + + if(names.has('rotate')) { + + names.delete('rotate'); + } + + names.add('rotate'); + break; + + case 'skew': + case 'skewX': + case 'skewY': + + if(names.has('skew')) { + + names.delete('skew'); + } + + names.add('skew'); + break; + + case 'perspective': + + if(names.has('perspective')) { + + names.delete('perspective'); + } + + names.add('perspective'); + break; + + case 'matrix': + case 'matrix3d': + + if(names.has('matrix')) { + + names.delete('matrix'); + } + + names.add('matrix'); + break; + } + if (matrix == null) { return null; } + + cumulative.push(...(minify(computeMatrix(transformList, identity()) as Matrix) as Token[] ?? transformList)); } return { - result: minify(matrix), - matrix: serialize(matrix) + // result: minify(matrix, [...names]), + matrix: serialize(matrix), + cumulative } } @@ -309,6 +395,48 @@ export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | break; + case 'perspective': { + + const values: Array = []; + + let child: Token; + let value: number | null; + + for (let k = 0; k < (transformList[i] as FunctionToken).chi.length; k++) { + + child = (transformList[i] as FunctionToken).chi[k]; + + if (child.typ == EnumToken.CommentTokenType || child.typ == EnumToken.WhitespaceTokenType) { + + continue; + } + + if (child.typ == EnumToken.IdenTokenType && child.val == 'none') { + + values.push(child); + continue; + } + + value = length2Px(child as LengthToken | NumberToken); + + if (value == null) { + + return null; + } + + values.push(value); + } + + if (values.length != 1) { + + return null; + } + + matrix = perspective(values[0], matrix); + } + + break; + default: return null; diff --git a/src/lib/ast/transform/matrix.ts b/src/lib/ast/transform/matrix.ts index 1a56a1f2..f8f1f9b1 100644 --- a/src/lib/ast/transform/matrix.ts +++ b/src/lib/ast/transform/matrix.ts @@ -3,6 +3,7 @@ import {EnumToken} from "../types.ts"; import type {Token} from "../../../@types/index.d.ts"; import {reduceNumber} from "../../renderer/render.ts"; +// use column-major order export function matrix(values: [number, number, number, number, number, number] | [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]): Matrix | null { const matrix = identity(); diff --git a/src/lib/ast/transform/minify.ts b/src/lib/ast/transform/minify.ts index 07400dbc..e161ac20 100644 --- a/src/lib/ast/transform/minify.ts +++ b/src/lib/ast/transform/minify.ts @@ -1,10 +1,10 @@ import {decompose, identity, Matrix} from "./utils.ts"; import {EnumToken} from "../types.ts"; -import {Token} from "../../../@types"; +import {FunctionToken, Token} from "../../../@types"; import {eq} from "../../parser/utils/eq.ts"; -export function minify(matrix: Matrix): Token[] | null { +export function minify(matrix: Matrix, names?: string[]): Token[] | null { const decomposed = decompose(matrix); @@ -17,7 +17,7 @@ export function minify(matrix: Matrix): Token[] | null { const scales = new Set(['x', 'y', 'z']); const skew = new Set(['x', 'y']); - const result: Token[] = []; + let result: Token[] = []; // check identity if (decomposed.translate[0] == 0 && decomposed.translate[1] == 0 && decomposed.translate[2] == 0) { @@ -35,7 +35,7 @@ export function minify(matrix: Matrix): Token[] | null { transforms.delete('skew'); } - if (decomposed.perspective[0] == 0 && decomposed.perspective[1] == 0 && decomposed.perspective[2] == 0 && decomposed.perspective[3] == 1) { + if (decomposed.perspective == null) { transforms.delete('perspective'); } @@ -272,8 +272,6 @@ export function minify(matrix: Matrix): Token[] | null { skew.delete('y'); } - // console.error({skew}); - for (let i = 0; i < 2; i++) { decomposed.skew[i] = +(Math.atan(decomposed.skew[i]) * 180 / Math.PI).toPrecision(6); @@ -302,6 +300,34 @@ export function minify(matrix: Matrix): Token[] | null { } } + if (transforms.has('perspective')) { + + result.push({ + typ: EnumToken.FunctionTokenType, + val: 'perspective', + chi: [ + {typ: EnumToken.Length, val: '' + decomposed.perspective, unit: 'px'}, + ] + }); + } + + if (names != null) { + + result = names.reduce((acc, curr) => { + + for (let i = 0; i < result.length; i++) { + + if (result[i].typ != EnumToken.FunctionTokenType || (result[i] as FunctionToken).val.startsWith(curr)) { + + acc.push(result[i]); + result.splice(i--, 1); + } + } + + return acc; + }, [] as Token[]); + } + // identity return result.length == 0 || eq(result, identity()) ? [ { diff --git a/src/lib/ast/transform/perspective.ts b/src/lib/ast/transform/perspective.ts index f36793c4..31f22f8f 100644 --- a/src/lib/ast/transform/perspective.ts +++ b/src/lib/ast/transform/perspective.ts @@ -1,9 +1,11 @@ -import {identity, Matrix} from "./utils.ts"; +import {identity, Matrix, multiply} from "./utils.ts"; +import {IdentToken} from "../../../@types"; -export function perspective(z: number): Matrix { +export function perspective(x: number | IdentToken, from: Matrix): Matrix { const matrix: Matrix = identity(); - matrix[2][3] = -1 / z; + // @ts-ignore + matrix[2][3] = typeof x == 'object' && x.val == 'none' ? 0 : x == 0 ? Number.NEGATIVE_INFINITY : -1 / x; - return matrix; -} \ No newline at end of file + return multiply(from, matrix); +} diff --git a/src/lib/ast/transform/utils.ts b/src/lib/ast/transform/utils.ts index cf90a138..ad1a1040 100644 --- a/src/lib/ast/transform/utils.ts +++ b/src/lib/ast/transform/utils.ts @@ -9,7 +9,7 @@ interface DecomposedMatrix3D { scale: [number, number, number]; rotate: [number, number, number, number]; translate: [number, number, number]; - perspective: [number, number, number, number]; + perspective: number | null; quaternion: [number, number, number, number]; } @@ -284,19 +284,28 @@ export function decompose2(matrix: Matrix) { } } -export function decompose(matrix: Matrix): DecomposedMatrix3D | null { +export function decompose(original: Matrix): DecomposedMatrix3D | null { // Normalize the matrix. - if (matrix[3][3] === 0) { + if (original[3][3] === 0) { return null; } - for (let i = 0; i < 4; i++) { + // @ts-ignore + const matrix: Matrix = original.reduce((acc, curr: Vector): Matrix => acc.concat([curr.slice()]), [] as Matrix); - for (let j = 0; j < 4; j++) { + const div = Math.abs(1 / matrix[3][3]); + // const div = 1 / matrix[3][3]; + + if (div != 1) { + + for (let i = 0; i < 4; i++) { + + for (let j = 0; j < 4; j++) { - matrix[i][j] /= matrix[3][3]; + matrix[i][j] *= div; + } } } @@ -318,15 +327,15 @@ export function decompose(matrix: Matrix): DecomposedMatrix3D | null { let rightHandSide: Vector = [0, 0, 0, 0]; let perspective: Vector = [0, 0, 0, 0]; - let translate: [number, number, number] = [0, 0, 0]; + let translate: [number, number, number] = [matrix[3][0], matrix[3][1], matrix[3][2]]; // First, isolate perspective. - if (matrix[0][3] !== 0 || matrix[1][3] !== 0 || matrix[2][3] !== 0) { + if (original[0][3] !== 0 || original[1][3] !== 0 || original[2][3] !== 0) { // rightHandSide is the right hand side of the equation. - rightHandSide[0] = matrix[0][3]; - rightHandSide[1] = matrix[1][3]; - rightHandSide[2] = matrix[2][3]; - rightHandSide[3] = matrix[3][3]; + rightHandSide[0] = original[0][3]; + rightHandSide[1] = original[1][3]; + rightHandSide[2] = original[2][3]; + rightHandSide[3] = original[3][3]; // Solve the equation by inverting perspectiveMatrix and multiplying // rightHandSide by the inverse. @@ -340,10 +349,10 @@ export function decompose(matrix: Matrix): DecomposedMatrix3D | null { } // Next take care of translation - for (let i = 0; i < 3; i++) { - - translate[i] = matrix[3][i]; - } +// for (let i = 0; i < 3; i++) { +// +// translate[i] = matrix[3][i]; +// } let row: [[number, number, number], [number, number, number], [number, number, number]] = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; @@ -445,11 +454,12 @@ export function decompose(matrix: Matrix): DecomposedMatrix3D | null { scale: toZero(scale) as [number, number, number], rotate: toZero([x1, y1, z1, angle]) as [number, number, number, number], translate: toZero(translate) as [number, number, number], - perspective, + perspective: original[2][3] == 0 ? null : +(-1 / original[2][3]).toPrecision( 6), quaternion }; } + export function toZero(v: [number, number] | [number, number, number] | [number, number, number, number]) { for (let i = 0; i < v.length; i++) { diff --git a/test/specs/code/transform.js b/test/specs/code/transform.js index d03a7678..06e0968a 100644 --- a/test/specs/code/transform.js +++ b/test/specs/code/transform.js @@ -338,4 +338,21 @@ export function run(describe, expect, transform, parse, render, dirname, readFil }); }); + describe('CSS perspective', function () { + + it('skew #22', function () { + const nesting1 = ` + + .now { + transform: perspective(50px) translateZ(100px) +} +`; + return transform(nesting1, { + beautify: true + }).then((result) => expect(result.code).equals(`.now { + transform: perspective(50px)translateZ(100px) +}`)); + }); + + }); } \ No newline at end of file From 8ec4e52dea7fd076e1d728daa204e45575c33cc7 Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Fri, 18 Apr 2025 19:32:55 -0400 Subject: [PATCH 09/10] add matrix() minification #75 --- CHANGELOG.md | 20 +-- README.md | 1 + dist/index-umd-web.js | 228 ++++++++++++++--------------- dist/index.cjs | 228 ++++++++++++++--------------- dist/index.d.ts | 1 + dist/lib/ast/features/transform.js | 23 ++- dist/lib/ast/transform/compute.js | 135 +++++++---------- dist/lib/ast/transform/matrix.js | 45 +++++- dist/lib/ast/transform/minify.js | 4 +- dist/lib/ast/transform/scale.js | 6 - dist/lib/ast/transform/utils.js | 2 +- dist/lib/parser/parse.js | 19 +-- dist/lib/syntax/syntax.js | 4 +- jsr.json | 2 +- package.json | 2 +- src/@types/index.d.ts | 1 + src/lib/ast/features/transform.ts | 27 ++-- src/lib/ast/transform/compute.ts | 178 +++++++++------------- src/lib/ast/transform/matrix.ts | 27 ++-- src/lib/ast/transform/minify.ts | 4 +- src/lib/ast/transform/scale.ts | 8 +- src/lib/ast/transform/utils.ts | 167 ++++++++------------- src/lib/parser/parse.ts | 20 +-- src/lib/syntax/syntax.ts | 4 +- test/specs/code/malformed.js | 3 +- test/specs/code/transform.js | 94 ++++++++---- 26 files changed, 577 insertions(+), 676 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c539f403..ea3f150d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,15 @@ # Changelog -# v0.9.2 - -- [ ] CSS transform module level 2 - - translate - - scale - - rotate - - skew - - perspective - - matrix - - matrix3d +# v1.0.0 + +- [x] experimental CSS transform module level 2 + - [x] translate + - [x] scale + - [x] rotate + - [x] skew + - [x] perspective + - [x] matrix + - [ ] matrix3d - [x] keyframes - [x] remove consecutive keyframes with the same name - [x] reduce keyframe selector 'from' to '0%' diff --git a/README.md b/README.md index 124f7e97..8a7b1bfc 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ Include ParseOptions and RenderOptions - expandNestingRules: boolean, optional. convert nesting rules into separate rules. will automatically set nestingRules to false. - removeDuplicateDeclarations: boolean, optional. remove duplicate declarations. +- computeTransform: boolean, optional. compute css transform functions. - computeShorthand: boolean, optional. compute shorthand properties. - computeCalcExpression: boolean, optional. evaluate calc() expression - inlineCssVariables: boolean, optional. replace some css variables with their actual value. they must be declared once diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 5aeffd8c..e59df34b 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -4029,12 +4029,12 @@ const colorFontTech = ['color-colrv0', 'color-colrv1', 'color-svg', 'color-sbix', 'color-cbdt']; const fontFeaturesTech = ['features-opentype', 'features-aat', 'features-graphite', 'incremental-patch', 'incremental-range', 'incremental-auto', 'variations', 'palettes']; const transformFunctions = [ - 'matrix', 'translate', 'scale', 'rotate', 'skew', 'perspective', + 'translate', 'scale', 'rotate', 'skew', 'perspective', 'translateX', 'translateY', 'translateZ', 'scaleX', 'scaleY', 'scaleZ', 'rotateX', 'rotateY', 'rotateZ', 'skewX', 'skewY', - 'rotate3d', 'translate3d', 'scale3d', 'matrix3d' + 'rotate3d', 'translate3d', 'scale3d', 'matrix', 'matrix3d' ]; // https://drafts.csswg.org/mediaqueries/#media-types const mediaTypes = ['all', 'print', 'screen', @@ -14413,6 +14413,7 @@ removeCharset: true, removeEmpty: true, removeDuplicateDeclarations: true, + computeTransform: false, computeShorthand: true, computeCalcExpression: true, inlineCssVariables: false, @@ -15076,24 +15077,6 @@ }; const result = parseDeclarationNode(node, errors, src, position); if (result != null) { - // if (options.validation) { - // - // const valid: ValidationResult = validateDeclaration(result, options, context); - // - // // console.error({valid}); - // - // if (valid.valid == ValidationLevel.Drop) { - // - // errors.push({ - // action: 'drop', - // message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, {minify: false}), '') + '"', - // // @ts-ignore - // location: {src, ...(map.get(valid.node) ?? position)} - // }); - // - // return null; - // } - // } // @ts-ignore context.chi.push(result); Object.defineProperty(result, 'parent', { ...definedPropertySettings, value: context }); @@ -17882,7 +17865,7 @@ } function toZero(v) { for (let i = 0; i < v.length; i++) { - if (Math.abs(v[i]) <= 1e-6) { + if (Math.abs(v[i]) <= 1e-5) { v[i] = 0; } else { @@ -18069,21 +18052,15 @@ function scaleX(x, from) { const matrix = identity(); matrix[0][0] = x; - // matrix[1][1] = 1; - // matrix[2][2] = 1; return multiply(from, matrix); } function scaleY(y, from) { const matrix = identity(); - // matrix[0][0] = 1; matrix[1][1] = y; - // matrix[2][2] = 1; return multiply(from, matrix); } function scaleZ(z, from) { const matrix = identity(); - // matrix[0][0] = 1; - // matrix[1][1] = 1; matrix[2][2] = z; return multiply(from, matrix); } @@ -18102,7 +18079,7 @@ } function minify$1(matrix, names) { - const decomposed = decompose(matrix); + const decomposed = /* is2DMatrix(matrix) ? decompose2(matrix) : */ decompose(matrix); if (decomposed == null) { return null; } @@ -18167,7 +18144,7 @@ else if (coordinates.has('z')) { result.push({ typ: exports.EnumToken.FunctionTokenType, - val: 'translate', + val: 'translate3d', chi: [ decomposed.translate[0] == 0 ? { typ: exports.EnumToken.NumberTokenType, @@ -18386,20 +18363,55 @@ return multiply(from, matrix); } + // use column-major order + function matrix(values) { + const matrix = identity(); + if (values.length === 6) { + // matrix(scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY()) + matrix[0][0] = values[0]; + matrix[0][1] = values[1]; + matrix[1][0] = values[2]; + matrix[1][1] = values[3]; + matrix[3][0] = values[4]; + matrix[3][1] = values[5]; + } + else if (values.length === 16) { + matrix[0][0] = values[0]; + matrix[0][1] = values[1]; + matrix[0][2] = values[2]; + matrix[0][3] = values[3]; + matrix[1][0] = values[4]; + matrix[1][1] = values[5]; + matrix[1][2] = values[6]; + matrix[1][3] = values[7]; + matrix[2][0] = values[8]; + matrix[2][1] = values[9]; + matrix[2][2] = values[10]; + matrix[2][3] = values[11]; + matrix[3][0] = values[12]; + matrix[3][1] = values[13]; + matrix[3][2] = values[14]; + matrix[3][3] = values[15]; + } + else { + throw new RangeError('expecting 6 or 16 values'); + } + return matrix; + } function serialize(matrix) { if (is2DMatrix(matrix)) { // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset return { typ: exports.EnumToken.FunctionTokenType, val: 'matrix', - chi: [ + chi: toZero([ matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1], matrix[3][0], matrix[3][1] - ].reduce((acc, t) => { + ]).reduce((acc, t) => { if (acc.length > 0) { acc.push({ typ: exports.EnumToken.CommaTokenType }); } @@ -18412,7 +18424,7 @@ }; } let m = []; - // console.error(JSON.stringify({matrix},null, 1)); + matrix = matrix.map((v) => toZero(v)); for (let i = 0; i < matrix.length; i++) { for (let j = 0; j < matrix[i].length; j++) { if (m.length > 0) { @@ -18445,75 +18457,24 @@ return null; } let matrix = identity(); - const names = new Set; + let mat; const cumulative = []; for (const transformList of splitTransformList(transformLists)) { - matrix = computeMatrix(transformList, matrix); - switch (transformList[0].val) { - case 'translate': - case 'translateX': - case 'translateY': - case 'translateZ': - case 'translate3d': - if (names.has('translate')) { - names.delete('translate'); - } - names.add('translate'); - break; - case 'scale': - case 'scaleX': - case 'scaleY': - case 'scaleZ': - case 'scale3d': - if (names.has('scale')) { - names.delete('scale'); - } - names.add('scale'); - break; - case 'rotate': - case 'rotateX': - case 'rotateY': - case 'rotateZ': - case 'rotate3d': - if (names.has('rotate')) { - names.delete('rotate'); - } - names.add('rotate'); - break; - case 'skew': - case 'skewX': - case 'skewY': - if (names.has('skew')) { - names.delete('skew'); - } - names.add('skew'); - break; - case 'perspective': - if (names.has('perspective')) { - names.delete('perspective'); - } - names.add('perspective'); - break; - case 'matrix': - case 'matrix3d': - if (names.has('matrix')) { - names.delete('matrix'); - } - names.add('matrix'); - break; - } - if (matrix == null) { + mat = computeMatrix(transformList, identity()); + if (mat == null) { return null; } - cumulative.push(...(minify$1(computeMatrix(transformList, identity())) ?? transformList)); + matrix = multiply(matrix, mat); + cumulative.push(...(minify$1(mat) ?? transformList)); } + const serialized = serialize(matrix); return { - // result: minify(matrix, [...names]), matrix: serialize(matrix), - cumulative + cumulative, + minified: minify$1(matrix) ?? [serialized] }; } - function computeMatrix(transformList, matrix) { + function computeMatrix(transformList, matrixVar) { let values = []; let val; let i = 0; @@ -18556,20 +18517,20 @@ return null; } if (transformList[i].val == 'translateX') { - matrix = translateX(values[0], matrix); + matrixVar = translateX(values[0], matrixVar); } else if (transformList[i].val == 'translateY') { - matrix = translateY(values[0], matrix); + matrixVar = translateY(values[0], matrixVar); } else if (transformList[i].val == 'translateZ') { - matrix = translateZ(values[0], matrix); + matrixVar = translateZ(values[0], matrixVar); } else if (transformList[i].val == 'translate') { - matrix = translate(values, matrix); + matrixVar = translate(values, matrixVar); } else { // @ts-ignore - matrix = translate3d(values, matrix); + matrixVar = translate3d(values, matrixVar); } } break; @@ -18613,10 +18574,10 @@ return null; } if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { - matrix = rotate(angle * 2 * Math.PI, matrix); + matrixVar = rotate(angle * 2 * Math.PI, matrixVar); } else { - matrix = rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + matrixVar = rotate3D(angle * 2 * Math.PI, x, y, z, matrixVar); } } break; @@ -18645,27 +18606,27 @@ if (values.length != 3) { return null; } - matrix = scale3d(...values, matrix); + matrixVar = scale3d(...values, matrixVar); break; } if (transformList[i].val == 'scale') { if (values.length != 1 && values.length != 2) { return null; } - matrix = scale(values[0], values[1] ?? values[0], matrix); + matrixVar = scale(values[0], values[1] ?? values[0], matrixVar); break; } if (values.length != 1) { return null; } else if (transformList[i].val == 'scaleX') { - matrix = scaleX(values[0], matrix); + matrixVar = scaleX(values[0], matrixVar); } else if (transformList[i].val == 'scaleY') { - matrix = scaleY(values[0], matrix); + matrixVar = scaleY(values[0], matrixVar); } else if (transformList[i].val == 'scaleZ') { - matrix = scaleZ(values[0], matrix); + matrixVar = scaleZ(values[0], matrixVar); } } break; @@ -18691,10 +18652,10 @@ return null; } if (transformList[i].val == 'skew') { - matrix = skew(values, matrix); + matrixVar = skew(values, matrixVar); } else { - matrix = transformList[i].val == 'skewX' ? skewX(values[0], matrix) : skewY(values[0], matrix); + matrixVar = transformList[i].val == 'skewX' ? skewX(values[0], matrixVar) : skewY(values[0], matrixVar); } } break; @@ -18721,15 +18682,41 @@ if (values.length != 1) { return null; } - matrix = perspective(values[0], matrix); + matrixVar = perspective(values[0], matrixVar); + } + break; + case 'matrix3d': + return null; + case 'matrix': + { + const values = []; + let value; + for (const token of transformList[i].chi) { + if ([exports.EnumToken.WhitespaceTokenType, exports.EnumToken.CommentTokenType, exports.EnumToken.CommaTokenType].includes(token.typ)) { + continue; + } + value = getNumber(token); + if (value == null) { + return null; + } + values.push(value); + } + if (transformList[i].val == 'matrix') { + if (values.length != 6) { + return null; + } + } + else if (values.length != 16) { + return null; + } + matrixVar = multiply(matrixVar, matrix(values)); } break; default: return null; - // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); } } - return matrix; + return matrixVar; } function splitTransformList(transformList) { let pattern = null; @@ -18771,7 +18758,7 @@ } static register(options) { // @ts-ignore - if (options.minify || options.computeCalcExpression || options.computeShorthand) { + if (options.computeTransform) { // @ts-ignore options.features.push(new TransformCssFeature()); } @@ -18792,21 +18779,18 @@ } const children = node.val.slice(); consumeWhitespace(children); - let { matrix, cumulative } = compute(children) ?? {}; - // console.error({result, matrix}); - // console.error( - // { - // // result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), - // matrix: matrix == null ? null : renderToken(matrix), - // cumulative: cumulative == null ? null : cumulative.reduce((acc, curr) => acc + renderToken(curr), '') - // }); - if (matrix == null) { + let { matrix, cumulative, minified } = compute(children) ?? {}; + if (matrix == null || cumulative == null || minified == null) { return; } - if (renderToken(matrix).length < cumulative.reduce((acc, t) => acc + renderToken(t), '').length) { - cumulative = [matrix]; + let result = cumulative; + if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = [matrix]; + } + if (matrix != minified[0] && minified.reduce((acc, t) => acc + renderToken(t), '').length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = minified; } - node.val = cumulative; + node.val = result; } } } diff --git a/dist/index.cjs b/dist/index.cjs index 1a27d2e1..5279d221 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -4028,12 +4028,12 @@ const fontFormat = ['collection', 'embedded-opentype', 'opentype', 'svg', 'truet const colorFontTech = ['color-colrv0', 'color-colrv1', 'color-svg', 'color-sbix', 'color-cbdt']; const fontFeaturesTech = ['features-opentype', 'features-aat', 'features-graphite', 'incremental-patch', 'incremental-range', 'incremental-auto', 'variations', 'palettes']; const transformFunctions = [ - 'matrix', 'translate', 'scale', 'rotate', 'skew', 'perspective', + 'translate', 'scale', 'rotate', 'skew', 'perspective', 'translateX', 'translateY', 'translateZ', 'scaleX', 'scaleY', 'scaleZ', 'rotateX', 'rotateY', 'rotateZ', 'skewX', 'skewY', - 'rotate3d', 'translate3d', 'scale3d', 'matrix3d' + 'rotate3d', 'translate3d', 'scale3d', 'matrix', 'matrix3d' ]; // https://drafts.csswg.org/mediaqueries/#media-types const mediaTypes = ['all', 'print', 'screen', @@ -14512,6 +14512,7 @@ async function doParse(iterator, options = {}) { removeCharset: true, removeEmpty: true, removeDuplicateDeclarations: true, + computeTransform: false, computeShorthand: true, computeCalcExpression: true, inlineCssVariables: false, @@ -15175,24 +15176,6 @@ async function parseNode(results, context, stats, options, errors, src, map, raw }; const result = parseDeclarationNode(node, errors, src, position); if (result != null) { - // if (options.validation) { - // - // const valid: ValidationResult = validateDeclaration(result, options, context); - // - // // console.error({valid}); - // - // if (valid.valid == ValidationLevel.Drop) { - // - // errors.push({ - // action: 'drop', - // message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, {minify: false}), '') + '"', - // // @ts-ignore - // location: {src, ...(map.get(valid.node) ?? position)} - // }); - // - // return null; - // } - // } // @ts-ignore context.chi.push(result); Object.defineProperty(result, 'parent', { ...definedPropertySettings, value: context }); @@ -17981,7 +17964,7 @@ function decompose(original) { } function toZero(v) { for (let i = 0; i < v.length; i++) { - if (Math.abs(v[i]) <= 1e-6) { + if (Math.abs(v[i]) <= 1e-5) { v[i] = 0; } else { @@ -18168,21 +18151,15 @@ function rotate(angle, from) { function scaleX(x, from) { const matrix = identity(); matrix[0][0] = x; - // matrix[1][1] = 1; - // matrix[2][2] = 1; return multiply(from, matrix); } function scaleY(y, from) { const matrix = identity(); - // matrix[0][0] = 1; matrix[1][1] = y; - // matrix[2][2] = 1; return multiply(from, matrix); } function scaleZ(z, from) { const matrix = identity(); - // matrix[0][0] = 1; - // matrix[1][1] = 1; matrix[2][2] = z; return multiply(from, matrix); } @@ -18201,7 +18178,7 @@ function scale3d(x, y, z, from) { } function minify$1(matrix, names) { - const decomposed = decompose(matrix); + const decomposed = /* is2DMatrix(matrix) ? decompose2(matrix) : */ decompose(matrix); if (decomposed == null) { return null; } @@ -18266,7 +18243,7 @@ function minify$1(matrix, names) { else if (coordinates.has('z')) { result.push({ typ: exports.EnumToken.FunctionTokenType, - val: 'translate', + val: 'translate3d', chi: [ decomposed.translate[0] == 0 ? { typ: exports.EnumToken.NumberTokenType, @@ -18485,20 +18462,55 @@ function skew(values, from) { return multiply(from, matrix); } +// use column-major order +function matrix(values) { + const matrix = identity(); + if (values.length === 6) { + // matrix(scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY()) + matrix[0][0] = values[0]; + matrix[0][1] = values[1]; + matrix[1][0] = values[2]; + matrix[1][1] = values[3]; + matrix[3][0] = values[4]; + matrix[3][1] = values[5]; + } + else if (values.length === 16) { + matrix[0][0] = values[0]; + matrix[0][1] = values[1]; + matrix[0][2] = values[2]; + matrix[0][3] = values[3]; + matrix[1][0] = values[4]; + matrix[1][1] = values[5]; + matrix[1][2] = values[6]; + matrix[1][3] = values[7]; + matrix[2][0] = values[8]; + matrix[2][1] = values[9]; + matrix[2][2] = values[10]; + matrix[2][3] = values[11]; + matrix[3][0] = values[12]; + matrix[3][1] = values[13]; + matrix[3][2] = values[14]; + matrix[3][3] = values[15]; + } + else { + throw new RangeError('expecting 6 or 16 values'); + } + return matrix; +} function serialize(matrix) { if (is2DMatrix(matrix)) { // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset return { typ: exports.EnumToken.FunctionTokenType, val: 'matrix', - chi: [ + chi: toZero([ matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1], matrix[3][0], matrix[3][1] - ].reduce((acc, t) => { + ]).reduce((acc, t) => { if (acc.length > 0) { acc.push({ typ: exports.EnumToken.CommaTokenType }); } @@ -18511,7 +18523,7 @@ function serialize(matrix) { }; } let m = []; - // console.error(JSON.stringify({matrix},null, 1)); + matrix = matrix.map((v) => toZero(v)); for (let i = 0; i < matrix.length; i++) { for (let j = 0; j < matrix[i].length; j++) { if (m.length > 0) { @@ -18544,75 +18556,24 @@ function compute(transformLists) { return null; } let matrix = identity(); - const names = new Set; + let mat; const cumulative = []; for (const transformList of splitTransformList(transformLists)) { - matrix = computeMatrix(transformList, matrix); - switch (transformList[0].val) { - case 'translate': - case 'translateX': - case 'translateY': - case 'translateZ': - case 'translate3d': - if (names.has('translate')) { - names.delete('translate'); - } - names.add('translate'); - break; - case 'scale': - case 'scaleX': - case 'scaleY': - case 'scaleZ': - case 'scale3d': - if (names.has('scale')) { - names.delete('scale'); - } - names.add('scale'); - break; - case 'rotate': - case 'rotateX': - case 'rotateY': - case 'rotateZ': - case 'rotate3d': - if (names.has('rotate')) { - names.delete('rotate'); - } - names.add('rotate'); - break; - case 'skew': - case 'skewX': - case 'skewY': - if (names.has('skew')) { - names.delete('skew'); - } - names.add('skew'); - break; - case 'perspective': - if (names.has('perspective')) { - names.delete('perspective'); - } - names.add('perspective'); - break; - case 'matrix': - case 'matrix3d': - if (names.has('matrix')) { - names.delete('matrix'); - } - names.add('matrix'); - break; - } - if (matrix == null) { + mat = computeMatrix(transformList, identity()); + if (mat == null) { return null; } - cumulative.push(...(minify$1(computeMatrix(transformList, identity())) ?? transformList)); + matrix = multiply(matrix, mat); + cumulative.push(...(minify$1(mat) ?? transformList)); } + const serialized = serialize(matrix); return { - // result: minify(matrix, [...names]), matrix: serialize(matrix), - cumulative + cumulative, + minified: minify$1(matrix) ?? [serialized] }; } -function computeMatrix(transformList, matrix) { +function computeMatrix(transformList, matrixVar) { let values = []; let val; let i = 0; @@ -18655,20 +18616,20 @@ function computeMatrix(transformList, matrix) { return null; } if (transformList[i].val == 'translateX') { - matrix = translateX(values[0], matrix); + matrixVar = translateX(values[0], matrixVar); } else if (transformList[i].val == 'translateY') { - matrix = translateY(values[0], matrix); + matrixVar = translateY(values[0], matrixVar); } else if (transformList[i].val == 'translateZ') { - matrix = translateZ(values[0], matrix); + matrixVar = translateZ(values[0], matrixVar); } else if (transformList[i].val == 'translate') { - matrix = translate(values, matrix); + matrixVar = translate(values, matrixVar); } else { // @ts-ignore - matrix = translate3d(values, matrix); + matrixVar = translate3d(values, matrixVar); } } break; @@ -18712,10 +18673,10 @@ function computeMatrix(transformList, matrix) { return null; } if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { - matrix = rotate(angle * 2 * Math.PI, matrix); + matrixVar = rotate(angle * 2 * Math.PI, matrixVar); } else { - matrix = rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + matrixVar = rotate3D(angle * 2 * Math.PI, x, y, z, matrixVar); } } break; @@ -18744,27 +18705,27 @@ function computeMatrix(transformList, matrix) { if (values.length != 3) { return null; } - matrix = scale3d(...values, matrix); + matrixVar = scale3d(...values, matrixVar); break; } if (transformList[i].val == 'scale') { if (values.length != 1 && values.length != 2) { return null; } - matrix = scale(values[0], values[1] ?? values[0], matrix); + matrixVar = scale(values[0], values[1] ?? values[0], matrixVar); break; } if (values.length != 1) { return null; } else if (transformList[i].val == 'scaleX') { - matrix = scaleX(values[0], matrix); + matrixVar = scaleX(values[0], matrixVar); } else if (transformList[i].val == 'scaleY') { - matrix = scaleY(values[0], matrix); + matrixVar = scaleY(values[0], matrixVar); } else if (transformList[i].val == 'scaleZ') { - matrix = scaleZ(values[0], matrix); + matrixVar = scaleZ(values[0], matrixVar); } } break; @@ -18790,10 +18751,10 @@ function computeMatrix(transformList, matrix) { return null; } if (transformList[i].val == 'skew') { - matrix = skew(values, matrix); + matrixVar = skew(values, matrixVar); } else { - matrix = transformList[i].val == 'skewX' ? skewX(values[0], matrix) : skewY(values[0], matrix); + matrixVar = transformList[i].val == 'skewX' ? skewX(values[0], matrixVar) : skewY(values[0], matrixVar); } } break; @@ -18820,15 +18781,41 @@ function computeMatrix(transformList, matrix) { if (values.length != 1) { return null; } - matrix = perspective(values[0], matrix); + matrixVar = perspective(values[0], matrixVar); + } + break; + case 'matrix3d': + return null; + case 'matrix': + { + const values = []; + let value; + for (const token of transformList[i].chi) { + if ([exports.EnumToken.WhitespaceTokenType, exports.EnumToken.CommentTokenType, exports.EnumToken.CommaTokenType].includes(token.typ)) { + continue; + } + value = getNumber(token); + if (value == null) { + return null; + } + values.push(value); + } + if (transformList[i].val == 'matrix') { + if (values.length != 6) { + return null; + } + } + else if (values.length != 16) { + return null; + } + matrixVar = multiply(matrixVar, matrix(values)); } break; default: return null; - // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); } } - return matrix; + return matrixVar; } function splitTransformList(transformList) { let pattern = null; @@ -18870,7 +18857,7 @@ class TransformCssFeature { } static register(options) { // @ts-ignore - if (options.minify || options.computeCalcExpression || options.computeShorthand) { + if (options.computeTransform) { // @ts-ignore options.features.push(new TransformCssFeature()); } @@ -18891,21 +18878,18 @@ class TransformCssFeature { } const children = node.val.slice(); consumeWhitespace(children); - let { matrix, cumulative } = compute(children) ?? {}; - // console.error({result, matrix}); - // console.error( - // { - // // result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), - // matrix: matrix == null ? null : renderToken(matrix), - // cumulative: cumulative == null ? null : cumulative.reduce((acc, curr) => acc + renderToken(curr), '') - // }); - if (matrix == null) { + let { matrix, cumulative, minified } = compute(children) ?? {}; + if (matrix == null || cumulative == null || minified == null) { return; } - if (renderToken(matrix).length < cumulative.reduce((acc, t) => acc + renderToken(t), '').length) { - cumulative = [matrix]; + let result = cumulative; + if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = [matrix]; + } + if (matrix != minified[0] && minified.reduce((acc, t) => acc + renderToken(t), '').length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = minified; } - node.val = cumulative; + node.val = result; } } } diff --git a/dist/index.d.ts b/dist/index.d.ts index c976ec07..d7b14abb 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1067,6 +1067,7 @@ interface MinifyOptions { expandNestingRules?: boolean; removeDuplicateDeclarations?: boolean; computeShorthand?: boolean; + computeTransform?: boolean; computeCalcExpression?: boolean; inlineCssVariables?: boolean; removeEmpty?: boolean; diff --git a/dist/lib/ast/features/transform.js b/dist/lib/ast/features/transform.js index 00f64406..c9746347 100644 --- a/dist/lib/ast/features/transform.js +++ b/dist/lib/ast/features/transform.js @@ -14,7 +14,7 @@ class TransformCssFeature { } static register(options) { // @ts-ignore - if (options.minify || options.computeCalcExpression || options.computeShorthand) { + if (options.computeTransform) { // @ts-ignore options.features.push(new TransformCssFeature()); } @@ -35,21 +35,18 @@ class TransformCssFeature { } const children = node.val.slice(); consumeWhitespace(children); - let { matrix, cumulative } = compute(children) ?? {}; - // console.error({result, matrix}); - // console.error( - // { - // // result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), - // matrix: matrix == null ? null : renderToken(matrix), - // cumulative: cumulative == null ? null : cumulative.reduce((acc, curr) => acc + renderToken(curr), '') - // }); - if (matrix == null) { + let { matrix, cumulative, minified } = compute(children) ?? {}; + if (matrix == null || cumulative == null || minified == null) { return; } - if (renderToken(matrix).length < cumulative.reduce((acc, t) => acc + renderToken(t), '').length) { - cumulative = [matrix]; + let result = cumulative; + if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = [matrix]; } - node.val = cumulative; + if (matrix != minified[0] && minified.reduce((acc, t) => acc + renderToken(t), '').length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + result = minified; + } + node.val = result; } } } diff --git a/dist/lib/ast/transform/compute.js b/dist/lib/ast/transform/compute.js index 0bcf01b1..efde7a11 100644 --- a/dist/lib/ast/transform/compute.js +++ b/dist/lib/ast/transform/compute.js @@ -1,4 +1,4 @@ -import { identity } from './utils.js'; +import { multiply, identity } from './utils.js'; import { EnumToken } from '../types.js'; import { length2Px } from './convert.js'; import { transformFunctions } from '../../syntax/syntax.js'; @@ -6,7 +6,7 @@ import '../minify.js'; import '../walk.js'; import '../../parser/parse.js'; import '../../parser/utils/config.js'; -import { getAngle, getNumber } from '../../renderer/color/color.js'; +import { getNumber, getAngle } from '../../renderer/color/color.js'; import '../../renderer/color/utils/constants.js'; import '../../renderer/sourcemap/lib/encode.js'; import { stripCommaToken } from '../../validation/utils/list.js'; @@ -15,7 +15,7 @@ import { rotate, rotate3D } from './rotate.js'; import { scale3d, scale, scaleX, scaleY, scaleZ } from './scale.js'; import { minify } from './minify.js'; import { skew, skewX, skewY } from './skew.js'; -import { serialize } from './matrix.js'; +import { serialize, matrix } from './matrix.js'; import { perspective } from './perspective.js'; function compute(transformLists) { @@ -25,75 +25,24 @@ function compute(transformLists) { return null; } let matrix = identity(); - const names = new Set; + let mat; const cumulative = []; for (const transformList of splitTransformList(transformLists)) { - matrix = computeMatrix(transformList, matrix); - switch (transformList[0].val) { - case 'translate': - case 'translateX': - case 'translateY': - case 'translateZ': - case 'translate3d': - if (names.has('translate')) { - names.delete('translate'); - } - names.add('translate'); - break; - case 'scale': - case 'scaleX': - case 'scaleY': - case 'scaleZ': - case 'scale3d': - if (names.has('scale')) { - names.delete('scale'); - } - names.add('scale'); - break; - case 'rotate': - case 'rotateX': - case 'rotateY': - case 'rotateZ': - case 'rotate3d': - if (names.has('rotate')) { - names.delete('rotate'); - } - names.add('rotate'); - break; - case 'skew': - case 'skewX': - case 'skewY': - if (names.has('skew')) { - names.delete('skew'); - } - names.add('skew'); - break; - case 'perspective': - if (names.has('perspective')) { - names.delete('perspective'); - } - names.add('perspective'); - break; - case 'matrix': - case 'matrix3d': - if (names.has('matrix')) { - names.delete('matrix'); - } - names.add('matrix'); - break; - } - if (matrix == null) { + mat = computeMatrix(transformList, identity()); + if (mat == null) { return null; } - cumulative.push(...(minify(computeMatrix(transformList, identity())) ?? transformList)); + matrix = multiply(matrix, mat); + cumulative.push(...(minify(mat) ?? transformList)); } + const serialized = serialize(matrix); return { - // result: minify(matrix, [...names]), matrix: serialize(matrix), - cumulative + cumulative, + minified: minify(matrix) ?? [serialized] }; } -function computeMatrix(transformList, matrix) { +function computeMatrix(transformList, matrixVar) { let values = []; let val; let i = 0; @@ -136,20 +85,20 @@ function computeMatrix(transformList, matrix) { return null; } if (transformList[i].val == 'translateX') { - matrix = translateX(values[0], matrix); + matrixVar = translateX(values[0], matrixVar); } else if (transformList[i].val == 'translateY') { - matrix = translateY(values[0], matrix); + matrixVar = translateY(values[0], matrixVar); } else if (transformList[i].val == 'translateZ') { - matrix = translateZ(values[0], matrix); + matrixVar = translateZ(values[0], matrixVar); } else if (transformList[i].val == 'translate') { - matrix = translate(values, matrix); + matrixVar = translate(values, matrixVar); } else { // @ts-ignore - matrix = translate3d(values, matrix); + matrixVar = translate3d(values, matrixVar); } } break; @@ -193,10 +142,10 @@ function computeMatrix(transformList, matrix) { return null; } if (transformList[i].val == 'rotate' || transformList[i].val == 'rotateZ') { - matrix = rotate(angle * 2 * Math.PI, matrix); + matrixVar = rotate(angle * 2 * Math.PI, matrixVar); } else { - matrix = rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + matrixVar = rotate3D(angle * 2 * Math.PI, x, y, z, matrixVar); } } break; @@ -225,27 +174,27 @@ function computeMatrix(transformList, matrix) { if (values.length != 3) { return null; } - matrix = scale3d(...values, matrix); + matrixVar = scale3d(...values, matrixVar); break; } if (transformList[i].val == 'scale') { if (values.length != 1 && values.length != 2) { return null; } - matrix = scale(values[0], values[1] ?? values[0], matrix); + matrixVar = scale(values[0], values[1] ?? values[0], matrixVar); break; } if (values.length != 1) { return null; } else if (transformList[i].val == 'scaleX') { - matrix = scaleX(values[0], matrix); + matrixVar = scaleX(values[0], matrixVar); } else if (transformList[i].val == 'scaleY') { - matrix = scaleY(values[0], matrix); + matrixVar = scaleY(values[0], matrixVar); } else if (transformList[i].val == 'scaleZ') { - matrix = scaleZ(values[0], matrix); + matrixVar = scaleZ(values[0], matrixVar); } } break; @@ -271,10 +220,10 @@ function computeMatrix(transformList, matrix) { return null; } if (transformList[i].val == 'skew') { - matrix = skew(values, matrix); + matrixVar = skew(values, matrixVar); } else { - matrix = transformList[i].val == 'skewX' ? skewX(values[0], matrix) : skewY(values[0], matrix); + matrixVar = transformList[i].val == 'skewX' ? skewX(values[0], matrixVar) : skewY(values[0], matrixVar); } } break; @@ -301,15 +250,41 @@ function computeMatrix(transformList, matrix) { if (values.length != 1) { return null; } - matrix = perspective(values[0], matrix); + matrixVar = perspective(values[0], matrixVar); + } + break; + case 'matrix3d': + return null; + case 'matrix': + { + const values = []; + let value; + for (const token of transformList[i].chi) { + if ([EnumToken.WhitespaceTokenType, EnumToken.CommentTokenType, EnumToken.CommaTokenType].includes(token.typ)) { + continue; + } + value = getNumber(token); + if (value == null) { + return null; + } + values.push(value); + } + if (transformList[i].val == 'matrix') { + if (values.length != 6) { + return null; + } + } + else if (values.length != 16) { + return null; + } + matrixVar = multiply(matrixVar, matrix(values)); } break; default: return null; - // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); } } - return matrix; + return matrixVar; } function splitTransformList(transformList) { let pattern = null; diff --git a/dist/lib/ast/transform/matrix.js b/dist/lib/ast/transform/matrix.js index 03fe9748..08e53914 100644 --- a/dist/lib/ast/transform/matrix.js +++ b/dist/lib/ast/transform/matrix.js @@ -1,21 +1,56 @@ -import { is2DMatrix } from './utils.js'; +import { is2DMatrix, toZero, identity } from './utils.js'; import { EnumToken } from '../types.js'; import { reduceNumber } from '../../renderer/render.js'; +// use column-major order +function matrix(values) { + const matrix = identity(); + if (values.length === 6) { + // matrix(scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY()) + matrix[0][0] = values[0]; + matrix[0][1] = values[1]; + matrix[1][0] = values[2]; + matrix[1][1] = values[3]; + matrix[3][0] = values[4]; + matrix[3][1] = values[5]; + } + else if (values.length === 16) { + matrix[0][0] = values[0]; + matrix[0][1] = values[1]; + matrix[0][2] = values[2]; + matrix[0][3] = values[3]; + matrix[1][0] = values[4]; + matrix[1][1] = values[5]; + matrix[1][2] = values[6]; + matrix[1][3] = values[7]; + matrix[2][0] = values[8]; + matrix[2][1] = values[9]; + matrix[2][2] = values[10]; + matrix[2][3] = values[11]; + matrix[3][0] = values[12]; + matrix[3][1] = values[13]; + matrix[3][2] = values[14]; + matrix[3][3] = values[15]; + } + else { + throw new RangeError('expecting 6 or 16 values'); + } + return matrix; +} function serialize(matrix) { if (is2DMatrix(matrix)) { // https://drafts.csswg.org/css-transforms-2/#two-dimensional-subset return { typ: EnumToken.FunctionTokenType, val: 'matrix', - chi: [ + chi: toZero([ matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1], matrix[3][0], matrix[3][1] - ].reduce((acc, t) => { + ]).reduce((acc, t) => { if (acc.length > 0) { acc.push({ typ: EnumToken.CommaTokenType }); } @@ -28,7 +63,7 @@ function serialize(matrix) { }; } let m = []; - // console.error(JSON.stringify({matrix},null, 1)); + matrix = matrix.map((v) => toZero(v)); for (let i = 0; i < matrix.length; i++) { for (let j = 0; j < matrix[i].length; j++) { if (m.length > 0) { @@ -47,4 +82,4 @@ function serialize(matrix) { }; } -export { serialize }; +export { matrix, serialize }; diff --git a/dist/lib/ast/transform/minify.js b/dist/lib/ast/transform/minify.js index 7a81dd2b..9f5621fd 100644 --- a/dist/lib/ast/transform/minify.js +++ b/dist/lib/ast/transform/minify.js @@ -3,7 +3,7 @@ import { EnumToken } from '../types.js'; import { eq } from '../../parser/utils/eq.js'; function minify(matrix, names) { - const decomposed = decompose(matrix); + const decomposed = /* is2DMatrix(matrix) ? decompose2(matrix) : */ decompose(matrix); if (decomposed == null) { return null; } @@ -68,7 +68,7 @@ function minify(matrix, names) { else if (coordinates.has('z')) { result.push({ typ: EnumToken.FunctionTokenType, - val: 'translate', + val: 'translate3d', chi: [ decomposed.translate[0] == 0 ? { typ: EnumToken.NumberTokenType, diff --git a/dist/lib/ast/transform/scale.js b/dist/lib/ast/transform/scale.js index 9f302f4a..35a6fc1b 100644 --- a/dist/lib/ast/transform/scale.js +++ b/dist/lib/ast/transform/scale.js @@ -3,21 +3,15 @@ import { identity, multiply } from './utils.js'; function scaleX(x, from) { const matrix = identity(); matrix[0][0] = x; - // matrix[1][1] = 1; - // matrix[2][2] = 1; return multiply(from, matrix); } function scaleY(y, from) { const matrix = identity(); - // matrix[0][0] = 1; matrix[1][1] = y; - // matrix[2][2] = 1; return multiply(from, matrix); } function scaleZ(z, from) { const matrix = identity(); - // matrix[0][0] = 1; - // matrix[1][1] = 1; matrix[2][2] = z; return multiply(from, matrix); } diff --git a/dist/lib/ast/transform/utils.js b/dist/lib/ast/transform/utils.js index 6d14d5df..d24d54ee 100644 --- a/dist/lib/ast/transform/utils.js +++ b/dist/lib/ast/transform/utils.js @@ -247,7 +247,7 @@ function decompose(original) { } function toZero(v) { for (let i = 0; i < v.length; i++) { - if (Math.abs(v[i]) <= 1e-6) { + if (Math.abs(v[i]) <= 1e-5) { v[i] = 0; } else { diff --git a/dist/lib/parser/parse.js b/dist/lib/parser/parse.js index c5e8babf..10fb612b 100644 --- a/dist/lib/parser/parse.js +++ b/dist/lib/parser/parse.js @@ -56,6 +56,7 @@ async function doParse(iterator, options = {}) { removeCharset: true, removeEmpty: true, removeDuplicateDeclarations: true, + computeTransform: false, computeShorthand: true, computeCalcExpression: true, inlineCssVariables: false, @@ -719,24 +720,6 @@ async function parseNode(results, context, stats, options, errors, src, map, raw }; const result = parseDeclarationNode(node, errors, src, position); if (result != null) { - // if (options.validation) { - // - // const valid: ValidationResult = validateDeclaration(result, options, context); - // - // // console.error({valid}); - // - // if (valid.valid == ValidationLevel.Drop) { - // - // errors.push({ - // action: 'drop', - // message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, {minify: false}), '') + '"', - // // @ts-ignore - // location: {src, ...(map.get(valid.node) ?? position)} - // }); - // - // return null; - // } - // } // @ts-ignore context.chi.push(result); Object.defineProperty(result, 'parent', { ...definedPropertySettings, value: context }); diff --git a/dist/lib/syntax/syntax.js b/dist/lib/syntax/syntax.js index 1edc21bf..a5116c01 100644 --- a/dist/lib/syntax/syntax.js +++ b/dist/lib/syntax/syntax.js @@ -20,12 +20,12 @@ const fontFormat = ['collection', 'embedded-opentype', 'opentype', 'svg', 'truet const colorFontTech = ['color-colrv0', 'color-colrv1', 'color-svg', 'color-sbix', 'color-cbdt']; const fontFeaturesTech = ['features-opentype', 'features-aat', 'features-graphite', 'incremental-patch', 'incremental-range', 'incremental-auto', 'variations', 'palettes']; const transformFunctions = [ - 'matrix', 'translate', 'scale', 'rotate', 'skew', 'perspective', + 'translate', 'scale', 'rotate', 'skew', 'perspective', 'translateX', 'translateY', 'translateZ', 'scaleX', 'scaleY', 'scaleZ', 'rotateX', 'rotateY', 'rotateZ', 'skewX', 'skewY', - 'rotate3d', 'translate3d', 'scale3d', 'matrix3d' + 'rotate3d', 'translate3d', 'scale3d', 'matrix', 'matrix3d' ]; // https://drafts.csswg.org/mediaqueries/#media-types const mediaTypes = ['all', 'print', 'screen', diff --git a/jsr.json b/jsr.json index 74af0a66..d65684f6 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@tbela99/css-parser", - "version": "0.9.2-alpha4", + "version": "1.0.0-alpha5", "publish": { "include": [ "src", diff --git a/package.json b/package.json index 90fa73cd..319f2aeb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tbela99/css-parser", "description": "CSS parser for node and the browser", - "version": "v0.9.2-alpha4", + "version": "v1.0.0-alpha5", "exports": { ".": "./dist/node/index.js", "./node": "./dist/node/index.js", diff --git a/src/@types/index.d.ts b/src/@types/index.d.ts index 393405b1..fd001d61 100644 --- a/src/@types/index.d.ts +++ b/src/@types/index.d.ts @@ -50,6 +50,7 @@ export interface MinifyOptions { expandNestingRules?: boolean; removeDuplicateDeclarations?: boolean; computeShorthand?: boolean; + computeTransform?: boolean; computeCalcExpression?: boolean; inlineCssVariables?: boolean; removeEmpty?: boolean; diff --git a/src/lib/ast/features/transform.ts b/src/lib/ast/features/transform.ts index 2c18f825..813f9621 100644 --- a/src/lib/ast/features/transform.ts +++ b/src/lib/ast/features/transform.ts @@ -20,7 +20,7 @@ export class TransformCssFeature { static register(options: MinifyFeatureOptions): void { // @ts-ignore - if (options.minify || options.computeCalcExpression || options.computeShorthand) { + if (options.computeTransform) { // @ts-ignore options.features.push(new TransformCssFeature()); @@ -54,27 +54,26 @@ export class TransformCssFeature { consumeWhitespace(children); - let {matrix, cumulative} = compute(children as Token[]) ?? {}; + let {matrix, cumulative, minified} = compute(children as Token[]) ?? {}; - // console.error({result, matrix}); - // console.error( - // { - // // result: result == null ? null :result.reduce((acc, curr) => acc + renderToken(curr), ''), - // matrix: matrix == null ? null : renderToken(matrix), - // cumulative: cumulative == null ? null : cumulative.reduce((acc, curr) => acc + renderToken(curr), '') - // }); - - if ( matrix == null) { + if (matrix == null || cumulative == null || minified == null) { return; } - if (renderToken(matrix).length < cumulative.reduce((acc, t) => acc + renderToken(t), '').length) { + let result: Token[] = cumulative; + + if (renderToken(matrix).length < result.reduce((acc, t) => acc + renderToken(t), '').length) { + + result = [matrix]; + } + + if (matrix != minified[0] && minified.reduce((acc, t) => acc + renderToken(t), '').length < result.reduce((acc, t) => acc + renderToken(t), '').length) { - cumulative = [matrix]; + result = minified; } - (node as AstDeclaration).val = cumulative; + (node as AstDeclaration).val = result; } } } diff --git a/src/lib/ast/transform/compute.ts b/src/lib/ast/transform/compute.ts index b5f6f8e1..45e215dd 100644 --- a/src/lib/ast/transform/compute.ts +++ b/src/lib/ast/transform/compute.ts @@ -1,5 +1,5 @@ import type {AngleToken, FunctionToken, IdentToken, LengthToken, NumberToken, Token} from "../../../@types/token.d.ts"; -import {identity, Matrix} from "./utils.ts"; +import {identity, Matrix, multiply} from "./utils.ts"; import {EnumToken} from "../types.ts"; import {length2Px} from "./convert.ts"; import {transformFunctions} from "../../syntax/index.ts"; @@ -10,13 +10,13 @@ import {rotate, rotate3D} from "./rotate.ts"; import {scale, scale3d, scaleX, scaleY, scaleZ} from "./scale.ts"; import {minify} from "./minify.ts"; import {skew, skewX, skewY} from "./skew.ts"; -import {serialize} from "./matrix.ts"; +import {matrix, serialize} from "./matrix.ts"; import {perspective} from "./perspective.ts"; export function compute(transformLists: Token[]): { - // result: Token[] | null; matrix: Token, - cumulative: Token[] + cumulative: Token[], + minified: Token[] } | null { transformLists = transformLists.slice(); @@ -28,108 +28,32 @@ export function compute(transformLists: Token[]): { } let matrix: Matrix | null = identity(); - const names: Set = new Set; + let mat: Matrix; const cumulative: Token[] = []; for (const transformList of splitTransformList(transformLists)) { - matrix = computeMatrix(transformList, matrix); + mat = computeMatrix(transformList, identity()) as Matrix; - switch ((transformList[0] as FunctionToken).val) { - - case 'translate': - case 'translateX': - case 'translateY': - case 'translateZ': - case 'translate3d': - - if(names.has('translate')) { - - names.delete('translate'); - } - - names.add('translate'); - break; - - case 'scale': - case 'scaleX': - case 'scaleY': - case 'scaleZ': - case 'scale3d': - - if(names.has('scale')) { - - names.delete('scale'); - } - - names.add('scale'); - - break; - - case 'rotate': - case 'rotateX': - case 'rotateY': - case 'rotateZ': - case 'rotate3d': - - if(names.has('rotate')) { - - names.delete('rotate'); - } - - names.add('rotate'); - break; - - case 'skew': - case 'skewX': - case 'skewY': - - if(names.has('skew')) { - - names.delete('skew'); - } - - names.add('skew'); - break; - - case 'perspective': - - if(names.has('perspective')) { - - names.delete('perspective'); - } - - names.add('perspective'); - break; - - case 'matrix': - case 'matrix3d': - - if(names.has('matrix')) { - - names.delete('matrix'); - } - - names.add('matrix'); - break; - } - - if (matrix == null) { + if (mat == null) { return null; } - cumulative.push(...(minify(computeMatrix(transformList, identity()) as Matrix) as Token[] ?? transformList)); + matrix = multiply(matrix, mat) as Matrix; + cumulative.push(...(minify(mat) as Token[] ?? transformList)); } + const serialized: Token = serialize(matrix); + return { - // result: minify(matrix, [...names]), matrix: serialize(matrix), - cumulative + cumulative, + minified: minify(matrix) ?? [serialized] } } -export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | null { +export function computeMatrix(transformList: Token[], matrixVar: Matrix): Matrix | null { let values: number[] = []; let val: number | null; @@ -196,22 +120,22 @@ export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | if ((transformList[i] as FunctionToken).val == 'translateX') { - matrix = translateX(values[0], matrix); + matrixVar = translateX(values[0], matrixVar); } else if ((transformList[i] as FunctionToken).val == 'translateY') { - matrix = translateY(values[0], matrix); + matrixVar = translateY(values[0], matrixVar); } else if ((transformList[i] as FunctionToken).val == 'translateZ') { - matrix = translateZ(values[0], matrix); + matrixVar = translateZ(values[0], matrixVar); } else if ((transformList[i] as FunctionToken).val == 'translate') { - matrix = translate(values as [number] | [number, number], matrix); + matrixVar = translate(values as [number] | [number, number], matrixVar); } else { // @ts-ignore - matrix = translate3d(values as [number] | [number, number], matrix); + matrixVar = translate3d(values as [number] | [number, number], matrixVar); } } break; @@ -271,10 +195,10 @@ export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | if ((transformList[i] as FunctionToken).val == 'rotate' || (transformList[i] as FunctionToken).val == 'rotateZ') { - matrix = rotate(angle * 2 * Math.PI, matrix); + matrixVar = rotate(angle * 2 * Math.PI, matrixVar); } else { - matrix = rotate3D(angle * 2 * Math.PI, x, y, z, matrix); + matrixVar = rotate3D(angle * 2 * Math.PI, x, y, z, matrixVar); } } @@ -319,7 +243,7 @@ export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | return null; } - matrix = scale3d(...values as [number, number, number], matrix); + matrixVar = scale3d(...values as [number, number, number], matrixVar); break; } @@ -330,7 +254,7 @@ export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | return null; } - matrix = scale(values[0], values[1] ?? values[0], matrix); + matrixVar = scale(values[0], values[1] ?? values[0], matrixVar); break; } @@ -339,13 +263,13 @@ export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | return null; } else if ((transformList[i] as FunctionToken).val == 'scaleX') { - matrix = scaleX(values[0], matrix); + matrixVar = scaleX(values[0], matrixVar); } else if ((transformList[i] as FunctionToken).val == 'scaleY') { - matrix = scaleY(values[0], matrix); + matrixVar = scaleY(values[0], matrixVar); } else if ((transformList[i] as FunctionToken).val == 'scaleZ') { - matrix = scaleZ(values[0], matrix); + matrixVar = scaleZ(values[0], matrixVar); } } @@ -386,10 +310,10 @@ export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | if ((transformList[i] as FunctionToken).val == 'skew') { - matrix = skew(values as [number] | [number, number], matrix); + matrixVar = skew(values as [number] | [number, number], matrixVar); } else { - matrix = (transformList[i] as FunctionToken).val == 'skewX' ? skewX(values[0], matrix) : skewY(values[0], matrix); + matrixVar = (transformList[i] as FunctionToken).val == 'skewX' ? skewX(values[0], matrixVar) : skewY(values[0], matrixVar); } } @@ -432,7 +356,48 @@ export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | return null; } - matrix = perspective(values[0], matrix); + matrixVar = perspective(values[0], matrixVar); + } + + break; + + case 'matrix3d': + return null; + + case 'matrix': { + + const values: number[] = []; + let value: number | null; + + for (const token of (transformList[i] as FunctionToken).chi) { + + if ([EnumToken.WhitespaceTokenType, EnumToken.CommentTokenType, EnumToken.CommaTokenType].includes(token.typ)) { + + continue; + } + + value = getNumber(token as NumberToken); + + if (value == null) { + + return null; + } + + values.push(value); + } + + if ((transformList[i] as FunctionToken).val == 'matrix') { + + if (values.length != 6) { + + return null; + } + } else if (values.length != 16) { + + return null; + } + + matrixVar = multiply(matrixVar, matrix(values as [number, number, number, number, number, number] | [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]) as Matrix); } break; @@ -440,11 +405,10 @@ export function computeMatrix(transformList: Token[], matrix: Matrix): Matrix | default: return null; - // throw new TypeError(`Unknown transform function: ${(transformList[i] as FunctionToken).val}`); } } - return matrix + return matrixVar } function splitTransformList(transformList: Token[]): Token[][] { diff --git a/src/lib/ast/transform/matrix.ts b/src/lib/ast/transform/matrix.ts index f8f1f9b1..fc57f9d7 100644 --- a/src/lib/ast/transform/matrix.ts +++ b/src/lib/ast/transform/matrix.ts @@ -1,4 +1,4 @@ -import {identity, is2DMatrix, Matrix} from "./utils.ts"; +import {identity, is2DMatrix, Matrix, toZero} from "./utils.ts"; import {EnumToken} from "../types.ts"; import type {Token} from "../../../@types/index.d.ts"; import {reduceNumber} from "../../renderer/render.ts"; @@ -10,6 +10,7 @@ export function matrix(values: [number, number, number, number, number, number] if (values.length === 6) { + // matrix(scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY()) matrix[0][0] = values[0]; matrix[0][1] = values[1]; matrix[1][0] = values[2]; @@ -50,37 +51,37 @@ export function serialize(matrix: Matrix): Token { return { typ: EnumToken.FunctionTokenType, val: 'matrix', - chi: [ + chi: toZero([ matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1], matrix[3][0], matrix[3][1] - ].reduce((acc,t) => { + ]).reduce((acc, t) => { - if (acc.length > 0) { + if (acc.length > 0) { - acc.push({ typ: EnumToken.CommaTokenType }); - } + acc.push({typ: EnumToken.CommaTokenType}); + } - acc.push({ - typ: EnumToken.NumberTokenType, - val: reduceNumber(t.toPrecision(6)) - }) + acc.push({ + typ: EnumToken.NumberTokenType, + val: reduceNumber(t.toPrecision(6)) + }) - return acc + return acc }, [] as Token[]) } } let m: Token[] = []; - // console.error(JSON.stringify({matrix},null, 1)); + matrix = matrix.map((v) => toZero(v)) as Matrix; for (let i = 0; i < matrix.length; i++) { - for (let j = 0; j < matrix[i].length; j ++) { + for (let j = 0; j < matrix[i].length; j++) { if (m.length > 0) { diff --git a/src/lib/ast/transform/minify.ts b/src/lib/ast/transform/minify.ts index e161ac20..07b038b4 100644 --- a/src/lib/ast/transform/minify.ts +++ b/src/lib/ast/transform/minify.ts @@ -6,7 +6,7 @@ import {eq} from "../../parser/utils/eq.ts"; export function minify(matrix: Matrix, names?: string[]): Token[] | null { - const decomposed = decompose(matrix); + const decomposed = /* is2DMatrix(matrix) ? decompose2(matrix) : */ decompose(matrix); if (decomposed == null) { @@ -94,7 +94,7 @@ export function minify(matrix: Matrix, names?: string[]): Token[] | null { result.push({ typ: EnumToken.FunctionTokenType, - val: 'translate', + val: 'translate3d', chi: [ decomposed.translate[0] == 0 ? { typ: EnumToken.NumberTokenType, diff --git a/src/lib/ast/transform/scale.ts b/src/lib/ast/transform/scale.ts index 537c6dd8..7f20607a 100644 --- a/src/lib/ast/transform/scale.ts +++ b/src/lib/ast/transform/scale.ts @@ -4,8 +4,6 @@ export function scaleX(x: number, from: Matrix): Matrix { const matrix = identity(); matrix[0][0] = x; - // matrix[1][1] = 1; - // matrix[2][2] = 1; return multiply(from, matrix); } @@ -13,9 +11,8 @@ export function scaleX(x: number, from: Matrix): Matrix { export function scaleY(y: number, from: Matrix): Matrix { const matrix = identity(); - // matrix[0][0] = 1; + matrix[1][1] = y; - // matrix[2][2] = 1; return multiply(from, matrix); } @@ -23,8 +20,7 @@ export function scaleY(y: number, from: Matrix): Matrix { export function scaleZ(z: number, from: Matrix): Matrix { const matrix = identity(); - // matrix[0][0] = 1; - // matrix[1][1] = 1; + matrix[2][2] = z; return multiply(from, matrix) as Matrix; diff --git a/src/lib/ast/transform/utils.ts b/src/lib/ast/transform/utils.ts index ad1a1040..cb406cc7 100644 --- a/src/lib/ast/transform/utils.ts +++ b/src/lib/ast/transform/utils.ts @@ -138,31 +138,6 @@ function normalize(point: Point): Point { return norm === 0 ? [0, 0, 0] : [x / norm, y / norm, z / norm]; } -function interpolate(quaternionA: [number, number, number, number], quaternionB: [number, number, number, number], t: number = 1) { - - let product = dot(quaternionA, quaternionB); - - const quaternionDst: [number, number, number, number] = [0, 0, 0, 0]; -// Clamp product to -1.0 <= product <= 1.0 - product = Math.min(product, 1.0); - product = Math.max(product, -1.0); - - if (Math.abs(product) === 1.0) { - return quaternionA; - } - - const theta = Math.acos(product); - const w = Math.sin(t * theta) / Math.sqrt(1 - product * product); - - for (let i = 0; i < 4; i++) { - quaternionA[i] *= Math.cos(t * theta) - product * w; - quaternionB[i] *= w; - quaternionDst[i] = quaternionA[i] + quaternionB[i]; - } - - return; -} - function dot(point1: Point, point2: Point): number; function dot(point1: [number, number, number, number], point2: [number, number, number, number]): number; @@ -203,7 +178,7 @@ export function multiply(matrixA: Matrix, matrixB: Matrix): Matrix { return result; } -export function decompose2(matrix: Matrix) { +export function decompose2(matrix: Matrix): DecomposedMatrix3D { let row0x = matrix[0][0] let row0y = matrix[1][0] @@ -274,13 +249,70 @@ export function decompose2(matrix: Matrix) { let m21 = row1x let m22 = row1y + let quaternion: [number, number, number, number] = [0, 0, 0, 0]; + +// Now, get the rotations out + quaternion[0] = 0.5 * Math.sqrt(Math.max(1 + m11 - m22 - 1, 0)); + quaternion[1] = 0.5 * Math.sqrt(Math.max(1 - m11 + m22 - 1, 0)); + quaternion[2] = 0.5 * Math.sqrt(Math.max(1 - m11 - m22 + 1, 0)); + quaternion[3] = 0.5 * Math.sqrt(Math.max(1 + m11 + m22 + 1, 0)); + + if (matrix[2][1] > matrix[1][2]) { + + quaternion[0] = -quaternion[0]; + } + + if (matrix[0][2] > matrix[2][0]) { + + quaternion[1] = -quaternion[1]; + } + + if (matrix[1][0] > matrix[0][1]) { + + quaternion[2] = -quaternion[2]; + } + + + let skew: [number, number, number] = [0, 0, 0]; + + let row: [[number, number, number], [number, number, number], [number, number, number]] = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; + +// Now get scale and shear. 'row' is a 3 element array of 3 component vectors + for (let i = 0; i < 3; i++) { + row[i][0] = matrix[i][0]; + row[i][1] = matrix[i][1]; + row[i][2] = matrix[i][2]; + } + + row[0] = normalize(row[0]); + +// Compute XY shear factor and make 2nd row orthogonal to 1st. + skew[0] = dot(row[0], row[1]); + row[1] = combine(row[1], row[0], 1.0, -skew[0]); + + skew[0] /= scale[1]; + +// Compute XZ and YZ shears, orthogonalize 3rd row + skew[1] = dot(row[0], row[2]); + row[2] = combine(row[2], row[0], 1.0, -skew[1]); + skew[2] = dot(row[1], row[2]); + row[2] = combine(row[2], row[1], 1.0, -skew[2]); + +// Next, g + // Convert into degrees because our rotation functions expect it. // angle = rad2deg(angle) angle *= 180 / Math.PI; return { - translate, scale, angle: +angle.toPrecision(12), m11, m12, m21, m22 + skew, + translate: toZero(translate.concat(0) as [number, number, number]) as [number, number, number], + scale: toZero(scale.concat(1) as [number, number, number]) as [number, number, number], + rotate: toZero([1, 1, 0, +angle.toPrecision(6)] as [number, number, number, number]) as [number, number, number, number], + perspective: null, + quaternion + // m11, m12, m21, m22 } } @@ -454,17 +486,17 @@ export function decompose(original: Matrix): DecomposedMatrix3D | null { scale: toZero(scale) as [number, number, number], rotate: toZero([x1, y1, z1, angle]) as [number, number, number, number], translate: toZero(translate) as [number, number, number], - perspective: original[2][3] == 0 ? null : +(-1 / original[2][3]).toPrecision( 6), + perspective: original[2][3] == 0 ? null : +(-1 / original[2][3]).toPrecision(6), quaternion }; } -export function toZero(v: [number, number] | [number, number, number] | [number, number, number, number]) { +export function toZero(v: [number, number] | [number, number, number] | [number, number, number, number] | number[]): [number, number] | [number, number, number] | [number, number, number, number] | number[] { for (let i = 0; i < v.length; i++) { - if (Math.abs(v[i]) <= 1e-6) { + if (Math.abs(v[i]) <= 1e-5) { v[i] = 0; } else { @@ -507,7 +539,7 @@ function getRotation3D(matrix: Matrix): { x: number, y: number, z: number, angle const x1: number = +r11.toPrecision(6); const y1: number = +r22.toPrecision(6); - const z1: number = +r33.toPrecision(6); + const z1: number = +r33.toPrecision(6); const max: number = Math.max(x1, y1, z1); x = y = z = 0; @@ -562,79 +594,6 @@ function getRotation3D(matrix: Matrix): { x: number, y: number, z: number, angle return {x, y, z, angle}; } -export function recompose( - translate: [number, number, number], - scale: [number, number, number], - skew: [number, number, number], - perspective: [number, number, number, number], - quaternion: [number, number, number, number] -): Matrix { - - let matrix: Matrix = identity(); -// apply perspective - for (let i = 0; i < 4; i++) { - matrix[i][3] = perspective[i]; - } - -// apply translation - for (let i = 0; i < 4; i++) { - for (let j = 0; j < 3; j++) { - matrix[3][i] += translate[j] * matrix[j][i]; - } - } - -// apply rotation - let x = quaternion[0]; - let y = quaternion[1]; - let z = quaternion[2]; - let w = quaternion[3]; - - const rotationMatrix: Matrix = identity(); -// Construct a composite rotation matrix from the quaternion values -// rotationMatrix is an identity 4x4 matrix initially - rotationMatrix[0][0] = 1 - 2 * (y * y + z * z); - rotationMatrix[0][1] = 2 * (x * y - z * w); - rotationMatrix[0][2] = 2 * (x * z + y * w); - rotationMatrix[1][0] = 2 * (x * y + z * w); - rotationMatrix[1][1] = 1 - 2 * (x * x + z * z); - rotationMatrix[1][2] = 2 * (y * z - x * w); - rotationMatrix[2][0] = 2 * (x * z - y * w); - rotationMatrix[2][1] = 2 * (y * z + x * w); - rotationMatrix[2][2] = 1 - 2 * (x * x + y * y); - - matrix = multiply(matrix, rotationMatrix); - - let temp: Matrix = identity(); - -// apply skew -// temp is an identity 4x4 matrix initially - if (skew[2]) { - temp[2][1] = skew[2]; - matrix = multiply(matrix, temp); - } - - if (skew[1]) { - temp[2][1] = 0; - temp[2][0] = skew[1]; - matrix = multiply(matrix, temp); - } - - if (skew[0]) { - temp[2][0] = 0; - temp[1][0] = skew[0]; - matrix = multiply(matrix, temp); - } - -// apply scale - for (let i = 0; i < 3; i++) { - for (let j = 0; j < 4; j++) { - matrix[i][j] *= scale[i]; - } - } - - return matrix; -} - // https://drafts.csswg.org/css-transforms-1/#2d-matrix export function is2DMatrix(matrix: Matrix): boolean { diff --git a/src/lib/parser/parse.ts b/src/lib/parser/parse.ts index 70f55b4d..c58007bd 100644 --- a/src/lib/parser/parse.ts +++ b/src/lib/parser/parse.ts @@ -156,6 +156,7 @@ export async function doParse(iterator: string, options: ParserOptions = {}): Pr removeCharset: true, removeEmpty: true, removeDuplicateDeclarations: true, + computeTransform: false, computeShorthand: true, computeCalcExpression: true, inlineCssVariables: false, @@ -1082,25 +1083,6 @@ async function parseNode(results: TokenizeResult[], context: AstRuleList | AstIn if (result != null) { - // if (options.validation) { - // - // const valid: ValidationResult = validateDeclaration(result, options, context); - // - // // console.error({valid}); - // - // if (valid.valid == ValidationLevel.Drop) { - // - // errors.push({ - // action: 'drop', - // message: valid.error + ' - "' + tokens.reduce((acc, curr) => acc + renderToken(curr, {minify: false}), '') + '"', - // // @ts-ignore - // location: {src, ...(map.get(valid.node) ?? position)} - // }); - // - // return null; - // } - // } - // @ts-ignore context.chi.push(result); Object.defineProperty(result, 'parent', {...definedPropertySettings, value: context}); diff --git a/src/lib/syntax/syntax.ts b/src/lib/syntax/syntax.ts index 68359c7d..86b43338 100644 --- a/src/lib/syntax/syntax.ts +++ b/src/lib/syntax/syntax.ts @@ -29,12 +29,12 @@ export const colorFontTech: string[] = ['color-colrv0', 'color-colrv1', 'color-s export const fontFeaturesTech: string[] = ['features-opentype', 'features-aat', 'features-graphite', 'incremental-patch', 'incremental-range', 'incremental-auto', 'variations', 'palettes']; export const transformFunctions: string[] = [ - 'matrix', 'translate', 'scale', 'rotate', 'skew', 'perspective', + 'translate', 'scale', 'rotate', 'skew', 'perspective', 'translateX', 'translateY', 'translateZ', 'scaleX', 'scaleY', 'scaleZ', 'rotateX', 'rotateY', 'rotateZ', 'skewX', 'skewY', - 'rotate3d', 'translate3d', 'scale3d', 'matrix3d' + 'rotate3d', 'translate3d', 'scale3d', 'matrix', 'matrix3d' ]; // https://drafts.csswg.org/mediaqueries/#media-types diff --git a/test/specs/code/malformed.js b/test/specs/code/malformed.js index e7f9cecd..38d0175f 100644 --- a/test/specs/code/malformed.js +++ b/test/specs/code/malformed.js @@ -167,7 +167,8 @@ color: ; `; - return transform(css, {minify: transform, resolveImport: true}).then(result => expect(render(result.ast, { + return transform(css, {minify: transform, + computeTransform: true, resolveImport: true}).then(result => expect(render(result.ast, { minify: false, removeComments: false, preserveLicense: true diff --git a/test/specs/code/transform.js b/test/specs/code/transform.js index 06e0968a..d2388bac 100644 --- a/test/specs/code/transform.js +++ b/test/specs/code/transform.js @@ -9,7 +9,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: translateY(-100px) }`)); @@ -23,7 +24,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: translate(-100px) }`)); @@ -37,7 +39,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: translate(-100px) }`)); @@ -51,7 +54,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: translateZ(-100px) }`)); @@ -65,7 +69,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -79,7 +84,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -93,7 +99,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: translateZ(14px) }`)); @@ -107,9 +114,10 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { - transform: translate(14px,0,14px) + transform: translate3d(14px,0,14px) }`)); }); @@ -125,7 +133,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: rotateX(45deg) }`)); @@ -139,7 +148,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: rotate(45deg) }`)); @@ -154,7 +164,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: rotate3d(2,-1,-1,-72deg) }`)); @@ -168,7 +179,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: scale3d(.5,1,1.7)rotate3d(1,1,1,67deg) }`)); @@ -182,7 +194,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: rotateY(180deg) }`)); @@ -196,7 +209,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: rotate3d(1,1,1,180deg) }`)); @@ -214,7 +228,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: scale3d(.5,.5,.5) }`)); @@ -228,7 +243,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -243,7 +259,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -257,7 +274,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -271,7 +289,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -285,7 +304,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: none }`)); @@ -299,7 +319,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: scale(1.5,2) }`)); @@ -313,7 +334,8 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: scaleX(0)scaleY(0)scaleZ(0) }`)); @@ -331,13 +353,15 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: translate(100px,100px)rotate(135deg)skew(10deg) }`)); }); }); + describe('CSS perspective', function () { it('skew #22', function () { @@ -348,11 +372,31 @@ export function run(describe, expect, transform, parse, render, dirname, readFil } `; return transform(nesting1, { - beautify: true + beautify: true, + computeTransform: true }).then((result) => expect(result.code).equals(`.now { transform: perspective(50px)translateZ(100px) }`)); }); }); + + describe('CSS perspective', function () { + + it('matrix #23', function () { + const nesting1 = ` + + .now { + transform: matrix(1, 0, 0, 1, 0, 20) +} +`; + return transform(nesting1, { + beautify: true, + computeTransform: true + }).then((result) => expect(result.code).equals(`.now { + transform: translateY(20px) +}`)); + }); + + }); } \ No newline at end of file From 87550ac16f2e2cb46caf3aeca50c7960deb029ad Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Mon, 21 Apr 2025 00:41:27 -0400 Subject: [PATCH 10/10] parse matrix3d() #75 --- CHANGELOG.md | 4 +- README.md | 3 +- dist/index-umd-web.js | 821 +++++++++++++---------------- dist/index.cjs | 821 +++++++++++++---------------- dist/lib/ast/features/transform.js | 28 +- dist/lib/ast/math/math.js | 10 +- dist/lib/ast/transform/compute.js | 19 +- dist/lib/ast/transform/matrix.js | 56 +- dist/lib/ast/transform/minify.js | 188 ++++--- dist/lib/ast/transform/utils.js | 410 +++++--------- dist/lib/renderer/render.js | 2 +- jsr.json | 2 +- package.json | 2 +- src/lib/ast/features/transform.ts | 35 +- src/lib/ast/math/math.ts | 15 +- src/lib/ast/transform/compute.ts | 26 +- src/lib/ast/transform/matrix.ts | 67 ++- src/lib/ast/transform/minify.ts | 243 +++++---- src/lib/ast/transform/utils.ts | 630 +++++++--------------- src/lib/renderer/render.ts | 2 +- test/specs/code/transform.js | 106 +++- 21 files changed, 1634 insertions(+), 1856 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea3f150d..cb92ed5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,14 @@ # v1.0.0 -- [x] experimental CSS transform module level 2 +- [x] experimental minification : CSS transform module level 2 - [x] translate - [x] scale - [x] rotate - [x] skew - [x] perspective - [x] matrix - - [ ] matrix3d + - [x] matrix3d - [x] keyframes - [x] remove consecutive keyframes with the same name - [x] reduce keyframe selector 'from' to '0%' diff --git a/README.md b/README.md index 8a7b1bfc..b93d2dae 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ $ deno add @tbela99/css-parser - convert nested css rules to legacy syntax - generate sourcemap - compute css shorthands. see supported properties list below +- experimental minification : css transform module level 2 - evaluate math functions: calc(), clamp(), min(), max(), round(), mod(), rem(), sin(), cos(), tan(), asin(), acos(), atan(), atan2(), pow(), sqrt(), hypot(), log(), exp(), abs(), sign() - inline css variables @@ -96,7 +97,7 @@ Javascript module from cdn