975
汽車大全
H5中基於Canvas實現的高斯模煳
自從扁平化流行起來之後,高斯模煳效果漸漸變成了視覺很喜歡用的一種表現形式,我們的視覺小姐姐也特別喜歡。為了滿足她,踩了無窮無盡的坑之後,最後隻能掏出Canvas來了。
沒有什麼視覺需求是Canvas解決不了的,如果有,再蓋一層Canvas —— 奈帆斯基
解決痛點
- CSS模煳 和 大麵積transform 混用時,會導致的性能問題 ( 卡 )
- CSS模煳 在圖片邊界的表現不夠優秀
- iOS下高像素的高斯模煳會出現 奇怪的現象 ( 突然顏色大變 )
- 一套解決方案,不再需要 svg+多種css兼容 判環境應用
理論原理
對算法部分無愛的弟兄們直接跳過本節也沒關係的。
理論
模煳的效果相信大家都不陌生,實際上就是一種加權平均算法。
而 高斯模煳( Gaussian Blur ) 就是以高斯分布作為權重的平均算法。高斯分布長下麵這個樣子。
[ 一維高斯分布 ]
圖片有x,y兩個維度,所以在平均的時候應該使用二維高斯分布
[ 二維高斯分布 ]
基本算法
- 輸入
圖片Img
,模煳半徑radius
- 按
radius
計算出高斯矩陣 gaussMatrix
避免重複計算 - 遍曆每一個像素
- 提取當前像素[x,y]{r,g,b,a}
- 求範圍 [ x ± radius , y ± radius ] 內的 {r,g,b,a} 各自在
gaussMatrix
內的加權均值
- 輸出
邊界的處理
觀察係統的高斯模煳效果,邊界總是半透明的。推測是在邊界處增加 alpha=0 的點補齊計算。
[ css模煳 - 紅色部分為初始邊界 ]
嗯,這個效果也算 痛點 之一吧。我的解決方案是:**僅計算存在的點的權重**
算法實現
const gaussBlur = function (imgData, radius) {
radius *= 3; //不知為什麼,我的模煳半徑是 css中 filter:bulr 值的三倍時效果才一致。
//Copy圖片內容
const pixes = new Uint8ClampedArray(imgData.data);
const width = imgData.width;
const height = imgData.height;
let gaussSum = 0,
x, y,
r, g, b, a, i;
//模煳半徑取整
radius = Math.floor(radius);
//sigma越小中心點權重越高, sigma越大越接近平均模煳
const sigma = radius / 3;
//兩個分布無相關性, 為了各方向上權重分布一致
const Ror = 0;
const L = radius * 2 + 1; //矩陣寬度
const Ror2 = Ror * Ror;
const s2 = sigma * sigma;
const c1 = 1 / ( 2 * Math.PI * s2 * Math.sqrt(1 - Ror * Ror));
const c2 = -1 / (2 * (1 - Ror2));
//定義高斯矩陣 , 存儲在一維數組中
const gaussMatrix = [];
//根據 xy 計算 index
gaussMatrix.getIndex = (x, y)=> {
return (x + radius) + (y + radius) * L;
}
//根據 xy 獲取權重
gaussMatrix.getWeight = (x, y)=> {
return gaussMatrix[gaussMatrix.getIndex(x, y)];
}
//根據 index 獲取 x 偏移
gaussMatrix.getX = (index)=> {
return index % L - radius;
}
//根據 index 獲取 y 偏移
gaussMatrix.getY = (index)=> {
return Math.floor(index / L) - radius;
}
//覆寫forEach , 方便遍曆
gaussMatrix.forEach = (f)=> {
gaussMatrix.map((w, i)=> {
f(w, gaussMatrix.getX(i), gaussMatrix.getY(i))
})
}
//生成高斯矩陣
for (y = -radius; y <= radius; y++) {
for (x = -radius; x <= radius; x++) {
let i = gaussMatrix.getIndex(x, y);
g = c1 * Math.exp(c2 * (x * x + 2 * Ror * x * y + y * y) / s2);
gaussMatrix[i] = g;
}
}
//快捷獲取像素點數據
const getPixel = (x, y)=> {
if (x < 0 || x >= width || y < 0 || y >= height) {
return null;
}
let p = (x + y * width) * 4;
return pixes.subarray(p, p + 4);
}
//遍曆圖像上的每個點
i = 0;
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
//重置 r g b a Sum
r = g = b = a = 0;
gaussSum = 0;
//遍曆模煳半徑內的其他點
gaussMatrix.forEach((w, dx, dy)=> {
let p = getPixel(x + dx, y + dy);
if (!p)return;
//求加權和
r += p[0] * w;
g += p[1] * w;
b += p[2] * w;
a += p[3] * w;
gaussSum += w;
});
//寫回imgData
imgData.data.set([r, g, b, a].map(v=>v / gaussSum), i);
//遍曆下一個點
i += 4;
}
}
return imgData;
};
寫完了實現的我,迫不及待的試了試
[ 效果拔群! 無與倫比! 掌聲呢?!!! ]
一般來說寫到這裏,就算功成名就了,不過我瞥了一眼控製台...
足足算了21秒,這可是我心愛的 MacPro,我要報警了!
優化算法
目前的算法,複雜度大約是 w * h * (2r)^2
之後我去搜了搜 大神代碼,發現他們是先進行一輪X軸方向模煳,再進行一輪Y軸方向模煳,複雜度隻有 2 * w * h * 2r , 一下少了好多運算量。
我們也來試試。
[ 效果立竿見影 ]
以我的數學水平,並不能證明兩者是等效的,但是從視覺上來看是一致的,為什麼可以這樣優化,期望大神賜教。
使用優化
從算法上可以看出來,運算量由三個方麵來決定:圖片寬w、高h,模煳半徑r。
這樣就能對我們的幾個常見使用場景進行優化
1. 大尺寸圖片
例如一張900x600的圖片,需要輸出一張300x200@2x
可以將圖片先縮放到300x200再計算模煳
2. 大半徑模煳
例如一張900x600的圖片,需要模煳半徑150,需要輸出一張300x200@2x的圖
這樣的圖可以說是細節全失,通常視覺隻Care成圖的大概色彩範圍,我們可以用一些粗暴的方法。
- 等比例計算,把圖片變成 6x4 r=1 ,
- 計算模煳,輸出 6x4 的圖片
- 使用css拉伸到 300x200
實現
說白了優化手段就是一招縮小射線,我們抽象一個參數: 縮小倍率 shrink
/**
* @public
* 暴露的異步模煳方法
* ---------------------
* @param URL 圖片地址,需要跨域支持
* @param r 模煳半徑 {Int}
* @param shrink 縮小比率 {Number}
* @return {Promise}
*/
export const blur = (URL, r, shrink = 1)=> {
return new Promise((resolve, reject)=> {
const IMG = new Image();
IMG.crossOrigin = '*'; //需要圖片跨域支持
IMG.onload = function () {
const Canvas = document.createElement('CANVAS'); //大量使用可考慮隻創建一次
let w = IMG.width, h = IMG.height;
//縮小比例不為1時 , 重新計算寬高比
if (shrink !== 1) {
w = Math.ceil(w / shrink);
h = Math.ceil(h / shrink);
r = Math.ceil(r / shrink);
}
//因為懶, 就全Try了, 實際上隻 Try跨域錯誤 即可
try {
//設置Canvas寬高,獲取上下文
Canvas.width = w;
Canvas.height = h;
let ctx = Canvas.getContext('2d');
ctx.drawImage(IMG, 0, 0, w, h);
//提取圖片信息
let d = ctx.getImageData(0, 0, w, h);
//進行高斯模煳
let gd = gaussBlur(d, r, 0);
//繪製模煳圖像
ctx.putImageData(gd, 0, 0);
resolve(Canvas.toDataURL());
} catch (e) {
reject(e);
}
};
IMG.src = URL;
})
};
以一張 640x426 的圖片,輸出{ 300x200,r=10 }為例:
對比
1. 原尺寸模煳
2. 縮小到1/10進行模煳
首先要明確的是,**在縮小情況下兩種算法並不等價**。小圖放大的模煳效果取決於瀏覽器本身的算法實現。最終視覺上**效果差別不顯著,完全可以使用**。
麵對形形色色的尺寸
考慮到來自服務端的圖片可能有各種神奇的尺寸,而通常輸出是一個確定的尺寸。
在這樣的情況下,縮小比例會產生一些冗餘,所以更適合另一個【鎖定輸出寬高的實現】。
/**
* @public
* 暴露的異步模煳方法
* ---------------------
* @param URL 圖片地址,需要跨域支持
* @param r 模煳半徑 {Int}
* @param w 輸出寬度 {Number}
* @param h 輸出高度 {Number}
* @return {Promise}
*/
export const blurWH = (URL, r, w ,h)=> {
return new Promise((resolve, reject)=> {
const IMG = new Image();
IMG.crossOrigin = '*'; //需要圖片跨域支持
IMG.onload = function () {
const Canvas = document.createElement('CANVAS'); //大量使用可考慮隻創建一次
//鎖定輸出寬高之後, 就不需要Care 原圖有多寬多高了
//let w = IMG.width, h = IMG.height;
//因為懶, 就全Try了, 實際上隻 Try跨域錯誤 即可
try {
//設置Canvas寬高,獲取上下文
Canvas.width = w;
Canvas.height = h;
let ctx = Canvas.getContext('2d');
ctx.drawImage(IMG, 0, 0, w, h);
//提取圖片信息
let d = ctx.getImageData(0, 0, w, h);
//進行高斯模煳
let gd = gaussBlur(d, r, 0);
//繪製模煳圖像
ctx.putImageData(gd, 0, 0);
resolve(Canvas.toDataURL());
} catch (e) {
reject(e);
}
};
IMG.src = URL;
})
};
總結
V8對連續執行的代碼有靜態優化,所以文中所列時間大家不要較真,看個數量級就好 ╮(╯▽╰)╭
兼容性
-
Uint8ClampedArray Can I use?
- Android 4+
- iOS safari 7.1+
-
Cross-Origin in <cavnas> Can I use?
- Android 4.4 +
- iOS safari 7.1+
完整實現
/**
* @fileOverview
* 高斯模煳
* @author iNahoo
* @since 2017/5/8.
*/
"use strict";
const gaussBlur = function (imgData, radius) {
radius *= 3; //不知為什麼,我的模煳半徑是 css中 filter:bulr 值的三倍時效果才一致。
//Copy圖片內容
let pixes = new Uint8ClampedArray(imgData.data);
const width = imgData.width;
const height = imgData.height;
let gaussMatrix = [],
gaussSum,
x, y,
r, g, b, a,
i, j, k,
w;
radius = Math.floor(radius);
const sigma = radius / 3;
a = 1 / (Math.sqrt(2 * Math.PI) * sigma);
b = -1 / (2 * sigma * sigma);
//生成高斯矩陣
for (i = -radius; i <= radius; i++) {
gaussMatrix.push(a * Math.exp(b * i * i));
}
//x 方向一維高斯運算
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
r = g = b = a = gaussSum = 0;
for (j = -radius; j <= radius; j++) {
k = x + j;
if (k >= 0 && k < width) {
i = (y * width + k) * 4;
w = gaussMatrix[j + radius];
r += pixes[i] * w;
g += pixes[i + 1] * w;
b += pixes[i + 2] * w;
a += pixes[i + 3] * w;
gaussSum += w;
}
}
i = (y * width + x) * 4;
//計算加權均值
imgData.data.set([r, g, b, a].map(v=>v / gaussSum), i);
}
}
pixes.set(imgData.data);
//y 方向一維高斯運算
for (x = 0; x < width; x++) {
for (y = 0; y < height; y++) {
r = g = b = a = gaussSum = 0;
for (j = -radius; j <= radius; j++) {
k = y + j;
if (k >= 0 && k < height) {
i = (k * width + x) * 4;
w = gaussMatrix[j + radius];
r += pixes[i] * w;
g += pixes[i + 1] * w;
b += pixes[i + 2] * w;
a += pixes[i + 3] * w;
gaussSum += w;
}
}
i = (y * width + x) * 4;
imgData.data.set([r, g, b, a].map(v=>v / gaussSum), i);
}
}
return imgData;
};
/**
* @public
* 暴露的異步模煳方法
* ---------------------
* @param URL 圖片地址,需要跨域支持
* @param r 模煳半徑 {Int}
* @param shrink 縮小比率 {Number}
* @return {Promise}
*/
export const blur = (URL, r, shrink = 1)=> {
return new Promise((resolve, reject)=> {
const IMG = new Image();
IMG.crossOrigin = '*'; //需要圖片跨域支持
IMG.onload = function () {
const Canvas = document.createElement('CANVAS'); //大量使用可考慮隻創建一次
let w = IMG.width, h = IMG.height;
//縮小比例不為1時 , 重新計算寬高比
if (shrink !== 1) {
w = Math.ceil(w / shrink);
h = Math.ceil(h / shrink);
r = Math.ceil(r / shrink);
}
//因為懶, 就全Try了, 實際上隻 Try跨域錯誤 即可
try {
//設置Canvas寬高,獲取上下文
Canvas.width = w;
Canvas.height = h;
let ctx = Canvas.getContext('2d');
ctx.drawImage(IMG, 0, 0, w, h);
//提取圖片信息
let d = ctx.getImageData(0, 0, w, h);
//進行高斯模煳
let gd = gaussBlur(d, r, 0);
//繪製模煳圖像
ctx.putImageData(gd, 0, 0);
resolve(Canvas.toDataURL());
} catch (e) {
reject(e);
}
};
IMG.src = URL;
})
};
/**
* @public
* 暴露的異步模煳方法
* ---------------------
* @param URL 圖片地址,需要跨域支持
* @param r 模煳半徑 {Int}
* @param w 輸出寬度 {Number}
* @param h 輸出高度 {Number}
* @return {Promise}
*/
export const blurWH = (URL, r, w, h)=> {
return new Promise((resolve, reject)=> {
const IMG = new Image();
IMG.crossOrigin = '*'; //需要圖片跨域支持
IMG.onload = function () {
const Canvas = document.createElement('CANVAS'); //大量使用可考慮隻創建一次
//鎖定輸出寬高之後, 就不需要Care 原圖有多寬多高了
//let w = IMG.width, h = IMG.height;
//因為懶, 就全Try了, 實際上隻 Try跨域錯誤 即可
try {
//設置Canvas寬高,獲取上下文
Canvas.width = w;
Canvas.height = h;
let ctx = Canvas.getContext('2d');
ctx.drawImage(IMG, 0, 0, w, h);
//提取圖片信息
let d = ctx.getImageData(0, 0, w, h);
//進行高斯模煳
let gd = gaussBlur(d, r, 0);
//繪製模煳圖像
ctx.putImageData(gd, 0, 0);
resolve(Canvas.toDataURL());
} catch (e) {
reject(e);
}
};
IMG.src = URL;
})
};
廣告
知道我為什麼不放Demo嘛?
我大A工作室開發的 《淘票票專業版》 已經上線啦!
想看demo的歡迎下載APP,瀏覽各個影片詳情的時候,順便瞅一眼頭部的海報背景,那我是逝去的頭f... 啊不,是我親手模煳的圖片。
╮(╯▽╰)╭
參考資料
最後更新:2017-07-24 11:03:30