1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 16578字,需瀏覽 34分鐘

         ·

        2021-02-04 03:07

        前言

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

        那么,我們就需要為我們的產(chǎn)品實(shí)現(xiàn)一個(gè)自定義截屏的功能,用戶(hù)點(diǎn)完"截圖"按鈕后,框選任意區(qū)域,隨后在框選的區(qū)域內(nèi)進(jìn)行圈選、畫(huà)箭頭、馬賽克、直線(xiàn)、打字等操作,做完操作后用戶(hù)可以選擇保存框選區(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é)果視頻:

        寫(xiě)在前面

        本文插件的寫(xiě)法采用的是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)工具欄,里面有框選、圈選、箭頭、直線(xiàn)、畫(huà)筆等工具,如下圖所示。

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

        • 隨后,我們?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畫(huà)布繪制蒙層
        • 在獲取到的內(nèi)容中進(jìn)行拖拽,繪制鏤空選區(qū)
        • 選擇截圖工具欄的工具,選擇畫(huà)筆大小等信息
        • 在選區(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畫(huà)布繪制蒙層

        我們拿到了轉(zhuǎn)換后的dom后,我們就需要繪制一個(gè)透明度為0.6的黑色蒙層,告知用戶(hù)你現(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)?{
        ??//?清除畫(huà)布
        ??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)寫(xiě)的很詳細(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ū)域畫(huà)canvas容器畫(huà)布
        ??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ū)效果。

        整理下上述話(huà)語(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ū)域畫(huà)布
        ??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ū)域畫(huà)canvas容器畫(huà)布
        ????????const?context?=?this.screenShortController.value?.getContext("2d");
        ????????if?(context?==?null)?return;

        ????????//?賦值截圖區(qū)域canvas畫(huà)布
        ????????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畫(huà)布
        ?*?@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
        )?
        {
        ??//?獲取畫(huà)布寬高
        ??const?canvasWidth?=?controller?.width;
        ??const?canvasHeight?=?controller?.height;

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

        ??//?清除畫(huà)布
        ??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
        ??};
        }

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

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

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

        在截圖工具欄的布局上,一開(kāi)始我的想法是直接在canvas畫(huà)布中把這些工具畫(huà)出來(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)寫(xiě)在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?是否為畫(huà)筆選項(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)?{
        ????//?獲取畫(huà)筆選項(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;
        }

        • 獲取畫(huà)筆選擇點(diǎn)擊時(shí)的class名
        /**
        ?*?獲取畫(huà)筆選項(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)前畫(huà)布狀態(tài)保存至history。
        ??/**
        ???*?保存當(dāng)前畫(huà)布狀態(tài)
        ???*?@private
        ???*/

        ??private?addHistoy()?{
        ????if?(
        ??????this.screenShortCanvas?!=?null?&&
        ??????this.screenShortController.value?!=?null
        ????)?{
        ??????//?獲取canvas畫(huà)布與容器
        ??????const?context?=?this.screenShortCanvas;
        ??????const?controller?=?this.screenShortController.value;
        ??????if?(this.history.length?>?this.maxUndoNum)?{
        ????????//?刪除最早的一條畫(huà)布記錄
        ????????this.history.unshift();
        ??????}
        ??????//?保存當(dāng)前畫(huà)布狀態(tài)
        ??????this.history.push({
        ????????data:?context.getImageData(0,?0,?controller.width,?controller.height)
        ??????});
        ??????//?啟用撤銷(xiāo)按鈕
        ??????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中最后一條記錄。
        ??/**
        ???*?顯示最新的畫(huà)布狀態(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畫(huà)布
        ?*?@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)行繪制的畫(huà)布
        ?*?@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)寫(xiě)的很清楚了,此處用到的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.?箭頭斜線(xiàn)P3到P2直線(xiàn)的長(zhǎng)度,P4與P3是對(duì)稱(chēng)的,因此P4到P2的長(zhǎng)度等于P3到P2的長(zhǎng)度
        ???*????3.?箭頭斜線(xiàn)P3到P1、P2直線(xiàn)的夾角角度(θ),因?yàn)槭菍?duì)稱(chēng)的,所以P4與P1、P2直線(xiàn)的夾角角度是相等的
        ???*?求:
        ???*????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)行繪制的畫(huà)布
        ?*?@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?箭頭斜線(xiàn)與直線(xiàn)的夾角角度?(θ)?P3?--->?(P1、P2)?||?P4?--->?P1(P1、P2)
        ?*?@param?headlen?箭頭斜線(xiàn)的長(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.?箭頭斜線(xiàn)(P3?||?P4)?--->?P2直線(xiàn)的長(zhǎng)度
        ???*????3.?箭頭斜線(xiàn)(P3?||?P4)?--->?(P1、P2)直線(xiàn)的夾角角度(θ)
        ???*?求:
        ???*????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的直線(xiàn)
        ??context.lineTo(mouseX,?mouseY);
        ??
        //?計(jì)算P3的位置
        ??arrowX?=?mouseX?+?topX;
        ??arrowY?=?mouseY?+?topY;
        ??
        //?移動(dòng)筆觸到P3坐標(biāo)
        ??context.moveTo(arrowX,?arrowY);
        ??
        //?繪制P2到P3的斜線(xiàn)
        ??context.lineTo(mouseX,?mouseY);
        ??
        //?計(jì)算P4的位置
        ??arrowX?=?mouseX?+?botX;
        ??arrowY?=?mouseY?+?botY;
        ??
        //?繪制P2到P4的斜線(xiàn)
        ??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)畫(huà)筆繪制

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

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

        /**
        ?*?畫(huà)筆初始化
        ?*/

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

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

        /**
        ?*?畫(huà)筆繪制
        ?*?@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)繪制成周?chē)嘟念伾?/section>

        具體的實(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)繪制成周?chē)嘟念伾?br>?*?@param?mouseX?當(dāng)前鼠標(biāo)X軸坐標(biāo)
        ?*?@param?mouseY?當(dāng)前鼠標(biāo)Y軸坐標(biāo)
        ?*?@param?size?馬賽克畫(huà)筆大小
        ?*?@param?degreeOfBlur?馬賽克模糊度
        ?*?@param?context?需要進(jìn)行繪制的畫(huà)布
        ?*/

        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)畫(huà)布像素點(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)讓用戶(hù)輸入文字,用戶(hù)輸入完成后將輸入的文字填充到指定區(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í)代表用戶(hù)輸入完畢,將用戶(hù)輸入的內(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)行繪制的畫(huà)布
        ?*/

        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ū)域畫(huà)布
        ??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)撤銷(xiāo)功能

        由于我們繪制圖形采用了歷史記錄模式,每次圖形繪制都會(huì)存儲(chǔ)一次畫(huà)布狀態(tài),我們只需要在點(diǎn)擊撤銷(xiāo)按鈕時(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向父組件推送銷(xiāo)毀的消息。

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

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

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

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

        當(dāng)用戶(hù)點(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ò)程就分享完畢了。

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

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

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

        寫(xiě)在最后

        • 文章中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)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            一级黄色中文电影影视视屏 | 好爽又高潮又大免费视频 | 大陆黄色视频 | 都市名器之乱淫后宫 | 亚洲电影一区二区三区 | 久久精品国产女主播 | 久久久久久99精品无码 | 国内精品视频在线播放 | 人人操人人超碰 | 综合免费视频 |