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-gradient
const 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 });
}
}
// 如果没有解析到颜色停止点,返回null
if (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-gradient
const 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 });
}
}
// 如果没有解析到颜色停止点,返回null
if (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;
// 检查浏览器是否支持 createConicGradient
if (typeof ctx.createConicGradient !== 'function') {
console.warn('当前浏览器不支持 createConicGradient');
return null;
}
// 解析 CSS conic-gradient
const 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 });
}
}
// 如果没有解析到颜色停止点,返回null
if (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;
}