将css中的线性渐变,径向渐变,锥心渐变,转成canvas中的渐变
1.css中的linear-gradient转成canvas中
/*** 将CSS linear-gradient 转换为 Canvas 渐变* @param ctx Canvas 2D 上下文* @param cssGradient CSS linear-gradient 字符串,如 "linear-gradient(45deg, red 0%, blue 50%, green 100%)"* @param width Canvas 宽度* @param height Canvas 高度* @returns CanvasGradient 对象*/
export const cssLinearGradientToCanvas = (ctx: CanvasRenderingContext2D | null,cssGradient: string,width: number,height: number
): CanvasGradient | null => {if (!ctx) return null;// 解析 CSS linear-gradientconst gradientMatch = cssGradient.match(/linear-gradient\s*\(\s*([^)]+)\s*\)/);if (!gradientMatch) return null;const gradientContent = gradientMatch[1];const parts = gradientContent.split(',').map(part => part.trim());// 解析方向和角度let angle = 0;let colorStops: Array<{color: string, position: number}> = [];let startIndex = 0;// 检查第一个参数是否为角度或方向const firstPart = parts[0];if (firstPart.includes('deg')) {angle = parseFloat(firstPart.replace('deg', ''));startIndex = 1;} else if (firstPart.includes('to ')) {// 处理方向关键字const direction = firstPart.replace('to ', '');switch (direction) {case 'top': angle = 0; break;case 'right': angle = 90; break;case 'bottom': angle = 180; break;case 'left': angle = 270; break;case 'top right': angle = 45; break;case 'bottom right': angle = 135; break;case 'bottom left': angle = 225; break;case 'top left': angle = 315; break;default: angle = 180; // 默认从上到下}startIndex = 1;}// 解析颜色停止点for (let i = startIndex; i < parts.length; i++) {const colorStop = parts[i].trim();const colorMatch = colorStop.match(/^(.+?)\s+(\d+(?:\.\d+)?%?)$/);if (colorMatch) {const color = colorMatch[1].trim();const positionStr = colorMatch[2];let position: number;if (positionStr.includes('%')) {position = parseFloat(positionStr.replace('%', '')) / 100;} else {// 如果没有百分比,假设是像素值,转换为比例position = parseFloat(positionStr) / Math.max(width, height);}colorStops.push({ color, position });} else {// 如果没有指定位置,均匀分布const color = colorStop.trim();const position = (i - startIndex) / (parts.length - startIndex - 1);colorStops.push({ color, position });}}// 如果没有解析到颜色停止点,返回nullif (colorStops.length === 0) return null;// 计算渐变的起点和终点坐标const angleRad = (angle * Math.PI) / 180;// 计算渐变线的长度const gradientLength = Math.abs(width * Math.cos(angleRad)) + Math.abs(height * Math.sin(angleRad));// 计算起点和终点const centerX = width / 2;const centerY = height / 2;const x1 = centerX - (gradientLength / 2) * Math.cos(angleRad);const y1 = centerY - (gradientLength / 2) * Math.sin(angleRad);const x2 = centerX + (gradientLength / 2) * Math.cos(angleRad);const y2 = centerY + (gradientLength / 2) * Math.sin(angleRad);// 创建Canvas渐变const gradient = ctx.createLinearGradient(x1, y1, x2, y2);// 添加颜色停止点colorStops.forEach(stop => {gradient.addColorStop(Math.max(0, Math.min(1, stop.position)), stop.color);});return gradient;
}
解析linear-gradient的角度,其中比较难一点的地方就是gradientLength是如何计算的,我在网上找了一张图,可以大致解释一下,这个x1,y1,x2,y2是怎么计算的:
可以看到,其实我们计算的点,就是x0,y0,x1,y1,那我么首先需要知道(x0,y0)到(x1,y1)之间的距离。
两个点之间的距离,就是两条红色线条的长度,分别作图,可以得到两点之间的距离=wcos(a)+hsin(a)
得到了两点之间的距离,(x0,y0),就是中心点(w/2,h/2)减去距离的一半旋转角度,(x1,y1)就是中心点(w/2,h/2)加上距离的一半旋转角度
2.cssRadialGradientToCanvas转成canvas对应渐变
/*** 将CSS radial-gradient 转换为 Canvas 径向渐变* @param ctx Canvas 2D 上下文* @param cssGradient CSS radial-gradient 字符串,如 "radial-gradient(circle, red 0%, blue 100%)"* @param width Canvas 宽度* @param height Canvas 高度* @returns CanvasGradient 对象*/
export const cssRadialGradientToCanvas = (ctx: CanvasRenderingContext2D | null,cssGradient: string,width: number,height: number
): CanvasGradient | null => {if (!ctx) return null;// 解析 CSS radial-gradientconst gradientMatch = cssGradient.match(/radial-gradient\s*\(\s*([^)]+)\s*\)/);if (!gradientMatch) return null;const gradientContent = gradientMatch[1];const parts = gradientContent.split(',').map(part => part.trim());// 默认参数let centerX = width / 2;let centerY = height / 2;let radius = Math.min(width, height) / 2;let colorStops: Array<{color: string, position: number}> = [];let startIndex = 0;// 检查第一个参数是否为形状或位置const firstPart = parts[0];if (firstPart.includes('circle') || firstPart.includes('ellipse')) {startIndex = 1;// 解析位置 (at center, at top left 等)if (firstPart.includes('at ')) {const positionMatch = firstPart.match(/at\s+(.+)/);if (positionMatch) {const position = positionMatch[1].trim();const coords = parsePosition(position, width, height);centerX = coords.x;centerY = coords.y;}}} else if (firstPart.includes('at ')) {// 只有位置,没有形状const positionMatch = firstPart.match(/at\s+(.+)/);if (positionMatch) {const position = positionMatch[1].trim();const coords = parsePosition(position, width, height);centerX = coords.x;centerY = coords.y;}startIndex = 1;}// 解析颜色停止点for (let i = startIndex; i < parts.length; i++) {const colorStop = parts[i].trim();const colorMatch = colorStop.match(/^(.+?)\s+(\d+(?:\.\d+)?%?)$/);if (colorMatch) {const color = colorMatch[1].trim();const positionStr = colorMatch[2];let position: number;if (positionStr.includes('%')) {position = parseFloat(positionStr.replace('%', '')) / 100;} else {position = parseFloat(positionStr) / radius;}colorStops.push({ color, position });} else {// 如果没有指定位置,均匀分布const color = colorStop.trim();const position = (i - startIndex) / (parts.length - startIndex - 1);colorStops.push({ color, position });}}// 如果没有解析到颜色停止点,返回nullif (colorStops.length === 0) return null;// 创建径向渐变const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);// 添加颜色停止点colorStops.forEach(stop => {gradient.addColorStop(Math.max(0, Math.min(1, stop.position)), stop.color);});return gradient;
}/*** 解析CSS位置关键字*/
function parsePosition(position: string, width: number, height: number): {x: number, y: number} {const parts = position.split(/\s+/);let x = width / 2;let y = height / 2;parts.forEach(part => {switch (part) {case 'left': x = 0; break;case 'right': x = width; break;case 'top': y = 0; break;case 'bottom': y = height; break;case 'center': break; // 保持默认中心位置default:if (part.includes('%')) {const percent = parseFloat(part.replace('%', '')) / 100;if (parts.indexOf(part) === 0) {x = width * percent;} else {y = height * percent;}} else if (part.includes('px')) {const pixels = parseFloat(part.replace('px', ''));if (parts.indexOf(part) === 0) {x = pixels;} else {y = pixels;}}}});return { x, y };
}
3.锥形渐变转成canvas对应渐变
/*** 解析CSS位置关键字*/
function parsePosition(position: string, width: number, height: number): {x: number, y: number} {const parts = position.split(/\s+/);let x = width / 2;let y = height / 2;parts.forEach(part => {switch (part) {case 'left': x = 0; break;case 'right': x = width; break;case 'top': y = 0; break;case 'bottom': y = height; break;case 'center': break; // 保持默认中心位置default:if (part.includes('%')) {const percent = parseFloat(part.replace('%', '')) / 100;if (parts.indexOf(part) === 0) {x = width * percent;} else {y = height * percent;}} else if (part.includes('px')) {const pixels = parseFloat(part.replace('px', ''));if (parts.indexOf(part) === 0) {x = pixels;} else {y = pixels;}}}});return { x, y };
}/*** 将CSS conic-gradient 转换为 Canvas 锥形渐变* @param ctx Canvas 2D 上下文* @param cssGradient CSS conic-gradient 字符串,如 "conic-gradient(red, yellow, green, blue, red)"* @param width Canvas 宽度* @param height Canvas 高度* @returns CanvasGradient 对象*/
export const cssConicGradientToCanvas = (ctx: CanvasRenderingContext2D | null,cssGradient: string,width: number,height: number
): CanvasGradient | null => {if (!ctx) return null;// 检查浏览器是否支持 createConicGradientif (typeof ctx.createConicGradient !== 'function') {console.warn('当前浏览器不支持 createConicGradient');return null;}// 解析 CSS conic-gradientconst gradientMatch = cssGradient.match(/conic-gradient\s*\(\s*([^)]+)\s*\)/);if (!gradientMatch) return null;const gradientContent = gradientMatch[1];const parts = gradientContent.split(',').map(part => part.trim());// 默认参数let centerX = width / 2;let centerY = height / 2;let startAngle = 0; // 默认从顶部开始 (0度)let colorStops: Array<{color: string, position: number}> = [];let startIndex = 0;// 检查第一个参数是否为角度或位置const firstPart = parts[0];// 解析起始角度 (from 90deg)if (firstPart.includes('from ') && firstPart.includes('deg')) {const angleMatch = firstPart.match(/from\s+(\d+(?:\.\d+)?)deg/);if (angleMatch) {startAngle = (parseFloat(angleMatch[1]) * Math.PI) / 180;}startIndex = 1;}// 解析位置 (at center, at top left 等)if (firstPart.includes('at ')) {const positionMatch = firstPart.match(/at\s+(.+?)(?:\s+from|$)/);if (positionMatch) {const position = positionMatch[1].trim();const coords = parsePosition(position, width, height);centerX = coords.x;centerY = coords.y;}if (!firstPart.includes('from ')) {startIndex = 1;}}// 解析颜色停止点for (let i = startIndex; i < parts.length; i++) {const colorStop = parts[i].trim();const colorMatch = colorStop.match(/^(.+?)\s+(\d+(?:\.\d+)?(?:deg|%))$/);if (colorMatch) {const color = colorMatch[1].trim();const positionStr = colorMatch[2];let position: number;if (positionStr.includes('deg')) {// 角度转换为 0-1 的比例const degrees = parseFloat(positionStr.replace('deg', ''));position = (degrees % 360) / 360;} else if (positionStr.includes('%')) {position = parseFloat(positionStr.replace('%', '')) / 100;} else {position = parseFloat(positionStr) / 360; // 假设是角度}colorStops.push({ color, position });} else {// 如果没有指定位置,均匀分布const color = colorStop.trim();const position = (i - startIndex) / (parts.length - startIndex - 1);colorStops.push({ color, position });}}// 如果没有解析到颜色停止点,返回nullif (colorStops.length === 0) return null;// 创建锥形渐变const gradient = (ctx as any).createConicGradient(startAngle, centerX, centerY);// 添加颜色停止点colorStops.forEach(stop => {gradient.addColorStop(Math.max(0, Math.min(1, stop.position)), stop.color);});return gradient;
}