跳到正文
4033 字
15 分钟
27
6
7

用 HDR 技术让网页里的字更亮

发布于
分类

手机看 HDR 视频时,太阳和灯光会比屏幕上的白色界面更亮,但网页里的字最亮也只能是白色。这篇记录我怎么绕过这个限制,让播放器的歌词在 HDR 屏幕上真的亮起来,以及路上踩的几个坑:唱完高潮句闪出一大块白,Safari 上完全不亮。

这个技术可以把用户眼睛亮瞎(取决于用户的屏幕素质)

先做个屏幕HDR小测试吧:你在下面这张图里面是否能看到文本,且文本比其他地方明显亮很多?(这张图背景和博客背景颜色一样,看不到不是没加载出来,且你正在阅读的这句话文本也是有HDR的,可以点击右上角的主题图标调成深色模式后看)

白色图片里藏着一行只有 HDR 屏幕才看得到的字
这是一张HDR图片

能看到的话,说明你的屏幕支持HDR!

如果用手机看过 HDR 视频的话,应该有这种体验:画面里的太阳、路灯、烟花等光源会比其他地方都亮,甚至有点刺眼。这就是HDR的体验,它能显示比「白色」更亮的东西。

可前端里的文字正常情况下做不到这一点。#fff 就是上限,和页面背景、按钮里的白色一样亮。

我最近在做博客的播放器的特效歌词,想着前端既然可以展示HDR图片,那能不能也用这个机制+一些CSS技巧,来实现HDR文字效果?理论存在,实践开始~。

文章里面其他截图看不出来HDR

除了开头那张,文中其他图都是普通截图,HDR的亮度截不出来。不过可以用支持HDR的屏幕体验一下我的播放器

白色为什么不是最亮的?

可以拿白纸和白炽灯来比。白纸再白,也只是把照到它的光反射出来;灯是自己发光,能比白纸亮好几倍。

普通网页里所有的颜色都是「白纸」,最亮就是 #fff,基本取决于屏幕和你的色彩配置,大概是 203 nits。HDR 屏幕能显示的「灯泡」可以到 1000 多 nits,但浏览器只允许图片和视频去用这部分亮度。

CSS 也没有办法写出比白更亮的颜色。新的 oklch()、display-p3 能让红更红、绿更绿,但亮度上限还是纯白色。能写「比白更亮」的语法还在相关草案里,主流浏览器都还没支持。

换个思路:能不能让图片替字发光?

既然图片可以亮,那就让图片来当字的颜色。

HDR 图片和普通图片的区别,在于它多带了一张「说明书」,告诉浏览器:我这里的像素不是普通的白,是 1000 nits 的亮光。浏览器读到这个说明,就会让屏幕把这块区域调亮。手机相册里那些「HDR」照片就是这么回事,开头那张图也是。

所以思路是:做一张很亮的纯色图片,把它铺在文字后面,再用 CSS 按字的形状裁一下,只留下字所在的部分。效果就是「用亮光写出来的字」,有点类似视频剪辑的蒙版。

.lyric-word {
  background-image: url("亮光图片.png"); /* 一张很亮的纯色图 */
  background-clip: text;  /* 只保留字形那一块 */
  color: transparent;     /* 字本身的颜色去掉,露出背景 */
}

这张亮光图片并没有做成图片资源,是浏览器里现场生成的。因为我们需要不同亮度,不同颜色的图片,单纯预制图片资源可能不是很灵活,就像你买彩笔,选了24色就不能画其他颜色。这是我们生产HDR资源的函数示例:

// 1. 把想要的亮度换算成 HDR 的编码值(PQ 曲线,HDR10 视频用的就是它)
function pq(nits: number) {
  const m1 = 2610 / 16384, m2 = 2523 / 32
  const c1 = 3424 / 4096, c2 = 2413 / 128, c3 = 2392 / 128
  const y = Math.min(1, nits / 10000) ** m1
  return ((c1 + c2 * y) / (1 + c3 * y)) ** m2
}

// 2. 在 PNG 里加一个 cICP 块,这就是那张「说明书」:
//    告诉浏览器这张图用的是 HDR 的色彩空间和亮度曲线
chunk('cICP', [9, 16, 0, 1])

在播放器里面亮度是这样定的:

  • 普通歌词:4倍白色亮度

  • 特效歌词:更亮一点

  • 点题歌词:20倍亮度,亮瞎用户眼睛(取决于屏幕上限)

点题句:神的随波逐流

粒子:先在草稿纸上画,再贴到会发光的屏幕上

歌词后面还有一层下雨、下雪、飘花瓣的粒子动画,是用 canvas 画的。canvas 的问题更直接:浏览器根本不让普通的 canvas 输出 HDR,设置了也不生效。

能输出 HDR 的是 WebGPU 的 canvas。但要用 WebGPU,就得把十几种粒子动画用着色器重写一遍,工作量太大。

后来我找到一个取巧的办法:粒子照旧用原来的代码画,只是画在一张看不见的「草稿纸」上。这张草稿纸支持比白更亮的颜色,只是自己显示不出来。然后每一帧把草稿纸上的内容整张复制到 WebGPU 的 canvas 上,由它负责显示。

// 页面上负责显示的 canvas:用 WebGPU,并打开 HDR 输出
gpu.configure({
  device,
  format: 'rgba16float',
  toneMapping: { mode: 'extended' }, // 关键:允许超过白色的亮度
  alphaMode: 'premultiplied',
})

// 看不见的草稿纸:普通 2D canvas,但能存下比白更亮的颜色
const draw = document.createElement('canvas').getContext('2d', { colorType: 'float16' })!
draw.fillStyle = 'color(srgb-linear 3 3 3)' // 白色的 3 倍亮

// 每一帧:把草稿纸整张复制过去
device.queue.copyExternalImageToTexture(
  { source: draw.canvas },
  { texture: gpu.getCurrentTexture() },
  [draw.canvas.width, draw.canvas.height],
)

两条路线:文字用图片,粒子用 WebGPU

性能上我原本担心会卡,实测开不开 HDR 都是 120 帧。真正的代价是显存,这种能存「超亮颜色」的画布比普通画布大一倍。所以我加了两个省显存的处理:没有粒子的时候把画布整个释放掉,有粒子时再建;画布本身用一半的分辨率,粒子都是软软的光点,看不出区别。

踩过的坑

唱完高潮句,闪出一大块白

这个问题折腾最久。每次最点题的那句唱完,歌词的位置会突然闪出一个白色方块,在 HDR 屏幕上亮得晃眼。

截图拍不到,静态页面也复现不出来,我一度以为是浏览器的 bug。最后发现是自己的 CSS 写漏了。

前面说过,字变亮要同时满足两件事:铺上亮光图片,并且按字形裁剪。可以把它们想成两个开关。我给「正在唱」的歌词写了一条规则,两个开关都打开;又给「高潮句」写了一条规则,让它的亮光更亮,但只打开了「铺亮光」,没打开「按字形裁剪」:

/* 正在唱:铺亮光 + 按字形裁剪,两个开关都开 */
.line--active .word { background-image: …; background-clip: text; color: transparent; }

/* 高潮句:只换了更亮的图片,没有裁剪 */
.line--spot .word { background-image: var(--hdr-spot); }

平时这两条规则是同时生效的,所以没问题。但追光灯在歌词唱完之后还要慢慢暗下去,大概有一秒多。这一秒里,这句歌词已经不算「正在唱」了,却还是「高潮句」。于是亮光图片铺上了,裁剪却没了,整个字的方框都被涂成了 20 倍亮的白色。

修起来就一行:把高潮句的规则改成「正在唱 并且 是高潮句」才生效。教训是,铺亮光和按字形裁剪必须永远写在同一条规则里,只铺不裁一定会出事。

Safari 上完全不亮

Chrome 上一切正常,换到 Safari 一点效果都没有。可 Safari 从 26 版开始明明支持 HDR 图片。

我做了个对照页,把同一张亮光图片换着法子显示:用 <img> 标签放一张 64×64 的,亮了;放一张 1×1 再拉大的,不亮;当背景图、用来裁剪文字,都不亮。再把图片换成 2×2、8×8,全都亮了。

问题出在尺寸上。我为了省事,亮光图片一直用的是 1×1,只有一个像素,反正它是纯色。而 Safari 看到只有一个像素的图片,会直接把它当成一个颜色去涂,不再按图片来画。这就像本来要贴一张会发光的贴纸,它却照着贴纸的颜色拿普通颜料刷了一遍,发光的部分自然就没了。

改成 2×2 就好了,图片也只大了几十个字节。

Safari:1×1 和 2×2 的区别

不过 Safari 上的粒子就没办法了。它的 WebGPU 会接受「打开 HDR」这个设置,查询的时候也说已经打开了,可实际显示出来还是普通亮度。这个只能等 Safari 后续修。

开头那张图,也折腾了三版

本来以为开头那张演示图最简单:白色背景,上面写一行更亮的字就行。结果做了三版。

第一版,背景用普通白色的亮度,字用 HDR 高亮。在普通屏幕上一看,背景居然是灰的,字反而是白的,效果完全反了。原来浏览器在普通屏幕上显示 HDR 图片时,会把整张图的亮度按比例压下来,好给高光留位置,普通白就被压成了灰色。我拿一条从暗到亮的渐变图测了一下,Chrome 要到 1000 nits 以上才会显示成纯白。

第二版,把背景也提到 1000 nits。普通屏幕上终于是一片白了,可到了 HDR 屏幕上,字亮一下就暗下去了。整张图一大片都是高亮,系统为了控制功耗,会把大面积的高亮压下来。

第三版,背景干脆做成透明的,直接露出网页本来的白,只有字是高亮。普通屏幕上,字被压成白色,正好和白色的页面融为一体;HDR 屏幕上只有一行字在发光,面积小,系统也不会去压它,终于稳定了。

带特效的字直接消失了

歌词里的关键词会一个字一个字地放大、抖动。为了做这些动画,每个字都被拆成了独立的小方块。

一开始我图省事,在整个词上设置亮光和裁剪,结果这些单独动起来的字完全不认父元素的背景,字直接消失了,只剩外面一圈光晕。后来改成给每个字单独设置亮光图片,才恢复正常。

一些小事

  • 测试的时候别把系统亮度开满。macOS 屏幕越亮,留给 HDR 的余量越少,开到最亮时几乎看不出区别。

  • 斜体字往右倾斜的那部分超出了字的方框,会被裁剪切掉一截。给每个字左右留一点空白就好。

  • 截图里永远看不到 HDR,判断效果只能靠眼睛看。调试的时候我是一帧一帧地检查元素的样式,看它的状态对不对。

代码都在 neo-blog 里,主要是 web/src/lib/hdr-color.ts 和 web/src/lib/hdr-canvas.ts。有 HDR 屏幕的话,可以去播放器页挑首歌听听,等到高潮那句喵~

用 HDR 技术让网页里的字更亮
发布于
2026/11/07
许可协议
CC BY-NC-SA 4.0

除非另有说明,本站文章许可协议都是 CC BY-NC-SA 4.0,转载请注明出处。