uni-app实战教程 从0到1开发 画图软件 (橡皮擦)
一、本期内容简述
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;}
}