使用 Vue3 實(shí)現(xiàn) Web 端自定義截屏
前言
當(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)的效果如下:

具體的實(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)的效果如下:

在組件中創(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
·END·
匯聚精彩的免費(fèi)實(shí)戰(zhàn)教程
喜歡本文,點(diǎn)個(gè)“在看”告訴我


