顏色也有距離?咋計算?一鍵找出上萬個檔案中的相近顏色並替換

2022-10-13 21:08:41

一、背景

前段時間在公司專案中推進全域性換膚之後,發現有個後遺症。專案中存在大量寫死的顏色值,導致部分場景無法達到動態換膚的效果,需要把這些顏色值全部找出來並替換成變數。再加上公司正在風風火火的實行UI規範大一統,對這個問題有迫切的解決需求。然而公司中現存專案數量眾多,在上萬個檔案中找出所有寫死的顏色值,無異於大海撈針。且在各業務線緊張的迭代中,還要花費大量的人力去查詢替換這些色值明顯是不太現實的

二、目標

通過工具化的方式,一鍵將專案中寫死的顏色值全部替換成相應的變數(less 變數或者 css3 變數),前提條件是公司必須有一套標準的配色表

三、實現方案

1、配色錶轉換

首先,需要將 UI 設計師提供的配色錶轉成對應的變數(本篇文章以 less 為例,其他前處理器同理)。例如設計師提供的配色表,如下

色號 色值
$ \color{#e6f7ff}{1號藍} $ #e6f7ff
$ \color{#bae7ff}{2號藍} $ #bae7ff
$ \color{#91d5ff}{3號藍} $ #91d5ff
$ \color{#69c0ff}{4號藍} $ #69c0ff
$ \color{#40a9ff}{5號藍} $ #40a9ff
$ \color{#1890ff}{6號藍} $ #1890ff
$ \color{#096dd9}{7號藍} $ #096dd9
$ \color{#0050b3}{8號藍} $ #0050b3
$ \color{#003a8c}{9號藍} $ #003a8c
$ \color{#002766}{10號藍} $ #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.readdirfs.readFile 配合正規表示式進行查詢匹配,進而可以得出以下表示式(可精確匹配 rgbahex 格式的顏色值,暫不考慮英文字面量顏色與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);
        //判斷檔案的狀態,用於區分檔名/資料夾
        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 的物件形式,方便後續的匹配操作

// 讀取配色表中的內容
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 格式;
  • 結合公式 $ \Delta RGB = \sqrt{(R_2 - R_1)^2 + (G_2 - G_1)^2 + (B_2 - B_1)^2} $ 進行計算

這種計算方法對於大部分顏色來講是沒問題的,但是對於某些顏色就行不通了,假設有下面這一組顏色

$ \color{#969696}{color1: rgb(150,150,150)} $

$ \color{#787878}{color2: rgb(120,120,120)} $

$ \color{#AA8282}{color3: rgb(170,130,130)} $

從計算結果來看,color3color1 的色差 34.64,明顯小於 color2color1 的色差 51.96。但是從視覺效果上來看,color2color3 更接近 color1,所以這個方法行不通

經過查閱相關資料,找到了一個相對比較完美的計算方案:

  • 將顏色全部轉成 LAB 格式
  • 結合 CIEDE2000 公式 $ \Delta E_{00}^* = \sqrt{ (\frac{\Delta L\prime}{K_LS_L})2 + (\frac{\Delta C\prime}{K_CS_C})2 + (\frac{\Delta H\prime}{K_HS_H})2 + R_T\frac{\Delta C^\prime}{K_CS_C}\frac{\Delta H^\prime}{K_HS_H} } $ 來計算,這個方案用 js 程式碼實現之後,跟 Photoshop 的色差計算結果相差無幾,基本上可以說是一模一樣。

上述公式中各字母代表的含義及計算方法:

接下來,咱們來稍微實現一下這個公式。。。可惜啊,已經有大佬實現了這個計算公式,秉著不重複造輪子觀念,就直接用這個庫吧(其實就是能力不行,實現不了