Update (2023/10/20): 部署了用 darkreader 生成的暗黑模式 CSS,并且参考 https://sergeyski.com/css-color-scheme-and-iframes-lessons-learned-from-disqus-background-bug/ 修了 Disqus 的问题。

这两天心血来潮,给自己好几个月没动的 blog 加了暗黑模式。起因是昨天在垃圾邮件箱看到了 Orion 浏览器的 Beta 版更新邮件,然后想起来自己当时好像确实填了注册信息,就下载下来试了试,顺便看了看它们的 GitHub organization 里面的东西(虽然浏览器本体不是开源的)。然后就看到有个快速为页面配置暗黑模式的脚本(MIT License):

var odmcss = `
:root {
    filter: invert(90%) hue-rotate(180deg) brightness(100%) contrast(100%);
    background: #fff;
}
iframe, img, image, video, [style*="background-image"] {
    filter: invert() hue-rotate(180deg) brightness(105%) contrast(105%);
}
`;

id="orion-browser-dark-theme";
ee = document.getElementById(id);
if (null != ee) ee.parentNode.removeChild(ee);
else {
  style = document.createElement('style');
  style.type = "text/css";
  style.id = id;
  if (style.styleSheet) style.styleSheet.cssText = odmcss;
  else style.appendChild(document.createTextNode(odmcss));
  document.head.appendChild(style);
}

写 CSS 对我来说是一种折磨,到现在我都还只会用个 Bootstrap,然后在浏览器的开发者工具里瞎调直到样式看起来差不多为止(现在我的 CSS 水平和我高中时候基本一致)。在 @media (prefers-color-scheme: dark) 里面加这么几条样式就能有暗黑模式支持,天下居然有这样的好事。

工作原理

这几行 CSS 的作用就是为网页添加「滤镜」,让它显示成协调的「暗黑模式」的效果。现代桌面操作系统的辅助功能里面一般都可以设置「反色」,但是直接开的话对于大多数人来说效果是没法看的,所以我们需要更复杂的东西。

prefers-color-scheme 需要相对比较新的浏览器,在实践中除非是 IE 用户、Windows XP 的现代浏览器用户,或者是安装之后从不升级浏览器的用户,否则兼容性上不会有问题。

其中的几条特效:

  • invert():反色,完全反色(100%)的情况中对于 RGB 来说运算就是 r = 255 - r; g = 255 - g; b = 255 - b;
  • hue-rotate(180deg):反色完之后颜色肯定不对,所以还需要一次色调(hue)的变换。

想象一下这样一个取色器:

A color picker example

右边的彩虹色条就是调节色调的。把这个条上下相接成一个 360 度的圆环,就能够解释 180deg 这个参数的意义了。我不是专门研究颜色管理的(本文写的东西不排除出现事实错误的可能性),所以下面举几个例子来看看,GIMP 可以点上面 HSV 按钮让取色器显示 HSV(色调、饱和度、亮度)。

  • 纯红 (rgb = 255, 0, 0),反色之后是纯蓝 (rgb = 0, 255, 255),此时色调从 0 变成了 180。色调偏转 180 度之后又变回了原来的颜色。
  • 纯白 (rgb = 255, 255, 255),反色之后是纯黑 (rgb = 0, 0, 0)。对于纯白、纯黑的场景,饱和度为 0,所以色调的色带变成了单色(纯白/纯黑),怎么调都不会改变颜色。
  • 目前博客里用的未访问过链接颜色 (#1abc9c; rgb = 26, 188, 156; hue = 168.1),反色之后大致是较深的粉色 (rgb = 229, 67, 99; hue = 348.1),偏转 180 度后颜色大致是淡蓝色、淡绿色 (rgb = 67, 229, 196.9; hue = 168.1)。
  • 代码块 背景色 (#f8f8f8; rgb = 248, 248, 248; hue = 0),反色之后变成 (rgb = 7, 7, 7; hue = 0)。因为饱和度为 0(似乎 RGB 值相同的颜色的饱和度都是 0),所以调色调也没有变化。
  • 代码字符串量的粉色 (#dd1144; rgb = 221, 17, 68; hue = 345),反色之后变成浅蓝色 (rgb = 34, 238, 187; hue = 165),色调偏转之后变成 (rgb = 238, 34, 85; hue = 345)。

可以发现反色总是会把色调偏转 180 度,所以之后再次偏转可能是出于让色调和原来一致的考虑(但是偏转不会导致颜色恢复成和原来一样)。与此同时,偏白/黑的颜色在反色后调转色调不会导致它们恢复成接近原来的颜色,所以就实现了「暗黑模式」的效果。

但是对于图片等对象来说进行额外的颜色处理是不合适的:会导致颜色出现奇怪的变化。以这只捕捉 Xfce 老鼠的猫猫为例:

xfce cat

(在 GIMP 中)经过完全反色(反相)+ 色调翻转(色相-饱和度)之后,就变成了这样:

darkened xfce cat

所以有第二条规则:对于图片、视频之类的东西(iframe, img, image, video, [style*="background-image"]),把颜色掰回来。理论上上面做的都是可逆变换:再来一次反色 + 色调翻转之后就能恢复到原来的颜色了。

但是实际操作中,浏览器实现的 hue-rotate() 似乎没那么靠谱:hue-rotate(360deg) 可以保持颜色不变,但是两个 hue-rotate(180deg) 叠加起来得到的颜色有一些怪异(例子),不管在 Firefox、Chrome 还是 Safari 里面都是这样。

所以如同 0.1 + 0.2 != 0.3 一样,180 + 180 != 360?搜了一下,看到了 https://stackoverflow.com/questions/19187905/why-doesnt-hue-rotation-by-180deg-and-180deg-yield-the-original-color

In both CSS and SVG filters, there is no conversion into HSV or HSL - the hueRotation shorthands are using a linear matrix approximation in RGB space to perform the hue rotation. This doesn’t conserve saturation or brightness very well for small rotations and highly saturated colors - as you’re seeing.

所以,

@media (prefers-color-scheme: dark) 里面加这么几条样式就能有暗黑模式支持,天下居然有这样的好事。

天下没有这样的好事(悲

emmm 在能容忍 hue-rotate 瞎搞的情况下继续吧(先凑合着用,写完这篇之后看看还是得重新写个不用 filter 的暗黑模式 CSS

Disqus

最开始部署之后发现 Disqus 评论仍然在用深色字体,结果看不清,然后发现这是个 iframe。

Disqus problem in filter dark mode

一开始脑抽了,想那就不要让 Disqus 的 iframe 加把颜色弄回去的 iframe 就行了,结果:

Disqus wrong appearance

emmmm 搜了一下发现 Disqus 会去根据文本颜色判断用浅色还是深色主题,但是 filter 不会更改字体颜色。我尝试过给 #disqus_thread 特别添加相关的 CSS 属性,没有成功。(我后来试了试,事实上 Disqus 就是在胡说八道,就算把整个页面字体都涂白,它还是会用浅色主题)

最后偷懒的办法就是……嗯……给 Disqus 的背景涂白(Disqus 的 iframe 有 allowtransparency="true" 属性,背景是透明的),然后上面加个「Disqus 不支持暗黑模式」这样的提示。上面不太正确的 hue-rotate 一个副作用似乎是让「恢复」之后的元素暗一些,所以应该也不会让在黑暗中开着暗黑模式阅读的人感到瞎眼。

实现很简单:

<p id="comment_darkmode_warning" style="display: none;">Note: Disqus 完整评论组件不以暗黑模式显示。</p>
@media (prefers-color-scheme: dark) {
  iframe[src*="disqus.com"] {
    background-color: #fff;
  }
  #comment_darkmode_warning {
    display: block !important;
  }
}

总结

所以结论是:如果你很懒,或者不在意图片的颜色有那么一些偏移,或者 1h 的 DDL 之后就必须要上线暗黑模式的话,可以试试这种方法。

另外顺便把博客的字体调了一下:

  • 给斜体用上了 font-synthesis: none,这样的话就算在 Safari 里中文也能正常不倾斜了;
  • 加粗了正文字体,原来的正文字体太细了;
  • blockquote 改了英文字体,Baskerville 不知道为什么感觉看起来很怪;
  • body 的主字体首先使用 Web font Bitter,和原本的 Whiteglass 主题一致。

希望之后自己能每个月都写点儿东西。