<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          使用 Vue3 實(shí)現(xiàn) Web 端自定義截屏

          共 16578字,需瀏覽 34分鐘

           ·

          2021-02-04 03:07

          前言

          當(dāng)客戶在使用我們的產(chǎn)品過(guò)程中,遇到問(wèn)題需要向我們反饋時(shí),如果用純文字的形式描述,我們很難懂客戶的意思,要是能配上問(wèn)題截圖,這樣我們就能很清楚的知道客戶的問(wèn)題了。

          那么,我們就需要為我們的產(chǎn)品實(shí)現(xiàn)一個(gè)自定義截屏的功能,用戶點(diǎn)完"截圖"按鈕后,框選任意區(qū)域,隨后在框選的區(qū)域內(nèi)進(jìn)行圈選、畫箭頭、馬賽克、直線、打字等操作,做完操作后用戶可以選擇保存框選區(qū)域的內(nèi)容到本地或者直接發(fā)送給我們。

          聰明的開(kāi)發(fā)者可能已經(jīng)猜到了,這是QQ/微信的截圖功能,我的開(kāi)源項(xiàng)目正好做到了截圖功能,在做之前我找了很多資料,沒(méi)有發(fā)現(xiàn)web端有這種東西存在,于是我就決定參照QQ的截圖自己實(shí)現(xiàn)一個(gè)并做成插件供大家使用。

          本文就跟大家分享下我在做這個(gè)"自定義截屏功能"時(shí)的實(shí)現(xiàn)思路以及過(guò)程,歡迎各位感興趣的開(kāi)發(fā)者閱讀本文。

          運(yùn)行結(jié)果視頻:

          寫在前面

          本文插件的寫法采用的是Vue3的compositionAPI,如果對(duì)其不了解的開(kāi)發(fā)者請(qǐng)移步我的另一篇文章:使用Vue3的CompositionAPI來(lái)優(yōu)化代碼量

          實(shí)現(xiàn)思路

          我們先來(lái)看下QQ的截屏流程,進(jìn)而分析它是怎么實(shí)現(xiàn)的。

          截屏流程分析

          我們先來(lái)分析下,截屏?xí)r的具體流程。

          • 點(diǎn)擊截屏按鈕后,我們會(huì)發(fā)現(xiàn)頁(yè)面上所有動(dòng)態(tài)效果都靜止不動(dòng)了,如下所示。

          • 隨后,我們按住鼠標(biāo)左鍵進(jìn)行拖動(dòng),屏幕上會(huì)出現(xiàn)黑色蒙板,鼠標(biāo)的拖動(dòng)區(qū)域會(huì)出現(xiàn)鏤空效果,如下所示(此處圖片過(guò)大,無(wú)法展示請(qǐng)移步原文查看)

          • 完成拖拽后,框選區(qū)域的下方會(huì)出現(xiàn)工具欄,里面有框選、圈選、箭頭、直線、畫筆等工具,如下圖所示。

            image-20210201142541572
          • 點(diǎn)擊工具欄中任意一個(gè)圖標(biāo),會(huì)出現(xiàn)畫筆選擇區(qū)域,在這里可以選擇畫筆大小、顏色如下所示。

          • 隨后,我們?cè)诳蜻x的區(qū)域內(nèi)進(jìn)行拖拽就會(huì)繪制出對(duì)應(yīng)的圖形,如下所示。

            image-20210201144004992
          • 最后,點(diǎn)擊截圖工具欄的下載圖標(biāo)即可將圖片保存至本地,或者點(diǎn)擊對(duì)號(hào)圖片會(huì)自動(dòng)粘貼到聊天輸入框,如下所示。

          截屏實(shí)現(xiàn)思路

          通過(guò)上述截屏流程,我們便得到了下述實(shí)現(xiàn)思路:

          • 獲取當(dāng)前可視區(qū)域的內(nèi)容,將其存儲(chǔ)起來(lái)
          • 為整個(gè)cnavas畫布繪制蒙層
          • 在獲取到的內(nèi)容中進(jìn)行拖拽,繪制鏤空選區(qū)
          • 選擇截圖工具欄的工具,選擇畫筆大小等信息
          • 在選區(qū)內(nèi)拖拽繪制對(duì)應(yīng)的圖形
          • 將選區(qū)內(nèi)的內(nèi)容轉(zhuǎn)換為圖片

          實(shí)現(xiàn)過(guò)程

          我們分析出了實(shí)現(xiàn)思路,接下來(lái)我們將上述思路逐一進(jìn)行實(shí)現(xiàn)。

          獲取當(dāng)前可視區(qū)域內(nèi)容

          當(dāng)點(diǎn)擊截圖按鈕后,我們需要獲取整個(gè)可視區(qū)域的內(nèi)容,后續(xù)所有的操作都是在獲取的內(nèi)容上進(jìn)行的,在web端我們可以使用canvas來(lái)實(shí)現(xiàn)這些操作。

          那么,我們就需要先將body區(qū)域的內(nèi)容轉(zhuǎn)換為canvas,如果要從零開(kāi)始實(shí)現(xiàn)這個(gè)轉(zhuǎn)換,有點(diǎn)復(fù)雜而且工作量很大。

          還好在前端社區(qū)中有個(gè)開(kāi)源庫(kù)叫html2canvas可以實(shí)現(xiàn)將指定dom轉(zhuǎn)換為canvas,我們就采用這個(gè)庫(kù)來(lái)實(shí)現(xiàn)我們的轉(zhuǎn)換。

          接下來(lái),我們來(lái)看下具體實(shí)現(xiàn)過(guò)程:

          新建一個(gè)名為screen-short.vue的文件,用于承載我們的整個(gè)截圖組件。

          • 首先我們需要一個(gè)canvas容器來(lái)顯示轉(zhuǎn)換后的可視區(qū)域內(nèi)容

          此處只展示了部分代碼,完整代碼請(qǐng)移步:screen-short.vue

          • 在組件掛載時(shí),調(diào)用html2canvas提供的方法,將body中的內(nèi)容轉(zhuǎn)換為canvas,存儲(chǔ)起來(lái)。
          import?html2canvas?from?"html2canvas";
          import?InitData?from?"@/module/main-entrance/InitData";

          export?default?class?EventMonitoring?{
          ??//?當(dāng)前實(shí)例的響應(yīng)式data數(shù)據(jù)
          ??private?readonly?data:?InitData;
          ??//?截圖區(qū)域canvas容器
          ??private?screenShortController:?Refnull>;
          ??//?截圖圖片存放容器
          ??private?screenShortImageController:?HTMLCanvasElement?|?undefined;
          ??
          ??constructor(props:?Record<string,?any>,?context:?SetupContext<any>)?{
          ????//?實(shí)例化響應(yīng)式data
          ????this.data?=?new?InitData();
          ????//?獲取截圖區(qū)域canvas容器
          ????this.screenShortController?=?this.data.getScreenShortController();
          ????
          ????onMounted(()?=>?{
          ??????//?設(shè)置截圖區(qū)域canvas寬高
          ??????this.data.setScreenShortInfo(window.innerWidth,?window.innerHeight);
          ??????
          ??????html2canvas(document.body,?{}).then(canvas?=>?{
          ????????//?裝載截圖的dom為null則退出
          ????????if?(this.screenShortController.value?==?null)?return;
          ????????
          ????????//?存放html2canvas截取的內(nèi)容
          ????????this.screenShortImageController?=?canvas;
          ??????})
          ????})
          ??}
          }

          此處只展示了部分代碼,完整代碼請(qǐng)移步:EventMonitoring.ts

          為canvas畫布繪制蒙層

          我們拿到了轉(zhuǎn)換后的dom后,我們就需要繪制一個(gè)透明度為0.6的黑色蒙層,告知用戶你現(xiàn)在處于截屏區(qū)域選區(qū)狀態(tài)。

          具體實(shí)現(xiàn)過(guò)程如下:

          • 創(chuàng)建DrawMasking.ts文件,蒙層的繪制邏輯在此文件中實(shí)現(xiàn),代碼如下。
          /**
          ?*?繪制蒙層
          ?*?@param?context?需要進(jìn)行繪制canvas
          ?*/

          export?function?drawMasking(context:?CanvasRenderingContext2D)?{
          ??//?清除畫布
          ??context.clearRect(0,?0,?window.innerWidth,?window.innerHeight);
          ??//?繪制蒙層
          ??context.save();
          ??context.fillStyle?=?"rgba(0,?0,?0,?.6)";
          ??context.fillRect(0,?0,?window.innerWidth,?window.innerHeight);
          ??//?繪制結(jié)束
          ??context.restore();
          }

          ??注釋已經(jīng)寫的很詳細(xì)了,對(duì)上述API不懂的開(kāi)發(fā)者請(qǐng)移步:clearRect、save、fillStyle、fillRect、restore

          • html2canvas函數(shù)回調(diào)中調(diào)用繪制蒙層函數(shù)
          html2canvas(document.body,?{}).then(canvas?=>?{
          ??//?獲取截圖區(qū)域畫canvas容器畫布
          ??const?context?=?this.screenShortController.value?.getContext("2d");
          ??if?(context?==?null)?return;
          ??//?繪制蒙層
          ??drawMasking(context);
          })

          繪制鏤空選區(qū)

          我們?cè)诤谏蓪又型献r(shí),需要獲取鼠標(biāo)按下時(shí)的起始點(diǎn)坐標(biāo)以及鼠標(biāo)移動(dòng)時(shí)的坐標(biāo),根據(jù)起始點(diǎn)坐標(biāo)和移動(dòng)時(shí)的坐標(biāo),我們就可以得到一個(gè)區(qū)域,此時(shí)我們將這塊區(qū)域的蒙層鑿開(kāi),將獲取到的canvas圖片內(nèi)容繪制到蒙層下方,這樣我們就實(shí)現(xiàn)了鏤空選區(qū)效果。

          整理下上述話語(yǔ),思路如下:

          • 監(jiān)聽(tīng)鼠標(biāo)按下、移動(dòng)、抬起事件
          • 獲取鼠標(biāo)按下、移動(dòng)時(shí)的坐標(biāo)
          • 根據(jù)獲取到的坐標(biāo)鑿開(kāi)蒙層
          • 將獲取到的canvas圖片內(nèi)容繪制到蒙層下方
          • 實(shí)現(xiàn)鏤空選區(qū)的拖拽與縮放

          實(shí)現(xiàn)的效果如下:

          具體代碼如下:

          export?default?class?EventMonitoring?{
          ???//?當(dāng)前實(shí)例的響應(yīng)式data數(shù)據(jù)
          ??private?readonly?data:?InitData;
          ??
          ??//?截圖區(qū)域canvas容器
          ??private?screenShortController:?Refnull>;
          ??//?截圖圖片存放容器
          ??private?screenShortImageController:?HTMLCanvasElement?|?undefined;
          ??//?截圖區(qū)域畫布
          ??private?screenShortCanvas:?CanvasRenderingContext2D?|?undefined;
          ??
          ??//?圖形位置參數(shù)
          ??private?drawGraphPosition:?positionInfoType?=?{
          ????startX:?0,
          ????startY:?0,
          ????width:?0,
          ????height:?0
          ??};
          ??//?臨時(shí)圖形位置參數(shù)
          ??private?tempGraphPosition:?positionInfoType?=?{
          ????startX:?0,
          ????startY:?0,
          ????width:?0,
          ????height:?0
          ??};

          ??//?裁剪框邊框節(jié)點(diǎn)坐標(biāo)事件
          ??private?cutOutBoxBorderArr:?Array?=?[];
          ??
          ??//?裁剪框頂點(diǎn)邊框直徑大小
          ??private?borderSize?=?10;
          ??//?當(dāng)前操作的邊框節(jié)點(diǎn)
          ??private?borderOption:?number?|?null?=?null;
          ??
          ??//?點(diǎn)擊裁剪框時(shí)的鼠標(biāo)坐標(biāo)
          ??private?movePosition:?movePositionType?=?{
          ????moveStartX:?0,
          ????moveStartY:?0
          ??};

          ??//?裁剪框修剪狀態(tài)
          ??private?draggingTrim?=?false;
          ??//?裁剪框拖拽狀態(tài)
          ??private?dragging?=?false;
          ??//?鼠標(biāo)點(diǎn)擊狀態(tài)
          ??private?clickFlag?=?false;
          ??
          ??constructor(props:?Record<string,?any>,?context:?SetupContext<any>)?{
          ?????//?實(shí)例化響應(yīng)式data
          ????this.data?=?new?InitData();
          ????
          ????//?獲取截圖區(qū)域canvas容器
          ????this.screenShortController?=?this.data.getScreenShortController();
          ????
          ????onMounted(()?=>?{
          ??????//?設(shè)置截圖區(qū)域canvas寬高
          ??????this.data.setScreenShortInfo(window.innerWidth,?window.innerHeight);
          ??????
          ??????html2canvas(document.body,?{}).then(canvas?=>?{
          ????????//?裝載截圖的dom為null則退出
          ????????if?(this.screenShortController.value?==?null)?return;

          ????????//?存放html2canvas截取的內(nèi)容
          ????????this.screenShortImageController?=?canvas;
          ????????//?獲取截圖區(qū)域畫canvas容器畫布
          ????????const?context?=?this.screenShortController.value?.getContext("2d");
          ????????if?(context?==?null)?return;

          ????????//?賦值截圖區(qū)域canvas畫布
          ????????this.screenShortCanvas?=?context;
          ????????//?繪制蒙層
          ????????drawMasking(context);

          ????????//?添加監(jiān)聽(tīng)
          ????????this.screenShortController.value?.addEventListener(
          ??????????"mousedown",
          ??????????this.mouseDownEvent
          ????????);
          ????????this.screenShortController.value?.addEventListener(
          ??????????"mousemove",
          ??????????this.mouseMoveEvent
          ????????);
          ????????this.screenShortController.value?.addEventListener(
          ??????????"mouseup",
          ??????????this.mouseUpEvent
          ????????);
          ??????})
          ????})
          ??}
          ??//?鼠標(biāo)按下事件
          ??private?mouseDownEvent?=?(event:?MouseEvent)?=>?{
          ????this.dragging?=?true;
          ????this.clickFlag?=?true;
          ????
          ????const?mouseX?=?nonNegativeData(event.offsetX);
          ????const?mouseY?=?nonNegativeData(event.offsetY);
          ????
          ????//?如果操作的是裁剪框
          ????if?(this.borderOption)?{
          ??????//?設(shè)置為拖動(dòng)狀態(tài)
          ??????this.draggingTrim?=?true;
          ??????//?記錄移動(dòng)時(shí)的起始點(diǎn)坐標(biāo)
          ??????this.movePosition.moveStartX?=?mouseX;
          ??????this.movePosition.moveStartY?=?mouseY;
          ????}?else?{
          ??????//?繪制裁剪框,記錄當(dāng)前鼠標(biāo)開(kāi)始坐標(biāo)
          ??????this.drawGraphPosition.startX?=?mouseX;
          ??????this.drawGraphPosition.startY?=?mouseY;
          ????}
          ??}
          ??
          ??//?鼠標(biāo)移動(dòng)事件
          ??private?mouseMoveEvent?=?(event:?MouseEvent)?=>?{
          ????this.clickFlag?=?false;
          ????
          ????//?獲取裁剪框位置信息
          ????const?{?startX,?startY,?width,?height?}?=?this.drawGraphPosition;
          ????//?獲取當(dāng)前鼠標(biāo)坐標(biāo)
          ????const?currentX?=?nonNegativeData(event.offsetX);
          ????const?currentY?=?nonNegativeData(event.offsetY);
          ????//?裁剪框臨時(shí)寬高
          ????const?tempWidth?=?currentX?-?startX;
          ????const?tempHeight?=?currentY?-?startY;
          ????
          ????//?執(zhí)行裁剪框操作函數(shù)
          ????this.operatingCutOutBox(
          ??????currentX,
          ??????currentY,
          ??????startX,
          ??????startY,
          ??????width,
          ??????height,
          ??????this.screenShortCanvas
          ????);
          ????//?如果鼠標(biāo)未點(diǎn)擊或者當(dāng)前操作的是裁剪框都return
          ????if?(!this.dragging?||?this.draggingTrim)?return;
          ????//?繪制裁剪框
          ????this.tempGraphPosition?=?drawCutOutBox(
          ??????startX,
          ??????startY,
          ??????tempWidth,
          ??????tempHeight,
          ??????this.screenShortCanvas,
          ??????this.borderSize,
          ??????this.screenShortController.value?as?HTMLCanvasElement,
          ??????this.screenShortImageController?as?HTMLCanvasElement
          ????)?as?drawCutOutBoxReturnType;
          ??}
          ??
          ????//?鼠標(biāo)抬起事件
          ??private?mouseUpEvent?=?()?=>?{
          ????//?繪制結(jié)束
          ????this.dragging?=?false;
          ????this.draggingTrim?=?false;
          ????
          ????//?保存繪制后的圖形位置信息
          ????this.drawGraphPosition?=?this.tempGraphPosition;
          ????
          ????//?如果工具欄未點(diǎn)擊則保存裁剪框位置
          ????if?(!this.data.getToolClickStatus().value)?{
          ??????const?{?startX,?startY,?width,?height?}?=?this.drawGraphPosition;
          ??????this.data.setCutOutBoxPosition(startX,?startY,?width,?height);
          ????}
          ????//?保存邊框節(jié)點(diǎn)信息
          ????this.cutOutBoxBorderArr?=?saveBorderArrInfo(
          ??????this.borderSize,
          ??????this.drawGraphPosition
          ????);
          ??}
          }

          ??繪制鏤空選區(qū)的代碼較多,此處僅僅展示了鼠標(biāo)的三個(gè)事件監(jiān)聽(tīng)的相關(guān)代碼,完整代碼請(qǐng)移步:EventMonitoring.ts

          • 繪制裁剪框的代碼如下
          /**
          ?*?繪制裁剪框
          ?*?@param?mouseX?鼠標(biāo)x軸坐標(biāo)
          ?*?@param?mouseY?鼠標(biāo)y軸坐標(biāo)
          ?*?@param?width?裁剪框?qū)挾?br>?*?@param?height?裁剪框高度
          ?*?@param?context?需要進(jìn)行繪制的canvas畫布
          ?*?@param?borderSize?邊框節(jié)點(diǎn)直徑
          ?*?@param?controller?需要進(jìn)行操作的canvas容器
          ?*?@param?imageController?圖片canvas容器
          ?*?@private
          ?*/

          export?function?drawCutOutBox(
          ??mouseX:?number,
          ??mouseY:?number,
          ??width:?number,
          ??height:?number,
          ??context:?CanvasRenderingContext2D,
          ??borderSize:?number,
          ??controller:?HTMLCanvasElement,
          ??imageController:?HTMLCanvasElement
          )?
          {
          ??//?獲取畫布寬高
          ??const?canvasWidth?=?controller?.width;
          ??const?canvasHeight?=?controller?.height;

          ??//?畫布、圖片不存在則return
          ??if?(!canvasWidth?||?!canvasHeight?||?!imageController?||?!controller)?return;

          ??//?清除畫布
          ??context.clearRect(0,?0,?canvasWidth,?canvasHeight);

          ??//?繪制蒙層
          ??context.save();
          ??context.fillStyle?=?"rgba(0,?0,?0,?.6)";
          ??context.fillRect(0,?0,?canvasWidth,?canvasHeight);
          ??//?將蒙層鑿開(kāi)
          ??context.globalCompositeOperation?=?"source-atop";
          ??//?裁剪選擇框
          ??context.clearRect(mouseX,?mouseY,?width,?height);
          ??//?繪制8個(gè)邊框像素點(diǎn)并保存坐標(biāo)信息以及事件參數(shù)
          ??context.globalCompositeOperation?=?"source-over";
          ??context.fillStyle?=?"#2CABFF";
          ??//?像素點(diǎn)大小
          ??const?size?=?borderSize;
          ??//?繪制像素點(diǎn)
          ??context.fillRect(mouseX?-?size?/?2,?mouseY?-?size?/?2,?size,?size);
          ??context.fillRect(
          ????mouseX?-?size?/?2?+?width?/?2,
          ????mouseY?-?size?/?2,
          ????size,
          ????size
          ??);
          ??context.fillRect(mouseX?-?size?/?2?+?width,?mouseY?-?size?/?2,?size,?size);
          ??context.fillRect(
          ????mouseX?-?size?/?2,
          ????mouseY?-?size?/?2?+?height?/?2,
          ????size,
          ????size
          ??);
          ??context.fillRect(
          ????mouseX?-?size?/?2?+?width,
          ????mouseY?-?size?/?2?+?height?/?2,
          ????size,
          ????size
          ??);
          ??context.fillRect(mouseX?-?size?/?2,?mouseY?-?size?/?2?+?height,?size,?size);
          ??context.fillRect(
          ????mouseX?-?size?/?2?+?width?/?2,
          ????mouseY?-?size?/?2?+?height,
          ????size,
          ????size
          ??);
          ??context.fillRect(
          ????mouseX?-?size?/?2?+?width,
          ????mouseY?-?size?/?2?+?height,
          ????size,
          ????size
          ??);
          ??//?繪制結(jié)束
          ??context.restore();
          ??//?使用drawImage將圖片繪制到蒙層下方
          ??context.save();
          ??context.globalCompositeOperation?=?"destination-over";
          ??context.drawImage(
          ????imageController,
          ????0,
          ????0,
          ????controller?.width,
          ????controller?.height
          ??);
          ??context.restore();
          ??//?返回裁剪框臨時(shí)位置信息
          ??return?{
          ????startX:?mouseX,
          ????startY:?mouseY,
          ????width:?width,
          ????height:?height
          ??};
          }

          ??同樣的,注釋寫的很詳細(xì),上述代碼用到的canvas API除了之前介紹的外,用到的新的API如下:globalCompositeOperation、drawImage

          實(shí)現(xiàn)截圖工具欄

          我們實(shí)現(xiàn)鏤空選區(qū)的相關(guān)功能后,接下來(lái)要做的就是在選區(qū)內(nèi)進(jìn)行圈選、框選、畫線等操作了,在QQ的截圖中這些操作位于截圖工具欄內(nèi),因此我們要將截圖工具欄做出來(lái),做到與canvas交互。

          在截圖工具欄的布局上,一開(kāi)始我的想法是直接在canvas畫布中把這些工具畫出來(lái),這樣應(yīng)該更容易交互一點(diǎn),但是我看了相關(guān)的api后,發(fā)現(xiàn)有點(diǎn)麻煩,把問(wèn)題復(fù)雜化了。

          琢磨了一陣后,想明白了,這塊還是需要使用div進(jìn)行布局的,在裁剪框繪制完畢后,根據(jù)裁剪框的位置信息計(jì)算出截圖工具欄的位置,改變其位置即可。

          工具欄與canvas的交互,可以綁定一個(gè)點(diǎn)擊事件到EventMonitoring.ts中,獲取當(dāng)前點(diǎn)擊項(xiàng),指定與之對(duì)應(yīng)的圖形繪制函數(shù)。

          實(shí)現(xiàn)的效果如下:

          222

          具體的實(shí)現(xiàn)過(guò)程如下:

          • screen-short.vue中,創(chuàng)建截圖工具欄div并布局好其樣式
          <template>
          ??<teleport?to="body">
          ???????
          ????<div
          ??????id="toolPanel"
          ??????v-show="toolStatus"
          ??????:style="{?left:?toolLeft?+?'px',?top:?toolTop?+?'px'?}"
          ??????ref="toolController"
          ????>

          ??????<div
          ????????v-for="item?in?toolbar"
          ????????:key="item.id"
          ????????:class="`item-panel?${item.title}?`"
          ????????@click="toolClickEvent(item.title,?item.id,?$event)"
          ??????>
          div>
          ??????
          ??????<div
          ????????v-if="undoStatus"
          ????????class="item-panel?undo"
          ????????@click="toolClickEvent('undo',?9,?$event)"
          ??????>
          div>
          ??????<div?v-else?class="item-panel?undo-disabled">div>
          ??????
          ??????<div
          ????????class="item-panel?close"
          ????????@click="toolClickEvent('close',?10,?$event)"
          ??????>
          div>
          ??????<div
          ????????class="item-panel?confirm"
          ????????@click="toolClickEvent('confirm',?11,?$event)"
          ??????>
          div>
          ????div>
          ??teleport>
          template>

          <script?lang="ts">
          import?eventMonitoring?from?"@/module/main-entrance/EventMonitoring";
          import?toolbar?from?"@/module/config/Toolbar.ts";

          export?default?{
          ??name:?"screen-short",
          ??setup(props:?Record,?context:?SetupContext)?{
          ????const?event?=?new?eventMonitoring(props,?context?as?SetupContext);
          ????const?toolClickEvent?=?event.toolClickEvent;
          ????return?{
          ??????toolClickEvent,
          ??????toolbar
          ????}
          ??}
          }
          script>

          ??上述代碼僅展示了組件的部分代碼,完整代碼請(qǐng)移步:screen-short.vue、screen-short.scss

          截圖工具條目點(diǎn)擊樣式處理

          截圖工具欄中的每一個(gè)條目都擁有三種狀態(tài):正常狀態(tài)、鼠標(biāo)移入、點(diǎn)擊,此處我的做法是將所有狀態(tài)寫在css里了,通過(guò)不同的class名來(lái)顯示不同的樣式。

          部分工具欄點(diǎn)擊狀態(tài)的css如下:

          .square-active?{
          ??background-image:?url("~@/assets/img/square-click.png");
          }

          .round-active?{
          ??background-image:?url("~@/assets/img/round-click.png");
          }

          .right-top-active?{
          ??background-image:?url("~@/assets/img/right-top-click.png");
          }

          一開(kāi)始我想在v-for渲染時(shí),定義一個(gè)變量,點(diǎn)擊時(shí)改變這個(gè)變量的狀態(tài),顯示每個(gè)點(diǎn)擊條目對(duì)應(yīng)的點(diǎn)擊時(shí)的樣式,但是我在做的時(shí)候卻發(fā)現(xiàn)問(wèn)題了,我的點(diǎn)擊時(shí)的class名是動(dòng)態(tài)的,沒(méi)法通過(guò)這種形式來(lái)弄,無(wú)奈我只好選擇dom操作的形式來(lái)實(shí)現(xiàn),點(diǎn)擊時(shí)傳$event到函數(shù),獲取當(dāng)前點(diǎn)擊項(xiàng)點(diǎn)擊時(shí)的class,判斷其是否有選中的class,如果有就刪除,然后為當(dāng)前點(diǎn)擊項(xiàng)添加class。

          實(shí)現(xiàn)代碼如下:

          • dom結(jié)構(gòu)
          <div
          ????v-for="item?in?toolbar"
          ????:key="item.id"
          ????:class="`item-panel?${item.title}?`"
          ????@click="toolClickEvent(item.title,?item.id,?$event)"
          >
          div>
          • 工具欄點(diǎn)擊事件
          ??/**
          ???*?裁剪框工具欄點(diǎn)擊事件
          ???*?@param?toolName
          ???*?@param?index
          ???*?@param?mouseEvent
          ???*/

          ??public?toolClickEvent?=?(
          ????toolName:?string,
          ????index:?number,
          ????mouseEvent:?MouseEvent
          ??)?=>?{
          ????//?為當(dāng)前點(diǎn)擊項(xiàng)添加選中時(shí)的class名
          ????setSelectedClassName(mouseEvent,?index,?false);
          ??}
          • 為當(dāng)前點(diǎn)擊項(xiàng)添加選中時(shí)的class,移除其兄弟元素選中時(shí)的class
          import?{?getSelectedClassName?}?from?"@/module/common-methords/GetSelectedCalssName";
          import?{?getBrushSelectedName?}?from?"@/module/common-methords/GetBrushSelectedName";

          /**
          ?*?為當(dāng)前點(diǎn)擊項(xiàng)添加選中時(shí)的class,移除其兄弟元素選中時(shí)的class
          ?*?@param?mouseEvent?需要進(jìn)行操作的元素
          ?*?@param?index?當(dāng)前點(diǎn)擊項(xiàng)
          ?*?@param?isOption?是否為畫筆選項(xiàng)
          ?*/

          export?function?setSelectedClassName(
          ??mouseEvent:?any,
          ??index:?number,
          ??isOption:?boolean
          )?
          {
          ??//?獲取當(dāng)前點(diǎn)擊項(xiàng)選中時(shí)的class名
          ??let?className?=?getSelectedClassName(index);
          ??if?(isOption)?{
          ????//?獲取畫筆選項(xiàng)選中時(shí)的對(duì)應(yīng)的class
          ????className?=?getBrushSelectedName(index);
          ??}
          ??//?獲取div下的所有子元素
          ??const?nodes?=?mouseEvent.path[1].children;
          ??for?(let?i?=?0;?i?????const?item?=?nodes[i];
          ????//?如果工具欄中已經(jīng)有選中的class則將其移除
          ????if?(item.className.includes("active"))?{
          ??????item.classList.remove(item.classList[2]);
          ????}
          ??}
          ??//?給當(dāng)前點(diǎn)擊項(xiàng)添加選中時(shí)的class
          ??mouseEvent.target.className?+=?"?"?+?className;
          }

          • 獲取截圖工具欄點(diǎn)擊時(shí)的class名
          export?function?getSelectedClassName(index:?number)?{
          ??let?className?=?"";
          ??switch?(index)?{
          ????case?1:
          ??????className?=?"square-active";
          ??????break;
          ????case?2:
          ??????className?=?"round-active";
          ??????break;
          ????case?3:
          ??????className?=?"right-top-active";
          ??????break;
          ????case?4:
          ??????className?=?"brush-active";
          ??????break;
          ????case?5:
          ??????className?=?"mosaicPen-active";
          ??????break;
          ????case?6:
          ??????className?=?"text-active";
          ??}
          ??return?className;
          }

          • 獲取畫筆選擇點(diǎn)擊時(shí)的class名
          /**
          ?*?獲取畫筆選項(xiàng)對(duì)應(yīng)的選中時(shí)的class名
          ?*?@param?itemName
          ?*/

          export?function?getBrushSelectedName(itemName:?number)?{
          ??let?className?=?"";
          ??switch?(itemName)?{
          ????case?1:
          ??????className?=?"brush-small-active";
          ??????break;
          ????case?2:
          ??????className?=?"brush-medium-active";
          ??????break;
          ????case?3:
          ??????className?=?"brush-big-active";
          ??????break;
          ??}
          ??return?className;
          }

          實(shí)現(xiàn)工具欄中的每個(gè)選項(xiàng)

          接下來(lái),我們來(lái)看看工具欄中每個(gè)選項(xiàng)的具體實(shí)現(xiàn)。

          工具欄中每個(gè)圖形的繪制都需要鼠標(biāo)按下、移動(dòng)、抬起這三個(gè)事件的配合下完成,為了防止鼠標(biāo)在移動(dòng)時(shí)圖形重復(fù)繪制,這里我們采用"歷史記錄"模式來(lái)解決這個(gè)問(wèn)題,我們先來(lái)看下重復(fù)繪制時(shí)的場(chǎng)景,如下所示:

          接下來(lái),我們來(lái)看下如何使用歷史記錄來(lái)解決這個(gè)問(wèn)題。

          • 首先,我們需要定義一個(gè)數(shù)組變量,取名為history。
          private?history:?Arraystring,?any>>?=?[];
          • 當(dāng)圖形繪制結(jié)束鼠標(biāo)抬起時(shí),將當(dāng)前畫布狀態(tài)保存至history
          ??/**
          ???*?保存當(dāng)前畫布狀態(tài)
          ???*?@private
          ???*/

          ??private?addHistoy()?{
          ????if?(
          ??????this.screenShortCanvas?!=?null?&&
          ??????this.screenShortController.value?!=?null
          ????)?{
          ??????//?獲取canvas畫布與容器
          ??????const?context?=?this.screenShortCanvas;
          ??????const?controller?=?this.screenShortController.value;
          ??????if?(this.history.length?>?this.maxUndoNum)?{
          ????????//?刪除最早的一條畫布記錄
          ????????this.history.unshift();
          ??????}
          ??????//?保存當(dāng)前畫布狀態(tài)
          ??????this.history.push({
          ????????data:?context.getImageData(0,?0,?controller.width,?controller.height)
          ??????});
          ??????//?啟用撤銷按鈕
          ??????this.data.setUndoStatus(true);
          ????}
          ??}
          • 當(dāng)鼠標(biāo)處于移動(dòng)狀態(tài)時(shí),我們?nèi)〕?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;">history中最后一條記錄。
          ??/**
          ???*?顯示最新的畫布狀態(tài)
          ???*?@private
          ???*/

          ??private?showLastHistory()?{
          ????if?(this.screenShortCanvas?!=?null)?{
          ??????const?context?=?this.screenShortCanvas;
          ??????if?(this.history.length?<=?0)?{
          ????????this.addHistoy();
          ??????}
          ??????context.putImageData(this.history[this.history.length?-?1]["data"],?0,?0);
          ????}
          ??}

          上述函數(shù)放在合適的時(shí)機(jī)執(zhí)行,即可解決圖形重復(fù)繪制的問(wèn)題,接下來(lái)我們看下解決后的繪制效果,如下所示:

          實(shí)現(xiàn)矩形繪制

          在前面的分析中,我們拿到了鼠標(biāo)的起始點(diǎn)坐標(biāo)和鼠標(biāo)移動(dòng)時(shí)的坐標(biāo),我們可以通過(guò)這些數(shù)據(jù)計(jì)算出框選區(qū)域的寬高,如下所示。

          //?獲取鼠標(biāo)起始點(diǎn)坐標(biāo)
          const?{?startX,?startY?}?=?this.drawGraphPosition;
          //?獲取當(dāng)前鼠標(biāo)坐標(biāo)
          const?currentX?=?nonNegativeData(event.offsetX);
          const?currentY?=?nonNegativeData(event.offsetY);
          //?裁剪框臨時(shí)寬高
          const?tempWidth?=?currentX?-?startX;
          const?tempHeight?=?currentY?-?startY;

          我們拿到這些數(shù)據(jù)后,即可通過(guò)canvas的rect這個(gè)API來(lái)繪制一個(gè)矩形了,代碼如下所示:

          /**
          ?*?繪制矩形
          ?*?@param?mouseX
          ?*?@param?mouseY
          ?*?@param?width
          ?*?@param?height
          ?*?@param?color?邊框顏色
          ?*?@param?borderWidth?邊框大小
          ?*?@param?context?需要進(jìn)行繪制的canvas畫布
          ?*?@param?controller?需要進(jìn)行操作的canvas容器
          ?*?@param?imageController?圖片canvas容器
          ?*/

          export?function?drawRectangle(
          ??mouseX:?number,
          ??mouseY:?number,
          ??width:?number,
          ??height:?number,
          ??color:?string,
          ??borderWidth:?number,
          ??context:?CanvasRenderingContext2D,
          ??controller:?HTMLCanvasElement,
          ??imageController:?HTMLCanvasElement
          )?
          {
          ??context.save();
          ??//?設(shè)置邊框顏色
          ??context.strokeStyle?=?color;
          ??//?設(shè)置邊框大小
          ??context.lineWidth?=?borderWidth;
          ??context.beginPath();
          ??//?繪制矩形
          ??context.rect(mouseX,?mouseY,?width,?height);
          ??context.stroke();
          ??//?繪制結(jié)束
          ??context.restore();
          ??//?使用drawImage將圖片繪制到蒙層下方
          ??context.save();
          ??context.globalCompositeOperation?=?"destination-over";
          ??context.drawImage(
          ????imageController,
          ????0,
          ????0,
          ????controller?.width,
          ????controller?.height
          ??);
          ??//?繪制結(jié)束
          ??context.restore();
          }

          實(shí)現(xiàn)橢圓繪制

          在繪制橢圓時(shí),我們需要根據(jù)坐標(biāo)信息計(jì)算出圓的半徑、圓心坐標(biāo),隨后調(diào)用ellipse函數(shù)即可繪制一個(gè)橢圓出來(lái),代碼如下所示:

          /**
          ?*?繪制圓形
          ?*?@param?context?需要進(jìn)行繪制的畫布
          ?*?@param?mouseX?當(dāng)前鼠標(biāo)x軸坐標(biāo)
          ?*?@param?mouseY?當(dāng)前鼠標(biāo)y軸坐標(biāo)
          ?*?@param?mouseStartX?鼠標(biāo)按下時(shí)的x軸坐標(biāo)
          ?*?@param?mouseStartY?鼠標(biāo)按下時(shí)的y軸坐標(biāo)
          ?*?@param?borderWidth?邊框?qū)挾?br>?*?@param?color?邊框顏色
          ?*/

          export?function?drawCircle(
          ??context:?CanvasRenderingContext2D,
          ??mouseX:?number,
          ??mouseY:?number,
          ??mouseStartX:?number,
          ??mouseStartY:?number,
          ??borderWidth:?number,
          ??color:?string
          )?
          {
          ??//?坐標(biāo)邊界處理,解決反向繪制橢圓時(shí)的報(bào)錯(cuò)問(wèn)題
          ??const?startX?=?mouseX???const?startY?=?mouseY???const?endX?=?mouseX?>=?mouseStartX???mouseX?:?mouseStartX;
          ??const?endY?=?mouseY?>=?mouseStartY???mouseY?:?mouseStartY;
          ??//?計(jì)算圓的半徑
          ??const?radiusX?=?(endX?-?startX)?*?0.5;
          ??const?radiusY?=?(endY?-?startY)?*?0.5;
          ??//?計(jì)算圓心的x、y坐標(biāo)
          ??const?centerX?=?startX?+?radiusX;
          ??const?centerY?=?startY?+?radiusY;
          ??//?開(kāi)始繪制
          ??context.save();
          ??context.beginPath();
          ??context.lineWidth?=?borderWidth;
          ??context.strokeStyle?=?color;

          ??if?(typeof?context.ellipse?===?"function")?{
          ????//?繪制圓,旋轉(zhuǎn)角度與起始角度都為0,結(jié)束角度為2*PI
          ????context.ellipse(centerX,?centerY,?radiusX,?radiusY,?0,?0,?2?*?Math.PI);
          ??}?else?{
          ????throw?"你的瀏覽器不支持ellipse,無(wú)法繪制橢圓";
          ??}
          ??context.stroke();
          ??context.closePath();
          ??//?結(jié)束繪制
          ??context.restore();
          }

          ??注釋已經(jīng)寫的很清楚了,此處用到的API有:beginPath、lineWidth、ellipse、closePath,對(duì)這些API不熟悉的開(kāi)發(fā)者請(qǐng)移步到指定位置進(jìn)行查閱。

          實(shí)現(xiàn)箭頭繪制

          箭頭繪制相比其他工具來(lái)說(shuō)是最復(fù)雜的,因?yàn)槲覀冃枰ㄟ^(guò)三角函數(shù)來(lái)計(jì)算箭頭兩個(gè)點(diǎn)的坐標(biāo),通過(guò)三角函數(shù)中的反正切函數(shù)來(lái)計(jì)算箭頭的角度

          既然需要用到三角函數(shù)來(lái)實(shí)現(xiàn),那我們先來(lái)看下我們的已知條件:

          ??/**
          ???*?已知:
          ???*????1.?P1、P2的坐標(biāo)
          ???*????2.?箭頭斜線P3到P2直線的長(zhǎng)度,P4與P3是對(duì)稱的,因此P4到P2的長(zhǎng)度等于P3到P2的長(zhǎng)度
          ???*????3.?箭頭斜線P3到P1、P2直線的夾角角度(θ),因?yàn)槭菍?duì)稱的,所以P4與P1、P2直線的夾角角度是相等的
          ???*?求:
          ???*????P3、P4的坐標(biāo)
          ???*/

          如上圖所示,P1為鼠標(biāo)按下時(shí)的坐標(biāo),P2為鼠標(biāo)移動(dòng)時(shí)的坐標(biāo),夾角θ的角度為30,我們知道這些信息后就可以求出P3和P4的坐標(biāo)了,求出坐標(biāo)后我們即可通過(guò)canvas的moveTo、lineTo來(lái)繪制箭頭了。

          實(shí)現(xiàn)代碼如下:

          /**
          ?*?繪制箭頭
          ?*?@param?context?需要進(jìn)行繪制的畫布
          ?*?@param?mouseStartX?鼠標(biāo)按下時(shí)的x軸坐標(biāo)?P1
          ?*?@param mouseStartY 鼠標(biāo)按下時(shí)
          的y軸坐標(biāo) P1
          ?*?@param?mouseX?當(dāng)前鼠標(biāo)x軸坐標(biāo)?P2
          ?*?@param?mouseY?當(dāng)前鼠標(biāo)y軸坐標(biāo)?P2
          ?*?@param?theta?箭頭斜線與直線的夾角角度?(θ)?P3?--->?(P1、P2)?||?P4?--->?P1(P1、P2)
          ?*?@param?headlen?箭頭斜線的長(zhǎng)度?P3?--->?P2?||?P4?--->?P2
          ?*?@param?borderWidth?邊框?qū)挾?br>?*?@param?color?邊框顏色
          ?*/

          export?function?drawLineArrow(
          ??context:?CanvasRenderingContext2D,
          ??mouseStartX:?number,
          ??mouseStartY:?number,
          ??mouseX:?number,
          ??mouseY:?number,
          ??theta:?number,
          ??headlen:?number,
          ??borderWidth:?number,
          ??color:?string
          )?
          {
          ??
          /**
          ???*?已知:
          ???*????1.?P1、P2的坐標(biāo)
          ???*????2.?箭頭斜線(P3?||?P4)?--->?P2直線的長(zhǎng)度
          ???*????3.?箭頭斜線(P3?||?P4)?--->?(P1、P2)直線的夾角角度(θ)
          ???*?求:
          ???*????P3、P4的坐標(biāo)
          ???*/

          ??
          const?angle?=
          ??????(
          Math.atan2(mouseStartY?-?mouseY,?mouseStartX?-?mouseX)?*?180)?/?Math.PI,?//?通過(guò)atan2來(lái)獲取箭頭的角度
          ????angle1?=?((angle?+?theta)?*?
          Math.PI)?/?180,?//?P3點(diǎn)的角度
          ????angle2?=?((angle?-?theta)?*?
          Math.PI)?/?180,?//?P4點(diǎn)的角度
          ????topX?=?headlen?*?
          Math.cos(angle1),?//?P3點(diǎn)的x軸坐標(biāo)
          ????topY?=?headlen?*?
          Math.sin(angle1),?//?P3點(diǎn)的y軸坐標(biāo)
          ????botX?=?headlen?*?
          Math.cos(angle2),?//?P4點(diǎn)的X軸坐標(biāo)
          ????botY?=?headlen?*?
          Math.sin(angle2);?//?P4點(diǎn)的Y軸坐標(biāo)

          ??
          //?開(kāi)始繪制
          ??context.save();
          ??context.beginPath();

          ??
          //?P3的坐標(biāo)位置
          ??
          let?arrowX?=?mouseStartX?-?topX,
          ????arrowY?=?mouseStartY?-?topY;

          ??
          //?移動(dòng)筆觸到P3坐標(biāo)
          ??context.moveTo(arrowX,?arrowY);
          ??
          //?移動(dòng)筆觸到P1
          ??context.moveTo(mouseStartX,?mouseStartY);
          ??
          //?繪制P1到P2的直線
          ??context.lineTo(mouseX,?mouseY);
          ??
          //?計(jì)算P3的位置
          ??arrowX?=?mouseX?+?topX;
          ??arrowY?=?mouseY?+?topY;
          ??
          //?移動(dòng)筆觸到P3坐標(biāo)
          ??context.moveTo(arrowX,?arrowY);
          ??
          //?繪制P2到P3的斜線
          ??context.lineTo(mouseX,?mouseY);
          ??
          //?計(jì)算P4的位置
          ??arrowX?=?mouseX?+?botX;
          ??arrowY?=?mouseY?+?botY;
          ??
          //?繪制P2到P4的斜線
          ??context.lineTo(arrowX,?arrowY);
          ??
          //?上色
          ??context.strokeStyle?=?color;
          ??context.lineWidth?=?borderWidth;
          ??
          //?填充
          ??context.stroke();
          ??
          //?結(jié)束繪制
          ??context.restore();
          }


          ??此處用到的新API有:moveTo、lineTo,對(duì)這些API不熟悉的開(kāi)發(fā)者請(qǐng)移步到指定位置進(jìn)行查閱。

          實(shí)現(xiàn)畫筆繪制

          畫筆的繪制我們需要通過(guò)lineTo來(lái)實(shí)現(xiàn),不過(guò)在繪制時(shí)需要注意:在鼠標(biāo)按下時(shí)需要通過(guò)beginPath來(lái)清空一條路徑,并移動(dòng)畫筆筆觸到鼠標(biāo)按下時(shí)的位置,否則鼠標(biāo)的起始位置始終是0,bug如下所示:

          那么要解決這個(gè)bug,就需要在鼠標(biāo)按下時(shí)初始化一下筆觸位置,代碼如下:

          /**
          ?*?畫筆初始化
          ?*/

          export?function?initPencli(
          ??context:?CanvasRenderingContext2D,
          ??mouseX:?number,
          ??mouseY:?number
          )?
          {
          ??//?開(kāi)始||清空一條路徑
          ??context.beginPath();
          ??//?移動(dòng)畫筆位置
          ??context.moveTo(mouseX,?mouseY);
          }

          隨后,在鼠標(biāo)位置時(shí)根據(jù)坐標(biāo)信息繪制線條即可,代碼如下:

          /**
          ?*?畫筆繪制
          ?*?@param?context
          ?*?@param?mouseX
          ?*?@param?mouseY
          ?*?@param?size
          ?*?@param?color
          ?*/

          export?function?drawPencli(
          ??context:?CanvasRenderingContext2D,
          ??mouseX:?number,
          ??mouseY:?number,
          ??size:?number,
          ??color:?string
          )?
          {
          ??//?開(kāi)始繪制
          ??context.save();
          ??//?設(shè)置邊框大小
          ??context.lineWidth?=?size;
          ??//?設(shè)置邊框顏色
          ??context.strokeStyle?=?color;
          ??context.lineTo(mouseX,?mouseY);
          ??context.stroke();
          ??//?繪制結(jié)束
          ??context.restore();
          }

          實(shí)現(xiàn)馬賽克繪制

          我們都知道圖片是由一個(gè)個(gè)像素點(diǎn)構(gòu)成的,當(dāng)我們把某個(gè)區(qū)域的像素點(diǎn)設(shè)置成同樣的顏色,這塊區(qū)域的信息就會(huì)被破壞掉,被我們破壞掉的區(qū)域就叫馬賽克。

          知道馬賽克的原理后,我們就可以分析出實(shí)現(xiàn)思路:

          • 獲取鼠標(biāo)劃過(guò)路徑區(qū)域的圖像信息
          • 將區(qū)域內(nèi)的像素點(diǎn)繪制成周圍相近的顏色

          具體的實(shí)現(xiàn)代碼如下:

          /**
          ?*?獲取圖像指定坐標(biāo)位置的顏色
          ?*?@param?imgData?需要進(jìn)行操作的圖片
          ?*?@param?x?x點(diǎn)坐標(biāo)
          ?*?@param?y?y點(diǎn)坐標(biāo)
          ?*/

          const?getAxisColor?=?(imgData:?ImageData,?x:?number,?y:?number)?=>?{
          ??const?w?=?imgData.width;
          ??const?d?=?imgData.data;
          ??const?color?=?[];
          ??color[0]?=?d[4?*?(y?*?w?+?x)];
          ??color[1]?=?d[4?*?(y?*?w?+?x)?+?1];
          ??color[2]?=?d[4?*?(y?*?w?+?x)?+?2];
          ??color[3]?=?d[4?*?(y?*?w?+?x)?+?3];
          ??return?color;
          };

          /**
          ?*?設(shè)置圖像指定坐標(biāo)位置的顏色
          ?*?@param?imgData?需要進(jìn)行操作的圖片
          ?*?@param?x?x點(diǎn)坐標(biāo)
          ?*?@param?y?y點(diǎn)坐標(biāo)
          ?*?@param?color?顏色數(shù)組
          ?*/

          const?setAxisColor?=?(
          ??imgData:?ImageData,
          ??x:?number,
          ??y:?number,
          ??color:?Array<number>
          )?=>?{
          ??const?w?=?imgData.width;
          ??const?d?=?imgData.data;
          ??d[4?*?(y?*?w?+?x)]?=?color[0];
          ??d[4?*?(y?*?w?+?x)?+?1]?=?color[1];
          ??d[4?*?(y?*?w?+?x)?+?2]?=?color[2];
          ??d[4?*?(y?*?w?+?x)?+?3]?=?color[3];
          };

          /**
          ?*?繪制馬賽克
          ?*????實(shí)現(xiàn)思路:
          ?*??????1.?獲取鼠標(biāo)劃過(guò)路徑區(qū)域的圖像信息
          ?*??????2.?將區(qū)域內(nèi)的像素點(diǎn)繪制成周圍相近的顏色
          ?*?@param?mouseX?當(dāng)前鼠標(biāo)X軸坐標(biāo)
          ?*?@param?mouseY?當(dāng)前鼠標(biāo)Y軸坐標(biāo)
          ?*?@param?size?馬賽克畫筆大小
          ?*?@param?degreeOfBlur?馬賽克模糊度
          ?*?@param?context?需要進(jìn)行繪制的畫布
          ?*/

          export?function?drawMosaic(
          ??mouseX:?number,
          ??mouseY:?number,
          ??size:?number,
          ??degreeOfBlur:?number,
          ??context:?CanvasRenderingContext2D
          )?
          {
          ??//?獲取鼠標(biāo)經(jīng)過(guò)區(qū)域的圖片像素信息
          ??const?imgData?=?context.getImageData(mouseX,?mouseY,?size,?size);
          ??//?獲取圖像寬高
          ??const?w?=?imgData.width;
          ??const?h?=?imgData.height;
          ??//?等分圖像寬高
          ??const?stepW?=?w?/?degreeOfBlur;
          ??const?stepH?=?h?/?degreeOfBlur;
          ??//?循環(huán)畫布像素點(diǎn)
          ??for?(let?i?=?0;?i?????for?(let?j?=?0;?j???????//?隨機(jī)獲取一個(gè)小方格的隨機(jī)顏色
          ??????const?color?=?getAxisColor(
          ????????imgData,
          ????????j?*?degreeOfBlur?+?Math.floor(Math.random()?*?degreeOfBlur),
          ????????i?*?degreeOfBlur?+?Math.floor(Math.random()?*?degreeOfBlur)
          ??????);
          ??????//?循環(huán)小方格的像素點(diǎn)
          ??????for?(let?k?=?0;?k?????????for?(let?l?=?0;?l???????????//?設(shè)置小方格的顏色
          ??????????setAxisColor(
          ????????????imgData,
          ????????????j?*?degreeOfBlur?+?l,
          ????????????i?*?degreeOfBlur?+?k,
          ????????????color
          ??????????);
          ????????}
          ??????}
          ????}
          ??}
          ??//?渲染打上馬賽克后的圖像信息
          ??context.putImageData(imgData,?mouseX,?mouseY);
          }

          實(shí)現(xiàn)文字繪制

          canvas沒(méi)有直接提供API來(lái)供我們輸入文字,但是它提供了填充文本的API,因此我們需要一個(gè)div來(lái)讓用戶輸入文字,用戶輸入完成后將輸入的文字填充到指定區(qū)域即可。

          實(shí)現(xiàn)的效果如下:

          1258
          • 在組件中創(chuàng)建一個(gè)div,開(kāi)啟div的可編輯屬性,布局好樣式
          <template>
          ??<teleport?to="body">
          ????????
          ????<div
          ??????id="textInputPanel"
          ??????ref="textInputController"
          ??????v-show="textStatus"
          ??????contenteditable="true"
          ??????spellcheck="false"
          ????>
          div>
          ??teleport>
          template>
          • 鼠標(biāo)按下時(shí),計(jì)算文本輸入?yún)^(qū)域位置
          //?計(jì)算文本框顯示位置
          const?textMouseX?=?mouseX?-?15;
          const?textMouseY?=?mouseY?-?15;
          //?修改文本區(qū)域位置
          this.textInputController.value.style.left?=?textMouseX?+?"px";
          this.textInputController.value.style.top?=?textMouseY?+?"px";
          • 輸入框位置發(fā)生變化時(shí)代表用戶輸入完畢,將用戶輸入的內(nèi)容渲染到canvas,繪制文本的代碼如下
          /**
          ?*?繪制文本
          ?*?@param?text?需要進(jìn)行繪制的文字
          ?*?@param?mouseX?繪制位置的X軸坐標(biāo)
          ?*?@param?mouseY?繪制位置的Y軸坐標(biāo)
          ?*?@param?color?字體顏色
          ?*?@param?fontSize?字體大小
          ?*?@param context 需要
          進(jìn)行繪制的畫布
          ?*/

          export?function?drawText(
          ??text:?string,
          ??mouseX:?number,
          ??mouseY:?number,
          ??color:?string,
          ??fontSize:?number,
          ??context:?CanvasRenderingContext2D
          )?
          {
          ??
          //?開(kāi)始繪制
          ??context.save();
          ??context.lineWidth?=?
          1;
          ??
          //?設(shè)置字體顏色
          ??context.fillStyle?=?color;
          ??context.textBaseline?=?
          "middle";
          ??context.font?=?
          `bold?${fontSize}px?微軟雅黑`;
          ??context.fillText(text,?mouseX,?mouseY);
          ??
          //?結(jié)束繪制
          ??context.restore();
          }


          實(shí)現(xiàn)下載功能

          下載功能比較簡(jiǎn)單,我們只需要將裁剪框區(qū)域的內(nèi)容放進(jìn)一個(gè)新的canvas中,然后調(diào)用toDataURL方法就能拿到圖片的base64地址,我們創(chuàng)建一個(gè)a標(biāo)簽,添加download屬性,觸發(fā)a標(biāo)簽的點(diǎn)擊事件即可下載。

          實(shí)現(xiàn)代碼如下:

          export?function?saveCanvasToImage(
          ??context:?CanvasRenderingContext2D,
          ??startX:?number,
          ??startY:?number,
          ??width:?number,
          ??height:?number
          )?
          {
          ??//?獲取裁剪框區(qū)域圖片信息
          ??const?img?=?context.getImageData(startX,?startY,?width,?height);
          ??//?創(chuàng)建canvas標(biāo)簽,用于存放裁剪區(qū)域的圖片
          ??const?canvas?=?document.createElement("canvas");
          ??canvas.width?=?width;
          ??canvas.height?=?height;
          ??//?獲取裁剪框區(qū)域畫布
          ??const?imgContext?=?canvas.getContext("2d");
          ??if?(imgContext)?{
          ????//?將圖片放進(jìn)裁剪框內(nèi)
          ????imgContext.putImageData(img,?0,?0);
          ????const?a?=?document.createElement("a");
          ????//?獲取圖片
          ????a.href?=?canvas.toDataURL("png");
          ????//?下載圖片
          ????a.download?=?`${new?Date().getTime()}.png`;
          ????a.click();
          ??}
          }

          實(shí)現(xiàn)撤銷功能

          由于我們繪制圖形采用了歷史記錄模式,每次圖形繪制都會(huì)存儲(chǔ)一次畫布狀態(tài),我們只需要在點(diǎn)擊撤銷按鈕時(shí),從history彈出一最后一條記錄即可。

          實(shí)現(xiàn)代碼如下:

          /**
          ?*?取出一條歷史記錄
          ?*/

          private?takeOutHistory()?{
          ??const?lastImageData?=?this.history.pop();
          ??if?(this.screenShortCanvas?!=?null?&&?lastImageData)?{
          ????const?context?=?this.screenShortCanvas;
          ????if?(this.undoClickNum?==?0?&&?this.history.length?>?0)?{
          ??????//?首次取出需要取兩條歷史記錄
          ??????const?firstPopImageData?=?this.history.pop()?as?Record<string,?any>;
          ??????context.putImageData(firstPopImageData["data"],?0,?0);
          ????}?else?{
          ??????context.putImageData(lastImageData["data"],?0,?0);
          ????}
          ??}

          ??this.undoClickNum++;
          ??//?歷史記錄已取完,禁用撤回按鈕點(diǎn)擊
          ??if?(this.history.length?<=?0)?{
          ????this.undoClickNum?=?0;
          ????this.data.setUndoStatus(false);
          ??}
          }

          實(shí)現(xiàn)關(guān)閉功能

          關(guān)閉功能指的是重置截圖組件,因此我們需要通過(guò)emit向父組件推送銷毀的消息。

          實(shí)現(xiàn)代碼如下:

          ??/**
          ???*?重置組件
          ???*/

          ??private?resetComponent?=?()?=>?{
          ????if?(this.emit)?{
          ??????//?隱藏截圖工具欄
          ??????this.data.setToolStatus(false);
          ??????//?初始化響應(yīng)式變量
          ??????this.data.setInitStatus(true);
          ??????//?銷毀組件
          ??????this.emit("destroy-component",?false);
          ??????return;
          ????}
          ????throw?"組件重置失敗";
          ??};

          實(shí)現(xiàn)確認(rèn)功能

          當(dāng)用戶點(diǎn)擊確認(rèn)后,我們需要將裁剪框內(nèi)的內(nèi)容轉(zhuǎn)為base64,然后通過(guò)emit推送給父組件,最后重置組件。

          實(shí)現(xiàn)代碼如下:

          const?base64?=?this.getCanvasImgData(false);
          this.emit("get-image-data",?base64);

          插件地址

          至此,插件的實(shí)現(xiàn)過(guò)程就分享完畢了。

          • 插件在線體驗(yàn)地址:chat-system

          • 插件GitHub倉(cāng)庫(kù)地址:screen-shot

          • 開(kāi)源項(xiàng)目地址:chat-system-github

          寫在最后

          • 文章中g(shù)if圖較大,可能無(wú)法查看,可點(diǎn)擊下方閱讀原文查看??
          • 公眾號(hào)無(wú)法外鏈,如果文中有鏈接,可點(diǎn)擊下方閱讀原文查看??

          ●?瀏覽器是如何工作的:Chrome V8讓你更懂JavaScript

          ●?現(xiàn)代瀏覽器內(nèi)部機(jī)制(四):換個(gè)角度看事件

          ●?你不知道的 Webkit 內(nèi)核(5000字,了解瀏覽器渲染原理)



          ·END·

          圖雀社區(qū)

          匯聚精彩的免費(fèi)實(shí)戰(zhàn)教程



          關(guān)注公眾號(hào)回復(fù) z 拉學(xué)習(xí)交流群


          喜歡本文,點(diǎn)個(gè)“在看”告訴我

          瀏覽 628
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久亚洲大家都在搜 | 成人午夜天天爽 | 草操视频 | 操逼电影网 | www亚洲视频 |