AeroCore Pro 首页幻灯片变形:object-fit:fill 的坑和一套 1640×700 Banner 标准

AI 摘要997笔记AI

首页轮播图在不同窗口宽度下变形,排查发现根因是 object-fit:fill 配合写死高度,改图尺寸无法解决。最终确定 Banner 标准:画布 1640×700,CSS 用 aspect-ratio 与 object-fit:cover,移动端 16:9,并留文字安全区、字号按两倍制作。

此内容由AI根据文章内容自动生成。

前几天把首页三张幻灯片换成了自己画的 banner,第二天用笔记本打开一看,标题胖了一圈,字是横向压扁的。一开始我以为是图没画好,改了两版字号还是不对劲。把浏览器窗口从 1920 一路拖到 400,发现变形率每个宽度都不一样——这就不是图的问题了,是渲染的问题。

这篇把整个排查过程记下来:怎么量、根因在哪、为什么改图尺寸救不了,最后给出我定下来的一套 Banner 标准(画布 1640×700,配一段 CSS),以及画图时踩到的三个文字遮挡坑。如果你也在用 AeroCore Pro 或者别的把轮播高度写死的主题,应该用得上。

顺带一提,站点里图片相关的问题之前也写过一篇:WordPress 插入大图看不清怎么办:多档尺寸怎么选 + 灯箱滚轮缩放改造,那篇讲的是正文插图,这篇讲的是首页轮播,两个是不同的坑。

一、先看现象:同一张图,不同窗口宽度胖瘦不一样

我当时的第一反应是截图对比。后来发现截一张不够,得把窗口宽度一个一个试过去,因为变形量跟容器宽度直接挂钩。

首页这个轮播在 AeroCore Pro 里是一个 swiper 容器,桌面端宽度恒定 820px、高度 350px(后台的滑块高度设置会写进内联变量 --slider-h)。问题出在窄一点的窗口:769px 到 1279px 这个区间里,容器宽度会跟着缩,高度却还是 350px。实测下来是这样:

视口宽度 容器实际渲染 显示比例 对 1720×640 的变形
≥1280 820 × 350 2.343 纵向拉伸 12.8%
1024 664 × 350 1.897 纵向拉伸 29.4%
820 521 × 350 1.489 纵向拉伸 44.6%
768 及以下 721 × 200 3.605 纵向压扁 34.1%
390(手机) 358 × 200 1.790 纵向拉伸 33.4%

820 宽那个断点最离谱,容器变成了 521×350,比例 1.489,一张 2.688 的横图塞进去等于被拉高了一倍多。文字全是歪的。

二、别猜,先量:用无头浏览器抓真实渲染尺寸

查 HTML 源码是看不出这些的,宽度是百分比、高度来自 CSS 变量,最后还有 JS 布局参与。我直接起了个无头 Chrome 连 CDP,在每个视口宽度下取 img.getBoundingClientRect():

// measure.js —— node measure.js
// 把轮播图在多个视口宽度下的真实渲染尺寸打出来
const widths = [1920, 1600, 1440, 1280, 1024, 820, 768, 480, 390];
for (const w of widths) {
  await send("Emulation.setDeviceMetricsOverride",
             { width: w, height: 900, deviceScaleFactor: 1, mobile: w < 700 });
  await sleep(900);
  const v = await eval_js(`(() => {
    const img = document.querySelector('.swiper-slide img');
    const r = img.getBoundingClientRect(), cs = getComputedStyle(img);
    return { w: r.width, h: r.height, fit: cs.objectFit,
             natW: img.naturalWidth, natH: img.naturalHeight };
  })()`);
  const squash = (v.w / v.h) / (v.natW / v.natH);
  console.log(w, v.w + "x" + v.h, "fit=" + v.fit,
              (Math.abs(squash - 1) * 100).toFixed(1) + "%");
}

关键是 cs.objectFit 这一列,它直接告诉了你答案。

三、根因:object-fit:fill 加上写死的高度

主题的轮播 CSS 是这么写的:

.slider-component .swiper{width:100%;height:var(--slider-h, 400px)}
.slider-component .swiper-slide img{width:100%;height:100%;display:block}

图片元素上还挂着一段内联样式:

<img src="..." style="object-fit:fill;object-position:left top" />

这几个条件凑一起就成了必然:

  • img 的盒子和容器一样大,宽度百分比、高度写死;
  • object-fit:fill 的意思是”不管原图比例,把内容拉满整个盒子”,这是它的默认行为,也是最容易踩的一个值;
  • 容器宽度随断点变、高度不变,于是盒子的比例在每个断点都不同。

fill、cover、contain 三者的差别,画出来最直观:

object-fit 的 fill 与 cover 在 820×350 容器里的表现对比:fill 把 1720×640 的图纵向拉伸 12.8%,圆变椭圆;cover 等比缩放只裁左右,零变形

cover 是等比缩放然后裁掉超出部分,contain 是等比缩放留白。这两种都不会让内容变形,代价是可能裁切或者留白。

四、为什么”把图做对尺寸”救不了

我一开始真的去改画布了。把 1720×640 改成跟桌面容器一样的 2.343:1,宽屏下确实好了。然后把窗口一缩,820 宽那个断点又变形了——容器是 521×350,比例 1.489。

把容器比例的区间列出来就明白了:1.489 到 3.605。一张静态图片只有一个比例,你不可能同时等于这五个数。所以只要渲染端还是 fill,怎么改图都会在某个断点翻车。

出路只有一条:让渲染端不许变形,再把图片比例对齐到最常用的那个断点,把裁切量压到零。

五、我定下来的 Banner 标准 v1

1. 画布 1640×700

桌面容器 820×350,比例 2.342857。源图做 1640×700,正好是这个比例的两倍。这样桌面端 1:1 显示,不缩放、不裁切、不变形。

为什么不是 820×350 原尺寸?高分屏和缩放会糊,两倍图是底线。格式统一 webp(quality 92),别再往站里塞 jpg/png。

2. 渲染端必须改 CSS

把下面这段放进主题的 custom_css(后台自定义 CSS 输入框就行):

/* 首页轮播:高度跟随图片比例,杜绝 object-fit:fill 拉伸 */
.slider-component .swiper{height:auto!important;aspect-ratio:1640/700}
.slider-component .swiper-slide img{object-fit:cover!important;object-position:center!important}
@media (max-width:768px){
  .slider-component .swiper{aspect-ratio:16/9}
}

两个地方必须带 !important,因为 object-fit:fill 是内联样式,普通样式表盖不过它。

效果:桌面 820 宽下高度算出来正好 350px,跟原来一模一样,视觉上零变化;窄屏时高度自动收缩(664 宽 → 283px,521 宽 → 222px),不再拉伸。移动端我改成了 16:9,在 390 宽的手机上高度是 219px,比写死的 200px 还稍微舒展一点。

3. 安全区:文字往中间放

cover 加 aspect-ratio 之后,桌面和中间断点都不裁切了,但移动端 16:9 会裁掉左右各 12%。所以文字不能顶边放:

1640×700 画布的安全区划分:L1 必读区水平 12% 到 88%、垂直 10% 到 90%,主标题副标题描述标签都在里面;L2 出血区 5% 到 95% 只放装饰

级别 区域(以 1640×700 计) 放什么
L1 必读区 水平 12%~88%,垂直 10%~90% 标题、副标题、描述、标签、代码卡片
L2 出血区 水平 5%~95% 大号装饰符号、辉光、纯色块

4. 字号 = 期望显示字号 × 2,最小 26px

这个是我第二处翻车的地方。桌面显示是源图的 0.5 倍,我在源图上写 22px 的描述文字,页面上就只剩 11px。别问怎么看出来的,问就是眯着眼看了半天。

元素 桌面显示目标 源图字号
主标题 32px 64px(放不下可降到 50px)
副标题 18px 36px
描述正文 13~14px 26~28px
标签 / 代码 13px 26px

5. 中英混排要逐字换字体

Consolas 没有中文字形,把中文字符塞给它只会得到豆腐块。代码类的图里我用了逐字符判断:汉字用微软雅黑,其余用 Consolas。另外不要对文字做横向压缩,等比缩放是底线。

六、三个遮挡坑,一个比一个隐蔽

上面的标准解决了拉伸,接着我碰到了遮挡。三张图都出过问题,记下来省得再犯。

坑一:从底部倒推定位,描述压到标签上

第一版我是先定标签的位置,再往上倒推描述、副标题、标题的 y 坐标。结果描述有两行,最后一行的底部到了 523px,标签胶囊顶部在 504px,重叠了 19px。倒推的算法对行数变化太敏感,多一行就全乱。后来改成了自上而下的流式排版,每块之间强制留至少 8px。

坑二:用 Consolas 量宽度,却用雅黑去画

这个坑最阴。标题里有英文(”OpenWrt + MosDNS + AdGuard”),我折行时按 Consolas 的字符宽度算,画的时候整行都用了雅黑。雅黑的拉丁字母比 Consolas 宽,实测那行标题渲染出来 954px,上限是 640px——直接冲出左栏,压到了右边终端卡片上,重叠 24×20px。

修法很朴素:测量和绘制必须用同一套字体。纯中文的块就用中文字体测量,不要混。

坑三:装饰元素画在了文字上

两个子问题。一是左侧那根强调竖条画在 x=220,标题也从 x=220 起,竖条直接压在标题第一个字上,挪到 x=192 就好。二是扫描线、网格、胶囊的半透明底色,我原来是把它们画在一个覆盖层上,最后才合成到整张图上面,等于给所有文字蒙了一层纱。后来把渲染顺序改成严格的分层:渐变底 → 扫描线 → 网格和辉光 → 装饰符号 → 胶囊底色 → 最后才是文字。

七、让脚本自己找遮挡

看图找遮挡不靠谱,我眼睛骗了我一次。现在生成器里每个文字块都会记下自己的包围盒,出完图跑一遍两两相交检测:

def validate(rects):
    """rects: [(name, x0, y0, x1, y1)] —— 两两相交或越出 L1 即报错"""
    errs = []
    for i in range(len(rects)):
        n0, x0, y0, x1, y1 = rects[i]
        if x0 < L1[0] or x1 > L1[2] or y0 < L1[1] or y1 > L1[3]:
            errs.append(f"[越界] {n0} 超出安全区 {L1}")
        for j in range(i + 1, len(rects)):
            n1, a0, b0, a1, b1 = rects[j]
            if x0 < a1 and a0 < x1 and y0 < b1 and b0 < y1:
                ov_w = min(x1, a1) - max(x0, a0)
                ov_h = min(y1, b1) - max(y0, b0)
                errs.append(f"[遮挡] {n0} x {n1} 重叠 {ov_w:.0f}x{ov_h:.0f}px")
    return errs

跑起来的输出长这样,每个块的坐标都打出来,有问题一眼能定位:

make_slides.py 的自动遮挡校验输出:kicker、title、desc、chip、brand 每个块的包围盒坐标,最后校验 PASS 无遮挡无越界

规则很简单:python make_slides.py 末尾必须是 总计问题: 0,不是 0 就不许上传。这条比我肉眼可靠多了。

八、上线前我核对的四件事

  1. 出图校验问题数为 0;
  2. custom_css 那段生效,getComputedStyle(img).objectFit 是 cover;
  3. CDP 在 1920 / 1440 / 1024 / 820 / 390 五个宽度下量一遍,变形率都接近 0%;
  4. 手机上看一眼关键字没被裁掉。

装完之后

这套标准落地之后,首页轮播在五个断点上都是干净的,字号也能看清楚了。相关几篇:

有别的主题也把轮播高度写死的话,思路是通的:先量,再改渲染,最后才轮到改图。

上一篇 WordPress 插入大图看不清怎么办:多档尺寸怎么选 + 灯箱滚轮缩放改造
下一篇 DoH/DoT 实测:13 家公共加密 DNS 延迟对比,和一个几乎人人填错的地址