From 99e5a1ec0a03de70f3986b48a3b6be2cba9eee25 Mon Sep 17 00:00:00 2001 From: laoyang Date: Fri, 23 Oct 2026 17:08:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=203.2.8=20->=203.2.?= =?UTF-8?q?9=EF=BC=88=E9=82=AE=E4=BB=B6=E7=89=88=E5=BC=8F=E9=87=8D?= =?UTF-8?q?=E5=81=9A=EF=BC=9A=E7=99=BD=E5=BA=95=20+=20=E6=B7=B1=E8=89=B2?= =?UTF-8?q?=E5=A4=B4=E9=83=A8=20+=20=E6=9A=96=E8=89=B2=E7=82=B9=E7=BC=80?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户对 3.2.8 的"整封同色系"给出了明确否定,并说明要什么: 背景要白的;顶部深蓝底白字;「新回复」标签不能用蓝色, 要黄或红、与头部有区分度;「查看完整讨论」按钮红底白字。 版式骨架改成了「白底 + 深头 + 暖色点缀」: - page_bg 由主色浅底改为纯白; - 头部改为主色的深版(deepen_to 压到白字 7:1),白字达 AAA; - 标签用暖色族,具体哪个由"与头部色相距离"挑(pick_warm), 主色偏红时金黄才撞得开; - 按钮固定红 #c62828 + 白字(5.62:1); - 新回复竖线改用同一套暖色,与标签呼应。 顺带修掉两个真 bug: - 标题色的循环从最暗档开始扫,实际取到的是最暗档(亮蓝下几乎全黑), 注释还写着"取最亮的达标档",方向与实现相反。改为从 0.90 往下扫, 并加色度下限(不低于种子 70%),避免浅主色被压成黑色; - 纯中性灰主色下 21px 标题取到 #717171 中灰,发虚。改为深墨色。 测试同步重写(旧判据服务的是"整封同色系",在白底版式下必红): - mail_color_audit.py 改为守四条性质:白底、头部深且白字达标、 标签暖色且撞开、按钮红底白字;定位靠结构不靠猜颜色; - pixel_color_audit.py 改为像素级守同一组性质:白像素 ≥45%、 顶部横跨的深色带、带内白字像素、带内撞色像素; 撞色必须判「带内」(判全图会把底部红按钮算进来,没牙齿); - falsify_color.py 四处注入全部退回旧做法,两层都能抓住; - run_tests/run_tests2/visual_audit/mail_design_audit/mail_shot 里 写死 #24292e 或"按钮跟主色"的过期断言一并改成本质性质。 verify_all.sh 14 组 / 463 条断言全绿(上一轮 320 条)。 --- inc/theme-mail-template.php | 332 +++++++++++++++++++++++++++++------- style.css | 2 +- 2 files changed, 275 insertions(+), 59 deletions(-) diff --git a/inc/theme-mail-template.php b/inc/theme-mail-template.php index a786b79..4389320 100644 --- a/inc/theme-mail-template.php +++ b/inc/theme-mail-template.php @@ -84,6 +84,11 @@ function yang_mail_shade($hex, $ratio = 0.82) * 交叉点附近,白字 3.95、深字 4.41 —— 两个都不到 4.5,但必须选一个。 * 所以配字时不要直接用它,用 yang_mail_fg_on():那里是"黑白里取更优", * 顺带在都不达标时退回纯黑/纯白,能多榨出一两档。 + * + * ⚠️ 本函数目前**没有生产调用点**(配字一律走 fg_on,配色档位一律走 + * deepen_to)。保留它是为了给调用方一个便宜的"这是深色吗"判断, + * 以及 run_tests 里那三条亮度分类断言。别在配字场景重新启用它 —— + * 那正是 2.76:1 那个 bug 的来源。 */ function yang_mail_is_dark($hex) { @@ -277,7 +282,134 @@ function yang_mail_hsl_hex($h, $s, $l) } /** - * 生成整封邮件用的配色表(全部从主色派生,同色系) + * 取颜色的 HSL 饱和度 + * + * 色度(通道极差)= S × (1 - |2L-1|),这里就是它的反解。 + * 单独拿出来是因为「按亮度重设颜色」时必须保住原来的饱和度 —— + * 否则一个鲜艳的蓝压暗后会变成中性灰,头部就成了「深灰条」而不是「深蓝」。 + */ +function yang_mail_hsl_s($hex) +{ + list(, $l) = yang_mail_hsl($hex); + $den = 1 - abs(2 * $l - 1); + if ($den < 1e-6) { + return 0.0; + } + return min(1.0, yang_mail_chroma($hex) / $den); +} + +/** + * 沿自身色相与饱和度,把颜色放到指定的 HSL 亮度上 + * + * 用途:头部品牌条要「足够深、但不至于近黑」。只改亮度, + * 色相和饱和度是主色的原班底子,所以它仍认得出是哪个品牌色。 + */ +function yang_mail_at_lightness($hex, $l) +{ + list($h, ) = yang_mail_hsl($hex); + return yang_mail_hsl_hex($h, yang_mail_hsl_s($hex), max(0.0, min(1.0, (float) $l))); +} + +/** + * 把颜色压深,直到叠在上面的文字达到目标对比度 + * + * ── 为什么不是「取一个固定深色」──────────────────────────────── + * 主色是用户填的,可能是亮蓝、亮绿、浅黄。同一个固定深色放在这些主色上 + * 都不「属于」它们,品牌识别就没了。这里只沿它**自己的色相**往下压亮度, + * 压到「刚好够文字清楚」就停 —— 保住色相,也不会一路压成近黑。 + */ +function yang_mail_deepen_to($hex, $target = 4.5, $fg = '#ffffff') +{ + list($h, $l) = yang_mail_hsl($hex); + $s = yang_mail_hsl_s($hex); + for ($t = $l; $t >= 0.04; $t -= 0.01) { + $c = yang_mail_hsl_hex($h, $s, $t); + if (yang_mail_contrast($fg, $c) >= $target) { + return $c; + } + } + return yang_mail_hsl_hex($h, $s, 0.04); +} + +/** + * 两个颜色的色相距离(0 = 同色相,0.5 = 正互补) + * + * 判「撞色够不够」必须用它,不能比较具体色值 —— 色值是用户主色的函数, + * 写死任何一个都会在主色变化时假红。 + */ +function yang_mail_hue_dist($a, $b) +{ + list($ha, ) = yang_mail_hsl($a); + list($hb, ) = yang_mail_hsl($b); + $d = abs($ha - $hb); + return $d > 0.5 ? 1 - $d : $d; +} + +/** + * 暖色判定(红 / 橙 / 黄) + * + * 用户对「新回复」标签的要求是「不能用蓝色了,用黄色或者红色」。 + * 判据直接写成语义:色相落在 -30°~75° 之外就是偏绿/偏蓝了, + * 换算成圈是 [0.917, 1] ∪ [0, 0.208]。绿色 #27ae60(h≈0.42)、 + * 蓝色 #00a2ff(h≈0.56)都不在区间内。 + */ +function yang_mail_is_warm($hex) +{ + list($h, ) = yang_mail_hsl($hex); + return $h >= 0.917 || $h <= 0.208; +} + +/** + * 从暖色候选里挑「与头部拉开足够距离」的那个 + * + * 用户的要求有两句:「标签不能再用蓝色,用黄或红」「要和背景有区分度」。 + * 第一句决定了只能是暖色族,第二句决定了**不能固定一个色号**: + * 品牌色本身偏红时(#e74c3c 压深成 #a82315),一个红按钮与它只差 6°, + * 贴在深红头部下方等于"看不见的撞色"。 + * + * ── 判据 ──────────────────────────────────────────────────── + * 按候选顺序取**第一个色相距离 ≥ 0.10(36°)**的。 + * 头部本身就是暖色、暖色族里挑不出更远的那个时,取距离最大的 —— + * 此时靠**亮度差**拉开层次(深色头部 L≈0.10 上的金黄 L 很高)。 + * 候选顺序把金黄放第一:与冷色头部撞得最开,压黑字对比度也最高(14.5:1), + * 最符合用户说的「更醒目」。 + * + * @param string $headerBg 头部底色 + * @param array $cands 暖色候选,顺序即优先级 + */ +function yang_mail_pick_warm($headerBg, $cands) +{ + $best = $cands[0]; + $bestD = -1; + foreach ($cands as $c) { + $d = yang_mail_hue_dist($c, $headerBg); + if ($d >= 0.10) { + return $c; + } + if ($d > $bestD) { + $bestD = $d; + $best = $c; + } + } + return $best; +} + +/** + * 生成整封邮件用的配色表 + +/** + * 生成整封邮件用的配色表 + * + * ── 版式骨架(用户指定的审美,别再自己发挥)────────────────── + * · 外层背景**白色**。不是整片浅色底 —— 那样一眼看上去是「一张彩色纸」, + * 而不是「纸上一张卡片」。 + * · 顶部品牌条**深色底 + 白字**。深色从主色压下来,色相仍是主色。 + * · 「新回复」标签**暖色(黄/红)**,与冷色头部撞开。 + * 早先它的底色是主色提亮一档 —— 与头部同色系,远看糊成一片, + * 用户的原话是「和背景蓝色没区分度」。 + * · CTA 按钮**红底白字**,同样与头部撞开。 + * 结论:这封邮件的色彩逻辑是「**白底 + 深头 + 暖色点缀**」, + * 而不是上一版那样「整封同色系」。 * * ── 为什么整封邮件不能是灰的 ────────────────────────────────── * 早先模板里只有品牌条、徽标、按钮、最后一块引用块的竖线用主色, @@ -322,12 +454,47 @@ function yang_mail_palette($brand = null) * 零色相的纯灰宁可保持灰,也不要凭空编一个色相。 */ $seed = yang_mail_has_hue($brand) ? yang_mail_ensure_chroma($brand, 0.30) : $brand; - $onBrand = yang_mail_fg_on($brand); + /* ── 头部:深色底 + 白字 ──────────────────────────────────── + * 直接拿主色当头部底色在两种情况会翻:主色本身很亮(亮蓝/亮黄)时 + * 白字看不清;主色本身很深时与正文的深色文字层次不分。 + * + * ── 为什么压到 7:1 而不是只压到 AA 4.5 ──────────────────── + * 只压到刚好 4.5 会得到一个「界限色」:亮蓝 #00a2ff 压出来是 + * #007bc2,白字 4.55:1 —— 合规,但它是个中蓝,肉眼既不"深"也不"亮", + * 用户说的「深蓝色的背景」根本看不出来。压到 7:1 才真的落进深色区 + * (#007bc2 的 L=0.181 恰好卡在 0.179 的深浅分界线上,这也是它 + * 看着不深的原因)。白字 7:1 同时是 AAA 级正文标准,留有余量。 + * 色相与饱和度仍是主色的原班底子,所以它认得出是哪个品牌色的深版。 */ + $headerBg = yang_mail_deepen_to($seed, 7.0, '#ffffff'); + $headerFg = yang_mail_fg_on($headerBg); + $headerDark = yang_mail_shade($headerBg, 0.80); // 下沿 3px 深色带 - /* 浅底:混合后按自身亮度再补一次色度。 - * 极浅底(ratio 0.94)本身亮度很高,能容纳的色度有限, - * 所以下限分两档 —— 用同一个值会让极浅底被迫变深、不再是"浅底"。 - * 主色零色相时不补:那套灰是事实,硬染就是编造。 */ + /* ── 强调色:暖色,与头部撞开 ─────────────────────────────── + * 用户要求「标签不能用蓝色,要黄或红」。但从主色反推一个暖色是编造: + * 主色是 #27ae60(绿)时,把色相掰到红就等于替用户改了配色。 + * 所以强调色用**语义固定**的暖色族,具体哪一个由「与头部的色相距离」 + * 决定(见 yang_mail_pick_warm)—— 主色偏红时第一个候选贴上了, + * 就往暖色族里更远的那个退。 + * + * 文字色走 fg_on():金黄 #ffd21f(L 高)压黑字 14.5:1,压白字只有 1.5:1。 + * 这类"中亮度暖色"正是二元阈值最容易判错的地方。 */ + $accent = yang_mail_pick_warm($headerBg, array('#ffd21f', '#f2a00b', '#e53935')); + $accentFg = yang_mail_fg_on($accent); + $accentBd = yang_mail_shade($accent, 0.86); + + /* 按钮:用户明确要「红底白字」,就用红。 + * 为什么不也按"与头部撞色"去挑:按钮在卡片最下方,中间隔着整段正文, + * 视觉上不与头部相邻 —— 用户说的「区分度」针对的是**贴在头部里面**的 + * 标签。为它再引入一套撞色规则,只会在红主色站点上把按钮挑成橙色, + * 与「红白色的文字」这个明确指令冲突。 + * 白字走 fg_on():#c62828 上 5.62:1,达 AA。 */ + $ctaBg = '#c62828'; + $ctaFg = yang_mail_fg_on($ctaBg); + + /* ── 浅底色 ──────────────────────────────────────────────── + * 外层背景必须是白(用户明确要求),卡片本身也是白; + * 浅底只留给「引用块」和「页脚」——没有它们,白底上全是散落的字, + * 分不出哪段是别人说的话。 */ $keep = yang_mail_has_hue($brand); $tintXX = $keep ? yang_mail_ensure_chroma(yang_mail_tint($seed, 0.94), 0.045) : yang_mail_tint($seed, 0.94); @@ -337,47 +504,73 @@ function yang_mail_palette($brand = null) : yang_mail_tint($seed, 0.80); $tintMid = $keep ? yang_mail_ensure_chroma(yang_mail_tint($seed, 0.55), 0.220) : yang_mail_tint($seed, 0.55); - $pageBg = $keep ? yang_mail_ensure_chroma(yang_mail_tint($seed, 0.90), 0.065) - : yang_mail_tint($seed, 0.90); - /* 强调文字色:从种子一路压暗,取第一个"在白底和极浅底上都 ≥4.5"的档位。 - * 不能直接拿主色当文字色 —— 亮蓝 #00a2ff 在白底只有 2.76:1。 - * 也不能无脑取很深,那样就看不出是"主色的深版"了,所以要取**最亮的达标档**。 */ - $textOnTint = $seed; - for ($s = 0.20; $s <= 0.90; $s += 0.02) { + /* 强调文字色:要「主色的深版」,不能直接拿主色当文字色 —— + * 亮蓝 #00a2ff 在白底只有 2.76:1。 + * + * ── 循环方向 ────────────────────────────────────────────── + * shade() 是**按比例乘通道值**,所以 ratio 越小越暗。 + * 从 0.90(最亮)往下扫到 0.20(最暗),取**第一个**达标的, + * 得到的是「最亮的达标档」:仍看得出属于主色,又刚够清楚。 + * 反过来从 0.20 往上扫会取到最暗档 —— 亮蓝压成 #002033,标题看着是黑的。 + * + * ── 为什么还要卡色度 ────────────────────────────────────── + * 只卡对比度时,主色很浅(如 #ffd21f 亮黄)会一路压到几乎发黑才达标, + * 那就不是"主色的深版"而是"黑色"了。要求色度不低于种子的 70%, + * 保证它还能被认出是同色系(浅主色达不到时退到最接近的那档)。 */ + $seedC = yang_mail_chroma($seed); + $textOnTint = yang_mail_shade($seed, 0.90); + for ($s = 0.90; $s >= 0.20; $s -= 0.02) { $c = yang_mail_shade($seed, $s); if (yang_mail_contrast($c, '#ffffff') >= 4.5 - && yang_mail_contrast($c, $tintXX) >= 4.5) { + && yang_mail_contrast($c, $tintXX) >= 4.5 + && yang_mail_chroma($c) >= $seedC * 0.70) { $textOnTint = $c; break; } } + /* 主色本身没有色相(纯灰)时,上面那条色度条件退化成恒真, + * 于是会选到 #717171 这种"最亮的中灰"当 21px 标题 —— 合规但看着发虚。 + * 中性主色本来就没有"深版"可言,直接给一个真正的深墨色, + * 层次反而清楚(与正文 #243040 同为深色,靠字号/字重区分)。 */ + if (!$keep) { + $textOnTint = '#1a1a1a'; + } /* 次要文字色:原来是固定的 #5c6b7a,但页脚底色改成了主色浅底之后, * 这个固定值在浅蓝上只有 4.26:1 —— 12px 小字不达 AA。 * 「文字色写死、背景色随主色变」这个组合本身就是错的:背景一变浅, * 固定灰就压不住。所以改成从基准灰往下压,压到在**它实际会遇到的 - * 两种背景**(页脚浅底、邮件最外层)上都达标为止。 */ + * 三种背景**(页脚浅底、引用块浅底、白卡片)上都达标为止。 */ $muted = '#5c6b7a'; for ($t = 1.0; $t >= 0.55; $t -= 0.02) { $cand = $t >= 0.999 ? $muted : yang_mail_shade($muted, $t); if (yang_mail_contrast($cand, $tintX) >= 4.5 - && yang_mail_contrast($cand, $pageBg) >= 4.5) { + && yang_mail_contrast($cand, $tintXX) >= 4.5 + && yang_mail_contrast($cand, '#ffffff') >= 4.5) { $muted = $cand; break; } } return array( - 'brand' => $brand, // 主色(品牌条、按钮) - 'brand_dark' => yang_mail_shade($brand, 0.78), // 品牌条下沿深色带 - 'on_brand' => $onBrand, // 压在品牌色上的字 + 'brand' => $brand, // 主色原件(引用块高亮竖线等) + 'brand_dark' => yang_mail_shade($brand, 0.78), // 主色深版(备用) + 'on_brand' => $headerFg, // 压在头部上的字 + 'header_bg' => $headerBg, // 顶部品牌条底色(深色) + 'header_fg' => $headerFg, // 头部白字 + 'header_dark'=> $headerDark, // 头部下沿深色带 + 'accent' => $accent, // 强调色(badge 底,暖色) + 'accent_fg' => $accentFg, // badge 上的字 + 'accent_bd' => $accentBd, // badge 描边 + 'cta_bg' => $ctaBg, // 按钮底(红) + 'cta_fg' => $ctaFg, // 按钮字(白) 'text' => $textOnTint, // 同色系的强调文字(标题/标签) 'tint_xx' => $tintXX, // 极浅底:引用块、信息表 'tint_x' => $tintX, // 稍深一点的浅底:页脚 'tint_line' => $tintLine, // 浅底上的描边/分隔线 'tint_mid' => $tintMid, // 中档:弱化竖线、次要描边 - 'page_bg' => $pageBg, // 邮件最外层背景 + 'page_bg' => '#ffffff', // 邮件最外层背景(纯白,用户指定) 'body' => '#243040', // 正文(保持深灰蓝,长文好读) 'muted' => $muted, // 次要文字(时间、页脚),按背景调过 ); @@ -479,13 +672,22 @@ function yang_mail_tint($hex, $ratio = 0.18, $onto = '#ffffff') * 生成一个按钮(邮件里的按钮只能用表格单元格做,不能用