目錄
- 繪制圖像,并自适應水準垂直居中
- 繪制圖像
- 自适應水準垂直居中
- 圖像資料的處理
- 圖像灰階處理
- 公式
- 平均值
- 圖像資料繪制到 canvas 上
- 灰階處理方法
- 提取圖像的主題色:平均值法(單色背景)、最多色值法(雙色背景)
- rgba 二維數組
- 平均值法(單色背景)
- 最多色值法(漸變背景)
- 中位切分法實作
用個小 demo 記錄一下,如何在 canvas 上操作圖像。
點選線上體驗
利用的是 canvas 的 api
drawImage
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
參數了解:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZwpmL2gjN4cjMygjNtUjNxgDN3UTOwcDMyATMyAjMtQjM1YDM48CXyATMyAjMvwFNyUjNwgzLcd2bsJ2Lc12bj5ycn9Gbi52YuAjMwIzZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
在 canvas 上繪制圖像,會存在這麼幾種情況:
- 圖像的長寬都比canvas的長寬大
- 圖像的長比canvas的大
- 圖像的寬比canvas的大
- 圖像的長寬都比canvas的小
是以在自适應時需要根據這幾種情況分别處理,利用寬高比,并且保證圖像的寬高比始終一緻。
其次水準垂直居中,利用的是 css 中處理水準垂直居中的方案:
top = (box.height - div.height) / 2
left = (box.width - div.width) / 2
于是就有了以下的方法:
// 計算圖檔居中繪制到畫布上時 的寬高及起點坐标位置
function calculate(canvasWidth, canvasHeight, imgWidth, imgHeight) {
let x = 0;
let y = 0;
const canvasWHRadio = canvasWidth / canvasHeight
const imgWHRadio = imgWidth / imgHeight
if (imgWidth < canvasWidth && imgHeight < canvasHeight) {
x = (canvasWidth - imgWidth) * 0.5
y = (canvasHeight - imgHeight) * 0.5
} else if (imgWHRadio > canvasWHRadio) {
imgHeight = canvasWidth / imgWHRadio
imgWidth = canvasWidth
y = (canvasHeight - imgHeight) * 0.5
} else {
imgWidth = canvasHeight * imgWHRadio
imgHeight = canvasHeight
x = (canvasWidth - imgWidth) * 0.5
}
return {
x,
y,
width: imgWidth,
height: imgHeight
}
}
要對圖像進行處理,比如灰階化,提取顔色等。都是在圖像資料上進行處理的。
擷取圖像資料,要用 canvas 提供的 api
getImageData
ImageData ctx.getImageData(sx, sy, sw, sh);
擷取到的資料中,包含擷取到的矩形圖像的
width
、
height
data
。要處理的就是 data 了。
data 是一個大的類數組,類型是Uint8ClampedArray(8位無符号整型固定數組),限定了數組值在[0-255]。其中,每 4 位表示一個 rgba 值。分别對應 r(紅)、g(綠)、b(藍)、a(透明度)。
RGB圖轉灰階圖經典的心理學公式:Gray = R0.299 + G0.587 + B*0.114
人眼對綠色的敏感度最高,對紅色的敏感度次之,對藍色的敏感度最低,是以使用不同的權重将得到比較合理的灰階圖像。
function getGrayColor (r, g, b) {
// 心理學灰階公式: Gray = R*0.299 + G*0.587 + B*0.114
// 考慮精度:Gray = (R*299 + G*587 + B*114) / 1000
// 考慮精度 + 速度:Gray = (R*38 + G*75 + B*15) >> 7
return (r * 38 + g * 75 + b * 15) >> 7
}
公式各種變體參考:從RGB色轉為灰階色算法
求出 rgb 的平均值,并把這個平均值賦給 rgb。處理出來的灰階圖可能會比較生硬,沒有公式法處理出來的灰階圖柔和。
function getGrayColorByAvg (r, g, b) {
// 平均值法
const avg = (r + g + b) / 3
return avg
}
處理好圖像資料了,灰階處理,再将圖像資料繪制到 canvas 上,利用的是
putImageData
void ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);
function imgGray () {
const {ctx, drawImgW, drawImgH, drawImgX, drawImgY} = global;
let imgData = ctx.getImageData(drawImgX, drawImgY, drawImgW, drawImgH);
global.imgData = imgData;
let copyImgData = new ImageData(new Uint8ClampedArray([...imgData.data]), imgData.width, imgData.height)
for (let i=0; i<copyImgData.data.length; i+=4) {
const R = copyImgData.data[i];
const G = copyImgData.data[i+1];
const B = copyImgData.data[i+2];
const gray = getGrayColor(R, G, B)
copyImgData.data[i] = gray;
copyImgData.data[i+1] = gray;
copyImgData.data[i+2] = gray;
}
ctx.putImageData(copyImgData, drawImgX, drawImgY);
}
圖像的主題色有什麼用呢?
用處之一就是作為圖像的背景色,當圖像沒加載出來之前,可以先用主題色填充。或者讓圖像的容器填充圖像的背景色填補空白部分,讓圖像觀感體驗更好。
關于提取圖像的主題色,其實是門深奧的技術。
主要是這麼幾種:顔色量化算法(中為切分法、八叉樹法)、聚類算法、顔色模組化。詳情可參考圖像主題色提取算法。
這些算法比較複雜,下面介紹的是比較簡單粗暴的。
const perChunkSize = 4;
const imgRgbaData = Array.from(imgData.data).reduce((rgba, item, index) => {
const subIndex = Math.floor(index / perChunkSize);
if (!rgba[subIndex]) {
rgba[subIndex] = []
}
rgba[subIndex].push(item)
return rgba;
}, [])
提取圖像的主題色,最簡單的方法是将圖像資料的所有 r、g、b 值加起來,再除以圖像的面積,求其平均值。
該方法的缺點在于:無法計算透明背景的主色調,主色調會被png圖檔透明區域的大小所影響。優點就是簡單明了,友善快捷。
主題色求出來了,互補色也比較簡單。就是用 255 - 主色調。即用 255 分别減去主色調的 r,g,b 的值分别得到一個新的 r,g,b 的值作為互補色調。
互補色有什麼用呢?
用處之一就是,填充文字的顔色,讓文字顯示正常。文字的顔色和主題色背景的顔色互斥(互補)時,會比較容易進入眼睛被看到。
function getColorByAvg (imgRgbaData, sizes) {
// 主色,平均值。将圖檔每一個像素點的r,g,b通道的值分别累加,然後分别用累加的r,g,b的值除以圖檔總像素點的個數,分别得到一個平均的r,g,b值并作為圖檔主色調的rgb值
const mainColor = {
r: 0,
g: 0,
b: 0
}
imgRgbaData.forEach(rgba => {
mainColor.r += rgba[0]
mainColor.g += rgba[1]
mainColor.b += rgba[2]
})
const area = sizes.width * sizes.height
mainColor.r = mainColor.r / area | 0
mainColor.g = mainColor.g / area | 0
mainColor.b = mainColor.b / area | 0
// 互補色,255 - 主色調。用255分别減去主色調的r,g,b的值分别得到一個新的r,g,b的值作為互補色調
const reverseColor = {
r: 255 - mainColor.r,
g: 255 - mainColor.g,
b: 255 - mainColor.b
}
return {
bgColor: `rgb(${mainColor.r}, ${mainColor.g}, ${mainColor.b})`,
txtColor: `rgb(${reverseColor.r}, ${reverseColor.g}, ${reverseColor.b})`
}
}
這種方法比較複雜一些。統計出每種顔色被使用到的次數,再根據次數降序排序,根據灰階值降序排序。取出第1個和第10個最為漸變色。
互補色利用灰階公式,比中間值 125 大的為白色,反之為黑色。
該方案借鑒的是grade.js
function get2ColorByCount (imgRgbaData) {
const filterData = imgRgbaData.filter(rgba => rgba.slice(0, 3).every(val => val > 0 && val < 255))
// 統計每一種顔色的使用次數
const countData = filterData.reduce((obj, rgba, index) => {
const key = rgba.join('|')
obj[key] = obj[key] ? ++(obj[key]) : 1;
return obj
}, {});
let sortData = Object.keys(countData).map(key => {
const rgba = key.split('|');
const gray = getGrayColor(rgba[0], rgba[1], rgba[2])
return {
rgba,
count: countData[key],
gray
}
})
sortData = sortData.sort((a, b) => a.count - b.count).reverse()
sortData = sortData.slice(0, 10).sort((a, b) => a.brightness - b.brightness).reverse()
const start = sortData[0].rgba
const end = sortData[sortData.length - 1].rgba
const rgb = [(start[0] / 2 + end[0] / 2) | 0, (start[1] / 2 + end[1] / 2) | 0, (start[2] / 2 + end[2] / 2) | 0]
const color = getGrayColor(rgb[0], rgb[1], rgb[2]) > 255 / 2 ? '#000' : '#fff'
return {
bgColor: [`rgb(${start[0]}, ${start[1]}, ${start[2]})`, `rgb(${end[0]}, ${end[1]}, ${end[2]})`],
txtColor: color
}
}
color-thief
不僅提取出了主題色,還提取出了互補色,配色。可以說非常厲害了。
以上就是記錄的全部了。