一、本期内容简述
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()) return
isDrawing.value = true
const 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 = true
eraseAtPoint(point)
}
}
其中eraseAtPoint
// 跟踪最后一个橡皮擦操作
let lastEraserOperation = null
let eraserTimeout = null
// 在指定点进行擦除
const eraseAtPoint = (point) => {
const size = eraserSize.value
const 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()) return
const 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) return
if (currentMode.value === 'draw' && currentPath.value.length > 0) {
// 保存完成的绘图路径
drawingHistory.value.push({
type: 'draw',
points: [...currentPath.value],
color: currentColor.value,
size: lineSize.value
})
}
isDrawing.value = false
currentPath.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;
}
}