天天看點

如何對圖檔主題色進行提取

一起網易雲 ????

網易雲音樂想必是大家很熟悉的一款 app 了,畢竟大家在深夜都會​

​網抑雲​

如何對圖檔主題色進行提取

開玩笑了,最近在網易雲聽歌時,發現了一個很有意思的特效:

如何對圖檔主題色進行提取

就是切換歌曲時,會根據目前封面替換背景色。作為資深切圖仔,我那該死的好奇心兜不住了,不行,我要去一探究竟。

首先我構思了很多它可能的實作方式:

  • 機器學習對圖檔進行色彩分析
  • 前端提取圖檔主色調,做漸變處理
  • 封面背景圖做高斯模糊

對于第一種,他不在我的知識範圍内,這裡就不展開說明了 ????。

第二種的話,一般都是利用​

​canvas​

​來實作。

第三種相對來說,從技術層面來看,實作上是最為簡單的。

做了猜測分析後,我默默打開了熟悉的 Chrome 控制台,打開了網易雲音樂的源代碼:

如何對圖檔主題色進行提取
如何對圖檔主題色進行提取

好家夥,果然是第三種實作方式。????

本來到這裡,本文就該結束了。但之前也有朋友問過我​

​如何對前端圖檔主題色進行提取​

​的問題,正好之前也做過類似的需求,這裡就展開做個說明吧。

我們這裡以一個圖檔網站為例,來展示實際業務中應用較廣的場景:

如何對圖檔主題色進行提取

在弱網下,圖檔加載速度較慢,此時在圖檔完全加載之前,提取圖檔的主色調,然後填充為背景色。這樣使用者體驗能有較大的提升。

那具體是怎麼實作的呢?????

我們這裡采用​

​canvas​

​來實作,具體分為三步:

  • 擷取圖檔資料
  • 對圖檔資料進行處理
  • 對顔色清單排序

這裡我們使用的測試圖檔為:

相對來說,主色調較為明顯,也便于測試~

擷取圖檔資料 ????

我們知道圖檔是由一個個像素點組成的。通過 canvas 的​

​getImageData()​

​方法恰好可以擷取圖檔的像素資料:

let imgObj = document.getElementById('yourId');

// 建立畫布
let canvas = document.createElement('canvas');
canvas.setAttribute('width', imgObj.width);
canvas.setAttribute('height', imgObj.height);
let context = canvas.getContext('2d');
// 将圖檔畫在畫布上
context.drawImage(imgObj, 0, 0);
// 擷取像素資料
let imgData = context.getImageData(0, 0, imgObj.width, imgObj.height);
let pixelData = imgData.data;      

但這時你去列印​

​pixelData​

​,你會發現結果為:

如何對圖檔主題色進行提取

好家夥,全是 0,,,????

我一時想不到是什麼原因:難道是 canvas 的 api 使用不熟練?

如何對圖檔主題色進行提取
如何對圖檔主題色進行提取

在​

​stackoverflow​

​上找到了上面的回答:

如何對圖檔主題色進行提取
如何對圖檔主題色進行提取

但是我修改後還是不行。

這時,我想到圖檔加載是異步的。可能圖檔還沒加載完畢就開始從畫布讀取圖檔資料了,顯然這是不對的。于是我對原有代碼做了一番調整:

getMainColor("./test.jpeg");
function getMainColor(image) {
  return new Promise((resolve, reject) => {
    try {
      const canvas = document.createElement("canvas");
      const img = new Image(); // 建立img元素
      img.src = image; // 設定圖檔源位址
      img.onload = () => {
        let color = getImageColor(canvas, img);
        resolve(color);
      };
    } catch (e) {
      reject(e);
    }
  });
}
function getImageColor(canvas, img) {
  const context = canvas.getContext("2d");
  context.drawImage(img, 0, 0);

  // 擷取像素資料
  let pixelData = context.getImageData(
    0,
    0,
    canvas.width,
    canvas.height
  ).data;
  console.log("pixelData", pixelData);
  return pixelData;
}      

事實證明:it's true

如何對圖檔主題色進行提取

擷取了圖檔資料,下一步就要對其進行相應的處理。

對圖檔資料進行處理 ????

展開上一步得到的資料:

如何對圖檔主題色進行提取

這裡的資料是什麼意思呢?其實就是​

​rgba​

​,分布代表​

​紅色(Red)​

​,​

​綠色(Green)​

​藍色(Blue)​

​和​

​透明度(Alpha)​

​。​

​rgba​

​ 的圖檔每個像素點是由上面四個數值表示的。也就是說每四個為一組。

知道了規律,那讓我們來對資料做一下清洗:主要就是對顔色進行分組,并統計每種顔色分别出現的次數:

function getImageColor(canvas, img) {
  const context = canvas.getContext("2d");
  context.drawImage(img, 0, 0);

  // 擷取像素資料
  let pixelData = context.getImageData(
    0,
    0,
    canvas.width,
    canvas.height
  ).data;
  console.log("pixelData", pixelData);
  return getCountsArr(pixelData);
}
function getCountsArr(pixelData) {
  let colorList = [];
  let rgba = [];
  let rgbaStr = "";
  // 分組循環
  for (let i = 0; i < pixelData.length; i += 4) {
    rgba[0] = pixelData[i];
    rgba[1] = pixelData[i + 1];
    rgba[2] = pixelData[i + 2];
    rgba[3] = pixelData[i + 3];

    if (rgba.indexOf(undefined) !== -1 || pixelData[i + 3] === 0) {
      continue;
    }
    // console.log("rgba", rgba);
    rgbaStr = rgba.join(",");
    if (rgbaStr in colorList) {
      ++colorList[rgbaStr];
    } else {
      colorList[rgbaStr] = 1;
    }
  }
  console.log("colorList", colorList);

  return colorList;
}      

列印​

​colorList​

​結果為:

到這裡,我們就得到了每種資料分别出現的次數。

對顔色清單排序 ????

最後一步,對上面得到的色值對象做一個排序:

for (let prop in colorList) {
  arr.push({
    // 如果隻擷取rgb,則為`rgb(${prop})`
    color: `rgba(${prop})`,
    count: colorList[prop],
  });
}
// 數組排序
arr.sort((a, b) => {
  return b.count - a.count;
});

console.log("arr", arr);      

排序後得到如下結果:

如何對圖檔主題色進行提取

到這裡我們就得到了圖檔色值出現次數從大到小的排序數組,我們來看排在第一位的​

​rgba(206,205,201,255)​

​:

如何對圖檔主題色進行提取

再把測試圖檔貼一下:

如何對圖檔主題色進行提取

肉眼可見的主題色已經被提取出來了! 

反思

最後還是回到文章最開始提到的網易雲音樂的播放器特效。不管它的實作方式是怎麼樣的,它的這種産品創意是值得我們學習的。

我們平時在浏覽國内外的一些網站或者使用一些 app 時,總能遇到一些讓你拍手稱贊的效果。而這些特效往往又與我們前端分不開。

俗話說:​

​前端是離産品最近的開發工程師​

​,那最近你有沒有遇到一些讓你感覺很驚豔或者很有想法的效果呢,歡迎在評論區留言