JavaScript?Canvas實(shí)現(xiàn)圖片局部放大鏡效果
前言
最近我在可視化課程中學(xué)習(xí)了如何在Canvas中利用像素處理來實(shí)現(xiàn)濾鏡效果,在這節(jié)課程的結(jié)尾留了一道局部放大鏡的題目,提示我們用像素處理的方式去實(shí)現(xiàn)這個(gè)效果,最終實(shí)現(xiàn)隨著鼠標(biāo)移動(dòng)將圖片局部放大,本著把學(xué)到的內(nèi)容落地實(shí)踐的想法,我就去思考了一番,但很不幸,我思考了好幾天也沒思考出結(jié)果,因?yàn)閯傞_始我想的一直是在一個(gè)Canvas上來操作,但是一來我對(duì)Canvas API還并不是很熟悉,二來我對(duì)像素處理還不夠熟練,然后第三是如果原圖的部分像素被處理了,那下一次放大就會(huì)有問題,因此我最終放棄了這個(gè)思路,選擇了再增加一個(gè)Canvas來完成最終的效果,以下就是利用這種方式實(shí)現(xiàn)圖片局部放大的效果。
像素處理
在實(shí)現(xiàn)這個(gè)效果之前,我們先來了解一下如何處理像素,有些小伙伴可能不太清楚,所以這里簡(jiǎn)單說一下,在屏幕上我們知道所有顯示的內(nèi)容都是由像素點(diǎn)組成的,那么在處理像素之前,我們需要先獲取到像素信息,那么Canvas就是提供了一個(gè)API叫做getImageData讓我們可以獲取到畫布上的像素信息,最終這個(gè)API返回的是一個(gè)ImageData類型的值,關(guān)于這個(gè)API的具體描述可以參考對(duì)應(yīng)的MDN頁面。
ImageData類型的數(shù)據(jù)包含三個(gè)屬性,包括data、width、height。width和height簡(jiǎn)單來說,就是被提取像素信息的區(qū)域的寬高,最主要的像素信息是在這個(gè)data屬性中。data屬性指向一個(gè)數(shù)組類型的值,準(zhǔn)確來說是Uint8ClampedArray的實(shí)例,Uint8ClampedArray表示8 位無符號(hào)整型固定數(shù)組,也就是說其中的元素是0到255之間的整數(shù),我們知道一個(gè)像素的顏色信息可以使用rgba四個(gè)分量表示,那么我們就得出在data數(shù)組中每四個(gè)元素就能表示一個(gè)像素點(diǎn)的信息,因此data數(shù)組的長(zhǎng)度就是width * height * 4。
了解完像素處理,我們就可以開始進(jìn)行具體的實(shí)現(xiàn)了。
具體實(shí)現(xiàn)
<canvas ref="canvasRef" width="0" height="0"></canvas> <canvas ref="magnifier" width="0" height="0"></canvas><!-- 放大鏡 -->
1. 準(zhǔn)備工作
在實(shí)現(xiàn)放大效果之前,我們需要先把圖片加載到Canvas上:
(async function() {
const img = await loadImage('src/assets/girl1.jpg');
canvasRef.value.width = img.width;
canvasRef.value.height = img.height;
context.drawImage(img, 0, 0);
}());
這里loadImage方法是通過Image對(duì)象來異步加載圖片,然后通過drawImage方法將圖片繪制到畫布上。
接著設(shè)置一個(gè)要放大的區(qū)域,也就是以鼠標(biāo)坐標(biāo)為中心,多少半徑以內(nèi)的內(nèi)容要被放大,這里我設(shè)置一個(gè)變量originSize用于存儲(chǔ)原圖大小,并設(shè)置一個(gè)5倍的放大倍數(shù)。
let originSize = 40; // 原圖大小
let zoom = 5; // 放大倍數(shù)
(async function() {
// ...
magnifier.value.width = originSize * zoom;
magnifier.value.height = originSize * zoom;
}());
用作于放大鏡的magnifier,我們使用originSize * zoom來設(shè)置它的寬高。
2. 鼠標(biāo)移動(dòng)事件監(jiān)聽
接下來就是主要的代碼實(shí)現(xiàn)。
首先是添加鼠標(biāo)移動(dòng)事件的監(jiān)聽:
const addEvent = () => {
canvasRef.value.addEventListener('mousemove', mouseDownHandler);
};
addEvent();
然后我們就來實(shí)現(xiàn)mouseDownHandler函數(shù)。
首先我們獲取鼠標(biāo)坐標(biāo)在Canvas中的相對(duì)坐標(biāo),并通過Math.floor取整
const mouseDownHandler = e => {
// 相對(duì)于畫布的坐標(biāo)
const center = {
x: Math.floor(e.pageX - left),
y: Math.floor(e.pageY - top)
};
};
然后利用getImageData方法獲取指定區(qū)域的像素信息,這里我們用到了OffscreenCanvas,它提供了一個(gè)可以脫離屏幕渲染的 canvas 對(duì)象,可以提升渲染性能;這樣我們就得到了待放大區(qū)域的像素信息。
const mouseDownHandler = e => {
// 相對(duì)于畫布的坐標(biāo)
// ...
// 待放大區(qū)域的imageData
const originImageData = getImageData(img, [center.x - originSize / 2, center.y - originSize / 2, originSize, originSize]);
};
現(xiàn)在我們需要一個(gè)ImageData類型的變量,用于存儲(chǔ)放大后的像素信息,因?yàn)樽罱K要渲染到magnifier這個(gè)Canvas上,我們就使用magnifier的2d上下文對(duì)象調(diào)用createImageData方法來創(chuàng)建一個(gè)ImageData對(duì)象,關(guān)于這個(gè)方法的使用具體可查看MDN文檔。
const mouseDownHandler = e => {
// 相對(duì)于畫布的坐標(biāo)
// ...
// 待放大區(qū)域的imageData
// ...
// 構(gòu)建一個(gè)imageData
const areaImageData = mContext.createImageData(magnifier.value.width, magnifier.value.height);
};
接下來就是具體的像素遍歷和處理,按照areaImageData的寬高來進(jìn)行遍歷,這里迭代的增量使用+zoom是因?yàn)?,?dāng)我們放大zoom倍數(shù)之后,原圖1個(gè)像素的信息在magnifier使用zoom*zoom個(gè)像素來放大,也就是zoom*zoom個(gè)像素點(diǎn)的色值和原圖中對(duì)應(yīng)的那個(gè)像素的色值是一樣的。在我們這段代碼中設(shè)置zoom為5,也就是放大后使用5*5=25個(gè)像素點(diǎn)表示之前的一個(gè)像素點(diǎn)。
const mouseDownHandler = e => {
// 相對(duì)于畫布的坐標(biāo)
// ...
// 待放大區(qū)域的imageData
// ...
// 構(gòu)建一個(gè)imageData
// ...
let count = 0;
for (let j = 0; j < originSize * zoom; j += zoom) {
for (let i = 0; i < originSize * zoom; i += zoom) {
// ...
}
}
};
所以我們繼續(xù)使用兩個(gè)for循環(huán)k和m,把a(bǔ)reaImageData的data數(shù)組中的對(duì)應(yīng)元素賦值為原圖對(duì)應(yīng)像素的色值,完成賦值后我們就可以通過putImageData方法將像素信息渲染到magnifier畫布上。
const mouseDownHandler = e => {
// 相對(duì)于畫布的坐標(biāo)
// ...
// 待放大區(qū)域的imageData
// ...
// 構(gòu)建一個(gè)imageData
// ...
let count = 0;
for (let j = 0; j < originSize * zoom; j += zoom) {
for (let i = 0; i < originSize * zoom; i += zoom) {
for (let k = j; k < j + zoom; k ++) {
for (let m = i; m < i + zoom; m ++) {
const index = (k * originSize * zoom + m) * 4;
areaImageData.data[index] = originImageData.data[count];
areaImageData.data[index + 1] = originImageData.data[count + 1];
areaImageData.data[index + 2] = originImageData.data[count + 2];
areaImageData.data[index + 3] = originImageData.data[count + 3];
}
}
count += 4;
}
}
mContext.putImageData(areaImageData, 0, 0);
};
至此我們就實(shí)現(xiàn)了基本的局部放大,但現(xiàn)在放大鏡不在原圖Canvas的上方,并且放大鏡是一個(gè)正方形,我們繼續(xù)簡(jiǎn)單優(yōu)化一下。
3. 簡(jiǎn)單優(yōu)化
首先因?yàn)槲覍?duì)Canvas API還不太熟悉,所以我現(xiàn)在通過css把放大鏡改為圓形,并加上一個(gè)陰影box-shadow來優(yōu)化視覺效果。
#magnifier {
position: absolute;
box-shadow: 0 0 10px 4px rgba(12, 12, 12, .5);
border-radius: 50%;
}
然后給兩個(gè)Canvas外層加一個(gè)div容器,把放大鏡設(shè)置絕對(duì)定位,把它放到鼠標(biāo)坐標(biāo)的位置,在鼠標(biāo)移動(dòng)過程中更新放大鏡的位置。
<div class="canvas-container" ref="containerRef" :style="{width: containerWidth + 'px'}">
<canvas ref="canvasRef" width="0" height="0"></canvas>
<canvas ref="magnifier" width="0" height="0" id="magnifier" :style="position"></canvas>
</div>
const position = reactive({
left: 0,
top: 0
});
const containerWidth = ref(0);
containerWidth.value = img.width;
// 在鼠標(biāo)移動(dòng)過程中更新放大鏡的位置
position.top = (center.y - originSize * zoom / 2) + 'px';
position.left = (center.x - originSize * zoom / 2) + 'px';
.canvas-container {
position: relative;
overflow: hidden;
}
這個(gè)時(shí)候放大鏡的位置就和我們預(yù)想的一致了,但是現(xiàn)在還有一個(gè)問題,就是放大鏡在原圖的上方,在移動(dòng)的過程中會(huì)看到放大鏡的渲染有點(diǎn)卡頓,這是因?yàn)槭髽?biāo)移動(dòng)事件是加在原圖Canvas上的,當(dāng)鼠標(biāo)懸浮在放大鏡上時(shí),這個(gè)移動(dòng)事件的監(jiān)聽就不連貫了,此時(shí)我們可以考慮把鼠標(biāo)移動(dòng)監(jiān)聽加改為加在外層容器上,這樣就能看到移動(dòng)過程中放大鏡的渲染是比較流暢了。
const addEvent = () => {
containerRef.value.addEventListener('mousemove', mouseDownHandler);
};
至此就完成了簡(jiǎn)單的局部放大效果,雖然還存在一些問題吧。
到此這篇關(guān)于JavaScript Canvas實(shí)現(xiàn)圖片局部放大鏡效果的文章就介紹到這了,更多相關(guān)JavaScript Canvas圖片局部放大內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS中創(chuàng)建自定義類型的常用模式總結(jié)【工廠模式,構(gòu)造函數(shù)模式,原型模式,動(dòng)態(tài)原型模式等】
這篇文章主要介紹了JS中創(chuàng)建自定義類型的常用模式,結(jié)合實(shí)例形式總結(jié)分析了工廠模式,構(gòu)造函數(shù)模式,原型模式,動(dòng)態(tài)原型模式等javascript創(chuàng)建自定義類型的常用模式與相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2019-01-01
javascript 函數(shù)參數(shù)原來是可以有缺省值的
前幾天看 javaEye 時(shí)看到一個(gè)童鞋寫的 getElementsByClassName 函數(shù)。2010-07-07
window.onload與$(document).ready()的區(qū)別分析
這篇文章主要介紹了window.onload與$(document).ready()的區(qū)別,實(shí)例分析了二者在加載頁面元素使用過程中的區(qū)別,需要的朋友可以參考下2015-05-05

