将css中的线性渐变,径向渐变,锥心渐变,转成canvas中的渐变

发布于:2025-09-05 ⋅ 阅读:(15) ⋅ 点赞:(0)

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;
}


网站公告

今日签到

点亮在社区的每一天
去签到