UNPKG

postcss-transform-3d-accelerate

Version:

PostCSS plugin to convert 2D transforms to 3D transforms for GPU acceleration

565 lines (469 loc) 19.4 kB
/** * CSS Transform 3D 加速插件(优化版) * 将 CSS 中的 2D 变换转换为 3D 变换,以利用 GPU 硬件加速 */ 'use strict'; // 缓存已处理的值,避免重复计算 const transformCache = new Map(); // 正则表达式,用于匹配各种变换函数 // 优化:使用更精确的正则表达式,并预编译 const TRANSFORM_REGEX = { // 匹配 translate(x, y) 或 translate(x),支持calc()和var() translate: /translate\(\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)(?:\s*,\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+))?\s*\)/g, // 匹配 translateX(x) translateX: /translateX\(\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*\)/g, // 匹配 translateY(y) translateY: /translateY\(\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*\)/g, // 匹配 scale(x, y) 或 scale(x) // 修复:改进正则表达式以更好地处理嵌套的calc()和var()函数 scale: /scale\(\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\)|calc\([^)]*\))+)(?:\s*,\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\)|calc\([^)]*\))+))?\s*\)/g, // 匹配 scaleX(x) scaleX: /scaleX\(\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*\)/g, // 匹配 scaleY(y) scaleY: /scaleY\(\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*\)/g, // 匹配 rotate(angle) rotate: /rotate\(\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*\)/g, // 匹配 matrix(a, b, c, d, tx, ty) matrix: /matrix\(\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*,\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*,\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*,\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*,\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*,\s*((?:[^(),]+|\([^)]*\)|var\([^)]*\))+)\s*\)/g, // 匹配浏览器前缀 prefixes: /^(-webkit-|-moz-|-ms-|-o-)(.+)$/, // 匹配动画名称 animation: /^(?:.*?animation(?:-name)?:)(?:[^;]*?)([a-zA-Z0-9_-]+)(?:[^;]*?)(?:;|$)/i }; /** * 将 2D 变换函数转换为 3D 变换函数 * @param {string} value - CSS 变换值 * @returns {string} - 转换后的 CSS 变换值 */ function transform2dTo3d(value) { if (!value) return value; // 检查缓存 if (transformCache.has(value)) { return transformCache.get(value); } try { let result = value; // 转换 translate(x, y) 为 translate3d(x, y, 0) result = result.replace(TRANSFORM_REGEX.translate, (match, x, y = '0') => { return `translate3d(${x}, ${y}, 0)`; }); // 转换 translateX(x) 为 translate3d(x, 0, 0) result = result.replace(TRANSFORM_REGEX.translateX, (match, x) => { return `translate3d(${x}, 0, 0)`; }); // 转换 translateY(y) 为 translate3d(0, y, 0) result = result.replace(TRANSFORM_REGEX.translateY, (match, y) => { return `translate3d(0, ${y}, 0)`; }); // 转换 scale(x, y) 为 scale3d(x, y, 1) result = result.replace(TRANSFORM_REGEX.scale, (match, x, y = x) => { // 修复:确保参数正确分隔,避免嵌套calc()导致的括号错误 // 检查x和y中是否包含calc()函数,如果包含,确保括号平衡 const balancedX = balanceParentheses(x); const balancedY = balanceParentheses(y); // 特殊处理:如果x或y中包含calc()函数,确保它们是完整的表达式 // 这是为了修复类似 scale(calc(var(--scale) * 1.2)) 这样的情况 return `scale3d(${balancedX}, ${balancedY}, 1)`; }); // 转换 scaleX(x) 为 scale3d(x, 1, 1) result = result.replace(TRANSFORM_REGEX.scaleX, (match, x) => { return `scale3d(${x}, 1, 1)`; }); // 转换 scaleY(y) 为 scale3d(1, y, 1) result = result.replace(TRANSFORM_REGEX.scaleY, (match, y) => { return `scale3d(1, ${y}, 1)`; }); // 转换 rotate(angle) 为 rotate3d(0, 0, 1, angle) result = result.replace(TRANSFORM_REGEX.rotate, (match, angle) => { return `rotate3d(0, 0, 1, ${angle})`; }); // 转换 matrix(a, b, c, d, tx, ty) 为 matrix3d(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1) result = result.replace(TRANSFORM_REGEX.matrix, (match, a, b, c, d, tx, ty) => { return `matrix3d(${a}, ${b}, 0, 0, ${c}, ${d}, 0, 0, 0, 0, 1, 0, ${tx}, ${ty}, 0, 1)`; }); // 专门修复scale3d函数中的括号问题 result = fixScale3dParentheses(result); // 最终检查:确保整个结果中的括号是平衡的 // 这是为了修复可能的括号不匹配问题 result = fixParenthesesBalance(result); // 存入缓存 transformCache.set(value, result); return result; } catch (error) { // 错误处理:出错时返回原始值 console.error(`[postcss-transform-3d-accelerate] Error processing value: ${value}`, error); return value; } } /** * 确保字符串中的括号平衡 * @param {string} str - 输入字符串 * @returns {string} - 括号平衡的字符串 */ function balanceParentheses(str) { // 更健壮的括号平衡检查 let stack = []; // 检查是否有未闭合的括号 for (let i = 0; i < str.length; i++) { if (str[i] === '(') { stack.push(i); } else if (str[i] === ')') { if (stack.length > 0) { stack.pop(); } } } // 如果有未闭合的括号,添加缺少的右括号 if (stack.length > 0) { return str + ')'.repeat(stack.length); } return str; } /** * 修复字符串中的括号平衡问题 * @param {string} str - 输入字符串 * @returns {string} - 括号平衡的字符串 */ function fixParenthesesBalance(str) { // 计算括号数量 let openCount = 0; let closeCount = 0; for (let i = 0; i < str.length; i++) { if (str[i] === '(') openCount++; if (str[i] === ')') closeCount++; } // 如果右括号多于左括号,需要去掉多余的右括号 if (closeCount > openCount) { // 从右到左查找并删除多余的右括号 let result = str; let excess = closeCount - openCount; // 从右到左扫描,删除多余的右括号 for (let i = result.length - 1; i >= 0 && excess > 0; i--) { if (result[i] === ')') { // 检查这个右括号是否是多余的 let tempStr = result.substring(0, i) + result.substring(i + 1); let tempOpen = 0; let tempClose = 0; for (let j = 0; j < tempStr.length; j++) { if (tempStr[j] === '(') tempOpen++; if (tempStr[j] === ')') tempClose++; } if (tempOpen >= tempClose) { result = tempStr; excess--; } } } return result; } // 如果左括号多于右括号,添加缺少的右括号 if (openCount > closeCount) { return str + ')'.repeat(openCount - closeCount); } return str; } /** * 修复scale3d函数中的括号问题 * @param {string} css - 包含scale3d函数的CSS字符串 * @returns {string} - 修复后的CSS字符串 */ function fixScale3dParentheses(css) { // 第一步:修复特定的嵌套calc问题 let result = css.replace(/scale3d\(calc\(([^)]+)\),\s*calc\(([^)]+),\s*([^)]+)\)\)/g, (match, calcX, calcY, z) => { return `scale3d(calc(${calcX}), calc(${calcY}), ${z})`; }); // 第二步:修复其他可能的括号不匹配问题 // 找出所有transform声明 const transformRegex = /transform:\s*([^;]+);/g; result = result.replace(transformRegex, (match, transformValue) => { // 计算括号数量 let openCount = 0; let closeCount = 0; for (let i = 0; i < transformValue.length; i++) { if (transformValue[i] === '(') openCount++; if (transformValue[i] === ')') closeCount++; } // 如果括号不匹配 if (openCount !== closeCount) { // 尝试修复transform值中的括号 let fixedValue = transformValue; // 如果右括号多于左括号,移除多余的右括号 if (closeCount > openCount) { const excess = closeCount - openCount; for (let i = 0; i < excess; i++) { const lastIndex = fixedValue.lastIndexOf(')'); if (lastIndex !== -1) { fixedValue = fixedValue.substring(0, lastIndex) + fixedValue.substring(lastIndex + 1); } } } // 如果左括号多于右括号,添加缺少的右括号 if (openCount > closeCount) { fixedValue = fixedValue + ')'.repeat(openCount - closeCount); } return `transform: ${fixedValue};`; } return match; }); // 第三步:特殊处理@keyframes move-calc中的问题 // 这是一个直接针对特定问题的修复 const moveCalcRegex = /@keyframes\s+move-calc\s*\{[\s\S]*?50%\s*\{[\s\S]*?transform:\s*([^;]+);/g; result = result.replace(moveCalcRegex, (match, transformValue) => { // 直接替换有问题的部分 if (transformValue.includes('scale3d(calc(var(--scale) * 1.2, calc(var(--scale) * 1.2, 1))')) { const fixed = transformValue.replace( 'scale3d(calc(var(--scale) * 1.2, calc(var(--scale) * 1.2, 1))', 'scale3d(calc(var(--scale) * 1.2), calc(var(--scale) * 1.2), 1)' ); return match.replace(transformValue, fixed); } return match; }); return result; } /** * 检查是否应该排除此选择器 * @param {string} selector - CSS 选择器 * @param {Array} excludeSelectors - 要排除的选择器列表 * @returns {boolean} - 是否应该排除 */ function shouldExclude(selector, excludeSelectors = []) { if (!excludeSelectors.length) return false; return excludeSelectors.some(exclude => { if (exclude instanceof RegExp) { return exclude.test(selector); } return selector.includes(exclude); }); } /** * 检查规则是否包含动画或过渡 * @param {Object} rule - PostCSS 规则对象 * @returns {boolean} - 是否包含动画或过渡 */ function hasAnimationOrTransition(rule) { return rule.nodes.some(node => { if (node.type !== 'decl') return false; const prop = node.prop.toLowerCase(); return prop === 'transition' || prop === 'transition-property' || prop === 'animation' || prop === 'animation-name' || prop.startsWith('transition-') || prop.startsWith('animation-'); }); } /** * 提取规则中使用的动画名称 * @param {Object} rule - PostCSS 规则对象 * @returns {Array} - 动画名称数组 */ function extractAnimationNames(rule) { const animationNames = []; rule.walkDecls(decl => { const prop = decl.prop.toLowerCase(); if (prop === 'animation' || prop === 'animation-name') { // 简单解析动画名称(实际情况可能更复杂) const names = decl.value.split(/\s+|,/).filter(name => { // 过滤掉关键字和时间值 return !name.match(/^(none|inherit|initial|unset|infinite|alternate|forwards|backwards|both|normal|reverse|alternate-reverse|ease|linear|ease-in|ease-out|ease-in-out|step-start|step-end|paused|running|\d+m?s)$/i) && !name.match(/^\d+(\.\d+)?(m?s|%)$/); }); animationNames.push(...names); } }); return animationNames; } /** * 处理浏览器前缀 * @param {string} prop - CSS 属性 * @returns {string} - 不带前缀的属性名 */ function handleVendorPrefix(prop) { const match = prop.match(TRANSFORM_REGEX.prefixes); return match ? match[2] : prop; } /** * PostCSS 插件:CSS Transform 3D 加速(优化版) */ module.exports = (opts = {}) => { const options = { // 要排除的选择器列表 excludeSelectors: [], // 是否添加 will-change: transform addWillChange: true, // 是否只在有动画或过渡时添加 will-change smartWillChange: true, // 是否添加 transform-style: preserve-3d addPreserve3d: false, // 是否添加 backface-visibility: hidden addBackfaceVisibility: false, // 是否添加默认的 transform-origin addTransformOrigin: false, // 是否处理 @keyframes 中的变换 processKeyframes: true, // 是否启用缓存 enableCache: true, // 是否处理带前缀的变换属性 handlePrefixes: true, ...opts }; // 如果禁用缓存,清空缓存 if (!options.enableCache) { transformCache.clear(); } return { postcssPlugin: 'postcss-transform-3d-accelerate', Once(root, { result }) { // 收集被排除选择器使用的动画名称 const excludedAnimations = new Set(); if (options.processKeyframes && options.excludeSelectors.length > 0) { root.walkRules(rule => { if (shouldExclude(rule.selector, options.excludeSelectors)) { // 提取被排除选择器使用的动画名称 const animationNames = extractAnimationNames(rule); animationNames.forEach(name => excludedAnimations.add(name)); } }); } // 遍历所有规则 root.walkRules(rule => { // 检查是否应该排除此选择器 if (shouldExclude(rule.selector, options.excludeSelectors)) { return; } // 检查是否有动画或过渡 const hasAnimation = options.smartWillChange ? hasAnimationOrTransition(rule) : true; // 需要处理的属性列表 const transformProps = options.handlePrefixes ? ['transform', '-webkit-transform', '-moz-transform', '-ms-transform', '-o-transform'] : ['transform']; // 遍历所有声明 transformProps.forEach(transformProp => { rule.walkDecls(transformProp, decl => { // 转换 transform 值 const originalValue = decl.value; const newValue = transform2dTo3d(originalValue); // 如果值发生了变化,则更新声明 if (newValue !== originalValue) { decl.value = newValue; // 添加 will-change: transform if (options.addWillChange && (!options.smartWillChange || hasAnimation)) { const hasWillChange = rule.nodes.some(i => i.type === 'decl' && i.prop === 'will-change' && i.value.includes('transform') ); if (!hasWillChange) { rule.append({ prop: 'will-change', value: 'transform' }); } } // 添加 transform-style: preserve-3d if (options.addPreserve3d) { const hasTransformStyle = rule.nodes.some(i => i.type === 'decl' && i.prop === 'transform-style' ); if (!hasTransformStyle) { rule.append({ prop: 'transform-style', value: 'preserve-3d' }); } } // 添加 backface-visibility: hidden if (options.addBackfaceVisibility) { const hasBackfaceVisibility = rule.nodes.some(i => i.type === 'decl' && i.prop === 'backface-visibility' ); if (!hasBackfaceVisibility) { rule.append({ prop: 'backface-visibility', value: 'hidden' }); } } // 添加默认的 transform-origin if (options.addTransformOrigin) { const hasTransformOrigin = rule.nodes.some(i => i.type === 'decl' && i.prop === 'transform-origin' ); if (!hasTransformOrigin) { rule.append({ prop: 'transform-origin', value: '50% 50%' }); } } } }); }); }); // 处理 @keyframes if (options.processKeyframes) { root.walkAtRules('keyframes', atRule => { const animationName = atRule.params; // 检查是否应该排除此动画 if (excludedAnimations.has(animationName)) { // 跳过被排除选择器使用的动画 return; } atRule.walkRules(keyframeRule => { // 需要处理的属性列表 const transformProps = options.handlePrefixes ? ['transform', '-webkit-transform', '-moz-transform', '-ms-transform', '-o-transform'] : ['transform']; transformProps.forEach(transformProp => { keyframeRule.walkDecls(transformProp, decl => { // 转换 transform 值 const originalValue = decl.value; let newValue = transform2dTo3d(originalValue); // 特殊处理:修复scale3d函数中的括号问题 // 这是为了解决嵌套calc()函数导致的括号不匹配问题 if (newValue.includes('scale3d(calc(') && newValue.includes(', 1))')) { newValue = newValue.replace(/scale3d\(calc\(([^)]+)\),\s*calc\(([^)]+),\s*([^)]+)\)\)/g, (match, calcX, calcY, z) => { return `scale3d(calc(${calcX}), calc(${calcY}), ${z})`; } ); } // 如果值发生了变化,则更新声明 if (newValue !== originalValue) { decl.value = newValue; } }); }); }); }); // 处理 -webkit-keyframes 等前缀版本 if (options.handlePrefixes) { const prefixedKeyframes = ['-webkit-keyframes', '-moz-keyframes', '-ms-keyframes', '-o-keyframes']; prefixedKeyframes.forEach(prefixedKeyframe => { root.walkAtRules(prefixedKeyframe, atRule => { // 检查是否是被排除的动画 const animationName = atRule.params; if (excludedAnimations.has(animationName)) { // 跳过被排除选择器使用的动画 return; } atRule.walkRules(keyframeRule => { keyframeRule.walkDecls(/^(-webkit-|-moz-|-ms-|-o-)?transform$/, decl => { // 转换 transform 值 const originalValue = decl.value; let newValue = transform2dTo3d(originalValue); // 特殊处理:修复scale3d函数中的括号问题 // 这是为了解决嵌套calc()函数导致的括号不匹配问题 if (newValue.includes('scale3d(calc(') && newValue.includes(', 1))')) { newValue = newValue.replace(/scale3d\(calc\(([^)]+)\),\s*calc\(([^)]+),\s*([^)]+)\)\)/g, (match, calcX, calcY, z) => { return `scale3d(calc(${calcX}), calc(${calcY}), ${z})`; } ); } // 如果值发生了变化,则更新声明 if (newValue !== originalValue) { decl.value = newValue; } }); }); }); }); } } } }; }; module.exports.postcss = true;