
我叫弗拉基米尔(Vladimir),我为Yandex Mail开发移动前端。 我们的应用有一个黑暗的主题已有一段时间了,但是它并不完整:只有界面和普通电子邮件是黑暗的。 具有自定义格式的消息仍然保持明亮状态,并在黑暗的界面上脱颖而出,在夜间伤害了用户的眼睛。
今天,我将告诉您我们如何解决此问题。 您将了解两种对我们不起作用的简单技术,以及最终解决问题的方法-自适应页面重新着色。 我还将分享一些有关使图像适应深色主题的想法。 公平地讲,使用自定义CSS来使页面变暗是一项非常特殊的任务,但是我相信有些人会发现我们的经验会有所帮助。
简单的方法
在采用神奇的颜色弯曲技术之前,我们尝试了两个非常基本的选项:对元素应用其他深色样式或CSS过滤器。 这两种方法都不适合我们,但在其他情况下它们可能更适合(简单很酷,对吧?)。
覆盖样式
这是一个非常简单的选项,从逻辑上扩展了应用程序自己的深色CSS主题。 您只需将多余的深色样式放在电子邮件容器上(或通常在容器上,使用户生成的内容变暗):
.message--dark { background-color: black; color: white; }
但是,电子邮件中元素的任何样式都将覆盖我们的根样式。 不!important
在这里无济于事。 通过阻止继承,可以使这一想法更进一步:
.message--dark * { background-color: black !important; color: white !important; border-color: #333 !important; }
在这种情况下,您不能没有!important
,因为选择器本身不是很具体。 这也会碰巧覆盖大多数内联样式(具有inline !important
样式仍然可以找到它们的方式,您对此无能为力)。
我们的风格渴望将所有颜色都涂成相同的颜色,从而带来了另一个问题。 设计师有可能要用他们安排颜色的方式说些什么(您知道,优先级,配对,所有设计师的东西),而我们只是采纳了他们的想法并将其扔到了窗外。 这不是一件好事。

如果您不像我那样尊重设计师,并决定采用这种方法,请不要忘记处理不太明显的事情:
box-shadow
。 但是,您将只能覆盖颜色。 要么完全消除阴影,要么与浅色的阴影和睦。- 语义元素的颜色,例如链接或输入。
- 内联SVG。 使用
fill
代替background
,而使用stroke
代替color
,但是您无法确定,这很可能是相反的。
从技术角度来看,这是一个可靠的方法:它需要三行代码(好的,三十行用于生产就绪版本,并考虑了所有的情况),它与世界上所有的浏览器兼容,可以在现成的动态页面上运行,而CSS附加到文档的方式则完全不相关。 还有一个不错的好处:您可以轻松地调整样式中的颜色以匹配应用程序的主要颜色(例如,将背景设置为#bbbbb8
而不是黑色)。
顺便说一下,这就是我们以前使电子邮件变暗的方式。 如果电子邮件有自己的样式,我们会放弃并保留它以防万一。
使用CSS过滤器
这是一种聪明而优雅的方法。 您可以使用CSS过滤器使页面变暗:
.message--dark { filter: invert(100) hue-rotate(180deg); }
图像变得令人毛骨悚然,但我们可以轻松地解决此问题:
.message-dark img { filter: invert(100) hue-rotate(180deg); }

但是,这不能解决通过background属性附加内容图像的问题(当然,它对于调整宽高比很方便,但是语义呢?)。 好的,假设我们可以找到所有这些元素,对其进行显式标记,并将它们的颜色也改回来。
这种方法的好处是可以保留原始的亮度和对比度平衡。 另一方面,问题多于优势:

- 深色页面变亮。
- 您无法控制最终颜色。 您将什么滤镜应用于背景,以使其与您的品牌
#bbbbb8
相匹配? 那是一个真正的头抓。 - 双重反转使图像褪色。
- 性能低下,特别是在移动设备上-这是有意义的,浏览器在每次绘制时都运行傅立叶变换,而不是简单地渲染页面。
此方法适用于带有中性色文本的电子邮件,但我从未见过任何收件箱中包含此类内容的人。 另一方面,过滤器是使元素变暗而不访问其内容(框架,Web组件或图像)的唯一方法。
自适应深色主题
现在是魔术的时候了! 基于前两种方法的缺点,我们列出了应注意的事项:
- 使背景变暗,文本变浅,并且边框介于两者之间。
- 检测深色页面并保留其颜色。
- 保持亮度和对比度的原始平衡。
- 控制结果颜色。
- 保持色调不变。
因此,您需要更改样式的颜色以使背景变暗。 从字面上看,为什么不这样做呢? 选取所有样式,提取与颜色相关的规则( color
, background
, border
, box-shadow
及其子属性),然后将其替换为“变暗”版本:变暗背景和边框(但不如背景),淡化文字等。
这种方法具有令人难以置信的优势,它将温暖任何开发人员的心。 您可以为每个属性配置颜色转换规则-是的,使用代码! 稍微想像一下,您就可以将颜色适合任何外部主题,执行所需的任何颜色校正(例如,执行浅主题而不是深色主题,或者您喜欢的任何颜色的主题),甚至添加一点点的上下文相关性-例如,以不同的方式对待宽窄的边界。
缺点是您希望采用“一切都在js”方法。 我们运行脚本,弄乱样式封装,并使用正则表达式解析CSS。 但是,后者并没有像HTML那样令人羞辱,因为CSS语法是常规的(至少在我们正在使用的级别上)。
这是我们的黑暗计划:
- 规范旧式样式属性(
bgcolor
和其他样式属性)并将其移入style="..."
。 - 查找所有内联样式。
- 查找每种样式中所有与颜色相关的规则(
background-color
, color
, box-shadow
等)。 - 从与颜色相关的属性中获取颜色,并将其与正确的转换器匹配(使背景变暗,使文本变亮)。
- 致电转换器。
- 将转换后的属性放回CSS中。
包装器代码-规范化,定位样式,解析-非常简单。 让我们看看魔术转换器的工作原理。
HSL转换
使颜色变暗并不像听起来那么简单,特别是如果您想保留色相(例如,将蓝色变成深蓝色而不是橙色)时,尤其如此。 您可以在普通RGB中执行此操作,但这是有问题的。 如果您从事算法设计,您会知道在RGB中,即使渐变也会被破坏。 同时,在HSL中使用颜色是一种真正的享受:除了不知道要使用的红色,绿色和蓝色外,还具有以下三个通道:
- 色相-我们想要保留的东西。
- 饱和度,这对我们现在不是很重要。
- 轻便,我们将改变。
您可以将其视为圆柱体。 我们需要将这个圆柱体颠倒过来。 颜色校正的功能如下: (h, s, l) => [h, s, 1-l]
。
已经可以的颜色
有时我们很幸运,电子邮件(或其一部分)的自定义设计已经很暗了。 这意味着我们不需要进行任何更改-设计师在选择颜色方面可能做得比我们的算法梦dream以求的要好。 在HSL中,您只需要考虑L-亮度。 如果其值高于或低于阈值(分别用于文本和背景)(可以设置),则不要理会它。
动态马戏团
即使我们不需要这些(非常感谢,消毒剂,您在这里节省了我的时间),我也会列出自适应主题需要使黑暗的整个页面变暗的其他功能,而不仅仅是90年代愚蠢的静态电子邮件。 公平警告:此任务并不适合所有人。
动态内联样式
更改内联样式是最简单的情况,它会使我们的深色页面混乱。 这很常见,但是有一个简单的解决方法:添加MutationObserver
并在发生更改后立即修复内联样式。
外部风格
由于异步性和@import
语句,使用页面内部的<link>
元素引用的样式可能会很@import
。 而且CORS并不能使事情变得更好。 看起来这个问题可以通过网络工作者很好地解决(代理所有*.css
)。
动态风格
更糟的是,脚本可以根据需要添加,删除或重新排列<style>
和<link>
元素,甚至可以更改<style>
的规则。 在这里,您还需要使用MutationObserver
,但是每次更改都会带来更多处理。
CSS变量
CSS变量发挥作用时,事情变得非常疯狂。 您无法使变量变暗:即使您猜测某个变量包含基于其格式的颜色(尽管我会建议这样做),您仍然不知道其作用是-背景,文本,边框或全部一次? 此外,CSS变量的值是继承的,因此,您不仅需要跟踪样式,还需要跟踪应用于样式的元素,这很快就会失去控制。
一旦CSS变量成为主流,我们就会遇到麻烦。 另一方面,届时将支持color()
,从而使我们可以将所有JS转换替换为color(var(--bg) lightness(-50%))
。
总结

在我们的案例中,当消毒剂仅保留内联样式时,在CSS级别上的自适应变暗效果就像一个魅力:它提供了更高质量的变暗效果,不会破坏原始结构,并且相对简单快捷。 但是,将其扩展为处理动态页面可能不值得这样做。 幸运的是,如果您使用的是用户生成的内容,而没有开发浏览器,则消毒剂的工作方式可能相同。
在实践中,自适应模式应与样式替代配对,因为<input>
或<a>
类的标准元素通常使用默认的灯光样式。
如何使图像变暗
图像变暗是另一个困扰我个人的问题。 这具有挑战性,这给了我一个借口,终于可以使用“光谱分析”这一短语。 深色主题中的图像存在几个典型问题。
有些图像太亮。 这些给我们带来了麻烦,就像我们开始寻求帮助时发送的明亮电子邮件一样。 这个问题通常出现在普通照片中,但并非唯一。 由于编写时事通讯CSS并不有趣,因此有些人喜欢通过插入复杂的布局作为图像来跳过角落,以防止变暗。 当我看到这一点时,我内心的完美主义者沮丧地rust吟着。 除非您要吓users用户,否则这些图像应变暗,但不能倒置。

然后是具有真实透明度的深色图像。 这是徽标的典型问题-设计徽标时要考虑浅色背景,当我们将其替换为深色背景时,徽标就会消失在徽标中。 这些图像需要反转。

介于两者之间的是使用白色背景的图像,应该代表透明度。 在黑暗的主题中,它们只是以怪异的白色三角形结尾。 在理想的情况下,我们可以将白色背景更改为透明背景,但是如果您曾经使用过魔术棒工具,您就会知道自动执行此操作有多么困难。

有趣的是,某些图像毫无意义。 这些通常是特别怪异的布局中的跟踪像素或“格式持有者”。 您可以安全地使它们不可见(例如, opacity: 0
)。

分析里面的东西
为了弄清楚如何针对深色主题调整图像,我们需要查看内部并分析其内容,最好使用一种快速简单的方法。 这是解决此问题的算法的第一个版本。
首先计算图像的暗,亮和透明像素。 作为一个明显的优化,可以考虑仅一小部分像素。 接下来,确定图像的整体亮度(亮,暗或中等),以及是否有透明度。 反转具有透明度的暗图像,使不透明的暗图像变暗,其余部分保持不变。
直到我遇到一本慈善通讯,上面有一所非洲学校的课程照片,我对此感到非常高兴。 设计师通过在侧面添加透明像素来使其居中。 我们不想参与有关攻击性图像识别的故事,因此我们决定将图像处理排除在第一次迭代之外。
将来,我们可以通过使用我称之为“频谱分析”的启发式方法来避免此类问题。 也就是说,计算图像中不同色相的数量,并且仅在色相不太多时才反转。 您可以使用相同的方法来识别明亮的粗略图像并将其反转。

结论
要设计一个完整的深色主题,我们必须找到一种使包含自定义格式的电子邮件变暗的方法,而我们做到了。 首先,我们尝试了两个简单的纯CSS解决方案-覆盖样式和使用CSS过滤器。 他们俩都没有达到目标:第一个在原始设计上太过刻苦,而另一个则表现不佳。 最后,我们决定使用自适应变暗。 我们将样式拆开,替换颜色,然后将它们放回原处。 我们目前正在努力将深色主题应用于图像。 为此,我们需要分析它们的内容,然后仅调整其中一些。
如果您需要更改自定义HTML代码段的颜色以使其适合深色主题,请记住以下三种方法:
- 覆盖样式。 这是一个没有大惊小怪的方法,而且无论如何您都需要使用它。 坏消息是它破坏了所有原始颜色。
- CSS过滤器。 这很有趣,但是还有很多不足之处。 将其保留给不容易访问的元素(例如框架或Web组件)。
- 样式重写。 此方法在变暗方面做得很好,但更为复杂。
即使您从未尝试过任何一种方法,也希望您度过了愉快的时光!