You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于背景生成符合WCAG/APCA标准的非黑白适配文本颜色(TypeScript)

实现适配背景的保留原色调文本颜色方案(符合WCAG 2 AA/APCA)

核心思路

要保留文本原有色彩同时满足对比度标准,核心是在不改变色相(H)和饱和度(S)的前提下,动态调整亮度(L),再通过对比度算法校验是否达标,避免直接切换黑白的生硬效果。

具体实现步骤

1. 颜色空间转换工具

先实现十六进制与HSL的互转,方便单独调整亮度:

// 十六进制转HSL
function hexToHsl(hex: string): { h: number, s: number, l: number } {
    const r = parseInt(hex.slice(1, 3), 16) / 255;
    const g = parseInt(hex.slice(3, 5), 16) / 255;
    const b = parseInt(hex.slice(5, 7), 16) / 255;

    const max = Math.max(r, g, b);
    const min = Math.min(r, g, b);
    let h = 0, s = 0;
    const l = (max + min) / 2;

    if (max !== min) {
        const d = max - min;
        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
        switch (max) {
            case r: h = (g - b) / d + (g < b ? 6 : 0); break;
            case g: h = (b - r) / d + 2; break;
            case b: h = (r - g) / d + 4; break;
        }
        h /= 6;
    }

    return { h: h * 360, s: s * 100, l: l * 100 };
}

// HSL转十六进制
function hslToHex(h: number, s: number, l: number): string {
    h /= 360;
    s /= 100;
    l /= 100;
    let r, g, b;

    if (s === 0) {
        r = g = b = l;
    } else {
        const hue2rgb = (p: number, q: number, t: number) => {
            if (t < 0) t += 1;
            if (t > 1) t -= 1;
            if (t < 1/6) return p + (q - p) * 6 * t;
            if (t < 1/2) return q;
            if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        };
        const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        const p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
    }

    const toHex = (x: number) => {
        const hex = Math.round(x * 255).toString(16);
        return hex.length === 1 ? '0' + hex : hex;
    };

    return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}

2. 对比度计算(支持双标准)

WCAG 2 AA 对比度计算

基于相对亮度的标准算法:

// 计算颜色相对亮度
function getRelativeLuminance(hex: string): number {
    const r = parseInt(hex.slice(1, 3), 16);
    const g = parseInt(hex.slice(3, 5), 16);
    const b = parseInt(hex.slice(5, 7), 16);

    const sRGB = [r, g, b].map(val => {
        const normalized = val / 255;
        return normalized <= 0.03928 ? normalized / 12.92 : Math.pow((normalized + 0.055) / 1.055, 2.4);
    });
    return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];
}

// WCAG对比度计算(返回比值,AA标准要求≥4.5)
function calculateWCAGContrast(fgHex: string, bgHex: string): number {
    const l1 = getRelativeLuminance(fgHex);
    const l2 = getRelativeLuminance(bgHex);
    return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}

APCA 对比度计算

更贴合人眼感知的现代算法,针对文本可读性优化:

// APCA对比度计算(返回感知值,正文要求≥45)
function calculateAPCAContrast(fgHex: string, bgHex: string): number {
    const getPerceptualLuminance = (hex: string) => {
        const r = parseInt(hex.slice(1, 3), 16);
        const g = parseInt(hex.slice(3, 5), 16);
        const b = parseInt(hex.slice(5, 7), 16);

        const rLin = r <= 10.3 ? r / 3294.6 : Math.pow((r + 14.025) / 269.025, 2.4);
        const gLin = g <= 10.3 ? g / 3294.6 : Math.pow((g + 14.025) / 269.025, 2.4);
        const bLin = b <= 10.3 ? b / 3294.6 : Math.pow((b + 14.025) / 269.025, 2.4);
        
        return 0.2126729 * rLin + 0.7151522 * gLin + 0.0721750 * bLin;
    };

    const bgY = getPerceptualLuminance(bgHex);
    const fgY = getPerceptualLuminance(fgHex);

    let contrast = 0;
    if (bgY > fgY) {
        const raw = (Math.pow(bgY, 0.55) - Math.pow(fgY, 0.55)) * 1.14;
        contrast = raw > 0.02 ? raw : 0;
    } else {
        const raw = (Math.pow(bgY, 0.55) - Math.pow(fgY, 0.55)) * 1.14;
        contrast = raw < -0.02 ? raw : 0;
    }
    return Math.abs(contrast) * 100;
}

3. 动态调整文本亮度

根据背景亮度方向,逐步调整文本亮度直到满足对比度要求:

function adjustTextColor(originalTextHex: string, bgHex: string, standard: 'WCAG_AA' | 'APCA'): string {
    const target = standard === 'WCAG_AA' ? 4.5 : 45;
    const { h, s, l } = hexToHsl(originalTextHex);
    const bgLuminance = getRelativeLuminance(bgHex);
    let adjustedL = l;
    // 背景亮则降低文本亮度,背景暗则提高
    const step = bgLuminance > 0.5 ? -2 : 2;

    // 循环调整,加次数限制避免死循环
    for (let i = 0; i < 50; i++) {
        const currentHex = hslToHex(h, s, adjustedL);
        const currentContrast = standard === 'WCAG_AA' 
            ? calculateWCAGContrast(currentHex, bgHex) 
            : calculateAPCAContrast(currentHex, bgHex);
        
        if ((standard === 'WCAG_AA' && currentContrast >= target) || 
            (standard === 'APCA' && currentContrast >= target)) {
            return currentHex;
        }

        // 亮度边界限制在0-100
        adjustedL += step;
        if (adjustedL < 0) adjustedL = 0;
        if (adjustedL > 100) adjustedL = 100;
    }

    // 极端情况 fallback 到黑白
    return bgLuminance > 0.5 ? '#000000' : '#ffffff';
}

针对动漫卡片的优化建议

  • 取背景平均亮度:如果背景是渐变或复杂图案,计算背景区域的平均亮度再做调整,避免单一像素的偏差
  • 饱和度微调:若调整亮度后色彩辨识度下降,可在满足对比度的前提下,小幅微调饱和度,保留原色彩特征
  • 缓存计算结果:对重复的背景+文本色组合缓存结果,提升性能

内容的提问来源于stack exchange,提问作者Entropy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 03:08:25