一闽坡、背景
前段時間在公司項目中推進全局換膚之后栽惶,發(fā)現有個后遺癥。項目中存在大量硬編碼的顏色值
疾嗅,導致部分場景無法達到動態(tài)換膚的效果外厂,需要把這些顏色值全部找出來并替換成變量。再加上公司正在風風火火的實行UI規(guī)范大一統(tǒng)宪迟,對這個問題有迫切的解決需求酣衷。然而公司中現存項目數量眾多,在上萬個文件
中找出所有硬編碼的顏色值次泽,無異于大海撈針穿仪。且在各業(yè)務線緊張的迭代中席爽,還要花費大量的人力去查找替換這些色值明顯是不太現實的
二、目標
通過工具化的方式啊片,一鍵將項目中硬編碼的顏色值全部替換成相應的變量(less
變量或者 css3
變量)只锻,前提條件是公司必須有一套標準的配色表
三、實現方案
1紫谷、配色表轉換
首先齐饮,需要將 UI 設計師提供的配色表轉成對應的變量(本篇文章以 less
為例,其他預處理器同理)笤昨。例如設計師提供的配色表祖驱,如下
色號 | 色值 |
---|---|
#e6f7ff | |
#bae7ff | |
#91d5ff | |
#69c0ff | |
#40a9ff | |
#1890ff | |
#096dd9 | |
#0050b3 | |
#003a8c | |
#002766 |
轉換成 less
變量之后,形式如下瞒窒,姑且把這個文件命名為:color-table.less
@blue-1: #e6f7ff;
@blue-2: #bae7ff;
@blue-3: #91d5ff;
@blue-4: #69c0ff;
@blue-5: #40a9ff;
@blue-6: #1890ff;
@blue-7: #096dd9;
@blue-8: #0050b3;
@blue-9: #003a8c;
@blue-10: #002766;
2捺僻、找顏色
前面準備工作做好之后,接下來就是怎么把顏色找出來了崇裁。嗯匕坯?那么多文件怎么找?很容易想到拔稳,可以通過 fs.readdir
葛峻、fs.readFile
配合正則表達式進行查找匹配,進而可以得出以下表達式(可精確匹配 rgba
與 hex
格式的顏色值巴比,暫不考慮英文字面量顏色與hsl格式
术奖,無他,項目中基本不用)
/(#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3}))|([rR][gG][Bb][Aa]?[\(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),){2}[\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),?[\s]*(0\.\d{1,2}|1|0)?[\)]{1})/g
這么長一串看著暈嗎匿辩?我也是這種感覺腰耙!那咱們把它可視化一下,最后長這樣:
666铲球,瞬間清晰多了
正則表達式有了之后,需要把它利用起來才能找到顏色值晰赞,這一塊邏輯的實現代碼如下:
// 匹配 rgba + hex 格式顏色值正則
const HEX_AND_RGB_COLOR_REG =
/(#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3}))|([rR][gG][Bb][Aa]?[\(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),){2}[\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),?[\s]*(0?\.\d{1,2}|1|0)?[\)]{1})/g;
// 讀取文件
const readFile = (filePath) => {
fs.readFile(filePath, function (err, data) {
if (err) {
return err;
}
let str = data.toString();
// 匹配出來的顏色值
const matchColor = str.match(HEX_AND_RGB_COLOR_REG);
console.log(matchColor);
});
};
// 讀取文件夾
const readDir = (filePath) => {
//遍歷目標文件夾
fs.readdir(filePath + "", function (err, files) {
if (err) {
console.log(err);
return err;
}
if (files.length !== 0) {
files.forEach((item) => {
const fileP = path.resolve(filePath, item);
//判斷文件的狀態(tài)稼病,用于區(qū)分文件名/文件夾
fs.stat(fileP, function (err, status) {
if (err) {
return err;
}
const isFile = status.isFile(); //是文件
const isDir = status.isDirectory(); //是文件夾
if (isFile) {
readFile(fileP);
}
if (isDir) {
readDir(fileP);
}
});
});
}
});
}
3、計算顏色距離
配色表有了掖鱼,項目中的顏色也找出來了然走。接下來到了最關鍵的一步:把顏色之間的相似度計算出來。也就是說需要知道兩個顏色之間的“距離”才能知道從項目中找出來的這個顏色值需要被替換成配色表中的哪個變量
在這之前需要把配色表文件 color-table.less
處理一下戏挡,使之成為 key-vlaue
的對象形式芍瑞,方便后續(xù)的匹配操作
// 讀取配色表中的內容
const colorTableStr = fs.readFileSync("./color-table.less", "utf-8");
// 去除行末的分號
const colorTableStrNext = colorTableStr.replace(/;/g, "");
// 將每一行數據裝進數組,形如:['@blue-1: #e6f7ff', '@blue-2: #bae7ff']
const colorTableArr = colorTableStrNext.split("\n").filter((item) => !!item);
// 配色表對象褐墅,形如:{'#e6f7ff': '@blue-1', '#bae7ff': '@blue-2'}
const colorVarObj = {};
colorTableArr.forEach((item) => {
item = item.toString();
if (item) {
const [key, value] = item.split(":");
// 去除空格
const val = value.replace(/\s/g, "");
colorVarObj[val] = key;
}
});
經過上述處理之后的配色表對象 colorVarObj
格式為:{'#e6f7ff': '@blue-1', '#bae7ff': '@blue-2'}
既然是計算拆檬,那肯定得有一套計算邏輯洪己,首先想到的計算方法是:
- 將顏色全部轉換為
RGB
格式; - 結合公式 進行計算
這種計算方法對于大部分顏色來講是沒問題的竟贯,但是對于某些顏色就行不通了答捕,假設有下面這一組顏色
從計算結果來看,color3
與 color1
的色差 34.64
屑那,明顯小于 color2
與 color1
的色差 51.96
拱镐。但是從視覺效果上來看,color2
比 color3
更接近 color1
持际,所以這個方法行不通
經過查閱相關資料沃琅,找到了一個相對比較完美的計算方案:
- 將顏色全部轉成
LAB
格式 - 結合
CIEDE2000
公式 來計算,這個方案用js
代碼實現之后蜘欲,跟Photoshop
的色差計算結果相差無幾阵难,基本上可以說是一模一樣。
上述公式中各字母代表的含義及計算方法:
接下來芒填,咱們來稍微實現一下這個公式呜叫。。殿衰≈烨欤可惜啊,已經有大佬實現了這個計算公式闷祥,秉著不重復造輪子觀念娱颊,就直接用這個庫吧(其實就是能力不行,實現不了??)凯砍。傳送門:DeltaE
不過呢箱硕,需要注意的是,這個公式接收的顏色格式為 LAB
悟衩,所以需要對顏色進行轉換剧罩,找了一圈類似的 js
庫,經測試發(fā)現轉換得并不精準座泳。那咱們自己實現一下惠昔?上代碼:
// rgb轉為lab
const rgb2lab = function ({ r, g, b }) {
r /= 255.0; // rgb range: 0 ~ 1
g /= 255.0;
b /= 255.0;
// gamma 2.2
if (r > 0.04045) {
r = Math.pow((r + 0.055) / 1.055, 2.4);
} else {
r = r / 12.92;
}
if (g > 0.04045) {
g = Math.pow((g + 0.055) / 1.055, 2.4);
} else {
g = g / 12.92;
}
if (b > 0.04045) {
b = Math.pow((b + 0.055) / 1.055, 2.4);
} else {
b = b / 12.92;
}
// sRGB
let X = r * 0.436052025 + g * 0.385081593 + b * 0.143087414;
let Y = r * 0.222491598 + g * 0.71688606 + b * 0.060621486;
let Z = r * 0.013929122 + g * 0.097097002 + b * 0.71418547;
// XYZ range: 0~100
X = X * 100.0;
Y = Y * 100.0;
Z = Z * 100.0;
// Reference White Point
const ref_X = 96.4221;
const ref_Y = 100.0;
const ref_Z = 82.5211;
X = X / ref_X;
Y = Y / ref_Y;
Z = Z / ref_Z;
// Lab
if (X > 0.008856) {
X = Math.pow(X, 1 / 3.0);
} else {
X = 7.787 * X + 16 / 116.0;
}
if (Y > 0.008856) {
Y = Math.pow(Y, 1 / 3.0);
} else {
Y = 7.787 * Y + 16 / 116.0;
}
if (Z > 0.008856) {
Z = Math.pow(Z, 1 / 3.0);
} else {
Z = 7.787 * Z + 16 / 116.0;
}
const lab_L = 116.0 * Y - 16.0;
const lab_A = 500.0 * (X - Y);
const lab_B = 200.0 * (Y - Z);
return [lab_L, lab_A, lab_B];
};
看到這里是不是有點疑惑,上面那一堆數字怎么來的挑势,你咋知道是這些镇防?附上參考資料:Conversion from RGB to lab
結合 DeltaE
公式,實現邏輯如下:
// 計算顏色距離
const calDistance = (current, source) => {
const [cl, ca, cb] = rgb2lab(tinyColor(current).toRgb());
const [sl, sa, sb] = rgb2lab(tinyColor(source).toRgb());
const distance = DeltaE.getDeltaE00(
{ L: cl, A: ca, B: cb },
{ L: sl, A: sa, B: sb }
);
return distance;
};
還是使用前面那一組顏色進行測試這個新公式的計算效果潮饱。color3
與 color1
的色差 16.11
来氧,是大于 color2
與 color1
的色差 10.82
。計算結果與視覺效果完全一致。
4啦扬、替換顏色
一切準備就緒中狂,替換顏色就很簡單了。但是考传,要注意以下三點:
-
less 文件:直接將相應的顏色值替換為
color-table.less
中的less
變量 -
tsx 文件:由于
tsx
文件不支持less
變量吃型,所以tsx
文件中硬編碼的顏色值,將被替換為color-table.less
中 less 變量對應的色值或 css3 變量 -
透明度不為 1 的 rgba 顏色值:由于
LAB
色彩空間沒有透明度信息僚楞,所以經過轉換之后勤晚,原色值的透明度會丟失,導致被轉換為錯誤的變量泉褐。此時赐写,需要結合 less 的 fade 方法還原丟失的透明度:fade(${less變量}, ${透明度 * 100}%)
代碼實現如下:
// 根據顏色距離,取出最接近的顏色
const nearColor = (color) => {
const colors = Object.keys(colorVarObj);
const distance = colors.map((item) => {
return {
distance: calDistance(color, item),
color: item,
};
});
let resColor = color;
let minDis = Number.MAX_SAFE_INTEGER;
distance.forEach((item) => {
if (item.distance < minDis) {
minDis = item.distance;
resColor = item.color;
}
});
// 替換為less變量
return [colorVarObj[resColor], resColor];
};
// 替換顏色
const replaceNearColor = (str, type) => {
let [colorVar, colorRes] = nearColor(str);
let realColor = colorTableObj[colorVar] ? colorTableObj[colorVar] : str;
// 透明度不為1的顏色特殊處理
const alp = tinyColor(str).getAlpha();
if (alp !== 1) {
const color = replaceCssVarFn(realColor);
colorVar = `fade(${color}, ${alp * 100}%)`;
}
// 處理非less文件中的顏色值
if (type !== 'less') {
if (alp !== 1) {
realColor = tinyColor(realColor).setAlpha(alp).toRgbString();
}
return realColor;
}
return colorVar;
};
5膜赃、效果體驗
在線體驗地址:https://hxkj.vip/demo/color
github 地址:https://github.com/TangSY/near-color-change
四挺邀、結語
作者:HashTang
別忘了點贊、關注跳座,支持一下哦~
歡迎提問交流端铛!
參考資料
http://zschuessler.github.io/DeltaE/learn/
https://zh.wikipedia.org/wiki/%E9%A2%9C%E8%89%B2%E5%B7%AE%E5%BC%82