一、本期內容簡述
1. 開發內容
上一期,我們一起學習了如何進行繪畫,本期我們將學習如何擦除我們所繪畫的內容,也就是“橡皮擦”功能。
首先,我們應該明確需求,橡皮擦可以擦除掉我們繪畫的內容。
2. 開發需求
所以開發需求:
(1)??擦除繪畫內容:
- 單指觸摸屏幕并緩慢移動即可擦除
??(2)修改橡皮擦的形狀和大小??:
- 可以選擇橡皮擦的形狀
- 可以調整橡皮擦的大小
二、核心實現代碼
1. html
添加橡皮擦的預覽效果顯示
<!-- 橡皮擦預覽 --><view class="eraser-preview":class="`shape-${eraserShape}`":style="{display: eraserPreviewVisible ? 'block' : 'none',left: `${eraserPreviewPos.x}px`,top: `${eraserPreviewPos.y}px`,width: `${eraserSize}px`,height: `${eraserSize}px`}"></view>
2. 常量定義
const currentMode = ref('draw') // 'draw' 或 'erase'
const eraserShapes = ref(['圓形', '方形'])
const eraserShapeIndex = ref(0) // 0: 圓形, 1: 方形
首先定義currentMode作為判斷當前是繪畫,還是使用橡皮擦的模式
定義橡皮擦的形狀,以及當前所選橡皮擦的索引值
3. 觸摸狀態
還是之前的核心三個方法的 內容,觸摸、觸摸中、觸摸結束
(1)handleTouchStart
你會發現,這次得如果是繪畫就是將單簽的位置添加到currentPaht中,如果是橡皮擦則記錄橡皮擦的位置,顯示橡皮擦,并
const handleTouchStart = async (e) => {if (!ensureContext()) returnisDrawing.value = trueconst point = {x: e.touches[0].x,y: e.touches[0].y}if (currentMode.value === 'draw') {// 開始新的繪圖路徑currentPath.value = [point]} else {// 橡皮擦模式eraserPreviewPos.value = { x: point.x, y: point.y }eraserPreviewVisible.value = trueeraseAtPoint(point)}
}
其中eraseAtPoint
// 跟蹤最后一個橡皮擦操作
let lastEraserOperation = null
let eraserTimeout = null// 在指定點進行擦除
const eraseAtPoint = (point) => {const size = eraserSize.valueconst halfSize = size / 2// 直接在畫布上繪制背景色來覆蓋原有內容ctx.value.setFillStyle('#ffffff') // 使用畫布背景色ctx.value.beginPath()if (eraserShape.value === 'circle') {// 圓形橡皮擦ctx.value.arc(point.x, point.y, halfSize, 0, 2 * Math.PI)} else {// 方形橡皮擦ctx.value.rect(point.x - halfSize,point.y - halfSize,size,size)}ctx.value.fill()ctx.value.draw(true)// 優化:批量處理橡皮擦操作const currentTime = Date.now()// 如果有最近的橡皮擦操作,且時間間隔短、參數相同,則合并if (lastEraserOperation && currentTime - lastEraserOperation.time < 100 && lastEraserOperation.size === size && lastEraserOperation.shape === eraserShape.value) {// 添加當前點到最后一個橡皮擦操作lastEraserOperation.points.push({ x: point.x, y: point.y })} else {// 創建新的橡皮擦操作lastEraserOperation = {type: 'eraser',points: [{ x: point.x, y: point.y }],size: size,shape: eraserShape.value,time: currentTime}drawingHistory.value.push(lastEraserOperation)}// 清除之前的定時器if (eraserTimeout) {clearTimeout(eraserTimeout)}// 設置定時器,在一段時間不操作后重置最后一個橡皮擦操作eraserTimeout = setTimeout(() => {lastEraserOperation = null}, 200)
}
- 執行擦除:在畫布上指定的 point 點,用橡皮擦的形狀和大小,覆蓋上背景色(白色),從而實現視覺上的擦除效果。
- 記錄歷史:將這次擦除操作作為一個對象,高效地添加到 drawingHistory 數組中。這里的“高效”體現在它會合并短時間內連續發生的、參數相同的擦除操作,以避免歷史記錄數組變得過于龐大,影響后續的重繪和撤銷操作。
- eraserTimeout 是一個計時器,它的核心作用是界定一次連續的、完整的橡皮擦操作。它通過一個“延遲重置”的機制,告訴程序:“如果用戶在短時間內(比如200毫秒)沒有再擦了,我們就認為他這次擦的動作已經結束了,下一次擦就是一次全新的動作了。”
(2)handleTouchMove
const handleTouchMove = async (e) => {if (!isDrawing.value || !ensureContext()) returnconst point = {x: e.touches[0].x,y: e.touches[0].y}if (currentMode.value === 'draw') {// 繪圖模式 - 添加點到當前路徑currentPath.value.push(point)// 優化:只繪制當前路徑的最后一段,而不是重繪整個畫布if (currentPath.value.length > 1) {const lastPoint = currentPath.value[currentPath.value.length - 2]const currentPoint = currentPath.value[currentPath.value.length - 1]ctx.value.setStrokeStyle(currentColor.value)ctx.value.setLineWidth(lineSize.value)ctx.value.setLineCap('round')ctx.value.setLineJoin('round')ctx.value.beginPath()ctx.value.moveTo(lastPoint.x, lastPoint.y)ctx.value.lineTo(currentPoint.x, currentPoint.y)ctx.value.stroke()ctx.value.draw(true)}} else {// 橡皮擦模式eraserPreviewPos.value = { x: point.x, y: point.y }eraseAtPoint(point)}
}
(3)handleTouchEnd
觸摸結束
const handleTouchEnd = () => {if (!isDrawing.value) returnif (currentMode.value === 'draw' && currentPath.value.length > 0) {// 保存完成的繪圖路徑drawingHistory.value.push({type: 'draw',points: [...currentPath.value],color: currentColor.value,size: lineSize.value})}isDrawing.value = falsecurrentPath.value = []eraserPreviewVisible.value = false
}
drawingHistory 是一個“記憶庫”或“操作日志”。它記錄了用戶在畫布上執行的每一個繪圖和擦除動作。這使得應用能夠實現重繪、撤銷/重做(如果需要添加的話)以及最終保存等高級功能。
4. css
/* 橡皮擦預覽樣式 */
.eraser-preview {position: absolute;pointer-events: none;z-index: 9999;background-color: rgba(200, 200, 200, 0.3);border: 1px dashed #666;transform: translate(-50%, -50%);&.shape-circle {border-radius: 50%;}&.shape-square {border-radius: 0;}
}