这个技术可以把用户眼睛亮瞎(取决于用户的屏幕素质)
先做个屏幕HDR小测试吧:你在下面这张图里面是否能看到文本,且文本比其他地方明显亮很多?(这张图背景和博客背景颜色一样,看不到不是没加载出来,且你正在阅读的这句话文本也是有HDR的,可以点击右上角的和调成深色模式后看)
能看到的话,说明你的屏幕支持HDR!
如果用手机看过 HDR 视频的话,应该有这种体验:画面里的太阳、路灯、烟花等光源会比其他地方都亮,甚至有点刺眼。这就是HDR的体验,它能显示比「白色」更亮的东西。
可前端里的文字正常情况下做不到这一点。#fff 就是上限,和页面背景、按钮里的白色一样亮。
我最近在做博客的播放器的特效歌词,想着前端既然可以展示HDR图片,那能不能也用这个机制+一些CSS技巧,来实现HDR文字效果?理论存在,实践开始~。
文章里面其他截图看不出来HDR
除了开头那张,文中其他图都是普通截图,HDR的亮度截不出来。不过可以用支持HDR的屏幕体验一下我的播放器
白色为什么不是最亮的?
可以拿白纸和白炽灯来比。白纸再白,也只是把照到它的光反射出来;灯是自己发光,能比白纸亮好几倍。
普通网页里所有的颜色都是「白纸」,最亮就是 #fff,基本取决于屏幕和你的色彩配置,大概是 203 nits。HDR 屏幕能显示的「灯泡」可以到 1000 多 nits,但浏览器只允许图片和视频去用这部分亮度。
CSS 也没有办法写出比白更亮的颜色。新的 oklch()、display-p3 能让红更红、绿更绿,但亮度上限还是纯白色。能写「比白更亮」的语法还在相关草案里,主流浏览器都还没支持。
既然前端支持HDR图片,能不能让图片替文字发光?
既然图片可以亮,那就让图片来当文字的光源。
HDR 图片相对于普通图片会在元数据多一些标记,告诉渲染器:这里的像素不是普通的白色,是 1000 nits 的亮光。浏览器读到这个说明,就会让屏幕把这块区域调亮。手机相册里那些「HDR」照片就是这么回事,开头那张图也是。
所以思路有了:做一张很亮的纯色图片,把它铺在文字后面,再用 CSS 按字的形状裁一下,只留下字所在的部分。效果就是「用亮光写出来的字」,有点类似视频剪辑的蒙版,有点像玩剪纸,在上面剪出字形空洞,然后覆盖到一块发光背板上。
.lyric-word {
background-image: url("亮光图片.png"); /* 一张很亮的纯色图 */
background-clip: text; /* 只保留字形那一块 */
color: transparent; /* 字本身的颜色去掉,露出背景 */
}
这里CSS仅供演示,实际上我们并没有把这个保存为一张图片文件,而是根据歌词的亮度和颜色实时生成HDR图片,来满足我们的灵活调色需求。
这是我们生生成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倍亮度
特效歌词:5倍亮度
点题歌词:20倍亮度,亮瞎用户眼睛(取决于屏幕上限)
粒子HDR实现
歌词后面还有一层下雨、下雪、飘花瓣等粒子动画,这个是用 canvas 画的。但是canvas 的问题更直接:浏览器不支持普通 canvas HDR,设置了也不生效,也许我们需要换一种思路了。
了解到:WebGPU 的 canvas可以支持HDR。但用 WebGPU代价是得把十几种粒子动画用着色器重写一遍,工作量十分甚至九分大。
后来Claude帮我找到一个取巧的办法:粒子依旧用原来的代码绘制,只是画在一张看不见的「草稿纸」上。这张草稿纸支持比白更亮的颜色,只是自己显示不出来。然后每一帧把草稿纸上的内容整张复制到 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],
)
在性能上,我原本担心会很卡,实测在我的MacBook Pro M5上,开不开 HDR 都是 120 帧。真正的代价是显存占用,这种能存「超亮颜色」的画布比普通画布大一倍。所以Claude帮我加了两个优化显存的处理:没有粒子的时候把画布整个释放掉,有粒子时再创建;画布本身用一半的分辨率,粒子都是软软的光点,看不出区别。(当然可能在一些性能比较低的设备上会很卡,所以我贴心地加了HDR和特效歌词开关)
踩过的坑:
唱完高潮句,闪出一大块白色的HDR背景
前文说过,歌词变亮要同时满足两件事:铺上亮光图片,并且按字形裁剪。可以把它们想成两个开关。我给「正在唱」的歌词写了一条规则,两个开关都打开;又给「高潮句」写了一条规则,让它的亮光更亮,但只打开了「铺亮光」,没打开「按字形裁剪」,于是直接把底板HDR图片给渲染出来了:
/* 正在唱:铺亮光 + 按字形裁剪,两个开关都开 */
.line--active .word { background-image: …; background-clip: text; color: transparent; }
/* 高潮句:只换了更亮的图片,没有裁剪 */
.line--spot .word { background-image: var(--hdr-spot); }
正常情况下这两条规则是同时生效的,所以没问题。但追光灯在歌词唱完之后还要慢慢暗下去,大概有一秒多。这一秒里,这句歌词已经唱完了,但是「高潮句」的特效还处于消散阶段。于是只有HDR背景而没有去裁剪,整个字的矩形区域都被变成了 20 倍亮的神秘白色矩形块。
Safari 上 1×1 的像素无法使用 HDR
事情起因是苹果在2006年引入了这个问题的开端,对所有1×1的图片按照普通SDR纯色进行渲染,2025上半年WebKit引入了HDR支持,但是HDR部分代码在这个if 1×1 then 纯色优化的分支之后!!!导致1×1的图片跳过了HDR渲染。
最初我的 HDR 歌词在 Chromium 内核的浏览器上一切正常,换到 Safari 一点效果都没有。Safari 从 26 开始支持 HDR 图片,所以只能先做对照测试:用来当蒙版的那张 1×1 HDR 背景图在 Safari 上是普通色域,换成 2×2 立刻就亮了。当时只能骂一句 WebKit(之前在 39-ai 实习时也被坑过几次,比如矢量图被当成位图渲染、放大就糊)。
使用AI检索webkit源代码,发现是一条 2016 年就有的老优化:恰好 1×1 且单帧的图片会被当成纯色,直接用一次 fillRect 画掉。而这条捷径排在 HDR 绘制分支前面,也没判断图里有没有超过白色的亮度。
判据写死在 Source/WebCore/loader/images/bitmap/ImageSource.cpp:51,
size() == IntSize(1, 1) && frameCount() == 1绘制入口里捷径在前、HDR 分支在后:Source/WebCore/loader/images/bitmap/BitmapImage.cpp:151
那个「纯色」是从 8-bit sRGB 的上下文里量出来的,超过白色的亮度在这里就被夹掉了:Source/WebCore/platform/graphics/cg/NativeImageCG.cpp:498
取到颜色之后也只是普通的矩形填充:Source/WebCore/loader/images/Image.cpp:332
2×2 以上才走真正的绘制,HDR 亮度到这一步才交给 CoreGraphics:Source/WebCore/platform/graphics/cg/GraphicsContextCG.cpp:548
所以判据是「恰好 1×1」而不是「小尺寸」,2×2 就恢复正常。这条捷径还会顺手忽略 gain map。我在 WebKit Bugzilla 里没搜到对应的报告,看起来是 2025 年 HDR 支持进来的时候,没人回头给这条老分支加捷径。
本次改动所有代码都在 neo-blog 里,主要在 web/src/lib/hdr-color.ts 和 web/src/lib/hdr-canvas.ts。
如果有 HDR 屏幕的话,可以去播放器页挑首歌听听,然后等到高潮那句看看效果喵~