likes
comments
collection
share

一次有点细节的自定义主题色实践

作者站长头像
站长
· 阅读数 17

前言

在项目开发中,基本都会接触到自定义主题色设置的需求,可能在C端中比较常见。自定义主题色设置的功能,能够适应更多受访者的场景。由于平时在网上浏览过不少网页换肤的解决方案,一开始不以为然,直到实际开发的时候,才发现有些场景并不简单。随着问题深挖,打开了神奇的新大陆,了解到色板生成算法的原理。

思路

和大多数的换肤解决方案一样,选择使用CSS变量作为自定义主题色方案。CSS变量因为是原生支持,无需依赖额外的工具或库。而且具有运行时动态性,这意味着可以在运行时根据需要更改变量的值,这使得在响应式设计和主题切换等场景下非常灵活。

声明CSS变量:

:root {
  --theme-color: rgba(62, 111, 255, 1);
}

使用CSS变量:

.container {
  background: var(--theme-color);
}

动态操作CSS变量:

// 设置CSS变量
document.documentElement.style.setProperty('--theme-color', 'rgba(255, 255, 255, 1)')
// 获取CSS变量
document.documentElement.style.getPropertyValue('--theme-color')

看起来还挺简单的,只需要把用到主题色的地方都用CSS变量替换,在进入网页初始化时获取主题色数据,动态设置变量就可以达到换肤的效果。但一切并没有这么一帆风顺...

遇到的问题

前面是比较理想的情况,只需要用到一种主题色。但在某些场景下,我们可能需要用到同色系的其他色彩。举个例子,下面有两个按钮,分别是两种状态。

一次有点细节的自定义主题色实践

第一种是激活态,使用主题色。

一次有点细节的自定义主题色实践

第二种是未激活态,是基于主题色更淡些的颜色。意味着需要构建一套颜色系统,提供给不同场景使用。

初步方案 - 基于 Alpha 通道生成

实践

我想大多数人的第一反应都是使用透明度生成吧,我这么想的。

import { TinyColor } from '@ctrl/tinycolor'

// 基于主题色透明度递减
const baseColor = document.documentElement.style.getPropertyValue('--theme-color')
const {r, g, b, a} = new TinyColor(baseColor).toRgb()
const percentArr = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
percentArr.forEach((percent, i) => {
	const value = `rgba(${r}, ${g}, ${b}, ${(a * percent).toFixed(2)})`
	document.documentElement.style.setProperty(`--theme-color-a${i + 1}`, value)
})

首先通过document.documentElement.style.getPropertyValue获取主题色,使用 TinyColor 库调用.toRgb()方法将主题色转换为 RGBA 格式。这将返回一个包含红色(r)、绿色(g)、蓝色(b)和透明度(a)的对象。

接下来,定义一个包含九个不同百分比值的数组percentArr,循环遍历计算出新的透明度值。在循环内部,通过将基准透明度a乘以当前百分比值percent

最后,通过document.documentElement.style.setProperty将新的颜色值作为 CSS 自定义属性应用到文档根元素上,属性名称为--theme-color-a1--theme-color-a9,其中索引i加1表示第几个百分比值。

通过这段代码,可以创建一系列基于主题色的不同透明度的颜色。

but,经过实践,缺点很快就出现了...

缺点

  1. 由于有透明度,当设置了网页背景色时,会透出背景色叠加显示。在背景色比较明显的情况下,效果会很差,达不到预期目标。
  2. 透明度只能递减,有些场景需要比主题色更深些的颜色时会无能为力。

总体看下来,这种方案的扩展性和可用性都不好。

思考

放弃基于透明度生成的方案,既然不能基于透明度构建,只能是始终保持透明度为 1,通过 rgb 不同的组合来生成基于主题色减淡和加深的一系列颜色,以下我们称一系列的颜色为**“色板”**。在这种方案下,则需要一种算法来计算出色板。

一次有点细节的自定义主题色实践

优化方案 - 基于 HSV 模型生成

HSV 模型是什么

HSV 模型是一种描述颜色的模型,它使用三个参数来表示颜色:色调(Hue)、饱和度(Saturation)和明度(Value)。

  • 色调(Hue):表示颜色在色谱中的位置。它的取值范围是0到360度,其中0和360表示红色,120表示绿色,240表示蓝色等。色调参数决定了颜色的基本属性。
  • 饱和度(Saturation):表示颜色的纯度或饱和度。它的取值范围是0到100%,其中0%表示灰色,100%表示最鲜艳的颜色。饱和度参数决定了颜色的鲜艳程度。
  • 明度(Value):表示颜色的明亮度。它的取值范围是0到100%,其中0%表示黑色,100%表示最亮的颜色。明度参数决定了颜色的明亮或暗的程度。

对比 RGBA 模型有什么区别

RGBA 模型常用于计算机图形学、屏幕显示和图像编码等领域,因为它可以精确表示颜色的数值和透明度,方便进行计算和图像合成。

HSV 模型常用于图像处理、计算机图形学和设计领域,因为它更符合人类对颜色的感知和理解,方便进行颜色调节和变换。

总的来说,HSV 模型相对于RGBA模型来说,更直观地表示了颜色的感知属性。通过调整色调、饱和度和明度这三个参数,可以实现颜色的调节和变换,例如改变颜色的鲜艳度、明亮度或色调等,而不需要直接操作RGB的数值。HSV 模型在图像处理、计算机图形学和设计领域中广泛应用,因为它更符合人类对颜色的感知和理解。

使用 Ant Design 色板算法

简述

在面向搜索引擎编程后,找到一篇梳理 Ant Design 色板生成算法的文章 Ant Design 色板生成算法演进之路。色板生成算法的最终方案就是基于 HSV 模型生成,通过对每个维度不同的计算返回色板。

总结基于 HSV 模型生成色板算法的思路如下:

  1. 色相(Hue)渐变:根据 Hue 色相的大小判断冷暖色调,而不再依赖于 RGB 中红绿蓝通道的大小关系。在冷暖色调的减淡与加深过程中,进行不同的处理,使得冷色调减淡时变亮的同时色相更暖,更符合人们对色彩的认知。
  2. 饱和度(Saturation)渐变:减淡与加深过程中,对于饱和度进行不同的处理。减淡过程中饱和度递减的值较大,使得饱和度快速下降。而加深过程中,饱和度不必增长过快,特别是在最深的颜色上进行了特殊处理,使得相邻的颜色饱和度相差不大。
  3. 明度(Value)渐变:减淡与加深过程中,对于明度进行不同的处理。加深过程中明度递减的值较大,使得明度迅速下降。减淡过程中,明度不宜增长过多,尤其是对于主色来说,由于其明度一般较高,因此明度的增长较为适度。

使用

这个算法也有独立开源出来,npm 下载 @ant-design/colors 即可。

import { generate } from '@ant-design/colors'

const colorPalette = generate(color)
const themeColorVars = ['light5', 'light4', 'light3', 'light2', 'light1', 'base', 'dark1', 'dark2', 'dark3', 'dark4']
// 基于主题色生成的调色板
themeColorVars.forEach((key, i) => {
	document.documentElement.style.setProperty(`--theme-color-${key}`, colorPalette[i])
})

generate 返回一个色板,数组内是 10 个由浅到深的颜色,其中第六个是主色。后面是基于色板做 CSS变量的设置。

最后

本文聊了下我在做自定义主题色的思考过程,从透明度的方案到基于HSV生成的方案,以及了解了 HSV 模型和 Ant Design 色板算法的大概思路。对于一开始的透明度方案依然保留,因为确实也有一定的应用场景。

不知道小伙伴在做这方面需求时有没有一些小细节,欢迎评论区分享交流~